v2.8.0 6aced72

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

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