swarm-nats-auth: a review classifier hears try-outs and answers by reply only
swarm-queue-client gains review_classifier: the try-out subject, the request and reply that travel on it, and the deadlines both ends share. The controller's reader grant may publish the subject; a client named with --review-classifier-client may subscribe to it and its inbox, and answers under NATS allow_responses (resp in the user JWT) within the run deadline. Every other client id is still denied.
This commit is contained in:
parent
fb5eb9edc1
commit
98c873965b
8 changed files with 354 additions and 13 deletions
|
|
@ -160,6 +160,9 @@ struct UserNats {
|
|||
/// Subjects this client may subscribe to; absent means every subject.
|
||||
#[serde(rename = "sub", skip_serializing_if = "Option::is_none")]
|
||||
subscribe: Option<Permission>,
|
||||
/// The reply allowance; absent means none.
|
||||
#[serde(rename = "resp", skip_serializing_if = "Option::is_none")]
|
||||
responses: Option<ResponsePermission>,
|
||||
subs: i64,
|
||||
data: i64,
|
||||
payload: i64,
|
||||
|
|
@ -178,6 +181,25 @@ struct Permission {
|
|||
allow: Vec<String>,
|
||||
}
|
||||
|
||||
/// NATS' `allow_responses`: the client may publish `max` messages to the
|
||||
/// reply subject of each request it receives, within `ttl` nanoseconds (Go's
|
||||
/// `time.Duration` in `jwt/v2`'s `ResponsePermission`).
|
||||
#[derive(Serialize)]
|
||||
struct ResponsePermission {
|
||||
max: i64,
|
||||
ttl: i64,
|
||||
}
|
||||
|
||||
impl ResponsePermission {
|
||||
/// One answer per request, within `window`.
|
||||
fn one_within(window: std::time::Duration) -> Self {
|
||||
Self {
|
||||
max: 1,
|
||||
ttl: i64::try_from(window.as_nanos()).unwrap_or(i64::MAX),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mint the user JWT an admitted client presents.
|
||||
///
|
||||
/// `account` is the account **name** from the server's `accounts` block (the
|
||||
|
|
@ -188,8 +210,7 @@ fn user_jwt(
|
|||
issuer: &KeyPair,
|
||||
account: &str,
|
||||
user_nkey: &str,
|
||||
publish: Vec<String>,
|
||||
subscribe: Option<Vec<String>>,
|
||||
permissions: &crate::policy::Permissions,
|
||||
) -> String {
|
||||
sign(
|
||||
Claims {
|
||||
|
|
@ -201,8 +222,14 @@ fn user_jwt(
|
|||
aud: Some(account.to_owned()),
|
||||
nats: UserNats {
|
||||
kind: "user",
|
||||
publish: Permission { allow: publish },
|
||||
subscribe: subscribe.map(|allow| Permission { allow }),
|
||||
publish: Permission {
|
||||
allow: permissions.publish.clone(),
|
||||
},
|
||||
subscribe: permissions
|
||||
.subscribe
|
||||
.clone()
|
||||
.map(|allow| Permission { allow }),
|
||||
responses: permissions.responses.map(ResponsePermission::one_within),
|
||||
subs: -1,
|
||||
data: -1,
|
||||
payload: -1,
|
||||
|
|
@ -228,14 +255,7 @@ pub fn grant(
|
|||
permissions: &crate::policy::Permissions,
|
||||
) -> String {
|
||||
let now = now_secs();
|
||||
let jwt = user_jwt(
|
||||
now,
|
||||
issuer,
|
||||
account,
|
||||
user_nkey,
|
||||
permissions.publish.clone(),
|
||||
permissions.subscribe.clone(),
|
||||
);
|
||||
let jwt = user_jwt(now, issuer, account, user_nkey, permissions);
|
||||
response(now, issuer, server_id, user_nkey, Ok(jwt))
|
||||
}
|
||||
|
||||
|
|
@ -359,6 +379,7 @@ mod tests {
|
|||
crate::policy::Permissions {
|
||||
publish: vec!["$KV.hive-status.alpha".to_owned()],
|
||||
subscribe: None,
|
||||
responses: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -401,6 +422,27 @@ mod tests {
|
|||
assert!(user["nats"]["sub"].is_null(), "{}", user["nats"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_issued_user_jwt_carries_a_reply_allowance_only_when_given_one() {
|
||||
let account = KeyPair::new_account();
|
||||
let answering = crate::policy::Permissions {
|
||||
responses: Some(std::time::Duration::from_mins(31)),
|
||||
..perms()
|
||||
};
|
||||
let wrapper = decode_claims(&grant(&account, "APP", "NSERVER", "UCLIENT", &answering));
|
||||
let user = decode_claims(wrapper["nats"]["jwt"].as_str().expect("a user jwt"));
|
||||
// The names and units nats-server's `jwt/v2` reads: `max` messages,
|
||||
// `ttl` as a Go duration in nanoseconds.
|
||||
assert_eq!(
|
||||
user["nats"]["resp"],
|
||||
serde_json::json!({ "max": 1, "ttl": 31 * 60 * 1_000_000_000_i64 })
|
||||
);
|
||||
|
||||
let wrapper = decode_claims(&grant(&account, "APP", "NSERVER", "UCLIENT", &perms()));
|
||||
let user = decode_claims(wrapper["nats"]["jwt"].as_str().expect("a user jwt"));
|
||||
assert!(user["nats"]["resp"].is_null(), "{}", user["nats"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_grant_carries_a_user_jwt_and_no_error() {
|
||||
let account = KeyPair::new_account();
|
||||
|
|
|
|||
Loading…
Reference in a new issue