A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1package auth
2
3import (
4 "fmt"
5 "strings"
6)
7
8// AccessEntry represents access permissions for a resource
9type AccessEntry struct {
10 Type string `json:"type"` // "repository"
11 Name string `json:"name,omitempty"` // e.g., "alice/myapp"
12 Actions []string `json:"actions,omitempty"` // e.g., ["pull", "push"]
13}
14
15// ParseScope parses Docker registry scope strings into AccessEntry structures
16// Scope format: "repository:alice/myapp:pull,push"
17// Multiple scopes can be provided
18func ParseScope(scopes []string) ([]AccessEntry, error) {
19 var access []AccessEntry
20
21 for _, scope := range scopes {
22 if scope == "" {
23 continue
24 }
25
26 parts := strings.Split(scope, ":")
27 if len(parts) < 2 {
28 return nil, fmt.Errorf("invalid scope format: %s", scope)
29 }
30
31 resourceType := parts[0]
32
33 var name string
34 var actions []string
35
36 if len(parts) == 2 {
37 // Format: "repository:alice/myapp" (no actions specified)
38 name = parts[1]
39 } else if len(parts) == 3 {
40 // Format: "repository:alice/myapp:pull,push"
41 name = parts[1]
42 if parts[2] != "" {
43 actions = strings.Split(parts[2], ",")
44 }
45 } else {
46 return nil, fmt.Errorf("invalid scope format: %s", scope)
47 }
48
49 access = append(access, AccessEntry{
50 Type: resourceType,
51 Name: name,
52 Actions: actions,
53 })
54 }
55
56 return access, nil
57}
58
59// ValidateAccess checks if the requested access is allowed for the user
60// For ATCR, users can only push to repositories under their own handle/DID
61func ValidateAccess(userDID, userHandle string, access []AccessEntry) error {
62 for _, entry := range access {
63 if entry.Type != "repository" {
64 continue
65 }
66
67 // Allow wildcard scope (e.g., "repository:*:pull,push")
68 // This is used by Docker credential helpers to request broad permissions
69 // Actual authorization happens later when accessing specific repositories
70 if entry.Name == "*" {
71 continue
72 }
73
74 // Extract the owner from repository name (e.g., "alice/myapp" -> "alice")
75 parts := strings.SplitN(entry.Name, "/", 2)
76 if len(parts) < 1 {
77 return fmt.Errorf("invalid repository name: %s", entry.Name)
78 }
79
80 repoOwner := parts[0]
81
82 // Check if user is trying to access their own repository
83 // They can use either their handle or DID
84 if repoOwner != userHandle && repoOwner != userDID {
85 // For push/delete operations, strict ownership check
86 for _, action := range entry.Actions {
87 if action == "push" || action == "delete" {
88 return fmt.Errorf("user %s cannot %s to repository %s", userHandle, action, entry.Name)
89 }
90 }
91 }
92 }
93
94 return nil
95}