v2.31.0-dev16 8d0a092

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	// AuthTypeToken uses JWT token-based authentication.
16	AuthTypeToken AuthType = "token"
17)
18
19// Token represents a generated authentication token stored in the secrets backend.
20type Token struct {
21	ID             string
22	Subject        string
23	Scopes         []string
24	IssuedAt       time.Time
25	ExpiresAt      time.Time // Zero value means never expires
26	Revoked        bool
27	LastExtendedAt time.Time // Zero value means never extended
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	IsEphemeral bool     `json:"-"` // true when token was issued by /auth/token (not stored)
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	AdminResourceLogs    = "logs"
83	AdminResourceVolumes = "volumes"
84	AdminResourceAll     = "*"
85)
86
87// Admin scope action constants.
88const (
89	AdminActionRead  = "read"
90	AdminActionWrite = "write"
91	AdminActionAll   = "*"
92)
93
94// Scope represents a Docker registry scope (repository:name:actions).
95// Format follows the Docker Registry v2 token scope specification.
96type Scope struct {
97	Type    string   // "repository" for repository access
98	Name    string   // repository name or "*" for all
99	Actions []string // ["pull", "push", "*"]
100}
101
102// ParseScope parses a Docker v2 scope string into a Scope struct.
103// Format: type:name:action1,action2 (e.g., "repository:myrepo:push,pull")
104func ParseScope(s string) (*Scope, error) {
105	parts := strings.SplitN(s, ":", 3)
106	if len(parts) != 3 {
107		return nil, fmt.Errorf("invalid scope format: %s", s)
108	}
109
110	scope := &Scope{
111		Type:    parts[0],
112		Name:    parts[1],
113		Actions: strings.Split(parts[2], ","),
114	}
115
116	// Trim whitespace from actions
117	for i, action := range scope.Actions {
118		scope.Actions[i] = strings.TrimSpace(action)
119	}
120
121	return scope, nil
122}
123
124// CanAccess checks if the scope grants the requested action on the repository.
125// Supports exact matches and wildcard matching for repository names.
126func (s *Scope) CanAccess(repoName string, action string) bool {
127	if s.Type != "repository" {
128		return false
129	}
130
131	// Check repository name match
132	if s.Name != "*" && s.Name != repoName {
133		// Check prefix match for org-level scopes (e.g., "myorg/*" matches "myorg/app")
134		if !strings.HasSuffix(s.Name, "/*") {
135			return false
136		}
137		prefix := strings.TrimSuffix(s.Name, "/*")
138		if !strings.HasPrefix(repoName, prefix+"/") {
139			return false
140		}
141	}
142
143	// Check action permission
144	for _, a := range s.Actions {
145		if a == "*" || a == action {
146			return true
147		}
148	}
149
150	return false
151}
152
153// String returns the scope as a string in Docker v2 format.
154func (s *Scope) String() string {
155	return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
156}
157
158// IsAdminScope returns true if the scope is an admin scope.
159func (s *Scope) IsAdminScope() bool {
160	return s.Type == ScopeTypeAdmin
161}
162
163// IsRepositoryScope returns true if the scope is a repository scope.
164func (s *Scope) IsRepositoryScope() bool {
165	return s.Type == ScopeTypeRepository
166}
167
168// AdminScope represents an admin API scope (admin:resource:actions).
169// Format: admin:routes:read,write or admin:*:* for full access.
170type AdminScope struct {
171	Resource string   // routes, secrets, config, status, logs, volumes, or *
172	Actions  []string // read, write, or *
173}
174
175// ParseAdminScope parses an admin scope string.
176// Format: admin:resource:action1,action2
177func ParseAdminScope(s string) (*AdminScope, error) {
178	parts := strings.SplitN(s, ":", 3)
179	if len(parts) != 3 {
180		return nil, fmt.Errorf("invalid admin scope format: %s", s)
181	}
182
183	if parts[0] != ScopeTypeAdmin {
184		return nil, fmt.Errorf("not an admin scope: %s", s)
185	}
186
187	scope := &AdminScope{
188		Resource: parts[1],
189		Actions:  strings.Split(parts[2], ","),
190	}
191
192	// Trim whitespace from actions
193	for i, action := range scope.Actions {
194		scope.Actions[i] = strings.TrimSpace(action)
195	}
196
197	return scope, nil
198}
199
200// CanAccess checks if the admin scope grants the requested action on the resource.
201func (s *AdminScope) CanAccess(resource string, action string) bool {
202	// Check resource match
203	if s.Resource != AdminResourceAll && s.Resource != resource {
204		return false
205	}
206
207	// Check action permission
208	for _, a := range s.Actions {
209		if a == AdminActionAll || a == action {
210			return true
211		}
212	}
213
214	return false
215}
216
217// String returns the admin scope as a string.
218func (s *AdminScope) String() string {
219	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, s.Resource, strings.Join(s.Actions, ","))
220}
221
222// HasAdminAccess checks if a list of scope strings grants admin access to the resource and action.
223func HasAdminAccess(scopes []string, resource, action string) bool {
224	return ScopesGrantAdminAccess(scopes, resource, action)
225}
226
227// ScopesGrantRegistryAccess reports whether any of the granted scope strings
228// authorise the given action on the named repository.
229// It handles simple shorthand scopes ("pull", "push", "*") for backwards
230// compatibility as well as full Docker v2 format scopes (repository:name:actions).
231func ScopesGrantRegistryAccess(grantedScopes []string, repoName, action string) bool {
232	for _, raw := range grantedScopes {
233		scopeStr := strings.TrimSpace(raw)
234
235		// Shorthand scopes: "*", "pull", "push"
236		switch scopeStr {
237		case ScopeActionAll:
238			return true
239		case ScopeActionPull, ScopeActionPush:
240			if scopeStr == action {
241				return true
242			}
243		}
244
245		// Full Docker v2 format: repository:name:actions
246		scope, err := ParseScope(scopeStr)
247		if err != nil {
248			continue
249		}
250		if scope.Type != ScopeTypeRepository {
251			continue
252		}
253		if scope.CanAccess(repoName, action) {
254			return true
255		}
256	}
257	return false
258}
259
260// ScopesGrantAdminAccess reports whether any of the granted scope strings
261// authorise the given action on the named admin resource.
262// It handles the wildcard shorthand ("*") as well as full admin scope format
263// (admin:resource:actions).
264func ScopesGrantAdminAccess(grantedScopes []string, resource, action string) bool {
265	for _, raw := range grantedScopes {
266		scopeStr := strings.TrimSpace(raw)
267		if scopeStr == ScopeActionAll {
268			return true
269		}
270
271		adminScope, err := ParseAdminScope(scopeStr)
272		if err != nil {
273			continue
274		}
275		if adminScope.CanAccess(resource, action) {
276			return true
277		}
278	}
279	return false
280}
281
282// AdminScopeAll creates a full admin access scope string.
283func AdminScopeAll() string {
284	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceAll, AdminActionAll)
285}
286
287// AdminScopeRoutes creates an admin scope for routes with the given actions.
288func AdminScopeRoutes(actions ...string) string {
289	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceRoutes, strings.Join(actions, ","))
290}
291
292// AdminScopeSecrets creates an admin scope for secrets with the given actions.
293func AdminScopeSecrets(actions ...string) string {
294	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceSecrets, strings.Join(actions, ","))
295}
296
297// AdminScopeConfig creates an admin scope for config with the given actions.
298func AdminScopeConfig(actions ...string) string {
299	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceConfig, strings.Join(actions, ","))
300}
301
302// AdminScopeStatus creates an admin scope for status with the given actions.
303func AdminScopeStatus(actions ...string) string {
304	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ","))
305}
306
307// AdminScopeLogs creates an admin scope for logs with the given actions.
308func AdminScopeLogs(actions ...string) string {
309	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceLogs, strings.Join(actions, ","))
310}
311
312// AdminScopeVolumes creates an admin scope for volumes with the given actions.
313func AdminScopeVolumes(actions ...string) string {
314	return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceVolumes, strings.Join(actions, ","))
315}
316
317// AuthStatus represents status of an authentication session.
318type AuthStatus struct {
319	Valid     bool
320	Subject   string
321	Scopes    []string
322	ExpiresAt int64 // Unix timestamp, 0 if never expires
323	IssuedAt  int64 // Unix timestamp
324}
325
326// Context keys for auth-related domain concerns.
327type authContextKey string
328
329const (
330	// TokenClaimsKey is the context key for storing TokenClaims.
331	// Allows use cases and adapters to access validated token claims from context.
332	TokenClaimsKey authContextKey = "token_claims"
333	// ContextKeyScopes is the context key for storing token scopes.
334	ContextKeyScopes authContextKey = "token_scopes"
335	// ContextKeySubject is the context key for storing token subject.
336	ContextKeySubject authContextKey = "token_subject"
337)
338
339// GetTokenClaims extracts TokenClaims from context.
340// Returns nil if claims are not present or type assertion fails.
341func GetTokenClaims(ctx context.Context) *TokenClaims {
342	claims, ok := ctx.Value(TokenClaimsKey).(*TokenClaims)
343	if !ok {
344		return nil
345	}
346	return claims
347}

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