Repository navigation
fix(platform-objects): name an organization membership, a two-factor enrollment and the SCIM join rows by the people and groups they point at - #30520
Conversation
…ookups point at Claude-Session: https://claude.ai/code/session_01Utp6CZdTcFgv9MUk1tpKUo Co-authored-by: Claude <noreply@anthropic.com>
…okups point at sys_member, sys_two_factor, sys_scim_projection_grant and sys_scim_group_member read their record title one hop through their lookups (ADR-0079 addendum 2026-10-18), as sys_business_unit_member does, instead of splicing the stored ids. Claude-Session: https://claude.ai/code/session_01Utp6CZdTcFgv9MUk1tpKUo Co-authored-by: Claude <noreply@anthropic.com>
…s on A membership read naming no columns computes every formula field, and sys_member's record title now reads the person one hop through user_id, a sys_user read and a round trip in every request's prelude. Claude-Session: https://claude.ai/code/session_01Utp6CZdTcFgv9MUk1tpKUo Co-authored-by: Claude <noreply@anthropic.com>
… prelude with a one-hop member title Claude-Session: https://claude.ai/code/session_01Utp6CZdTcFgv9MUk1tpKUo Co-authored-by: Claude <noreply@anthropic.com>
…r projects its membership reads Claude-Session: https://claude.ai/code/session_01Utp6CZdTcFgv9MUk1tpKUo Co-authored-by: Claude <noreply@anthropic.com>
…ness header Claude-Session: https://claude.ai/code/session_01Utp6CZdTcFgv9MUk1tpKUo Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 40 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b539479e3f712a496ce410706bc03848e9194f20 && git checkout b539479e3f712a496ce410706bc03848e9194f20
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa9c0aa2a0a8c2752f83b57765f3aae978ae196a 2d857e278e2a2ca27772105eb4e908fd7ca5d47e && git checkout -B drift-repro fa9c0aa2a0a8c2752f83b57765f3aae978ae196a && git merge --no-ff 2d857e278e2a2ca27772105eb4e908fd7ca5d47e
node scripts/docs-audit/affected-docs.mjs --json fa9c0aa2a0a8c2752f83b57765f3aae978ae196a
|
|
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb 维护者速读
席位意见:同意落地。席位逐个核对了三处投影列与调用方读到的字段:本人成员读用到 organization_id、role 和有效期窗口;组织成员首读只用 user_id;计数读用的是 user_id 加上前面四列。没有漏列。草稿 PR #30287 也改同一文件,但改的是另一段,谁后合谁 merge main。 席位核对Head
|
Requested by Bill · project thread
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb
维护者速读
packages/core/src/security/resolve-authz-context.ts的MEMBER_ROW_COLUMNS与三处sys_member读;packages/platform-objects/src/identity/sys-member.object.ts的display_title;resolve-authz-context.grant-projection.test.ts新增的成员投影 describe。pnpm --filter @objectstack/plugin-auth exec vitest run src/auth-prelude-query-count.pin.test.ts,预期 8 条全绿;在只改标题、不收窄读的树上其中 6 条红(sys_user 2 读变 4 读)。Fixes #30454
Clause-②: no — no exported member, accepted value or authorable key changes: four shipped platform objects' computed
display_titlenow serves names instead of ids, and the authorization resolver's internalsys_memberreads name their columnsMetadata-protocol: none — shipped platform metadata and an internal read, not what a metadata author writes
Skills-docs: none —
git grep -e sys_member -e sys_two_factor -e sys_scim_group_member -e sys_scim_projection_grant -- 'skills/**'hitsskills/objectstack-automation/SKILL.md:168andskills/objectstack-automation/evals/flows-triggers-approvals.json:194, both about a system-context write tosys_memberbeing refused, which stays trueWhat changes
1. Four record titles read names one hop through their lookups (ADR-0079 addendum 2026-10-18), in the form
sys_business_unit_memberandsys_team_memberuse. A person isisBlank(trim(record.user_id.name)) ? record.user_id.email : record.user_id.name. Each hop is read under the reader's authority; an unreadable or missing target leaves the title null, never an id. Each title keeps its shape, and the fields' labels and descriptions are unchanged, so no translation bundle moves.sys_memberUSER_ID (role)Ada Lovelace (admin);Ada Lovelacewith no rolesys_two_factorTwo-factor for USER_IDTwo-factor for Ada Lovelacesys_scim_projection_grantrole → USER_IDbilling_admin → Ada Lovelacesys_scim_group_memberSCIM_USER_ID in GROUP_IDada.lovelace@idp.example in OperationsFor the SCIM join, each hop names its target's own title pointer:
sys_scim_user.nameFieldisuser_nameandsys_scim_group.nameFieldisdisplay_name. Both are required stored text, so there is no second hop and no null guard. The pins assert this.2. The authorization resolver's
sys_memberreads name their columns (packages/core/src/security/resolve-authz-context.ts; seat ruling A, Claim amendment 6658119458). With no projection, a read computes every formula field.sys_member's title now reads the person, so each unprojected read added a batchedsys_userread: two per authenticated request, measured byauth-prelude-query-count.pin.test.ts(sys_user2 → 4, 5 of 7 red). Each read now names what its caller reads, asGRANT_ROW_COLUMNSalready does forsys_user_permission_set:{ user_id }, Leg 1):MEMBER_ROW_COLUMNS=organization_id,role,valid_from,valid_until. These are read byaccessible_org_ids(§3b),heldPositionNamesandnextGrantValidityBoundary;{ organization_id }, Leg 1):ORG_MEMBER_PAGE_COLUMNS=id,user_id, the columns its completion pages already ask for. §5 keeps onlyuser_id;countPositionHolders:MEMBER_HOLDER_ROW_COLUMNS=user_idplus the four above.sys_memberdoes not declarevalid_fromorvalid_untiltoday. They are named on purpose. Measured on the real engine (ObjectQL + driver-sql), a projection that names an undeclared column leaves it out with no error:['organization_id','role','valid_from','valid_until']returns{ organization_id, role }. So nothing changes today, and the window is read as soon as the object declares those columns. That is what §3's comment promises ("correct the moment they do"). Leaving them out would silently drop that promise. Lines 515–660 (draft PR #30287) are untouched.Changeset:
@objectstack/platform-objectsand@objectstack/core, patch, both declaration lines.Tests
identity-display-title.test.ts(the shared title harness): the four cases now carry the related rows that the read path preloads, are titled by names, and still equal the text of the retired template. They never contain an id. A new top-leveldescribepins the following for each object:nameFieldand that the field is required text.resolve-authz-context.grant-projection.test.ts(the shared projection harness): a new top-leveldescribepins the two prelude membership reads' column sets. It also pins that the envelope resolved over projected rows equals the envelope over full rows. That equality is checked for seven membership states: owner, another organization, external, two roles,valid_untilpast,valid_fromfuture, and no active organization. The states resolve at least five distinct envelopes. It further pins thatcountPositionHolderscounts the same from projected rows and readsuser_idbeside the four columns. The first case's "no other read names any" is narrowed to excludesys_member.auth-prelude-query-count.pin.test.ts: a new case shows that the shippedsys_membertitle readsrecord.user_id.name, that a full read computes it (Prelude (owner)), and that the prelude still makes 8 reads in 4 round trips. The pinned counts are unchanged.Acceptance notes
sys_memberare authoredkind: 'full'pages, and they name their own columns:sys_organization_detail(Members,user_id, role, created_at) andsys_user_detail(Organizations,organization_id, role, created_at).sys_user_detaildoes not listsys_two_factor. No Setup navigation reachessys_scim_userorsys_scim_group. So norelatedListColumnsis added.sys_memberreaders that name no columns now pay the title's hop: one batchedsys_userread per suchfind(measured[1, 0], unprojected vs projected). The prelude read is the per-request one and is fixed here. Readers elsewhere (git grep "'sys_member'"lists plugin-auth, plugin-sharing, plugin-approvals, service-messaging and others) were not swept for projections. The plugin-auth, plugin-sharing and plugin-approvals suites are green on this head. Noted, not filed · carrier: seat.sys_business_unit_memberfollows.sys_scim_groupandsys_scim_group_memberboth default to the row scopeid == null, so without an explicit grant a reader sees neither the rows nor their titles.sys_scim_subject(nameField: 'user_id', a bare lookup pointer) is outside this card's four objects and unchanged.Verification
Readings on head
2d857e278e(the fix's six commits plus one merge ofmainatf506b79d85, which brings in #30452) unless a leg names its own commit.Red on main, green, before and after the projection, ablation
The titles a read serves, through the real engine (one-off measurement, not committed)
ObjectQL + SqlDriver (better-sqlite3 in memory) over the built
dist/at671545ad, registeringSysUser,SysOrganization, the four objects and their SCIM targets, with a reader-scoping middleware (the setup ofplugin-security/src/business-unit-member-title.test.ts):Probe on the same engine:
find('sys_member', { fields: ['organization_id','role','valid_from','valid_until'] })returns[{"organization_id":"org_default","role":"admin"}]; an undeclared projected column is left out, not refused.Suites, typecheck, lint
Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon2d857e278e: 64 commands; each run with its exit code recorded before any pipe.--ran: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loads,check:lean-entry-closureandcheck:dts-closureafter a whole-workspace build,pnpm check:i18n,check:authz-resolverandcheck:engine-double-contract.node scripts/check-plugin-teardown-shape.mjs --self-test: exit 0 after fetching its control commit into this shallow clone.check:auth-mount-ledger,check:changeset-fixed,check:error-code-casingandcheck:filter-alias-parity, all exit 0.Dogfood: not owed (no
!in the title, no(narrowing)arm).Generated by Claude Code