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}