From 1af3546e892ec390d4fbb3646b567910b2d485a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 19:58:32 +0000 Subject: [PATCH] feat(spec,plugin-security): export the kernel platform-admin capability declaration from @objectstack/spec (#11663 Choice 6A, leg L1) Part of #11965 - @objectstack/spec exports ADMIN_FULL_ACCESS_CAPABILITIES (objects wildcard + systemPermissions) beside ADMIN_FULL_ACCESS in identity/eval-user.zod.ts; the #3544/#8681 export-axis rulings travel with the declaration. - plugin-security's admin_full_access declaration imports that list, so exactly one copy exists. Behaviour-neutral: pinned deep-equal against the exact pre-move inline literal in default-permission-sets.test.ts. - api-surface / export-origins baselines regenerated (2 added, 0 breaking); authorable-surface unmoved. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012xGvxcwPRTJfA7RfjXEYA4 --- .../platform-admin-capability-export.md | 6 ++ .../objects/default-permission-sets.test.ts | 53 +++++++++++++++ .../src/objects/default-permission-sets.ts | 56 ++++------------ packages/spec/api-surface/identity.json | 1 + packages/spec/api-surface/root.json | 1 + packages/spec/export-origins/identity.json | 1 + packages/spec/export-origins/root.json | 1 + packages/spec/src/identity/eval-user.zod.ts | 67 +++++++++++++++++++ .../platform-admin-capabilities.test.ts | 49 ++++++++++++++ packages/spec/src/index.ts | 1 + 10 files changed, 192 insertions(+), 44 deletions(-) create mode 100644 .changeset/platform-admin-capability-export.md create mode 100644 packages/spec/src/identity/platform-admin-capabilities.test.ts diff --git a/.changeset/platform-admin-capability-export.md b/.changeset/platform-admin-capability-export.md new file mode 100644 index 0000000000..64d9b0c90c --- /dev/null +++ b/.changeset/platform-admin-capability-export.md @@ -0,0 +1,6 @@ +--- +'@objectstack/spec': patch +'@objectstack/plugin-security': patch +--- + +Export the kernel platform-admin capability declaration from `@objectstack/spec` (`ADMIN_FULL_ACCESS_CAPABILITIES`) and import it in plugin-security's `admin_full_access` permission-set declaration, so exactly one copy of the capability list exists (#11663 Choice 6A, leg L1). Behaviour-neutral: the declared capability set is byte-for-byte unchanged, pinned by test. diff --git a/packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts b/packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts index 62c5fe9fe0..9151c61f76 100644 --- a/packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts +++ b/packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts @@ -4,6 +4,8 @@ import { describe, it, expect } from 'vitest'; import * as PlatformObjects from '@objectstack/platform-objects'; +import { PermissionSetSchema } from '@objectstack/spec/security'; +import { ADMIN_FULL_ACCESS_CAPABILITIES } from '@objectstack/spec'; import { defaultPermissionSets, BETTER_AUTH_MANAGED_OBJECTS } from './default-permission-sets.js'; import { MANAGED_DENY_TARGET_SETS } from '../managed-object-write-denies.js'; @@ -310,3 +312,54 @@ describe('sys_comment delete is moderation-shaped, not ownership-shaped (#8839)' } }); }); + +/** + * [#11965 / #11663 Choice 6A] platform-admin re-anchor, L1 behaviour-neutrality + * pin. + * + * `admin_full_access`'s capability CONTENT moved to `@objectstack/spec` + * (`ADMIN_FULL_ACCESS_CAPABILITIES`) and is IMPORTED here — one list, one copy. + * L1 is ruled behaviour-neutral, so the parsed declaration must be deep-equal + * to what the previously-inline literal produced. The literal below is the + * exact pre-#11965 inline value (comments elided); if this pin fails, the spec + * export changed the declared capability set — that is a capability change + * riding on a refactor card, and it must not land silently. + */ +describe('admin_full_access imports the kernel capability declaration unchanged (#11965)', () => { + it('parsed declaration deep-equals the pre-#11965 inline literal', () => { + const preMove = PermissionSetSchema.parse({ + name: 'admin_full_access', + label: 'Administrator — Full Access', + objects: { + '*': { + allowRead: true, + allowCreate: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + // [#8681] no `allowExport` — deliberate, see the spec declaration. + }, + }, + systemPermissions: [ + 'manage_users', + 'manage_metadata', + 'manage_platform_settings', + 'manage_sharing', + 'setup.access', + 'setup.write', + 'studio.access', + ], + }); + expect(setByName('admin_full_access')).toEqual(preMove); + }); + + it('the imported spec constant is the declaration content — no local fork', () => { + const admin = setByName('admin_full_access'); + // Same values, sourced from the one spec-exported copy. + expect(admin.objects).toEqual( + PermissionSetSchema.parse({ name: 'admin_full_access', ...ADMIN_FULL_ACCESS_CAPABILITIES }).objects, + ); + expect(admin.systemPermissions).toEqual(ADMIN_FULL_ACCESS_CAPABILITIES.systemPermissions); + }); +}); diff --git a/packages/plugins/plugin-security/src/objects/default-permission-sets.ts b/packages/plugins/plugin-security/src/objects/default-permission-sets.ts index 7138f418f5..417f9321cc 100644 --- a/packages/plugins/plugin-security/src/objects/default-permission-sets.ts +++ b/packages/plugins/plugin-security/src/objects/default-permission-sets.ts @@ -2,6 +2,8 @@ import { PermissionSetSchema, type PermissionSet } from '@objectstack/spec/security'; import { + ADMIN_FULL_ACCESS, + ADMIN_FULL_ACCESS_CAPABILITIES, ORGANIZATION_ADMIN, ORGANIZATION_ADMIN_NO_BYPASS, BUILTIN_IDENTITY_ORG_ADMIN, @@ -117,51 +119,17 @@ const denyWritesOnManagedObjects = (): Record = { + objects: { + '*': { + allowRead: true, + allowCreate: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + // [#3544] Export is an OPT-IN grant and is deliberately NOT implied by + // the super-user bits — "may see all data" and "may take a bulk copy of + // it" are separable on purpose (SAP S_GUI 61 / segregation of duties). + // + // [#8681] NO `allowExport` HERE, and it is not an oversight. This set + // shipped `allowExport: true` on the wildcard through 17.0.0 GA, which + // made the export axis undeniable for anyone holding it: an app could + // declare an object exportable by nobody and the platform exported it + // anyway, with no supported opt-out (editing a code-package set answers + // `403 [not_overridable]`, and the admin holds no app-authored set to + // put the per-object `false` into). Measured on GA, hotcrm#1152: an org + // owner exported three objects no app set grants export on, 200 with + // full rows. Maintainer ruling (2026-08-15) removes the grant — the + // export axis's half of #5491, which removed `member_default`'s CRUD + // wildcard for the identical "a wildcard nobody can get under" reason. + // + // ⛔ Do not restore it, and do not restore a NARROWER wildcard either — + // "which platform objects should ship an explicit export grant" is an + // OPEN question the ruling deliberately left to a separate decision, and + // any `'*'` export grant here re-opens the hole for every object the + // platform does not know about. Where admin export is intended, grant + // `allowExport` per object in an APP permission set. + }, + }, + systemPermissions: [ + 'manage_users', + 'manage_metadata', + 'manage_platform_settings', + // [ADR-0111 D9] Sharing administration — gates the sharing-rule surface + // and (in the DEPTH extension) non-owner share management. + 'manage_sharing', + 'setup.access', + 'setup.write', + 'studio.access', + ], +}; + /** * Permission-set name whose grant is the source of truth for the `TENANT_ADMIN` * posture rung (ADR-0095 D3). Auto-granted (org-scoped) to every `sys_member` diff --git a/packages/spec/src/identity/platform-admin-capabilities.test.ts b/packages/spec/src/identity/platform-admin-capabilities.test.ts new file mode 100644 index 0000000000..37ff91e9b2 --- /dev/null +++ b/packages/spec/src/identity/platform-admin-capabilities.test.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// [#11965 / #11663 Choice 6A] platform-admin re-anchor L1 — the kernel +// platform-admin capability declaration is exported from the contract package. + +import { describe, it, expect } from 'vitest'; +import { ADMIN_FULL_ACCESS, ADMIN_FULL_ACCESS_CAPABILITIES } from './eval-user.zod'; +import { PermissionSetSchema } from '../security/permission.zod'; +import { PLATFORM_CAPABILITY_NAMES } from '../security/capabilities'; + +describe('ADMIN_FULL_ACCESS_CAPABILITIES (#11965, Choice 6A)', () => { + it('carries exactly the two capability-bearing fields — name/label stay with the declaring package', () => { + // The export is the capability CONTENT, not a permission set. `name` / + // `label` (or any other authored field) creeping in here would make the + // spec copy a second declaration instead of the single shared list. + expect(Object.keys(ADMIN_FULL_ACCESS_CAPABILITIES).sort()).toEqual([ + 'objects', + 'systemPermissions', + ]); + }); + + it('composes into a valid strict permission-set declaration under the canonical name', () => { + // Exactly how plugin-security consumes it: spread into the authored shape. + const parsed = PermissionSetSchema.parse({ + name: ADMIN_FULL_ACCESS, + label: 'Administrator — Full Access', + ...ADMIN_FULL_ACCESS_CAPABILITIES, + }); + expect(parsed.name).toBe('admin_full_access'); + // The PLATFORM_ADMIN posture rung derives from these bits (ADR-0095 D3). + expect(parsed.objects['*'].viewAllRecords).toBe(true); + expect(parsed.objects['*'].modifyAllRecords).toBe(true); + }); + + it('the wildcard grants NO export — #8681 ruling pinned at the declaration\'s new home', () => { + // [#3544/#8681] export is an OPT-IN axis, deliberately absent from the + // super-user wildcard (maintainer ruling 2026-08-15). Moving the + // declaration into spec must not resurrect it. + expect('allowExport' in ADMIN_FULL_ACCESS_CAPABILITIES.objects!['*']).toBe(false); + const parsed = PermissionSetSchema.parse({ name: ADMIN_FULL_ACCESS, ...ADMIN_FULL_ACCESS_CAPABILITIES }); + expect(parsed.objects['*'].allowExport).not.toBe(true); + }); + + it('every granted system permission is a declared built-in capability (ADR-0066 registry)', () => { + const unknown = (ADMIN_FULL_ACCESS_CAPABILITIES.systemPermissions ?? []).filter( + (name) => !PLATFORM_CAPABILITY_NAMES.has(name), + ); + expect(unknown).toEqual([]); + }); +}); diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index a2a790b19e..4acfd285a6 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -277,6 +277,7 @@ export { BUILTIN_IDENTITY_ORG_ADMIN, BUILTIN_IDENTITY_ORG_MEMBER, ADMIN_FULL_ACCESS, + ADMIN_FULL_ACCESS_CAPABILITIES, ORGANIZATION_ADMIN, ORGANIZATION_ADMIN_NO_BYPASS, ORGANIZATION_ADMIN_GRANTS,