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
13 changes: 13 additions & 0 deletions .changeset/security-props-liveness-recheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
---

docs(audits): recheck the "parsed-but-unenforced security props" cluster (#1878)

Records the current per-property enforcement status of the security cluster the
2026-06 audit flagged as "false compliance." Most items are now enforced
(ADR-0069 auth settings, `allowTransfer`, `apiEnabled`/`apiMethods`, flow
`runAs`, ADR-0057 scope, criteria SharingRules) or were correctly pruned
(`PolicySchema`, agent `visibility`, `role.parent`). Supersedes the two stale
2026-06 security audit docs (annotated); lists the genuine remaining loose ends
(prune `AuditRetentionPolicySchema`, enforce-or-prune SharingRule owner/group/
guest recipients, per-org IP allow-list #2571). Docs-only; releases nothing.
7 changes: 7 additions & 0 deletions docs/audits/2026-06-security-identity-property-liveness.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Audit: Security/Identity metadata liveness & necessity

> **⚠️ SUPERSEDED — 2026-07-25.** This point-in-time snapshot is now factually
> wrong in several places (notably "PolicySchema is 100% dead" — it was *deleted*
> in #2387 and its capabilities *rebuilt as enforced settings* by ADR-0069). For
> the current per-property status and the genuine remaining loose ends, see
> [`2026-07-security-props-liveness-recheck.md`](./2026-07-security-props-liveness-recheck.md).
> The body below is retained as the historical record.

**Date**: 2026-06-15 · **Scope**: `packages/spec/src/{identity,security}/*.zod.ts` — RoleSchema, PermissionSetSchema, PolicySchema, SharingRuleSchema. **Consumers**: `plugin-security`, `plugin-sharing`, `plugin-auth`, `objectql`. No dedicated Studio designers — generic metadata-admin forms only. **⚠️ This layer is security-critical: "parsed but unenforced" = latent access-control gap.**

## 🔴🔴 PolicySchema is 100% DEAD (highest impact)
Expand Down
74 changes: 74 additions & 0 deletions docs/audits/2026-07-security-props-liveness-recheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Recheck: "parsed-but-unenforced security props" — current state

**Date**: 2026-07-25 · **Supersedes**: the security cluster of
[`README.md` §1](./README.md) and [`2026-06-security-identity-property-liveness.md`](./2026-06-security-identity-property-liveness.md)
(both dated 2026-06-15). · **Umbrella**: #1878.

The 2026-06 audit flagged a cluster of security-shaped properties as
"parsed-but-unenforced — latent access-control gaps / false compliance." Those
docs are now **substantially stale**: the gap was closed the correct way — the
dead spec surface was *deleted*, the real capabilities were *rebuilt as enforced
settings / engine paths*, and the remaining schema-only items were *pruned*.
This recheck records the current per-property status with `file:line` evidence
so the umbrella can be closed on this cluster and the two remaining prune
candidates can be actioned.

> **Method note.** Three independent read-only passes over the open-framework
> PRIMARY checkout. Enforcement that ships in the closed
> `@objectstack/security-enterprise` / cloud `service-ai` packages is **not**
> in this repo — where a capability is enterprise/cloud-enforced it is called
> out explicitly, and the open edition's **fail-closed** default is cited.

## Per-property status

| Property (2026-06 claim) | Current status | Evidence | Disposition |
|---|---|---|---|
| **`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 |
| ├ password complexity / min-length | **ENFORCED** | `auth-manager.ts` `assertPasswordComplexity` (sign-up/reset/change); native `min/maxPasswordLength` | keep |
| ├ HIBP breach check | **ENFORCED** (opt-in) | `auth-manager.ts` mounts `haveIBeenPwned()` when `password_reject_breached` | keep |
| ├ account lockout | **ENFORCED** | `assertAccountNotLocked` + `recordSignInOutcome` (`failed_login_count` / `locked_until`), settings `lockout_threshold`/`lockout_duration_minutes` | keep |
| ├ MFA / `forceMfa` | **ENFORCED** (renamed) | `computeAuthGate` → `MFA_REQUIRED`; spec `forceMfa` → setting `mfa_required` + per-org `sys_organization.require_mfa` | keep (intentional naming drift) |
| ├ session timeout (expiry/idle/absolute/concurrent) | **ENFORCED** | native `session.expiresIn`/`updateAge` + `enforceSessionControls` (P2) | keep |
| ├ 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**) |
| ├ audit retention (old `PolicySchema.audit.*`) | spec field gone; behavior on the **lifecycle** surface | retention enforced by LifecycleService from `lifecycle` declarations + settings | keep |
| **Permission `allowTransfer`** | **ENFORCED** | owner-write guard `plugin-security/src/security-plugin.ts` → `checkObjectPermission('transfer', …)` → `PermissionDeniedError` (#3004) | keep |
| **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) |
| **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 |
| **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`) |
| **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) |
| **Agent `visibility` / `tenantId`** | **PRUNED** | removed as security-shaped fields that lie (`#1901` / `#2377`) | done |
| **Flow `runAs`** | **ENFORCED** (data ops) | `service-automation/src/engine.ts` → `runtime-identity.ts` → `builtin/crud-nodes.ts`; dogfood `flow-runas.dogfood.test.ts` | keep |
| **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) |
| **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 |
| **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) |

## Net

The "false compliance" narrative no longer holds: of the eight flagged items,
**five are enforced** (password/MFA/lockout/session policy, `allowTransfer`,
`apiEnabled`/`apiMethods`, `runAs`, ADR-0057 scope + criteria SharingRules),
**two were correctly pruned** (`PolicySchema`, agent `visibility`; `role.parent`
never existed), and the rest are roadmap (`allowRestore`/`allowPurge` M2) or
enterprise/cloud-enforced (agent `access`).

## Genuine remaining loose ends (actionable)

1. **Prune `AuditRetentionPolicySchema`** (`packages/spec/src/system/audit.zod.ts`)
— a real parsed-only/dead spec schema with no runtime consumer; retention is
enforced on the `lifecycle` surface. Prune (or wire-rename to `lifecycle`).
2. **SharingRule dead recipient types** — `owner`-type rules and `group`/`guest`
recipients are skipped by the bootstrap while criteria-type rules enforce.
Enforce-or-prune the dead subset so authored rules don't silently no-op.
3. **Per-org / per-user IP allow-list** — global-only today; per-org
`sys_organization.allowed_ip_ranges` is unimplemented (tracked **#2571**).
4. **Doc / ledger drift** (non-code-behavior):
- `packages/qa/dogfood/test/authz-conformance.matrix.ts` still marks
`flow-run-as` as `removed / ADR-0049 → roadmap M2`, contradicting the live
enforcement + passing dogfood tests. Stale ledger entry.
- The agent `access`/`permissions` schema comment asserts in-repo
enforcement, but that gate lives in the untracked `service-ai/dist` — the
comment should point at the enterprise package.
- This doc supersedes the two stale 2026-06 security audits (annotated).

None of these is a live "authored-but-silently-unenforced" access-control gap of
the kind the 2026-06 audit warned about; they are cleanup + one tracked feature.
2 changes: 2 additions & 0 deletions docs/audits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ The most serious cluster — properties that imply a security boundary but enfor
- **Action `disabled`** (CEL form) — silently ignored (renderer reads non-spec `enabled`).
- **Role `parent`** / **SharingRuleSchema** — manager rollup & spec sharing rules disconnected from the live engine.

**✅ Rechecked 2026-07-25 — this cluster is largely closed.** `PolicySchema` was *deleted* (#2387) and its capabilities *rebuilt as enforced settings* (ADR-0069: password/MFA/lockout/session/IP); `allowTransfer`, `apiEnabled`/`apiMethods`, flow `runAs`, ADR-0057 scope and criteria-type SharingRules are all **enforced**; agent `visibility` and `role.parent` were pruned (the latter never existed — positions are flat, ADR-0090). Remaining: prune `AuditRetentionPolicySchema`, enforce-or-prune SharingRule owner/group/guest recipients, per-org IP allow-list (#2571). Full per-property evidence: [`2026-07-security-props-liveness-recheck.md`](./2026-07-security-props-liveness-recheck.md).

### 2. 🔴 ADR-0021 analytics migration debt
Spec moved to `dataset`+`values`/`dimensions`, but: the **chart view variant** + **dashboard renderer + Studio WidgetConfigPanel** still read the *removed* legacy `object/valueField/categoryField/aggregate` shape. (Same debt that invalidated the showcase dashboard/report seeds.)

Expand Down