Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/platform-admin-capability-export.md
Original file line numberDiff line numberDiff line change
@@ -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.
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand DownExpand Up@@ -117,51 +119,17 @@ const denyWritesOnManagedObjects = (): Record<string, {
*/
const baseDefaultPermissionSets: PermissionSet[] = [
PermissionSetSchema.parse({
name: 'admin_full_access',
name: ADMIN_FULL_ACCESS,
label: 'Administrator — Full Access',
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',
],
// [#11965 / #11663 Choice 6A] The capability CONTENT (object wildcard +
// `systemPermissions`) is the kernel platform-admin declaration exported
// by `@objectstack/spec` — one list, imported here and read by core's
// platform-admin derivation, so exactly one copy exists. The export-axis
// rulings travel with the declaration (see `ADMIN_FULL_ACCESS_CAPABILITIES`
// in `spec/src/identity/eval-user.zod.ts`: #3544 export is opt-in; #8681
// ⛔ no `allowExport` on the wildcard, and do not restore a narrower one).
// Behaviour-neutrality is pinned by `default-permission-sets.test.ts`.
...ADMIN_FULL_ACCESS_CAPABILITIES,
}),
// ── Organization Administrator ──────────────────────────────────────
//
Expand Down
1 change: 1 addition & 0 deletions packages/spec/api-surface/identity.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
"entry": "./identity",
"exports": [
"ADMIN_FULL_ACCESS (const)",
"ADMIN_FULL_ACCESS_CAPABILITIES (const)",
"AUDIENCE_ANCHOR_POSITIONS (const)",
"AUTH_CONSTANTS (const)",
"AUTH_ERROR_CODES (const)",
Expand Down
1 change: 1 addition & 0 deletions packages/spec/api-surface/root.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
"entry": ".",
"exports": [
"ADMIN_FULL_ACCESS (const)",
"ADMIN_FULL_ACCESS_CAPABILITIES (const)",
"ALL_CONVERSIONS (const)",
"ASSEMBLED_VIEW_ITEMS_KEY (const)",
"AUDIENCE_ANCHOR_POSITIONS (const)",
Expand Down
1 change: 1 addition & 0 deletions packages/spec/export-origins/identity.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
"entry": "./identity",
"exports": {
"ADMIN_FULL_ACCESS": "src/identity/eval-user.zod.ts#ADMIN_FULL_ACCESS (const)",
"ADMIN_FULL_ACCESS_CAPABILITIES": "src/identity/eval-user.zod.ts#ADMIN_FULL_ACCESS_CAPABILITIES (const)",
"AUDIENCE_ANCHOR_POSITIONS": "src/identity/position.zod.ts#AUDIENCE_ANCHOR_POSITIONS (const)",
"AUTH_CONSTANTS": "src/identity/protocol.ts#AUTH_CONSTANTS (const)",
"AUTH_ERROR_CODES": "src/identity/protocol.ts#AUTH_ERROR_CODES (const)",
Expand Down
1 change: 1 addition & 0 deletions packages/spec/export-origins/root.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@
"entry": ".",
"exports": {
"ADMIN_FULL_ACCESS": "src/identity/eval-user.zod.ts#ADMIN_FULL_ACCESS (const)",
"ADMIN_FULL_ACCESS_CAPABILITIES": "src/identity/eval-user.zod.ts#ADMIN_FULL_ACCESS_CAPABILITIES (const)",
"ALL_CONVERSIONS": "src/conversions/registry.ts#ALL_CONVERSIONS (const)",
"ASSEMBLED_VIEW_ITEMS_KEY": "src/ui/assembled-views.zod.ts#ASSEMBLED_VIEW_ITEMS_KEY (const)",
"AUDIENCE_ANCHOR_POSITIONS": "src/identity/position.zod.ts#AUDIENCE_ANCHOR_POSITIONS (const)",
Expand Down
67 changes: 67 additions & 0 deletions packages/spec/src/identity/eval-user.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@

import { z } from 'zod';
import { lazySchema } from '../shared/lazy-schema';
import type { PermissionSet } from '../security/permission.zod';

/**
* EvalUser — the one user-context contract (ADR-0068 D1).
Expand DownExpand Up@@ -59,6 +60,72 @@ export type BuiltinIdentityName = (typeof BUILTIN_IDENTITY_NAMES)[number];
*/
export const ADMIN_FULL_ACCESS = 'admin_full_access';

/**
* [#11663 Choice 6A] The kernel platform-admin CAPABILITY DECLARATION — the
* capability content (object grants + `systemPermissions`) of the
* {@link ADMIN_FULL_ACCESS} permission set, declared ONCE here in the contract
* package so exactly one copy exists:
*
* - `@objectstack/plugin-security`'s `admin_full_access` declaration
* (`objects/default-permission-sets.ts`) spreads this object into its
* `PermissionSetSchema.parse({ name, label, ... })` entry — the metadata
* declaration that wins at enforcement time.
* - `@objectstack/core`'s platform-admin derivation (the re-anchor's L2 leg)
* reads the same list to fill `grants.systemPermissions`, so the derived
* envelope and the declared set can never drift apart.
*
* Shape note: these are the two capability-bearing fields of the authored
* permission-set contract (`PermissionSetSchema`); `name`/`label` remain with
* the declaring package. Behaviour-neutral by construction — the values are
* byte-for-byte the ones previously inlined in plugin-security, pinned by
* `objects/default-permission-sets.test.ts` there.
*/
export const ADMIN_FULL_ACCESS_CAPABILITIES: Pick<PermissionSet, 'objects' | 'systemPermissions'> = {
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`
Expand Down
49 changes: 49 additions & 0 deletions packages/spec/src/identity/platform-admin-capabilities.test.ts
Original file line numberDiff line numberDiff line change
@@ -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([]);
});
});
1 change: 1 addition & 0 deletions packages/spec/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand Down
Loading