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 AdminResourceAll = "*"
86)
87
88// Admin scope action constants.
89const (
90 AdminActionRead = "read"
91 AdminActionWrite = "write"
92 AdminActionAll = "*"
93)
94
95// Scope represents a Docker registry scope (repository:name:actions).
96// Format follows the Docker Registry v2 token scope specification.
97type Scope struct {
98 Type string // "repository" for repository access
99 Name string // repository name or "*" for all
100 Actions []string // ["pull", "push", "*"]
101}
102
103// ParseScope parses a Docker v2 scope string into a Scope struct.
104// Format: type:name:action1,action2 (e.g., "repository:myrepo:push,pull")
105func ParseScope(s string) (*Scope, error) {
106 parts := strings.SplitN(s, ":", 3)
107 if len(parts) != 3 {
108 return nil, fmt.Errorf("invalid scope format: %s", s)
109 }
110
111 scope := &Scope{
112 Type: parts[0],
113 Name: parts[1],
114 Actions: strings.Split(parts[2], ","),
115 }
116
117 // Trim whitespace from actions
118 for i, action := range scope.Actions {
119 scope.Actions[i] = strings.TrimSpace(action)
120 }
121
122 return scope, nil
123}
124
125// CanAccess checks if the scope grants the requested action on the repository.
126// Supports exact matches and wildcard matching for repository names.
127func (s *Scope) CanAccess(repoName string, action string) bool {
128 if s.Type != "repository" {
129 return false
130 }
131
132 // Check repository name match
133 if s.Name != "*" && s.Name != repoName {
134 // Check prefix match for org-level scopes (e.g., "myorg/*" matches "myorg/app")
135 if !strings.HasSuffix(s.Name, "/*") {
136 return false
137 }
138 prefix := strings.TrimSuffix(s.Name, "/*")
139 if !strings.HasPrefix(repoName, prefix+"/") {
140 return false
141 }
142 }
143
144 // Check action permission
145 for _, a := range s.Actions {
146 if a == "*" || a == action {
147 return true
148 }
149 }
150
151 return false
152}
153
154// String returns the scope as a string in Docker v2 format.
155func (s *Scope) String() string {
156 return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
157}
158
159// IsAdminScope returns true if the scope is an admin scope.
160func (s *Scope) IsAdminScope() bool {
161 return s.Type == ScopeTypeAdmin
162}
163
164// IsRepositoryScope returns true if the scope is a repository scope.
165func (s *Scope) IsRepositoryScope() bool {
166 return s.Type == ScopeTypeRepository
167}
168
169// AdminScope represents an admin API scope (admin:resource:actions).
170// Format: admin:routes:read,write or admin:*:* for full access.
171type AdminScope struct {
172 Resource string // routes, secrets, config, status, logs, volumes, or *
173 Actions []string // read, write, or *
174}
175
176// ParseAdminScope parses an admin scope string.
177// Format: admin:resource:action1,action2
178func ParseAdminScope(s string) (*AdminScope, error) {
179 parts := strings.SplitN(s, ":", 3)
180 if len(parts) != 3 {
181 return nil, fmt.Errorf("invalid admin scope format: %s", s)
182 }
183
184 if parts[0] != ScopeTypeAdmin {
185 return nil, fmt.Errorf("not an admin scope: %s", s)
186 }
187
188 scope := &AdminScope{
189 Resource: parts[1],
190 Actions: strings.Split(parts[2], ","),
191 }
192
193 // Trim whitespace from actions
194 for i, action := range scope.Actions {
195 scope.Actions[i] = strings.TrimSpace(action)
196 }
197
198 return scope, nil
199}
200
201// CanAccess checks if the admin scope grants the requested action on the resource.
202func (s *AdminScope) CanAccess(resource string, action string) bool {
203 // Check resource match
204 if s.Resource != AdminResourceAll && s.Resource != resource {
205 return false
206 }
207
208 // Check action permission
209 for _, a := range s.Actions {
210 if a == AdminActionAll || a == action {
211 return true
212 }
213 }
214
215 return false
216}
217
218// String returns the admin scope as a string.
219func (s *AdminScope) String() string {
220 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, s.Resource, strings.Join(s.Actions, ","))
221}
222
223// HasAdminAccess checks if a list of scope strings grants admin access to the resource and action.
224func HasAdminAccess(scopes []string, resource, action string) bool {
225 return ScopesGrantAdminAccess(scopes, resource, action)
226}
227
228// ScopesGrantRegistryAccess reports whether any of the granted scope strings
229// authorise the given action on the named repository.
230// It handles simple shorthand scopes ("pull", "push", "*") for backwards
231// compatibility as well as full Docker v2 format scopes (repository:name:actions).
232func ScopesGrantRegistryAccess(grantedScopes []string, repoName, action string) bool {
233 for _, raw := range grantedScopes {
234 scopeStr := strings.TrimSpace(raw)
235
236 // Shorthand scopes: "*", "pull", "push"
237 switch scopeStr {
238 case ScopeActionAll:
239 return true
240 case ScopeActionPull, ScopeActionPush:
241 if scopeStr == action {
242 return true
243 }
244 }
245
246 // Full Docker v2 format: repository:name:actions
247 scope, err := ParseScope(scopeStr)
248 if err != nil {
249 continue
250 }
251 if scope.Type != ScopeTypeRepository {
252 continue
253 }
254 if scope.CanAccess(repoName, action) {
255 return true
256 }
257 }
258 return false
259}
260
261// ScopesGrantAdminAccess reports whether an explicit admin scope authorises
262// the given action on the named admin resource. Untyped shorthand scopes such
263// as "*" are registry-only and never grant administrative access.
264func ScopesGrantAdminAccess(grantedScopes []string, resource, action string) bool {
265 for _, raw := range grantedScopes {
266 scopeStr := strings.TrimSpace(raw)
267 adminScope, err := ParseAdminScope(scopeStr)
268 if err != nil {
269 continue
270 }
271 if adminScope.CanAccess(resource, action) {
272 return true
273 }
274 }
275 return false
276}
277
278// AdminScopeAll creates a full admin access scope string.
279func AdminScopeAll() string {
280 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceAll, AdminActionAll)
281}
282
283// AdminScopeRoutes creates an admin scope for routes with the given actions.
284func AdminScopeRoutes(actions ...string) string {
285 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceRoutes, strings.Join(actions, ","))
286}
287
288// AdminScopeSecrets creates an admin scope for secrets with the given actions.
289func AdminScopeSecrets(actions ...string) string {
290 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceSecrets, strings.Join(actions, ","))
291}
292
293// AdminScopeConfig creates an admin scope for config with the given actions.
294func AdminScopeConfig(actions ...string) string {
295 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceConfig, strings.Join(actions, ","))
296}
297
298// AdminScopeStatus creates an admin scope for status with the given actions.
299func AdminScopeStatus(actions ...string) string {
300 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ","))
301}
302
303// AdminScopeLogs creates an admin scope for logs with the given actions.
304func AdminScopeLogs(actions ...string) string {
305 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceLogs, strings.Join(actions, ","))
306}
307
308// AdminScopeVolumes creates an admin scope for volumes with the given actions.
309func AdminScopeVolumes(actions ...string) string {
310 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceVolumes, strings.Join(actions, ","))
311}
312
313// AuthStatus represents status of an authentication session.
314type AuthStatus struct {
315 Valid bool
316 Subject string
317 Scopes []string
318 ExpiresAt int64 // Unix timestamp, 0 if never expires
319 IssuedAt int64 // Unix timestamp
320}
321
322// Context keys for auth-related domain concerns.
323type authContextKey string
324
325const (
326 // TokenClaimsKey is the context key for storing TokenClaims.
327 // Allows use cases and adapters to access validated token claims from context.
328 TokenClaimsKey authContextKey = "token_claims"
329 // ContextKeyScopes is the context key for storing token scopes.
330 ContextKeyScopes authContextKey = "token_scopes"
331 // ContextKeySubject is the context key for storing token subject.
332 ContextKeySubject authContextKey = "token_subject"
333)
334
335// GetTokenClaims extracts TokenClaims from context.
336// Returns nil if claims are not present or type assertion fails.
337func GetTokenClaims(ctx context.Context) *TokenClaims {
338 claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims)
339 if !ok {
340 return nil
341 }
342 return claims
343}