Files
at-container-registry/docs/CREW_ACCESS_CONTROL.md
T

6.7 KiB

Hold Crew Access Control

Overview

ATCR uses crew-based access control for hold (storage) services. Crew records are stored in the hold's embedded PDS (not the owner's or user's PDS), making the hold a self-contained ATProto actor with its own access control.

Current Implementation

Records in Hold's PDS

Captain record - Hold ownership (single record at io.atcr.hold.captain/self):

{
  "$type": "io.atcr.hold.captain",
  "owner": "did:plc:alice123",
  "public": false,
  "deployedAt": "2025-10-14T...",
  "region": "iad",
  "provider": "fly.io"
}

Crew records - Access control (one per member at io.atcr.hold.crew/{rkey}):

{
  "$type": "io.atcr.hold.crew",
  "member": "did:plc:bob456",
  "role": "admin",
  "permissions": ["blob:read", "blob:write"],
  "addedAt": "2025-10-14T..."
}

Authorization Logic

Write authorization follows this priority:

isAuthorizedWrite(userDID):
  1. If userDID == captain.owner → ALLOW
  2. If crew record exists for userDID → ALLOW
  3. Default → DENY

Read authorization depends on HOLD_PUBLIC setting:

  • Public hold (HOLD_PUBLIC=true): Anonymous + all authenticated users can read
  • Private hold (HOLD_PUBLIC=false): Requires crew membership for reads

Configuration

# Access control environment variables
HOLD_PUBLIC=false  # Require authentication for reads
HOLD_ALLOW_ALL_CREW=false  # Only explicit crew members can write

Crew Management

Crew records are managed by the hold captain (owner) using standard ATProto operations on the hold's embedded PDS:

Add crew member:

# Via hold's PDS (requires captain's OAuth)
atproto put-record \
  --pds https://hold.example.com \
  --collection io.atcr.hold.crew \
  --rkey "{memberDID}" \
  --value '{
    "$type": "io.atcr.hold.crew",
    "member": "did:plc:bob456",
    "role": "admin",
    "permissions": ["blob:read", "blob:write"],
    "addedAt": "2025-10-14T12:00:00Z"
  }'

Remove crew member:

atproto delete-record \
  --pds https://hold.example.com \
  --collection io.atcr.hold.crew \
  --rkey "{memberDID}"

List crew members:

# Via XRPC
GET https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo={holdDID}&collection=io.atcr.hold.crew

Authentication Flow

1. User pushes image to atcr.io/alice/myapp

2. AppView gets service token from alice's PDS:
   GET /xrpc/com.atproto.server.getServiceAuth?aud={holdDID}
   Response: { "token": "..." }

3. AppView calls hold with service token:
   POST /xrpc/io.atcr.hold.initiateUpload
   Authorization: Bearer {serviceToken}

4. Hold validates service token:
   - Checks token is from alice's PDS
   - Extracts alice's DID from token

5. Hold checks crew membership:
   - Queries its own PDS: com.atproto.repo.getRecord
   - Collection: io.atcr.hold.crew
   - Record key: alice's DID

6. If crew record found → allow upload
   Else → deny with 403 Forbidden

Trust model: "Trust but verify"

  • User OAuth'd to AppView (proves identity)
  • Service token from user's PDS (proves AppView is acting on behalf of user)
  • Crew record in hold's PDS (proves user has access to this hold)

Use Cases

1. Personal Hold (Private)

# Owner only
HOLD_PUBLIC=false
HOLD_ALLOW_ALL_CREW=false
# No additional crew records needed - captain has implicit access

2. Team Hold (Shared)

# Multiple team members
HOLD_PUBLIC=false
HOLD_ALLOW_ALL_CREW=false

# Captain adds crew members:
# - did:plc:alice (admin)
# - did:plc:bob (member)
# - did:plc:charlie (member)

3. Public Hold (Community)

# Allow any authenticated user (TODO: Implement HOLD_ALLOW_ALL_CREW)
HOLD_PUBLIC=true
HOLD_ALLOW_ALL_CREW=true

Planned Features

Pattern-Based Access Control

Status: Planned but not yet implemented.

Concept: Allow crew records with pattern matching instead of explicit DIDs:

{
  "$type": "io.atcr.hold.crew",
  "memberPattern": "*.example.com",
  "role": "write"
}

Use cases:

  • "*" - Allow all authenticated users
  • "*.company.com" - Allow all users from company domain
  • "*.community.social" - Allow all community members

Implementation needed:

  • Add memberPattern field to crew record schema (make member optional)
  • Add handle resolution (DID → handle lookup)
  • Add pattern matching logic
  • Update authorization to check patterns

Barred List (Access Revocation)

Status: Planned but not yet implemented.

Concept: Explicit deny list that overrides crew membership:

{
  "$type": "io.atcr.hold.crew.barred",
  "member": "did:plc:former-employee",
  "reason": "No longer with company",
  "barredAt": "2025-10-13T12:00:00Z"
}

Priority: Barred list checked before crew list.

HOLD_ALLOW_ALL_CREW

Status: Environment variable exists but full implementation pending.

Concept: Automatically create/manage wildcard crew record via env var:

HOLD_ALLOW_ALL_CREW=true  # Creates crew record with memberPattern: "*"

Implementation needed:

  • Auto-create wildcard crew record on startup if env=true
  • Auto-delete wildcard crew record if env changes to false
  • Use well-known rkey "allow-all" for managed record

Architecture Notes

Why Hold's Embedded PDS?

Key insight: Crew records are shared data about the hold, not user-specific data.

Benefits:

  • Self-contained: Hold is independent ATProto actor
  • Portable: Hold can move without coordinating with user PDSs
  • Discoverable: Query hold's PDS to see who has access
  • Standard: Uses normal ATProto sync endpoints (subscribeRepos, getRecord, listRecords)

Comparison:

  • User's PDS: Stores user-specific data (manifests, sailor profile)
  • Hold's PDS: Stores hold-specific data (captain, crew, configuration)
  • Clear separation of concerns

Security Considerations

  1. Public Records: Crew records are public (anyone can see who has access to a hold)
  2. Service Tokens: Hold trusts user's PDS to issue valid service tokens
  3. DID-Based: Crew membership is DID-based (permanent), not handle-based
  4. Captain Control: Only captain can modify crew records (via OAuth to hold's PDS)

Future Improvements

  1. Crew management UI - Web interface for adding/removing crew members
  2. Pattern-based matching - Implement memberPattern field
  3. Barred list - Implement access revocation
  4. Role-based permissions - Fine-grained permissions beyond read/write
  5. Temporary access - Time-limited crew membership (expiresAt field)
  6. Audit logging - Track access grants/denials

References