refactor(#3255): one knowledge subject, single writer and many readers
Review call: the event was addressed per hive — `$SWARM.events.<hive>.knowledge`, published in a loop over the roster, granted through a wildcard. It does not need to be. The payload is empty and the event means the same thing to every hive, so one publish to one subject delivers exactly what N publishes to N subjects did, and core NATS already fans out to whoever is subscribed. A hive that was down misses it either way and reconciles on its next periodic pull. That deletes rather than reshuffles: the roster loop, the wildcard, and the shared subject-building function whose entire purpose was keeping the grant and the publish from drifting apart. With one literal there is nothing to disagree about. The per-hive shape was justified by the callout policy's rule that an extra subject must contain the hive name. That rule governs `extra_hive_subjects` — what a HIVE may publish. This subject lives in the controller's reader grant, which the rule does not constrain, so a real rule was carried across into a decision it had no authority over. Knowledge becomes its own category rather than a leaf under a general event namespace, since a namespace shaped for events that do not exist yet is a decision made before there is anything to decide from. The empty config-PR match arm goes with it: an arm with no body claims this is where the deploy path is handled, and it is not. The deny test stays and matters more, not less: with one shared subject a forged event would reach the whole swarm where a per-hive one reached a single hive.
This commit is contained in:
parent
84125b0806
commit
9b939f4626
6 changed files with 104 additions and 156 deletions
|
|
@ -234,18 +234,19 @@ impl Policy {
|
|||
// stays for a named/durable consumer.
|
||||
format!("$JS.API.CONSUMER.CREATE.{stream}"),
|
||||
format!("$JS.API.CONSUMER.CREATE.{stream}.>"),
|
||||
// Swarm events. The controller is the only publisher, and it
|
||||
// publishes to *every* hive's subject, so the grant takes the
|
||||
// wildcard form — from the same function the publisher calls, so a
|
||||
// rename cannot leave the grant naming a subject nobody uses.
|
||||
// The knowledge event. One writer, many readers: the controller is
|
||||
// the only publisher and every hive subscribes, so this is one
|
||||
// literal subject rather than a per-hive family — named from the
|
||||
// crate the publisher and the subscriber also name, so a rename
|
||||
// cannot leave the grant pointing at a subject nobody uses.
|
||||
//
|
||||
// This is the reader's only non-JetStream subject, and without it
|
||||
// the controller cannot emit an event at all. Worth stating because
|
||||
// the controller cannot emit the event at all. Worth stating because
|
||||
// the symptom is unhelpful: a refused publish reaches the client as
|
||||
// a **timeout**, so the visible failure is a hive that never hears
|
||||
// about a change, with nothing in the controller's log to say a
|
||||
// permission was the reason.
|
||||
swarm_queue_client::events::knowledge(swarm_queue_client::events::ANY_HIVE),
|
||||
swarm_queue_client::knowledge::SUBJECT.to_owned(),
|
||||
]);
|
||||
subjects
|
||||
}
|
||||
|
|
@ -308,36 +309,35 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn a_reader_may_publish_swarm_events_for_every_hive() {
|
||||
fn a_reader_may_publish_the_knowledge_event() {
|
||||
let p = policy().permissions("swarm-controller").expect("a reader");
|
||||
assert!(
|
||||
p.publish.contains(&swarm_queue_client::events::knowledge(
|
||||
swarm_queue_client::events::ANY_HIVE
|
||||
)),
|
||||
"the controller is the only event publisher; without this its \
|
||||
publish is refused, and a refusal arrives as a timeout"
|
||||
p.publish
|
||||
.contains(&swarm_queue_client::knowledge::SUBJECT.to_owned()),
|
||||
"the controller is the only publisher of this event; without the \
|
||||
grant its publish is refused, and a refusal arrives as a timeout"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hive_may_not_publish_a_swarm_event_to_anyone_including_itself() {
|
||||
fn a_hive_may_not_publish_the_knowledge_event_to_anyone_including_itself() {
|
||||
// The controller *interprets* what a delivery means; a hive receives
|
||||
// that verdict. A hive that could publish on this subject could tell a
|
||||
// neighbour — or itself — that the knowledge repo changed when it did
|
||||
// that verdict. A hive able to publish here could tell every other hive
|
||||
// in the swarm — or itself — that the knowledge repo changed when it did
|
||||
// not, which is an unauthenticated write into someone else's control
|
||||
// path wearing an event's shape.
|
||||
//
|
||||
// Asserted on the subject ROOT rather than on one rendered subject: a
|
||||
// future event leaf added to this namespace must fail this test too,
|
||||
// rather than passing because the test only knew about `knowledge`.
|
||||
// One writer and many readers makes this arm matter MORE, not less: with
|
||||
// a single shared subject a forged event reaches the whole swarm, where
|
||||
// a per-hive subject would have reached one.
|
||||
let p = policy()
|
||||
.permissions("hive-alpha")
|
||||
.expect("a hive is admitted");
|
||||
assert!(
|
||||
!p.publish
|
||||
.iter()
|
||||
.any(|s| s.starts_with(swarm_queue_client::events::SUBJECT_ROOT)),
|
||||
"a hive must not publish into the swarm event namespace: {:?}",
|
||||
.any(|s| s == swarm_queue_client::knowledge::SUBJECT),
|
||||
"a hive must not publish the knowledge event: {:?}",
|
||||
p.publish
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue