v3.4.1 de8f68e

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

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