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// Scope represents a Docker registry scope (repository:name:actions).
68// Format follows the Docker Registry v2 token scope specification.
69type Scope struct {
70 Type string // "repository" for repository access
71 Name string // repository name or "*" for all
72 Actions []string // ["pull", "push", "*"]
73}
74
75// ParseScope parses a Docker v2 scope string into a Scope struct.
76// Format: type:name:action1,action2 (e.g., "repository:myrepo:push,pull")
77func ParseScope(s string) (*Scope, error) {
78 parts := strings.SplitN(s, ":", 3)
79 if len(parts) != 3 {
80 return nil, fmt.Errorf("invalid scope format: %s", s)
81 }
82
83 scope := &Scope{
84 Type: parts[0],
85 Name: parts[1],
86 Actions: strings.Split(parts[2], ","),
87 }
88
89 // Trim whitespace from actions
90 for i, action := range scope.Actions {
91 scope.Actions[i] = strings.TrimSpace(action)
92 }
93
94 return scope, nil
95}
96
97// CanAccess checks if the scope grants the requested action on the repository.
98// Supports exact matches and wildcard matching for repository names.
99func (s *Scope) CanAccess(repoName string, action string) bool {
100 if s.Type != "repository" {
101 return false
102 }
103
104 // Check repository name match
105 if s.Name != "*" && s.Name != repoName {
106 // Check prefix match for org-level scopes (e.g., "myorg/*" matches "myorg/app")
107 if !strings.HasSuffix(s.Name, "/*") {
108 return false
109 }
110 prefix := strings.TrimSuffix(s.Name, "/*")
111 if !strings.HasPrefix(repoName, prefix+"/") {
112 return false
113 }
114 }
115
116 // Check action permission
117 for _, a := range s.Actions {
118 if a == "*" || a == action {
119 return true
120 }
121 }
122
123 return false
124}
125
126// String returns the scope as a string in Docker v2 format.
127func (s *Scope) String() string {
128 return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
129}