Skip to content

Commit 3798424

Browse files
Elon Muskclaude
andauthored
Repair the ten plugin-auth durability swallows — batch 5 of the #12981 worklist (#13488)
* fix(plugin-auth): report the refused identity writes ten `catch {}` sites swallowed Ten tier-1 DARK durability swallows in `plugin-auth`: a refused write that was neither logged, rethrown, nor delivered to the caller. Control flow is unchanged at every site — a bookkeeping write must never turn a valid sign-in into a 500 — but the failure is no longer silent. Eight in auth-manager.ts (stampIdentitySource, stampPasswordChangedAt, recordPasswordHistory, recordSignInOutcome, stampLastLogin, unlockUser, enforceSessionControls, enforceConcurrentCap) report through a new in-file `logDurabilityDegradation` on the guaranteed `warn` channel. Two in auth-plugin.ts report at `error` through the kernel Logger, whose `error` is required. Three inline `.catch(() => undefined)` swallows inside the two session controls are repaired in the same change: they sit between the refused write and the enclosing catch, so repairing only the outer handler would have produced a reporter that could never fire. What was hiding in the silence was mostly security controls that had quietly stopped enforcing — lockout accounting, the password-reuse ring, session revocation, the concurrent-session cap, and the managed-identity provenance stamp that gates local-password self-service. Every seam is pinned by a test that fails if the site goes silent again, plus two absence-asserting cases so a seam that warns unconditionally cannot pass. The `AuthManager` sink is re-exported from the package index.ts and declares no `error`, so the LEVEL stays `warn` and is recorded on the programme card rather than changed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi * docs(permissions): re-anchor the census row this batch's line shift rotted PURE LINE ROT, established before fixing: the page carries exactly one anchor for auth-plugin.ts and this file's `isSystem` occurrence list is byte-identical to the merge base's — same 11 occurrences, same text, same order — so no elevation read was added or removed. `--fix` rewrote the one anchor, 1254 -> 1288. Row 10 keeps the value that landed on main meanwhile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0b9ad00 commit 3798424

6 files changed

Lines changed: 785 additions & 24 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): report the refused identity writes ten `catch {}` sites swallowed (#12981)
6+
7+
Batch 5 of the ruled `catch { return null; }` worklist, scoped to `plugin-auth`.
8+
A re-run of the census instrument
9+
(`scripts/measure-durability-swallow-family.mjs`) moves tier-1 DARK from
10+
**21 sites in 11 files to 11 in 10**, with `LOUD` rising **32 → 34** and the
11+
adjacent `QUIET` bucket **85 → 93** — the ten, moved, nothing else touched.
12+
13+
Every site swallowed a refused write into a bare `catch` whose comment said
14+
"best-effort". That was true about **control flow** — a bookkeeping write must
15+
never turn a valid sign-in into a 500 — and it was being read as permission to
16+
say nothing, which is a different decision. The request answered 200, the
17+
session was issued, and nothing downstream looked wrong; the write simply did
18+
not happen. In `plugin-auth` that silence was hiding **security controls that
19+
had quietly stopped enforcing**.
20+
21+
**What an operator now sees that they did not before** — one line per seam,
22+
each naming the object that did not land, the control that is consequently not
23+
enforced, and the remedy:
24+
25+
- **`recordSignInOutcome`** — a refused `failed_login_count` / `locked_until`
26+
write means consecutive failures never accumulate and the account is never
27+
locked: `lockoutThreshold` (ADR-0069 D2) is configured and not enforced, so a
28+
brute-force run against that account meets no limit.
29+
- **`recordPasswordHistory`** — a refused ring write means the password just
30+
replaced was never recorded, so `passwordHistoryCount` (ADR-0069 D1) will not
31+
refuse it next time. "You cannot reuse your last N passwords" is advertised
32+
and, for that identity, not enforced.
33+
- **`enforceSessionControls`** — a refused revocation leaves a session that IS
34+
past its idle / absolute limit fully live (ADR-0069 D4). A refused activity
35+
heartbeat is reported separately, because it fails the other way: the idle
36+
clock keeps measuring from an older instant, so an active user is signed out
37+
**earlier** than the configured window.
38+
- **`enforceConcurrentCap`** — a refused revocation leaves the account holding
39+
more simultaneous sessions than `maxConcurrentSessions` allows.
40+
- **`stampIdentitySource`** (and its SCIM twin in `auth-plugin.ts`) — a refused
41+
provenance stamp leaves a federated identity still reading `env_native`, so it
42+
is offered the local-password actions that are supposed to hide for a managed
43+
identity (cloud ADR-0024 D4) — the path by which a managed user self-mints a
44+
password that bypasses enforced SSO. The SCIM hook is the sharper of the two:
45+
it is the **only** stamp on the adapter-level path and nothing retries it.
46+
- **`unlockUser`** — the password stage is cleared and the method still answers
47+
`true`, so the admin is told the unlock worked while a user locked at the
48+
second factor stays locked with no escape hatch at all. That `true` is
49+
exactly why the silence had to go.
50+
- **`stampPasswordChangedAt`** — a refused write leaves any admin-issued
51+
force-change flag SET (so the user is told to change a password they just
52+
changed) and leaves the password-age policy reading a timestamp for a password
53+
that no longer exists.
54+
- **`stampLastLogin`** — this write is what plugin-audit turns into the
55+
change-trail row, so a refusal leaves the sign-in with **no** trace in the
56+
compliance ledger at all.
57+
58+
Three inline `.catch(() => undefined)` swallows inside the two session controls
59+
are repaired in the same change. They are not optional to it: they sit between
60+
the refused write and the enclosing `catch`, so repairing only the outer handler
61+
would have produced a reporter that could never fire — a green-looking fix over
62+
an unchanged silence.
63+
64+
Level: the eight `AuthManager` seams report at `warn`, not `error`.
65+
`AuthManagerOptions.logger` declares `{ info?; warn }` with no `error` and is
66+
re-exported from the package `index.ts`, so adding one is a published-shape
67+
change; #12981 routes that question to #13398 and scopes this batch to the
68+
**silence**, exactly as batches 1 and 2 did for `plugin-security`. The two
69+
`AuthPlugin` seams log through the kernel `Logger`, whose `error` is required,
70+
and use it.
71+
72+
No entry was added to `scripts/durability-degradation.baseline.json`, and the
73+
gate vocabulary is untouched in either direction.

content/docs/permissions/system-context.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ that silently does not happen.
9797
| 8 | `explain()` may target a principal other than the caller | plugin-security | Get: no `manage_users` / delegated-admin check | `security-plugin.ts:3808` |
9898
| 9 | Anonymous-deny treats the caller as authenticated | core | Get: passes the 401 seam with no `userId` | `anonymous-deny.ts:154` |
9999
| 10 | Permission-set projection middleware skipped | plugin-security | Lose: projection of permission-set-derived columns | `permission-set-projection.ts:1015` |
100-
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1254` |
100+
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1288` |
101101
| 12 | Per-request performance timings disclosed | observability | Get: timing headers a normal caller cannot pull | `perf-timing.ts:474` |
102102
| 13 | Permission-set **overlay discard** skips the tenant-admin assertion | plugin-security | Get: an overlay can be discarded with no authenticated tenant administrator | `permission-set-overlay-discard.ts:142` |
103103
| 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:246` |

0 commit comments

Comments
 (0)