From 2e9ce53a32e5c332d742bbf1607f1f56c2fe5fb6 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 17 Aug 2026 01:31:56 +0200 Subject: [PATCH] docs(#3297): move each subject set's rationale next to the list it constrains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module doc had grown a 37-line preamble carrying three separate arguments, which the comment-block lint refuses. Splitting it is the better fix than raising the limit: the reader who is about to widen a subject list meets the reason not to at the list, not seven screens up. Also corrects one claim I had no measurement for. The CREATE note said creating an existing stream with a different config "is an error rather than a rewrite" — asserted, not observed. What is observed is narrower and enough: a hive holding this grant leaves the stream config untouched and never publishes $JS.API.STREAM.UPDATE at all. --- swarm-nats-auth/src/policy.rs | 60 +++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/swarm-nats-auth/src/policy.rs b/swarm-nats-auth/src/policy.rs index 68829ff9..bf58f70d 100644 --- a/swarm-nats-auth/src/policy.rs +++ b/swarm-nats-auth/src/policy.rs @@ -12,31 +12,19 @@ //! the failure would be silent. Denying costs a loud refusal the first time a //! new consumer appears, which is a config line to fix. //! -//! # Subjects were measured, not reasoned about -//! -//! The defaults below are the *minimal* sets, established by granting a -//! candidate set and then removing each subject in turn to confirm the client -//! breaks without it (`state/attack-3297-js-api-door.sh`, -//! `state/attack-3297-reader-scope.sh` in atlas's notes). Two things that a -//! reader would otherwise reasonably assume, and that are false: -//! -//! - `$KV..` alone does **not** let a client write that key. The -//! client looks the bucket up first, so `$JS.API.STREAM.INFO.KV_` is -//! part of the minimum. -//! - `$JS.API.>` is not "the `JetStream` permission". It also covers -//! `$JS.API.STREAM.DELETE.KV_`, with which any hive can destroy the -//! whole bucket — every hive's data — while being correctly refused on a -//! neighbour's individual key. Granting it makes per-key scoping decorative. -//! -//! # Only publish is scoped -//! -//! Subscription permissions are left unrestricted. The queue's confidentiality -//! boundary is the account, and a hive reading another hive's *published* -//! status is not the problem this solves — writing it is. Scoping `sub` is a -//! separate change with its own measurement, and claiming it here without one -//! would be the same mistake as `$JS.API.>` above. +//! Each subject set below is *minimal by removal*, and carries its own note +//! saying what breaks without it — the reasoning lives next to the list it +//! constrains rather than in one block here, because that is where a reader +//! about to edit the list will meet it. /// The subjects an admitted client may publish to. +/// +/// **Publish only — subscription is left unrestricted.** The queue's +/// confidentiality boundary is the account, and a hive reading another hive's +/// *published* status is not the problem this solves; writing it is. Scoping +/// `sub` is a separate change with its own measurement, and claiming it here +/// without one would repeat the `$JS.API.>` mistake described on +/// `Policy::hive_subjects`. #[derive(Debug, PartialEq, Eq)] pub struct Permissions { /// Subjects allowed for publish. Never empty: an empty allow-list is a @@ -181,14 +169,32 @@ impl Policy { /// connects first therefore has to be able to create it. /// /// Narrower than it looks: this is `CREATE` on one named stream, not - /// `UPDATE` and not the `$JS.API.>` wildcard. Creating a stream that - /// already exists with a different config is an error rather than a - /// rewrite, so a second arrival cannot reshape the bucket the first one - /// made. + /// `UPDATE` and not the `$JS.API.>` wildcard. Measured rather than + /// assumed — a hive holding this grant and running `stream edit` leaves + /// the stream's config untouched, and no `$JS.API.STREAM.UPDATE` is ever + /// published. Worth stating because the failure it would hide is quiet: a + /// hive able to reshape the shared bucket could set `MaxMsgs: 1` and evict + /// every other hive's status without ever touching `STREAM.DELETE`, and + /// per-key scoping would still look intact. fn create(&self) -> String { format!("$JS.API.STREAM.CREATE.{}", self.stream()) } + /// What one hive may publish: the account minimum, the bucket lookup, + /// creation, and its **own** key. + /// + /// Two things a reader would reasonably assume, both false and both + /// measured (`state/attack-3297-js-api-door.sh` in atlas's notes): + /// + /// - `$KV..` alone does **not** let a client write that key. + /// The client resolves the bucket first, so the `STREAM.INFO` subject is + /// part of the minimum for a plain write. + /// - `$JS.API.>` is not "the `JetStream` permission". It also covers + /// `$JS.API.STREAM.DELETE`, with which a hive correctly refused on a + /// neighbour's individual key can destroy the whole bucket — every + /// hive's data. Granting it would make per-key scoping decorative, which + /// is why these are named one at a time and a test asserts the wildcard + /// never returns as a convenience. fn hive_subjects(&self, hive: &str) -> Vec { let mut subjects = Self::jetstream_minimum().to_vec(); subjects.extend([