|
| 1 | +# Recheck: "parsed-but-unenforced security props" — current state |
| 2 | + |
| 3 | +**Date**: 2026-07-25 · **Supersedes**: the security cluster of |
| 4 | +[`README.md` §1](./README.md) and [`2026-06-security-identity-property-liveness.md`](./2026-06-security-identity-property-liveness.md) |
| 5 | +(both dated 2026-06-15). · **Umbrella**: #1878. |
| 6 | + |
| 7 | +The 2026-06 audit flagged a cluster of security-shaped properties as |
| 8 | +"parsed-but-unenforced — latent access-control gaps / false compliance." Those |
| 9 | +docs are now **substantially stale**: the gap was closed the correct way — the |
| 10 | +dead spec surface was *deleted*, the real capabilities were *rebuilt as enforced |
| 11 | +settings / engine paths*, and the remaining schema-only items were *pruned*. |
| 12 | +This recheck records the current per-property status with `file:line` evidence |
| 13 | +so the umbrella can be closed on this cluster and the two remaining prune |
| 14 | +candidates can be actioned. |
| 15 | + |
| 16 | +> **Method note.** Three independent read-only passes over the open-framework |
| 17 | +> PRIMARY checkout. Enforcement that ships in the closed |
| 18 | +> `@objectstack/security-enterprise` / cloud `service-ai` packages is **not** |
| 19 | +> in this repo — where a capability is enterprise/cloud-enforced it is called |
| 20 | +> out explicitly, and the open edition's **fail-closed** default is cited. |
| 21 | +
|
| 22 | +## Per-property status |
| 23 | + |
| 24 | +| Property (2026-06 claim) | Current status | Evidence | Disposition | |
| 25 | +|---|---|---|---| |
| 26 | +| **`PolicySchema`** — "100% dead, false compliance" | **DELETED, then rebuilt as enforced settings** | Schema removed in v11.0 (`#2387`, ADR-0049 enforce-or-**remove**); capabilities rebuilt on the auth Settings namespace by **ADR-0069** (`service-settings/.../auth.manifest.ts`, bound via `plugin-auth/src/auth-plugin.ts` `bindAuthSettings`) | keep-removed | |
| 27 | +| ├ password complexity / min-length | **ENFORCED** | `auth-manager.ts` `assertPasswordComplexity` (sign-up/reset/change); native `min/maxPasswordLength` | keep | |
| 28 | +| ├ HIBP breach check | **ENFORCED** (opt-in) | `auth-manager.ts` mounts `haveIBeenPwned()` when `password_reject_breached` | keep | |
| 29 | +| ├ account lockout | **ENFORCED** | `assertAccountNotLocked` + `recordSignInOutcome` (`failed_login_count` / `locked_until`), settings `lockout_threshold`/`lockout_duration_minutes` | keep | |
| 30 | +| ├ MFA / `forceMfa` | **ENFORCED** (renamed) | `computeAuthGate` → `MFA_REQUIRED`; spec `forceMfa` → setting `mfa_required` + per-org `sys_organization.require_mfa` | keep (intentional naming drift) | |
| 31 | +| ├ session timeout (expiry/idle/absolute/concurrent) | **ENFORCED** | native `session.expiresIn`/`updateAge` + `enforceSessionControls` (P2) | keep | |
| 32 | +| ├ IP allow-list | **PARTIAL** — global enforced; per-org/user not landed | `AuthManager.isClientIpAllowed` + auth-route 403; per-org `sys_organization.allowed_ip_ranges` unimplemented | wire per-org (**#2571**) | |
| 33 | +| ├ audit retention (old `PolicySchema.audit.*`) | spec field gone; behavior on the **lifecycle** surface | retention enforced by LifecycleService from `lifecycle` declarations + settings | keep | |
| 34 | +| **Permission `allowTransfer`** | **ENFORCED** | owner-write guard `plugin-security/src/security-plugin.ts` → `checkObjectPermission('transfer', …)` → `PermissionDeniedError` (#3004) | keep | |
| 35 | +| **Permission `allowRestore` / `allowPurge`** | **PARTIAL** — bits pre-mapped + fail-closed; the `restore`/`purge` ops are M2-pending (nothing ungated) | `permission-evaluator.ts` `DESTRUCTIVE_OPERATIONS` deny-by-default; no `restore`/`purge` ObjectQL op exists yet | keep (M2 roadmap) | |
| 36 | +| **Object `apiEnabled` / `apiMethods`** | **ENFORCED** on both surfaces | hono `rest-server.ts` `enforceApiAccess` (404/405); runtime dispatcher `http-dispatcher.ts` → `api-exposure.ts checkApiExposure` (landed #1937; `import`/`export` verbs added #3391/#3025) | keep | |
| 37 | +| **Action `disabled` (CEL)** | **DEAD in framework** — runtime gating is an objectui concern | spec converged on `disabled` (`action.zod.ts`; no `enabled` key); only framework reader is the design-time lint `validate-expressions.ts` | wire-rename in **objectui** (`action-button.tsx`) | |
| 38 | +| **Agent `permissions` / `access`** | **ENFORCED in the shipped runtime** (enterprise/cloud), PARSED-ONLY in tracked source | seat plumbing tracked (`core/src/security/resolve-authz-context.ts`, `plugin-hono-server`); the gate that reads them + 403s ships in the **git-untracked** `service-ai/dist` bundle | keep (document as enterprise-enforced) | |
| 39 | +| **Agent `visibility` / `tenantId`** | **PRUNED** | removed as security-shaped fields that lie (`#1901` / `#2377`) | done | |
| 40 | +| **Flow `runAs`** | **ENFORCED** (data ops) | `service-automation/src/engine.ts` → `runtime-identity.ts` → `builtin/crud-nodes.ts`; dogfood `flow-runas.dogfood.test.ts` | keep | |
| 41 | +| **Role `parent`** (manager rollup) | **DEAD / never existed** | "Role" → `sys_position`, explicitly **flat** (`identity/position.zod.ts`, ADR-0090 D3); hierarchy uses the BU tree + `sys_user.manager_id`, not a position parent | done (superseded) | |
| 42 | +| **SharingRuleSchema** | **PARTIAL** — criteria-type enforced; owner-type + group/guest recipients dead | registered `metadata/src/plugin.ts`; enforce path `plugin-sharing/.../sharing-rule-service.ts` (criteria → `recordMatches` → `expandRecipient` → `sys_record_share`); owner-type / group / guest recipients skipped in `bootstrap-declared-sharing-rules.ts` | **enforce-or-prune** the dead recipient types | |
| 43 | +| **ADR-0057 hierarchy scope** (`unit`/`unit_and_below`/`own_and_reports`) | **ENFORCED** mechanism; `own`/`org` concrete here, the three hierarchy depths **fail-closed** to enterprise resolver | evaluator `permission-evaluator.ts` → filter `sharing-service.ts` → live queries `sharing-plugin.ts`; hierarchy depths via pluggable `IHierarchyScopeResolver`, open edition returns owner-only when absent; authoring gate `stack.zod.ts` requires `hierarchy-security` | keep (seam correct, fail-closed) | |
| 44 | + |
| 45 | +## Net |
| 46 | + |
| 47 | +The "false compliance" narrative no longer holds: of the eight flagged items, |
| 48 | +**five are enforced** (password/MFA/lockout/session policy, `allowTransfer`, |
| 49 | +`apiEnabled`/`apiMethods`, `runAs`, ADR-0057 scope + criteria SharingRules), |
| 50 | +**two were correctly pruned** (`PolicySchema`, agent `visibility`; `role.parent` |
| 51 | +never existed), and the rest are roadmap (`allowRestore`/`allowPurge` M2) or |
| 52 | +enterprise/cloud-enforced (agent `access`). |
| 53 | + |
| 54 | +## Genuine remaining loose ends (actionable) |
| 55 | + |
| 56 | +1. **Prune `AuditRetentionPolicySchema`** (`packages/spec/src/system/audit.zod.ts`) |
| 57 | + — a real parsed-only/dead spec schema with no runtime consumer; retention is |
| 58 | + enforced on the `lifecycle` surface. Prune (or wire-rename to `lifecycle`). |
| 59 | +2. **SharingRule dead recipient types** — `owner`-type rules and `group`/`guest` |
| 60 | + recipients are skipped by the bootstrap while criteria-type rules enforce. |
| 61 | + Enforce-or-prune the dead subset so authored rules don't silently no-op. |
| 62 | +3. **Per-org / per-user IP allow-list** — global-only today; per-org |
| 63 | + `sys_organization.allowed_ip_ranges` is unimplemented (tracked **#2571**). |
| 64 | +4. **Doc / ledger drift** (non-code-behavior): |
| 65 | + - `packages/qa/dogfood/test/authz-conformance.matrix.ts` still marks |
| 66 | + `flow-run-as` as `removed / ADR-0049 → roadmap M2`, contradicting the live |
| 67 | + enforcement + passing dogfood tests. Stale ledger entry. |
| 68 | + - The agent `access`/`permissions` schema comment asserts in-repo |
| 69 | + enforcement, but that gate lives in the untracked `service-ai/dist` — the |
| 70 | + comment should point at the enterprise package. |
| 71 | + - This doc supersedes the two stale 2026-06 security audits (annotated). |
| 72 | + |
| 73 | +None of these is a live "authored-but-silently-unenforced" access-control gap of |
| 74 | +the kind the 2026-06 audit warned about; they are cleanup + one tracked feature. |
0 commit comments