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