Skip to content

Commit d54e239

Browse files
docs(adr-0069): D3 states what the gate holds — the account's standing, so an API key answers as the session does (ruling C′ item 5) (#23472)
Part of #23287 Clause-②: no This PR writes ruling C′ item 5 into ADR-0069 D3. The ruling is on #23287 (comment [`6159450584`](#23287 (comment)), the maintainer's 「同意」). The sentence also records the default for the forced-reset revocation, which the maintainer set as letter A (record [`6165848580`](#23287 (comment))). The card's code is PR #23440. This PR contains no code. **Tier H** (`docs/adr/**`). The PR stays a draft until the maintainer approves it. Before that approval, no seat marks it ready, queues it or arms auto-merge. `Clause-②: no`: this diff adds nothing to a published payload and publishes no package. The claim's `Clause-②: yes` line belongs to the code PR #23440. Changeset: none. `docs/adr/**` publishes nothing, so the PR carries the `skip-changeset` label. ## The change: one file, +12 / −1 `docs/adr/0069-enterprise-authentication-hardening.md` changes in two places. Nothing else in the record changes: 1. **D3's note.** The note under the D3 table ends with "Gate at the session-validation seam so a half-authenticated (password-only) session cannot read data." It now has one more paragraph: a bold provenance label and **one sentence**. 2. **The status line** gets one `· **Amended**` entry that points to that note. ### 1. D3: the new paragraph (exact) ```text > **What the gate holds (amended 2026-10-14: ruling C′ on [#23287](#23287), > [`6159450584`](#23287 (comment)), maintainer 「同意」; > the revocation's default is the maintainer's letter A, > [`6165848580`](#23287 (comment) > The gate holds the account's standing and an API key answers as the session does (a held > user's key is refused with the session's code and remediation, and admitted again once the > standing clears: a refusal, not a revocation); password expiry (D1) holds password sign-in > alone, so the user's keys keep working; and an administrator's forced password reset (D7) > carries an explicit revocation of the user's sessions and API keys, on by default and off only > as the administrator's explicit act. ``` ### 2. Status line: the appended entry (exact) ```text · **Amended** (2026-10-14, ruling C′ on [#23287](#23287), [`6159450584`](#23287 (comment)), maintainer 「同意」) — D3 states what the gate holds for an API key, for password expiry and for a forced reset; see the note under D3. ``` ## How each clause maps to the ruling | Clause in the sentence | Ruling text it carries | |---|---| | "The gate holds the account's standing and an API key answers as the session does" | Item 5: "the gate holds the account's standing and a key answers as the session does" | | "(a held user's key is refused with the session's code and remediation, and admitted again once the standing clears: a refusal, not a revocation)" | Item 4: "The refusal is not a revocation: the key resumes the moment the user enrols (or the account posture clears); the 403 body carries the posture code and the remediation pointer the session gets." | | "password expiry (D1) holds password sign-in alone, so the user's keys keep working" | Item 5: "password expiry holds the password alone"; item 1: "it holds password sign-in alone; the user's keys keep working" | | "an administrator's forced password reset (D7) carries an explicit revocation of the user's sessions and API keys, on by default and off only as the administrator's explicit act" | Item 5: "a forced reset carries an explicit revocation"; letter A (`6165848580`): `revokeSessionsAndKeys` stays ON by default for every forced reset, and turning it off is the administrator's explicit act | ## Why it is placed and worded this way (measured on `main` at `1685cd5ca7`) - **Placement.** D3 states what the gate holds in two places: its `mfa_required` row and the note under the table. The note names the seam and why it is there, so the sentence continues the note rather than widening a table cell. The sentence cites D1's `password_expiry_days` row and D7's "force password reset" action by section name. Neither one is edited. - **Provenance convention.** This record already writes its one earlier settled question in two places: the status line ("Session-of-record question settled 2026-08-04 (#4785)") and bold-led notes inside D2 and D4. Sibling amendments on `main` use a `· **Amended** (date, ruling …)` entry on the status line plus the text in place: ADR-0138 D3 item 6 (`34d144ca4d`), ADR-0005 and ADR-0043. This PR follows both conventions. The status entry is a pointer and does not repeat the decision, so the two texts cannot drift apart. - **True as a decision record, whichever PR lands first.** The sentence names no symbol that exists only in PR #23440. On `main`, `PASSWORD_RESET_REQUIRED` has 0 hits under `packages/`, and the sentence does not name `AUTH_GATE_SCOPES` or `resolveUserAuthGates`. "The account's standing" and "password sign-in alone" are the ruling's scope words (item 1), written as prose rather than as code identifiers. - **Prime Directive 13 grep.** Before this PR, ADR-0069 mentioned API keys 0 times. Across `docs/adr/`, a case-insensitive grep for `authGate|un-stepped-up|MFA_REQUIRED|PASSWORD_EXPIRED` hits ADR-0069 and two other places: a field list in ADR-0092 and an allowlist predicate in ADR-0138. Neither rules on a held user's key. ADR-0101 (stdio key admission) runs a key "under posture rules (ADR-0099)", and this sentence does not contradict that. ## Gates On head `38a672e9b6`, `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` was run with no paths. It derived the change set from the merge base `1685cd5ca` (1 path, +12 / −1) and listed **19 commands**. All 19 ran, each exit code was captured before any pipe, and **all 19 exited 0**. One of them, `pnpm --filter @objectstack/lint run check:doc-formula-expressions`, first exited 3 with `PREREQUISITE NOT MET` because `@objectstack/formula` and `@objectstack/lint` were not built. That run measured nothing. After a build of those two packages under `os-verify-lock` (`VERDICT command-exit 0`), the gate re-ran and exited 0. The `--ran` reconciliation with the exit codes recorded printed: `19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)`. The gates' own verdict lines: - `pnpm check:adr-anchors`: `OK (68 anchored file(s), every governing ADR still referenced; 136 decision number(s), …; 46287 citation(s) across 6051 file(s) resolve; …)` - `node scripts/check-adr-links.mjs`: `761 relative link destination(s) under docs/adr/ resolve` - `node scripts/check-adr-symbol-anchors.mjs`: `2347 anchors across 142 records resolve` - `pnpm check:doc-authoring`: clean on all three axes - `pnpm check:nul-bytes`: `no raw ASCII control bytes` - `node scripts/pm/check-governed-merges.mjs --branch claude/issue-23287-adr-0069-d3`: exit 3, which is this tool's governed verdict and not a failure: `landing tier: H(人合)`, `docs/adr/** ×1`, `13 changed line(s) (+12 / -1) ≤ 3000` These run in CI, not locally: the lint workflow's full set of checks, link checking, and the PR-body guards (Part-of / closing keyword, closing-target claim, duplicate fix). The derivation itself names 15 families that apply only once a changeset exists. This PR has none: it carries the `skip-changeset` label. ## Acceptance notes - **Left unedited on purpose** (one sentence, no other ADR edit): the P1 row of the implementation-status table ("✅ implemented", 2026-07-04) and item 4 of the enforcement-seam summary. On `main`, the shared key-admission seam does not ask about the key owner's standing, and a forced reset still answers `PASSWORD_EXPIRED` with no revocation switch. PR #23440 changes both. Whichever of the two PRs lands first, the amended sentence reads as a decision and stays true as one. The status table does not claim that the new sentence is enforced. If the maintainer wants that table row to name the amended D3 rule and its enforcement, that is a second sentence and would need this same approval. - **Landing order** with PR #23440 does not affect whether the text is true. The seat already holds that PR's landing behind objectstack-ai/objectui#12365 and the `.objectui-sha` bump (`6165848580`, part 2). This PR has no such dependency. ## 维护者速读(草稿) - **改了什么:** ADR-0069 D3 加了一句话,状态行加了一条「Amended」指针,只改这一个文件(+12 / −1),不改代码。这句话写的是:这道闸扣的是**账号的状态**,API key 和会话得到同一个答案。被扣用户的 key 收到和会话相同的拒绝码与补救指引;账号恢复合规后 key 自动恢复,这是拒绝,不是吊销。**密码过期只扣密码登录**,key 照常可用。**管理员强制改密时附带一次明示的吊销**,吊销该用户的会话和 API key。这个吊销默认开,只有管理员明确关掉时才不吊销。 - **为什么改:** 这是裁决 C′ 第 5 项的要求。ADR-0069 原文只讲「会话」,对 API key 只字未提,结果同一个请求在不同接口得到两种答案(#23287)。记录不补这一句,下一个人照原文读,会认为闸只管会话,把 PR #23440 的修复当成越权改回去。 - **风险与代价(含回滚):** 纯文档,不进任何发布包,也不改任何运行时行为。合入顺序有两种情况。本 PR 先合:记录里已写着这条规则,运行时要到 PR #23440 合入后才兑现。这句话是决定,不是「已实现」的声明;状态表也没有改成「已实现」。#23440 先合:代码先于记录生效。两种顺序下文字都成立。回滚只需 revert 这一个提交。 - **席位意见:** - **你要做的:** 读 D3 下面新增的那一句话。同意的话,在本 PR 上批准(Tier H,要您亲手批)。要改措辞,直接在 PR 上说,dev 按您的原话改。 Drafted by the `os-dev` agent that `domain:spec` seat 4 dispatched, on the claim `6165524186` (branch `claude/issue-23287-adr-0069-d3`). --- _Generated by [Claude Code](https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0f05907 commit d54e239

1 file changed

Lines changed: 12 additions & 1 deletion

File tree

‎docs/adr/0069-enterprise-authentication-hardening.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0069: Enterprise authentication hardening — password policy, enforced MFA, SSO, session controls, network gating, and anti-brute-force, all enforcement-wired
22

3-
**Status**: Accepted — **P1 + P2 implemented** (2026-07-04); P3 partially landed (see Addendum). Original proposal 2026-06-24. Session-of-record question settled 2026-08-04 (#4785): sessions live in `sys_session`; the kernel `cache` service backs D2's rate-limit counters only.
3+
**Status**: Accepted — **P1 + P2 implemented** (2026-07-04); P3 partially landed (see Addendum). Original proposal 2026-06-24. Session-of-record question settled 2026-08-04 (#4785): sessions live in `sys_session`; the kernel `cache` service backs D2's rate-limit counters only. · **Amended** (2026-10-14, ruling C′ on [#23287](https://github.com/objectstack-ai/objectstack/issues/23287), [`6159450584`](https://github.com/objectstack-ai/objectstack/issues/23287#issuecomment-6159450584), maintainer 「同意」) — D3 states what the gate holds for an API key, for password expiry and for a forced reset; see the note under D3.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (**the governing constraint** — a security property that isn't enforced at runtime is forbidden; no toggle may be a "false surface"), [ADR-0007](./0007-settings-manifest-and-kv-store.md) (settings manifest + cascade KV store), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`sys_role` is platform-native, decoupled from better-auth; org scoping), [ADR-0066](./0066-unified-authorization-model.md) (capability/assignment split), [ADR-0068](./0068-unified-user-context-and-built-in-identity-roles.md) (`current_user` contract, built-in roles)
66
**Consumers**: `@objectstack/plugin-auth` (better-auth wiring, `bindAuthSettings`/`applyConfigPatch`, auth route middleware), `@objectstack/service-settings` (`auth.manifest.ts`), `@objectstack/platform-objects` (identity objects `sys_user`/`sys_session`/`sys_account`), `@objectstack/rest` (auth request middleware seam), `../objectui` (settings UI rendering)
@@ -85,6 +85,17 @@ Legend: **[native]** = a better-auth config/plugin does the enforcing; **[custom
8585
| `sys_organization.require_mfa` (per-org) | — | `customSession` org resolution | **[custom]** an org may *require* MFA above the global floor |
8686

8787
> better-auth makes MFA *available*; making it *required* is the missing enforcement. Gate at the session-validation seam so a half-authenticated (password-only) session cannot read data.
88+
>
89+
> **What the gate holds (amended 2026-10-14: ruling C′ on [#23287](https://github.com/objectstack-ai/objectstack/issues/23287),
90+
> [`6159450584`](https://github.com/objectstack-ai/objectstack/issues/23287#issuecomment-6159450584), maintainer 「同意」;
91+
> the revocation's default is the maintainer's letter A,
92+
> [`6165848580`](https://github.com/objectstack-ai/objectstack/issues/23287#issuecomment-6165848580)).**
93+
> The gate holds the account's standing and an API key answers as the session does (a held
94+
> user's key is refused with the session's code and remediation, and admitted again once the
95+
> standing clears: a refusal, not a revocation); password expiry (D1) holds password sign-in
96+
> alone, so the user's keys keep working; and an administrator's forced password reset (D7)
97+
> carries an explicit revocation of the user's sessions and API keys, on by default and off only
98+
> as the administrator's explicit act.
8899
89100
### D4 — Session controls (P2)
90101

0 commit comments

Comments
 (0)