mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-26 04:04:15 +00:00
251 lines
6.7 KiB
Markdown
251 lines
6.7 KiB
Markdown
# 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`):
|
|
```json
|
|
{
|
|
"$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}`):
|
|
```json
|
|
{
|
|
"$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
|
|
|
|
```bash
|
|
# 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:**
|
|
```bash
|
|
# 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:**
|
|
```bash
|
|
atproto delete-record \
|
|
--pds https://hold.example.com \
|
|
--collection io.atcr.hold.crew \
|
|
--rkey "{memberDID}"
|
|
```
|
|
|
|
**List crew members:**
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# Owner only
|
|
HOLD_PUBLIC=false
|
|
HOLD_ALLOW_ALL_CREW=false
|
|
# No additional crew records needed - captain has implicit access
|
|
```
|
|
|
|
### 2. Team Hold (Shared)
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```json
|
|
{
|
|
"$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:
|
|
|
|
```json
|
|
{
|
|
"$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:
|
|
|
|
```bash
|
|
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
|
|
|
|
- [EMBEDDED_PDS.md](./EMBEDDED_PDS.md) - Embedded PDS architecture details
|
|
- [BYOS.md](./BYOS.md) - BYOS deployment and usage
|
|
- [ATProto Lexicon Spec](https://atproto.com/specs/lexicon)
|