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_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)