1use serde::{Deserialize, Deserializer, Serialize};
2use serde_with::{serde_as, TimestampSeconds};
3
4fn is_false(value: &bool) -> bool {
5 !value
6}
7
8fn string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
9where
10 D: Deserializer<'de>,
11{
12 #[derive(Deserialize)]
13 #[serde(untagged)]
14 enum StringOrVec {
15 String(String),
16 Vec(Vec<String>),
17 }
18
19 match StringOrVec::deserialize(deserializer)? {
20 StringOrVec::String(s) => Ok(vec![s]),
21 StringOrVec::Vec(v) => Ok(v),
22 }
23}
24
25#[serde_as]
26#[derive(Debug, Serialize, Deserialize, Default, Clone)]
27#[serde_with::skip_serializing_none]
28#[serde(default)]
29pub struct Claims {
30 #[serde(default, rename = "root", skip_serializing_if = "String::is_empty")]
33 pub root: String,
34
35 #[serde(
38 default,
39 rename = "put",
40 skip_serializing_if = "Vec::is_empty",
41 deserialize_with = "string_or_vec"
42 )]
43 pub publish: Vec<String>,
44
45 #[serde(default, rename = "cluster", skip_serializing_if = "is_false")]
51 pub cluster: bool,
52
53 #[serde(
57 default,
58 rename = "get",
59 skip_serializing_if = "Vec::is_empty",
60 deserialize_with = "string_or_vec"
61 )]
62 pub subscribe: Vec<String>,
63
64 #[serde(rename = "exp")]
66 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
67 pub expires: Option<std::time::SystemTime>,
68
69 #[serde(rename = "iat")]
71 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
72 pub issued: Option<std::time::SystemTime>,
73}
74
75impl Claims {
76 pub fn validate(&self) -> anyhow::Result<()> {
77 if self.publish.is_empty() && self.subscribe.is_empty() {
78 anyhow::bail!("no publish or subscribe allowed; token is useless");
79 }
80
81 Ok(())
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 use std::time::{Duration, SystemTime};
90
91 fn create_test_claims() -> Claims {
92 Claims {
93 root: "test-path".to_string(),
94 publish: vec!["test-pub".into()],
95 cluster: false,
96 subscribe: vec!["test-sub".into()],
97 expires: Some(SystemTime::now() + Duration::from_secs(3600)),
98 issued: Some(SystemTime::now()),
99 }
100 }
101
102 #[test]
103 fn test_claims_validation_success() {
104 let claims = create_test_claims();
105 assert!(claims.validate().is_ok());
106 }
107
108 #[test]
109 fn test_claims_validation_no_publish_or_subscribe() {
110 let claims = Claims {
111 root: "test-path".to_string(),
112 publish: vec![],
113 subscribe: vec![],
114 cluster: false,
115 expires: None,
116 issued: None,
117 };
118
119 let result = claims.validate();
120 assert!(result.is_err());
121 assert!(result
122 .unwrap_err()
123 .to_string()
124 .contains("no publish or subscribe allowed; token is useless"));
125 }
126
127 #[test]
128 fn test_claims_validation_only_publish() {
129 let claims = Claims {
130 root: "test-path".to_string(),
131 publish: vec!["test-pub".into()],
132 subscribe: vec![],
133 cluster: false,
134 expires: None,
135 issued: None,
136 };
137
138 assert!(claims.validate().is_ok());
139 }
140
141 #[test]
142 fn test_claims_validation_only_subscribe() {
143 let claims = Claims {
144 root: "test-path".to_string(),
145 publish: vec![],
146 subscribe: vec!["test-sub".into()],
147 cluster: false,
148 expires: None,
149 issued: None,
150 };
151
152 assert!(claims.validate().is_ok());
153 }
154
155 #[test]
156 fn test_claims_validation_path_not_prefix_relative_publish() {
157 let claims = Claims {
158 root: "test-path".to_string(), publish: vec!["relative-pub".into()], subscribe: vec![],
161 cluster: false,
162 expires: None,
163 issued: None,
164 };
165
166 let result = claims.validate();
167 assert!(result.is_ok()); }
169
170 #[test]
171 fn test_claims_validation_path_not_prefix_relative_subscribe() {
172 let claims = Claims {
173 root: "test-path".to_string(), publish: vec![],
175 subscribe: vec!["relative-sub".into()], cluster: false,
177 expires: None,
178 issued: None,
179 };
180
181 let result = claims.validate();
182 assert!(result.is_ok()); }
184
185 #[test]
186 fn test_claims_validation_path_not_prefix_absolute_publish() {
187 let claims = Claims {
188 root: "test-path".to_string(), publish: vec!["/absolute-pub".into()], subscribe: vec![],
191 cluster: false,
192 expires: None,
193 issued: None,
194 };
195
196 assert!(claims.validate().is_ok());
197 }
198
199 #[test]
200 fn test_claims_validation_path_not_prefix_absolute_subscribe() {
201 let claims = Claims {
202 root: "test-path".to_string(), publish: vec![],
204 subscribe: vec!["/absolute-sub".into()], cluster: false,
206 expires: None,
207 issued: None,
208 };
209
210 assert!(claims.validate().is_ok());
211 }
212
213 #[test]
214 fn test_claims_validation_path_not_prefix_empty_publish() {
215 let claims = Claims {
216 root: "test-path".to_string(), publish: vec!["".into()], subscribe: vec![],
219 cluster: false,
220 expires: None,
221 issued: None,
222 };
223
224 assert!(claims.validate().is_ok());
225 }
226
227 #[test]
228 fn test_claims_validation_path_not_prefix_empty_subscribe() {
229 let claims = Claims {
230 root: "test-path".to_string(), publish: vec![],
232 subscribe: vec!["".into()], cluster: false,
234 expires: None,
235 issued: None,
236 };
237
238 assert!(claims.validate().is_ok());
239 }
240
241 #[test]
242 fn test_claims_validation_path_is_prefix() {
243 let claims = Claims {
244 root: "test-path".to_string(), publish: vec!["relative-pub".into()], subscribe: vec!["relative-sub".into()], cluster: false,
248 expires: None,
249 issued: None,
250 };
251
252 assert!(claims.validate().is_ok());
253 }
254
255 #[test]
256 fn test_claims_validation_empty_path() {
257 let claims = Claims {
258 root: "".to_string(), publish: vec!["test-pub".into()],
260 subscribe: vec![],
261 cluster: false,
262 expires: None,
263 issued: None,
264 };
265
266 assert!(claims.validate().is_ok());
267 }
268
269 #[test]
270 fn test_claims_serde() {
271 let claims = create_test_claims();
272 let json = serde_json::to_string(&claims).unwrap();
273 let deserialized: Claims = serde_json::from_str(&json).unwrap();
274
275 assert_eq!(deserialized.root, claims.root);
276 assert_eq!(deserialized.publish, claims.publish);
277 assert_eq!(deserialized.subscribe, claims.subscribe);
278 assert_eq!(deserialized.cluster, claims.cluster);
279 }
280
281 #[test]
282 fn test_claims_default() {
283 let claims = Claims::default();
284 assert_eq!(claims.root, "");
285 assert!(claims.publish.is_empty());
286 assert!(claims.subscribe.is_empty());
287 assert!(!claims.cluster);
288 assert_eq!(claims.expires, None);
289 assert_eq!(claims.issued, None);
290 }
291
292 #[test]
293 fn test_is_false_helper() {
294 assert!(is_false(&false));
295 assert!(!is_false(&true));
296 }
297
298 #[test]
299 fn test_deserialize_string_as_vec() {
300 let json = r#"{
301 "root": "test",
302 "put": "single-publish",
303 "get": "single-subscribe"
304 }"#;
305
306 let claims: Claims = serde_json::from_str(json).unwrap();
307 assert_eq!(claims.publish, vec!["single-publish"]);
308 assert_eq!(claims.subscribe, vec!["single-subscribe"]);
309 }
310
311 #[test]
312 fn test_deserialize_vec_as_vec() {
313 let json = r#"{
314 "root": "test",
315 "put": ["pub1", "pub2"],
316 "get": ["sub1", "sub2"]
317 }"#;
318
319 let claims: Claims = serde_json::from_str(json).unwrap();
320 assert_eq!(claims.publish, vec!["pub1", "pub2"]);
321 assert_eq!(claims.subscribe, vec!["sub1", "sub2"]);
322 }
323
324 #[test]
325 fn test_deserialize_mixed() {
326 let json = r#"{
327 "root": "test",
328 "put": "single",
329 "get": ["multi1", "multi2"]
330 }"#;
331
332 let claims: Claims = serde_json::from_str(json).unwrap();
333 assert_eq!(claims.publish, vec!["single"]);
334 assert_eq!(claims.subscribe, vec!["multi1", "multi2"]);
335 }
336}