fix(#3297): grant the JetStream subjects every client needs first
A grant carrying every bucket-specific subject and neither of these cannot create the bucket at all: the client times out on `$JS.API.INFO` long before it reaches a subject that was granted, and a NATS denial reaches the client as a hang rather than an error. Both were named by the server's own log, not reasoned about. `$JS.API.INFO` is the account-level JetStream info every client requests on connect; `$JS.API.STREAM.NAMES` is how a client finds the stream backing a bucket. The latter lets a client enumerate stream names in the account, which in an account holding one bucket discloses a name both ends already share. Every earlier measurement missed them, because each 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, so the leave-one-out that trimmed the reader's set could not have found this — every candidate it tried was tried in a world where the bucket existed. Found by running the shipping gate against the real binary. No unit test could have: the failure is a timeout inside a real server's permission check.
This commit is contained in:
parent
7b5f383b05
commit
61a13a63d4
1 changed files with 47 additions and 4 deletions
|
|
@ -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<String> {
|
||||
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<String> {
|
||||
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.<stream>`
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue