Add an instance owner/operator role (env-declared via OWNER_EMAIL)

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>
This commit is contained in:
2026-06-09 23:16:45 -04:00
parent 6fbad3106d
commit c5631d3eab
15 changed files with 467 additions and 5 deletions
+81
View File
@@ -4187,6 +4187,28 @@
}
}
}
},
"/api/v1/admin/instance": {
"get": {
"tags": [
"admin"
],
"summary": "Instance Status",
"description": "Operator dashboard data. Requires the caller to be an instance owner.",
"operationId": "instance_status_api_v1_admin_instance_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InstanceStatus"
}
}
}
}
}
}
}
},
"components": {
@@ -5399,6 +5421,60 @@
],
"title": "ImportReport"
},
"InstanceStatus": {
"properties": {
"version": {
"type": "string",
"title": "Version"
},
"env": {
"type": "string",
"title": "Env"
},
"owner_emails": {
"items": {
"type": "string"
},
"type": "array",
"title": "Owner Emails"
},
"require_email_verification": {
"type": "boolean",
"title": "Require Email Verification"
},
"user_count": {
"type": "integer",
"title": "User Count"
},
"tree_count": {
"type": "integer",
"title": "Tree Count"
},
"default_llm_provider": {
"type": "string",
"title": "Default Llm Provider"
},
"ai_providers": {
"items": {
"$ref": "#/components/schemas/ConfiguredProvider"
},
"type": "array",
"title": "Ai Providers"
}
},
"type": "object",
"required": [
"version",
"env",
"owner_emails",
"require_email_verification",
"user_count",
"tree_count",
"default_llm_provider",
"ai_providers"
],
"title": "InstanceStatus"
},
"LoginRequest": {
"properties": {
"email": {
@@ -7194,6 +7270,11 @@
"type": "string",
"format": "date-time",
"title": "Created At"
},
"is_instance_owner": {
"type": "boolean",
"title": "Is Instance Owner",
"default": false
}
},
"type": "object",