swarm-nats-auth: answer callout requests concurrently, skip expired ones
Each request now runs in its own task, at most CONCURRENT_REQUESTS (32) at once, instead of being awaited inline by the subscription loop. A request whose exp (nats-server sets it to send time plus its authorization.timeout) has passed is skipped without a lookup or reply. Fixes #5434.
This commit is contained in:
parent
bf848ba65f
commit
6971b4e4ed
6 changed files with 334 additions and 48 deletions
|
|
@ -48,19 +48,24 @@ pub struct ServerId {
|
|||
pub id: String,
|
||||
}
|
||||
|
||||
/// Split a NATS JWT and deserialize its claim set's `nats` object.
|
||||
/// The claims of a request that this responder acts on. `iss`/`sub` are the
|
||||
/// server's business, and claiming to understand fields we do not act on
|
||||
/// would invite someone to trust them.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Claims {
|
||||
/// When the server stops waiting for the answer, in whole Unix seconds:
|
||||
/// nats-server sets it to the time it sent the request plus its
|
||||
/// `authorization.timeout`, rounded down.
|
||||
#[serde(default)]
|
||||
pub exp: Option<i64>,
|
||||
pub nats: AuthRequest,
|
||||
}
|
||||
|
||||
/// Split a NATS JWT and deserialize its claim set.
|
||||
///
|
||||
/// The signature is **not** verified here — see the module docs on
|
||||
/// `main.rs` for why that is a decision and not an oversight.
|
||||
pub fn decode(payload: &[u8]) -> Result<AuthRequest> {
|
||||
/// Only the `nats` object is modelled; `iss`/`sub`/`exp` are the server's
|
||||
/// business, and claiming to understand fields we do not act on would
|
||||
/// invite someone to trust them.
|
||||
#[derive(Deserialize)]
|
||||
struct Claims {
|
||||
nats: AuthRequest,
|
||||
}
|
||||
|
||||
pub fn decode(payload: &[u8]) -> Result<Claims> {
|
||||
let text = std::str::from_utf8(payload).context("auth request is not utf-8")?;
|
||||
let mut parts = text.split('.');
|
||||
// Exactly three segments: a fourth means this is not the shape we think
|
||||
|
|
@ -73,9 +78,7 @@ pub fn decode(payload: &[u8]) -> Result<AuthRequest> {
|
|||
};
|
||||
let raw = base64_url_decode(claims).context("claim segment is not base64url")?;
|
||||
|
||||
let claims: Claims =
|
||||
serde_json::from_slice(&raw).context("claim segment is not the expected JSON")?;
|
||||
Ok(claims.nats)
|
||||
serde_json::from_slice(&raw).context("claim segment is not the expected JSON")
|
||||
}
|
||||
|
||||
/// base64url without padding, as JWT uses. Written out rather than pulled in:
|
||||
|
|
@ -101,15 +104,16 @@ mod tests {
|
|||
fn decodes_a_token_request() {
|
||||
let req = decode(
|
||||
jwt_with(
|
||||
r#"{"nats":{"connect_opts":{"auth_token":"t0k"},
|
||||
r#"{"exp":1760000002,"nats":{"connect_opts":{"auth_token":"t0k"},
|
||||
"user_nkey":"UABC","server_id":{"id":"NDEADBEEF"}}}"#,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("decode");
|
||||
assert_eq!(req.connect_opts.auth_token.as_deref(), Some("t0k"));
|
||||
assert_eq!(req.user_nkey, "UABC");
|
||||
assert_eq!(req.server_id.id, "NDEADBEEF");
|
||||
assert_eq!(req.nats.connect_opts.auth_token.as_deref(), Some("t0k"));
|
||||
assert_eq!(req.nats.user_nkey, "UABC");
|
||||
assert_eq!(req.nats.server_id.id, "NDEADBEEF");
|
||||
assert_eq!(req.exp, Some(1_760_000_002));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -122,7 +126,8 @@ mod tests {
|
|||
.as_bytes(),
|
||||
)
|
||||
.expect("decode");
|
||||
assert!(req.connect_opts.auth_token.is_none());
|
||||
assert!(req.nats.connect_opts.auth_token.is_none());
|
||||
assert_eq!(req.exp, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue