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 } // 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] // 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 }