Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326package secrets
import ( "context" "fmt" "log/slog" "path" "strings" "time"
"github.com/bluesky-social/indigo/atproto/syntax" vault "github.com/openbao/openbao/api/v2")
type OpenBaoManager struct { client *vault.Client mountPath string logger *slog.Logger connectionTimeout time.Duration}
type OpenBaoManagerOpt func(*OpenBaoManager)
func WithMountPath(mountPath string) OpenBaoManagerOpt { return func(v *OpenBaoManager) { v.mountPath = mountPath }}
func WithConnectionTimeout(timeout time.Duration) OpenBaoManagerOpt { return func(v *OpenBaoManager) { v.connectionTimeout = timeout }}
// NewOpenBaoManager creates a new OpenBao manager that connects to a Bao Proxy// The proxyAddress should point to the local Bao Proxy (e.g., "http://127.0.0.1:8200")// The proxy handles all authentication automatically via Auto-Authfunc NewOpenBaoManager(proxyAddress string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) { if proxyAddress == "" { return nil, fmt.Errorf("proxy address cannot be empty") }
config := vault.DefaultConfig() config.Address = proxyAddress
client, err := vault.NewClient(config) if err != nil { return nil, fmt.Errorf("failed to create openbao client: %w", err) }
manager := &OpenBaoManager{ client: client, mountPath: "spindle", // default KV v2 mount path logger: logger, connectionTimeout: 10 * time.Second, // default connection timeout }
for _, opt := range opts { opt(manager) }
if err := manager.testConnection(); err != nil { return nil, fmt.Errorf("failed to connect to bao proxy: %w", err) }
logger.Info("successfully connected to bao proxy", "address", proxyAddress) return manager, nil}
// testConnection verifies that we can connect to the proxyfunc (v *OpenBaoManager) testConnection() error { ctx, cancel := context.WithTimeout(context.Background(), v.connectionTimeout) defer cancel()
// try token self-lookup as a quick way to verify proxy works // and is authenticated _, err := v.client.Auth().Token().LookupSelfWithContext(ctx) if err != nil { return fmt.Errorf("proxy connection test failed: %w", err) }
return nil}
func (v *OpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error { if err := ValidateKey(secret.Key); err != nil { return err }
secretPath := v.buildSecretPath(secret.Repo, secret.Key) v.logger.Debug("adding secret", "repo", secret.Repo, "key", secret.Key, "path", secretPath)
// Check if secret already exists existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) if err == nil && existing != nil { v.logger.Debug("secret already exists", "path", secretPath) return ErrKeyAlreadyPresent }
createdAt := secret.CreatedAt if createdAt.IsZero() { createdAt = time.Now() } secretData := map[string]interface{}{ "value": secret.Value, "repo": string(secret.Repo), "key": secret.Key, "created_at": createdAt.UTC().Format(time.RFC3339), "created_by": secret.CreatedBy.String(), }
v.logger.Debug("writing secret to openbao", "path", secretPath, "mount", v.mountPath) resp, err := v.client.KVv2(v.mountPath).Put(ctx, secretPath, secretData) if err != nil { v.logger.Error("failed to write secret", "path", secretPath, "error", err) return fmt.Errorf("failed to store secret in openbao: %w", err) }
v.logger.Debug("secret write response", "version", resp.VersionMetadata.Version, "created_time", resp.VersionMetadata.CreatedTime)
v.logger.Debug("verifying secret was written", "path", secretPath) readBack, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) if err != nil { v.logger.Error("failed to verify secret after write", "path", secretPath, "error", err) return fmt.Errorf("secret not found after writing to %s/%s: %w", v.mountPath, secretPath, err) }
if readBack == nil || readBack.Data == nil { v.logger.Error("secret verification returned empty data", "path", secretPath) return fmt.Errorf("secret verification failed: empty data returned for %s/%s", v.mountPath, secretPath) }
v.logger.Info("secret added and verified successfully", "repo", secret.Repo, "key", secret.Key, "version", readBack.VersionMetadata.Version) return nil}
func (v *OpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error { secretPath := v.buildSecretPath(secret.Repo, secret.Key)
// check if secret exists existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) if err != nil || existing == nil { return ErrKeyNotFound }
err = v.client.KVv2(v.mountPath).DeleteMetadata(ctx, secretPath) if err != nil { return fmt.Errorf("failed to delete secret from openbao: %w", err) }
v.logger.Debug("secret removed successfully", "repo", secret.Repo, "key", secret.Key) return nil}
func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) { repoPath := v.buildRepoPath(repo)
secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) if err != nil { if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") { return []LockedSecret{}, nil } return nil, fmt.Errorf("failed to list secrets: %w", err) }
if secretsList == nil || secretsList.Data == nil { return []LockedSecret{}, nil }
keys, ok := secretsList.Data["keys"].([]interface{}) if !ok { return []LockedSecret{}, nil }
var secrets []LockedSecret
for _, keyInterface := range keys { key, ok := keyInterface.(string) if !ok { continue }
secretPath := fmt.Sprintf("%s/%s", repoPath, key) secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) if err != nil { v.logger.Warn("failed to read secret metadata", "path", secretPath, "error", err) continue }
if secretData == nil || secretData.Data == nil { continue }
data := secretData.Data
createdAtStr, ok := data["created_at"].(string) if !ok { createdAtStr = time.Now().Format(time.RFC3339) }
createdAt, err := time.Parse(time.RFC3339, createdAtStr) if err != nil { createdAt = time.Now() }
createdByStr, ok := data["created_by"].(string) if !ok { createdByStr = "" }
keyStr, ok := data["key"].(string) if !ok { keyStr = key }
secret := LockedSecret{ Key: keyStr, Repo: repo, CreatedAt: createdAt, CreatedBy: syntax.DID(createdByStr), }
secrets = append(secrets, secret) }
v.logger.Debug("retrieved locked secrets", "repo", repo, "count", len(secrets)) return secrets, nil}
func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) { repoPath := v.buildRepoPath(repo)
secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) if err != nil { if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") { return []UnlockedSecret{}, nil } return nil, fmt.Errorf("failed to list secrets: %w", err) }
if secretsList == nil || secretsList.Data == nil { return []UnlockedSecret{}, nil }
keys, ok := secretsList.Data["keys"].([]interface{}) if !ok { return []UnlockedSecret{}, nil }
var secrets []UnlockedSecret
for _, keyInterface := range keys { key, ok := keyInterface.(string) if !ok { continue }
secretPath := fmt.Sprintf("%s/%s", repoPath, key) secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath) if err != nil { v.logger.Warn("failed to read secret", "path", secretPath, "error", err) continue }
if secretData == nil || secretData.Data == nil { continue }
data := secretData.Data
valueStr, ok := data["value"].(string) if !ok { v.logger.Warn("secret missing value", "path", secretPath) continue }
createdAtStr, ok := data["created_at"].(string) if !ok { createdAtStr = time.Now().Format(time.RFC3339) }
createdAt, err := time.Parse(time.RFC3339, createdAtStr) if err != nil { createdAt = time.Now() }
createdByStr, ok := data["created_by"].(string) if !ok { createdByStr = "" }
keyStr, ok := data["key"].(string) if !ok { keyStr = key }
secret := UnlockedSecret{ Key: keyStr, Value: valueStr, Repo: repo, CreatedAt: createdAt, CreatedBy: syntax.DID(createdByStr), }
secrets = append(secrets, secret) }
v.logger.Debug("retrieved unlocked secrets", "repo", repo, "count", len(secrets)) return secrets, nil}
// buildRepoPath creates a safe path for a repositoryfunc (v *OpenBaoManager) buildRepoPath(repo RepoIdentifier) string { // convert RepoIdentifier to a safe path by replacing special characters repoPath := strings.ReplaceAll(string(repo), "/", "_") repoPath = strings.ReplaceAll(repoPath, ":", "_") repoPath = strings.ReplaceAll(repoPath, ".", "_") return fmt.Sprintf("repos/%s", repoPath)}
// buildSecretPath creates a path for a specific secretfunc (v *OpenBaoManager) buildSecretPath(repo RepoIdentifier, key string) string { return path.Join(v.buildRepoPath(repo), key)}