feat(#3297): scope a hive's queue grant to its own subjects
Every admitted client got the same unrestricted grant, so any hive could write any other hive's status key. The responder now derives a permission set from the caller's identity and mints it into the user JWT. A hive may publish to its own KV key and the two JetStream subjects needed to reach it; the controller may list and fetch every key and write none; anything else is denied outright. Deny is the default because every other shape fails open, and silently: a client that matched no rule and kept the old grant would make the policy advisory. The subject sets are measured rather than reasoned about, and two of them are counter-intuitive. `$KV.<bucket>.<key>` alone does not let a client write that key, because the client resolves the bucket first. And `$JS.API.>` is not "the JetStream permission": it also covers `$JS.API.STREAM.DELETE`, with which a hive correctly refused on a neighbour's key can delete the whole bucket and every hive's data with it. Granting it would have made per-key scoping decorative, so the subjects are named individually and a test asserts the wildcard does not come back as a convenience. Minimality is by removal: each subject was dropped in turn to confirm the client breaks without it. That is not pedantry — an additive search had called a set minimal while two of its five subjects were never needed, which ships an unnecessary grant with a measurement attached making it look earned. Both grants include `STREAM.CREATE` on the one named stream, because `status::open_or_create` is called by both ends: either may arrive first on a fresh swarm, and without it a new swarm never gets a bucket at all. `CREATE` is not `UPDATE`, so a second arrival cannot reshape the bucket the first one made. `status::BUCKET` moves out from behind the `kv` feature so this responder can share it. The name is a `&str` with no dependencies and only `open_or_create` needs JetStream; gating the name forced a third consumer to choose between a stack it does not use and a copied literal, and the copied literal is exactly the disagreement that module exists to prevent. Only publish is scoped. Subscription permissions are unrestricted and unmeasured, and the module docs say so rather than implying a property nothing established.
This commit is contained in:
parent
5539819330
commit
c8a3159297
7 changed files with 436 additions and 21 deletions
|
|
@ -144,9 +144,16 @@ pub fn chain(error: &dyn std::error::Error) -> String {
|
|||
}
|
||||
|
||||
/// The hive-status KV bucket, shared by the hive that writes it and the
|
||||
/// controller that reads it. Behind the `kv` feature — see the module doc
|
||||
/// for why a bucket name and its config belong to neither end alone.
|
||||
#[cfg(feature = "kv")]
|
||||
/// controller that reads it. See the module doc for why a bucket name and
|
||||
/// its config belong to neither end alone.
|
||||
///
|
||||
/// The module itself is unconditional; only the parts that *open* the bucket
|
||||
/// need the `kv` feature. The name is a `&str` with no dependencies, and a
|
||||
/// third end names it too — the auth-callout responder, which derives the
|
||||
/// subjects a hive may publish to from it without ever speaking `jetstream`.
|
||||
/// Gating the name behind `kv` would have forced that consumer to choose
|
||||
/// between pulling a JetStream stack it does not use and copying the literal,
|
||||
/// which is the disagreement this module exists to prevent.
|
||||
pub mod status;
|
||||
|
||||
/// Only the fields this needs; authelia returns several.
|
||||
|
|
|
|||
|
|
@ -15,10 +15,15 @@
|
|||
//! nobody chose. Sharing the constructor makes the race have one outcome
|
||||
//! instead of two.
|
||||
//!
|
||||
//! Feature-gated (`kv`) so the crate's other consumer, the auth-callout
|
||||
//! responder, still pulls neither `jetstream` nor `kv`: it speaks the
|
||||
//! connect and nothing else.
|
||||
//! The bucket *name* is unconditional; only [`open_or_create`] is behind the
|
||||
//! `kv` feature. A third end names the bucket without ever opening it — the
|
||||
//! auth-callout responder, which derives the subjects a hive may publish to
|
||||
//! from it — and it speaks neither `jetstream` nor `kv`. Gating the name too
|
||||
//! would have made that consumer choose between a JetStream stack it does not
|
||||
//! use and a copied literal, and a copied literal is precisely the agreement
|
||||
//! nothing checks.
|
||||
|
||||
#[cfg(feature = "kv")]
|
||||
use crate::Error;
|
||||
|
||||
/// The KV bucket hives publish their status snapshots into, one key per
|
||||
|
|
@ -39,6 +44,7 @@ pub const BUCKET: &str = "hive-status";
|
|||
/// controller and the hives come up in no particular order, and a bucket
|
||||
/// that must pre-exist turns "the swarm was deployed in the wrong order"
|
||||
/// into a permanent, silent absence of data.
|
||||
#[cfg(feature = "kv")]
|
||||
pub async fn open_or_create(
|
||||
client: &async_nats::Client,
|
||||
) -> Result<async_nats::jetstream::kv::Store, Error> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue