Properties
category: reference tags: - phase-2 - acl - middleware last_updated: 2026-03-13
P2-3: ACL Enforcement Middleware
Branch: feat/P2-3-acl-enforcement (from phase-2)
Status: Complete — 21/21 tests passing
Deliverables
app/auth/permissions.py
- String constants:
READ,WRITE,UPLOAD,ADMIN ROLE_PERMISSIONSdict mapping owner/editor/viewer to permission tuplespermissions_for_role(role)— returns tuple or raisesValueErrorformat_permission_header(permissions)— comma-separated string
app/auth/acl.py
AclEnforcerclass with constructor injection (acl_model,wiki_model)check_access(user_id, wiki_id)— looks up ACL entry, returns{role, permissions}or raisesAuthError(403)check_public_access(wiki_id)— parsesowner_id:wiki_slug, checksis_publicflag, returns READ or raisescheck_bearer_token(token)— delegates toWikiModel.scan_by_token(), returns editor-level permissions or raisesAuthError(401)
app/auth/headers.py
build_proxy_headers(email, name, permissions)— returns dict withx-otterwiki-name,x-otterwiki-email,x-otterwiki-permissions
app/models/wiki.py (modified)
- Added
scan_by_token(plaintext_token)— scans all wikis withmcp_token_hash, checks each withbcrypt.checkpw(), handles pagination - Added
bcryptandAttrimports
tests/test_acl.py
- 21 tests using moto for DynamoDB mocking
- Covers: role mapping (4), format_permission_header (4), check_access (4), check_public_access (3), check_bearer_token (3), build_proxy_headers (3)
Design Decisions
- Bearer token validation scans all wikis then bcrypt-checks each (bcrypt salts are non-deterministic)
wiki_idformat:{owner_id}:{wiki_slug}— parsed by_parse_wiki_id()helper- Token-authenticated requests get editor-level permissions (READ, WRITE, UPLOAD)
- No new dependencies beyond
bcrypt(already needed for token hashing)