diff --git a/content/docs/automation/hooks.mdx b/content/docs/automation/hooks.mdx index 61b538a22c1..2498dcacd71 100644 --- a/content/docs/automation/hooks.mdx +++ b/content/docs/automation/hooks.mdx @@ -154,7 +154,7 @@ A hook's `ctx.api` runs with the context of the write that **fired** it. That is | `runAs` | The hook's `ctx.api` runs as | |:---|:---| | `'inherit'` *(default)* | the context of the triggering write — the behaviour every hook had before this key existed. | -| `'system'` | elevated: a full-access, RLS-bypassing system principal. Row and field permissions do not apply, and the static `readonly` strip is skipped. | +| `'system'` | elevated: an RLS-bypassing system principal. Row and field permissions do not apply, and the static `readonly` strip is skipped. Elevation stops at identity assignment: an insert, update, delete, transfer, restore or purge of `sys_user_position`, `sys_user_permission_set`, `sys_invitation` or `sys_member` from an app-authored hook's system context (`runAs: 'system'`, a system context it inherits, or `ctx.api.sudo()`) is refused with `PERMISSION_DENIED`, and a `runAs: 'system'` hook whose `language: 'js'` body writes one of them by name through `ctx.api.object('…')` is refused when it is saved — grant positions and permission sets from a hook with `runAs: 'user'`, so the delegated-administration gate judges the user it acts for, and change invitations and memberships through the organization endpoints the refusal names ([row 1b](/docs/permissions/system-context#1-authorization-and-scoping)). | | `'user'` | the triggering **user**, so the hook can never exceed that user's grants. A hook fired by a write that carried no user has no identity to scope to, so its `ctx.api` data operations are **refused** (`HOOK_UNSCOPED_DATA_ACCESS`) rather than run unscoped. | ```ts diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx index 8568909d2c8..0340889279c 100644 --- a/content/docs/ui/actions.mdx +++ b/content/docs/ui/actions.mdx @@ -442,8 +442,18 @@ is a spec proposal for a properly named key, not a values map under this one. An action body and a registered handler both run **elevated**: `ctx.api` and `ctx.engine` carry `isSystem`, so they bypass row- and field-level security by -design (that is what lets an action do work the caller cannot do directly). The -consequence is that any caller-specific rule has to be re-established **inside** +design (that is what lets an action do work the caller cannot do directly). +Elevation stops at identity assignment: an insert, update, delete, transfer, +restore or purge of `sys_user_position`, `sys_user_permission_set`, +`sys_invitation` or `sys_member` from either is refused with +`PERMISSION_DENIED`, and a `type: 'script'` action whose body writes one of them +by name through `ctx.api.object('…')` is refused when it is saved — make a +position or permission-set grant from a flow whose `runAs` is `'user'`, which an +action of `type: 'flow'` runs as the invoking user so the +delegated-administration gate judges that user, and change invitations and +memberships through the organization endpoints the refusal names +([row 1b](/docs/permissions/system-context#1-authorization-and-scoping)). The +other consequence is that any caller-specific rule has to be re-established **inside** your handler — and the predicate most people reach for first does not work: ```js