diff --git a/swarm-nats-auth/src/policy.rs b/swarm-nats-auth/src/policy.rs index bf58f70d..4f3a76b2 100644 --- a/swarm-nats-auth/src/policy.rs +++ b/swarm-nats-auth/src/policy.rs @@ -223,6 +223,16 @@ impl Policy { // `store.keys()` — the controller lists before it fetches, so a // reader without this can get a key it already knows and discover // nothing. + // + // BOTH forms, and the bare one is the one that matters. `keys()` + // creates an **ephemeral** ordered consumer, whose create subject + // carries no consumer name — and `>` matches one or more tokens, + // never zero, so the `.>` form alone does not cover it. Granting + // only that produced `Permissions Violation for Publish to + // "$JS.API.CONSUMER.CREATE.KV_hive-status"`, which reaches the + // client as a **timeout** and the operator as a 503. The `.>` form + // stays for a named/durable consumer. + format!("$JS.API.CONSUMER.CREATE.{stream}"), format!("$JS.API.CONSUMER.CREATE.{stream}.>"), ]); subjects @@ -356,6 +366,17 @@ mod tests { let p = policy() .permissions("swarm-controller") .expect("the reader is admitted"); + // The BARE subject, asserted separately and first: `keys()` creates an + // ephemeral consumer, so its create subject ends at the stream name, + // and `>` matches one or more tokens rather than zero. This assertion + // used to name only the `.>` form below — which reads as covering the + // bare one and does not, so the suite was green while every list timed + // out in production. + assert!( + p.publish + .contains(&"$JS.API.CONSUMER.CREATE.KV_hive-status".to_owned()), + "an ephemeral consumer create carries no name token" + ); assert!( p.publish .contains(&"$JS.API.CONSUMER.CREATE.KV_hive-status.>".to_owned())