Skip to content

Commit e2519b3

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-7630-adr-0087-v17-strictness-entry
2 parents f24cd81 + 569611f commit e2519b3

10 files changed

Lines changed: 229 additions & 30 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
---
4+
5+
fix(platform-objects): drop the dead Setup › Advanced › Signing Keys (JWKS) nav entry (#7544)
6+
7+
`Setup › Advanced › Signing Keys (JWKS)` could never load, for **any** persona.
8+
`sys_jwks` declares `enable.apiEnabled: false` / `apiMethods: []`, so the list
9+
request answers `OBJECT_API_DISABLED` (404) — and the console masked that as a
10+
generic "No identity records" empty state, so the surface read as *"you have no
11+
signing keys"* rather than *"this page cannot work"*.
12+
13+
**Why the gate it carried could not help.** The entry was contributed with
14+
`requiredPermissions: ['manage_platform_settings']`, and an in-code comment
15+
claimed a non-admin's list "403s server-side" — which reads as though an admin
16+
could list the keys. None could. `apiAccessDenialFromEnable` (`rest-server.ts`)
17+
is a **pure function of the object's `enable` block**: it takes no user, no
18+
permissions and no context, so the 404 is identical for every persona, platform
19+
admin included. A permission gate on the entry and an API-disabled object are
20+
independent conditions, and no combination of the first prunes the second.
21+
22+
**The repair is the entry, not the object.** `sys_jwks` rows are the
23+
environment's JWT signing keys (`private_key` — private key material); opening a
24+
read path onto them over the generic data API would be a credential disclosure.
25+
`enable` is unchanged, and a test now pins that it stays `apiEnabled: false` /
26+
`apiMethods: []` (fails CLOSED since #3391) and `access: { default: 'private' }`
27+
(ADR-0066 ④). better-auth continues to read the keys through its adapter under a
28+
system context, so token signing and verification are unaffected.
29+
30+
This matches how the same class is already handled two lines below in
31+
`setup-nav.contributions.ts`: `sys_verification` and `sys_device_code` omit
32+
`list` and therefore get no browse entry. `sys_jwks` was the only one of the
33+
repo's seven API-disabled objects that still had a nav entry — the six
34+
`sys_oauth_*` token/consent stores never had one.
35+
36+
Also landed with the removal:
37+
38+
- The four `apps.setup.navigation.nav_jwks` labels move into the
39+
`DEAD_SETUP_NAV_IDS` tombstone (`setup-nav-dead-key-tombstone.test.ts`), which
40+
refuses a label with no declaring nav item and states the order for re-adding
41+
one. The `sys_jwks` **object** labels in the generated bundles are untouched —
42+
the object still exists.
43+
- A new invariant in `platform-objects.test.ts`: every contributed
44+
`type: 'object'` Setup entry must target an object that can actually serve a
45+
`list`, judged through the same single derivation source the REST gate uses
46+
(`resolveEffectiveApiMethods` / `isApiOperationAllowed`, #3391). It asserts the
47+
control too — `nav_api_keys``sys_api_key` still lists, so a fix that pruned
48+
both would fail.
49+
50+
**Not addressed here** (reported on #7544 instead): nav gating has no declaration
51+
that can express "prune when the destination cannot serve". `filterAppForUser`
52+
gates `requiredPermissions` and `requiresService` server-side and deliberately
53+
leaves `requiresObject` to the client, and nothing anywhere consults
54+
`enable.apiEnabled` — so re-pointing this entry at a `requiresObject` gate would
55+
not have pruned it either. Closing that gap is a contract-face change and belongs
56+
in its own card.

content/docs/ui/setup-app.mdx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ anchors are:
5656
| **Configuration** (`group_configuration`) | All Settings · Localization · Company · Branding · Authentication · Email · File Storage · AI & Embedder · Knowledge · Feature Flags — `platform-objects` |
5757
| **Diagnostics** (`group_diagnostics`) | Sessions · Notification Events — `platform-objects`; Audit Logs — `plugin-audit` |
5858
| **Integrations** (`group_integrations`) | `plugin-webhooks` |
59-
| **Advanced** (`group_advanced`) | OAuth Applications · Signing Keys (JWKS) · Identity Links · User Preferences — `platform-objects` |
59+
| **Advanced** (`group_advanced`) | OAuth Applications · Identity Links · User Preferences — `platform-objects` |
6060

6161
The exact rendered menu depends on which capability plugins are loaded.
6262
A few notable entries:
@@ -65,13 +65,17 @@ A few notable entries:
6565
into `group_diagnostics`.
6666
- **OAuth Applications** (`sys_oauth_application`) — third-party OAuth
6767
client registrations, contributed into `group_advanced`.
68-
- **Signing Keys (JWKS)** (`sys_jwks`) — JWKS keys used for OIDC / JWT
69-
signing, contributed into `group_advanced`. The nav item is gated on the
70-
`manage_platform_settings` capability, and the object itself is
71-
`access: { default: 'private' }` (ADR-0066 ④) — signing keys are never
72-
covered by the wildcard grant, so non-admins are denied server-side.
73-
(`sys_verification` and `sys_device_code` are deliberately **not** in the
74-
nav: sensitive, ephemeral secrets — not browsable, and also `private`.)
68+
- **Signing Keys (JWKS)** (`sys_jwks`) — **not** in the nav (#7544). The
69+
object is the environment's JWT signing-key store (`private_key`) and
70+
declares `enable.apiEnabled: false` / `apiMethods: []`, so the generic data
71+
API answers `OBJECT_API_DISABLED` (404) on every list — for **every**
72+
persona, platform admin included, since that gate reads only the object's
73+
`enable` block and takes no user or permissions. The entry that used to be
74+
here carried a `manage_platform_settings` gate, which could not prune it:
75+
a permission gate and an API-disabled object are independent conditions.
76+
(`sys_verification` and `sys_device_code` are likewise deliberately absent:
77+
sensitive, ephemeral secrets — not browsable, and also `private`. All three
78+
are pinned in `setup-nav-dead-key-tombstone.test.ts`.)
7579
- **Audit Logs** (`sys_audit_log`) — contributed by `plugin-audit` into
7680
`group_diagnostics`. (The `sys_activity` and `sys_comment` objects also
7781
live in `plugin-audit`, but they are not contributed as Setup nav

docs/qa/platform-checklist/areas/platform-core.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -482,9 +482,9 @@
482482
"Setup detail: confirm each settings URL entry (nav_settings_* → /apps/setup/system/settings/<namespace>) opens the settings namespace form (localization/company/branding/auth/mail/storage/ai/knowledge/feature_flags), and Users / Organization / Business Units / Teams / Sessions / OAuth Applications / Identity Links / User Preferences render",
483483
"Studio detail: confirm each metadata:resource list (object/app/view/page/dashboard/report/dataset/action/hook/flow/agent/tool/skill/email_template) renders, and the component surfaces render (App Builder studio:builder, All Metadata Types metadata:directory, Packages developer:packages, API Console developer:api-console, Flow Runs developer:flow-runs, Public Forms developer:public-forms)",
484484
"Account detail: confirm Profile (account:profile_card), Notifications (sys_inbox_message/mine), Approvals (sys_approval_request/my_pending), Linked Accounts (sys_account), Active Sessions (sys_session/mine), API Keys (sys_api_key/mine), OAuth Applications (sys_oauth_application/mine) each render",
485-
"confirm the gated entries resolve to ABSENT-not-erroring for the admin: nav_organizations (requiresService org-scoping) is absent in single-org mode; nav_jwks Signing Keys (requiredPermissions manage_platform_settings, sys_jwks private per ADR-0066) is PRESENT for admin; SSO Providers is absent unless OS_SSO_ENABLED (knownGap)",
485+
"confirm the gated entries resolve to ABSENT-not-erroring for the admin: nav_organizations (requiresService org-scoping) is absent in single-org mode; nav_jwks Signing Keys is ABSENT for EVERY persona, admin included — the entry was removed in #7544 because sys_jwks declares apiEnabled:false, so its list answers OBJECT_API_DISABLED (404) for all callers and no permission gate could prune it; SSO Providers is absent unless OS_SSO_ENABLED (knownGap)",
486486
"sign in as the plain member: attempt to open Setup (com.objectstack.setup) and Studio (com.objectstack.studio) — capture the app-level refusal (App.requiredPermissions setup.access / studio.access); open Account (declares no requiredPermissions) — capture it opening",
487-
"as the member, GET /api/v1/meta/app for a reachable app and confirm every manage_platform_settings-gated entry (JWKS, API Keys, the manage_platform_settings settings URLs) is ABSENT from the member's payload — the server prunes, the client does not merely hide",
487+
"as the member, GET /api/v1/meta/app for a reachable app and confirm every manage_platform_settings-gated entry (API Keys, the manage_platform_settings settings URLs) is ABSENT from the member's payload — the server prunes, the client does not merely hide. JWKS is deliberately NOT in this list any more: it is absent for admins too, so finding it absent for a member proves nothing about the gate (#7544)",
488488
"cross-check served-vs-rendered: diff each app's meta/app nav destinations against what actually rendered so nothing is silently outside the walk"
489489
],
490490
"acceptance": [
@@ -541,7 +541,7 @@
541541
"packages/platform-objects/src/apps/setup.app.ts (shell group anchors + requiredPermissions setup.access)",
542542
"packages/platform-objects/src/apps/studio.app.ts (static nav; requiredPermissions studio.access)",
543543
"packages/platform-objects/src/apps/account.app.ts (no requiredPermissions; hidden from App Switcher; requiresObject/requiresService entry gates)",
544-
"packages/platform-objects/src/apps/setup-nav.contributions.ts (nav_jwks manage_platform_settings; nav_organizations requiresService org-scoping; nav_api_keys manage_platform_settings)",
544+
"packages/platform-objects/src/apps/setup-nav.contributions.ts (nav_organizations requiresService org-scoping; nav_api_keys manage_platform_settings; no nav_jwks — removed in #7544)",
545545
"packages/plugins/plugin-auth/src/auth-plugin.ts (SSO Providers nav_sso_providers contributed only when isSsoWired())",
546546
"ADR-0048 (Setup/Studio/Account as one-app packages com.objectstack.{setup,studio,account}); ADR-0029 (nav contributions); ADR-0066 (sys_jwks private)"
547547
],

packages/platform-objects/src/apps/setup-nav.contributions.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,30 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [
134134
priority: BASE_PRIORITY,
135135
items: [
136136
{ id: 'nav_oauth_apps', type: 'object', label: 'OAuth Applications', objectName: 'sys_oauth_application', icon: 'app-window' },
137-
// nav_jwks is capability-gated (like nav_api_keys): sys_jwks is
138-
// `access.default:'private'` (ADR-0066 ④ — signing keys), so a
139-
// non-admin's list request 403s server-side; gating the nav item keeps
140-
// the menu honest instead of showing an entry that can only error.
141-
{ id: 'nav_jwks', type: 'object', label: 'Signing Keys (JWKS)', objectName: 'sys_jwks', icon: 'key-round', requiredPermissions: ['manage_platform_settings'] },
137+
// No `nav_jwks` here (#7544). `sys_jwks` is the environment's JWT SIGNING
138+
// KEY store (`private_key` — private key material), and it declares
139+
// `enable.apiEnabled: false` / `apiMethods: []`, so the generic data API
140+
// answers `OBJECT_API_DISABLED` (404) on every list request.
141+
//
142+
// ⚠️ That 404 is NOT a permission outcome and no permission gate can
143+
// prune it. `apiAccessDenialFromEnable` (rest-server.ts) is a PURE
144+
// function of the object's `enable` block — it takes no user, no
145+
// permissions and no context — so the 404 is identical for every
146+
// persona, platform admin included. The entry this replaces carried
147+
// `requiredPermissions: ['manage_platform_settings']` and a comment
148+
// claiming a non-admin "403s server-side", which read as though an ADMIN
149+
// could list the keys. None could: the page was dead for everyone, and
150+
// the console masked the 404 as a generic empty state, so the surface
151+
// read as "you have no signing keys" rather than "this page cannot work".
152+
//
153+
// ⛔ The repair is the entry, never the object: opening a read path onto
154+
// private signing keys over the generic data API would be a credential
155+
// disclosure. `apiMethods: []` fails CLOSED by design (#3391).
156+
//
157+
// The same reasoning already governs the two entries below, and it is why
158+
// the other six API-disabled objects (the `sys_oauth_*` token/consent
159+
// stores) have no nav entry either — `sys_jwks` was the only one that did.
160+
//
142161
// `sys_verification` (email/phone tokens) and `sys_device_code` (OAuth
143162
// device-grant codes) deliberately omit `list` from their `apiMethods`
144163
// (sensitive, ephemeral secrets — not browsable), so an object/list-view

packages/platform-objects/src/apps/translations/en.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,13 @@ export const en: TranslationData = {
139139
// No `nav_verifications` / `nav_device_codes` here on purpose:
140140
// `sys_verification` and `sys_device_code` omit `list` from their
141141
// `apiMethods`, so `setup-nav.contributions.ts` deliberately declares
142-
// no browse entry for them (#2266). Nor `nav_metadata` — that entry
143-
// moved to Studio as `nav_metadata_directory`. Re-adding a label here
144-
// means re-adding the nav item first; the tombstone in
142+
// no browse entry for them (#2266). Nor `nav_jwks` — `sys_jwks` is
143+
// `apiEnabled: false`, so its list 404s for EVERY persona and the entry
144+
// was removed (#7544). Nor `nav_metadata` — that entry moved to Studio
145+
// as `nav_metadata_directory`. Re-adding a label here means re-adding
146+
// the nav item first; the tombstone in
145147
// `setup-nav-dead-key-tombstone.test.ts` states the whole rule (#6660).
146148
nav_oauth_apps: { label: 'OAuth Applications' },
147-
nav_jwks: { label: 'Signing Keys (JWKS)' },
148149
nav_accounts: { label: 'Identity Links' },
149150
nav_user_preferences: { label: 'User Preferences' },
150151
},

packages/platform-objects/src/apps/translations/es-ES.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ export const esES: TranslationData = {
9898
nav_datasources: { label: 'Fuentes de datos' },
9999

100100
nav_oauth_apps: { label: 'Aplicaciones OAuth' },
101-
nav_jwks: { label: 'Claves de Firma (JWKS)' },
102101
nav_accounts: { label: 'Enlaces de Identidad' },
103102
nav_user_preferences: { label: 'Preferencias de Usuario' },
104103
},

packages/platform-objects/src/apps/translations/ja-JP.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ export const jaJP: TranslationData = {
9898
nav_datasources: { label: 'データソース' },
9999

100100
nav_oauth_apps: { label: 'OAuth アプリケーション' },
101-
nav_jwks: { label: '署名キー (JWKS)' },
102101
nav_accounts: { label: 'ID 連携' },
103102
nav_user_preferences: { label: 'ユーザー設定' },
104103
},

packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
// deliberately NOT built (#6659's triage): it is a separate maintainer-facing
2222
// call, not a prerequisite for labelling the ids it cannot see.
2323
//
24-
// This file makes no general claim. It pins exactly four ids that were checked
24+
// This file makes no general claim. It pins exactly five ids that were checked
2525
// ONE BY ONE against a repo-wide grep — `id: '<key>'` returned zero hits for
26-
// each of them on `61282f906`, against a control probe (`nav_webhooks`) that
27-
// returned five — and each of which has a recorded reason to be gone:
26+
// each of them on `61282f906` (`nav_jwks` on `68a1edb`), against a control
27+
// probe (`nav_webhooks`) that returned five — and each of which has a recorded
28+
// reason to be gone:
2829
//
2930
// nav_approval_processes the process engine was retired in favour of the
3031
// approval flow node (#1408, ADR-0019 P4/P5)
@@ -34,16 +35,26 @@
3435
// ever render "failed to load" (#2266, and the
3536
// comment that records it in
3637
// `setup-nav.contributions.ts`)
38+
// nav_jwks `sys_jwks` (JWT signing PRIVATE keys) declares
39+
// `apiEnabled: false` / `apiMethods: []`, so its list
40+
// answers `OBJECT_API_DISABLED` (404). Unlike the two
41+
// above this entry EXISTED and was dead for every
42+
// persona — `apiAccessDenialFromEnable` is a pure
43+
// function of `enable`, so the admin-looking
44+
// `requiredPermissions` gate it carried could never
45+
// prune it (#7544)
3746
// nav_metadata moved to Studio as `nav_metadata_directory` when
3847
// the Studio app was split out (482eb67cc)
3948
//
4049
// ---------------------------------------------------------------------------
4150
// What to do when this test goes red
4251
// ---------------------------------------------------------------------------
43-
// It goes red on exactly one event: one of the four ids comes back. That is not
44-
// automatically wrong — re-adding `nav_verifications` or `nav_device_codes` is a
45-
// deliberate security decision (it requires enabling `list` on the object
46-
// first), and `nav_approval_processes` could return with a new owner. The rule
52+
// It goes red on exactly one event: one of the five ids comes back. That is not
53+
// automatically wrong — re-adding `nav_verifications`, `nav_device_codes` or
54+
// `nav_jwks` is a deliberate security decision (each requires enabling the read
55+
// on the object first — for `sys_jwks`, opening a data-API read path onto
56+
// private signing-key material, which is why it is the least likely of the
57+
// three), and `nav_approval_processes` could return with a new owner. The rule
4758
// is the ORDER: the declaring nav item comes back first, the label second, and
4859
// the id's line is deleted from `DEAD_SETUP_NAV_IDS` in that same commit. A
4960
// label with no declaring nav item is what this tombstone exists to refuse.
@@ -61,6 +72,7 @@ const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES } as const;
6172
const DEAD_SETUP_NAV_IDS = [
6273
'nav_approval_processes',
6374
'nav_device_codes',
75+
'nav_jwks',
6476
'nav_metadata',
6577
'nav_verifications',
6678
] as const;

packages/platform-objects/src/apps/translations/zh-CN.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ export const zhCN: TranslationData = {
108108
nav_datasources: { label: '数据源' },
109109

110110
nav_oauth_apps: { label: 'OAuth 应用' },
111-
nav_jwks: { label: '签名密钥 (JWKS)' },
112111
nav_accounts: { label: '身份链接' },
113112
nav_user_preferences: { label: '用户偏好' },
114113
},

0 commit comments

Comments
 (0)