v2.31.1 f37fe67

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

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