--- 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_PERMISSIONS` dict mapping owner/editor/viewer to permission tuples - `permissions_for_role(role)` — returns tuple or raises `ValueError` - `format_permission_header(permissions)` — comma-separated string ### `app/auth/acl.py` - `AclEnforcer` class with constructor injection (`acl_model`, `wiki_model`) - `check_access(user_id, wiki_id)` — looks up ACL entry, returns `{role, permissions}` or raises `AuthError(403)` - `check_public_access(wiki_id)` — parses `owner_id:wiki_slug`, checks `is_public` flag, returns READ or raises - `check_bearer_token(token)` — delegates to `WikiModel.scan_by_token()`, returns editor-level permissions or raises `AuthError(401)` ### `app/auth/headers.py` - `build_proxy_headers(email, name, permissions)` — returns dict with `x-otterwiki-name`, `x-otterwiki-email`, `x-otterwiki-permissions` ### `app/models/wiki.py` (modified) - Added `scan_by_token(plaintext_token)` — scans all wikis with `mcp_token_hash`, checks each with `bcrypt.checkpw()`, handles pagination - Added `bcrypt` and `Attr` imports ### `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_id` format: `{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)