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