diff --git a/swarm-nats-auth/src/policy.rs b/swarm-nats-auth/src/policy.rs index 6589fd1e..749ed0e3 100644 --- a/swarm-nats-auth/src/policy.rs +++ b/swarm-nats-auth/src/policy.rs @@ -802,6 +802,16 @@ impl Policy { format!("$JS.API.CONSUMER.CREATE.{agent_status}"), format!("$JS.API.CONSUMER.CREATE.{agent_status}.>"), ]); + // The permissions bucket, listed only: the controller's five-minute + // re-mirror calls `keys()`, whose ephemeral ordered consumer has no + // name, so the bare create subject is the one subject it needs. No + // `.>` form: that is for named consumers, and none is created here. + // Open, read and write of the bucket are already covered by + // `STREAM.INFO.*`, `DIRECT.GET.*.>` and `$KV.*.>` above. + subjects.push(format!( + "$JS.API.CONSUMER.CREATE.{}", + swarm_wire::agent_permissions::STREAM + )); subjects } } @@ -852,6 +862,29 @@ mod tests { assert!(!subjects.contains(&format!("$JS.API.STREAM.DELETE.{s}"))); } + #[test] + fn the_reader_can_list_the_agent_permissions_bucket_and_no_other_stream() { + let subjects = policy().reader_subjects(); + let s = swarm_wire::agent_permissions::STREAM; + // The bare form is what `keys()` needs: its ephemeral consumer has + // no name. + assert!(subjects.contains(&format!("$JS.API.CONSUMER.CREATE.{s}"))); + // Negative controls: the grant names this one stream, and holds no + // wildcard, `.>` form or delete/info subject for it. + assert!(!subjects.contains(&"$JS.API.CONSUMER.CREATE.*".to_owned())); + assert!(!subjects.contains(&format!("$JS.API.CONSUMER.CREATE.{s}.>"))); + assert!(!subjects.contains(&format!("$JS.API.CONSUMER.CREATE.{s}.*"))); + assert!( + !subjects + .iter() + .any(|x| x.starts_with("$JS.API.CONSUMER.DELETE")) + ); + assert!( + !subjects.contains(&"$JS.API.CONSUMER.CREATE.KV_unrelated".to_owned()), + "an unrelated stream must not be creatable" + ); + } + #[test] fn no_hive_may_write_another_role_s_agent_status() { // A hive publishes its *own* agents' status under