feat(domain): add admin scopes and image label constants
- commit
- 328a5efb05b450fab445909b8ab6a30bec5955f0
- author
- bnema · · 2026-01-16 20:51 UTC
- parent
- b41867d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +156 -0
M internal/domain/auth.go +137 -0
| @@ -64,6 +64,30 @@ const ( | |||
| 64 | 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 | 91 | // Scope represents a Docker registry scope (repository:name:actions). | |
| 68 | 92 | // Format follows the Docker Registry v2 token scope specification. | |
| 69 | 93 | type Scope struct { | |
| @@ -127,3 +151,116 @@ func (s *Scope) CanAccess(repoName string, action string) bool { | |||
| 127 | 151 | func (s *Scope) String() string { | |
| 128 | 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 | + | } | |
| @@ -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 ( | |||
| 13 | 13 | ||
| 14 | 14 | // Image labels (set in Dockerfile) | |
| 15 | 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 | ) | |
| @@ -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 | |||
| 2 | 2 | ||
| 3 | 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 | 14 | // Manifest represents an OCI image manifest. | |
| 6 | 15 | type Manifest struct { | |
| 7 | 16 | 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 string | 16 | Name string | ||