Permission Drift Audit
Verify that permissions remain correct as objects evolve through their lifecycle. Permissions are not static: they change when teams change, projects are duplicated, visibility is toggled, users are removed, and URLs are shared. This audit catches the class of bugs where access controls were correct at creation but have drifted out of alignment with intent.
## Key Points
1. User A is the owner of Project X with Assets, Scenes, and Settings.
2. Add User B as a collaborator on Project X.
3. Verify User B can access: Project X, its Assets, its Scenes, its Settings.
4. Remove User B as collaborator.
5. Immediately test User B's access to each resource.
- [ ] User B cannot access Project X (403 or 404).
- [ ] User B cannot access any Asset in Project X.
- [ ] User B cannot access any Scene in Project X.
- [ ] User B cannot access Project X Settings.
- [ ] User B cannot access Project X via direct URL.
- [ ] User B cannot access Project X via API with known IDs.
- [ ] User B does not see Project X in any listing (projects, search, recent).
## Quick Example
```
[ ] Duplication creates new ACL records (deep copy)
[ ] ACL entries reference the new project ID (not old)
[ ] No shared foreign keys between original and copy ACLs
[ ] Duplication includes sub-resource ACLs (scenes, assets)
```
```
[ ] Permission cache TTL is short (< 5 minutes) or invalidated on change
[ ] OR permissions are checked live on every request (no cache)
[ ] Role change triggers cache invalidation for affected user
[ ] JWT tokens do NOT embed permissions (or have short TTL)
If permissions are in JWT: role change requires token refresh
```skilldb get production-audit-skills/permission-drift-auditFull skill: 449 linesPermission Drift Audit
Purpose
Verify that permissions remain correct as objects evolve through their lifecycle. Permissions are not static: they change when teams change, projects are duplicated, visibility is toggled, users are removed, and URLs are shared. This audit catches the class of bugs where access controls were correct at creation but have drifted out of alignment with intent.
Scope
| Area | What We Test |
|---|---|
| Team role changes | Do permission changes propagate immediately and completely? |
| Collaborator removal | Is access fully revoked across all resources? |
| Project duplication | Do copied projects inherit correct (not original) permissions? |
| Visibility toggling | Does public/private actually control access? |
| URL-based access | Do signed/shared URLs expire correctly? |
| Cross-tenant isolation | Can users in Tenant A access Tenant B's data? |
| Deleted user cleanup | Are deleted users' owned resources reassigned or secured? |
| Worker/service boundaries | Do background jobs respect tenant boundaries? |
Risk Pattern Table
| Pattern | What It Hits | Risk | Symptom |
|---|---|---|---|
| Stale role cache | Authorization | HIGH | User's role changed but old permissions still active |
| Missing cascade on collaborator removal | Data access | HIGH | Removed collaborator still accesses shared sub-resources |
| Duplicated project inherits ACL references | Data access | CRITICAL | Duplicate project shares ACL entries with original; changes affect both |
| Public URL not invalidated on visibility change | Data exposure | HIGH | Asset set to private but old public URL still works |
| Signed URL with excessive TTL | Data exposure | MEDIUM | Signed URL valid for 30 days; should be 1 hour |
| Cross-tenant query without tenant filter | Data exposure | CRITICAL | API endpoint fetches by ID without checking tenant ownership |
| Deleted user still owns resources | Access control | HIGH | Deleted user's projects orphaned; no one can manage them |
| Worker processes all tenants' data | Data exposure | HIGH | Background job processes items across tenants without isolation |
| API key with over-broad scope | Data exposure | HIGH | API key grants access to all projects instead of specific ones |
| Inherited permissions not recalculated | Authorization | MEDIUM | Nested resource permissions stale after parent permission change |
Concrete Test Cases
TEST-PD-001: Remove Collaborator, Verify Full Revocation
Objective: Verify that removing a collaborator immediately revokes all access.
Steps:
- User A is the owner of Project X with Assets, Scenes, and Settings.
- Add User B as a collaborator on Project X.
- Verify User B can access: Project X, its Assets, its Scenes, its Settings.
- Remove User B as collaborator.
- Immediately test User B's access to each resource.
Pass Criteria:
- User B cannot access Project X (403 or 404).
- User B cannot access any Asset in Project X.
- User B cannot access any Scene in Project X.
- User B cannot access Project X Settings.
- User B cannot access Project X via direct URL.
- User B cannot access Project X via API with known IDs.
- User B does not see Project X in any listing (projects, search, recent).
- Access revocation is immediate (no cache delay > 60 seconds).
Fail Criteria:
- User B can still access any sub-resource after removal.
- Access revocation takes more than 5 minutes.
- User B can access via API even though UI blocks.
- User B can see the project in search results.
TEST-PD-002: Duplicate Project, Inspect ACLs
Objective: Verify that duplicated projects have independent permissions.
Steps:
- User A owns Project X with collaborators B and C.
- User A duplicates Project X, creating Project X-Copy.
- Inspect Project X-Copy's collaborator list.
- Add collaborator D to Project X-Copy.
- Verify D does NOT have access to original Project X.
- Remove collaborator B from Project X.
- Verify B still has access to Project X-Copy (if they were copied).
Pass Criteria:
- Project X-Copy has its own independent collaborator list.
- Changes to X-Copy's collaborators do not affect Project X.
- Changes to Project X's collaborators do not affect X-Copy.
- Owner of X-Copy is the user who duplicated (not original owner, unless same user).
- Duplicated assets have their own ACL entries (not shared references).
Fail Criteria:
- X-Copy shares ACL entries with Project X (pointer, not copy).
- Adding D to X-Copy also gives D access to Project X.
- Removing B from X also removes B from X-Copy.
Implementation Check:
[ ] Duplication creates new ACL records (deep copy)
[ ] ACL entries reference the new project ID (not old)
[ ] No shared foreign keys between original and copy ACLs
[ ] Duplication includes sub-resource ACLs (scenes, assets)
TEST-PD-003: Change Visibility, Verify URL Access
Objective: Verify that changing resource visibility immediately affects access.
Steps:
- Create a project/asset with public visibility.
- Access it via public URL (unauthenticated). Confirm access.
- Change visibility to private.
- Access the same public URL (unauthenticated). Confirm denied.
- Change back to public. Confirm access restored.
Pass Criteria:
- Public -> Private: URL returns 403 or 404 within 60 seconds.
- Private -> Public: URL returns 200 within 60 seconds.
- CDN/cache does not serve stale public content after going private.
- Embedded URLs (in emails, shared links) respect current visibility.
- Search engines excluded from private content (noindex, robots.txt).
CDN Cache Check:
[ ] CDN cache is purged when visibility changes
[ ] OR CDN respects origin's auth headers (no caching of private content)
[ ] OR signed URLs used instead of public URLs (inherently controlled)
[ ] Cache-Control headers set correctly:
Public: Cache-Control: public, max-age=3600
Private: Cache-Control: private, no-cache, no-store
TEST-PD-004: Signed URL Expiry Verification
Objective: Verify that signed/temporary URLs expire as configured.
Steps:
- Generate a signed URL for a private asset.
- Access it immediately. Confirm access.
- Wait for the TTL to expire (or manipulate the expiry parameter).
- Access the same URL. Confirm denied.
Pass Criteria:
- Signed URL works before expiry.
- Signed URL returns 403 after expiry.
- TTL is appropriate for use case:
- Preview/thumbnail: 1 hour
- Download link: 15 minutes to 1 hour
- API response URLs: 1 hour
- Shared links: configurable, max 7 days
- Signed URL cannot be modified (changing expiry or path invalidates signature).
Signed URL Audit:
| URL Type | TTL | Renewable? | Revocable? | Assessment |
|----------|-----|-----------|-----------|------------|
| Asset preview | 1 hour | On page load | On delete | OK |
| Download link | 15 min | On click | On delete | OK |
| API response | 1 hour | Per request | On visibility change | CHECK |
| Shared link | 7 days | Manual | Manual | REVIEW TTL |
| Export file | 24 hours | No | On expiry | OK |
TEST-PD-005: Cross-Tenant Data Isolation
Objective: Verify that users in one tenant cannot access another tenant's data.
Steps:
- Create resources in Tenant A (User A).
- Log in as User B (Tenant B).
- Attempt to access Tenant A's resources via:
- Direct URL with known IDs
- API calls with known IDs
- Search functionality
- Shared/public listings
Pass Criteria:
- Every API endpoint filters by tenant (WHERE tenant_id = current_tenant).
- Direct ID access returns 404 (not 403) for cross-tenant resources.
- 404 is preferred over 403 to avoid confirming resource existence.
- Search results only include current tenant's resources.
- No listing endpoint leaks cross-tenant data.
- Background jobs process within tenant boundaries.
Implementation Verification:
-- EVERY query must include tenant filter:
-- BAD:
SELECT * FROM projects WHERE id = ?;
-- GOOD:
SELECT * FROM projects WHERE id = ? AND tenant_id = ?;
-- Verify with query log analysis:
-- Search for queries missing tenant_id filter
-- Check: are there any SELECT/UPDATE/DELETE without tenant_id in WHERE clause?
Cross-Tenant Test Matrix:
| Endpoint | Method | Tenant A ID | As Tenant B | Expected | Actual |
|----------|--------|------------|-------------|----------|--------|
| /api/projects/:id | GET | proj_A_1 | User B | 404 | |
| /api/assets/:id | GET | asset_A_1 | User B | 404 | |
| /api/projects/:id | PUT | proj_A_1 | User B | 404 | |
| /api/projects/:id | DELETE | proj_A_1 | User B | 404 | |
| /api/projects | GET | - | User B | [] (empty, no A projects) | |
| /api/search?q=... | GET | - | User B | No A results | |
TEST-PD-006: Deleted User Resource Handling
Objective: Verify that deleting a user does not leave orphaned or inaccessible resources.
Steps:
- User A owns 3 projects with assets and collaborators.
- Delete User A's account.
- Check: what happened to User A's projects?
Pass Criteria (one of):
- Projects transferred to organization admin.
- Projects transferred to a designated successor.
- Projects soft-deleted with grace period for recovery.
- Collaborators notified of ownership change.
Fail Criteria:
- Projects exist but no one can manage them (orphaned).
- Projects disappear without warning to collaborators.
- User A's data is partially deleted (some resources remain, some don't).
- User A's user record deleted but referenced by foreign keys (crashes on access).
Cleanup Verification:
[ ] User deletion cascades to: sessions, API keys, tokens
[ ] User deletion transfers or archives: owned resources
[ ] User deletion removes: collaborator memberships, team memberships
[ ] User deletion does NOT delete: shared projects owned by others
[ ] Audit log records who deleted the account and when
[ ] Foreign key references are either cascaded or SET NULL (not dangling)
TEST-PD-007: Role Change Propagation
Objective: Verify that changing a user's role immediately updates their permissions.
Steps:
- User B has "editor" role on Project X (can edit, cannot delete).
- Change User B's role to "viewer" (can view, cannot edit).
- Immediately test: can User B still edit?
- Change User B's role to "admin" (can edit, delete, manage).
- Immediately test: can User B now delete?
Pass Criteria:
- Role change takes effect within 60 seconds.
- No cached permissions allow old role's actions.
- Role change is reflected in UI (buttons enabled/disabled).
- API enforces new role immediately (not just UI).
- Active sessions are updated (no need to re-login).
Permission Cache Audit:
[ ] Permission cache TTL is short (< 5 minutes) or invalidated on change
[ ] OR permissions are checked live on every request (no cache)
[ ] Role change triggers cache invalidation for affected user
[ ] JWT tokens do NOT embed permissions (or have short TTL)
If permissions are in JWT: role change requires token refresh
TEST-PD-008: Worker Tenant Boundary Verification
Objective: Verify that background workers respect tenant boundaries.
Steps:
- Enqueue jobs for multiple tenants.
- Inspect worker processing: does it access resources across tenants?
- Verify: worker queries include tenant filters.
Pass Criteria:
- Worker receives tenant_id in job payload.
- All worker DB queries filter by tenant_id.
- Worker cannot access storage buckets of other tenants.
- Worker logs include tenant_id for every operation.
- Failure in one tenant's job does not affect another tenant's jobs.
Permission Model Documentation Template
ROLES:
owner: Full access. Can delete project, manage members.
admin: Can edit, configure, manage members. Cannot delete project.
editor: Can create and edit content. Cannot manage members.
viewer: Read-only access. Cannot modify anything.
RESOURCES & PERMISSIONS:
| Resource | owner | admin | editor | viewer |
|----------|-------|-------|--------|--------|
| Project settings | CRUD | CRUD | R | R |
| Project members | CRUD | CRUD | R | R |
| Assets | CRUD | CRUD | CRUD | R |
| Scenes | CRUD | CRUD | CRUD | R |
| Generation jobs | CRUD | CRUD | CRU | R |
| Billing | CRUD | R | - | - |
| Delete project | D | - | - | - |
INHERITANCE:
Project access grants access to all child resources (assets, scenes).
Removing project access removes all child access.
Child resources do NOT have independent ACLs (inherited from project).
Post-Audit Checklist
[ ] Collaborator removal immediately revokes all access (including sub-resources)
[ ] Project duplication creates independent ACL entries
[ ] Visibility changes take effect within 60 seconds (including CDN)
[ ] Signed URLs expire correctly and cannot be tampered with
[ ] Cross-tenant access is impossible via any endpoint
[ ] Deleted users' resources are transferred or archived (not orphaned)
[ ] Role changes take effect immediately (cache invalidated)
[ ] Background workers filter by tenant on every query
[ ] Permission model is documented and matches implementation
[ ] No API endpoint is missing authorization check
[ ] 404 returned for cross-tenant access (not 403)
[ ] JWT tokens have short TTL if they embed permissions
What Earlier Audits Miss
Security testing verifies that access controls exist. This audit matters because:
- Penetration tests check if unauthenticated users can access protected resources. They rarely test if a removed collaborator retains access.
- Unit tests verify permission checks at the function level. They miss that a project duplication shares ACL references with the original.
- Integration tests verify permissions at creation time. They never re-verify after role changes, collaborator removal, or visibility toggles.
- Code reviews check that authorization middleware exists on endpoints but miss that a new endpoint was added without it.
- Compliance audits verify policy documentation but not that the code matches the documented permission model.
This would be called a Permission Drift Audit -- specifically testing whether access controls remain correct when teams change, projects are duplicated, visibility is toggled, users are deleted, and URLs are shared.
Automation Opportunities
| Test | Automatable? | Method |
|---|---|---|
| TEST-PD-001: Collaborator revocation | YES | Add user, remove user, attempt access via API, assert 403/404 |
| TEST-PD-002: Duplicate ACLs | YES | Duplicate project, modify copy ACLs, assert original unchanged |
| TEST-PD-003: Visibility change | YES | Toggle public/private, attempt unauthenticated access, assert correct |
| TEST-PD-004: Signed URL expiry | YES | Generate signed URL, wait for TTL, attempt access, assert 403 |
| TEST-PD-005: Cross-tenant isolation | YES | Attempt cross-tenant access on all endpoints, assert 404 |
| TEST-PD-006: Deleted user handling | PARTIAL | Delete user, verify resources transferred/archived |
| TEST-PD-007: Role propagation | YES | Change role, immediately test permission boundaries |
| TEST-PD-008: Worker tenant boundary | PARTIAL | Inspect worker query logs for tenant filter presence |
# Automated cross-tenant isolation test
TENANT_A_TOKEN="..."
TENANT_B_TOKEN="..."
# Get a resource ID from tenant A
RESOURCE_ID=$(curl -s -H "Authorization: Bearer $TENANT_A_TOKEN" /api/projects | jq -r '.[0].id')
# Attempt access from tenant B
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TENANT_B_TOKEN" \
"/api/projects/$RESOURCE_ID")
[ "$HTTP_CODE" = "404" ] && echo "PASS: Cross-tenant blocked" || echo "FAIL: Got HTTP $HTTP_CODE"
# Test all endpoints systematically
for endpoint in "projects" "assets" "scenes" "settings"; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TENANT_B_TOKEN" \
"/api/$endpoint/$RESOURCE_ID")
echo "$endpoint: HTTP $CODE $([ "$CODE" = "404" ] && echo "PASS" || echo "FAIL")"
done
Reusable Audit Report Template
# Permission Drift Audit Report
## System: _______________
## Date: YYYY-MM-DD
## Auditor: _______________
## Permission Model Summary
| Role | Create | Read | Update | Delete | Manage Members |
|------|--------|------|--------|--------|----------------|
| Owner | | | | | |
| Admin | | | | | |
| Editor | | | | | |
| Viewer | | | | | |
## Test Results
| Test ID | Description | Result | Evidence |
|---------|-------------|--------|----------|
| TEST-PD-001 | Collaborator revocation | PASS/FAIL | Access after removal: ___ |
| TEST-PD-002 | Duplicate ACLs | PASS/FAIL | Shared ACL references: ___ |
| TEST-PD-003 | Visibility change | PASS/FAIL | Stale public access: yes/no |
| TEST-PD-004 | Signed URL expiry | PASS/FAIL | Access after TTL: yes/no |
| TEST-PD-005 | Cross-tenant isolation | PASS/FAIL | Endpoints leaking data: ___ |
| TEST-PD-006 | Deleted user handling | PASS/FAIL | Orphaned resources: ___ |
| TEST-PD-007 | Role propagation | PASS/FAIL | Stale permissions after change: yes/no |
| TEST-PD-008 | Worker tenant boundary | PASS/FAIL | Missing tenant filters: ___ |
## Score: PASS / PARTIAL / FAIL
Priority Targeting
Run this audit FIRST if:
- The system is multi-tenant
- Users can share resources with collaborators
- Visibility (public/private) can be toggled
- Role-based access control was recently modified
- User deletion was recently implemented
- Cross-tenant data leak would be a critical security incident
- Signed URLs are used for asset access
Install this skill directly: skilldb add production-audit-skills