use serde::{Deserialize, Deserializer, Serialize};
use serde_with::{TimestampSeconds, serde_as};
fn is_false(value: &bool) -> bool {
!value
}
fn string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}
match StringOrVec::deserialize(deserializer)? {
StringOrVec::String(s) => Ok(vec![s]),
StringOrVec::Vec(v) => Ok(v),
}
}
#[serde_with::skip_serializing_none]
#[serde_as]
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
#[serde(default)]
pub struct Claims {
#[serde(default, rename = "root", skip_serializing_if = "String::is_empty")]
pub root: String,
#[serde(
default,
rename = "put",
skip_serializing_if = "Vec::is_empty",
deserialize_with = "string_or_vec"
)]
pub publish: Vec<String>,
#[serde(default, rename = "cluster", skip_serializing_if = "is_false")]
pub cluster: bool,
#[serde(
default,
rename = "get",
skip_serializing_if = "Vec::is_empty",
deserialize_with = "string_or_vec"
)]
pub subscribe: Vec<String>,
#[serde(rename = "exp")]
#[serde_as(as = "Option<TimestampSeconds<i64>>")]
pub expires: Option<std::time::SystemTime>,
#[serde(rename = "iat")]
#[serde_as(as = "Option<TimestampSeconds<i64>>")]
pub issued: Option<std::time::SystemTime>,
}
impl Claims {
pub fn validate(&self) -> anyhow::Result<()> {
if self.publish.is_empty() && self.subscribe.is_empty() {
anyhow::bail!("no publish or subscribe allowed; token is useless");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, SystemTime};
fn create_test_claims() -> Claims {
Claims {
root: "test-path".to_string(),
publish: vec!["test-pub".into()],
cluster: false,
subscribe: vec!["test-sub".into()],
expires: Some(SystemTime::now() + Duration::from_secs(3600)),
issued: Some(SystemTime::now()),
}
}
#[test]
fn test_claims_validation_success() {
let claims = create_test_claims();
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_validation_no_publish_or_subscribe() {
let claims = Claims {
root: "test-path".to_string(),
publish: vec![],
subscribe: vec![],
cluster: false,
expires: None,
issued: None,
};
let result = claims.validate();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("no publish or subscribe allowed; token is useless")
);
}
#[test]
fn test_claims_validation_only_publish() {
let claims = Claims {
root: "test-path".to_string(),
publish: vec!["test-pub".into()],
subscribe: vec![],
cluster: false,
expires: None,
issued: None,
};
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_validation_only_subscribe() {
let claims = Claims {
root: "test-path".to_string(),
publish: vec![],
subscribe: vec!["test-sub".into()],
cluster: false,
expires: None,
issued: None,
};
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_validation_path_not_prefix_relative_publish() {
let claims = Claims {
root: "test-path".to_string(), publish: vec!["relative-pub".into()], subscribe: vec![],
cluster: false,
expires: None,
issued: None,
};
let result = claims.validate();
assert!(result.is_ok()); }
#[test]
fn test_claims_validation_path_not_prefix_relative_subscribe() {
let claims = Claims {
root: "test-path".to_string(), publish: vec![],
subscribe: vec!["relative-sub".into()], cluster: false,
expires: None,
issued: None,
};
let result = claims.validate();
assert!(result.is_ok()); }
#[test]
fn test_claims_validation_path_not_prefix_absolute_publish() {
let claims = Claims {
root: "test-path".to_string(), publish: vec!["/absolute-pub".into()], subscribe: vec![],
cluster: false,
expires: None,
issued: None,
};
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_validation_path_not_prefix_absolute_subscribe() {
let claims = Claims {
root: "test-path".to_string(), publish: vec![],
subscribe: vec!["/absolute-sub".into()], cluster: false,
expires: None,
issued: None,
};
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_validation_path_not_prefix_empty_publish() {
let claims = Claims {
root: "test-path".to_string(), publish: vec!["".into()], subscribe: vec![],
cluster: false,
expires: None,
issued: None,
};
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_validation_path_not_prefix_empty_subscribe() {
let claims = Claims {
root: "test-path".to_string(), publish: vec![],
subscribe: vec!["".into()], cluster: false,
expires: None,
issued: None,
};
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_validation_path_is_prefix() {
let claims = Claims {
root: "test-path".to_string(), publish: vec!["relative-pub".into()], subscribe: vec!["relative-sub".into()], cluster: false,
expires: None,
issued: None,
};
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_validation_empty_path() {
let claims = Claims {
root: "".to_string(), publish: vec!["test-pub".into()],
subscribe: vec![],
cluster: false,
expires: None,
issued: None,
};
assert!(claims.validate().is_ok());
}
#[test]
fn test_claims_serde() {
let claims = create_test_claims();
let json = serde_json::to_string(&claims).unwrap();
let deserialized: Claims = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.root, claims.root);
assert_eq!(deserialized.publish, claims.publish);
assert_eq!(deserialized.subscribe, claims.subscribe);
assert_eq!(deserialized.cluster, claims.cluster);
}
#[test]
fn test_claims_default() {
let claims = Claims::default();
assert_eq!(claims.root, "");
assert!(claims.publish.is_empty());
assert!(claims.subscribe.is_empty());
assert!(!claims.cluster);
assert_eq!(claims.expires, None);
assert_eq!(claims.issued, None);
}
#[test]
fn test_is_false_helper() {
assert!(is_false(&false));
assert!(!is_false(&true));
}
#[test]
fn test_deserialize_string_as_vec() {
let json = r#"{
"root": "test",
"put": "single-publish",
"get": "single-subscribe"
}"#;
let claims: Claims = serde_json::from_str(json).unwrap();
assert_eq!(claims.publish, vec!["single-publish"]);
assert_eq!(claims.subscribe, vec!["single-subscribe"]);
}
#[test]
fn test_deserialize_vec_as_vec() {
let json = r#"{
"root": "test",
"put": ["pub1", "pub2"],
"get": ["sub1", "sub2"]
}"#;
let claims: Claims = serde_json::from_str(json).unwrap();
assert_eq!(claims.publish, vec!["pub1", "pub2"]);
assert_eq!(claims.subscribe, vec!["sub1", "sub2"]);
}
#[test]
fn test_deserialize_mixed() {
let json = r#"{
"root": "test",
"put": "single",
"get": ["multi1", "multi2"]
}"#;
let claims: Claims = serde_json::from_str(json).unwrap();
assert_eq!(claims.publish, vec!["single"]);
assert_eq!(claims.subscribe, vec!["multi1", "multi2"]);
}
}