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 // AuthTypePassword uses bcrypt-hashed password authentication.
16 AuthTypePassword AuthType = "password"
17 // AuthTypeToken uses JWT token-based authentication.
18 AuthTypeToken AuthType = "token"
19)
20
21// Token represents a generated authentication token stored in the secrets backend.
22type Token struct {
23 ID string
24 Subject string
25 Scopes []string
26 IssuedAt time.Time
27 ExpiresAt time.Time // Zero value means never expires
28 Revoked bool
29}
30
31// TokenClaims represents the JWT claims for a token.
32type TokenClaims struct {
33 ID string `json:"jti"`
34 Subject string `json:"sub"`
35 Scopes []string `json:"scopes"`
36 IssuedAt int64 `json:"iat"`
37 ExpiresAt int64 `json:"exp,omitempty"` // 0 means never expires
38 Issuer string `json:"iss"`
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 for _, scopeStr := range scopes {
223 scope, err := ParseScope(scopeStr)
224 if err != nil {
225 continue
226 }
227
228 if !scope.IsAdminScope() {
229 continue
230 }
231
232 adminScope, err := ParseAdminScope(scopeStr)
233 if err != nil {
234 continue
235 }
236
237 if adminScope.CanAccess(resource, action) {
238 return true
239 }
240 }
241 return false
242}
243
244// AdminScopeAll creates a full admin access scope string.
245func AdminScopeAll() string {
246 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceAll, AdminActionAll)
247}
248
249// AdminScopeRoutes creates an admin scope for routes with the given actions.
250func AdminScopeRoutes(actions ...string) string {
251 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceRoutes, strings.Join(actions, ","))
252}
253
254// AdminScopeSecrets creates an admin scope for secrets with the given actions.
255func AdminScopeSecrets(actions ...string) string {
256 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceSecrets, strings.Join(actions, ","))
257}
258
259// AdminScopeConfig creates an admin scope for config with the given actions.
260func AdminScopeConfig(actions ...string) string {
261 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceConfig, strings.Join(actions, ","))
262}
263
264// AdminScopeStatus creates an admin scope for status with the given actions.
265func AdminScopeStatus(actions ...string) string {
266 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ","))
267}
268
269// AuthStatus represents status of an authentication session.
270type AuthStatus struct {
271 Valid bool
272 Subject string
273 Scopes []string
274 ExpiresAt int64 // Unix timestamp, 0 if never expires
275 IssuedAt int64 // Unix timestamp
276}
277
278// Context keys for auth-related domain concerns.
279type authContextKey string
280
281const (
282 // TokenClaimsKey is the context key for storing TokenClaims.
283 // Allows use cases and adapters to access validated token claims from context.
284 TokenClaimsKey authContextKey = "token_claims"
285 // ContextKeyScopes is the context key for storing token scopes.
286 ContextKeyScopes authContextKey = "token_scopes"
287 // ContextKeySubject is the context key for storing token subject.
288 ContextKeySubject authContextKey = "token_subject"
289)
290
291// GetTokenClaims extracts TokenClaims from context.
292// Returns nil if claims are not present or type assertion fails.
293func GetTokenClaims(ctx context.Context) *TokenClaims {
294 claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims)
295 if !ok {
296 return nil
297 }
298 return claims
299}