From a73cc83ee617c03d08b6933db157ef16af059bdd Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 31 Aug 2026 20:05:54 +0200 Subject: [PATCH] swarm-nats-auth: grant each hive its own notices publish subject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hive_subjects` granted `$JS.API.STREAM.INFO`/`CREATE` on `hive-notices` but never the per-hive subject a publish actually targets, so every `notices::publish` was refused with a permissions violation naming `hive-notices.`. The gap survived because `notices_subjects`'s own doc comment asserted `extra_hive_subjects` covered it "since that one *is* per-hive". Nothing in `nix/` has ever passed `--hive-publish-subject` (`git grep` → 0 hits; control: `--reader-client` is passed at `nix/host-modules/swarm-nats.nix:711`), so the publish had been refused for as long as the feature existed. Subject comes from `swarm_queue_client::notices::subject`, the same constant the publisher formats from, rather than a literal here. Refs #3859 --- swarm-nats-auth/src/policy.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/swarm-nats-auth/src/policy.rs b/swarm-nats-auth/src/policy.rs index ae5900c3..48e94651 100644 --- a/swarm-nats-auth/src/policy.rs +++ b/swarm-nats-auth/src/policy.rs @@ -189,8 +189,8 @@ impl Policy { /// no such per-hive split at the `STREAM.INFO`/`STREAM.CREATE` layer /// — the stream itself, not a slice of it, is what every hive's /// `open_or_create` needs to reach before it can publish to its own - /// `hive-notices.` subject (which `extra_hive_subjects` already - /// covers, since that one *is* per-hive). Granting these two subjects + /// `hive-notices.` subject, which [`Self::hive_subjects`] grants + /// beside these from the same crate constant. Granting these two subjects /// to every hive is therefore correct, not a widening: it is /// `CREATE`/`INFO` on one named stream, the same shape already /// measured safe for the hive-status bucket in [`Self::create`] — @@ -233,6 +233,11 @@ impl Policy { format!("$KV.{}.{hive}", self.bucket), ]); subjects.extend(Self::notices_subjects()); + // The hive's own slice of the stream the subjects above let it open. + // Named from the defining crate so a rename cannot leave the grant + // pointing at a subject nobody publishes to. Not `extra_hive_subjects`: + // that is for streams this crate does not know about. + subjects.push(swarm_queue_client::notices::subject(hive)); subjects.extend( self.extra_hive_subjects .iter() @@ -544,6 +549,27 @@ mod tests { ); } + #[test] + fn a_hive_may_publish_to_its_own_notices_subject() { + // Opening the stream and writing into it are separate grants. + let p = policy() + .permissions("hive-alpha") + .expect("a hive is admitted"); + assert!(p.publish.contains(&"hive-notices.alpha".to_owned())); + } + + #[test] + fn a_hive_may_not_publish_to_another_hives_notices_subject() { + // The stream-level subjects are identical for every hive; this one + // must not be, or reaching the stream would carry writing to every + // other hive's slice of it. + let p = policy() + .permissions("hive-alpha") + .expect("a hive is admitted"); + assert!(!p.publish.contains(&"hive-notices.beta".to_owned())); + assert!(!p.publish.iter().any(|s| s == "hive-notices.>")); + } + #[test] fn the_notices_stream_grant_is_identical_across_hives() { // Unlike `$KV..` or an `extra_hive_subjects`