You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
sys_user_position is apiEnabled and its names are unconstrained, so a tenant can mint a row spelling any ADR-0068 built-in identity name — platform_admin, org_owner, org_admin.
PR #15948 (card #15136) closes every in-repo reader that turned such a name into authority. ⛔ It does not stop the row existing. A future reader — or an existing one outside this repository — can reintroduce the hole, and nothing mechanical will catch it.
Provenance and what is measured
Found by the os-dev seat while reworking #15948 after its Clause-② review FAILED on exactly one instance of this class. ⭐ That instance is worth reading before working this card, because it shows how invisible the shape is:
isPlatformAdmin was derived as positions.includes(BUILTIN_IDENTITY_PLATFORM_ADMIN).
Measured on the real pipeline: a plain org member holding a sys_user_position row spelled platform_admin got isPlatformAdmin: true and passed judgePlatformAdmin, the /admin/* mount gate, while hasPlatformAdminStanding — the rung /sso/register and impersonation use — said false.
Reachable by a tenant admin (who clears the D12 gate outright) or a manageAssignments delegate, because the seeded platform_admin position has no bound sets, so assertAssignmentWrite's boundSets.every(...) passes vacuously.
Core's own resolver already forbids the form, in a comment at resolve-authz-context.ts:1125-1129: "Read the RUNG — never positions.includes(...); an ADR-0057 D4 sys_user_position row may spell that very name."
⚠️That ⛔ is a comment, not a gate. It was there the whole time and did not prevent the read.
Why the instance being fixed does not close the class
⚠️The name is still writable. Nothing at the write path refuses a sys_position name that collides with a built-in identity. Every defence today is a reader choosing to consult the rung.
⚠️Out-of-repo readers are unmeasured. The rework grepped isPlatformAdminUser / judgePlatformAdmin repo-wide with a firing control (51 in-package hits) and found no consumer outside plugin-auth — but that covers objectstack and objectui only. cloud was not reachable from that session, and a name-as-authority read there would be invisible to every gate in this repo. ⇒ A fleet sweep is owed, and is this card's first deliverable.
⭐ The card that created the exposure did not create the class.action.visible 的 current_user.positions 装的是 auth 角色而非安全层岗位,按岗位收敛的按钮对所有人静默消失(17.2.0) #15136's ruling A moved positions onto the security axis, which moved these strings into tenant-writable space. Before that, positions carried auth roles and no tenant could make the platform say platform_admin. So the ruling was right and it converted a dormant naming collision into a live one — worth stating plainly so nobody reads this card as an argument against A.
Direction, ⛔ not prescribed
A sys_positionname-collision guard, refusing the ADR-0068 built-in identity names at write time, would close it at the source rather than one reader at a time. ⚠️ That is a security-design call with real questions attached, which is why this is filed rather than fixed:
What happens to rows that already collide in a live deployment — refuse at write only, or migrate?
Is the reserved set exactly the ADR-0068 built-ins, or does it need org_* shapes too?
Does the guard belong at the object layer, the API layer, or both?
⛔ Do not close this by fixing readers. Readers are #15948's job and it did that; this card exists because the readers are not the invariant.
First deliverable for whoever takes it
Not a fix — a census across the fleet: every reader, in any repository, that treats a name in positions[] (or in a sys_user_position row) as authority rather than reading the capability rung. With a control that fires, because a zero-hit grep across an unfamiliar repository proves nothing.
The class, not the instance
sys_user_positionisapiEnabledand its names are unconstrained, so a tenant can mint a row spelling any ADR-0068 built-in identity name —platform_admin,org_owner,org_admin.PR #15948 (card #15136) closes every in-repo reader that turned such a name into authority. ⛔ It does not stop the row existing. A future reader — or an existing one outside this repository — can reintroduce the hole, and nothing mechanical will catch it.
Provenance and what is measured
Found by the
os-devseat while reworking #15948 after its Clause-② review FAILED on exactly one instance of this class. ⭐ That instance is worth reading before working this card, because it shows how invisible the shape is:isPlatformAdminwas derived aspositions.includes(BUILTIN_IDENTITY_PLATFORM_ADMIN).sys_user_positionrow spelledplatform_admingotisPlatformAdmin: trueand passedjudgePlatformAdmin, the/admin/*mount gate, whilehasPlatformAdminStanding— the rung/sso/registerand impersonation use — saidfalse.manageAssignmentsdelegate, because the seededplatform_adminposition has no bound sets, soassertAssignmentWrite'sboundSets.every(...)passes vacuously.resolve-authz-context.ts:1125-1129: "Read the RUNG — neverpositions.includes(...); an ADR-0057 D4sys_user_positionrow may spell that very name."Why the instance being fixed does not close the class
sys_positionname that collides with a built-in identity. Every defence today is a reader choosing to consult the rung.isPlatformAdminUser/judgePlatformAdminrepo-wide with a firing control (51 in-package hits) and found no consumer outsideplugin-auth— but that coversobjectstackandobjectuionly.cloudwas not reachable from that session, and a name-as-authority read there would be invisible to every gate in this repo. ⇒ A fleet sweep is owed, and is this card's first deliverable.positionsonto the security axis, which moved these strings into tenant-writable space. Before that,positionscarried auth roles and no tenant could make the platform sayplatform_admin. So the ruling was right and it converted a dormant naming collision into a live one — worth stating plainly so nobody reads this card as an argument against A.Direction, ⛔ not prescribed
A⚠️ That is a security-design call with real questions attached, which is why this is filed rather than fixed:
sys_positionname-collision guard, refusing the ADR-0068 built-in identity names at write time, would close it at the source rather than one reader at a time.org_*shapes too?⛔ Do not close this by fixing readers. Readers are #15948's job and it did that; this card exists because the readers are not the invariant.
First deliverable for whoever takes it
Not a fix — a census across the fleet: every reader, in any repository, that treats a name in
positions[](or in asys_user_positionrow) as authority rather than reading the capability rung. With a control that fires, because a zero-hit grep across an unfamiliar repository proves nothing.Related
positions[]is the security axis, not the better-auth role scalar #15948 — the instance, the ruling that exposed it, and the three-way-agreement pin (isPlatformAdmin/judgePlatformAdmin/hasPlatformAdminStandingmust agree) that now guards the fixed readers.'admin' in current_user.positions收敛选项,而admin从来不在服务端的 positions 轴上(membershipadmin被映射成org_admin) #15943 — a showcase object gating on'admin' in current_user.positions, whereadminis never on the server's axis. Opposite direction, same underlying confusion about what that array contains.