-
Notifications
You must be signed in to change notification settings - Fork 0
ADR 0003 ownership based authorization
Status: Accepted
Before real authentication existed, every route trusted a client-supplied
owner_id with no verification, and almost no route filtered by it anyway —
of every route touching a specific collection or profile, only three
actually checked ownership. Any caller could read, edit, or delete any other
user's data by UUID. This needed a real fix, but the deployment target is a
small, self-hosted, single-organization instance (a handful of known users,
not a multi-tenant SaaS) — not a system that needs teams, shared workspaces,
or per-resource ACLs.
Two roles only: user (owns their own collections/profiles/tokens, has
read-only access to anything another user marked public) and admin
(bypasses ownership entirely, for oversight). No teams, no organizations, no
granular per-resource sharing beyond the existing public/private binary that
already existed on Collection.visibility/Profile.is_public. The first
person to ever register becomes admin automatically; ADMIN_EMAILS promotes
others afterward.
- Full RBAC with per-resource permissions — rejected as disproportionate to the actual deployment shape. It would mean new tables, a permissions model, and UI for managing grants, for a use case that doesn't need collaboration finer-grained than "mine" / "everyone can see this."
- Teams/organizations grouping users — rejected for the same reason; explicitly out of scope per the decision that prompted this work.
-
Enforce visibility with a database-level policy (Postgres RLS) —
considered and rejected for now: the application-layer check
(
authorize_access()/owner_or_public_clause()) is sufficient given there's exactly one, trusted, first-party client of the database (the backend itself). RLS would add real value if a second, less-trusted service ever queried the same database directly.
- Authorization logic is small and centralized: two functions
(
backend/app/core/authz.py), used consistently across every route — see invariants.md. - Denied access always returns 404, not 403, everywhere, by construction — a resource's existence is never revealed to someone who can't access it.
-
The gap this doesn't cover: a public profile can reference a private
collection, and compiling that profile exposes the private collection's
artifacts to anyone who can see the public profile —
compile_profile()doesn't re-check per-collection visibility. This is an accepted trade-off of "no granular sharing," documented explicitly in invariants.md rather than silently left as a surprise. - Adding real team/shared-workspace support later means introducing a new
primitive (not stretching
is_admin/ownership to fake it) — this ADR is the marker for "we knew this was out of scope, here's why."
Generated from docs/ by scripts/sync_wiki.py. Back to repo
- Home
- Architecture
- Data Model
- Invariants
- Extending MyACE
- Debugging
-
ADR Index
- ADR-0001-canonical-ir-as-markdown-with-frontmatter
- ADR-0002-session-cookie-auth
- ADR-0003-ownership-based-authorization
- ADR-0004-github-export-via-rest-api
- ADR-0005-email-password-baseline-auth
- ADR-0006-encrypted-admin-editable-secrets
- ADR-0007-additive-user-role-column
- ADR-0008-collection-moderation-state-machine
- ADR-0009-manifest-based-drift-detection
- ADR-0010-structured-handoff-field
- ADR-0011-public-demo-sandbox
- ADR-0012-manual-collection-freshness-verification
- ADR-0013-post-hoc-unpublish
- Adapter Research