forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
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}
20
21type OpenBaoManagerOpt func(*OpenBaoManager)
22
23func WithMountPath(mountPath string) OpenBaoManagerOpt {
24 return func(v *OpenBaoManager) {
25 v.mountPath = mountPath
26 }
27}
28
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
32func NewOpenBaoManager(proxyAddress string, logger *slog.Logger, opts ...OpenBaoManagerOpt) (*OpenBaoManager, error) {
33 if proxyAddress == "" {
34 return nil, fmt.Errorf("proxy address cannot be empty")
35 }
36
37 config := vault.DefaultConfig()
38 config.Address = proxyAddress
39
40 client, err := vault.NewClient(config)
41 if err != nil {
42 return nil, fmt.Errorf("failed to create openbao client: %w", err)
43 }
44
45 manager := &OpenBaoManager{
46 client: client,
47 mountPath: "spindle", // default KV v2 mount path
48 logger: logger,
49 }
50
51 for _, opt := range opts {
52 opt(manager)
53 }
54
55 if err := manager.testConnection(); err != nil {
56 return nil, fmt.Errorf("failed to connect to bao proxy: %w", err)
57 }
58
59 logger.Info("successfully connected to bao proxy", "address", proxyAddress)
60 return manager, nil
61}
62
63// testConnection verifies that we can connect to the proxy
64func (v *OpenBaoManager) testConnection() error {
65 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
66 defer cancel()
67
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)
71 if err != nil {
72 return fmt.Errorf("proxy connection test failed: %w", err)
73 }
74
75 return nil
76}
77
78func (v *OpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error {
79 if err := ValidateKey(secret.Key); err != nil {
80 return err
81 }
82
83 secretPath := v.buildSecretPath(secret.Repo, secret.Key)
84 v.logger.Debug("adding secret", "repo", secret.Repo, "key", secret.Key, "path", secretPath)
85
86 // Check if secret already exists
87 existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
88 if err == nil && existing != nil {
89 v.logger.Debug("secret already exists", "path", secretPath)
90 return ErrKeyAlreadyPresent
91 }
92
93 secretData := map[string]interface{}{
94 "value": secret.Value,
95 "repo": string(secret.Repo),
96 "key": secret.Key,
97 "created_at": secret.CreatedAt.Format(time.RFC3339),
98 "created_by": secret.CreatedBy.String(),
99 }
100
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)
103 if err != nil {
104 v.logger.Error("failed to write secret", "path", secretPath, "error", err)
105 return fmt.Errorf("failed to store secret in openbao: %w", err)
106 }
107
108 v.logger.Debug("secret write response", "version", resp.VersionMetadata.Version, "created_time", resp.VersionMetadata.CreatedTime)
109
110 v.logger.Debug("verifying secret was written", "path", secretPath)
111 readBack, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
112 if err != nil {
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)
115 }
116
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)
120 }
121
122 v.logger.Info("secret added and verified successfully", "repo", secret.Repo, "key", secret.Key, "version", readBack.VersionMetadata.Version)
123 return nil
124}
125
126func (v *OpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error {
127 secretPath := v.buildSecretPath(secret.Repo, secret.Key)
128
129 // check if secret exists
130 existing, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
131 if err != nil || existing == nil {
132 return ErrKeyNotFound
133 }
134
135 err = v.client.KVv2(v.mountPath).DeleteMetadata(ctx, secretPath)
136 if err != nil {
137 return fmt.Errorf("failed to delete secret from openbao: %w", err)
138 }
139
140 v.logger.Debug("secret removed successfully", "repo", secret.Repo, "key", secret.Key)
141 return nil
142}
143
144func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) {
145 repoPath := v.buildRepoPath(repo)
146
147 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
148 if err != nil {
149 if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
150 return []LockedSecret{}, nil
151 }
152 return nil, fmt.Errorf("failed to list secrets: %w", err)
153 }
154
155 if secretsList == nil || secretsList.Data == nil {
156 return []LockedSecret{}, nil
157 }
158
159 keys, ok := secretsList.Data["keys"].([]interface{})
160 if !ok {
161 return []LockedSecret{}, nil
162 }
163
164 var secrets []LockedSecret
165
166 for _, keyInterface := range keys {
167 key, ok := keyInterface.(string)
168 if !ok {
169 continue
170 }
171
172 secretPath := fmt.Sprintf("%s/%s", repoPath, key)
173 secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
174 if err != nil {
175 v.logger.Warn("failed to read secret metadata", "path", secretPath, "error", err)
176 continue
177 }
178
179 if secretData == nil || secretData.Data == nil {
180 continue
181 }
182
183 data := secretData.Data
184
185 createdAtStr, ok := data["created_at"].(string)
186 if !ok {
187 createdAtStr = time.Now().Format(time.RFC3339)
188 }
189
190 createdAt, err := time.Parse(time.RFC3339, createdAtStr)
191 if err != nil {
192 createdAt = time.Now()
193 }
194
195 createdByStr, ok := data["created_by"].(string)
196 if !ok {
197 createdByStr = ""
198 }
199
200 keyStr, ok := data["key"].(string)
201 if !ok {
202 keyStr = key
203 }
204
205 secret := LockedSecret{
206 Key: keyStr,
207 Repo: repo,
208 CreatedAt: createdAt,
209 CreatedBy: syntax.DID(createdByStr),
210 }
211
212 secrets = append(secrets, secret)
213 }
214
215 v.logger.Debug("retrieved locked secrets", "repo", repo, "count", len(secrets))
216 return secrets, nil
217}
218
219func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) {
220 repoPath := v.buildRepoPath(repo)
221
222 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath))
223 if err != nil {
224 if strings.Contains(err.Error(), "no secret found") || strings.Contains(err.Error(), "no handler for route") {
225 return []UnlockedSecret{}, nil
226 }
227 return nil, fmt.Errorf("failed to list secrets: %w", err)
228 }
229
230 if secretsList == nil || secretsList.Data == nil {
231 return []UnlockedSecret{}, nil
232 }
233
234 keys, ok := secretsList.Data["keys"].([]interface{})
235 if !ok {
236 return []UnlockedSecret{}, nil
237 }
238
239 var secrets []UnlockedSecret
240
241 for _, keyInterface := range keys {
242 key, ok := keyInterface.(string)
243 if !ok {
244 continue
245 }
246
247 secretPath := fmt.Sprintf("%s/%s", repoPath, key)
248 secretData, err := v.client.KVv2(v.mountPath).Get(ctx, secretPath)
249 if err != nil {
250 v.logger.Warn("failed to read secret", "path", secretPath, "error", err)
251 continue
252 }
253
254 if secretData == nil || secretData.Data == nil {
255 continue
256 }
257
258 data := secretData.Data
259
260 valueStr, ok := data["value"].(string)
261 if !ok {
262 v.logger.Warn("secret missing value", "path", secretPath)
263 continue
264 }
265
266 createdAtStr, ok := data["created_at"].(string)
267 if !ok {
268 createdAtStr = time.Now().Format(time.RFC3339)
269 }
270
271 createdAt, err := time.Parse(time.RFC3339, createdAtStr)
272 if err != nil {
273 createdAt = time.Now()
274 }
275
276 createdByStr, ok := data["created_by"].(string)
277 if !ok {
278 createdByStr = ""
279 }
280
281 keyStr, ok := data["key"].(string)
282 if !ok {
283 keyStr = key
284 }
285
286 secret := UnlockedSecret{
287 Key: keyStr,
288 Value: valueStr,
289 Repo: repo,
290 CreatedAt: createdAt,
291 CreatedBy: syntax.DID(createdByStr),
292 }
293
294 secrets = append(secrets, secret)
295 }
296
297 v.logger.Debug("retrieved unlocked secrets", "repo", repo, "count", len(secrets))
298 return secrets, nil
299}
300
301// buildRepoPath creates a safe path for a repository
302func (v *OpenBaoManager) buildRepoPath(repo DidSlashRepo) string {
303 // convert DidSlashRepo to a safe path by replacing special characters
304 repoPath := strings.ReplaceAll(string(repo), "/", "_")
305 repoPath = strings.ReplaceAll(repoPath, ":", "_")
306 repoPath = strings.ReplaceAll(repoPath, ".", "_")
307 return fmt.Sprintf("repos/%s", repoPath)
308}
309
310// buildSecretPath creates a path for a specific secret
311func (v *OpenBaoManager) buildSecretPath(repo DidSlashRepo, key string) string {
312 return path.Join(v.buildRepoPath(repo), key)
313}