v2.30.0-pre 7341e0e

auth.go RawHistoryFolder

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	LastExtendedAt time.Time // Zero value means never extended
30}
31
32// TokenClaims represents the JWT claims for a token.
33type TokenClaims struct {
34	ID        string   `json:"jti"`
35	Subject   string   `json:"sub"`
36	Scopes    []string `json:"scopes"`
37	IssuedAt  int64    `json:"iat"`
38	ExpiresAt int64    `json:"exp,omitempty"` // 0 means never expires
39	Issuer    string   `json:"iss"`
40}
41
42// IsExpired checks if the token has expired.
43func (t *Token) IsExpired() bool {
44	if t.ExpiresAt.IsZero() {
45		return false // Never expires
46	}
47	return time.Now().UTC().After(t.ExpiresAt)
48}
49
50// SecretsBackend represents the type of secrets storage backend.
51type SecretsBackend string
52
53const (
54	// SecretsBackendPass uses the pass password manager.
55	SecretsBackendPass SecretsBackend = "pass"
56	// SecretsBackendSops uses SOPS for encrypted secrets.
57	SecretsBackendSops SecretsBackend = "sops"
58	// SecretsBackendUnsafe stores secrets in plain text (development only).
59	SecretsBackendUnsafe SecretsBackend = "unsafe"
60)
61
62// Scope action constants for registry operations.
63const (
64	ScopeActionPull = "pull"
65	ScopeActionPush = "push"
66	ScopeActionAll  = "*"
67)
68
69// Admin scope type constants.
70const (
71	// ScopeTypeRepository is for Docker registry repository access.
72	ScopeTypeRepository = "repository"
73	// ScopeTypeAdmin is for admin API access.
74	ScopeTypeAdmin = "admin"
75)
76
77// Admin scope resource constants.
78const (
79	AdminResourceRoutes  = "routes"
80	AdminResourceSecrets = "secrets"
81	AdminResourceConfig  = "config"
82	AdminResourceStatus  = "status"
83	AdminResourceAll     = "*"
84)
85
86// Admin scope action constants.
87const (
88	AdminActionRead  = "read"
89	AdminActionWrite = "write"
90	AdminActionAll   = "*"
91)
92
93// Scope represents a Docker registry scope (repository:name:actions).
94// Format follows the Docker Registry v2 token scope specification.
95type Scope struct {
96	Type    string   // "repository" for repository access
97	Name    string   // repository name or "*" for all
98	Actions []string // ["pull", "push", "*"]
99}
100
101// ParseScope parses a Docker v2 scope string into a Scope struct.
102// Format: type:name:action1,action2 (e.g., "repository:myrepo:push,pull")
103func ParseScope(s string) (*Scope, error) {
104	parts := strings.SplitN(s, ":", 3)
105	if len(parts) != 3 {
106		return nil, fmt.Errorf("invalid scope format: %s", s)
107	}
108
109	scope := &Scope{
110		Type:    parts[0],
111		Name:    parts[1],
112		Actions: strings.Split(parts[2], ","),
113	}
114
115	// Trim whitespace from actions
116	for i, action := range scope.Actions {
117		scope.Actions[i] = strings.TrimSpace(action)
118	}
119
120	return scope, nil
121}
122
123// CanAccess checks if the scope grants the requested action on the repository.
124// Supports exact matches and wildcard matching for repository names.
125func (s *Scope) CanAccess(repoName string, action string) bool {
126	if s.Type != "repository" {
127		return false
128	}
129
130	// Check repository name match
131	if s.Name != "*" && s.Name != repoName {
132		// Check prefix match for org-level scopes (e.g., "myorg/*" matches "myorg/app")
133		if !strings.HasSuffix(s.Name, "/*") {
134			return false
135		}
136		prefix := strings.TrimSuffix(s.Name, "/*")
137		if !strings.HasPrefix(repoName, prefix+"/") {
138			return false
139		}
140	}
141
142	// Check action permission
143	for _, a := range s.Actions {
144		if a == "*" || a == action {
145			return true
146		}
147	}
148
149	return false
150}
151
152// String returns the scope as a string in Docker v2 format.
153func (s *Scope) String() string {
154	return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
155}
156
157// IsAdminScope returns true if the scope is an admin scope.
158func (s *Scope) IsAdminScope() bool {
159	return s.Type == ScopeTypeAdmin
160}
161
162// IsRepositoryScope returns true if the scope is a repository scope.
163func (s *Scope) IsRepositoryScope() bool {
164	return s.Type == ScopeTypeRepository
165}
166
167// AdminScope represents an admin API scope (admin:resource:actions).
168// Format: admin:routes:read,write or admin:*:* for full access.
169type AdminScope struct {
170	Resource string   // routes, secrets, config, status, or *
171	Actions  []string // read, write, or *
172}
173
174// ParseAdminScope parses an admin scope string.
175// Format: admin:resource:action1,action2
176func ParseAdminScope(s string) (*AdminScope, error) {
177	parts := strings.SplitN(s, ":", 3)
178	if len(parts) != 3 {
179		return nil, fmt.Errorf("invalid admin scope format: %s", s)
180	}
181
182	if parts[0] != ScopeTypeAdmin {
183		return nil, fmt.Errorf("not an admin scope: %s", s)
184	}
185
186	scope := &AdminScope{
187		Resource: parts[1],
188		Actions:  strings.Split(parts[2], ","),
189	}
190
191	// Trim whitespace from actions
192	for i, action := range scope.Actions {
193		scope.Actions[i] = strings.TrimSpace(action)
194	}
195
196	return scope, nil
197}
198
199// CanAccess checks if the admin scope grants the requested action on the resource.
200func (s *AdminScope) CanAccess(resource string, action string) bool {
201	// Check resource match
202	if s.Resource != AdminResourceAll && s.Resource != resource {
203		return false
204	}
205
206	// Check action permission
207	for _, a := range s.Actions {
208		if a == AdminActionAll || a == action {
209			return true
210		}
211	}
212
213	return false
214}
215
216// String returns the admin scope as a string.
217func (s *AdminScope) String() string {
218	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, s.Resource, strings.Join(s.Actions, ","))
219}
220
221// HasAdminAccess checks if a list of scope strings grants admin access to the resource and action.
222func HasAdminAccess(scopes []string, resource, action string) bool {
223	for _, scopeStr := range scopes {
224		scope, err := ParseScope(scopeStr)
225		if err != nil {
226			continue
227		}
228
229		if !scope.IsAdminScope() {
230			continue
231		}
232
233		adminScope, err := ParseAdminScope(scopeStr)
234		if err != nil {
235			continue
236		}
237
238		if adminScope.CanAccess(resource, action) {
239			return true
240		}
241	}
242	return false
243}
244
245// AdminScopeAll creates a full admin access scope string.
246func AdminScopeAll() string {
247	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceAll, AdminActionAll)
248}
249
250// AdminScopeRoutes creates an admin scope for routes with the given actions.
251func AdminScopeRoutes(actions ...string) string {
252	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceRoutes, strings.Join(actions, ","))
253}
254
255// AdminScopeSecrets creates an admin scope for secrets with the given actions.
256func AdminScopeSecrets(actions ...string) string {
257	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceSecrets, strings.Join(actions, ","))
258}
259
260// AdminScopeConfig creates an admin scope for config with the given actions.
261func AdminScopeConfig(actions ...string) string {
262	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceConfig, strings.Join(actions, ","))
263}
264
265// AdminScopeStatus creates an admin scope for status with the given actions.
266func AdminScopeStatus(actions ...string) string {
267	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ","))
268}
269
270// AuthStatus represents status of an authentication session.
271type AuthStatus struct {
272	Valid     bool
273	Subject   string
274	Scopes    []string
275	ExpiresAt int64 // Unix timestamp, 0 if never expires
276	IssuedAt  int64 // Unix timestamp
277}
278
279// Context keys for auth-related domain concerns.
280type authContextKey string
281
282const (
283	// TokenClaimsKey is the context key for storing TokenClaims.
284	// Allows use cases and adapters to access validated token claims from context.
285	TokenClaimsKey authContextKey = "token_claims"
286	// ContextKeyScopes is the context key for storing token scopes.
287	ContextKeyScopes authContextKey = "token_scopes"
288	// ContextKeySubject is the context key for storing token subject.
289	ContextKeySubject authContextKey = "token_subject"
290)
291
292// GetTokenClaims extracts TokenClaims from context.
293// Returns nil if claims are not present or type assertion fails.
294func GetTokenClaims(ctx context.Context) *TokenClaims {
295	claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims)
296	if !ok {
297		return nil
298	}
299	return claims
300}

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