diff --git a/swarm-nats-auth/src/policy.rs b/swarm-nats-auth/src/policy.rs index 9b7f2006..68829ff9 100644 --- a/swarm-nats-auth/src/policy.rs +++ b/swarm-nats-auth/src/policy.rs @@ -150,6 +150,28 @@ impl Policy { format!("KV_{}", self.bucket) } + /// What *any* `JetStream` client must be able to ask before it can do + /// anything at all, bucket-specific or not. + /// + /// Both were measured from the server's own refusals, not reasoned about: + /// a grant carrying every bucket-specific subject and neither of these + /// cannot even create the bucket — the client times out on `$JS.API.INFO` + /// long before it reaches a subject that was granted. + /// + /// - `$JS.API.INFO` — account-level `JetStream` info, requested on connect. + /// - `$JS.API.STREAM.NAMES` — how a client finds the stream backing a + /// bucket. It lets a client enumerate stream names in the account, which + /// in an account holding one bucket discloses a name both ends already + /// share. + /// + /// 🩸 Earlier measurements missed both, because they either granted + /// `$JS.API.>` wholesale or ran against a bucket the *setup* had already + /// created while unscoped. A minimum established against an existing + /// bucket is not the minimum for making one. + fn jetstream_minimum() -> [String; 2] { + ["$JS.API.INFO".to_owned(), "$JS.API.STREAM.NAMES".to_owned()] + } + /// Creating the bucket, which **both** ends need. /// /// `swarm_queue_client::status::open_or_create` is called by the hive that @@ -168,11 +190,12 @@ impl Policy { } fn hive_subjects(&self, hive: &str) -> Vec { - let mut subjects = vec![ + let mut subjects = Self::jetstream_minimum().to_vec(); + subjects.extend([ format!("$JS.API.STREAM.INFO.{}", self.stream()), self.create(), format!("$KV.{}.{hive}", self.bucket), - ]; + ]); subjects.extend( self.extra_hive_subjects .iter() @@ -183,7 +206,8 @@ impl Policy { fn reader_subjects(&self) -> Vec { let stream = self.stream(); - vec![ + let mut subjects = Self::jetstream_minimum().to_vec(); + subjects.extend([ format!("$JS.API.STREAM.INFO.{stream}"), self.create(), // The `.>` form specifically: the bare `$JS.API.DIRECT.GET.` @@ -194,7 +218,8 @@ impl Policy { // reader without this can get a key it already knows and discover // nothing. format!("$JS.API.CONSUMER.CREATE.{stream}.>"), - ] + ]); + subjects } } @@ -281,6 +306,24 @@ mod tests { ); } + #[test] + fn every_grant_carries_the_jetstream_minimum() { + // 🩸 Found by the shipping gate, not by any unit test: a grant with + // every bucket-specific subject and neither of these cannot create the + // bucket at all. The client times out on `$JS.API.INFO` before it + // reaches anything that was granted, and a NATS denial looks like a + // hang from the client side — the server log is what named them. + for client in ["hive-alpha", "swarm-controller"] { + let p = policy().permissions(client).expect("admitted"); + for required in ["$JS.API.INFO", "$JS.API.STREAM.NAMES"] { + assert!( + p.publish.iter().any(|s| s == required), + "{client} is missing {required}, so it cannot use JetStream at all" + ); + } + } + } + #[test] fn both_ends_may_create_the_bucket_but_not_reshape_it() { // 🩸 The bug the subject measurements could not see: they ran against