Skip to content

Commit c19dde3

Browse files
committed
docs: comments outside packages/spec say a principal-less context is refused since ADR-0096 D5
Comment and docstring text only: the sentences that described the security middleware handing through, skipping or falling open for a context with no principal now say that it used to, and that D5 refuses it. No executable line moves. Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9add183 commit c19dde3

16 files changed

Lines changed: 70 additions & 48 deletions

File tree

‎examples/app-showcase/src/automation/flows/index.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,9 +1581,11 @@ export const InboundTaskWebhookFlow = defineFlow({
15811581
type: 'api',
15821582
status: 'active',
15831583
// An inbound webhook has no authenticated user, so the create must run as the
1584-
// system principal (#1888 runAs is now enforced). Without this it relies on the
1585-
// "no identity → security-skipped" fall-through, which breaks the moment the
1586-
// target object carries row-level security.
1584+
// system principal (#1888 runAs is now enforced). Without this the run is
1585+
// user-less under the default `runAs:'user'`, so its create is refused
1586+
// (`AUTOMATION_UNSCOPED_RUN_DATA_ACCESS`); the "no identity → security-skipped"
1587+
// fall-through it once relied on is gone (ADR-0096 D5 refuses a principal-less
1588+
// context in the security middleware too).
15871589
runAs: 'system',
15881590
nodes: [
15891591
{

‎packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
* [#21911, ADR-0096] The metadata protocol's platform-store calls carry the
55
* EXPLICIT system opt-in, never a principal-less context.
66
*
7-
* plugin-security hands an engine operation whose context carries no user, no
8-
* position, no permission set and no `isSystem` straight to `next()` (the
9-
* ADR-0096 E1 hand-off). The protocol's own reads and writes of the
7+
* plugin-security used to hand an engine operation whose context carries no
8+
* user, no position, no permission set and no `isSystem` straight to `next()`
9+
* (the ADR-0096 E1 hand-off); since ADR-0096 D5 it refuses one. The protocol's
10+
* own reads and writes of the
1011
* `sys_metadata` family reached the engine exactly that way — a `{ where }`
1112
* with no `context`, or a repository transaction context with only its
1213
* handle — so they worked only because the hand-off let them through. They

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11230,11 +11230,13 @@ export class ObjectStackProtocolImplementation implements
1123011230
// were eliminated by measurement, not by reading:
1123111231
// - the driver's tenant wall never engages — `buildDriverOptions`
1123211232
// sets `DriverOptions.tenantId` only from `execCtx.tenantId`
11233-
// (`objectql/engine.ts`), and this read passes no context;
11234-
// - plugin-security's Layer 0 never engages — the middleware takes
11235-
// its principal-less `return next()` thousands of lines before the
11236-
// `objectFields.has('organization_id')` gate that would have
11237-
// carried it;
11233+
// (`objectql/engine.ts`), and this read's context carries no
11234+
// `tenantId` (only the system opt-in below);
11235+
// - plugin-security's Layer 0 never engages — the middleware
11236+
// short-circuits on that `isSystem` opt-in (it used to take its
11237+
// principal-less `return next()`, refused since ADR-0096 D5)
11238+
// thousands of lines before the `objectFields.has('organization_id')`
11239+
// gate that would have carried it;
1123811240
// - no posture would save it anyway: `computeTenantLayer0Filter`
1123911241
// yields `null` under `single` and the deny sentinel under
1124011242
// `isolated` with no tenantId.

‎packages/platform-objects/src/system/sys-secret.object.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ export const SysSecret = ObjectSchema.create({
4646
// store (settings/datasource credentials). Not covered by the wildcard `'*'`
4747
// grant — ordinary members get 403 from the generic data layer. Platform
4848
// admins retain access via the posture-gated superuser bypass. Internal
49-
// readers are unaffected: `engine.resolveSecret` reads at DRIVER level,
50-
// SettingsService / the datasource secret-binder read with no principal
51-
// (middleware falls open for principal-less internal calls).
49+
// readers are unaffected: `engine.resolveSecret` reads at DRIVER level, and
50+
// SettingsService / the datasource secret-binder read with the explicit system
51+
// opt-in (`isSystem: true`), which the middleware short-circuits. A
52+
// principal-less read is refused (ADR-0096 D5).
5253
access: { default: 'private' },
5354
description:
5455
'Cipher store written by three privileged producers (see managedBy); each holds its handle in its own column. Never holds plaintext.',

‎packages/plugins/plugin-auth/src/auth-manager.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3382,8 +3382,8 @@ export class AuthManager {
33823382
// Both reads run as the platform (`withSystemContext`): this hook
33833383
// IS the slug guard — the organization id is the `where`, not the
33843384
// reader — so neither read reaches the engine with no principal
3385-
// and no opt-in (the security middleware's principal-less
3386-
// hand-off, ADR-0096).
3385+
// and no opt-in (the security middleware's former principal-less
3386+
// hand-off, which ADR-0096 D5 replaced with a refusal).
33873387
const dataEngine = withSystemContext(rawEngine) as any;
33883388

33893389
let currentSlug: string | undefined;

‎packages/plugins/plugin-auth/src/auth-plugin.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,8 +2478,8 @@ export class AuthPlugin implements Plugin {
24782478
// the write both carry the explicit system opt-in (`isSystem: true`).
24792479
// The platform-admin judge above is this route's authorization; the
24802480
// raw engine would have handed the security middleware a context with
2481-
// no principal and no opt-in — the principal-less hand-off (ADR-0096),
2482-
// which is not an authorization at all.
2481+
// no principal and no opt-in — the principal-less hand-off it no longer
2482+
// makes (ADR-0096 D5 refuses that context), and never an authorization.
24832483
const rawEngine = this.authManager!.getDataEngine();
24842484
if (!rawEngine) {
24852485
return c.json({ success: false, error: { code: 'SERVICE_UNAVAILABLE', message: 'Data engine unavailable' } }, 503);

‎packages/plugins/plugin-auth/src/principal-less-producers-system-context.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
* - the SCIM bearer verifier (`verifyScimBearerToken`): its credential probe
1111
* carries the opt-in in the read's trailing options.
1212
*
13-
* A context with neither a principal nor `isSystem` is the security
14-
* middleware's principal-less hand-off (ADR-0096), which is not an
15-
* authorization: each of these calls is already authorized by its own door
13+
* A context with neither a principal nor `isSystem` was the security
14+
* middleware's principal-less hand-off (ADR-0096 E1) and is refused since
15+
* ADR-0096 D5; it was never an authorization: each of these calls is already
16+
* authorized by its own door
1617
* (the platform-admin judge; the bearer digest). The opt-in names that.
1718
*
1819
* Measured on a REAL engine: a middleware registered on it records the context

‎packages/plugins/plugin-auth/src/scim-connection-service.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ interface CredentialEngine {
119119
* verifier runs BEFORE any caller is known — the bearer it is verifying is the
120120
* only identity on the request — so there is no principal to carry, and the
121121
* read must say so rather than reach the engine with no principal and no
122-
* opt-in (the security middleware's principal-less hand-off, ADR-0096). The
122+
* opt-in (the security middleware's former principal-less hand-off, which
123+
* ADR-0096 D5 replaced with a refusal). The
123124
* digest equality in the `where` is the whole of what the probe may match.
124125
*/
125126
const CREDENTIAL_PROBE_CONTEXT = { context: { isSystem: true } } as const;

‎packages/plugins/plugin-security/src/get-metadata-readable-fields.test.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
* `getReadableFields`.
66
*
77
* The two differ in exactly one place: what a caller resolving to ZERO
8-
* permission sets gets. On the DATA plane, falling open is the drift-free
9-
* answer — the engine middleware skips its whole gate for such a caller, and
10-
* reporting a narrowing the data path would not enforce is its own kind of
11-
* drift. On the METADATA plane the question is disclosure, and D7 rules that a
8+
* permission sets gets. On the DATA plane `getReadableFields` keeps its
9+
* fall-open answer, written when the engine middleware skipped its whole gate
10+
* for such a caller (the middleware now refuses it: ADR-0056 D2's deny
11+
* baseline at object admission, and ADR-0096 D5 outright for a principal-less
12+
* one). On the METADATA plane the question is disclosure, and D7 rules that a
1213
* guest/public deployment's schema exposure must be a deliberate
1314
* permission-set decision rather than an accidental everything-default. So the
1415
* zero-set caller goes through the same fallback resolution
@@ -81,7 +82,8 @@ describe('[ADR-0106 D7] SecurityPlugin.getMetadataReadableFields', () => {
8182
const dataPlane = await plugin.getReadableFields('deal', context);
8283
const metadataPlane = await plugin.getMetadataReadableFields('deal', context);
8384

84-
// The data plane keeps its middleware-mirroring fall-open — unchanged.
85+
// The data plane keeps its fall-open answer — unchanged (the middleware it
86+
// once mirrored refuses this principal-less caller since ADR-0096 D5).
8587
expect(dataPlane).toEqual(['id', 'name', 'secret']);
8688
// The metadata plane does not.
8789
expect(metadataPlane).toEqual(['id', 'name']);

‎packages/plugins/plugin-security/src/security-plugin.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6058,11 +6058,14 @@ export class SecurityPlugin implements Plugin {
60586058
* `/auth/me/permissions` uses ({@link fallbackPermissionSet}, default
60596059
* `member_default`) instead of falling open to the full field set.
60606060
*
6061-
* Why the two differ rather than converge. `getReadableFields` mirrors the
6062-
* engine middleware, which skips its grant-based gates for a caller with no
6063-
* permission sets — reporting a narrowing the data path would not enforce is
6064-
* its own kind of drift, so on the DATA plane falling open is the correct,
6065-
* drift-free answer. What reaches that caller is the object's posture
6061+
* Why the two differ rather than converge. `getReadableFields` keeps the
6062+
* answer it gave when the engine middleware skipped its grant-based gates for
6063+
* a caller with no permission sets — reporting a narrowing the data path did
6064+
* not enforce was its own kind of drift, so on the DATA plane falling open was
6065+
* the drift-free answer. The middleware no longer serves that caller: object
6066+
* admission refuses it (ADR-0056 D2's deny baseline) and a principal-less one
6067+
* is refused outright (ADR-0096 D5), so no data-plane read reaches the fields
6068+
* named here. What reaches that caller is the object's posture
60666069
* ({@link resolveCallerPosture}): [#20995] its masking rules, and a masked
60676070
* field is a served column, so it stays; [#21063] its per-field capability
60686071
* contract, and a capability-gated field is not served, so it leaves. The

0 commit comments

Comments
 (0)