|
19 | 19 | // [#8711] That completeness is over ROUTES, not over primitives: a primitive |
20 | 20 | // enforced by a predicate inside an existing resolver adds no entry point, so |
21 | 21 | // it can be neither UNCLASSIFIED nor STALE. Measured against the rows below: |
22 | | -// 43 of 50 carry no `covers` key at all (7 rows, 9 keys, every one an |
23 | | -// HTTP/transport pin), and 37 of the file's 43 `enforced` rows are exactly |
24 | | -// that in-resolver shape — the ADR-0049/#8613 `active` rows among them (see |
25 | | -// their own block further down) are the normal case, not an exception. Of the |
| 22 | +// 44 of 51 carry no `covers` key at all (7 rows, 9 keys, every one an |
| 23 | +// HTTP/transport pin), and 38 of the file's 44 `enforced` rows are exactly |
| 24 | +// that in-resolver shape — the ADR-0049/#8613 `active` rows and the ADR-0091 |
| 25 | +// grant-validity-window row among them (see their own blocks further down) |
| 26 | +// are the normal case, not an exception. Of the |
26 | 27 | // 9 `covers` keys that DO exist, 5 are GATE pins tied to the enforcement call |
27 | 28 | // itself, not merely a function name — delete `shouldDenyAnonymous` from |
28 | 29 | // `/actions`, `/automation` or `/packages`, or drop the MCP context-threading |
@@ -285,6 +286,25 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ |
285 | 286 | enforcement: 'core/security/resolve-authz-context.ts step 6a — isRowActive gates BOTH halves, and only both hold it: (i) only ACTIVE position ids collect their `sys_position_permission_set` linkage, so a deactivated position carries no bound set; (ii) the deactivated NAME is dropped from `grants.positions`, because resolvePermissionSetsForContext requests positions as permission-set NAMES and a name left standing resolves the same grant one layer down', |
286 | 287 | note: 'Only a name whose `sys_position` row is EXPLICITLY deactivated is dropped — a name with no row at all (`org_owner`, a membership-derived role, the built-in `everyone` audience anchor) has no flag to read and is untouched. Deliberately NOT a blanket revocation of the sets themselves: a set held via BOTH a deactivated position AND a direct user grant still resolves, since the direct grant is a different grant (resolve-authz-context.test.ts pins exactly that case). Symmetrically, the WRITE gates and blast-radius reads in plugin-security (assertAudienceAnchorBindingGate, setsBoundToPosition, the delegated-admin surfaces) stay UNFILTERED on purpose — dropping a deactivated row there would make a refused binding permitted, narrow a delegate\'s boundary, and make a deactivated position unmanageable. Unit-proven in core/security/resolve-authz-context.test.ts (a deactivated position stops granting its sets; an active one still grants; an absent column grants; the 0/1 shape deactivates; deactivating ONE position leaves the others granting) + core/security/row-active.test.ts. Not HIGH_RISK for the same reason as `permission-set-active`.' }, |
287 | 288 |
|
| 289 | + // ── ADR-0091 D1/D2 — grant validity windows (#8811) ─────────────────── |
| 290 | + // |
| 291 | + // The sibling of the `active` switch above, and enforced at the same seam |
| 292 | + // for the same stated reason: a grant that is supposed to lapse on a date, |
| 293 | + // but only lapses when a cleanup job runs, is an unenforced security |
| 294 | + // property (ADR-0049). `valid_from` / `valid_until` are therefore read at |
| 295 | + // RESOLUTION time by one shared predicate, `isGrantActive` |
| 296 | + // (@objectstack/core), so no reader can drift from another — the same |
| 297 | + // one-predicate discipline `isRowActive` gets above. |
| 298 | + // |
| 299 | + // The window is half-open `[from, until)` in UTC: absent/null bounds mean |
| 300 | + // unbounded (pre-ADR-0091 rows behave exactly as before), and a bound that |
| 301 | + // is PRESENT but unparseable DISABLES the grant — fail-closed, deliberately |
| 302 | + // unlike API-key `isExpired`, because a grant row is standing authority |
| 303 | + // rather than a single credential. |
| 304 | + { id: 'grant-validity-window', summary: '`valid_from` / `valid_until` on a grant row — a grant outside its half-open window resolves to nothing, at resolution time and with no cleanup job (ADR-0091 D1/D2)', state: 'enforced', |
| 305 | + enforcement: 'core/security/grant-validity.ts isGrantActive — ONE half-open [valid_from, valid_until) predicate (UTC; absent bound = unbounded; a PRESENT but unparseable bound fails CLOSED), applied by every reader that turns a window-carrying row into authority: core/security/resolve-authz-context.ts step 4 (sys_user_position) and step 6 (sys_user_permission_set, dropped BEFORE any derivation so an expired admin_full_access cannot yield platform_admin either); plugin-security/explain-engine.ts buildContextForUser (plus isGrantExpired for the dedicated "expired" contributor state); plugin-sharing/position-graph.ts PositionGraphService.expandPositionUsers; plugin-security/delegated-admin-gate.ts held-scope resolution; plugin-auth/last-admin-guard.ts administrator-standing reads; plugin-approvals/approval-service.ts lookupActiveDelegation (sys_approval_delegation); runtime/domains/keys.ts membership check', |
| 306 | + note: '[#8811] NO `covers`, and — exactly like the two `active` rows above — that is a statement about the RATCHET rather than an omission: `discover()` enumerates HTTP entry points from a curated per-file probe table, and a predicate inside an existing resolver adds no entry point, so this primitive could never have surfaced as UNCLASSIFIED or STALE during any period it was inert. Per the #8711 ruling (2026-08-15) the invariant\'s advertised SCOPE is narrowed to what the ratchet can check; this row is owed under the hand-maintained half the file header still states. WHAT IS AND IS NOT WINDOW-FILTERED — measured at the call sites, not taken from the filing: the columns are declared on exactly THREE objects (sys_user_position, sys_user_permission_set, sys_approval_delegation), and on all three EVERY authorization-resolution seam applies the predicate, which is what makes this row `enforced` rather than partial. Two reads deliberately do NOT filter and neither is a live hole: (i) `sys_member` carries no window columns at all, so the resolver\'s membership call (which builds accessible_org_ids) is FORWARD-LOOKING, and the org-administration role projection beside it is NOT window-filtered (#8802) — both are no-ops until the columns exist, and last-admin-guard.ts\'s MEMBER standing-key notes already record that the resolver is where the two halves must be reconciled FIRST if they ever land; (ii) plugin-approvals\' own expandPositionUsers projects user_id alone by the #8710 ruling, because approval ROUTING is not grant resolution — that note explicitly forbids "fixing" it with a filter. ⛔ #8802 is a defect IN the enforcement, not in this ledger: it does not change this row\'s state, and closing it leaves the row exactly as owed. Predicate unit-pinned in core/security/grant-validity.test.ts (half-open boundaries, seconds-vs-ms epoch, fail-closed on garbage, camelCase spellings); the resolver halves in core/security/resolve-authz-context.test.ts "grant validity windows (ADR-0091 D1/D2)". Deliberately NOT in HIGH_RISK for the same reason as the `active` rows — that list marks primitives guarding object DATA through a sibling HTTP entry point, and this one guards grant DERIVATION. Unlike those rows an end-to-end proof already EXISTS and is merely unclaimed by this ledger: delegation-of-duty.dogfood.test.ts boots a real stack and asserts the delegate STOPS resolving the delegated sys_user_position at valid_until through the real resolveAuthzContext. It is not cited here because the #7976 mutual-attribution contract requires that file to name this row back (a `// authz-row:` header line), an edit outside this card\'s declared file surface — filed as a follow-up.' }, |
| 307 | + |
288 | 308 | // ── Experimental — declared, NOT enforced (ADR-0049/0056 D8) ─────────── |
289 | 309 | { id: 'field-encryption', summary: 'at-rest field encryption', state: 'experimental', |
290 | 310 | note: 'no crypto provider reads the config; marked [EXPERIMENTAL] (D8). Deliberately KEPT (2026-07 D8 disposition): at-rest encryption is a real enterprise roadmap item with a stable schema shape — removing and re-adding would cost more (ADR-0087) than carrying it marked.' }, |
|
0 commit comments