Commit Graph

9 Commits

Author SHA1 Message Date
Joseph Doherty e29fab19f6 test(acl): correct the array-cell comment — captured locals do take ref (re-review nit) 2026-08-18 07:13:16 -04:00
Joseph Doherty f4b91dbbab test(acl): race harness — hit diagnostics, 10k rounds, idiom notes (review round) 2026-08-18 07:10:44 -04:00
Joseph Doherty 441b34a391 fix(acl): make InvalidateIfStale a monotonic-max CAS loop — stamp publication is never deferred 2026-08-17 07:15:30 -04:00
Joseph Doherty d54b4fab62 docs(acl): fix the two comment nits from the T1 re-review — displacement precision, straggler-test claim 2026-08-17 06:09:13 -04:00
Joseph Doherty 98d6a1908e fix(acl): a stale-generation straggler can no longer roll the cache stamp backward and thrash ClearAll 2026-08-17 05:51:24 -04:00
Joseph Doherty 1979cf4119 fix(acl): stamp the generation on Shard so a rollover racer cannot serve a stale-generation verdict; eviction-swap capacity hint 2026-08-17 05:37:56 -04:00
Joseph Doherty d884934593 fix(acl): shard eviction swaps the shard object so _globalCount cannot over-subtract; concurrency stress test 2026-08-17 03:55:13 -04:00
Joseph Doherty debd44c213 perf(acl): per-session decision-cache shards — a crawl no longer evicts the fleet 2026-08-15 21:38:48 -04:00
Joseph Doherty 00aa486677 feat(security): enforce node ACLs on read, browse and subscribe (#520 stage 6)
Completes data-plane enforcement behind Security:Acl:Enabled. Read (every
attribute, not just Value), Browse, TranslateBrowsePath, CreateMonitoredItems,
SubscribeToEvents and SetMonitoringMode now consult scoped NodeAcl grants.
With the flag off nothing changes.

Four things went beyond the stage as written, each because leaving it out
would have made the feature look like it worked:

- Browse needed ancestor visibility. The trie inherits downward, and a client
  reaches a deep node by browsing the nodes above it, so without this every
  grant narrower than the cluster was authored, shipped and unreachable. It is
  a separate non-inheriting marker, NOT a synthesized Browse grant on each
  ancestor: the additive downward walk would carry such a grant from Plant onto
  Plant/Siemens/dev9 and every other sibling subtree. Pinned by a test that
  goes through the evaluator — probing the trie helper directly passes against
  the broken design, because a sibling is absent from the trie either way.

- Revocation had to reach LIVE subscriptions. Every other operation re-asks per
  request; a subscription is authorized once and then streams, and streaming is
  how machine consumers actually read this server — so stopping only the
  re-asking operations would have made revocation close to cosmetic.
  IDataPlaneAuthoritySource gained a Changed event.

  A denied item is queued BadUserAccessDenied and then DETACHED from its
  MonitoredNode2 fan-out, not set MonitoringMode.Disabled. Disabling discards
  the item's queued notifications, so the client is silenced without ever being
  told why. The over-the-wire test caught this; the unit-level path reports the
  item as stood down either way. Detaching is also reversible, so a restored
  grant resumes the feed with no client action.

- The decision cache holds the grant WALK, not the decision. Grants and the
  trie are fixed for a generation; a key's ExpiresUtc passes with the clock, so
  caching the whole decision would let an expired key keep working until the
  next deploy — the stage-5 open-session hole in a subtler form.

- TranslateBrowsePath is gated, without which browse filtering is decorative: a
  client refused a browse asks TranslateBrowsePathsToNodeIds for the same path.

Found en route: UserAuthorizationState's staleness ceiling couples two clocks.
MembershipResolvedUtc is stamped from DataPlaneAuthorizer's TimeProvider while
IsStale is evaluated against TriePermissionEvaluator's; they are the same
instance in production and diverge the moment anything injects a test clock, at
which point the evaluator denies EVERYTHING with a verdict indistinguishable
from "no grant matched". Stage 5's claim that these predicates cannot fire was
accidentally true; they are now disabled explicitly.

Also fixed: ContinuationPoint.NodeToBrowse is the manager HANDLE, not a
NodeState. The declared type is object, so the wrong cast compiled cleanly and
silently yielded null — skipping the browse source gate entirely.

Falsified: disabling the transit marking reddens exactly the three tests that
assert transit works; implementing transit as an ancestor grant reddens the
sibling-leak test; skipping the fan-out detach reddens the revocation test.
Full solution suite green.

The stage 8 live gate has NOT run yet.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
2026-08-01 09:22:12 -04:00