1use serde::{Deserialize, Deserializer, Serialize};
2use serde_with::{TimestampSeconds, serde_as};
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_with::skip_serializing_none]
26#[serde_as]
27#[derive(Debug, Serialize, Deserialize, Default, Clone)]
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!(
122 result
123 .unwrap_err()
124 .to_string()
125 .contains("no publish or subscribe allowed; token is useless")
126 );
127 }
128
129 #[test]
130 fn test_claims_validation_only_publish() {
131 let claims = Claims {
132 root: "test-path".to_string(),
133 publish: vec!["test-pub".into()],
134 subscribe: vec![],
135 cluster: false,
136 expires: None,
137 issued: None,
138 };
139
140 assert!(claims.validate().is_ok());
141 }
142
143 #[test]
144 fn test_claims_validation_only_subscribe() {
145 let claims = Claims {
146 root: "test-path".to_string(),
147 publish: vec![],
148 subscribe: vec!["test-sub".into()],
149 cluster: false,
150 expires: None,
151 issued: None,
152 };
153
154 assert!(claims.validate().is_ok());
155 }
156
157 #[test]
158 fn test_claims_validation_path_not_prefix_relative_publish() {
159 let claims = Claims {
160 root: "test-path".to_string(), publish: vec!["relative-pub".into()], subscribe: vec![],
163 cluster: false,
164 expires: None,
165 issued: None,
166 };
167
168 let result = claims.validate();
169 assert!(result.is_ok()); }
171
172 #[test]
173 fn test_claims_validation_path_not_prefix_relative_subscribe() {
174 let claims = Claims {
175 root: "test-path".to_string(), publish: vec![],
177 subscribe: vec!["relative-sub".into()], cluster: false,
179 expires: None,
180 issued: None,
181 };
182
183 let result = claims.validate();
184 assert!(result.is_ok()); }
186
187 #[test]
188 fn test_claims_validation_path_not_prefix_absolute_publish() {
189 let claims = Claims {
190 root: "test-path".to_string(), publish: vec!["/absolute-pub".into()], subscribe: vec![],
193 cluster: false,
194 expires: None,
195 issued: None,
196 };
197
198 assert!(claims.validate().is_ok());
199 }
200
201 #[test]
202 fn test_claims_validation_path_not_prefix_absolute_subscribe() {
203 let claims = Claims {
204 root: "test-path".to_string(), publish: vec![],
206 subscribe: vec!["/absolute-sub".into()], cluster: false,
208 expires: None,
209 issued: None,
210 };
211
212 assert!(claims.validate().is_ok());
213 }
214
215 #[test]
216 fn test_claims_validation_path_not_prefix_empty_publish() {
217 let claims = Claims {
218 root: "test-path".to_string(), publish: vec!["".into()], subscribe: vec![],
221 cluster: false,
222 expires: None,
223 issued: None,
224 };
225
226 assert!(claims.validate().is_ok());
227 }
228
229 #[test]
230 fn test_claims_validation_path_not_prefix_empty_subscribe() {
231 let claims = Claims {
232 root: "test-path".to_string(), publish: vec![],
234 subscribe: vec!["".into()], cluster: false,
236 expires: None,
237 issued: None,
238 };
239
240 assert!(claims.validate().is_ok());
241 }
242
243 #[test]
244 fn test_claims_validation_path_is_prefix() {
245 let claims = Claims {
246 root: "test-path".to_string(), publish: vec!["relative-pub".into()], subscribe: vec!["relative-sub".into()], cluster: false,
250 expires: None,
251 issued: None,
252 };
253
254 assert!(claims.validate().is_ok());
255 }
256
257 #[test]
258 fn test_claims_validation_empty_path() {
259 let claims = Claims {
260 root: "".to_string(), publish: vec!["test-pub".into()],
262 subscribe: vec![],
263 cluster: false,
264 expires: None,
265 issued: None,
266 };
267
268 assert!(claims.validate().is_ok());
269 }
270
271 #[test]
272 fn test_claims_serde() {
273 let claims = create_test_claims();
274 let json = serde_json::to_string(&claims).unwrap();
275 let deserialized: Claims = serde_json::from_str(&json).unwrap();
276
277 assert_eq!(deserialized.root, claims.root);
278 assert_eq!(deserialized.publish, claims.publish);
279 assert_eq!(deserialized.subscribe, claims.subscribe);
280 assert_eq!(deserialized.cluster, claims.cluster);
281 }
282
283 #[test]
284 fn test_claims_default() {
285 let claims = Claims::default();
286 assert_eq!(claims.root, "");
287 assert!(claims.publish.is_empty());
288 assert!(claims.subscribe.is_empty());
289 assert!(!claims.cluster);
290 assert_eq!(claims.expires, None);
291 assert_eq!(claims.issued, None);
292 }
293
294 #[test]
295 fn test_is_false_helper() {
296 assert!(is_false(&false));
297 assert!(!is_false(&true));
298 }
299
300 #[test]
301 fn test_deserialize_string_as_vec() {
302 let json = r#"{
303 "root": "test",
304 "put": "single-publish",
305 "get": "single-subscribe"
306 }"#;
307
308 let claims: Claims = serde_json::from_str(json).unwrap();
309 assert_eq!(claims.publish, vec!["single-publish"]);
310 assert_eq!(claims.subscribe, vec!["single-subscribe"]);
311 }
312
313 #[test]
314 fn test_deserialize_vec_as_vec() {
315 let json = r#"{
316 "root": "test",
317 "put": ["pub1", "pub2"],
318 "get": ["sub1", "sub2"]
319 }"#;
320
321 let claims: Claims = serde_json::from_str(json).unwrap();
322 assert_eq!(claims.publish, vec!["pub1", "pub2"]);
323 assert_eq!(claims.subscribe, vec!["sub1", "sub2"]);
324 }
325
326 #[test]
327 fn test_deserialize_mixed() {
328 let json = r#"{
329 "root": "test",
330 "put": "single",
331 "get": ["multi1", "multi2"]
332 }"#;
333
334 let claims: Claims = serde_json::from_str(json).unwrap();
335 assert_eq!(claims.publish, vec!["single"]);
336 assert_eq!(claims.subscribe, vec!["multi1", "multi2"]);
337 }
338}