Something went wrong. Try again.
A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
Something went wrong. Try again.
12 kB · 485 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486package auth
import ( "strings" "testing")
func TestParseScope_Valid(t *testing.T) { tests := []struct { name string scopes []string expectedCount int expectedType string expectedName string expectedActions []string }{ { name: "repository with actions", scopes: []string{"repository:alice/myapp:pull,push"}, expectedCount: 1, expectedType: "repository", expectedName: "alice/myapp", expectedActions: []string{"pull", "push"}, }, { name: "repository without actions", scopes: []string{"repository:alice/myapp"}, expectedCount: 1, expectedType: "repository", expectedName: "alice/myapp", expectedActions: nil, }, { name: "wildcard repository", scopes: []string{"repository:*:pull,push"}, expectedCount: 1, expectedType: "repository", expectedName: "*", expectedActions: []string{"pull", "push"}, }, { name: "empty scope ignored", scopes: []string{""}, expectedCount: 0, }, { name: "multiple scopes", scopes: []string{"repository:alice/app1:pull", "repository:alice/app2:push"}, expectedCount: 2, expectedType: "repository", expectedName: "alice/app1", expectedActions: []string{"pull"}, }, { name: "single action", scopes: []string{"repository:alice/myapp:pull"}, expectedCount: 1, expectedType: "repository", expectedName: "alice/myapp", expectedActions: []string{"pull"}, }, { name: "three actions", scopes: []string{"repository:alice/myapp:pull,push,delete"}, expectedCount: 1, expectedType: "repository", expectedName: "alice/myapp", expectedActions: []string{"pull", "push", "delete"}, }, // Note: DIDs with colons cannot be used directly in scope strings due to // the colon delimiter. This is a known limitation. { name: "empty actions string", scopes: []string{"repository:alice/myapp:"}, expectedCount: 1, expectedType: "repository", expectedName: "alice/myapp", expectedActions: nil, }, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { access, err := ParseScope(tt.scopes) if err != nil { t.Fatalf("ParseScope() error = %v", err) }
if len(access) != tt.expectedCount { t.Errorf("Expected %d access entries, got %d", tt.expectedCount, len(access)) return }
if tt.expectedCount > 0 { entry := access[0] if entry.Type != tt.expectedType { t.Errorf("Expected type %q, got %q", tt.expectedType, entry.Type) } if entry.Name != tt.expectedName { t.Errorf("Expected name %q, got %q", tt.expectedName, entry.Name) } if len(entry.Actions) != len(tt.expectedActions) { t.Errorf("Expected %d actions, got %d", len(tt.expectedActions), len(entry.Actions)) } for i, expectedAction := range tt.expectedActions { if i < len(entry.Actions) && entry.Actions[i] != expectedAction { t.Errorf("Expected action[%d] = %q, got %q", i, expectedAction, entry.Actions[i]) } } } }) }}
func TestParseScope_Invalid(t *testing.T) { tests := []struct { name string scopes []string }{ { name: "missing colon", scopes: []string{"repository"}, }, { name: "too many parts", scopes: []string{"repository:name:actions:extra"}, }, { name: "single part only", scopes: []string{"invalid"}, }, { name: "four colons", scopes: []string{"a:b:c:d:e"}, }, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := ParseScope(tt.scopes) if err == nil { t.Error("Expected error for invalid scope format") } if !strings.Contains(err.Error(), "invalid scope") { t.Errorf("Expected error message to contain 'invalid scope', got: %v", err) } }) }}
func TestParseScope_SpecialCharacters(t *testing.T) { tests := []struct { name string scope string expectedName string }{ { name: "hyphen in name", scope: "repository:alice-bob/my-app:pull", expectedName: "alice-bob/my-app", }, { name: "underscore in name", scope: "repository:alice_bob/my_app:pull", expectedName: "alice_bob/my_app", }, { name: "dot in name", scope: "repository:alice.bsky.social/myapp:pull", expectedName: "alice.bsky.social/myapp", }, { name: "numbers in name", scope: "repository:user123/app456:pull", expectedName: "user123/app456", }, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { access, err := ParseScope([]string{tt.scope}) if err != nil { t.Fatalf("ParseScope() error = %v", err) }
if len(access) != 1 { t.Fatalf("Expected 1 access entry, got %d", len(access)) }
if access[0].Name != tt.expectedName { t.Errorf("Expected name %q, got %q", tt.expectedName, access[0].Name) } }) }}
func TestParseScope_MultipleScopes(t *testing.T) { scopes := []string{ "repository:alice/app1:pull", "repository:alice/app2:push", "repository:bob/app3:pull,push", }
access, err := ParseScope(scopes) if err != nil { t.Fatalf("ParseScope() error = %v", err) }
if len(access) != 3 { t.Fatalf("Expected 3 access entries, got %d", len(access)) }
// Verify first entry if access[0].Name != "alice/app1" { t.Errorf("Expected first name %q, got %q", "alice/app1", access[0].Name) } if len(access[0].Actions) != 1 || access[0].Actions[0] != "pull" { t.Errorf("Expected first actions [pull], got %v", access[0].Actions) }
// Verify second entry if access[1].Name != "alice/app2" { t.Errorf("Expected second name %q, got %q", "alice/app2", access[1].Name) } if len(access[1].Actions) != 1 || access[1].Actions[0] != "push" { t.Errorf("Expected second actions [push], got %v", access[1].Actions) }
// Verify third entry if access[2].Name != "bob/app3" { t.Errorf("Expected third name %q, got %q", "bob/app3", access[2].Name) } if len(access[2].Actions) != 2 { t.Errorf("Expected third entry to have 2 actions, got %d", len(access[2].Actions)) }}
func TestValidateAccess_Owner(t *testing.T) { userDID := "did:plc:alice123" userHandle := "alice.bsky.social"
tests := []struct { name string repoName string actions []string shouldErr bool errorMsg string }{ { name: "owner can push to own repo (by handle)", repoName: "alice.bsky.social/myapp", actions: []string{"push"}, shouldErr: false, }, { name: "owner can push to own repo (by DID)", repoName: "did:plc:alice123/myapp", actions: []string{"push"}, shouldErr: false, }, { name: "owner cannot push to others repo", repoName: "bob.bsky.social/myapp", actions: []string{"push"}, shouldErr: true, errorMsg: "cannot push", }, { name: "wildcard scope allowed", repoName: "*", actions: []string{"push", "pull"}, shouldErr: false, }, { name: "owner can pull from others repo", repoName: "bob.bsky.social/myapp", actions: []string{"pull"}, shouldErr: false, }, { name: "owner cannot delete others repo", repoName: "bob.bsky.social/myapp", actions: []string{"delete"}, shouldErr: true, errorMsg: "cannot delete", }, { name: "multiple actions with push fails for others", repoName: "bob.bsky.social/myapp", actions: []string{"pull", "push"}, shouldErr: true, }, { name: "empty repository name", repoName: "", actions: []string{"push"}, shouldErr: true, }, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { access := []AccessEntry{ { Type: "repository", Name: tt.repoName, Actions: tt.actions, }, }
err := ValidateAccess(userDID, userHandle, access) if tt.shouldErr && err == nil { t.Error("Expected error but got none") } if !tt.shouldErr && err != nil { t.Errorf("Expected no error but got: %v", err) } if tt.shouldErr && err != nil && tt.errorMsg != "" { if !strings.Contains(err.Error(), tt.errorMsg) { t.Errorf("Expected error to contain %q, got: %v", tt.errorMsg, err) } } }) }}
func TestValidateAccess_NonRepositoryType(t *testing.T) { userDID := "did:plc:alice123" userHandle := "alice.bsky.social"
// Non-repository types should be ignored access := []AccessEntry{ { Type: "registry", Name: "something", Actions: []string{"admin"}, }, }
err := ValidateAccess(userDID, userHandle, access) if err != nil { t.Errorf("Expected non-repository types to be ignored, got error: %v", err) }}
func TestValidateAccess_EmptyAccess(t *testing.T) { userDID := "did:plc:alice123" userHandle := "alice.bsky.social"
err := ValidateAccess(userDID, userHandle, nil) if err != nil { t.Errorf("Expected no error for empty access, got: %v", err) }
err = ValidateAccess(userDID, userHandle, []AccessEntry{}) if err != nil { t.Errorf("Expected no error for empty access slice, got: %v", err) }}
func TestValidateAccess_InvalidRepositoryName(t *testing.T) { userDID := "did:plc:alice123" userHandle := "alice.bsky.social"
// Repository name without slash - invalid format access := []AccessEntry{ { Type: "repository", Name: "justareponame", Actions: []string{"push"}, }, }
err := ValidateAccess(userDID, userHandle, access) if err != nil { // Should fail because can't extract owner from name without slash // and it's not "*", so it will try to access [0] which is the whole string // This is expected behavior - validate that owner check happens t.Logf("Got expected validation error: %v", err) }}
func TestValidateAccess_DIDAndHandleBothWork(t *testing.T) { userDID := "did:plc:alice123" userHandle := "alice.bsky.social"
// Test with handle as owner accessByHandle := []AccessEntry{ { Type: "repository", Name: "alice.bsky.social/myapp", Actions: []string{"push"}, }, }
err := ValidateAccess(userDID, userHandle, accessByHandle) if err != nil { t.Errorf("Expected no error for handle match, got: %v", err) }
// Test with DID as owner accessByDID := []AccessEntry{ { Type: "repository", Name: "did:plc:alice123/myapp", Actions: []string{"push"}, }, }
err = ValidateAccess(userDID, userHandle, accessByDID) if err != nil { t.Errorf("Expected no error for DID match, got: %v", err) }}
func TestValidateAccess_MixedActionsAndOwnership(t *testing.T) { userDID := "did:plc:alice123" userHandle := "alice.bsky.social"
// Mix of own and others' repositories access := []AccessEntry{ { Type: "repository", Name: "alice.bsky.social/myapp", Actions: []string{"push", "pull"}, }, { Type: "repository", Name: "bob.bsky.social/bobapp", Actions: []string{"pull"}, // OK - just pull }, }
err := ValidateAccess(userDID, userHandle, access) if err != nil { t.Errorf("Expected no error for valid mixed access, got: %v", err) }
// Now add push to someone else's repo - should fail access = []AccessEntry{ { Type: "repository", Name: "alice.bsky.social/myapp", Actions: []string{"push"}, }, { Type: "repository", Name: "bob.bsky.social/bobapp", Actions: []string{"push"}, // FAIL - can't push to others }, }
err = ValidateAccess(userDID, userHandle, access) if err == nil { t.Error("Expected error when trying to push to others' repository") }}
func TestParseScope_EmptyActionsArray(t *testing.T) { // Test with empty actions (colon present but no actions after it) access, err := ParseScope([]string{"repository:alice/myapp:"}) if err != nil { t.Fatalf("ParseScope() error = %v", err) }
if len(access) != 1 { t.Fatalf("Expected 1 entry, got %d", len(access)) }
// Actions should be nil or empty when actions string is empty if len(access[0].Actions) > 0 { t.Errorf("Expected nil or empty actions, got %v", access[0].Actions) }}
func TestParseScope_NilInput(t *testing.T) { access, err := ParseScope(nil) if err != nil { t.Fatalf("ParseScope() with nil input error = %v", err) }
if len(access) != 0 { t.Errorf("Expected empty access for nil input, got %d entries", len(access)) }}