Implement multi-user roles and review attribution #22

Open
opened 2026-07-28 04:55:06 +00:00 by claude-bot · 1 comment

Context

UserRole exists and require_role guards routes, but the app is effectively
single-user: there is no user management, and decisions and comments are not surfaced
with their author. Phase 2 is where multiple family members review together, and shared
review without attribution makes disagreements impossible to trace.

Scope

Role management and visible attribution across the review surfaces.

Implementation notes

  • Define the role set concretely against the spec — at minimum viewer, reviewer, admin —
    and document exactly what each may do. Ambiguity here becomes an authorization bug later.
  • Admin-facing user management: list users, change roles, deactivate. Full admin console
    is v0.4.0; this is the minimum needed for a working multi-user deployment.
  • An admin must not be able to remove their own last admin role and lock the collection out.
  • Show the author and timestamp on every decision, comment, note edit, and manual evidence
    row. The data is already there — created_by is populated — it just is not surfaced.
  • Show attribution in the history tab so the sequence of who changed what is readable.
  • Deactivating a user must preserve their prior contributions. Review history is
    append-only; removing a person cannot rewrite what they decided.
  • Extend the authorization tests from #8 to cover the full role matrix.

Done when

  • Roles are documented with an explicit permission matrix
  • An admin can manage users and roles
  • The last admin cannot be demoted or removed
  • Decisions, comments, notes, and evidence display their author
  • Deactivation preserves history
  • The role matrix is covered by tests

References

  • backend/app/models/models.py (UserRole, created_by fields)
  • backend/app/auth/session.py (require_role)
  • docs/circa-spec.md Phase 2

Related: #8 (authorization tests).

## Context `UserRole` exists and `require_role` guards routes, but the app is effectively single-user: there is no user management, and decisions and comments are not surfaced with their author. Phase 2 is where multiple family members review together, and shared review without attribution makes disagreements impossible to trace. ## Scope Role management and visible attribution across the review surfaces. ## Implementation notes - Define the role set concretely against the spec — at minimum viewer, reviewer, admin — and document exactly what each may do. Ambiguity here becomes an authorization bug later. - Admin-facing user management: list users, change roles, deactivate. Full admin console is v0.4.0; this is the minimum needed for a working multi-user deployment. - An admin must not be able to remove their own last admin role and lock the collection out. - Show the author and timestamp on every decision, comment, note edit, and manual evidence row. The data is already there — `created_by` is populated — it just is not surfaced. - Show attribution in the history tab so the sequence of who changed what is readable. - Deactivating a user must preserve their prior contributions. Review history is append-only; removing a person cannot rewrite what they decided. - Extend the authorization tests from #8 to cover the full role matrix. ## Done when - [ ] Roles are documented with an explicit permission matrix - [ ] An admin can manage users and roles - [ ] The last admin cannot be demoted or removed - [ ] Decisions, comments, notes, and evidence display their author - [ ] Deactivation preserves history - [ ] The role matrix is covered by tests ## References - `backend/app/models/models.py` (`UserRole`, `created_by` fields) - `backend/app/auth/session.py` (`require_role`) - `docs/circa-spec.md` Phase 2 Related: #8 (authorization tests).
claude-bot added this to the v0.4.0 milestone 2026-07-28 04:55:06 +00:00
Author

Amended by the audit of 2026-07-28.

The current default role is a live vulnerability, not just future work (#56). User.role
defaults to UserRole.reviewer and upsert_from_provider passes no role — so every account that
completes OAuth gets write access to the entire collection, with no allowlist and no approval step.
reviewer can also reach POST /api/ingest, which is the arbitrary-write primitive in #55.

That fix belongs in v0.1.1 (#56), ahead of this issue.

Two further splits:

  • Display names (#107) — the attribution slice is small, independently valuable, and needed as
    soon as a second person touches the collection. It also removes the UUID disclosure that #57
    requires anyway.
  • Enforcement gaps that exist today (#73) — zero admin-only routes exist (_ADMIN is defined
    and never used), the viewer role is unreachable, and /api/ingest's docstring says
    "Admin-only in Phase 1" while the code enforces reviewer.
**Amended by the audit of 2026-07-28.** **The current default role is a live vulnerability, not just future work (#56).** `User.role` defaults to `UserRole.reviewer` and `upsert_from_provider` passes no role — so every account that completes OAuth gets write access to the entire collection, with no allowlist and no approval step. `reviewer` can also reach `POST /api/ingest`, which is the arbitrary-write primitive in #55. That fix belongs in v0.1.1 (#56), ahead of this issue. Two further splits: - **Display names (#107)** — the attribution slice is small, independently valuable, and needed as soon as a second person touches the collection. It also removes the UUID disclosure that #57 requires anyway. - **Enforcement gaps that exist today (#73)** — zero admin-only routes exist (`_ADMIN` is defined and never used), the `viewer` role is unreachable, and `/api/ingest`'s docstring says "Admin-only in Phase 1" while the code enforces reviewer.
Sign in to join this conversation.
No description provided.