Skip to content

Commit 749be7d

Browse files
os-zhuangclaude
andcommitted
docs(audit): Security/Identity + System/Integration property liveness
MOST SERIOUS findings: PolicySchema 100% dead (password/session/mfa/ip/audit parsed-but-unenforced; not even registered) -> false compliance; permission allowTransfer/allowRestore/allowPurge dead (destructive ops ungated); Role parent dead (no manager rollup); SharingRuleSchema disconnected from the live sys_sharing_rule engine. System: EmailTemplateSchema registered but runtime reads differently-shaped sys_email_template; PortalSchema fully aspirational; Job retryPolicy/timeout dead; Webhook non-HMAC auth/payloadFields dead; TranslationConfig messageFormat(ICU)/supportedLocales/cache dead; Theme spacing/breakpoints/logo merged-but-never-emitted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2b80dcc commit 749be7d

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Audit: Security/Identity metadata liveness & necessity
2+
3+
**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.**
4+
5+
## 🔴🔴 PolicySchema is 100% DEAD (highest impact)
6+
Every prop — `password.*` (minLength/requireUppercase/…), `session.*` (idle/absolute timeout), `forceMfa`, `network.*` (`trustedRanges`/`blockUnknown`/`vpnRequired`), `audit.*` (retention/redaction), `isDefault`, `assignedProfiles` — has **zero runtime consumers**, and `PolicySchema` **isn't even registered as a metadata type** (absent from `metadata-type-schemas.ts`). `better-auth` (`auth-manager.ts:458`) runs its own **hardcoded** session/scrypt config, fully independent. **Authoring a security/compliance Policy gives a false sense of compliance with zero enforcement.**
7+
8+
## 🔴 PermissionSet — destructive lifecycle ops not gated
9+
- `objects.*.allowTransfer` / `allowRestore` / `allowPurge`**DEAD**: omitted from `OPERATION_TO_PERMISSION` (`permission-evaluator.ts:8-16`). Ownership transfer, undelete, and hard-delete/GDPR purge (the most destructive ops) are **not gated by RBAC**.
10+
- `isProfile` — DEAD (profile-vs-permset never gates anything).
11+
- `systemPermissions` — PARTIAL: enforced only for **app-entry/nav visibility** (`hono-plugin.ts:741`), **not** as a general capability gate (e.g. `manage_users` is checked nowhere in the data path).
12+
- `tabPermissions` — PARTIAL: only `'hidden'` is read; `default_on`/`default_off` never read; UI-only, not a boundary.
13+
- `contextVariables` (RLS) — **DEAD**: `rls-compiler.ts` never reads it (doc claims runtime evaluation; RLS uses only `current_user.*` built-ins).
14+
- **LIVE & fail-closed**: `objects.*` CRUD, `viewAllRecords`/`modifyAllRecords`, `fields.*` FLS (read-mask **and** write-deny), `rowLevelSecurity` (find + analytics raw-SQL).
15+
16+
## 🔴 Role.parent is DEAD
17+
`team-graph.ts:27` explicitly does **not** walk a hierarchy. The schema's documented "managers see subordinates' data" rollup is **unimplemented** everywhere. `label`/`description` display-only.
18+
19+
## 🔴 SharingRuleSchema is disconnected from the live engine
20+
The runtime enforces a **separate, divergent** `sys_sharing_rule`/`sys_record_share` model: `criteria_json` is a **JSON ObjectQL filter, not** the spec's CEL `condition` (unparsable CEL → "match nothing", `sharing-rule-service.ts:33`); `recipient_type ∈ user/team/department/role/queue`, **not** the spec enum (`role_and_subordinates`/`group`/`guest`). The spec `SharingRuleSchema` has **no runtime consumer** — authoring it has no effect. (Runtime sharing itself IS live + enforced via `sharing-plugin.ts:227`, just under a different contract; role expansion is flat — no subordinate rollup.)
21+
22+
## Recommendation (security ADR — high priority)
23+
1. **PolicySchema**: enforce (wire into better-auth + audit) or delete — shipping unenforced security policy is a compliance liability.
24+
2. **Gate `allowTransfer`/`allowRestore`/`allowPurge`** (add to `OPERATION_TO_PERMISSION`) — destructive ops must be permissioned.
25+
3. **Reconcile SharingRuleSchema with `sys_sharing_rule`** (one contract) or document the spec as non-authoritative.
26+
4. Implement or remove Role `parent` hierarchy and RLS `contextVariables`.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Audit: System/Integration metadata liveness & necessity
2+
3+
**Date**: 2026-06-15 · **Scope**: EmailTemplateSchema, TranslationBundle/Config, ThemeSchema, JobSchema, WebhookSchema, PortalSchema. **Consumers**: email/messaging/i18n/job/webhook services + objectui theme engine.
4+
5+
## 🔴 EmailTemplateSchema — worst drift case
6+
Registered as a live metadata type (`email_template`, `metadata-plugin.zod.ts:106`) **but the entire runtime email path reads a structurally different object** `sys_email_template` (columns `body_html`/`body_text`/`from_name`/`from_address`/`variables_json`/`locale`) — not the spec's `body`/`bodyType`/`variables`. The spec `EmailTemplateSchema` has **zero importers**. Reconcile with `sys_email_template` or delete.
7+
8+
## 🔴 PortalSchema — 100% aspirational
9+
Not registered as a metadata type (no `'portal'` in `metadata-plugin.zod.ts`), no server route wiring (`routePrefix`/`anonymousEntry` have no consumer), no objectui renderer. 326 lines of richly-documented schema with **no consumer** (the file's own header describes consumers as *future*). Every prop DEAD.
10+
11+
## ThemeSchema — `core/theme/ThemeEngine.ts`
12+
LIVE: `name`, `mode`, `colors`, `typography`, `borderRadius`, `shadows`, `animation`, `zIndex`, `customVars`, `extends` (inheritance). **DEAD**: `spacing`, `breakpoints`, `logo` (merged by `mergeThemes` but **never emitted** as CSS vars), `density`, `rtl`, `touchTarget`, `keyboardNavigation`. PARTIAL: `wcagContrast` (`meetsContrastLevel` helper exists but isn't driven by the prop). `label`/`description` display-only.
13+
14+
## TranslationConfig — 5 dead knobs
15+
LIVE: `defaultLocale` (`app-plugin.ts:838`), `fallbackLocale` (i18n adapter). **DEAD**: `supportedLocales`, `messageFormat` (**no ICU engine exists anywhere**`messageFormat:'icu'` is an unkept promise), `fileOrganization`, `lazyLoad`, `cache`. TranslationData groups (objects/apps/messages) resolve via generic dot-path — convention, not validated keys. HTTP locale fallback uses a heuristic `resolveLocale()`, not the configured `fallbackLocale`.
16+
17+
## JobSchema — `app-plugin.ts:382` → service-job adapters
18+
LIVE: `name`, `schedule` (cron/interval/once: expression/timezone/intervalMs/at), `handler`, `enabled`. **DEAD**: `id`, `label`, `description`, **`retryPolicy`**, **`timeout`** — the cron adapter's `execute()` only try/catch-logs; no retry/backoff/timeout despite the detailed `RetryPolicySchema`.
19+
20+
## WebhookSchema (outbound) — `sys_webhook` + `auto-enqueuer.ts`
21+
LIVE: `name`, `object` (→ row `object_name`, **naming drift**), `triggers`, `url`, `method`, `headers`, `authentication.secret` (HMAC), `timeoutMs`, `isActive` (→ row `active`, **drift**). **DEAD**: `body`, `payloadFields`, `includeSession`, `authentication` (bearer/basic/api-key block — only `secret` read), `retryPolicy` (owned by the outbox), `description`, `tags`. `WebhookReceiverSchema` (inbound) — entirely DEAD.
22+
23+
## Headline
24+
EmailTemplate (live type, dead shape) and Portal (fully aspirational) are the two worst; both should be reconciled-or-deleted. Job `retryPolicy`/`timeout`, Webhook auth (non-HMAC) + `payloadFields`, Theme `spacing`/`breakpoints`, and TranslationConfig `messageFormat`-ICU are richly-specced features with no runtime — classic aspirational config that misleads authors.

0 commit comments

Comments
 (0)