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: 3 additions & 3 deletions .changeset/24341-identity-objects-default-row-scope.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/platform-objects': minor
'@objectstack/plugin-audit': minor
Expand All @@ -7,9 +7,9 @@

Clause-②: no (narrowing)

Twenty-three platform objects now declare `defaultRowScope`, each with the row policy the shipped permission sets of `@objectstack/plugin-security` carry for it, moved verbatim: the seven SCIM projections (`sys_scim_user`, `sys_scim_subject`, `sys_scim_projection_grant`, `sys_scim_identity_tombstone`, `sys_scim_group`, `sys_scim_group_member`, `sys_scim_connection_binding`), `sys_verification` and `sys_jwks`, `sys_organization`, `sys_user`, `sys_session`, `sys_account`, `sys_team_member`, `sys_two_factor`, `sys_user_preference`, `sys_api_key`, `sys_device_code`, `sys_oauth_access_token`, `sys_oauth_refresh_token`, `sys_oauth_consent`, `sys_oauth_application` (`@objectstack/platform-objects`) and `sys_audit_log` (`@objectstack/plugin-audit`, which owns the ledger). `sys_user` is the one merge: the key holds one policy, so its two shipped policies are joined into `id == current_user.id || id in current_user.org_user_ids`, a `select` policy. The platform-global objects (`sys_metadata*`, `sys_job*`, `sys_migration*`, `sys_platform_setting`, `sys_presence`, `sys_flow_dispatch`) declare none.
Twenty-three platform objects now declare `defaultRowScope`, each with the row policy the shipped permission sets of `@objectstack/plugin-security` carried for it, moved verbatim: the seven SCIM projections (`sys_scim_user`, `sys_scim_subject`, `sys_scim_projection_grant`, `sys_scim_identity_tombstone`, `sys_scim_group`, `sys_scim_group_member`, `sys_scim_connection_binding`), `sys_verification` and `sys_jwks`, `sys_organization`, `sys_user`, `sys_session`, `sys_account`, `sys_team_member`, `sys_two_factor`, `sys_user_preference`, `sys_api_key`, `sys_device_code`, `sys_oauth_access_token`, `sys_oauth_refresh_token`, `sys_oauth_consent`, `sys_oauth_application` (`@objectstack/platform-objects`) and `sys_audit_log` (`@objectstack/plugin-audit`, which owns the ledger). `sys_user` is the one merge: the key holds one policy, so its two shipped policies are joined into `id == current_user.id || id in current_user.org_user_ids`, a `select` policy. The platform-global objects (`sys_metadata*`, `sys_job*`, `sys_migration*`, `sys_platform_setting`, `sys_presence`, `sys_flow_dispatch`) declare none.

The security plugin's row-policy collection applies an object's declared default when the permission sets the caller holds carry no row policy for that object and operation. The shipped sets still carry the same policies, so a holder of them reads exactly what it read before.
The security plugin's row-policy collection applies an object's declared default when the permission sets the caller holds carry no row policy for that object and operation. On each object and operation the shipped sets either still carry the same policy or, where they no longer carry a copy (the `@objectstack/plugin-security` entry names the thirteen), collect the object's identical default, so a holder of them reads exactly what it read before.

**BREAKING — what moves for consumers.** This narrows what a principal reads on these objects when its sets carry no row policy for them, shipped as `minor` under the launch-window convention for accept-set narrowings.

Expand All @@ -19,7 +19,7 @@

**Unchanged.**

- A deployment with the platform baseline (`member_default`, the default) serves every human exactly what it served before, on every one of the 23 objects and every operation: each human holds the baseline, and the baseline carries a policy for each object and operation. Measured on a booted composition with a member, a viewer, an organisation admin, a user in no organisation and the platform administrator, before and after the declarations.
- A deployment with the platform baseline (`member_default`, the default) serves every human exactly what it served before, on every one of the 23 objects and every operation: each human holds the baseline, and on each object and operation the baseline either carries a policy or collects the object's identical default. Measured on a booted composition with a member, a viewer, an organisation admin, a user in no organisation and the platform administrator, before and after the declarations.
- An MCP agent acting for a user reads the same rows, and gets the same explain output, as before. Its ceiling set carries no row policy, and the security plugin does not collect an object's declared default on a delegated principal's own side of the intersection (see the `@objectstack/plugin-security` entry), so the agent's rows stay bounded by its delegator's side alone. Measured on a booted composition, before and after the declarations, for the agents of a member, a viewer, an organisation admin and the platform administrator; the platform administrator's agent still reads every row.
- The platform administrator's superuser bypass is decided before any row policy is collected.
- No export, option or stored shape moves.
Expand Down
18 changes: 18 additions & 0 deletions .changeset/24484-sets-stop-copying-identity-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/plugin-security': patch
---

fix(plugin-security): the shipped permission sets stop copying the row scope thirteen identity objects declare as their own default

Clause-②: no

`organization_admin` (and so `organization_admin_no_bypass`), `member_default` and `viewer_readonly` no longer carry their copies of the row policy that thirteen identity objects declare as their `defaultRowScope`. The thirteen are the seven SCIM projection tables (`sys_scim_user`, `sys_scim_subject`, `sys_scim_projection_grant`, `sys_scim_identity_tombstone`, `sys_scim_group`, `sys_scim_group_member`, `sys_scim_connection_binding`), `sys_verification` and `sys_jwks`, and the `select` self scope of `sys_account`, `sys_team_member`, `sys_oauth_access_token` and `sys_oauth_refresh_token`. Each copy was byte-identical to its object's declaration. The row-policy collection applies an object's declared default wherever the caller's held sets carry no policy for that object and operation, so every holder of the shipped sets reads exactly the rows it read before. That holds with the platform baseline and without it (`fallbackPermissionSet: null`), under the walled and the `single` posture, and for an MCP agent acting for a holder.

Explain now names the object's default (`via: 'object_default'`) where it named the set's policy on those thirteen objects.

**Kept in the sets.**

- `sys_user`'s two policies, and every `all`-spelled self scope (`sys_session`, `sys_two_factor`, `sys_user_preference`, `sys_api_key`, `sys_device_code`, `sys_oauth_consent`, `sys_oauth_application`, `sys_organization`). A member's `update` and `delete` collection holds the `'*'` ownership floor, so the object's default is never collected there, and these copies are what scope a member's writes to their own rows.
- `sys_audit_log_org`. Where no organization wall is enforced, the platform's own tenant policies are stripped by provenance, and that provenance is read off these sets. The copy is what makes the ledger's identical default the platform's policy, so `single` keeps serving the ledger unscoped.

**If an application set authors a row policy for one of these thirteen objects**, that policy is used as authored and the object's default is not added beside it. Before, a holder who also held a shipped set had that set's copy OR-ed in, so an authored scope that left out the holder's own rows was widened back to them. Now the authored policy alone decides on that object and operation. Measured: no package or example in this repository authors such a policy.
14 changes: 7 additions & 7 deletions packages/platform-objects/src/identity/sys-account.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,13 @@ export const SysAccount = ObjectSchema.create({
},

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today (the `_self`
// carve-outs in `default-permission-sets.ts`), moved here verbatim. Layer 0 is inert
// on this table (no organization column), so Layer 1 silence means this scope, not no
// filter. The security plugin's row-policy collection applies it when no set the
// caller holds carries a row policy for the object and operation; the shipped sets
// still carry it, so their holders read as before. An `extend` contribution cannot set
// or loosen it (refused at registration).
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_account_self',
operation: 'select',
Expand Down
10 changes: 5 additions & 5 deletions packages/platform-objects/src/identity/sys-jwks.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ export const SysJwks = ObjectSchema.create({
highlightFields: ['id', 'created_at', 'expires_at'],

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today
// (`privateCredentialRowScope` in `default-permission-sets.ts`), moved here verbatim.
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation; the
// shipped sets still carry it, so their holders read as before. An `extend`
// contribution cannot set or loosen it (refused at registration).
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_jwks_none',
operation: 'select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ export const SysOauthAccessToken = ObjectSchema.create({
highlightFields: ['client_id', 'user_id', 'expires_at'],

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today (the `_self`
// carve-outs in `default-permission-sets.ts`), moved here verbatim. Layer 0 is inert
// on this table (no organization column), so Layer 1 silence means this scope, not no
// filter. The security plugin's row-policy collection applies it when no set the
// caller holds carries a row policy for the object and operation; the shipped sets
// still carry it, so their holders read as before. An `extend` contribution cannot set
// or loosen it (refused at registration).
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_oauth_access_token_self',
operation: 'select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ export const SysOauthRefreshToken = ObjectSchema.create({
highlightFields: ['client_id', 'user_id', 'expires_at'],

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today (the `_self`
// carve-outs in `default-permission-sets.ts`), moved here verbatim. Layer 0 is inert
// on this table (no organization column), so Layer 1 silence means this scope, not no
// filter. The security plugin's row-policy collection applies it when no set the
// caller holds carries a row policy for the object and operation; the shipped sets
// still carry it, so their holders read as before. An `extend` contribution cannot set
// or loosen it (refused at registration).
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_oauth_refresh_token_self',
operation: 'select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ export const SysScimConnectionBinding = ObjectSchema.create({
},

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today
// (`scimProjectionRowScope` in `default-permission-sets.ts`), moved here verbatim.
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation; the
// shipped sets still carry it, so their holders read as before. An `extend`
// contribution cannot set or loosen it (refused at registration).
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_scim_connection_binding_none',
operation: 'select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ export const SysScimGroupMember = ObjectSchema.create({
},

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today
// (`scimProjectionRowScope` in `default-permission-sets.ts`), moved here verbatim.
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation; the
// shipped sets still carry it, so their holders read as before. An `extend`
// contribution cannot set or loosen it (refused at registration).
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_scim_group_member_none',
operation: 'select',
Expand Down
10 changes: 5 additions & 5 deletions packages/platform-objects/src/identity/sys-scim-group.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ export const SysScimGroup = ObjectSchema.create({
},

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today
// (`scimProjectionRowScope` in `default-permission-sets.ts`), moved here verbatim.
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation; the
// shipped sets still carry it, so their holders read as before. An `extend`
// contribution cannot set or loosen it (refused at registration).
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_scim_group_none',
operation: 'select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ export const SysScimIdentityTombstone = ObjectSchema.create({
},

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today
// (`scimProjectionRowScope` in `default-permission-sets.ts`), moved here verbatim.
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation; the
// shipped sets still carry it, so their holders read as before. An `extend`
// contribution cannot set or loosen it (refused at registration).
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_scim_identity_tombstone_self',
operation: 'select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ export const SysScimProjectionGrant = ObjectSchema.create({
},

// [ADR-0095 D1, 2026-10-18 note] This object's default row scope: the one policy
// plugin-security's shipped permission sets spread for it today
// (`scimProjectionRowScope` in `default-permission-sets.ts`), moved here verbatim.
// plugin-security's shipped permission sets used to spread for it, moved here
// verbatim; the sets no longer carry a copy (#24164 ruling E′ item 3's follow-up).
// Layer 0 is inert on this table (no organization column), so Layer 1 silence means
// this scope, not no filter. The security plugin's row-policy collection applies it
// when no set the caller holds carries a row policy for the object and operation; the
// shipped sets still carry it, so their holders read as before. An `extend`
// contribution cannot set or loosen it (refused at registration).
// when no set the caller holds carries a row policy for the object and operation —
// so to every shipped set's holder, whose reads and explain name this scope. An
// `extend` contribution cannot set or loosen it (refused at registration).
defaultRowScope: {
name: 'sys_scim_projection_grant_self',
operation: 'select',
Expand Down
Loading
Loading