v2.4.0 79b3461

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// GetAll returns all secrets for a domain as a key-value map.
61func (s *Service) GetAll(ctx context.Context, domain string) (map[string]string, error) {
62	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
63		zerowrap.FieldLayer:   "usecase",
64		zerowrap.FieldUseCase: "GetAll",
65		"domain":              domain,
66	})
67	log := zerowrap.FromCtx(ctx)
68
69	if err := ValidateDomain(domain); err != nil {
70		log.Warn().Err(err).Msg("domain validation failed")
71		return nil, err
72	}
73
74	secrets, err := s.store.GetAll(domain)
75	if err != nil {
76		log.Error().Err(err).Msg("failed to get secrets")
77		return nil, err
78	}
79
80	log.Debug().Int("count", len(secrets)).Msg("retrieved secrets")
81	return secrets, nil
82}
83
84// Set sets or updates multiple secrets for a domain, merging with existing.
85func (s *Service) Set(ctx context.Context, domain string, secrets map[string]string) error {
86	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
87		zerowrap.FieldLayer:   "usecase",
88		zerowrap.FieldUseCase: "Set",
89		"domain":              domain,
90	})
91	log := zerowrap.FromCtx(ctx)
92
93	if err := ValidateDomain(domain); err != nil {
94		log.Warn().Err(err).Msg("domain validation failed")
95		return err
96	}
97
98	if err := s.store.Set(domain, secrets); err != nil {
99		log.Error().Err(err).Msg("failed to set secrets")
100		return err
101	}
102
103	log.Info().Int("count", len(secrets)).Msg("secrets set")
104	return nil
105}
106
107// Delete removes a specific secret key from a domain.
108func (s *Service) Delete(ctx context.Context, domain, key string) error {
109	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
110		zerowrap.FieldLayer:   "usecase",
111		zerowrap.FieldUseCase: "Delete",
112		"domain":              domain,
113		"key":                 key,
114	})
115	log := zerowrap.FromCtx(ctx)
116
117	if err := ValidateDomain(domain); err != nil {
118		log.Warn().Err(err).Msg("domain validation failed")
119		return err
120	}
121
122	if err := s.store.Delete(domain, key); err != nil {
123		log.Error().Err(err).Msg("failed to delete secret")
124		return err
125	}
126
127	log.Info().Msg("secret deleted")
128	return nil
129}
130
131// ValidateDomain validates that a domain is safe to use for secret storage.
132// This prevents path traversal attacks and ensures the domain is valid.
133func ValidateDomain(domain string) error {
134	// Check for empty domain
135	if domain == "" {
136		return ErrDomainEmpty
137	}
138
139	// Check domain length (max DNS name length is 253)
140	if len(domain) > 253 {
141		return ErrDomainTooLong
142	}
143
144	// Check for path traversal attempts
145	if strings.Contains(domain, "..") {
146		return ErrDomainPathTraversal
147	}
148
149	// Check for null bytes (can cause issues in file paths)
150	if strings.ContainsRune(domain, '\x00') {
151		return ErrDomainInvalidChars
152	}
153
154	return nil
155}

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