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
plugin-security promotes the OLDEST human sys_user row, so an app that seeds a people directory grants platform admin to a row nobody can log in as #14348
Found while implementing #14157 (the dev-admin seed gating on user rows instead of on a login). Filed unassigned; this is a CODE READING at 909a441, not an end-to-end measurement — the composed boot was not driven.
The reading
bootstrapPlatformAdmin (packages/plugins/plugin-security/src/bootstrap-platform-admin.ts, the single posture) selects its promotion target as the OLDEST human sys_user row:
507 const allUsers = await tryFind(ql, 'sys_user', {}, 50);
508 const humanUsers = allUsers.filter(isHumanUser);
513 const target = oldestOf(humanUsers); // sorted by created_at, first wins
526 logger.info('[security] first user promoted to platform admin: ' + target.email)
isHumanUser there is the mirror of isHumanUserRow — it excludes only the legacy usr_system service account. A person declared in defineStack({ data }) is an ordinary human row with no sys_account, and the declarative seed is awaited inside AppPlugin.start(), so those rows are always OLDER than any account created at kernel:ready or later.
So on an app that seeds people, the reading is that admin_full_access is granted to person0@demo.example — a directory row with no credential, which nobody can sign in as — while the account that CAN sign in is not promoted. sys_user_permission_set then carries a grant that can never be exercised, and the loginable admin lands in an app it cannot read.
Two things make this worth a card rather than a footnote:
The grant is WRITTEN, not merely computed — an unusable row lands in the permission tables and stays there.
What to measure before fixing
Drive a real composed boot (objectstack dev) on an app whose defineStack({ data }) declares sys_user rows, then read sys_user_permission_set: which user_id holds admin_full_access, and does that user have any sys_account row?
Check the ordering: does bootstrapPlatformAdmin run before or after AuthPlugin's kernel:ready seed? The selection is over whatever population exists when it runs.
claimSeedOwnership hands seeded business records to the promoted admin, so a wrong target also mis-assigns ownership.
Direction, not a decision
The narrow reading of "first user" that the code comment gives ("the real admin login") suggests the target should be the oldest human that can actually authenticate — one with a sys_account row — which is the same row-versus-login correction #14157 made one package over. Adjacent but distinct: #11979 is about config-anchoring the single posture itself, not about which row it selects.
Found while implementing #14157 (the dev-admin seed gating on user rows instead of on a login). Filed unassigned; this is a CODE READING at 909a441, not an end-to-end measurement — the composed boot was not driven.
The reading
bootstrapPlatformAdmin(packages/plugins/plugin-security/src/bootstrap-platform-admin.ts, thesingleposture) selects its promotion target as the OLDEST humansys_userrow:isHumanUserthere is the mirror ofisHumanUserRow— it excludes only the legacyusr_systemservice account. A person declared indefineStack({ data })is an ordinary human row with nosys_account, and the declarative seed is awaited insideAppPlugin.start(), so those rows are always OLDER than any account created atkernel:readyor later.So on an app that seeds people, the reading is that
admin_full_accessis granted toperson0@demo.example— a directory row with no credential, which nobody can sign in as — while the account that CAN sign in is not promoted.sys_user_permission_setthen carries a grant that can never be exercised, and the loginable admin lands in an app it cannot read.Two things make this worth a card rather than a footnote:
maybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157 fixes. That card moved the dev-admin seed off "any human row" and onto "a login exists"; this site still treats a directory row as a candidate for the platform-admin grant. AftermaybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157 lands,objectstack devon a people-seeding app DOES mint a loginable admin — and on this reading that admin still would not be promoted, so the CLI's declared "then promote it to platform admin" is only half delivered.maybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157 deliberately did not touch it: choosing between "oldest human", "oldest human WITH an account", and "the account the seed just provisioned" is a design decision, not a mechanical repair.What to measure before fixing
objectstack dev) on an app whosedefineStack({ data })declaressys_userrows, then readsys_user_permission_set: whichuser_idholdsadmin_full_access, and does that user have anysys_accountrow?bootstrapPlatformAdminrun before or after AuthPlugin'skernel:readyseed? The selection is over whatever population exists when it runs.claimSeedOwnershiphands seeded business records to the promoted admin, so a wrong target also mis-assigns ownership.Direction, not a decision
The narrow reading of "first user" that the code comment gives ("the real admin login") suggests the target should be the oldest human that can actually authenticate — one with a
sys_accountrow — which is the same row-versus-login correction #14157 made one package over. Adjacent but distinct: #11979 is about config-anchoring thesingleposture itself, not about which row it selects.Generated by Claude Code
Generated by Claude Code