detached cd0d5fb

feat(domain): add admin scopes and image label constants

commit
cd0d5fb98e7ac8c7e55d5f1f7668452bcba33e29
author
bnema · · 2026-01-16 20:51 UTC
parent
14ae069
signature
unsigned
3 files · +156 -0
M internal/domain/auth.go +137 -0
@@ -64,6 +64,30 @@ const (
6464 ScopeActionAll = "*"
6565 )
6666
67+// Admin scope type constants.
68+const (
69+ // ScopeTypeRepository is for Docker registry repository access.
70+ ScopeTypeRepository = "repository"
71+ // ScopeTypeAdmin is for admin API access.
72+ ScopeTypeAdmin = "admin"
73+)
74+
75+// Admin scope resource constants.
76+const (
77+ AdminResourceRoutes = "routes"
78+ AdminResourceSecrets = "secrets"
79+ AdminResourceConfig = "config"
80+ AdminResourceStatus = "status"
81+ AdminResourceAll = "*"
82+)
83+
84+// Admin scope action constants.
85+const (
86+ AdminActionRead = "read"
87+ AdminActionWrite = "write"
88+ AdminActionAll = "*"
89+)
90+
6791 // Scope represents a Docker registry scope (repository:name:actions).
6892 // Format follows the Docker Registry v2 token scope specification.
6993 type Scope struct {
@@ -127,3 +151,116 @@ func (s *Scope) CanAccess(repoName string, action string) bool {
127151 func (s *Scope) String() string {
128152 return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
129153 }
154+
155+// IsAdminScope returns true if the scope is an admin scope.
156+func (s *Scope) IsAdminScope() bool {
157+ return s.Type == ScopeTypeAdmin
158+}
159+
160+// IsRepositoryScope returns true if the scope is a repository scope.
161+func (s *Scope) IsRepositoryScope() bool {
162+ return s.Type == ScopeTypeRepository
163+}
164+
165+// AdminScope represents an admin API scope (admin:resource:actions).
166+// Format: admin:routes:read,write or admin:*:* for full access.
167+type AdminScope struct {
168+ Resource string // routes, secrets, config, status, or *
169+ Actions []string // read, write, or *
170+}
171+
172+// ParseAdminScope parses an admin scope string.
173+// Format: admin:resource:action1,action2
174+func ParseAdminScope(s string) (*AdminScope, error) {
175+ parts := strings.SplitN(s, ":", 3)
176+ if len(parts) != 3 {
177+ return nil, fmt.Errorf("invalid admin scope format: %s", s)
178+ }
179+
180+ if parts[0] != ScopeTypeAdmin {
181+ return nil, fmt.Errorf("not an admin scope: %s", s)
182+ }
183+
184+ scope := &AdminScope{
185+ Resource: parts[1],
186+ Actions: strings.Split(parts[2], ","),
187+ }
188+
189+ // Trim whitespace from actions
190+ for i, action := range scope.Actions {
191+ scope.Actions[i] = strings.TrimSpace(action)
192+ }
193+
194+ return scope, nil
195+}
196+
197+// CanAccess checks if the admin scope grants the requested action on the resource.
198+func (s *AdminScope) CanAccess(resource string, action string) bool {
199+ // Check resource match
200+ if s.Resource != AdminResourceAll && s.Resource != resource {
201+ return false
202+ }
203+
204+ // Check action permission
205+ for _, a := range s.Actions {
206+ if a == AdminActionAll || a == action {
207+ return true
208+ }
209+ }
210+
211+ return false
212+}
213+
214+// String returns the admin scope as a string.
215+func (s *AdminScope) String() string {
216+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, s.Resource, strings.Join(s.Actions, ","))
217+}
218+
219+// HasAdminAccess checks if a list of scope strings grants admin access to the resource and action.
220+func HasAdminAccess(scopes []string, resource, action string) bool {
221+ for _, scopeStr := range scopes {
222+ scope, err := ParseScope(scopeStr)
223+ if err != nil {
224+ continue
225+ }
226+
227+ if !scope.IsAdminScope() {
228+ continue
229+ }
230+
231+ adminScope, err := ParseAdminScope(scopeStr)
232+ if err != nil {
233+ continue
234+ }
235+
236+ if adminScope.CanAccess(resource, action) {
237+ return true
238+ }
239+ }
240+ return false
241+}
242+
243+// AdminScopeAll creates a full admin access scope string.
244+func AdminScopeAll() string {
245+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceAll, AdminActionAll)
246+}
247+
248+// AdminScopeRoutes creates an admin scope for routes with the given actions.
249+func AdminScopeRoutes(actions ...string) string {
250+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceRoutes, strings.Join(actions, ","))
251+}
252+
253+// AdminScopeSecrets creates an admin scope for secrets with the given actions.
254+func AdminScopeSecrets(actions ...string) string {
255+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceSecrets, strings.Join(actions, ","))
256+}
257+
258+// AdminScopeConfig creates an admin scope for config with the given actions.
259+func AdminScopeConfig(actions ...string) string {
260+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceConfig, strings.Join(actions, ","))
261+}
262+
263+// AdminScopeStatus creates an admin scope for status with the given actions.
264+func AdminScopeStatus(actions ...string) string {
265+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ","))
266+}
@@ -64,6 +64,30 @@ const (
64 ScopeActionAll = "*"64 ScopeActionAll = "*"
65 )65 )
66 66
67+// Admin scope type constants.
68+const (
69+ // ScopeTypeRepository is for Docker registry repository access.
70+ ScopeTypeRepository = "repository"
71+ // ScopeTypeAdmin is for admin API access.
72+ ScopeTypeAdmin = "admin"
73+)
74+
75+// Admin scope resource constants.
76+const (
77+ AdminResourceRoutes = "routes"
78+ AdminResourceSecrets = "secrets"
79+ AdminResourceConfig = "config"
80+ AdminResourceStatus = "status"
81+ AdminResourceAll = "*"
82+)
83+
84+// Admin scope action constants.
85+const (
86+ AdminActionRead = "read"
87+ AdminActionWrite = "write"
88+ AdminActionAll = "*"
89+)
90+
67 // Scope represents a Docker registry scope (repository:name:actions).91 // Scope represents a Docker registry scope (repository:name:actions).
68 // Format follows the Docker Registry v2 token scope specification.92 // Format follows the Docker Registry v2 token scope specification.
69 type Scope struct {93 type Scope struct {
@@ -127,3 +151,116 @@ func (s *Scope) CanAccess(repoName string, action string) bool {
127 func (s *Scope) String() string {151 func (s *Scope) String() string {
128 return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))152 return fmt.Sprintf("%s:%s:%s", s.Type, s.Name, strings.Join(s.Actions, ","))
129 }153 }
154+
155+// IsAdminScope returns true if the scope is an admin scope.
156+func (s *Scope) IsAdminScope() bool {
157+ return s.Type == ScopeTypeAdmin
158+}
159+
160+// IsRepositoryScope returns true if the scope is a repository scope.
161+func (s *Scope) IsRepositoryScope() bool {
162+ return s.Type == ScopeTypeRepository
163+}
164+
165+// AdminScope represents an admin API scope (admin:resource:actions).
166+// Format: admin:routes:read,write or admin:*:* for full access.
167+type AdminScope struct {
168+ Resource string // routes, secrets, config, status, or *
169+ Actions []string // read, write, or *
170+}
171+
172+// ParseAdminScope parses an admin scope string.
173+// Format: admin:resource:action1,action2
174+func ParseAdminScope(s string) (*AdminScope, error) {
175+ parts := strings.SplitN(s, ":", 3)
176+ if len(parts) != 3 {
177+ return nil, fmt.Errorf("invalid admin scope format: %s", s)
178+ }
179+
180+ if parts[0] != ScopeTypeAdmin {
181+ return nil, fmt.Errorf("not an admin scope: %s", s)
182+ }
183+
184+ scope := &AdminScope{
185+ Resource: parts[1],
186+ Actions: strings.Split(parts[2], ","),
187+ }
188+
189+ // Trim whitespace from actions
190+ for i, action := range scope.Actions {
191+ scope.Actions[i] = strings.TrimSpace(action)
192+ }
193+
194+ return scope, nil
195+}
196+
197+// CanAccess checks if the admin scope grants the requested action on the resource.
198+func (s *AdminScope) CanAccess(resource string, action string) bool {
199+ // Check resource match
200+ if s.Resource != AdminResourceAll && s.Resource != resource {
201+ return false
202+ }
203+
204+ // Check action permission
205+ for _, a := range s.Actions {
206+ if a == AdminActionAll || a == action {
207+ return true
208+ }
209+ }
210+
211+ return false
212+}
213+
214+// String returns the admin scope as a string.
215+func (s *AdminScope) String() string {
216+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, s.Resource, strings.Join(s.Actions, ","))
217+}
218+
219+// HasAdminAccess checks if a list of scope strings grants admin access to the resource and action.
220+func HasAdminAccess(scopes []string, resource, action string) bool {
221+ for _, scopeStr := range scopes {
222+ scope, err := ParseScope(scopeStr)
223+ if err != nil {
224+ continue
225+ }
226+
227+ if !scope.IsAdminScope() {
228+ continue
229+ }
230+
231+ adminScope, err := ParseAdminScope(scopeStr)
232+ if err != nil {
233+ continue
234+ }
235+
236+ if adminScope.CanAccess(resource, action) {
237+ return true
238+ }
239+ }
240+ return false
241+}
242+
243+// AdminScopeAll creates a full admin access scope string.
244+func AdminScopeAll() string {
245+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceAll, AdminActionAll)
246+}
247+
248+// AdminScopeRoutes creates an admin scope for routes with the given actions.
249+func AdminScopeRoutes(actions ...string) string {
250+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceRoutes, strings.Join(actions, ","))
251+}
252+
253+// AdminScopeSecrets creates an admin scope for secrets with the given actions.
254+func AdminScopeSecrets(actions ...string) string {
255+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceSecrets, strings.Join(actions, ","))
256+}
257+
258+// AdminScopeConfig creates an admin scope for config with the given actions.
259+func AdminScopeConfig(actions ...string) string {
260+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceConfig, strings.Join(actions, ","))
261+}
262+
263+// AdminScopeStatus creates an admin scope for status with the given actions.
264+func AdminScopeStatus(actions ...string) string {
265+ return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ","))
266+}
M internal/domain/labels.go +10 -0
@@ -13,4 +13,14 @@ const (
1313
1414 // Image labels (set in Dockerfile)
1515 LabelProxyPort = "gordon.proxy.port"
16+
17+ // Auto-route image labels (for automatic route discovery)
18+ // LabelDomains specifies multiple route domains (comma-separated).
19+ LabelDomains = "gordon.domains"
20+ // LabelHealth specifies the health check endpoint path.
21+ LabelHealth = "gordon.health"
22+ // LabelPort specifies the container port to proxy.
23+ LabelPort = "gordon.port"
24+ // LabelEnvFile specifies the path to .env file inside the image.
25+ LabelEnvFile = "gordon.env-file"
1626 )
@@ -13,4 +13,14 @@ const (
13 13
14 // Image labels (set in Dockerfile)14 // Image labels (set in Dockerfile)
15 LabelProxyPort = "gordon.proxy.port"15 LabelProxyPort = "gordon.proxy.port"
16+
17+ // Auto-route image labels (for automatic route discovery)
18+ // LabelDomains specifies multiple route domains (comma-separated).
19+ LabelDomains = "gordon.domains"
20+ // LabelHealth specifies the health check endpoint path.
21+ LabelHealth = "gordon.health"
22+ // LabelPort specifies the container port to proxy.
23+ LabelPort = "gordon.port"
24+ // LabelEnvFile specifies the path to .env file inside the image.
25+ LabelEnvFile = "gordon.env-file"
16 )26 )
M internal/domain/registry.go +9 -0
@@ -2,6 +2,15 @@ package domain
22
33 import "time"
44
5+// ImageLabels represents extracted Gordon labels from an image.
6+type ImageLabels struct {
7+ Domain string // Primary domain
8+ Domains []string // Additional domains
9+ Health string // Health check path
10+ Port string // Container port
11+ EnvFile string // Env file path in image
12+}
13+
514 // Manifest represents an OCI image manifest.
615 type Manifest struct {
716 Name string
@@ -2,6 +2,15 @@ package domain
2 2
3 import "time"3 import "time"
4 4
5+// ImageLabels represents extracted Gordon labels from an image.
6+type ImageLabels struct {
7+ Domain string // Primary domain
8+ Domains []string // Additional domains
9+ Health string // Health check path
10+ Port string // Container port
11+ EnvFile string // Env file path in image
12+}
13+
5 // Manifest represents an OCI image manifest.14 // Manifest represents an OCI image manifest.
6 type Manifest struct {15 type Manifest struct {
7 Name string16 Name string

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