1use crate::path;
2use serde::{Deserialize, Serialize};
3use serde_with::{OneOrMany, TimestampSeconds, formats::PreferMany, serde_as};
4
5#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
16pub struct Scope {
17 #[serde(default, skip_serializing_if = "String::is_empty")]
19 pub root: String,
20
21 #[serde(default, rename = "put", skip_serializing_if = "Vec::is_empty")]
23 pub publish: Vec<String>,
24
25 #[serde(default, rename = "get", skip_serializing_if = "Vec::is_empty")]
27 pub subscribe: Vec<String>,
28}
29
30impl Scope {
31 pub fn validate(&self) -> crate::Result<()> {
33 if self.publish.is_empty() && self.subscribe.is_empty() {
34 return Err(crate::Error::UselessScope);
35 }
36
37 Ok(())
38 }
39
40 pub fn allows(&self, claims: &Claims) -> bool {
51 covers(&self.root, &self.publish, &claims.root, &claims.publish)
52 && covers(&self.root, &self.subscribe, &claims.root, &claims.subscribe)
53 }
54}
55
56fn covers(scope_root: &str, granted: &[String], claims_root: &str, requested: &[String]) -> bool {
62 let absolute = |root: &str, relative: &str| path::join(&path::normalize(root), &path::normalize(relative));
63
64 requested.iter().all(|request| {
65 let request = absolute(claims_root, request);
66 granted
67 .iter()
68 .any(|grant| path::has_prefix(&request, &absolute(scope_root, grant)))
69 })
70}
71
72#[derive(Debug, Clone, Default, PartialEq, Eq)]
78pub struct Permissions {
79 pub subscribe: Vec<String>,
81
82 pub publish: Vec<String>,
84}
85
86#[serde_with::skip_serializing_none]
99#[serde_as]
100#[derive(Debug, Serialize, Deserialize, Default, Clone)]
101#[serde(default)]
102#[non_exhaustive]
103pub struct Claims {
104 #[serde(default, rename = "root", skip_serializing_if = "String::is_empty")]
107 pub root: String,
108
109 #[serde(default, rename = "put", skip_serializing_if = "Vec::is_empty")]
112 #[serde_as(as = "OneOrMany<_, PreferMany>")]
113 pub publish: Vec<String>,
114
115 #[serde(default, rename = "get", skip_serializing_if = "Vec::is_empty")]
119 #[serde_as(as = "OneOrMany<_, PreferMany>")]
120 pub subscribe: Vec<String>,
121
122 #[serde(rename = "exp")]
124 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
125 pub expires: Option<std::time::SystemTime>,
126
127 #[serde(rename = "iat")]
129 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
130 pub issued: Option<std::time::SystemTime>,
131}
132
133impl Claims {
134 pub fn with_root(mut self, root: impl Into<String>) -> Self {
136 self.root = root.into();
137 self
138 }
139
140 pub fn with_publish(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
142 self.publish = paths.into_iter().map(Into::into).collect();
143 self
144 }
145
146 pub fn with_subscribe(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
148 self.subscribe = paths.into_iter().map(Into::into).collect();
149 self
150 }
151
152 pub fn with_expires(mut self, at: impl Into<Option<std::time::SystemTime>>) -> Self {
156 self.expires = at.into();
157 self
158 }
159
160 pub fn with_issued(mut self, at: impl Into<Option<std::time::SystemTime>>) -> Self {
164 self.issued = at.into();
165 self
166 }
167
168 pub fn validate(&self) -> crate::Result<()> {
170 if self.publish.is_empty() && self.subscribe.is_empty() {
171 return Err(crate::Error::UselessToken);
172 }
173
174 Ok(())
175 }
176
177 pub fn authorize(&self, path: &str) -> crate::Result<Permissions> {
197 let path = path::normalize(path);
198 let root = path::normalize(&self.root);
199
200 let (suffix, prefix) = if let Some(suffix) = path::strip_prefix(&path, &root) {
203 (suffix, "")
204 } else if let Some(prefix) = path::strip_prefix(&root, &path) {
205 ("", prefix)
206 } else {
207 return Err(crate::Error::RootMismatch(path));
208 };
209
210 let scope = |paths: &[String]| -> Vec<String> {
211 paths
212 .iter()
213 .filter_map(|granted| {
214 let granted = path::join(prefix, &path::normalize(granted));
215
216 if let Some(remaining) = path::strip_prefix(&granted, suffix) {
217 Some(remaining.to_string())
219 } else if path::has_prefix(suffix, &granted) {
220 Some(String::new())
223 } else {
224 None
225 }
226 })
227 .collect()
228 };
229
230 let permissions = Permissions {
231 subscribe: scope(&self.subscribe),
232 publish: scope(&self.publish),
233 };
234
235 if permissions.subscribe.is_empty() && permissions.publish.is_empty() {
236 return Err(crate::Error::NoAccess(path));
237 }
238
239 Ok(permissions)
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 use std::time::{Duration, SystemTime};
248
249 fn create_test_claims() -> Claims {
250 Claims {
251 root: "test-path".to_string(),
252 publish: vec!["test-pub".into()],
253 subscribe: vec!["test-sub".into()],
254 expires: Some(SystemTime::now() + Duration::from_secs(3600)),
255 issued: Some(SystemTime::now()),
256 }
257 }
258
259 #[test]
260 fn scope_allows_contained_claims() {
261 let scope = Scope {
262 root: "project".into(),
263 publish: vec!["live".into()],
264 subscribe: vec!["watch".into()],
265 };
266 let claims = Claims {
267 root: "project/live/room".into(),
268 publish: vec!["".into()],
269 subscribe: vec![],
270 ..Default::default()
271 };
272 assert!(scope.allows(&claims));
273 }
274
275 #[test]
276 fn scope_rejects_sibling_and_role_escalation() {
277 let scope = Scope {
278 root: "project".into(),
279 publish: vec!["live".into()],
280 subscribe: vec![],
281 };
282 let sibling = Claims {
283 root: "project/lively".into(),
284 publish: vec!["".into()],
285 ..Default::default()
286 };
287 let role = Claims {
288 root: "project/live".into(),
289 subscribe: vec!["".into()],
290 ..Default::default()
291 };
292 assert!(!scope.allows(&sibling));
293 assert!(!scope.allows(&role));
294 }
295
296 #[test]
297 fn scope_ignores_how_the_root_is_split() {
298 let scope = Scope {
300 root: "project".into(),
301 publish: vec!["live".into()],
302 subscribe: vec![],
303 };
304
305 for claims in [
306 Claims {
307 root: "project".into(),
308 publish: vec!["live/room".into()],
309 ..Default::default()
310 },
311 Claims {
312 root: String::new(),
313 publish: vec!["project/live/room".into()],
314 ..Default::default()
315 },
316 Claims {
317 root: "/project/live/".into(),
318 publish: vec!["/room".into()],
319 ..Default::default()
320 },
321 ] {
322 assert!(scope.allows(&claims), "{claims:?}");
323 }
324 }
325
326 #[test]
327 fn scope_rejects_escaping_above_its_root() {
328 let scope = Scope {
329 root: "project".into(),
330 publish: vec!["live".into()],
331 subscribe: vec![],
332 };
333
334 let claims = Claims {
337 root: String::new(),
338 publish: vec!["".into()],
339 ..Default::default()
340 };
341 assert!(!scope.allows(&claims));
342 }
343
344 #[test]
345 fn scope_empty_prefix_grants_everything_beneath_it() {
346 let scope = Scope {
347 root: "project".into(),
348 publish: vec![String::new()],
349 subscribe: vec![],
350 };
351 let claims = Claims {
352 root: "project/anything/deep".into(),
353 publish: vec!["".into()],
354 ..Default::default()
355 };
356 assert!(scope.allows(&claims));
357 }
358
359 #[test]
360 fn scope_requires_every_requested_path() {
361 let scope = Scope {
363 root: "project".into(),
364 publish: vec!["live".into()],
365 subscribe: vec![],
366 };
367 let claims = Claims {
368 root: "project".into(),
369 publish: vec!["live/room".into(), "other".into()],
370 ..Default::default()
371 };
372 assert!(!scope.allows(&claims));
373 }
374
375 #[test]
376 fn scope_without_grants_is_useless() {
377 assert!(matches!(Scope::default().validate(), Err(crate::Error::UselessScope)));
378 }
379
380 #[test]
381 fn test_claims_validation_success() {
382 let claims = create_test_claims();
383 assert!(claims.validate().is_ok());
384 }
385
386 #[test]
387 fn test_claims_validation_no_publish_or_subscribe() {
388 let claims = Claims {
389 root: "test-path".to_string(),
390 publish: vec![],
391 subscribe: vec![],
392 expires: None,
393 issued: None,
394 };
395
396 let result = claims.validate();
397 assert!(result.is_err());
398 assert!(
399 result
400 .unwrap_err()
401 .to_string()
402 .contains("no publish or subscribe allowed; token is useless")
403 );
404 }
405
406 #[test]
407 fn test_claims_validation_only_publish() {
408 let claims = Claims {
409 root: "test-path".to_string(),
410 publish: vec!["test-pub".into()],
411 subscribe: vec![],
412 expires: None,
413 issued: None,
414 };
415
416 assert!(claims.validate().is_ok());
417 }
418
419 #[test]
420 fn test_claims_validation_only_subscribe() {
421 let claims = Claims {
422 root: "test-path".to_string(),
423 publish: vec![],
424 subscribe: vec!["test-sub".into()],
425 expires: None,
426 issued: None,
427 };
428
429 assert!(claims.validate().is_ok());
430 }
431
432 #[test]
433 fn test_claims_validation_path_not_prefix_relative_publish() {
434 let claims = Claims {
435 root: "test-path".to_string(), publish: vec!["relative-pub".into()], subscribe: vec![],
438 expires: None,
439 issued: None,
440 };
441
442 let result = claims.validate();
443 assert!(result.is_ok()); }
445
446 #[test]
447 fn test_claims_validation_path_not_prefix_relative_subscribe() {
448 let claims = Claims {
449 root: "test-path".to_string(), publish: vec![],
451 subscribe: vec!["relative-sub".into()], expires: None,
453 issued: None,
454 };
455
456 let result = claims.validate();
457 assert!(result.is_ok()); }
459
460 #[test]
461 fn test_claims_validation_path_not_prefix_absolute_publish() {
462 let claims = Claims {
463 root: "test-path".to_string(), publish: vec!["/absolute-pub".into()], subscribe: vec![],
466 expires: None,
467 issued: None,
468 };
469
470 assert!(claims.validate().is_ok());
471 }
472
473 #[test]
474 fn test_claims_validation_path_not_prefix_absolute_subscribe() {
475 let claims = Claims {
476 root: "test-path".to_string(), publish: vec![],
478 subscribe: vec!["/absolute-sub".into()], expires: None,
480 issued: None,
481 };
482
483 assert!(claims.validate().is_ok());
484 }
485
486 #[test]
487 fn test_claims_validation_path_not_prefix_empty_publish() {
488 let claims = Claims {
489 root: "test-path".to_string(), publish: vec!["".into()], subscribe: vec![],
492 expires: None,
493 issued: None,
494 };
495
496 assert!(claims.validate().is_ok());
497 }
498
499 #[test]
500 fn test_claims_validation_path_not_prefix_empty_subscribe() {
501 let claims = Claims {
502 root: "test-path".to_string(), publish: vec![],
504 subscribe: vec!["".into()], expires: None,
506 issued: None,
507 };
508
509 assert!(claims.validate().is_ok());
510 }
511
512 #[test]
513 fn test_claims_validation_path_is_prefix() {
514 let claims = Claims {
515 root: "test-path".to_string(), publish: vec!["relative-pub".into()], subscribe: vec!["relative-sub".into()], expires: None,
519 issued: None,
520 };
521
522 assert!(claims.validate().is_ok());
523 }
524
525 #[test]
526 fn test_claims_validation_empty_path() {
527 let claims = Claims {
528 root: "".to_string(), publish: vec!["test-pub".into()],
530 subscribe: vec![],
531 expires: None,
532 issued: None,
533 };
534
535 assert!(claims.validate().is_ok());
536 }
537
538 #[test]
539 fn test_claims_serde() {
540 let claims = create_test_claims();
541 let json = serde_json::to_string(&claims).unwrap();
542 let deserialized: Claims = serde_json::from_str(&json).unwrap();
543
544 assert_eq!(deserialized.root, claims.root);
545 assert_eq!(deserialized.publish, claims.publish);
546 assert_eq!(deserialized.subscribe, claims.subscribe);
547 }
548
549 #[test]
550 fn test_claims_default() {
551 let claims = Claims::default();
552 assert_eq!(claims.root, "");
553 assert!(claims.publish.is_empty());
554 assert!(claims.subscribe.is_empty());
555 assert_eq!(claims.expires, None);
556 assert_eq!(claims.issued, None);
557 }
558
559 fn authorize_claims(root: &str, subscribe: &[&str], publish: &[&str]) -> Claims {
560 Claims {
561 root: root.to_string(),
562 subscribe: subscribe.iter().map(|s| s.to_string()).collect(),
563 publish: publish.iter().map(|s| s.to_string()).collect(),
564 ..Default::default()
565 }
566 }
567
568 #[test]
569 fn test_authorize_path_equals_root() {
570 let claims = authorize_claims("room/123", &[""], &["alice"]);
571 let permissions = claims.authorize("room/123").unwrap();
572
573 assert_eq!(permissions.subscribe, [""]);
574 assert_eq!(permissions.publish, ["alice"]);
575 }
576
577 #[test]
578 fn test_authorize_path_extends_root() {
579 let claims = authorize_claims("room/123", &["bob"], &["alice"]);
581 let permissions = claims.authorize("room/123/alice").unwrap();
582
583 assert_eq!(permissions.subscribe, Vec::<String>::new());
584 assert_eq!(permissions.publish, [""]);
585 }
586
587 #[test]
588 fn test_authorize_path_is_parent_of_root() {
589 let claims = authorize_claims("demo", &[""], &["alice"]);
591 let permissions = claims.authorize("/").unwrap();
592
593 assert_eq!(permissions.subscribe, ["demo"]);
594 assert_eq!(permissions.publish, ["demo/alice"]);
595 }
596
597 #[test]
598 fn test_authorize_empty_root() {
599 let claims = authorize_claims("", &["demo"], &[]);
601 let permissions = claims.authorize("demo/room").unwrap();
602
603 assert_eq!(permissions.subscribe, [""]);
604 assert_eq!(permissions.publish, Vec::<String>::new());
605 }
606
607 #[test]
608 fn test_authorize_slashes_are_implicit() {
609 let claims = authorize_claims("/room/123/", &["/bob/"], &[]);
610 let permissions = claims.authorize("//room/123//").unwrap();
611
612 assert_eq!(permissions.subscribe, ["bob"]);
613 }
614
615 #[test]
616 fn test_authorize_respects_segment_boundaries() {
617 let claims = authorize_claims("foo", &[""], &[""]);
619 assert!(matches!(claims.authorize("foobar"), Err(crate::Error::RootMismatch(_))));
620 }
621
622 #[test]
623 fn test_authorize_unrelated_path() {
624 let claims = authorize_claims("demo", &[""], &[""]);
625 assert!(matches!(claims.authorize("other"), Err(crate::Error::RootMismatch(_))));
626 }
627
628 #[test]
629 fn test_authorize_no_access_at_path() {
630 let claims = authorize_claims("", &["demo"], &[]);
632 assert!(matches!(claims.authorize("other"), Err(crate::Error::NoAccess(_))));
633 }
634
635 #[test]
636 fn test_deserialize_string_as_vec() {
637 let json = r#"{
638 "root": "test",
639 "put": "single-publish",
640 "get": "single-subscribe"
641 }"#;
642
643 let claims: Claims = serde_json::from_str(json).unwrap();
644 assert_eq!(claims.publish, vec!["single-publish"]);
645 assert_eq!(claims.subscribe, vec!["single-subscribe"]);
646 }
647
648 #[test]
649 fn test_deserialize_vec_as_vec() {
650 let json = r#"{
651 "root": "test",
652 "put": ["pub1", "pub2"],
653 "get": ["sub1", "sub2"]
654 }"#;
655
656 let claims: Claims = serde_json::from_str(json).unwrap();
657 assert_eq!(claims.publish, vec!["pub1", "pub2"]);
658 assert_eq!(claims.subscribe, vec!["sub1", "sub2"]);
659 }
660
661 #[test]
662 fn test_deserialize_mixed() {
663 let json = r#"{
664 "root": "test",
665 "put": "single",
666 "get": ["multi1", "multi2"]
667 }"#;
668
669 let claims: Claims = serde_json::from_str(json).unwrap();
670 assert_eq!(claims.publish, vec!["single"]);
671 assert_eq!(claims.subscribe, vec!["multi1", "multi2"]);
672 }
673}