Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

1package secrets 2 3import ( 4 "context" 5 "fmt" 6 "log/slog" 7 "path" 8 "strings" 9 "time" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 vault "github.com/openbao/openbao/api/v2" 13) 14 15type OpenBaoManager struct { 16 client *vault.Client 17 mountPath string 18 logger *slog.Logger 19 connectionTimeout time.Duration 20} 21 22type OpenBaoManagerOpt func(*OpenBaoManager) 23 24func WithMountPath(mountPath string) OpenBaoManagerOpt { 25 return func(v *OpenBaoManager) { 26 v.mountPath = mountPath 27 } 28} 29 30func WithConnectionTimeout(timeout time.Duration) OpenBaoManagerOpt { 31 return func(v *OpenBaoManager) { 32 v.connectionTimeout = timeout 33 } 34} 35 36// NewOpenBaoManager creates a new OpenBao manager that connects to a Bao Proxy 37// The proxyAddress should point to the local Bao Proxy (e.g., "http://127.0.0.1:8200") 38// The proxy handles all authentication automatically via Auto-Auth 39func NewOpenBaoManager(proxyAddress string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) { 40 if proxyAddress == "" { 41 return nil, fmt.Errorf("proxy address cannot be empty") 42 } 43 44 config := vault.DefaultConfig() 45 config.Address = proxyAddress 46 47 client, err := vault.NewClient(config) 48 if err != nil { 49 return nil, fmt.Errorf("failed to create openbao client: %w", err) 50 } 51 52 manager := &OpenBaoManager{ 53 client: client, 54 mountPath: "spindle", // default KV v2 mount path 55 logger: logger, 56 connectionTimeout: 10 * time.Second, // default connection timeout 57 } 58 59 for _, opt := range opts { 60 opt(manager) 61 } 62 63 if err := manager.testConnection(); err != nil { 64 return nil, fmt.Errorf("failed to connect to bao proxy: %w", err) 65 } 66 67 logger.Info("successfully connected to bao proxy", "address", proxyAddress) 68 return manager, nil 69} 70 71// testConnection verifies that we can connect to the proxy 72func (v *OpenBaoManager) testConnection() error { 73 ctx, cancel := context.WithTimeout(context.Background(), v.connectionTimeout) 74 defer cancel() 75 76 // try token self-lookup as a quick way to verify proxy works 77 // and is authenticated 78 _, err := v.client.Auth().Token().LookupSelfWithContext(ctx) 79 if err != nil { 80 return fmt.Errorf("proxy connection test failed: %w", err) 81 } 82 83 return nil 84} 85 86func (v *OpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error { 87 if err := ValidateKey(secret.Key); err != nil { 88 return err 89 } 90 91 secretPath := v.buildSecretPath(secret.Repo, secret.Key) 92 v.logger.Debug("adding secret", "repo", secret.Repo, "key", secret.Key, "path", secretPath) 93 94 // Check if secret already exists 95 existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 96 if err == nil && existing != nil { 97 v.logger.Debug("secret already exists", "path", secretPath) 98 return ErrKeyAlreadyPresent 99 } 100 101 createdAt := secret.CreatedAt 102 if createdAt.IsZero() { 103 createdAt = time.Now() 104 } 105 secretData := map[string]interface{}{ 106 "value": secret.Value, 107 "repo": string(secret.Repo), 108 "key": secret.Key, 109 "created_at": createdAt.UTC().Format(time.RFC3339), 110 "created_by": secret.CreatedBy.String(), 111 } 112 113 v.logger.Debug("writing secret to openbao", "path", secretPath, "mount", v.mountPath) 114 resp, err := v.client.KVv2(v.mountPath).Put(ctx, secretPath, secretData) 115 if err != nil { 116 v.logger.Error("failed to write secret", "path", secretPath, "error", err) 117 return fmt.Errorf("failed to store secret in openbao: %w", err) 118 } 119 120 v.logger.Debug("secret write response", "version", resp.VersionMetadata.Version, "created_time", resp.VersionMetadata.CreatedTime) 121 122 v.logger.Debug("verifying secret was written", "path", secretPath) 123 readBack, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 124 if err != nil { 125 v.logger.Error("failed to verify secret after write", "path", secretPath, "error", err) 126 return fmt.Errorf("secret not found after writing to %s/%s: %w", v.mountPath, secretPath, err) 127 } 128 129 if readBack == nil || readBack.Data == nil { 130 v.logger.Error("secret verification returned empty data", "path", secretPath) 131 return fmt.Errorf("secret verification failed: empty data returned for %s/%s", v.mountPath, secretPath) 132 } 133 134 v.logger.Info("secret added and verified successfully", "repo", secret.Repo, "key", secret.Key, "version", readBack.VersionMetadata.Version) 135 return nil 136} 137 138func (v *OpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error { 139 secretPath := v.buildSecretPath(secret.Repo, secret.Key) 140 141 // check if secret exists 142 existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 143 if err != nil || existing == nil { 144 return ErrKeyNotFound 145 } 146 147 err = v.client.KVv2(v.mountPath).DeleteMetadata(ctx, secretPath) 148 if err != nil { 149 return fmt.Errorf("failed to delete secret from openbao: %w", err) 150 } 151 152 v.logger.Debug("secret removed successfully", "repo", secret.Repo, "key", secret.Key) 153 return nil 154} 155 156func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) { 157 repoPath := v.buildRepoPath(repo) 158 159 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) 160 if err != nil { 161 if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") { 162 return []LockedSecret{}, nil 163 } 164 return nil, fmt.Errorf("failed to list secrets: %w", err) 165 } 166 167 if secretsList == nil || secretsList.Data == nil { 168 return []LockedSecret{}, nil 169 } 170 171 keys, ok := secretsList.Data["keys"].([]interface{}) 172 if !ok { 173 return []LockedSecret{}, nil 174 } 175 176 var secrets []LockedSecret 177 178 for _, keyInterface := range keys { 179 key, ok := keyInterface.(string) 180 if !ok { 181 continue 182 } 183 184 secretPath := fmt.Sprintf("%s/%s", repoPath, key) 185 secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 186 if err != nil { 187 v.logger.Warn("failed to read secret metadata", "path", secretPath, "error", err) 188 continue 189 } 190 191 if secretData == nil || secretData.Data == nil { 192 continue 193 } 194 195 data := secretData.Data 196 197 createdAtStr, ok := data["created_at"].(string) 198 if !ok { 199 createdAtStr = time.Now().Format(time.RFC3339) 200 } 201 202 createdAt, err := time.Parse(time.RFC3339, createdAtStr) 203 if err != nil { 204 createdAt = time.Now() 205 } 206 207 createdByStr, ok := data["created_by"].(string) 208 if !ok { 209 createdByStr = "" 210 } 211 212 keyStr, ok := data["key"].(string) 213 if !ok { 214 keyStr = key 215 } 216 217 secret := LockedSecret{ 218 Key: keyStr, 219 Repo: repo, 220 CreatedAt: createdAt, 221 CreatedBy: syntax.DID(createdByStr), 222 } 223 224 secrets = append(secrets, secret) 225 } 226 227 v.logger.Debug("retrieved locked secrets", "repo", repo, "count", len(secrets)) 228 return secrets, nil 229} 230 231func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) { 232 repoPath := v.buildRepoPath(repo) 233 234 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) 235 if err != nil { 236 if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") { 237 return []UnlockedSecret{}, nil 238 } 239 return nil, fmt.Errorf("failed to list secrets: %w", err) 240 } 241 242 if secretsList == nil || secretsList.Data == nil { 243 return []UnlockedSecret{}, nil 244 } 245 246 keys, ok := secretsList.Data["keys"].([]interface{}) 247 if !ok { 248 return []UnlockedSecret{}, nil 249 } 250 251 var secrets []UnlockedSecret 252 253 for _, keyInterface := range keys { 254 key, ok := keyInterface.(string) 255 if !ok { 256 continue 257 } 258 259 secretPath := fmt.Sprintf("%s/%s", repoPath, key) 260 secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) 261 if err != nil { 262 v.logger.Warn("failed to read secret", "path", secretPath, "error", err) 263 continue 264 } 265 266 if secretData == nil || secretData.Data == nil { 267 continue 268 } 269 270 data := secretData.Data 271 272 valueStr, ok := data["value"].(string) 273 if !ok { 274 v.logger.Warn("secret missing value", "path", secretPath) 275 continue 276 } 277 278 createdAtStr, ok := data["created_at"].(string) 279 if !ok { 280 createdAtStr = time.Now().Format(time.RFC3339) 281 } 282 283 createdAt, err := time.Parse(time.RFC3339, createdAtStr) 284 if err != nil { 285 createdAt = time.Now() 286 } 287 288 createdByStr, ok := data["created_by"].(string) 289 if !ok { 290 createdByStr = "" 291 } 292 293 keyStr, ok := data["key"].(string) 294 if !ok { 295 keyStr = key 296 } 297 298 secret := UnlockedSecret{ 299 Key: keyStr, 300 Value: valueStr, 301 Repo: repo, 302 CreatedAt: createdAt, 303 CreatedBy: syntax.DID(createdByStr), 304 } 305 306 secrets = append(secrets, secret) 307 } 308 309 v.logger.Debug("retrieved unlocked secrets", "repo", repo, "count", len(secrets)) 310 return secrets, nil 311} 312 313// buildRepoPath creates a safe path for a repository 314func (v *OpenBaoManager) buildRepoPath(repo RepoIdentifier) string { 315 // convert RepoIdentifier to a safe path by replacing special characters 316 repoPath := strings.ReplaceAll(string(repo), "/", "_") 317 repoPath = strings.ReplaceAll(repoPath, ":", "_") 318 repoPath = strings.ReplaceAll(repoPath, ".", "_") 319 return fmt.Sprintf("repos/%s", repoPath) 320} 321 322// buildSecretPath creates a path for a specific secret 323func (v *OpenBaoManager) buildSecretPath(repo RepoIdentifier, key string) string { 324 return path.Join(v.buildRepoPath(repo), key) 325}