You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a58fe66
Browse filesBrowse the repository at this point in the historyBrowse files
fix(rest): a door whose environment's security service cannot be resolved refuses rather than proceeds (#31404)
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:REST 层所有读取环境 security 服务的地方都走同一个解析器,按服务注册表自带的「从未注册」标记分两种情况:没注册 ⇒
视为该部署没有权限体系,各门照旧(`/security` 各门照旧答 501);注册了但解析不出来 ⇒ 答 503
SERVICE_UNAVAILABLE,什么都不提供。
- 为什么改:加固规则——一个门所在环境的 security 服务解析不出来时,这个门拒绝,而不是继续(ADR-0106 D6
第三档、ADR-0049:解析失败永远不能读成放行)。
- 风险与回滚:只在「security 已注册但构造失败,或在 security 这一步取不到环境内核」的故障态下生效:导出、导入模板、对象
schema 读取、`/security` 各门答 503;记录页照常返回,只有 explain 与 fieldAccess 两个分片各自带
503。没装 plugin-security、或单内核没有 security 服务的部署行为不变(已钉测试)。回滚 = revert 本 PR。
- 先看:① `packages/rest/src/rest-server.ts` 的
`resolveSecurityService`(核心分类,catch 分支 4 行);②
`packages/rest/src/security-routes.ts`(两个族改走同一解析器,各自 catch 增加 503 分支);③
`packages/rest/src/record-page-routes.ts` 的 `answerExplain`(解析挪进每个操作自己的
try)。
- 两分钟验证:`pnpm --filter @objectstack/rest exec vitest run --project local
--maxWorkers=2 src/rest-environment-scoped-providers.test.ts
src/rest-export-permission-gate.test.ts
src/security-explain-envelope.test.ts
src/security-suggested-bindings-envelope.test.ts
src/rest-server-suggested-bindings-status.test.ts`,预期 5 个文件、225 个用例全绿。
- 需要你做的:无,席位落地。
## What changes
**Rule: a door whose environment's security service cannot be resolved
refuses rather than proceeds.**
`RestServer.resolveSecurityService` keeps the two answers the `security`
slot can give apart, by the service registry's own "never registered"
brand (`isServiceNotRegisteredError`), never by message text:
- **Not registered** answers `undefined`: no provider is wired, the
environment's kernel registers no `security` service, or the branded
rejection reaches the resolver. The deployment has no permission posture
(ADR-0106 D6 tier 1), and every caller keeps its documented degradation
for that case, the `/security` doors' `501 NOT_IMPLEMENTED` included.
- **Registered but unresolvable** (a registered factory that throws, or
the environment's kernel failing to be acquired at the security step)
raises the existing `AuthzStoreUnavailableError('security', cause)`,
answered as `503 SERVICE_UNAVAILABLE` (ADR-0106 D6 tier 3; ADR-0049). No
error code, envelope key or public export is added.
The doors listed in the table below ask this one resolver. The
access-explanation family and the suggested-binding family in
`security-routes.ts` hand their reads to it through their route hosts
instead of carrying their own; each family's `catch` answers the outage
as the data door's own `503 SERVICE_UNAVAILABLE` (`mapDataError`,
message withheld as every 5xx there is) through the family's one
emitter. The record page resolves the explain part's security service
once, inside each operation's own `try` and after the caller check (the
order `/security/explain` asks in), so the part carries the refusal and
the page still answers. This is the same classification the anonymous
form doors' tenancy read already makes. The environment-scoped provider
in `rest-api-plugin.ts` is unchanged.
Measured on the hosted multi-environment shape (the first six rows and
the explain door and suggested bindings), in each security family's
envelope test (the delegable scope, confirm, dismiss and reopen), and by
a one-off probe on `b7ef72106` for the effective-access door (`503` /
`501` / healthy `200`), which no test drives:
| Door | Security service registered but unresolvable | Not registered |
|:--|:--|:--|
| `GET …/data/:object/export` | `503`, no row read, nothing written |
serves, as before |
| `GET …/data/:object/export?template=true`, its gate | `503`, no
template | serves, as before |
| the same template, its column projection | `503`, no template |
projection `none`, as before |
| `GET …/meta/object/:name` (object-schema mask, on by default) | `503`,
no schema | unmasked, as before |
| `GET …/data/:object/:id/field-access` | `503` | `501`, as before |
| `POST …/data/:object/:id/page`, `explain` and `fieldAccess` parts |
each part carries the `503`; the page answers `200` with the record |
each part `501`, as before |
| `GET`/`POST …/security/explain`, `…/security/my-delegable-scope`,
`…/security/users/:id/effective-access` | `503` (nested envelope) |
`501`, as before |
| `…/security/suggested-bindings`, its confirm, dismiss and reopen |
`503` (nested envelope) | `501`, as before |
Docblocks of the resolver, the export gate, the import-template gate,
the object masker, both security families and the record page's explain
part state the rule.
## Tests
`packages/rest/src/rest-environment-scoped-providers.test.ts`, two
top-level describes on the existing hosted rig (real `ObjectKernel`s
behind one REST plugin), each case on both the scoped and the hostname
mount:
- *a door whose environment's security service cannot be resolved
refuses rather than proceeds*: for a security slot registered as a
throwing factory, and for an acquisition rejecting at the security step,
the export, the import template's gate and the object-schema mask each
refuse with `503 SERVICE_UNAVAILABLE` and serve nothing; the template's
column projection refuses when its own security step cannot be resolved
after the gate answered; the record page still answers, its explain and
field-access parts each carrying the `503`; the access explanation and
the suggested bindings refuse with `503`. Controls in the same describe:
a healthy service (export `403 EXPORT_NOT_PERMITTED`, template `403
PERMISSION_DENIED`, projection `writable`, schema masked, explain part
and both `/security` doors answered by the environment's service); an
environment with no security service (every door serves, projection
`none`, schema unmasked, the explain part and both `/security` doors
`501`); an environment no acquisition reaches (still refused before any
door serves); and the resolver still answering `undefined` where nothing
is registered.
- *a single kernel with no security service keeps serving every door, as
before*.
`packages/rest/src/rest-export-permission-gate.test.ts`: a wired
provider that rejects answers `503 SERVICE_UNAVAILABLE` with nothing
read; a provider rejecting with the registry's own "never registered"
rejection (from a real kernel, asserted branded) is allowed, exactly as
no security service at all.
`packages/rest/src/security-explain-envelope.test.ts` and
`packages/rest/src/security-suggested-bindings-envelope.test.ts`, one
describe each, in the family's nested envelope: a security provider that
rejects answers `503 SERVICE_UNAVAILABLE` on every route; a caller
context whose authorization store cannot be read answers the same `503`,
and the service is never asked; the registry's "never registered"
rejection keeps the `501`.
## Acceptance notes
- The two security families' outage arm answers any
`AuthzStoreUnavailableError` reaching their `catch`, so a caller context
whose authorization store cannot be read answers these doors `503
SERVICE_UNAVAILABLE`. Same rule, same family emitters; pinned in both
envelope test files.
- The data-action call sites inherit the classification through their
route host with no edit (the table above).
## Verification
Final head `b7ef72106` (`origin/main` merged in at `f90cb94fb` and
`b5a46e2aa`; the rest dependency closure rebuilt after the first). Each
reading names the head it was taken on.
<details>
<summary>Ablations of the server's resolver (round 1, on
40711ac)</summary>
Through `scripts/ablation-replace.mjs`: anchor hit as declared, blob
changed, restore proven each time (blob equals HEAD's, `git diff HEAD`
empty). The subject resolves through relative `src` imports, so no build
sits between mutation and run.
- The classification reverted on disk: 15 red, 129 green of 144 (the 14
refusal legs and the unit case for a rejecting provider); every control
green.
- The "never registered" branch removed: 1 red, 143 green of 144 (the
unit case for the registry's rejection). A first attempt was refused by
the tool before any run (its replacement was a substring of its anchor);
it is not counted.
</details>
<details>
<summary>Ablations of round 2</summary>
- On `a77a0c67a`, the explain part's resolution put back before the
loop, outside the per-operation try: 4 red, 143 green of 147, exactly
the record-page legs for the two unresolvable environments on both
mounts. Restored.
- On `a77a0c67a`, both security families' resolvers made to swallow the
rejection: 4 red, 143 green of 147, exactly the access-explanation and
suggested-binding legs for the two unresolvable environments on both
mounts. Restored.
- On `cbc8eeaf8`, both families' outage arms disabled: 4 red, 58 green
of 62, exactly the four outage pins in the two envelope files. Restored.
A first attempt is void and not counted: its replacement inverted the
explain helper's guard instead of disabling it, so four unrelated
`500`-arm cases went red too.
- `b7ef72106` after these adds only a type annotation to the two
families' resolvers (nothing emitted differently).
</details>
<details>
<summary>Suites and typecheck</summary>
- `pnpm --filter @objectstack/rest test` on `b7ef72106`: 394 files
passed, 7538 tests passed, 332 skipped; lock verdict `command-exit 0`.
- The five files of the two-minute check on `b7ef72106`: 5 files, 225
tests passed.
- `pnpm --filter @objectstack/rest typecheck` (tsc plus the test-layer
typecheck) on `b7ef72106`: `command-exit 0`; `tsc -p tsconfig.test.json
--listFilesOnly` lists all four edited test files, and the debt ledger
holds 0 files.
- `test:repo` (the package's `repo` vitest project) is not run locally:
this diff edits none of its files; CI's Test Core runs it.
</details>
<details>
<summary>Derived gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `b7ef72106`: 71 commands (49 pnpm, 22
node), each run with its exit code captured before any pipe: 70 exit 0;
`check:dual-build-cjs-loads` answered PREREQUISITE NOT MET (exit 3) at
its place in the run, before a later gate had built the remaining
packages, and exit 0 when re-run after the run.
- `--ran` reconciliation on the same head: 71 derived, 71 run, 0
NOT-MEASURED, 0 UNRUN, a derived zero (every line carries its exit code;
the re-run's for that one gate).
- Every family CI runs is read from this PR's own check-runs, not re-run
here.
</details>
Written for the security#1 seat (security#1 · sunhaolin ·
session_017xRBcT1w3zpekijraHJE4f) by its os-dev executor.
<!-- signed: security#1 · sunhaolin · session_017xRBcT1w3zpekijraHJE4f
-->
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(rest): a door whose environment's security service cannot be resolved refuses rather than proceeds
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
The REST server's security resolver keeps two answers apart, by the service registry's own "never registered" brand:
11
+
12
+
-**Not registered** — no `plugin-security`, or a single kernel without a security service — is a deployment with no permission posture (ADR-0106 D6 tier 1). Every door answers it exactly as before, the `/security` doors' `501 NOT_IMPLEMENTED` included.
13
+
-**Registered but unresolvable** — a registered security service whose factory throws, or an environment kernel that cannot be acquired at the security step — is an unhealthy security service (ADR-0106 D6 tier 3). The request answers `503 SERVICE_UNAVAILABLE`, the same answer an unreadable authorization input gives everywhere else, and nothing is served.
14
+
15
+
These doors ask this resolver:
16
+
17
+
- the data export; the import template, at its gate and at its column projection; every object-schema read under `/meta` while object-schema masking is on (the default); and the record field-access door — each answers the `503`;
18
+
- the record page, whose access-explanation and field-access parts each carry the `503` as their own part refusal, while the page still answers with the record and every other part;
19
+
- the `/security` doors — the access explanation, the caller's delegable scope, a user's effective access, and the suggested audience bindings with their confirm, dismiss and reopen — each answers the `503` in its family's nested envelope. A caller context whose authorization store cannot be read answers the same `503` at these doors.
20
+
21
+
On a multi-environment host, an operator who sees this `503` for one environment restores that environment's security service; the other environments are unaffected.
0 commit comments