···
"github.com/bluesky-social/indigo/atproto/syntax"
···
type OpenBaoManager struct {
21
-
stopCh chan struct{}
22
-
tokenMu sync.RWMutex
···
34
-
func NewOpenBaoManager(address, roleID, secretID string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) {
36
-
return nil, fmt.Errorf("address cannot be empty")
39
-
return nil, fmt.Errorf("role_id cannot be empty")
42
-
return nil, fmt.Errorf("secret_id cannot be empty")
29
+
// NewOpenBaoManager creates a new OpenBao manager that connects to a Bao Proxy
30
+
// The proxyAddress should point to the local Bao Proxy (e.g., "http://127.0.0.1:8200")
31
+
// The proxy handles all authentication automatically via Auto-Auth
32
+
func NewOpenBaoManager(proxyAddress string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) {
33
+
if proxyAddress == "" {
34
+
return nil, fmt.Errorf("proxy address cannot be empty")
config := vault.DefaultConfig()
46
-
config.Address = address
38
+
config.Address = proxyAddress
client, err := vault.NewClient(config)
return nil, fmt.Errorf("failed to create openbao client: %w", err)
53
-
// Authenticate using AppRole
54
-
err = authenticateAppRole(client, roleID, secretID)
56
-
return nil, fmt.Errorf("failed to authenticate with AppRole: %w", err)
manager := &OpenBaoManager{
mountPath: "spindle", // default KV v2 mount path
64
-
stopCh: make(chan struct{}),
···
72
-
go manager.tokenRenewalLoop()
77
-
// authenticateAppRole authenticates the client using AppRole method
78
-
func authenticateAppRole(client *vault.Client, roleID, secretID string) error {
79
-
authData := map[string]interface{}{
81
-
"secret_id": secretID,
84
-
resp, err := client.Logical().Write("auth/approle/login", authData)
86
-
return fmt.Errorf("failed to login with AppRole: %w", err)
89
-
if resp == nil || resp.Auth == nil {
90
-
return fmt.Errorf("no auth info returned from AppRole login")
93
-
client.SetToken(resp.Auth.ClientToken)
97
-
// stop stops the token renewal goroutine
98
-
func (v *OpenBaoManager) Stop() {
102
-
// tokenRenewalLoop runs in a background goroutine to automatically renew or re-authenticate tokens
103
-
func (v *OpenBaoManager) tokenRenewalLoop() {
104
-
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
105
-
defer ticker.Stop()
112
-
ctx := context.Background()
113
-
if err := v.ensureValidToken(ctx); err != nil {
114
-
v.logger.Error("openbao token renewal failed", "error", err)
120
-
// ensureValidToken checks if the current token is valid and renews or re-authenticates if needed
121
-
func (v *OpenBaoManager) ensureValidToken(ctx context.Context) error {
123
-
defer v.tokenMu.Unlock()
125
-
// check current token info
126
-
tokenInfo, err := v.client.Auth().Token().LookupSelf()
128
-
// token is invalid, need to re-authenticate
129
-
v.logger.Warn("token lookup failed, re-authenticating", "error", err)
130
-
return v.reAuthenticate()
55
+
if err := manager.testConnection(); err != nil {
56
+
return nil, fmt.Errorf("failed to connect to bao proxy: %w", err)
133
-
if tokenInfo == nil || tokenInfo.Data == nil {
134
-
return v.reAuthenticate()
138
-
ttlRaw, ok := tokenInfo.Data["ttl"]
140
-
return v.reAuthenticate()
144
-
switch t := ttlRaw.(type) {
152
-
return v.reAuthenticate()
155
-
// if TTL is less than 5 minutes, try to renew
157
-
v.logger.Info("token ttl low, attempting renewal", "ttl_seconds", ttl)
159
-
renewResp, err := v.client.Auth().Token().RenewSelf(3600) // 1h
161
-
v.logger.Warn("token renewal failed, re-authenticating", "error", err)
162
-
return v.reAuthenticate()
165
-
if renewResp == nil || renewResp.Auth == nil {
166
-
v.logger.Warn("token renewal returned no auth info, re-authenticating")
167
-
return v.reAuthenticate()
170
-
v.logger.Info("token renewed successfully", "new_ttl_seconds", renewResp.Auth.LeaseDuration)
59
+
logger.Info("successfully connected to bao proxy", "address", proxyAddress)
176
-
// reAuthenticate performs a fresh authentication using AppRole
177
-
func (v *OpenBaoManager) reAuthenticate() error {
178
-
v.logger.Info("re-authenticating with approle")
63
+
// testConnection verifies that we can connect to the proxy
64
+
func (v *OpenBaoManager) testConnection() error {
65
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
180
-
err := authenticateAppRole(v.client, v.roleID, v.secretID)
68
+
// try token self-lookup as a quick way to verify proxy works
69
+
// and is authenticated
70
+
_, err := v.client.Auth().Token().LookupSelfWithContext(ctx)
182
-
return fmt.Errorf("re-authentication failed: %w", err)
72
+
return fmt.Errorf("proxy connection test failed: %w", err)
185
-
v.logger.Info("re-authentication successful")
func (v *OpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error {
191
-
defer v.tokenMu.RUnlock()
if err := ValidateKey(secret.Key); err != nil {
secretPath := v.buildSecretPath(secret.Repo, secret.Key)
84
+
v.logger.Debug("adding secret", "repo", secret.Repo, "key", secret.Key, "path", secretPath)
198
-
fmt.Println(v.mountPath, secretPath)
86
+
// Check if secret already exists
existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
if err == nil && existing != nil {
89
+
v.logger.Debug("secret already exists", "path", secretPath)
return ErrKeyAlreadyPresent
···
"created_by": secret.CreatedBy.String(),
213
-
_, err = v.client.KVv2(v.mountPath).Put(ctx, secretPath, secretData)
101
+
v.logger.Debug("writing secret to openbao", "path", secretPath, "mount", v.mountPath)
102
+
resp, err := v.client.KVv2(v.mountPath).Put(ctx, secretPath, secretData)
104
+
v.logger.Error("failed to write secret", "path", secretPath, "error", err)
return fmt.Errorf("failed to store secret in openbao: %w", err)
108
+
v.logger.Debug("secret write response", "version", resp.VersionMetadata.Version, "created_time", resp.VersionMetadata.CreatedTime)
110
+
v.logger.Debug("verifying secret was written", "path", secretPath)
111
+
readBack, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
113
+
v.logger.Error("failed to verify secret after write", "path", secretPath, "error", err)
114
+
return fmt.Errorf("secret not found after writing to %s/%s: %w", v.mountPath, secretPath, err)
117
+
if readBack == nil || readBack.Data == nil {
118
+
v.logger.Error("secret verification returned empty data", "path", secretPath)
119
+
return fmt.Errorf("secret verification failed: empty data returned for %s/%s", v.mountPath, secretPath)
122
+
v.logger.Info("secret added and verified successfully", "repo", secret.Repo, "key", secret.Key, "version", readBack.VersionMetadata.Version)
func (v *OpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error {
223
-
defer v.tokenMu.RUnlock()
secretPath := v.buildSecretPath(secret.Repo, secret.Key)
129
+
// check if secret exists
existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
if err != nil || existing == nil {
···
return fmt.Errorf("failed to delete secret from openbao: %w", err)
140
+
v.logger.Debug("secret removed successfully", "repo", secret.Repo, "key", secret.Key)
func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) {
241
-
defer v.tokenMu.RUnlock()
repoPath := v.buildRepoPath(repo)
244
-
secretsList, err := v.client.Logical().List(fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
147
+
secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
return []LockedSecret{}, nil
···
269
-
secretPath := path.Join(repoPath, key)
172
+
secretPath := fmt.Sprintf("%s/%s", repoPath, key)
secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
272
-
continue // Skip secrets we can't read
175
+
v.logger.Warn("failed to read secret metadata", "path", secretPath, "error", err)
if secretData == nil || secretData.Data == nil {
···
secrets = append(secrets, secret)
215
+
v.logger.Debug("retrieved locked secrets", "repo", repo, "count", len(secrets))
func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) {
316
-
defer v.tokenMu.RUnlock()
repoPath := v.buildRepoPath(repo)
319
-
secretsList, err := v.client.Logical().List(fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
222
+
secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
return []UnlockedSecret{}, nil
···
344
-
secretPath := path.Join(repoPath, key)
247
+
secretPath := fmt.Sprintf("%s/%s", repoPath, key)
secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
250
+
v.logger.Warn("failed to read secret", "path", secretPath, "error", err)
···
valueStr, ok := data["value"].(string)
358
-
continue // skip secrets without values
262
+
v.logger.Warn("secret missing value", "path", secretPath)
createdAtStr, ok := data["created_at"].(string)
···
secrets = append(secrets, secret)
297
+
v.logger.Debug("retrieved unlocked secrets", "repo", repo, "count", len(secrets))
395
-
// buildRepoPath creates an OpenBao path for a repository
301
+
// buildRepoPath creates a safe path for a repository
func (v *OpenBaoManager) buildRepoPath(repo DidSlashRepo) string {
// convert DidSlashRepo to a safe path by replacing special characters
repoPath := strings.ReplaceAll(string(repo), "/", "_")
···
return fmt.Sprintf("repos/%s", repoPath)
404
-
// buildSecretPath creates an OpenBao path for a specific secret
310
+
// buildSecretPath creates a path for a specific secret
func (v *OpenBaoManager) buildSecretPath(repo DidSlashRepo, key string) string {
return path.Join(v.buildRepoPath(repo), key)