---
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)
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9