From 683acf2c274af29f29faf02b44ff986ab88e8466 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 10:49:41 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(adr-0069):=20record=20ruling=20C?= =?UTF-8?q?=E2=80=B2's=20enforcement=20and=20the=20API-key=20admission=20s?= =?UTF-8?q?eam?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dated status note names what PR #23440 and PR #23508 enforce on main: the closed posture vocabulary and its scopes, the held outcome of the shared key-admission seam, every key-admitting seam asking the owner's posture, and the forced reset's revocation, on by default. The seam summary gains the API-key admission seam as its sixth entry, so "each row names exactly one of these seams" stays true for D3. Claude-Session: https://claude.ai/code/session_01XgS6gBdsbPNicv9ELY6ne5 Co-authored-by: Claude --- docs/adr/0069-enterprise-authentication-hardening.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/adr/0069-enterprise-authentication-hardening.md b/docs/adr/0069-enterprise-authentication-hardening.md index b4bd841a4bd..91528e3abea 100644 --- a/docs/adr/0069-enterprise-authentication-hardening.md +++ b/docs/adr/0069-enterprise-authentication-hardening.md @@ -169,6 +169,7 @@ Plus admin-facing surface: a `sys_user` list-view filter for locked accounts + a 3. **Hono auth-route middleware** (in `plugin-auth` route registration, *before* delegating to better-auth) — IP allowlist, rate-limit pre-check. 4. **Session-validation path** (resolve-execution-context / `customSession`) — MFA step-up gate, password-expiry gate, idle-timeout, locked-account rejection. 5. **`bindAuthSettings` -> `applyConfigPatch`** — the existing channel that turns every new setting into live config without restart. +6. **API-key admission** (`resolveApiKeyAdmission` in `packages/core/src/security/api-key.ts`, fed by `userAuthGates`) — the key half of D3's amended note: an owner's `account`-scope posture holds the key (`held`). Each row in D1-D6 names exactly one of these seams. No setting is introduced without one. @@ -189,6 +190,13 @@ Each row in D1-D6 names exactly one of these seams. No setting is introduced wit | **P2/P3** (D6) | 🟔 partial | Generic OIDC RP wired (`genericOAuth`/`sso`); admin OIDC **trust-list settings UI** still env/`sys_sso_provider`-only. | | **P3** (SAML, broader social) | 🟔 partial | `@better-auth/sso` present (SAML now better-auth-native — see Addendum); broader settings-driven social providers pending. | +**Status (2026-10-14): ruling C′ is enforced.** D3's amended note holds on `main` through PR [#23440](https://github.com/objectstack-ai/objectstack/pull/23440) (`0f059078ec`) and PR [#23508](https://github.com/objectstack-ai/objectstack/pull/23508) (`daafae6d6d`): + +- **The posture vocabulary is closed.** `AuthGateCodeSchema` (`@objectstack/spec/kernel`) is the closed set of codes an `authGate` may carry: `PASSWORD_EXPIRED`, `PASSWORD_RESET_REQUIRED` and `MFA_REQUIRED`. `AUTH_GATE_SCOPES`, typed total over it, declares each code's scope once: `MFA_REQUIRED` holds the **account** (every credential of the user); `PASSWORD_EXPIRED` and `PASSWORD_RESET_REQUIRED` hold the **credential** (password sign-in alone). +- **An API key answers as the session does** (seam 6). `resolveApiKeyAdmission` asks `userAuthGates` for the key's owner and returns the `held` outcome on an `account`-scope posture; the door refuses the key with the posture's code. Nothing is written: the same key is admitted again once the posture clears. A `credential`-scope posture leaves the key admitted. +- **Every seam that admits a key asks.** PR #23508 made `userAuthGates` a required input of `resolveAuthzContext`, `resolveApiKeyAdmission` and `resolveApiKeyPrincipal`, which closed the two seams that still admitted a held owner's key: the `@objectstack/cloud-connection` install-local resolver and the `@objectstack/mcp` stdio face. The runtime dispatcher, the REST data API, the settings, storage-download, datasource-admin and share-link routes pass the source as well. `resolveApiKeyPrincipal` answers a held owner's key with no principal. +- **The forced reset revokes, on by default.** An administrator's forced password reset (D7) carries `revokeSessionsAndKeys`: it ends every session of the user (the D4 mechanism) and revokes every API key they own. It is on unless the administrator passes a literal `false`. Unlike the hold, this is a revocation: a revoked key stays revoked after the password change. + ## Out of scope / deferred - **SCIM / directory provisioning & bulk import** — separate ADR (provisioning, not authentication); tracked in #2246. From 18b3f3215b5bc3d0a7572a2dc6fe808cb6357481 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 11:35:35 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(adr-0069):=20the=20held=20key's=20use?= =?UTF-8?q?=20is=20still=20stamped=20=E2=80=94=20correct=20"nothing=20is?= =?UTF-8?q?=20written"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the held path resolveApiKeyAdmission stamps the key (last_used_at, and a no-expiry key's grace end) before it returns, so the status note's "nothing is written" was untrue. The note now says what holds: a refusal, not a revocation, no state of its own, the use stamped so the inactivity window does not run out during the hold. Claude-Session: https://claude.ai/code/session_01XgS6gBdsbPNicv9ELY6ne5 Co-authored-by: Claude --- docs/adr/0069-enterprise-authentication-hardening.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0069-enterprise-authentication-hardening.md b/docs/adr/0069-enterprise-authentication-hardening.md index 91528e3abea..b8251a292fd 100644 --- a/docs/adr/0069-enterprise-authentication-hardening.md +++ b/docs/adr/0069-enterprise-authentication-hardening.md @@ -193,7 +193,7 @@ Each row in D1-D6 names exactly one of these seams. No setting is introduced wit **Status (2026-10-14): ruling C′ is enforced.** D3's amended note holds on `main` through PR [#23440](https://github.com/objectstack-ai/objectstack/pull/23440) (`0f059078ec`) and PR [#23508](https://github.com/objectstack-ai/objectstack/pull/23508) (`daafae6d6d`): - **The posture vocabulary is closed.** `AuthGateCodeSchema` (`@objectstack/spec/kernel`) is the closed set of codes an `authGate` may carry: `PASSWORD_EXPIRED`, `PASSWORD_RESET_REQUIRED` and `MFA_REQUIRED`. `AUTH_GATE_SCOPES`, typed total over it, declares each code's scope once: `MFA_REQUIRED` holds the **account** (every credential of the user); `PASSWORD_EXPIRED` and `PASSWORD_RESET_REQUIRED` hold the **credential** (password sign-in alone). -- **An API key answers as the session does** (seam 6). `resolveApiKeyAdmission` asks `userAuthGates` for the key's owner and returns the `held` outcome on an `account`-scope posture; the door refuses the key with the posture's code. Nothing is written: the same key is admitted again once the posture clears. A `credential`-scope posture leaves the key admitted. +- **An API key answers as the session does** (seam 6). `resolveApiKeyAdmission` asks `userAuthGates` for the key's owner and returns the `held` outcome on an `account`-scope posture; the door refuses the key with the posture's code. The hold is a refusal, not a revocation: it keeps no state of its own, and the same key is admitted again once the posture clears. The key's use is still stamped while it is held (`last_used_at`, at most hourly, and, where the lifecycle policy anchors one, a no-expiry key's grace end as `expires_at`), so its inactivity window does not run out during the hold. A `credential`-scope posture leaves the key admitted. - **Every seam that admits a key asks.** PR #23508 made `userAuthGates` a required input of `resolveAuthzContext`, `resolveApiKeyAdmission` and `resolveApiKeyPrincipal`, which closed the two seams that still admitted a held owner's key: the `@objectstack/cloud-connection` install-local resolver and the `@objectstack/mcp` stdio face. The runtime dispatcher, the REST data API, the settings, storage-download, datasource-admin and share-link routes pass the source as well. `resolveApiKeyPrincipal` answers a held owner's key with no principal. - **The forced reset revokes, on by default.** An administrator's forced password reset (D7) carries `revokeSessionsAndKeys`: it ends every session of the user (the D4 mechanism) and revokes every API key they own. It is on unless the administrator passes a literal `false`. Unlike the hold, this is a revocation: a revoked key stays revoked after the password change.