From bc03362a9c4a369652020d5565b8f28cd11c243d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 15:42:29 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(platform-objects):=20secure-by-default?= =?UTF-8?q?=20posture=20for=20sensitive=20system=20objects=20(ADR-0066=20?= =?UTF-8?q?=E2=91=A3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The platform's raw secret/credential stores no longer ride the wildcard `'*'` permission grant. `sys_secret`, `sys_jwks`, `sys_verification`, `sys_oauth_access_token`, `sys_oauth_refresh_token`, and `sys_device_code` declare `access: { default: 'private' }` — an ordinary member's generic data-layer access gets 403; platform admins retain access via the posture-gated viewAllRecords/modifyAllRecords superuser bypass. Every runtime consumer is unaffected (verified per reader): better-auth reads via its adapter (system context), `engine.resolveSecret` reads at driver level, SettingsService / the datasource secret-binder read principal-less (middleware falls open for internal calls), API-key auth reads isSystem. - `sys_scim_provider` gains `requiredPermissions: ['manage_platform_settings']` (D3 capability gate), mirroring its sibling `sys_sso_provider`. - Setup nav `nav_jwks` is capability-gated like `nav_api_keys` so non-admins don't see a menu entry that can only 403. - Member self-service objects (sys_session, sys_api_key, sys_oauth_application, sys_two_factor) deliberately keep the public posture — the Account app reads them as the member; row scoping is their guard. A test asserts this exclusion too. - Pinned against regression: platform-objects.test.ts posture suite + the ADR-0056 D10 conformance-matrix row `secure-by-default-posture`. Tests: platform-objects 71, plugin-auth 230, dogfood conformance + anonymous-deny + two-doors e2e, service-settings 129, service-datasource 93 — all green; full turbo build green. Refs #2561 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0187GeNqezxV6g5jiLiggfbt --- .../secure-by-default-system-objects.md | 34 ++++++++++++++ content/docs/permissions/authorization.mdx | 10 +++++ docs/adr/0066-unified-authorization-model.md | 2 +- .../dogfood/test/authz-conformance.matrix.ts | 3 ++ .../src/apps/setup-nav.contributions.ts | 6 ++- .../src/identity/sys-device-code.object.ts | 6 +++ .../src/identity/sys-jwks.object.ts | 7 +++ .../identity/sys-oauth-access-token.object.ts | 5 +++ .../sys-oauth-refresh-token.object.ts | 5 +++ .../src/identity/sys-scim-provider.object.ts | 8 ++++ .../src/identity/sys-verification.object.ts | 6 +++ .../src/platform-objects.test.ts | 45 ++++++++++++++++++- .../src/system/sys-secret.object.ts | 8 ++++ 13 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 .changeset/secure-by-default-system-objects.md diff --git a/.changeset/secure-by-default-system-objects.md b/.changeset/secure-by-default-system-objects.md new file mode 100644 index 0000000000..eb3c895db1 --- /dev/null +++ b/.changeset/secure-by-default-system-objects.md @@ -0,0 +1,34 @@ +--- +'@objectstack/platform-objects': minor +--- + +Secure-by-default posture for sensitive system objects (ADR-0066 ④, system-object +slice) — the platform's raw secret/credential stores no longer ride the wildcard +`'*'` permission grant. + +`sys_secret` (encrypted settings/datasource secrets), `sys_jwks` (JWT signing +keys), `sys_verification` (password-reset / verify tokens), +`sys_oauth_access_token`, `sys_oauth_refresh_token` (live bearer credentials), +and `sys_device_code` (pending device-grant codes) now declare +`access: { default: 'private' }`: an ordinary member's generic data-layer +read/write gets 403 instead of being covered by `member_default`'s +`'*': allowRead`. Platform admins retain access via the posture-gated +`viewAllRecords`/`modifyAllRecords` superuser bypass, and every runtime consumer +is unaffected — better-auth reads via its adapter (system context), +`engine.resolveSecret` reads at driver level, and SettingsService / the +datasource secret-binder read principal-less (middleware falls open for internal +calls). + +`sys_scim_provider` (SCIM bearer-token config) gains the object-level +`requiredPermissions: ['manage_platform_settings']` capability gate, mirroring +its sibling `sys_sso_provider`. The Setup nav item for Signing Keys (JWKS) is +now capability-gated like API Keys, so non-admins don't see a menu entry that +can only 403. + +Member self-service objects (`sys_session`, `sys_api_key`, +`sys_oauth_application`, `sys_two_factor`) deliberately keep the public posture — +the Account app ("My Sessions" / "My API Keys" / "My Apps" / 2FA "My +Enrollment") reads them through the generic data layer as the member; row +scoping remains their guard. The declarations are pinned by +`platform-objects.test.ts` and the ADR-0056 D10 conformance-matrix row +`secure-by-default-posture`, so dropping the flag from a secret store fails CI. diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx index 2966a1e74e..14ac3ce36d 100644 --- a/content/docs/permissions/authorization.mdx +++ b/content/docs/permissions/authorization.mdx @@ -181,6 +181,16 @@ The complete, prioritized gap map lives in issue **#2561** (the production object's `requiredPermissions` may be a `string[]` (gates all CRUD) or a `{ read, create, update, delete }` map (read-open / write-gated), enforced per operation by plugin-security (`security-plugin.ts` capability AND-gate). +- **Secure-by-default rollout** (ADR-0066 ④) — **system-object slice landed**: + the raw secret/credential stores (`sys_secret`, `sys_jwks`, + `sys_verification`, `sys_oauth_access_token`, `sys_oauth_refresh_token`, + `sys_device_code`) declare `access: { default: 'private' }` — no wildcard + grant reaches them; platform admins retain access via the posture-gated + superuser bypass. `sys_scim_provider` is capability-gated like + `sys_sso_provider`. Member self-service objects (`sys_session`, + `sys_api_key`, `sys_oauth_application`, `sys_two_factor`) deliberately stay + public-posture (the Account app reads them as the member; row scoping is + their guard). Still open: Studio posture surfacing (objectui). - **Deny/muting subtract layer** (ADR-0005 overlay; ADR-0066 precedence step 4) — how an environment adjusts a *packaged* set without forking it; deferred until proven need (ADR-0086 P2). diff --git a/docs/adr/0066-unified-authorization-model.md b/docs/adr/0066-unified-authorization-model.md index b1643173bb..9093a1e7e4 100644 --- a/docs/adr/0066-unified-authorization-model.md +++ b/docs/adr/0066-unified-authorization-model.md @@ -102,7 +102,7 @@ All of this is **open mechanism** (framework `spec` + `plugin-security`): schema Captured for the record; **out of scope for Phases 1–4 above**. Each is anchored to a mainstream-platform precedent. -- **④ Deny-by-default target for sensitive objects.** Salesforce / Dataverse / ServiceNow / SAP are all deny-by-default; ObjectStack stays allow-by-default for *tenant business* objects (low-code ergonomics, à la Airtable/Notion within a workspace) but should make **system / control-plane / sensitive** objects `private` by default, ship genuine reference data (countries, currencies, picklists) as explicit `public`, and surface each object's posture visibly in Studio. The `access` flag (D2) is the primitive; this is a defaults + visibility call, staged per object — no forced migration. +- **④ Deny-by-default target for sensitive objects.** Salesforce / Dataverse / ServiceNow / SAP are all deny-by-default; ObjectStack stays allow-by-default for *tenant business* objects (low-code ergonomics, à la Airtable/Notion within a workspace) but should make **system / control-plane / sensitive** objects `private` by default, ship genuine reference data (countries, currencies, picklists) as explicit `public`, and surface each object's posture visibly in Studio. The `access` flag (D2) is the primitive; this is a defaults + visibility call, staged per object — no forced migration. **System-object slice landed (2026-07):** the raw secret/credential stores — `sys_secret`, `sys_jwks`, `sys_verification`, `sys_oauth_access_token`, `sys_oauth_refresh_token`, `sys_device_code` — declare `access: { default: 'private' }`, and `sys_scim_provider` carries the D3 capability gate (mirroring `sys_sso_provider`); pinned by `platform-objects.test.ts` + the D10 matrix row `secure-by-default-posture`. Member self-service objects (`sys_session`, `sys_api_key`, `sys_oauth_application`, `sys_two_factor`) deliberately stay public-posture — the Account app reads them with a member context; row scoping is their guard. Still open: Studio posture surfacing (objectui) and the explicit-`public` reference-data convention (no such objects ship in framework today). - **⑤ Per-operation `requiredPermissions`.** Today object-level `requiredPermissions` gates all of CRUD. ERP routinely needs "read-open / write-gated" (Salesforce & Dataverse separate capability by operation). Allow `requiredPermissions` to be either `string[]` (all operations) or a per-operation map `{ read, create, update, delete }`. Field-level (D3) and action-level (D4) requirements already give finer control; this closes the object-level gap. **Landed (2026-07):** spec `ObjectRequiredPermissionsSchema` union (`spec/data/object.zod.ts`); enforced per operation in `plugin-security/security-plugin.ts` (the `all` bucket preserves the array form's gate-everything semantics, so it is backward-compatible). `transfer`/`restore` fold into `update`, `purge` into `delete` via `crudBucketForOperation`. - **⑥ Capabilities in the expression surface.** Salesforce *Custom Permissions* are referenceable in formulas / validation / flows (`$Permission.X`). Expose the caller's held capabilities to the CEL/predicate surface (ADR-0058) so `visible` / validation / sharing predicates can branch on a capability. High-leverage once D1 makes capabilities first-class. - **⑦ Permission-set groups + subtractive *muting*.** Pure union does not scale governance ("permission-set explosion"); Salesforce added permission-set-group *muting* precisely to allow taking access away. Roles→permission-sets already bundle; a subtractive/deny layer (precedence step 4) is the missing piece for large-org administration. Pairs with delegated admin (#9). diff --git a/packages/dogfood/test/authz-conformance.matrix.ts b/packages/dogfood/test/authz-conformance.matrix.ts index df54bcc7b6..31a672c8df 100644 --- a/packages/dogfood/test/authz-conformance.matrix.ts +++ b/packages/dogfood/test/authz-conformance.matrix.ts @@ -75,6 +75,9 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ enforcement: 'plugin-security/rls-compiler.ts isSupportedRlsExpression + warn' }, { id: 'system-permissions', summary: 'systemPermissions / tab-app gating', state: 'enforced', enforcement: 'rest/rest-server.ts filterAppForUser' }, + { id: 'secure-by-default-posture', summary: 'ADR-0066 ④ — sensitive system objects opt out of the wildcard grant (access.default: private)', state: 'enforced', + enforcement: 'plugin-security/permission-evaluator.ts resolveObjectPermission (plain wildcard does not cover a private object) + posture-gated superuser bypass; declarations in platform-objects (sys_secret, sys_jwks, sys_verification, sys_oauth_access_token, sys_oauth_refresh_token, sys_device_code) + sys_scim_provider D3 capability gate', + note: 'Primitive enforcement unit-proven in plugin-security/security-plugin.test.ts (ADR-0066 posture suite); the per-object declarations are pinned by platform-objects.test.ts "secure-by-default posture" so dropping the flag from a secret store fails CI, not review. Member self-service objects (sys_session, sys_api_key, sys_oauth_application, sys_two_factor) deliberately stay public-posture — the Account app reads them with a member context; row scoping (owner/tenant RLS + _self carve-outs) is their guard.' }, // ── Experimental — declared, NOT enforced (ADR-0049/0056 D8) ─────────── { id: 'compliance-configs', summary: 'GDPR/HIPAA/PCI configs', state: 'experimental', note: 'no runtime consumer; marked [EXPERIMENTAL] (D8)' }, diff --git a/packages/platform-objects/src/apps/setup-nav.contributions.ts b/packages/platform-objects/src/apps/setup-nav.contributions.ts index 876b10969b..e3c44e9293 100644 --- a/packages/platform-objects/src/apps/setup-nav.contributions.ts +++ b/packages/platform-objects/src/apps/setup-nav.contributions.ts @@ -122,7 +122,11 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [ priority: BASE_PRIORITY, items: [ { id: 'nav_oauth_apps', type: 'object', label: 'OAuth Applications', objectName: 'sys_oauth_application', icon: 'app-window' }, - { id: 'nav_jwks', type: 'object', label: 'Signing Keys (JWKS)', objectName: 'sys_jwks', icon: 'key-round' }, + // nav_jwks is capability-gated (like nav_api_keys): sys_jwks is + // `access.default:'private'` (ADR-0066 ④ — signing keys), so a + // non-admin's list request 403s server-side; gating the nav item keeps + // the menu honest instead of showing an entry that can only error. + { id: 'nav_jwks', type: 'object', label: 'Signing Keys (JWKS)', objectName: 'sys_jwks', icon: 'key-round', requiredPermissions: ['manage_platform_settings'] }, // `sys_verification` (email/phone tokens) and `sys_device_code` (OAuth // device-grant codes) deliberately omit `list` from their `apiMethods` // (sensitive, ephemeral secrets — not browsable), so an object/list-view diff --git a/packages/platform-objects/src/identity/sys-device-code.object.ts b/packages/platform-objects/src/identity/sys-device-code.object.ts index 25857ad495..9c659b777f 100644 --- a/packages/platform-objects/src/identity/sys-device-code.object.ts +++ b/packages/platform-objects/src/identity/sys-device-code.object.ts @@ -26,6 +26,12 @@ export const SysDeviceCode = ObjectSchema.create({ icon: 'key-round', isSystem: true, managedBy: 'better-auth', + // [ADR-0066 D2/④] Secure-by-default: rows are LIVE pending device-grant + // codes — reading `user_code`/`device_code` lets an attacker hijack a + // pending CLI login. Not covered by the wildcard `'*'` grant; admins retain + // access via the superuser bypass; better-auth reads via its adapter + // (system context), so the device-grant flow is unaffected. + access: { default: 'private' }, // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema, // but may add overlay row-level config. Use `no-overlay` if you need to // forbid sys_metadata overlays entirely. diff --git a/packages/platform-objects/src/identity/sys-jwks.object.ts b/packages/platform-objects/src/identity/sys-jwks.object.ts index a2c94bcdf6..903f92cd49 100644 --- a/packages/platform-objects/src/identity/sys-jwks.object.ts +++ b/packages/platform-objects/src/identity/sys-jwks.object.ts @@ -21,6 +21,13 @@ export const SysJwks = ObjectSchema.create({ icon: 'key', isSystem: true, managedBy: 'better-auth', + // [ADR-0066 D2/④] Secure-by-default: rows are the environment's JWT SIGNING + // KEYS (private key material). Not covered by the wildcard `'*'` grant — an + // ordinary member gets 403 from the generic data layer. Platform admins + // (viewAllRecords/modifyAllRecords) retain access via the posture-gated + // superuser bypass; better-auth itself reads via its adapter (system + // context), so token signing/verification is unaffected. + access: { default: 'private' }, // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema, // but may add overlay row-level config. Use `no-overlay` if you need to // forbid sys_metadata overlays entirely. diff --git a/packages/platform-objects/src/identity/sys-oauth-access-token.object.ts b/packages/platform-objects/src/identity/sys-oauth-access-token.object.ts index 1831fb3b69..9d644f91e8 100644 --- a/packages/platform-objects/src/identity/sys-oauth-access-token.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-access-token.object.ts @@ -22,6 +22,11 @@ export const SysOauthAccessToken = ObjectSchema.create({ icon: 'ticket', isSystem: true, managedBy: 'better-auth', + // [ADR-0066 D2/④] Secure-by-default: rows are LIVE bearer credentials — + // reading one is session hijack. Not covered by the wildcard `'*'` grant; + // admins retain access via the superuser bypass; better-auth reads via its + // adapter (system context), so OAuth flows are unaffected. + access: { default: 'private' }, // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema, // but may add overlay row-level config. Use `no-overlay` if you need to // forbid sys_metadata overlays entirely. diff --git a/packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts b/packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts index f7da8b1780..d877bb1945 100644 --- a/packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts +++ b/packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts @@ -21,6 +21,11 @@ export const SysOauthRefreshToken = ObjectSchema.create({ icon: 'refresh-cw', isSystem: true, managedBy: 'better-auth', + // [ADR-0066 D2/④] Secure-by-default: rows are LIVE long-lived credentials — + // a refresh token mints new access tokens. Not covered by the wildcard `'*'` + // grant; admins retain access via the superuser bypass; better-auth reads + // via its adapter (system context), so OAuth flows are unaffected. + access: { default: 'private' }, // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema, // but may add overlay row-level config. Use `no-overlay` if you need to // forbid sys_metadata overlays entirely. diff --git a/packages/platform-objects/src/identity/sys-scim-provider.object.ts b/packages/platform-objects/src/identity/sys-scim-provider.object.ts index cf5cff86c2..ded4a1f2bd 100644 --- a/packages/platform-objects/src/identity/sys-scim-provider.object.ts +++ b/packages/platform-objects/src/identity/sys-scim-provider.object.ts @@ -33,6 +33,14 @@ export const SysScimProvider = ObjectSchema.create({ icon: 'users', isSystem: true, managedBy: 'better-auth', + // [ADR-0066 D3/④] Admin-only identity config carrying a live credential + // (`scim_token` — the bearer external IdPs authenticate provisioning calls + // with). Object-level capability gate, mirroring the sibling + // `sys_sso_provider`: ordinary members are denied entirely (without it, the + // `member_default` wildcard `'*': allowRead` would expose SCIM connections + // to every authenticated user). better-auth's own endpoints read via a + // system context, so SCIM provisioning is unaffected. + requiredPermissions: ['manage_platform_settings'], // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema. protection: { lock: 'full', diff --git a/packages/platform-objects/src/identity/sys-verification.object.ts b/packages/platform-objects/src/identity/sys-verification.object.ts index 3d54808b18..0b336e7f95 100644 --- a/packages/platform-objects/src/identity/sys-verification.object.ts +++ b/packages/platform-objects/src/identity/sys-verification.object.ts @@ -17,6 +17,12 @@ export const SysVerification = ObjectSchema.create({ icon: 'shield-check', isSystem: true, managedBy: 'better-auth', + // [ADR-0066 D2/④] Secure-by-default: rows are LIVE one-time credentials + // (email/phone verification + password-reset tokens) — reading one is + // account takeover. Not covered by the wildcard `'*'` grant; admins retain + // access via the superuser bypass; better-auth reads via its adapter + // (system context), so verification flows are unaffected. + access: { default: 'private' }, // ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema, // but may add overlay row-level config. Use `no-overlay` if you need to // forbid sys_metadata overlays entirely. diff --git a/packages/platform-objects/src/platform-objects.test.ts b/packages/platform-objects/src/platform-objects.test.ts index 6a642fa54f..bd14800696 100644 --- a/packages/platform-objects/src/platform-objects.test.ts +++ b/packages/platform-objects/src/platform-objects.test.ts @@ -4,10 +4,15 @@ import { describe, expect, it } from 'vitest'; import { SysAccount, SysApiKey, + SysDeviceCode, SysInvitation, + SysJwks, SysMember, + SysOauthAccessToken, SysOauthApplication, + SysOauthRefreshToken, SysOrganization, + SysScimProvider, SysSession, SysTeam, SysTeamMember, @@ -27,7 +32,7 @@ import { SysMetadata, SysMetadataHistoryObject, } from './metadata/index.js'; -import { SysSetting } from './system/index.js'; +import { SysSecret, SysSetting } from './system/index.js'; import { ACCOUNT_APP, SETUP_APP, SETUP_NAV_CONTRIBUTIONS, STUDIO_APP } from './apps/index.js'; import { AppSchema } from '@objectstack/spec/ui'; @@ -63,6 +68,44 @@ describe('@objectstack/platform-objects', () => { expect((object as any).tableName).toBeUndefined(); }); + describe('secure-by-default posture (ADR-0066 ④)', () => { + // Raw secret / live-credential stores are opted OUT of the wildcard `'*'` + // grant: only an explicit per-object grant or the posture-gated superuser + // bypass (viewAllRecords/modifyAllRecords) reaches them. These assertions + // PIN the rollout — dropping the flag from any of these objects silently + // re-exposes signing keys / reset tokens / bearer credentials to every + // authenticated member, so a removal must fail here first (ADR-0078: + // no silent regression to the inert/exposed state). + const privateObjects = [ + ['SysSecret', SysSecret], // encrypted settings/datasource secrets + ['SysJwks', SysJwks], // JWT signing private keys + ['SysVerification', SysVerification], // password-reset / verify tokens + ['SysOauthAccessToken', SysOauthAccessToken], // live bearer tokens + ['SysOauthRefreshToken', SysOauthRefreshToken], // live refresh tokens + ['SysDeviceCode', SysDeviceCode], // pending device-grant codes + ] as const; + + it.each(privateObjects)('%s declares access.default = private', (_exportName, object) => { + expect((object as any).access?.default).toBe('private'); + }); + + it('SysScimProvider is capability-gated like its sibling sys_sso_provider', () => { + // Admin config with an embedded live credential (scim_token) — the D3 + // capability gate (not the private posture) is the sso/scim pattern. + expect((SysScimProvider as any).requiredPermissions).toEqual(['manage_platform_settings']); + }); + + it('member self-service objects deliberately stay on the public posture', () => { + // Account app ("My Sessions" / "My API Keys" / "My Apps") and the 2FA + // "My Enrollment" view read these via the generic data layer with a + // MEMBER context — flipping them private would break self-service. + // Row scoping (owner/tenant RLS + _self carve-outs) is their guard. + for (const object of [SysSession, SysApiKey, SysOauthApplication, SysTwoFactor]) { + expect((object as any).access?.default).not.toBe('private'); + } + }); + }); + describe('sysadmin row actions', () => { // Setup-App admins must be able to drive the access-control lifecycle // without dropping to SQL. These assertions lock in the high-traffic diff --git a/packages/platform-objects/src/system/sys-secret.object.ts b/packages/platform-objects/src/system/sys-secret.object.ts index aad9b06d53..ee21a8353b 100644 --- a/packages/platform-objects/src/system/sys-secret.object.ts +++ b/packages/platform-objects/src/system/sys-secret.object.ts @@ -36,6 +36,14 @@ export const SysSecret = ObjectSchema.create({ icon: 'key', isSystem: true, managedBy: 'system', + // [ADR-0066 D2/④] Secure-by-default: the environment's encrypted-secrets + // store (settings/datasource credentials). Not covered by the wildcard `'*'` + // grant — ordinary members get 403 from the generic data layer. Platform + // admins retain access via the posture-gated superuser bypass. Internal + // readers are unaffected: `engine.resolveSecret` reads at DRIVER level, + // SettingsService / the datasource secret-binder read with no principal + // (middleware falls open for principal-less internal calls). + access: { default: 'private' }, description: 'Cipher store referenced by sys_setting handles. Never holds plaintext.', highlightFields: ['namespace', 'key', 'kms_key_id', 'version', 'rotated_at'], listViews: { From cfa69d92db2d9828f6d07bd49ee08af7f4e15d90 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 15:45:22 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(setup-app):=20correct=20Advanced-group?= =?UTF-8?q?=20nav=20list=20+=20note=20JWKS=20gating=20(ADR-0066=20?= =?UTF-8?q?=E2=91=A3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Advanced table listed Verifications and Device Codes, but those nav entries were deliberately removed (sensitive ephemeral secrets — not browsable; see setup-nav.contributions.ts). Also document that nav_jwks is capability-gated and sys_jwks is private-posture. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0187GeNqezxV6g5jiLiggfbt --- content/docs/ui/setup-app.mdx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/content/docs/ui/setup-app.mdx b/content/docs/ui/setup-app.mdx index 47f1c1241f..f99c9dc311 100644 --- a/content/docs/ui/setup-app.mdx +++ b/content/docs/ui/setup-app.mdx @@ -56,7 +56,7 @@ anchors are: | **Configuration** (`group_configuration`) | All Settings · Branding · Authentication · Email · File Storage · AI & Embedder · Knowledge · Feature Flags — `platform-objects` | | **Diagnostics** (`group_diagnostics`) | Sessions · Notification Events — `platform-objects`; Audit Logs — `plugin-audit` | | **Integrations** (`group_integrations`) | `plugin-webhooks` | -| **Advanced** (`group_advanced`) | OAuth Applications · Signing Keys (JWKS) · Verifications · Device Codes · Identity Links · User Preferences — `platform-objects` | +| **Advanced** (`group_advanced`) | OAuth Applications · Signing Keys (JWKS) · Identity Links · User Preferences — `platform-objects` | The exact rendered menu depends on which capability plugins are loaded. A few notable entries: @@ -66,7 +66,12 @@ A few notable entries: - **OAuth Applications** (`sys_oauth_application`) — third-party OAuth client registrations, contributed into `group_advanced`. - **Signing Keys (JWKS)** (`sys_jwks`) — JWKS keys used for OIDC / JWT - signing, contributed into `group_advanced`. + signing, contributed into `group_advanced`. The nav item is gated on the + `manage_platform_settings` capability, and the object itself is + `access: { default: 'private' }` (ADR-0066 ④) — signing keys are never + covered by the wildcard grant, so non-admins are denied server-side. + (`sys_verification` and `sys_device_code` are deliberately **not** in the + nav: sensitive, ephemeral secrets — not browsable, and also `private`.) - **Audit Logs** (`sys_audit_log`) — contributed by `plugin-audit` into `group_diagnostics`. (The `sys_activity` and `sys_comment` objects also live in `plugin-audit`, but they are not contributed as Setup nav