Files
at-container-registry/pkg/auth/scope.go
2026-04-07 22:26:21 -05:00

115 lines
3.3 KiB
Go

package auth
import (
"fmt"
"strings"
)
// AccessEntry represents access permissions for a resource
type AccessEntry struct {
Type string `json:"type"` // "repository"
Name string `json:"name,omitempty"` // e.g., "alice/myapp"
Actions []string `json:"actions,omitempty"` // e.g., ["pull", "push"]
}
// ParseScope parses Docker registry scope strings into AccessEntry structures
// Scope format: "repository:alice/myapp:pull,push"
// Multiple scopes can be provided
func ParseScope(scopes []string) ([]AccessEntry, error) {
var access []AccessEntry
for _, scope := range scopes {
if scope == "" {
continue
}
parts := strings.Split(scope, ":")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid scope format: %s", scope)
}
resourceType := parts[0]
var name string
var actions []string
if len(parts) == 2 {
// Format: "repository:alice/myapp" (no actions specified)
name = parts[1]
} else if len(parts) == 3 {
// Format: "repository:alice/myapp:pull,push"
name = parts[1]
if parts[2] != "" {
actions = strings.Split(parts[2], ",")
}
} else {
return nil, fmt.Errorf("invalid scope format: %s", scope)
}
access = append(access, AccessEntry{
Type: resourceType,
Name: name,
Actions: actions,
})
}
return access, nil
}
// DecodeDIDFromHyphens converts a hyphen-encoded DID back to colon-separated form.
// "did-plc-abc123" → "did:plc:abc123", "did-web-example.com" → "did:web:example.com"
// Returns the decoded DID and true if the input matched, or ("", false) otherwise.
func DecodeDIDFromHyphens(s string) (string, bool) {
if strings.HasPrefix(s, "did-plc-") {
return "did:plc:" + strings.TrimPrefix(s, "did-plc-"), true
}
if strings.HasPrefix(s, "did-web-") {
return "did:web:" + strings.TrimPrefix(s, "did-web-"), true
}
return "", false
}
// ValidateAccess checks if the requested access is allowed for the user
// For ATCR, users can only push to repositories under their own handle/DID
func ValidateAccess(userDID, userHandle string, access []AccessEntry) error {
for _, entry := range access {
if entry.Type != "repository" {
continue
}
// Allow wildcard scope (e.g., "repository:*:pull,push")
// This is used by Docker credential helpers to request broad permissions
// Actual authorization happens later when accessing specific repositories
if entry.Name == "*" {
continue
}
// Extract the owner from repository name (e.g., "alice/myapp" -> "alice")
parts := strings.SplitN(entry.Name, "/", 2)
if len(parts) < 1 {
return fmt.Errorf("invalid repository name: %s", entry.Name)
}
repoOwner := parts[0]
// Decode hyphen-encoded DIDs (e.g., did-plc-abc123 → did:plc:abc123)
// Image paths use hyphens because colons are parsed as transport separators
if decoded, ok := DecodeDIDFromHyphens(repoOwner); ok {
repoOwner = decoded
}
// Check if user is trying to access their own repository
// They can use either their handle or DID
if repoOwner != userHandle && repoOwner != userDID {
// For push/delete operations, strict ownership check
for _, action := range entry.Actions {
if action == "push" || action == "delete" {
return fmt.Errorf("user %s cannot %s to repository %s", userHandle, action, entry.Name)
}
}
}
}
return nil
}