Repository navigation
fix(platform-objects): offer the platform-admin-gated user, OAuth and SSO actions only to a platform admin - #22068
Conversation
… a platform admin The thirteen sys_user, sys_oauth_application and sys_sso_provider actions whose door runs the ADR-0068 platform-admin gate now declare `visible: 'current_user.isPlatformAdmin == true'`, composed with their existing terms. A repo-wide enumeration pin holds every first-party action targeting a platform-admin-gated door to the same standing. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 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 139d7857393d62678f5ec07afd0dcf6ae29cdd02 && git checkout 139d7857393d62678f5ec07afd0dcf6ae29cdd02
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 56bf27affbe5e3f5aca8001ed0218d013b04bf22 efd167f5a317ae9ecfe8c24f8c80e13cd8cd4d7f && git checkout -B drift-repro 56bf27affbe5e3f5aca8001ed0218d013b04bf22 && git merge --no-ff efd167f5a317ae9ecfe8c24f8c80e13cd8cd4d7f
node scripts/docs-audit/affected-docs.mjs --json 56bf27affbe5e3f5aca8001ed0218d013b04bf22
|
ACCEPT (seat review) — PR #22068 at head
|
Fixes #21903
Clause-②: no
What changes
Thirteen first-party actions are now offered only to a platform administrator. That is the one standing their endpoints admit.
visible: 'current_user.isPlatformAdmin == true'. That is ruling A-lite's predicate (record 6019378035), the onesys_member.add_memberlanded with in PR fix(platform-objects): offer Add Member only to a platform admin #22064. It is ANDed with any term the action already had, andrequiresFeaturecomposes onto it at parse time.sys_user:ban_user,unban_user,unlock_user,create_user,set_user_password,impersonate_userandset_user_manager;sys_oauth_application:disable_oauth_applicationandenable_oauth_application;sys_sso_provider:register_sso_provider,register_saml_provider,request_domain_verificationandverify_domain.packages/platform-objects/src/platform-admin-affordance-standing.test.tsimports every*.object.tsunderpackages/and judges every action whose target door is platform-admin gated. An action added later fails it until itsvisiblecarries the standing.The doors and the callers they admit are unchanged. No key, export or parameter is added, and no label changes. That makes
Clause-②: no, and the changeset is a@objectstack/platform-objectspatch.File surface. This is the surface the claim declared:
packages/platform-objects/src/identity/;packages/platform-objects/src/platform-objects.test.ts;.changeset/21903-platform-admin-affordance-visibility.md.Two existing tests in the same
identity/directory evaluate these predicates, so their fixtures were re-judged (see Acceptance notes):action-predicate-sparse-face.test.tsandsys-user-set-manager-action.test.ts. There is nopackages/spec,plugin-auth,@objectstack/formula,docs/adror dogfood edit.The census (H1), measured at
56bf27affbEvery first-party action with a
/api/v1/auth/target lives inpackages/platform-objects/src/identity/. The gate lines are inpackages/plugins/plugin-auth/src/auth-plugin.tsunless named otherwise.visibleserved beforevisibleserved aftersys_user.ban_user/admin/ban-user:2670,gateAdmin:2672features.admin == true(current_user.isPlatformAdmin == true) && features.admin == truesys_user.unban_user/admin/unban-user:2691,gateAdmin:2693features.admin == trueban_usersys_user.unlock_user/admin/unlock-user:2490,judgePlatformAdmin:2502features.admin == trueban_usersys_user.create_user/admin/create-user:2601,gateAdmin:2603features.admin == trueban_usersys_user.set_user_password/admin/set-user-password:2851,gateAdmin:2853features.admin == trueban_usersys_user.impersonate_user/admin/impersonate-useradmin-impersonate-endpoint.ts:198, caller predicate:213, wired atauth-manager.ts:3609)features.admin == trueban_usersys_user.set_user_manager/admin/set-user-manager:2536,gateAdmin:2538has(record.source) && record.source != "idp_provisioned"current_user.isPlatformAdmin == true && has(record.source) && record.source != "idp_provisioned"sys_oauth_application.disable_oauth_application/admin/oauth2/toggle-disabled:2355,judgePlatformAdmin:2372(has(record.disabled) && record.disabled != true) && features.oidcProvider != false(current_user.isPlatformAdmin == true && has(record.disabled) && record.disabled != true) && features.oidcProvider != falsesys_oauth_application.enable_oauth_application/admin/oauth2/toggle-disabled(has(record.disabled) && record.disabled == true) && features.oidcProvider != false(current_user.isPlatformAdmin == true && has(record.disabled) && record.disabled == true) && features.oidcProvider != falsesys_sso_provider.register_sso_provider/admin/sso/register:2468,gateAdmin:2470current_user.isPlatformAdmin == truesys_sso_provider.register_saml_provider/admin/sso/register-saml:2958,gateAdmin:2962current_user.isPlatformAdmin == truesys_sso_provider.request_domain_verification/admin/sso/request-domain-verification:2983,gateAdmin:2988current_user.isPlatformAdmin == truesys_sso_provider.verify_domain/admin/sso/verify-domain:3002,gateAdmin:3005current_user.isPlatformAdmin == truesys_member.add_member(the precedent)/organization/add-member:2932,gateAdmin:2934(current_user.isPlatformAdmin == true) && features.organization != falsesys_oauth_application.create_oauth_application/sys-oauth-application/register:3038, session onlyfeatures.oidcProvider != falsesys_oauth_application.rotate_client_secret/oauth2/client/rotate-secret@better-auth/oauth-provider1.7.3rotateClientSecretEndpointauthorizes the client's owner (client.userId === session.user.id); plugin-auth configures noclientPrivilegesfeatures.oidcProvider != falsesys_oauth_application.delete_oauth_application/oauth2/delete-clientdeleteClientEndpoint, same owner checkfeatures.oidcProvider != falsesys_sso_provider.delete_sso_provider/sso/delete-provider@better-auth/sso1.7.3checkProviderAccessadmits the provider's owner or an admin of its organizationTargets are spelled after
/api/v1/auth.The dispatch's hypotheses, measured
add_member.sys_oauth_application: two actions" is two of the five that object declares. The other three authorize the session or the application's owner, as the table shows.sys_sso_provider: four actions" is four of five.delete_sso_provider's door is the vendor's provider-access check.service-datasourceregisters two type-level actions. Their doors ask for themanage_platform_settingscapability (admin-routes.ts:272), and the file rules out anisPlatformAdminarm (admin-routes.ts:268). They are not members.sys_organization.change_slugtargets/api/v1/cloud/..., which another repository serves. Its door is NOT MEASURED here.lowerRequiresFeature(packages/spec/src/kernel/public-auth-features.ts:352) composes(existing) && gateat:417.visible, the standing is ANDed in the repo's existing spelling for an authored composed predicate. That is one flat conjunction with the principal term first, as thesys_userself-service predicates spell theirs.distwithcelEngine, bindingcurrent_userthe way the console does. The whole scope goes in asextra, with one subject undercurrent_user,user,ctx.userandos.user.56bf27affb, every member was offered to all five principals: platform admin, org owner, org admin, delegated admin and plain member.efd167f5a3, each member is offered to the platform admin alone.judgePlatformAdminon the same five session shapes. It admits the platform admin and refuses the other four with 403PERMISSION_DENIED. It also refuses a session whosepositionscarry a tenant-writtenplatform_adminwithout the rung.admin-impersonate-endpoint.ts:213) refuses the same principals with 403YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS. Its oracle is the posture rung, whichauth-manager.ts:4083also emits as the session'sisPlatformAdmin.admin-route-nonadmin-refusal.dogfood.test.tsandadmin-platform-admin-standing.dogfood.test.tspassed 14 of 14. They cover all thirteen member doors in both directions.sys_sso_provideralso requiresmanage_platform_settingsat object level (sys-sso-provider.object.ts:60). That is a capability, not the rung, so the predicate is still needed for a holder of the capability who is not a platform admin.plugin-authexports no mount table.auth-route-ledger.tsis package-internal, and its rows state the gate only innoteprose.VENDOR_ADMIN_PATH_PREFIXnames the namespace, butplugin-authis not a dependency ofplatform-objects./api/v1/auth/admin/namespace;/admin/has-permission(a query that answers every caller) and/admin/stop-impersonating(it admits the impersonated session);/api/v1/auth/organization/add-member.Tests
All runs are at the final head
efd167f5a3.platform-objects.test.ts: every member has a row with its composed served predicate, besideadd_member's row.set_user_managerand the foursys_sso_providerrows are new.platform-admin-affordance-standing.test.ts: 5 cases.visiblecarriescurrent_user.isPlatformAdmin == true.platform_adminposition without the rung.current_useris bound throughextra.pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2passed 63 files and 1006 tests. Onmainit ran 62 files and 996 tests: this adds 5 pin cases and 5 matrix rows.pnpm --filter @objectstack/platform-objects typecheckpassed.tsconfig.test.json) compiles all four touched test files:--listFilescounts 1 hit each, and the build program counts 0.src/feature-gate-guard.test.ts.admin-route-nonadmin-refusalandadmin-platform-admin-standing: 14 of 14;org-admin-affordance-reach: 14 of 14.Reverse verification, on committed HEAD
efd167f5a3scripts/ablation-replace.mjsreplacedset_user_manager's predicate with its pre-change text,has(record.source) && record.source != "idp_provisioned".0b9e8e738fb4to0ac51042a82d.trap … EXIT INT TERMused absolute paths..object.tspath. Nodist/is in their resolution path, so no rebuild was needed.SysUser.set_user_managerrow expectedcurrent_user.isPlatformAdmin == true && has(record.source) && record.source != "idp_provisioned"and received the mutated text.sys_user.set_user_manager.set_user_manager.git checkout HEAD --on the absolute path.0b9e8e738fb4911b9a379b2ebc699344f68127bd.git diff HEADis empty, andgit statusis clean.ablation-replaceand the outer script proved this.Gates
These ran at
efd167f5a3.origin/mainwas still56bf27affbat the last fetch, so there was nothing to merge.turbo run build --filter=!@objectstack/docs --concurrency=2passed 72 of 72 tasks.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 63 commands (31 pnpm, 32 node). All 63 exited 0.--ran, with each exit code recorded:63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN(a derived zero).check:dual-build-cjs-loadsloaded 106 entries across 66 packages, andcheck:dts-closureswept 72 packages.check-closing-target-claim,check-partof-closing-keywordandcheck-single-claim-paths.check-partof-closing-keywordwas rerun with this body asPR_BODY. The other two need this PR's number, and CI runs them.check-sdui-manifestpassed, but its objectui version comparison is NOT CHECKED: there is no objectui checkout here.check:console-injectionran its self-test only, because there is no consoledist/.check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchorsandcheck:adr-anchorsall passed.pnpm exec eslint --no-inline-config --format jsonover the seven touched TypeScript files.--print-configresolves a config for each file, so none is ignored.parserOptions.projectorprojectService, andeslint.config.mjs:327-328states the repo never enables it. So this diff cannot move the verdict on any untouched file.pnpm lint.Acceptance notes
user.role === 'admin'scalar (platform-admin-gate.ts:80-84, and the impersonate predicate's first half), and this predicate does not read it. This is the boundary PR fix(platform-objects): offer Add Member only to a platform admin #22064 recorded foradd_member. Nothing in ObjectStack writes that scalar for a platform admin, and re-synthesizing it is vetoed. Noted, not filed.action-predicate-sparse-face.test.tsbound an org owner throughuser:. Under that binding@objectstack/formulare-derivesisPlatformAdminfrompositions.false, and the sweep exists to test exactly that half.extra, with the rung, the way the console binds it. The only alternative underuser:was aplatform_adminposition, the spelling the standing must never be read from.sys-user-set-manager-action.test.tsevaluated its "offered to an admin" case for a principal with no standing. It now binds a platform admin the same way.request_domain_verificationandverify_domainpass the platform-admin gate and then delegate to@better-auth/sso'scheckProviderAccess. So doesdelete_sso_provider's door, with no gate in front.Generated by Claude Code