Skip to content

[Design] Re-anchor platform-admin: admin_full_access becomes a kernel metadata declaration; WHO holds it comes from env-configured verified emails — retiring the org-less row anchor #11663

Description

@os-zhuang

Chartered by maintainer ruling, 2026-08-24, live PM chat, verbatim: 「立卡,优先处理,并评估有没有相关任务要一起处理」 — with the design direction proposed by the maintainer in the same session: 「admin_full_access 能否在内核中用元数据约定,然后具体谁是超级管理员通过环境变量配置邮箱?」. Direction is ruled; this card designs the details and sequences the implementation. Design-first: no implementation dispatch until the design is accepted.

The problem (business form, from the #2734#10103#11532 lineage)

The platform's own super-admin right is anchored on a data row inside the tenant data model: an org-less admin_full_access row in sys_permission_set, referenced by row id from an unscoped sys_user_permission_set grant (resolve-authz-context.ts §6b), seeded ~1.3s before the first organization exists. It is the last structural exception to the ruled walled-tenancy doctrine ("every row belongs to exactly one organization"; #10103 Option C). Costs: single-point fragility (touch the row → platform lockout or silent re-mint), a recurring tax on every sweep/gate/diagnostic (three ruling rounds spent on this family already), and an asterisk in the security story.

Ruled direction

  1. The permission set's CONTENT is a kernel metadata declaration — code-declared, versioned, spec-validated, PR-reviewed. No runtime row holds the super-admin capability set; nothing can delete or mutate it at runtime. (Mainstream precedent: Keycloak KEYCLOAK_ADMIN, GitLab root bootstrap, Grafana GF_SECURITY_ADMIN_*, Kubernetes deployment-credential superuser — operator-level admin comes from deployment config, never from business data.)
  2. The HOLDER comes from deployment config: an env variable listing admin email(s). Platform-admin derivation becomes: caller's verified email ∈ configured list → PLATFORM_ADMIN with the declared capability set.

Five security pins the design must nail (from the chartering discussion)

  1. Verified-email match only — the env entry confers nothing until an account holding that email has passed verification; normalize (lowercase) and match exactly. Otherwise registering the configured address is a privilege escalation.
  2. Empty config = fail closed — no platform admin exists; the development-mode dev-admin seed stays development-only.
  3. Auditability — the resolved admin list is surfaced read-only (Setup / discovery / health) since there is no grant row left to query.
  4. Revocation = config change + reload — acceptable for this role; cloud's control plane injects per-deployment env, which fits.
  5. Loud migration — existing deployments carrying the row-id grant: derivation prefers env; a detected legacy grant logs a deprecation pointing at the config path; removal of the legacy path is a sequenced follow-up, never a silent dual-track.

Related tasks assessed for joint handling (maintainer asked; triage's reading)

cardrelationsequencing
#10348 (ruled C today, pm:queue)Consolidates the two stray plugin-auth judges onto resolve-authz-context.ts — the very function this design rewiresLand #10348 first: then the derivation changes in exactly one place. The design card's implementation must not start before it.
#10949 (pm:blocked, live authz defects in those judges)May be partially absorbed by #10348-CRe-priced there (already noted on #10348).
objectstack#11211 (merged: walled owner-email elevation) + the #10103 "env-door projection"An email/env elevation mechanism already partially exists.⚠️ Premise-first for the designer: measure the landed mechanism and unify onto it — one email door, not two parallel ones.
cloud#1509 (P0: first self-registrant becomes platform_admin, bypassing invite-only)The env-email anchor is the structural fix for this class: self-registration must never confer platform admin; only configured emails doDesign must state the walled bootstrap flow end-to-end; a cloud-side leg follows the design (coordinate with the cloud seat's #1614#1509 train — do not collide with its in-flight acceptance).
#11532 (closed today: honest diagnostic)The 8 org-less rows' retirement becomes possible once nothing references admin_full_access by row idSeeders stop minting + reap of the 8 rows is the implementation's final leg (the other 7 rows have per-org copies already; verify zero readers before reaping).

Not related despite proximity: #11579 (auth settings pre-bind), #11627 (MySQL hash keys), #10556 (sinks).

Discipline

Priority: maintainer-ordered front-of-queue (priority:p0 per the verbatim above — jump authorization recorded; ⛔ does not exempt claim protocol or serial constraints). Clause-②: yes (changes authorization derivation — the widest contract there is); contract-review tier mandatory on the implementation. Deliverable of THIS card: the design document (options where real choices remain — e.g. env spelling, multi-admin, reload semantics — with a four-axis pass), then split implementation cards per surface (packages/core derivation · kernel declaration · plugin-security seeders/reap · migration · cloud leg).

Refs: #10103 (ruling + fork analysis) · #11532 (measured constraints: row-id derivation, bootstrap-before-first-org) · #10348 (consolidation prerequisite) · cloud#1406/#1509 · #2734 · ADR-0068 · ADR-0120 D3.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions