diff --git a/pkg/appview/middleware/registry.go b/pkg/appview/middleware/registry.go index 1249ff3..f17f601 100644 --- a/pkg/appview/middleware/registry.go +++ b/pkg/appview/middleware/registry.go @@ -302,7 +302,7 @@ func (nr *NamespaceResolver) Repository(ctx context.Context, name reference.Name // Support hyphen-encoded DIDs in image paths (e.g., did-plc-abc123/repo:tag) // OCI reference grammar doesn't allow colons in path components, so DIDs must // be encoded with hyphens instead: did:plc:abc123 → did-plc-abc123 - if decoded, ok := token.DecodeDIDFromHyphens(identityStr); ok { + if decoded, ok := auth.DecodeDIDFromHyphens(identityStr); ok { identityStr = decoded } diff --git a/pkg/auth/scope.go b/pkg/auth/scope.go index 93b517b..7428c62 100644 --- a/pkg/auth/scope.go +++ b/pkg/auth/scope.go @@ -56,6 +56,19 @@ func ParseScope(scopes []string) ([]AccessEntry, error) { 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 { @@ -79,6 +92,12 @@ func ValidateAccess(userDID, userHandle string, access []AccessEntry) error { 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 { diff --git a/pkg/auth/scope_test.go b/pkg/auth/scope_test.go index d383bc2..6954a0a 100644 --- a/pkg/auth/scope_test.go +++ b/pkg/auth/scope_test.go @@ -413,6 +413,40 @@ func TestValidateAccess_DIDAndHandleBothWork(t *testing.T) { } } +func TestValidateAccess_HyphenEncodedDID(t *testing.T) { + userDID := "did:plc:pddp4xt5lgnv2qsegbzzs4xg" + userHandle := "evan.atcr.io" + + // Hyphen-encoded DID should match (this is how DIDs appear in image paths) + access := []AccessEntry{ + { + Type: "repository", + Name: "did-plc-pddp4xt5lgnv2qsegbzzs4xg/charts/loom", + Actions: []string{"pull", "push"}, + }, + } + + err := ValidateAccess(userDID, userHandle, access) + if err != nil { + t.Errorf("Expected no error for hyphen-encoded DID, got: %v", err) + } + + // did:web hyphen-encoded + webDID := "did:web:example.com" + accessWeb := []AccessEntry{ + { + Type: "repository", + Name: "did-web-example.com/myapp", + Actions: []string{"push"}, + }, + } + + err = ValidateAccess(webDID, "example.com", accessWeb) + if err != nil { + t.Errorf("Expected no error for hyphen-encoded did:web, got: %v", err) + } +} + func TestValidateAccess_MixedActionsAndOwnership(t *testing.T) { userDID := "did:plc:alice123" userHandle := "alice.bsky.social" diff --git a/pkg/auth/token/handler.go b/pkg/auth/token/handler.go index c112236..8b7cbe3 100644 --- a/pkg/auth/token/handler.go +++ b/pkg/auth/token/handler.go @@ -284,7 +284,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // into username="did", password="plc:abc123:". Reconstructed here. func parseBasicAuthDID(username, password string) (string, string) { // Case 1: Hyphen-encoded DID (e.g., did-plc-abc123 or did-web-example.com) - if did, ok := DecodeDIDFromHyphens(username); ok { + if did, ok := auth.DecodeDIDFromHyphens(username); ok { return did, password } @@ -309,15 +309,3 @@ func parseBasicAuthDID(username, password string) (string, string) { return username, password } -// 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 -}