Files
at-container-registry/pkg/hold/pds/auth_test.go
T

588 lines
16 KiB
Go

package pds
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
"time"
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/atproto/auth/oauth"
)
// Tests for authorization functions in auth.go
// mockPDSClient is a mock HTTP client that simulates a PDS server
// It validates DPoP tokens and returns session information
type mockPDSClient struct{}
func (m *mockPDSClient) Do(req *http.Request) (*http.Response, error) {
// Verify request is for getSession endpoint
if !strings.Contains(req.URL.Path, "/xrpc/com.atproto.server.getSession") {
return &http.Response{
StatusCode: http.StatusNotFound,
Body: http.NoBody,
}, nil
}
// Verify DPoP headers are present
authHeader := req.Header.Get("Authorization")
dpopHeader := req.Header.Get("DPoP")
if authHeader == "" || dpopHeader == "" {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: http.NoBody,
}, nil
}
// Extract access token from Authorization header
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "DPoP" {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: http.NoBody,
}, nil
}
accessToken := parts[1]
// Parse token to extract DID
did, _, err := extractDIDFromToken(accessToken)
if err != nil {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: http.NoBody,
}, nil
}
// Return session response
session := SessionResponse{
DID: did,
Handle: strings.Replace(did, "did:plc:", "", 1) + ".test",
}
body, _ := json.Marshal(session)
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(string(body))),
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
}
// DPoPTestHelper provides utilities for creating valid DPoP requests in tests
type DPoPTestHelper struct {
privKey atcrypto.PrivateKey
did string
pdsURL string
}
// NewDPoPTestHelper creates a new test helper for the given DID and PDS
func NewDPoPTestHelper(did, pdsURL string) (*DPoPTestHelper, error) {
// Generate a test P-256 key (required for OAuth DPoP)
// Note: ATProto uses K-256 for DID keys, but OAuth DPoP requires P-256
privKey, err := atcrypto.GeneratePrivateKeyP256()
if err != nil {
return nil, fmt.Errorf("failed to generate key: %w", err)
}
return &DPoPTestHelper{
privKey: privKey,
did: did,
pdsURL: pdsURL,
}, nil
}
// CreateAccessToken creates a mock OAuth access token for testing
// This mimics what a real PDS would issue
func (h *DPoPTestHelper) CreateAccessToken() (string, error) {
// Create access token claims
claims := map[string]any{
"sub": h.did, // Subject (DID)
"iss": h.pdsURL, // Issuer (PDS URL)
"aud": "atcr", // Audience
"iat": time.Now().Unix(), // Issued at
"exp": time.Now().Add(1 * time.Hour).Unix(), // Expires in 1 hour
}
// For testing, we create a valid JWT structure without actually validating the signature
// The ValidateDPoPRequest in real use would validate this by calling the PDS
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"ES256K","typ":"JWT"}`))
payload, err := json.Marshal(claims)
if err != nil {
return "", fmt.Errorf("failed to marshal claims: %w", err)
}
encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
// Create a mock signature (in real use, the PDS validates this)
signature := base64.RawURLEncoding.EncodeToString([]byte("mock-signature-for-testing"))
tokenString := fmt.Sprintf("%s.%s.%s", header, encodedPayload, signature)
return tokenString, nil
}
// CreateDPoPProof creates a DPoP proof JWT for the given HTTP request
func (h *DPoPTestHelper) CreateDPoPProof(method, url string) (string, error) {
return oauth.NewAuthDPoP(method, url, "", h.privKey)
}
// AddDPoPToRequest adds proper DPoP headers to an HTTP request
func (h *DPoPTestHelper) AddDPoPToRequest(req *http.Request) error {
// Create access token
accessToken, err := h.CreateAccessToken()
if err != nil {
return fmt.Errorf("failed to create access token: %w", err)
}
// Create DPoP proof for this specific request
dpopProof, err := h.CreateDPoPProof(req.Method, req.URL.String())
if err != nil {
return fmt.Errorf("failed to create DPoP proof: %w", err)
}
// Add headers
req.Header.Set("Authorization", "DPoP "+accessToken)
req.Header.Set("DPoP", dpopProof)
return nil
}
// AddTestDPoP is a quick helper for common test case: owner with standard PDS
func AddTestDPoP(req *http.Request, did, pdsURL string) error {
helper, err := NewDPoPTestHelper(did, pdsURL)
if err != nil {
return err
}
return helper.AddDPoPToRequest(req)
}
// TestValidateBlobWriteAccess_Owner tests that the hold owner has write access
func TestValidateBlobWriteAccess_Owner(t *testing.T) {
pds, ctx := setupTestPDS(t)
ownerDID := "did:plc:owner123"
// Bootstrap with owner
err := pds.Bootstrap(ctx, ownerDID, true, false)
if err != nil {
t.Fatalf("Failed to bootstrap PDS: %v", err)
}
// Create DPoP helper for owner
dpopHelper, err := NewDPoPTestHelper(ownerDID, "https://test-pds.example.com")
if err != nil {
t.Fatalf("Failed to create DPoP helper: %v", err)
}
// Create request with proper DPoP tokens
req := httptest.NewRequest(http.MethodPost, "/test", nil)
if err := dpopHelper.AddDPoPToRequest(req); err != nil {
t.Fatalf("Failed to add DPoP to request: %v", err)
}
// Use mock PDS client
mockClient := &mockPDSClient{}
// Test owner has write access
user, err := ValidateBlobWriteAccess(req, pds, mockClient)
if err != nil {
t.Errorf("Expected owner to have write access, got error: %v", err)
}
if user == nil {
t.Fatal("Expected non-nil user")
}
if user.DID != ownerDID {
t.Errorf("Expected DID %s, got %s", ownerDID, user.DID)
}
if !user.Authorized {
t.Error("Expected user to be authorized")
}
}
// TestValidateBlobWriteAccess_CrewPermissions tests crew permission checking
func TestValidateBlobWriteAccess_CrewPermissions(t *testing.T) {
pds, ctx := setupTestPDS(t)
ownerDID := "did:plc:owner123"
// Bootstrap
err := pds.Bootstrap(ctx, ownerDID, true, false)
if err != nil {
t.Fatalf("Failed to bootstrap PDS: %v", err)
}
// Add crew member with blob:write permission
writerDID := "did:plc:writer123"
_, err = pds.AddCrewMember(ctx, writerDID, "writer", []string{"blob:write"})
if err != nil {
t.Fatalf("Failed to add crew member: %v", err)
}
// Add crew member without blob:write permission
readerDID := "did:plc:reader123"
_, err = pds.AddCrewMember(ctx, readerDID, "reader", []string{"blob:read"})
if err != nil {
t.Fatalf("Failed to add crew member: %v", err)
}
mockClient := &mockPDSClient{}
// Test writer (has blob:write permission) can write
t.Run("crew with blob:write can write", func(t *testing.T) {
dpopHelper, err := NewDPoPTestHelper(writerDID, "https://test-pds.example.com")
if err != nil {
t.Fatalf("Failed to create DPoP helper: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/test", nil)
if err := dpopHelper.AddDPoPToRequest(req); err != nil {
t.Fatalf("Failed to add DPoP to request: %v", err)
}
user, err := ValidateBlobWriteAccess(req, pds, mockClient)
if err != nil {
t.Errorf("Expected writer to have write access, got error: %v", err)
}
if user == nil || user.DID != writerDID {
t.Errorf("Expected user DID %s, got %v", writerDID, user)
}
})
// Test reader (no blob:write permission) cannot write
t.Run("crew without blob:write cannot write", func(t *testing.T) {
dpopHelper, err := NewDPoPTestHelper(readerDID, "https://test-pds.example.com")
if err != nil {
t.Fatalf("Failed to create DPoP helper: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/test", nil)
if err := dpopHelper.AddDPoPToRequest(req); err != nil {
t.Fatalf("Failed to add DPoP to request: %v", err)
}
_, err = ValidateBlobWriteAccess(req, pds, mockClient)
if err == nil {
t.Error("Expected reader without blob:write permission to be denied")
}
if !strings.Contains(err.Error(), "blob:write") {
t.Errorf("Expected error about blob:write permission, got: %v", err)
}
})
}
// TestValidateBlobReadAccess_PublicHold tests public hold access
func TestValidateBlobReadAccess_PublicHold(t *testing.T) {
pds, ctx := setupTestPDS(t)
ownerDID := "did:plc:owner123"
// Bootstrap with public=true
err := pds.Bootstrap(ctx, ownerDID, true, false)
if err != nil {
t.Fatalf("Failed to bootstrap PDS: %v", err)
}
// Verify captain record has public=true
_, captain, err := pds.GetCaptainRecord(ctx)
if err != nil {
t.Fatalf("Failed to get captain record: %v", err)
}
if !captain.Public {
t.Error("Expected public=true for captain record")
}
// Create request without auth headers (anonymous user)
req := httptest.NewRequest(http.MethodGet, "/test", nil)
// This should return nil (public access allowed) for public holds
user, err := ValidateBlobReadAccess(req, pds, nil)
if err != nil {
t.Errorf("Expected public access for public hold, got error: %v", err)
}
// nil user indicates public access
if user != nil {
t.Error("Expected nil user for public access")
}
}
// TestValidateBlobReadAccess_PrivateHold tests private hold access
func TestValidateBlobReadAccess_PrivateHold(t *testing.T) {
pds, ctx := setupTestPDS(t)
ownerDID := "did:plc:owner123"
// Bootstrap with public=false
err := pds.Bootstrap(ctx, ownerDID, false, false)
if err != nil {
t.Fatalf("Failed to bootstrap PDS: %v", err)
}
// Update captain to be private
_, err = pds.UpdateCaptainRecord(ctx, false, false)
if err != nil {
t.Fatalf("Failed to update captain record: %v", err)
}
// Verify captain record has public=false
_, captain, err := pds.GetCaptainRecord(ctx)
if err != nil {
t.Fatalf("Failed to get captain record: %v", err)
}
if captain.Public {
t.Error("Expected public=false for captain record")
}
// Create request without auth headers (anonymous user)
req := httptest.NewRequest(http.MethodGet, "/test", nil)
// This should return error (auth required) for private holds
user, err := ValidateBlobReadAccess(req, pds, nil)
if err == nil {
t.Error("Expected error for private hold without auth")
}
if user != nil {
t.Error("Expected nil user when auth fails")
}
}
// TestValidateOwnerOrCrewAdmin tests admin permission checking
func TestValidateOwnerOrCrewAdmin(t *testing.T) {
pds, ctx := setupTestPDS(t)
ownerDID := "did:plc:owner123"
// Bootstrap
err := pds.Bootstrap(ctx, ownerDID, true, false)
if err != nil {
t.Fatalf("Failed to bootstrap PDS: %v", err)
}
// Add crew member with crew:admin permission
adminDID := "did:plc:admin123"
_, err = pds.AddCrewMember(ctx, adminDID, "admin", []string{"crew:admin", "blob:write", "blob:read"})
if err != nil {
t.Fatalf("Failed to add crew admin: %v", err)
}
// Add crew member without crew:admin permission
writerDID := "did:plc:writer123"
_, err = pds.AddCrewMember(ctx, writerDID, "writer", []string{"blob:write"})
if err != nil {
t.Fatalf("Failed to add crew writer: %v", err)
}
// Verify crew records were created
crew, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("Failed to list crew members: %v", err)
}
// Verify admin has crew:admin permission
hasAdminPermission := false
for _, member := range crew {
if member.Record.Member == adminDID {
if slices.Contains(member.Record.Permissions, "crew:admin") {
hasAdminPermission = true
}
}
}
if !hasAdminPermission {
t.Error("Admin crew member should have crew:admin permission")
}
// Verify writer does NOT have crew:admin permission
writerHasAdminPermission := false
for _, member := range crew {
if member.Record.Member == writerDID {
if slices.Contains(member.Record.Permissions, "crew:admin") {
writerHasAdminPermission = true
}
}
}
if writerHasAdminPermission {
t.Error("Writer crew member should NOT have crew:admin permission")
}
// Test that function requires auth (will fail without DPoP tokens)
req := httptest.NewRequest(http.MethodPost, "/test", nil)
_, err = ValidateOwnerOrCrewAdmin(req, pds, nil)
if err == nil {
t.Error("Expected error for missing auth headers")
}
}
// TestCrewPermissions tests various permission combinations
func TestCrewPermissions(t *testing.T) {
pds, ctx := setupTestPDS(t)
ownerDID := "did:plc:owner123"
// Bootstrap
err := pds.Bootstrap(ctx, ownerDID, true, false)
if err != nil {
t.Fatalf("Failed to bootstrap PDS: %v", err)
}
tests := []struct {
name string
did string
role string
permissions []string
}{
{
name: "full admin",
did: "did:plc:fulladmin",
role: "admin",
permissions: []string{"crew:admin", "blob:write", "blob:read"},
},
{
name: "writer only",
did: "did:plc:writer",
role: "writer",
permissions: []string{"blob:write"},
},
{
name: "reader only",
did: "did:plc:reader",
role: "reader",
permissions: []string{"blob:read"},
},
{
name: "read-write",
did: "did:plc:readwrite",
role: "editor",
permissions: []string{"blob:read", "blob:write"},
},
}
// Add all crew members
for _, tt := range tests {
_, err := pds.AddCrewMember(ctx, tt.did, tt.role, tt.permissions)
if err != nil {
t.Fatalf("Failed to add crew member %s: %v", tt.name, err)
}
}
// Verify all crew members were created
crew, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("Failed to list crew members: %v", err)
}
// Should have: 1 owner (from bootstrap) + 4 test crew members
expectedCount := len(tests) + 1
if len(crew) != expectedCount {
t.Errorf("Expected %d crew members (owner + %d test members), got %d",
expectedCount, len(tests), len(crew))
}
// Verify each crew member has the expected permissions
for _, tt := range tests {
found := false
for _, member := range crew {
if member.Record.Member == tt.did {
found = true
// Check that all expected permissions are present
for _, expectedPerm := range tt.permissions {
hasPerm := slices.Contains(member.Record.Permissions, expectedPerm)
if !hasPerm {
t.Errorf("Crew member %s missing expected permission %s",
tt.name, expectedPerm)
}
}
// Verify role
if member.Record.Role != tt.role {
t.Errorf("Crew member %s has role %s, expected %s",
tt.name, member.Record.Role, tt.role)
}
}
}
if !found {
t.Errorf("Crew member %s not found in list", tt.name)
}
}
}
// TestCaptainRecordSettings tests captain record public/allowAllCrew settings
func TestCaptainRecordSettings(t *testing.T) {
tests := []struct {
name string
public bool
allowAllCrew bool
}{
{
name: "public hold, crew approval required",
public: true,
allowAllCrew: false,
},
{
name: "public hold, open crew",
public: true,
allowAllCrew: true,
},
{
name: "private hold, crew approval required",
public: false,
allowAllCrew: false,
},
{
name: "private hold, open crew",
public: false,
allowAllCrew: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pds, ctx := setupTestPDS(t)
ownerDID := "did:plc:owner123"
// Bootstrap with specified settings
err := pds.Bootstrap(ctx, ownerDID, tt.public, tt.allowAllCrew)
if err != nil {
t.Fatalf("Failed to bootstrap PDS: %v", err)
}
// Verify captain record has expected settings
_, captain, err := pds.GetCaptainRecord(ctx)
if err != nil {
t.Fatalf("Failed to get captain record: %v", err)
}
if captain.Public != tt.public {
t.Errorf("Expected public=%v, got %v", tt.public, captain.Public)
}
if captain.AllowAllCrew != tt.allowAllCrew {
t.Errorf("Expected allowAllCrew=%v, got %v", tt.allowAllCrew, captain.AllowAllCrew)
}
if captain.Owner != ownerDID {
t.Errorf("Expected owner %s, got %s", ownerDID, captain.Owner)
}
})
}
}