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