No admin-only routes exist and the viewer role is unenforced #73
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: MEDIUM
The problem
The route × role matrix built during the audit found:
_ADMIN = Depends(require_role(UserRole.admin))is definedat
backend/app/api/routes/ingest.py:27and never used.require_role(UserRole.admin)appearson no route. The admin role currently grants exactly one extra capability: deleting other
users' comments.
viewerrole is unreachable. No user is ever assigned it, and no route distinguishesit from reviewer for reads.
docs/circa-spec.md§4.2 ("read-only access to approved photosonly") is entirely unimplemented — a viewer, if one existed, could read
pendinganddisputedphotos./api/ingest's own docstring says "Admin-only in Phase 1" while the code enforcesrequire_role(reviewer, admin). Given this is the arbitrary-write route, the docstring isprobably the intent and the code is the bug.
Fix
/api/ingestto admin, matching its docstring and the spec.This overlaps #22 (multi-user roles and attribution), which builds the full model. Keep this
issue for the enforcement gaps that exist today; #22 can build on it.
Done when
References
backend/app/api/routes/ingest.py:27,49backend/app/auth/session.py(require_role)docs/circa-spec.md§4.2Related: #8, #22.
Fixed in
5f2cbdb. 144 tests passing.All three gaps from the audit's route matrix.
Ingest is now admin-only. Its docstring said "Admin-only in Phase 1" while the code enforced
reviewer. Since this is the arbitrary-write primitive from #55, the docstring was right and thecode was wrong — and that mismatch is what made the RCE chain reachable by any account the IdP
accepted.
Viewer reads are enforced. Viewers see approved photos only, per spec §4.2. Two details:
the difference between the two responses is exactly the information being protected.
statusparameter, and an explicitrequest for a non-approved status returns empty rather than erroring — a viewer asking for
disputed photos gets nothing, not a hint that such photos exist.
Admin routes exist.
/api/userssupports listing and role/activation changes. This was notoptional once #56 made
viewerthe default: without it there is no path from "someone logged in"to "someone can review", and the spec's "role assignments are managed by an Admin in the UI"
was unimplementable.
Lockout guard. The last active admin cannot be demoted or deactivated. Checked before either
change is applied, because demotion and deactivation are two routes to the same outcome — and
admin_exists()filters onis_active, so the two guards agree with each other.All role changes emit
user_role_changedaudit events with before/after context. Migration 002 addsthat enum value; on SQLite
sa.Enumis a CHECK constraint so it rebuilds the table, and itsdowngrade refuses if any such event exists rather than deleting append-only history.
Tested: the full role matrix, viewer scoping across all four photo statuses, ingest refusing a
reviewer, and both lockout paths.