v2.5.0 b9b6391

auth.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close