Skip to main content
Film & TelevisionProduction Audit449 lines

Permission Drift Audit

Quick Summary35 lines
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 lines
Paste into your CLAUDE.md or agent config

Permission 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

AreaWhat We Test
Team role changesDo permission changes propagate immediately and completely?
Collaborator removalIs access fully revoked across all resources?
Project duplicationDo copied projects inherit correct (not original) permissions?
Visibility togglingDoes public/private actually control access?
URL-based accessDo signed/shared URLs expire correctly?
Cross-tenant isolationCan users in Tenant A access Tenant B's data?
Deleted user cleanupAre deleted users' owned resources reassigned or secured?
Worker/service boundariesDo background jobs respect tenant boundaries?

Risk Pattern Table

PatternWhat It HitsRiskSymptom
Stale role cacheAuthorizationHIGHUser's role changed but old permissions still active
Missing cascade on collaborator removalData accessHIGHRemoved collaborator still accesses shared sub-resources
Duplicated project inherits ACL referencesData accessCRITICALDuplicate project shares ACL entries with original; changes affect both
Public URL not invalidated on visibility changeData exposureHIGHAsset set to private but old public URL still works
Signed URL with excessive TTLData exposureMEDIUMSigned URL valid for 30 days; should be 1 hour
Cross-tenant query without tenant filterData exposureCRITICALAPI endpoint fetches by ID without checking tenant ownership
Deleted user still owns resourcesAccess controlHIGHDeleted user's projects orphaned; no one can manage them
Worker processes all tenants' dataData exposureHIGHBackground job processes items across tenants without isolation
API key with over-broad scopeData exposureHIGHAPI key grants access to all projects instead of specific ones
Inherited permissions not recalculatedAuthorizationMEDIUMNested 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:

  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.

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:

  1. User A owns Project X with collaborators B and C.
  2. User A duplicates Project X, creating Project X-Copy.
  3. Inspect Project X-Copy's collaborator list.
  4. Add collaborator D to Project X-Copy.
  5. Verify D does NOT have access to original Project X.
  6. Remove collaborator B from Project X.
  7. 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:

  1. Create a project/asset with public visibility.
  2. Access it via public URL (unauthenticated). Confirm access.
  3. Change visibility to private.
  4. Access the same public URL (unauthenticated). Confirm denied.
  5. 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:

  1. Generate a signed URL for a private asset.
  2. Access it immediately. Confirm access.
  3. Wait for the TTL to expire (or manipulate the expiry parameter).
  4. 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:

  1. Create resources in Tenant A (User A).
  2. Log in as User B (Tenant B).
  3. 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:

  1. User A owns 3 projects with assets and collaborators.
  2. Delete User A's account.
  3. 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:

  1. User B has "editor" role on Project X (can edit, cannot delete).
  2. Change User B's role to "viewer" (can view, cannot edit).
  3. Immediately test: can User B still edit?
  4. Change User B's role to "admin" (can edit, delete, manage).
  5. 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:

  1. Enqueue jobs for multiple tenants.
  2. Inspect worker processing: does it access resources across tenants?
  3. 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

TestAutomatable?Method
TEST-PD-001: Collaborator revocationYESAdd user, remove user, attempt access via API, assert 403/404
TEST-PD-002: Duplicate ACLsYESDuplicate project, modify copy ACLs, assert original unchanged
TEST-PD-003: Visibility changeYESToggle public/private, attempt unauthenticated access, assert correct
TEST-PD-004: Signed URL expiryYESGenerate signed URL, wait for TTL, attempt access, assert 403
TEST-PD-005: Cross-tenant isolationYESAttempt cross-tenant access on all endpoints, assert 404
TEST-PD-006: Deleted user handlingPARTIALDelete user, verify resources transferred/archived
TEST-PD-007: Role propagationYESChange role, immediately test permission boundaries
TEST-PD-008: Worker tenant boundaryPARTIALInspect 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

Get CLI access →