v2.7.1 b3a38c8

service.go RawHistoryFolder

1// Package secrets implements the secret management use case.
2package secrets
3
4import (
5	"context"
6	"errors"
7	"strings"
8
9	"github.com/bnema/zerowrap"
10
11	"gordon/internal/boundaries/out"
12)
13
14// Domain validation errors.
15var (
16	ErrDomainEmpty         = errors.New("domain cannot be empty")
17	ErrDomainTooLong       = errors.New("domain exceeds maximum length of 253 characters")
18	ErrDomainPathTraversal = errors.New("domain contains path traversal sequence")
19	ErrDomainInvalidChars  = errors.New("domain contains invalid characters")
20)
21
22// Service implements the SecretService interface.
23type Service struct {
24	store out.DomainSecretStore
25	log   zerowrap.Logger
26}
27
28// NewService creates a new secrets service.
29func NewService(store out.DomainSecretStore, log zerowrap.Logger) *Service {
30	return &Service{
31		store: store,
32		log:   log,
33	}
34}
35
36// ListKeys returns the list of secret keys for a domain (not values).
37func (s *Service) ListKeys(ctx context.Context, domain string) ([]string, error) {
38	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
39		zerowrap.FieldLayer:   "usecase",
40		zerowrap.FieldUseCase: "ListKeys",
41		"domain":              domain,
42	})
43	log := zerowrap.FromCtx(ctx)
44
45	if err := ValidateDomain(domain); err != nil {
46		log.Warn().Err(err).Msg("domain validation failed")
47		return nil, err
48	}
49
50	keys, err := s.store.ListKeys(domain)
51	if err != nil {
52		log.Error().Err(err).Msg("failed to list secret keys")
53		return nil, err
54	}
55
56	log.Debug().Int("count", len(keys)).Msg("listed secret keys")
57	return keys, nil
58}
59
60// ListKeysWithAttachments returns the list of secret keys for a domain
61// along with any attachment secrets for containers associated with the domain.
62func (s *Service) ListKeysWithAttachments(ctx context.Context, domain string) ([]string, []out.AttachmentSecrets, error) {
63	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
64		zerowrap.FieldLayer:   "usecase",
65		zerowrap.FieldUseCase: "ListKeysWithAttachments",
66		"domain":              domain,
67	})
68	log := zerowrap.FromCtx(ctx)
69
70	if err := ValidateDomain(domain); err != nil {
71		log.Warn().Err(err).Msg("domain validation failed")
72		return nil, nil, err
73	}
74
75	// Get domain secrets
76	keys, err := s.store.ListKeys(domain)
77	if err != nil {
78		log.Error().Err(err).Msg("failed to list secret keys")
79		return nil, nil, err
80	}
81
82	// Get attachment secrets
83	attachments, err := s.store.ListAttachmentKeys(domain)
84	if err != nil {
85		log.Warn().Err(err).Msg("failed to list attachment secrets, continuing without them")
86		attachments = nil // Don't fail, just return empty attachments
87	}
88
89	log.Debug().
90		Int("domain_keys", len(keys)).
91		Int("attachments", len(attachments)).
92		Msg("listed secret keys with attachments")
93
94	return keys, attachments, nil
95}
96
97// GetAll returns all secrets for a domain as a key-value map.
98func (s *Service) GetAll(ctx context.Context, domain string) (map[string]string, error) {
99	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
100		zerowrap.FieldLayer:   "usecase",
101		zerowrap.FieldUseCase: "GetAll",
102		"domain":              domain,
103	})
104	log := zerowrap.FromCtx(ctx)
105
106	if err := ValidateDomain(domain); err != nil {
107		log.Warn().Err(err).Msg("domain validation failed")
108		return nil, err
109	}
110
111	secrets, err := s.store.GetAll(domain)
112	if err != nil {
113		log.Error().Err(err).Msg("failed to get secrets")
114		return nil, err
115	}
116
117	log.Debug().Int("count", len(secrets)).Msg("retrieved secrets")
118	return secrets, nil
119}
120
121// Set sets or updates multiple secrets for a domain, merging with existing.
122func (s *Service) Set(ctx context.Context, domain string, secrets map[string]string) error {
123	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
124		zerowrap.FieldLayer:   "usecase",
125		zerowrap.FieldUseCase: "Set",
126		"domain":              domain,
127	})
128	log := zerowrap.FromCtx(ctx)
129
130	if err := ValidateDomain(domain); err != nil {
131		log.Warn().Err(err).Msg("domain validation failed")
132		return err
133	}
134
135	if err := s.store.Set(domain, secrets); err != nil {
136		log.Error().Err(err).Msg("failed to set secrets")
137		return err
138	}
139
140	log.Info().Int("count", len(secrets)).Msg("secrets set")
141	return nil
142}
143
144// Delete removes a specific secret key from a domain.
145func (s *Service) Delete(ctx context.Context, domain, key string) error {
146	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
147		zerowrap.FieldLayer:   "usecase",
148		zerowrap.FieldUseCase: "Delete",
149		"domain":              domain,
150		"key":                 key,
151	})
152	log := zerowrap.FromCtx(ctx)
153
154	if err := ValidateDomain(domain); err != nil {
155		log.Warn().Err(err).Msg("domain validation failed")
156		return err
157	}
158
159	if err := s.store.Delete(domain, key); err != nil {
160		log.Error().Err(err).Msg("failed to delete secret")
161		return err
162	}
163
164	log.Info().Msg("secret deleted")
165	return nil
166}
167
168// ValidateDomain validates that a domain is safe to use for secret storage.
169// This prevents path traversal attacks and ensures the domain is valid.
170func ValidateDomain(domain string) error {
171	// Check for empty domain
172	if domain == "" {
173		return ErrDomainEmpty
174	}
175
176	// Check domain length (max DNS name length is 253)
177	if len(domain) > 253 {
178		return ErrDomainTooLong
179	}
180
181	// Check for path traversal attempts
182	if strings.Contains(domain, "..") {
183		return ErrDomainPathTraversal
184	}
185
186	// Check for null bytes (can cause issues in file paths)
187	if strings.ContainsRune(domain, '\x00') {
188		return ErrDomainInvalidChars
189	}
190
191	return nil
192}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close