1// Package domain contains pure business types without external dependencies.
2package domain
3
4import (
5 "fmt"
6 "strings"
7 "time"
8)
9
10// AuthType represents the type of authentication configured for the registry.
11type AuthType string
12
13const (
14 // AuthTypePassword uses bcrypt-hashed password authentication.
15 AuthTypePassword AuthType = "password"
16 // AuthTypeToken uses JWT token-based authentication.
17 AuthTypeToken AuthType = "token"
18)
19
20// Token represents a generated authentication token stored in the secrets backend.
21type Token struct {
22 ID string
23 Subject string
24 Scopes []string
25 IssuedAt time.Time
26 ExpiresAt time.Time // Zero value means never expires
27 Revoked bool
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}
39
40// IsExpired checks if the token has expired.
41func (t *Token) IsExpired() bool {
42 if t.ExpiresAt.IsZero() {
43 return false // Never expires
44 }
45 return time.Now().After(t.ExpiresAt)
46}
47
48// SecretsBackend represents the type of secrets storage backend.
49type SecretsBackend string
50
51const (
52 // SecretsBackendPass uses the pass password manager.
53 SecretsBackendPass SecretsBackend = "pass"
54 // SecretsBackendSops uses SOPS for encrypted secrets.
55 SecretsBackendSops SecretsBackend = "sops"
56 // SecretsBackendUnsafe stores secrets in plain text (development only).
57 SecretsBackendUnsafe SecretsBackend = "unsafe"
58)
59
60// Scope action constants for registry operations.
61const (
62 ScopeActionPull = "pull"
63 ScopeActionPush = "push"
64 ScopeActionAll = "*"
65)
66
67// Admin scope type constants.
68const (
69 // ScopeTypeRepository is for Docker registry repository access.
70 ScopeTypeRepository = "repository"
71 // ScopeTypeAdmin is for admin API access.
72 ScopeTypeAdmin = "admin"
73)
74
75// Admin scope resource constants.
76const (
77 AdminResourceRoutes = "routes"
78 AdminResourceSecrets = "secrets"
79 AdminResourceConfig = "config"
80 AdminResourceStatus = "status"
81 AdminResourceAll = "*"
82)
83
84// Admin scope action constants.
85const (
86 AdminActionRead = "read"
87 AdminActionWrite = "write"
88 AdminActionAll = "*"
89)
90
91// Scope represents a Docker registry scope (repository:name:actions).
92// Format follows the Docker Registry v2 token scope specification.
93type Scope struct {
94 Type string // "repository" for repository access
95 Name string // repository name or "*" for all
96 Actions []string // ["pull", "push", "*"]
97}
98
99// ParseScope parses a Docker v2 scope string into a Scope struct.
100// Format: type:name:action1,action2 (e.g., "repository:myrepo:push,pull")
101func ParseScope(s string) (*Scope, error) {
102 parts := strings.SplitN(s, ":", 3)
103 if len(parts) != 3 {
104 return nil, fmt.Errorf("invalid scope format: %s", s)
105 }
106
107 scope := &Scope{
108 Type: parts[0],
109 Name: parts[1],
110 Actions: strings.Split(parts[2], ","),
111 }
112
113 // Trim whitespace from actions
114 for i, action := range scope.Actions {
115 scope.Actions[i] = strings.TrimSpace(action)
116 }
117
118 return scope, nil
119}
120
121// CanAccess checks if the scope grants the requested action on the repository.
122// Supports exact matches and wildcard matching for repository names.
123func (s *Scope) CanAccess(repoName string, action string) bool {
124 if s.Type != "repository" {
125 return false
126 }
127
128 // Check repository name match
129 if s.Name != "*" && s.Name != repoName {
130 // Check prefix match for org-level scopes (e.g., "myorg/*" matches "myorg/app")
131 if !strings.HasSuffix(s.Name, "/*") {
132 return false
133 }
134 prefix := strings.TrimSuffix(s.Name, "/*")
135 if !strings.HasPrefix(repoName, prefix+"/") {
136 return false
137 }
138 }
139
140 // Check action permission
141 for _, a := range s.Actions {
142 if a == "*" || a == action {
143 return true
144 }
145 }
146
147 return false
148}
149
150// String returns the scope as a string in Docker v2 format.
151func (s *Scope) String() string {
152 return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
153}
154
155// IsAdminScope returns true if the scope is an admin scope.
156func (s *Scope) IsAdminScope() bool {
157 return s.Type == ScopeTypeAdmin
158}
159
160// IsRepositoryScope returns true if the scope is a repository scope.
161func (s *Scope) IsRepositoryScope() bool {
162 return s.Type == ScopeTypeRepository
163}
164
165// AdminScope represents an admin API scope (admin:resource:actions).
166// Format: admin:routes:read,write or admin:*:* for full access.
167type AdminScope struct {
168 Resource string // routes, secrets, config, status, or *
169 Actions []string // read, write, or *
170}
171
172// ParseAdminScope parses an admin scope string.
173// Format: admin:resource:action1,action2
174func ParseAdminScope(s string) (*AdminScope, error) {
175 parts := strings.SplitN(s, ":", 3)
176 if len(parts) != 3 {
177 return nil, fmt.Errorf("invalid admin scope format: %s", s)
178 }
179
180 if parts[0] != ScopeTypeAdmin {
181 return nil, fmt.Errorf("not an admin scope: %s", s)
182 }
183
184 scope := &AdminScope{
185 Resource: parts[1],
186 Actions: strings.Split(parts[2], ","),
187 }
188
189 // Trim whitespace from actions
190 for i, action := range scope.Actions {
191 scope.Actions[i] = strings.TrimSpace(action)
192 }
193
194 return scope, nil
195}
196
197// CanAccess checks if the admin scope grants the requested action on the resource.
198func (s *AdminScope) CanAccess(resource string, action string) bool {
199 // Check resource match
200 if s.Resource != AdminResourceAll && s.Resource != resource {
201 return false
202 }
203
204 // Check action permission
205 for _, a := range s.Actions {
206 if a == AdminActionAll || a == action {
207 return true
208 }
209 }
210
211 return false
212}
213
214// String returns the admin scope as a string.
215func (s *AdminScope) String() string {
216 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, s.Resource, strings.Join(s.Actions, ","))
217}
218
219// HasAdminAccess checks if a list of scope strings grants admin access to the resource and action.
220func HasAdminAccess(scopes []string, resource, action string) bool {
221 for _, scopeStr := range scopes {
222 scope, err := ParseScope(scopeStr)
223 if err != nil {
224 continue
225 }
226
227 if !scope.IsAdminScope() {
228 continue
229 }
230
231 adminScope, err := ParseAdminScope(scopeStr)
232 if err != nil {
233 continue
234 }
235
236 if adminScope.CanAccess(resource, action) {
237 return true
238 }
239 }
240 return false
241}
242
243// AdminScopeAll creates a full admin access scope string.
244func AdminScopeAll() string {
245 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceAll, AdminActionAll)
246}
247
248// AdminScopeRoutes creates an admin scope for routes with the given actions.
249func AdminScopeRoutes(actions ...string) string {
250 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceRoutes, strings.Join(actions, ","))
251}
252
253// AdminScopeSecrets creates an admin scope for secrets with the given actions.
254func AdminScopeSecrets(actions ...string) string {
255 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceSecrets, strings.Join(actions, ","))
256}
257
258// AdminScopeConfig creates an admin scope for config with the given actions.
259func AdminScopeConfig(actions ...string) string {
260 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceConfig, strings.Join(actions, ","))
261}
262
263// AdminScopeStatus creates an admin scope for status with the given actions.
264func AdminScopeStatus(actions ...string) string {
265 return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ","))
266}