Every OAuth login is auto-provisioned as reviewer #56
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: CRITICAL
The bug
backend/app/models/models.py:162:upsert_from_provider(backend/app/repositories/users.py:38-50) creates users with no roleargument, so every account that completes OAuth becomes a reviewer. There is no allowlist,
no approval step, no pending state, and no endpoint anywhere to change a user's role.
revieweris not a limited role in this application. It can create decisions on any photo,overwrite notes on any photo, create manual evidence, supersede any evidence row by id, comment,
enqueue jobs, and reach
POST /api/ingest— which is the arbitrary-write primitive in thecompanion issue. "Can log in" currently equals "can destroy the collection."
Context for this deployment
The IdP is self-hosted Authentik, so the exposure is bounded by Authentik's registration policy
rather than being open to the whole internet. That reduces severity from the worst case but does
not remove it: it still means anyone who can register with Authentik gets write access to the
family archive, which is almost certainly not the intent, and it couples two systems whose
trust levels should differ.
Contradicts the spec
docs/circa-spec.md§4.2 definesvieweras "read-only access to approved photos only" andstates "all role assignments are managed by an Admin in the UI after bootstrap." The
implementation inverts this — the most privileged non-admin role is the default, and the viewer
role is unreachable.
Fix
UserRole.viewer.CIRCA_ALLOWED_EMAILSand/orCIRCA_ALLOWED_EMAIL_DOMAINS; reject logins matching neitherbefore the user row is created.
is_active=Falseand require an admin to activate.get_current_useralready enforcesis_active, so the mechanism exists and is unused.usertable for accounts that should not be there.Done when
viewerReferences
backend/app/models/models.py:162backend/app/repositories/users.py:38-50backend/app/api/routes/auth.py:52-57docs/circa-spec.md§4.2Related: #22 builds out the full role model; this issue is the live vulnerability underneath it.
Fixed in
6268f9e. CI green.What changed
Default role is now
viewer(models.py). No migration is required: the column has noserver_default, so the default is applied client-side at INSERT. Existing rows keep whateverrole they already have — see the action item below.
Account creation is gated before any row is written (
auth.py, beforeupsert_from_provider):Settings.is_email_allowed()denies by default. An address is permitted if it is inCIRCA_ALLOWED_EMAILS, matches a domain inCIRCA_ALLOWED_EMAIL_DOMAINS, or appears inCIRCA_FIRST_ADMIN_EMAILS— naming someone an admin plainly implies they may log in, and requiringthem to be listed twice would be a footgun.
With none of the three configured, logins are permitted in development and refused in
production. That keeps local setup frictionless while an unconfigured deployment fails closed
rather than open. Note the development fallback applies only to the unconfigured case: configuring
an allowlist enforces it everywhere, which is covered by a test.
Tests
backend/tests/test_access_control.py. Beyond the obvious cases:attacker@evil-example.comandattacker@example.com.evil.netmust notsatisfy an
example.comallowlist@on a domain entry is toleratedSettingsis constructed with every relevant field set explicitly, so aCIRCA_*variable in theambient environment or on CI cannot change a result.
Writing these caught a real subtlety worth recording:
default=on a mapped column applies atINSERT, not on attribute access.
User().roleisNoneuntil flushed, so the first version ofthe role test passed vacuously against a broken implementation. It now goes through a session, with
a declarative check on
User.__table__.c.role.default.argalongside it.Action item for you
Audit the existing
usertable. This fix stops new unauthorised accounts; it does notdemote anyone already provisioned as a reviewer. Worth running before the app goes back online:
Related
Still open in this milestone: #73 (no admin-only routes exist, viewer role is unenforced for reads),
#60 (
email_verifiedis not checked before the email grants admin), #8 (the authorization matrixthese changes should be regression-tested against).