Instance owner/operator role (env-declared via OWNER_EMAIL) #240

Merged
justin merged 1 commits from instance-owner into main 2026-06-09 23:17:09 -04:00
Owner

Gives a self-hosted Provenance instance a real owner/operator, which it previously lacked — ownership was only per-tree, so no account could administer the instance.

Model

  • OWNER_EMAIL (comma-separated) names the instance owner(s). Derived at request time — no DB column, no migration, cannot drift from the env, survives DB resets.
  • Ownership requires a verified email (independent of REQUIRE_EMAIL_VERIFICATION). Registration is open, so without this someone could seize the role by registering the owner address first; verification ties it to inbox control.
  • GET /api/v1/admin/instance (owner-only): version, env, user/tree counts, configured AI providers. No tree data or PII — instance ownership is an operator role, not a privacy-engine bypass (documented invariant).
  • /users/me reports is_instance_owner; frontend adds an owner-only /admin page + conditional sidebar link (server-enforced via the InstanceOwner dependency, not just client-hidden).

Security review (ran before merge)

A 3-lens adversarial review (privacy-bypass / authz-edgecases / surface) with per-finding verification surfaced 8 findings; 2 confirmed and fixed in this PR:

  1. Owner land-grab (medium) — is_instance_owner matched on email without checking verification → fixed by requiring email_verified_at, plus a regression test.
  2. Admin page null-deref (low) — crashed on 401/5xx instead of failing closed → now renders a graceful owner-only/unavailable state.
    The other 6 were rejected as false alarms (e.g. the /admin/instance aggregates were confirmed PII-free).

Tests

tests/test_instance_owner.py: owner matching (case-insensitive), the verified-email land-grab guard, /users/me, owner-only /admin. Suite 96 passing.

No migration. Activating it on a deployment is just OWNER_EMAIL=<you> in .env + a restart (and a verified account).

🤖 Generated with Claude Code

Gives a self-hosted Provenance instance a real **owner/operator**, which it previously lacked — ownership was only per-tree, so no account could administer the instance. ### Model - `OWNER_EMAIL` (comma-separated) names the instance owner(s). **Derived at request time** — no DB column, no migration, cannot drift from the env, survives DB resets. - Ownership requires a **verified email** (independent of `REQUIRE_EMAIL_VERIFICATION`). Registration is open, so without this someone could seize the role by registering the owner address first; verification ties it to inbox control. - `GET /api/v1/admin/instance` (owner-only): version, env, user/tree counts, configured AI providers. **No tree data or PII** — instance ownership is an operator role, *not* a privacy-engine bypass (documented invariant). - `/users/me` reports `is_instance_owner`; frontend adds an owner-only `/admin` page + conditional sidebar link (**server-enforced** via the `InstanceOwner` dependency, not just client-hidden). ### Security review (ran before merge) A 3-lens adversarial review (privacy-bypass / authz-edgecases / surface) with per-finding verification surfaced 8 findings; 2 confirmed and **fixed in this PR**: 1. **Owner land-grab** (medium) — `is_instance_owner` matched on email without checking verification → fixed by requiring `email_verified_at`, plus a regression test. 2. **Admin page null-deref** (low) — crashed on 401/5xx instead of failing closed → now renders a graceful owner-only/unavailable state. The other 6 were rejected as false alarms (e.g. the `/admin/instance` aggregates were confirmed PII-free). ### Tests `tests/test_instance_owner.py`: owner matching (case-insensitive), the verified-email land-grab guard, `/users/me`, owner-only `/admin`. **Suite 96 passing.** No migration. Activating it on a deployment is just `OWNER_EMAIL=<you>` in `.env` + a restart (and a verified account). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
justin added 1 commit 2026-06-09 23:17:02 -04:00
Provenance had no system-level owner: ownership was only per-tree
(TreeMembership), so a self-hosted instance had no operator account and no
instance-admin surface. This adds one, declared by environment per the project's
twelve-factor rule.

- OWNER_EMAIL (comma-separated): the account(s) named here are instance owners.
  Derived at request time — no DB column, no migration, can't drift from the env,
  survives DB resets. is_instance_owner()/InstanceOwner dependency in api/deps.py.
- Ownership requires a VERIFIED email (independent of REQUIRE_EMAIL_VERIFICATION).
  Registration is open, so without this an attacker could seize the role by
  registering the owner address first; verification ties it to inbox control.
- GET /api/v1/admin/instance (owner-only): operational status — version, env,
  user/tree counts, configured AI providers. Deliberately exposes no tree data
  or PII: instance ownership is an operator role, NOT a privacy-engine bypass.
- /users/me reports is_instance_owner; frontend gains an owner-only /admin page
  and a conditional sidebar link (server-enforced, not just client-hidden).

Found-and-fixed by an adversarial security review before merge: the
verified-email land-grab (above) and a frontend null-deref where the admin page
crashed on 401/5xx instead of failing closed.

Docs: .env.example + ARCHITECTURE (notes the not-a-privacy-bypass boundary and
the verified-email requirement). Tests: owner matching, the land-grab guard,
/users/me, and owner-only /admin. Suite 96 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
justin merged commit 15504ba6e1 into main 2026-06-09 23:17:09 -04:00
justin deleted branch instance-owner 2026-06-09 23:17:09 -04:00
Sign in to join this conversation.