1// Package domain contains pure business types without external dependencies.
2package domain
3
4import (
5 "context"
6 "fmt"
7 "strings"
8 "time"
9)
10
11// AuthType represents the type of authentication configured for the registry.
12type AuthType string
13
14const (
15 // AuthTypeToken uses JWT token-based authentication.
16 AuthTypeToken AuthType = "token"
17)
18
19// Token represents a generated authentication token stored in the secrets backend.
20type Token struct {
21 ID string
22 Subject string
23 Scopes []string
24 IssuedAt time.Time
25 ExpiresAt time.Time // Zero value means never expires
26 Revoked bool
27 LastExtendedAt time.Time // Zero value means never extended
28}
29
30// TokenClaims represents the JWT claims for a token.
31type TokenClaims struct {
32 ID string `json:"jti"`
33 Subject string `json:"sub"`
34 Scopes []string `json:"scopes"`
35 IssuedAt int64 `json:"iat"`
36 ExpiresAt int64 `json:"exp,omitempty"` // 0 means never expires
37 Issuer string `json:"iss"`
38 TokenType string `json:"token_type"`
39 IsEphemeral bool `json:"-"` // true when token was issued by /auth/token (not stored)
40}
41
42// IsExpired checks if the token has expired.
43func (t *Token) IsExpired() bool {
44 if t.ExpiresAt.IsZero() {
45 return false // Never expires
46 }
47 return time.Now().UTC().After(t.ExpiresAt)
48}
49
50// SecretsBackend represents the type of secrets storage backend.
51type SecretsBackend string
52
53const (
54 // SecretsBackendPass uses the pass password manager.
55 SecretsBackendPass SecretsBackend = "pass"
56 // SecretsBackendSops uses SOPS for encrypted secrets.
57 SecretsBackendSops SecretsBackend = "sops"
58 // SecretsBackendUnsafe stores secrets in plain text (development only).
59 SecretsBackendUnsafe SecretsBackend = "unsafe"
60)
61
62// Scope action constants for registry operations.
63const (
64 ScopeActionPull = "pull"
65 ScopeActionPush = "push"
66 ScopeActionAll = "*"
67)
68
69// Admin scope type constants.
70const (
71 // ScopeTypeRepository is for Docker registry repository access.
72 ScopeTypeRepository = "repository"
73 // ScopeTypeAdmin is for admin API access.
74 ScopeTypeAdmin = "admin"
75)
76
77// Admin scope resource constants.
78const (
79 AdminResourceRoutes = "routes"
80 AdminResourceSecrets = "secrets"
81 AdminResourceConfig = "config"
82 AdminResourceStatus = "status"
83 AdminResourceLogs = "logs"
84 AdminResourceVolumes = "volumes"
85 // AdminResourceApps gates the declarative app admin surface.
86 AdminResourceApps = "apps"
87 AdminResourceAll = "*"
88)
89
90// Admin scope action constants.
91const (
92 AdminActionRead = "read"
93 AdminActionWrite = "write"
94 AdminActionAll = "*"
95)
96
97// Scope represents a Docker registry scope (repository:name:actions).
98// Format follows the Docker Registry v2 token scope specification.
99type Scope struct {
100 Type string // "repository" for repository access
101 Name string // repository name or "*" for all
102 Actions []string // ["pull", "push", "*"]
103}
104
105// ParseScope parses a Docker v2 scope string into a Scope struct.
106// Format: type:name:action1,action2 (e.g., "repository:myrepo:push,pull")
107func ParseScope(s string) (*Scope, error) {
108 parts := strings.SplitN(s, ":", 3)
109 if len(parts) != 3 {
110 return nil, fmt.Errorf("invalid scope format: %s", s)
111 }
112
113 scope := &Scope{
114 Type: parts[0],
115 Name: parts[1],
116 Actions: strings.Split(parts[2], ","),
117 }
118
119 // Trim whitespace from actions
120 for i, action := range scope.Actions {
121 scope.Actions[i] = strings.TrimSpace(action)
122 }
123
124 return scope, nil
125}
126
127// CanAccess checks if the scope grants the requested action on the repository.
128// Supports exact matches and wildcard matching for repository names.
129func (s *Scope) CanAccess(repoName string, action string) bool {
130 if s.Type != "repository" {
131 return false
132 }
133
134 // Check repository name match
135 if s.Name != "*" && s.Name != repoName {
136 // Check prefix match for org-level scopes (e.g., "myorg/*" matches "myorg/app")
137 if !strings.HasSuffix(s.Name, "/*") {
138 return false
139 }
140 prefix := strings.TrimSuffix(s.Name, "/*")
141 if !strings.HasPrefix(repoName, prefix+"/") {
142 return false
143 }
144 }
145
146 // Check action permission
147 for _, a := range s.Actions {
148 if a == "*" || a == action {
149 return true
150 }
151 }
152
153 return false
154}
155
156// String returns the scope as a string in Docker v2 format.
157func (s *Scope) String() string {
158 return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
159}
160
161// IsAdminScope returns true if the scope is an admin scope.
162func (s *Scope) IsAdminScope() bool {
163 return s.Type == ScopeTypeAdmin
164}
165
166// IsRepositoryScope returns true if the scope is a repository scope.
167func (s *Scope) IsRepositoryScope() bool {
168 return s.Type == ScopeTypeRepository
169}
170
171// AdminScope represents an admin API scope (admin:resource:actions).
172// Format: admin:routes:read,write or admin:*:* for full access.
173type AdminScope struct {
174 Resource string // routes, secrets, config, status, logs, volumes, or *
175 Actions []string // read, write, or *
176}
177
178// ParseAdminScope parses an admin scope string.
179// Format: admin:resource:action1,action2
180func ParseAdminScope(s string) (*AdminScope, error) {
181 parts := strings.SplitN(s, ":", 3)
182 if len(parts) != 3 {
183 return nil, fmt.Errorf("invalid admin scope format: %s", s)
184 }
185
186 if parts[0] != ScopeTypeAdmin {
187 return nil, fmt.Errorf("not an admin scope: %s", s)
188 }
189
190 scope := &AdminScope{
191 Resource: parts[1],
192 Actions: strings.Split(parts[2], ","),
193 }
194
195 // Trim whitespace from actions
196 for i, action := range scope.Actions {
197 scope.Actions[i] = strings.TrimSpace(action)
198 }
199
200 return scope, nil
201}
202
203// CanAccess checks if the admin scope grants the requested action on the resource.
204func (s *AdminScope) CanAccess(resource string, action string) bool {
205 // Check resource match
206 if s.Resource != AdminResourceAll && s.Resource != resource {
207 return false
208 }
209
210 // Check action permission
211 for _, a := range s.Actions {
212 if a == AdminActionAll || a == action {
213 return true
214 }
215 }
216
217 return false
218}
219
220// String returns the admin scope as a string.
221func (s *AdminScope) String() string {
222 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, s.Resource, strings.Join(s.Actions, ","))
223}
224
225// HasAdminAccess checks if a list of scope strings grants admin access to the resource and action.
226func HasAdminAccess(scopes []string, resource, action string) bool {
227 return ScopesGrantAdminAccess(scopes, resource, action)
228}
229
230// ScopesGrantRegistryAccess reports whether any of the granted scope strings
231// authorise the given action on the named repository.
232// It handles simple shorthand scopes ("pull", "push", "*") for backwards
233// compatibility as well as full Docker v2 format scopes (repository:name:actions).
234func ScopesGrantRegistryAccess(grantedScopes []string, repoName, action string) bool {
235 for _, raw := range grantedScopes {
236 scopeStr := strings.TrimSpace(raw)
237
238 // Shorthand scopes: "*", "pull", "push"
239 switch scopeStr {
240 case ScopeActionAll:
241 return true
242 case ScopeActionPull, ScopeActionPush:
243 if scopeStr == action {
244 return true
245 }
246 }
247
248 // Full Docker v2 format: repository:name:actions
249 scope, err := ParseScope(scopeStr)
250 if err != nil {
251 continue
252 }
253 if scope.Type != ScopeTypeRepository {
254 continue
255 }
256 if scope.CanAccess(repoName, action) {
257 return true
258 }
259 }
260 return false
261}
262
263// ScopesGrantAdminAccess reports whether an explicit admin scope authorises
264// the given action on the named admin resource. Untyped shorthand scopes such
265// as "*" are registry-only and never grant administrative access.
266func ScopesGrantAdminAccess(grantedScopes []string, resource, action string) bool {
267 for _, raw := range grantedScopes {
268 scopeStr := strings.TrimSpace(raw)
269 adminScope, err := ParseAdminScope(scopeStr)
270 if err != nil {
271 continue
272 }
273 if adminScope.CanAccess(resource, action) {
274 return true
275 }
276 }
277 return false
278}
279
280// AdminScopeAll creates a full admin access scope string.
281func AdminScopeAll() string {
282 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceAll, AdminActionAll)
283}
284
285// AdminScopeRoutes creates an admin scope for routes with the given actions.
286func AdminScopeRoutes(actions ...string) string {
287 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceRoutes, strings.Join(actions, ","))
288}
289
290// AdminScopeSecrets creates an admin scope for secrets with the given actions.
291func AdminScopeSecrets(actions ...string) string {
292 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceSecrets, strings.Join(actions, ","))
293}
294
295// AdminScopeConfig creates an admin scope for config with the given actions.
296func AdminScopeConfig(actions ...string) string {
297 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceConfig, strings.Join(actions, ","))
298}
299
300// AdminScopeStatus creates an admin scope for status with the given actions.
301func AdminScopeStatus(actions ...string) string {
302 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ","))
303}
304
305// AdminScopeLogs creates an admin scope for logs with the given actions.
306func AdminScopeLogs(actions ...string) string {
307 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceLogs, strings.Join(actions, ","))
308}
309
310// AdminScopeVolumes creates an admin scope for volumes with the given actions.
311func AdminScopeVolumes(actions ...string) string {
312 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceVolumes, strings.Join(actions, ","))
313}
314
315// AdminScopeApps creates an admin scope for apps with the given actions.
316func AdminScopeApps(actions ...string) string {
317 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceApps, strings.Join(actions, ","))
318}
319
320// AuthStatus represents status of an authentication session.
321type AuthStatus struct {
322 Valid bool
323 Subject string
324 Scopes []string
325 ExpiresAt int64 // Unix timestamp, 0 if never expires
326 IssuedAt int64 // Unix timestamp
327}
328
329// Context keys for auth-related domain concerns.
330type authContextKey string
331
332const (
333 // TokenClaimsKey is the context key for storing TokenClaims.
334 // Allows use cases and adapters to access validated token claims from context.
335 TokenClaimsKey authContextKey = "token_claims"
336 // ContextKeyScopes is the context key for storing token scopes.
337 ContextKeyScopes authContextKey = "token_scopes"
338 // ContextKeySubject is the context key for storing token subject.
339 ContextKeySubject authContextKey = "token_subject"
340)
341
342// GetTokenClaims extracts TokenClaims from context.
343// Returns nil if claims are not present or type assertion fails.
344func GetTokenClaims(ctx context.Context) *TokenClaims {
345 claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims)
346 if !ok {
347 return nil
348 }
349 return claims
350}