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)
|
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.
|
/// Creating the bucket, which **both** ends need.
|
||||||
///
|
///
|
||||||
/// `swarm_queue_client::status::open_or_create` is called by the hive that
|
/// `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> {
|
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()),
|
format!("$JS.API.STREAM.INFO.{}", self.stream()),
|
||||||
self.create(),
|
self.create(),
|
||||||
format!("$KV.{}.{hive}", self.bucket),
|
format!("$KV.{}.{hive}", self.bucket),
|
||||||
];
|
]);
|
||||||
subjects.extend(
|
subjects.extend(
|
||||||
self.extra_hive_subjects
|
self.extra_hive_subjects
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -183,7 +206,8 @@ impl Policy {
|
||||||
|
|
||||||
fn reader_subjects(&self) -> Vec<String> {
|
fn reader_subjects(&self) -> Vec<String> {
|
||||||
let stream = self.stream();
|
let stream = self.stream();
|
||||||
vec![
|
let mut subjects = Self::jetstream_minimum().to_vec();
|
||||||
|
subjects.extend([
|
||||||
format!("$JS.API.STREAM.INFO.{stream}"),
|
format!("$JS.API.STREAM.INFO.{stream}"),
|
||||||
self.create(),
|
self.create(),
|
||||||
// The `.>` form specifically: the bare `$JS.API.DIRECT.GET.<stream>`
|
// 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
|
// reader without this can get a key it already knows and discover
|
||||||
// nothing.
|
// nothing.
|
||||||
format!("$JS.API.CONSUMER.CREATE.{stream}.>"),
|
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]
|
#[test]
|
||||||
fn both_ends_may_create_the_bucket_but_not_reshape_it() {
|
fn both_ends_may_create_the_bucket_but_not_reshape_it() {
|
||||||
// 🩸 The bug the subject measurements could not see: they ran against
|
// 🩸 The bug the subject measurements could not see: they ran against
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue