1use crate::path;
2use moq_pattern::Patterns;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
19#[serde(try_from = "crate::wire::Scope", into = "crate::wire::Scope")]
20pub struct Scope {
21 pub root: String,
23
24 pub publish: Patterns,
26
27 pub subscribe: Patterns,
29}
30
31impl Scope {
32 pub fn validate(&self) -> crate::Result<()> {
34 if self.publish.is_empty() && self.subscribe.is_empty() {
35 return Err(crate::Error::UselessScope);
36 }
37
38 Ok(())
39 }
40
41 pub fn allows(&self, claims: &Claims) -> bool {
52 let covers = |granted: &Patterns, requested: &Patterns| {
53 match (granted.rooted(&self.root), requested.rooted(&claims.root)) {
54 (Ok(granted), Ok(requested)) => granted.covers(&requested),
55 _ => false,
57 }
58 };
59
60 covers(&self.publish, &claims.publish) && covers(&self.subscribe, &claims.subscribe)
61 }
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Eq)]
71pub struct Permissions {
72 pub subscribe: Patterns,
74
75 pub publish: Patterns,
77}
78
79impl Permissions {
80 pub fn new(publish: Patterns, subscribe: Patterns) -> Self {
82 Self { publish, subscribe }
83 }
84
85 pub fn is_empty(&self) -> bool {
87 self.publish.is_empty() && self.subscribe.is_empty()
88 }
89}
90
91#[derive(Debug, Serialize, Deserialize, Default, Clone)]
110#[serde(try_from = "crate::wire::Claims", into = "crate::wire::Claims")]
111#[non_exhaustive]
112pub struct Claims {
113 pub root: String,
116
117 pub publish: Patterns,
120
121 pub subscribe: Patterns,
124
125 pub expires: Option<std::time::SystemTime>,
127
128 pub issued: Option<std::time::SystemTime>,
130}
131
132impl Claims {
133 pub fn with_root(mut self, root: impl Into<String>) -> Self {
135 self.root = root.into();
136 self
137 }
138
139 pub fn with_publish(mut self, patterns: impl IntoIterator<Item = moq_pattern::Pattern>) -> Self {
141 self.publish = patterns.into_iter().collect();
142 self
143 }
144
145 pub fn with_subscribe(mut self, patterns: impl IntoIterator<Item = moq_pattern::Pattern>) -> Self {
147 self.subscribe = patterns.into_iter().collect();
148 self
149 }
150
151 pub fn with_expires(mut self, at: impl Into<Option<std::time::SystemTime>>) -> Self {
155 self.expires = at.into();
156 self
157 }
158
159 pub fn with_issued(mut self, at: impl Into<Option<std::time::SystemTime>>) -> Self {
163 self.issued = at.into();
164 self
165 }
166
167 pub fn validate(&self) -> crate::Result<()> {
169 if self.publish.is_empty() && self.subscribe.is_empty() {
170 return Err(crate::Error::UselessToken);
171 }
172
173 Ok(())
174 }
175
176 pub fn authorize(&self, path: &str) -> crate::Result<Permissions> {
196 let path = path::normalize(path);
197 let root = path::normalize(&self.root);
198
199 let (suffix, prefix) = if let Some(suffix) = path::strip_prefix(&path, &root) {
202 (suffix, "")
203 } else if let Some(prefix) = path::strip_prefix(&root, &path) {
204 ("", prefix)
205 } else {
206 return Err(crate::Error::RootMismatch(path));
207 };
208
209 let scope = |patterns: &Patterns| -> crate::Result<Patterns> {
210 if prefix.is_empty() {
211 Ok(patterns.rebase(suffix))
213 } else {
214 Ok(patterns.rooted(prefix)?)
216 }
217 };
218
219 let permissions = Permissions {
220 subscribe: scope(&self.subscribe)?,
221 publish: scope(&self.publish)?,
222 };
223
224 if permissions.subscribe.is_empty() && permissions.publish.is_empty() {
225 return Err(crate::Error::NoAccess(path));
226 }
227
228 Ok(permissions)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 use std::time::{Duration, SystemTime};
237
238 fn patterns(texts: &[&str]) -> Patterns {
239 texts.iter().map(|text| text.parse().unwrap()).collect()
240 }
241
242 fn create_test_claims() -> Claims {
243 Claims {
244 root: "test-path".to_string(),
245 publish: patterns(&["test-pub/**"]),
246 subscribe: patterns(&["test-sub/**"]),
247 expires: Some(SystemTime::now() + Duration::from_secs(3600)),
248 issued: Some(SystemTime::now()),
249 }
250 }
251
252 #[test]
253 fn scope_allows_contained_claims() {
254 let scope = Scope {
255 root: "project".into(),
256 publish: patterns(&["live/**"]),
257 subscribe: patterns(&["watch/**"]),
258 };
259 let claims = Claims {
260 root: "project/live/room".into(),
261 publish: patterns(&["**"]),
262 ..Default::default()
263 };
264 assert!(scope.allows(&claims));
265 }
266
267 #[test]
268 fn scope_rejects_sibling_and_role_escalation() {
269 let scope = Scope {
270 root: "project".into(),
271 publish: patterns(&["live/**"]),
272 subscribe: Patterns::new(),
273 };
274 let sibling = Claims {
275 root: "project/lively".into(),
276 publish: patterns(&["**"]),
277 ..Default::default()
278 };
279 let role = Claims {
280 root: "project/live".into(),
281 subscribe: patterns(&["**"]),
282 ..Default::default()
283 };
284 assert!(!scope.allows(&sibling));
285 assert!(!scope.allows(&role));
286 }
287
288 #[test]
289 fn scope_ignores_how_the_root_is_split() {
290 let scope = Scope {
292 root: "project".into(),
293 publish: patterns(&["live/**"]),
294 subscribe: Patterns::new(),
295 };
296
297 for claims in [
298 Claims {
299 root: "project".into(),
300 publish: patterns(&["live/room/**"]),
301 ..Default::default()
302 },
303 Claims {
304 root: String::new(),
305 publish: patterns(&["project/live/room/**"]),
306 ..Default::default()
307 },
308 Claims {
309 root: "/project/live/".into(),
310 publish: patterns(&["room/**"]),
311 ..Default::default()
312 },
313 ] {
314 assert!(scope.allows(&claims), "{claims:?}");
315 }
316 }
317
318 #[test]
319 fn scope_rejects_escaping_above_its_root() {
320 let scope = Scope {
321 root: "project".into(),
322 publish: patterns(&["live/**"]),
323 subscribe: Patterns::new(),
324 };
325
326 let claims = Claims {
329 root: String::new(),
330 publish: patterns(&["**"]),
331 ..Default::default()
332 };
333 assert!(!scope.allows(&claims));
334 }
335
336 #[test]
337 fn scope_globstar_grants_everything_beneath_it() {
338 let scope = Scope {
339 root: "project".into(),
340 publish: patterns(&["**"]),
341 subscribe: Patterns::new(),
342 };
343 let claims = Claims {
344 root: "project/anything/deep".into(),
345 publish: patterns(&["**"]),
346 ..Default::default()
347 };
348 assert!(scope.allows(&claims));
349 }
350
351 #[test]
352 fn scope_requires_every_requested_pattern() {
353 let scope = Scope {
355 root: "project".into(),
356 publish: patterns(&["live/**"]),
357 subscribe: Patterns::new(),
358 };
359 let claims = Claims {
360 root: "project".into(),
361 publish: patterns(&["live/room/**", "other/**"]),
362 ..Default::default()
363 };
364 assert!(!scope.allows(&claims));
365 }
366
367 #[test]
368 fn scope_is_exact_about_a_literal() {
369 let scope = Scope {
371 root: "project".into(),
372 publish: patterns(&["live"]),
373 subscribe: Patterns::new(),
374 };
375 let exact = Claims {
376 root: "project".into(),
377 publish: patterns(&["live"]),
378 ..Default::default()
379 };
380 let subtree = Claims {
381 root: "project".into(),
382 publish: patterns(&["live/**"]),
383 ..Default::default()
384 };
385 assert!(scope.allows(&exact));
386 assert!(!scope.allows(&subtree));
387 }
388
389 #[test]
390 fn scope_without_grants_is_useless() {
391 assert!(matches!(Scope::default().validate(), Err(crate::Error::UselessScope)));
392 }
393
394 #[test]
395 fn scope_refuses_null_grants() {
396 assert!(serde_json::from_str::<Scope>(r#"{"put":null,"publish":["room"]}"#).is_err());
397 }
398
399 #[test]
400 fn scope_reads_legacy_prefixes_as_subtrees() {
401 let scope: Scope = serde_json::from_str(r#"{"root":"demo","put":["room"],"get":[""]}"#).unwrap();
402 assert_eq!(scope.publish, patterns(&["room/**"]));
403 assert_eq!(scope.subscribe, patterns(&["**"]));
404 }
405
406 #[test]
407 fn scope_writes_legacy_prefixes_only_when_faithful() {
408 let subtrees = Scope {
409 root: "demo".into(),
410 publish: patterns(&["room/**"]),
411 subscribe: patterns(&["**"]),
412 };
413 assert_eq!(
414 serde_json::to_string(&subtrees).unwrap(),
415 r#"{"root":"demo","put":["room"],"get":[""]}"#
416 );
417
418 let exact = Scope {
419 root: "demo".into(),
420 publish: patterns(&["room"]),
421 subscribe: Patterns::new(),
422 };
423 assert_eq!(
424 serde_json::to_string(&exact).unwrap(),
425 r#"{"root":"demo","publish":["room"]}"#
426 );
427 }
428
429 #[test]
430 fn test_claims_validation_success() {
431 let claims = create_test_claims();
432 assert!(claims.validate().is_ok());
433 }
434
435 #[test]
436 fn test_claims_validation_no_publish_or_subscribe() {
437 let claims = Claims {
438 root: "test-path".to_string(),
439 ..Default::default()
440 };
441
442 let result = claims.validate();
443 assert!(result.is_err());
444 assert!(
445 result
446 .unwrap_err()
447 .to_string()
448 .contains("no publish or subscribe allowed; token is useless")
449 );
450 }
451
452 #[test]
453 fn test_claims_validation_only_publish() {
454 let claims = Claims {
455 root: "test-path".to_string(),
456 publish: patterns(&["test-pub"]),
457 ..Default::default()
458 };
459
460 assert!(claims.validate().is_ok());
461 }
462
463 #[test]
464 fn test_claims_validation_only_subscribe() {
465 let claims = Claims {
466 root: "test-path".to_string(),
467 subscribe: patterns(&["test-sub"]),
468 ..Default::default()
469 };
470
471 assert!(claims.validate().is_ok());
472 }
473
474 #[test]
475 fn test_claims_serde() {
476 let claims = create_test_claims();
477 let json = serde_json::to_string(&claims).unwrap();
478 let deserialized: Claims = serde_json::from_str(&json).unwrap();
479
480 assert_eq!(deserialized.root, claims.root);
481 assert_eq!(deserialized.publish, claims.publish);
482 assert_eq!(deserialized.subscribe, claims.subscribe);
483 }
484
485 #[test]
486 fn test_claims_serde_names() {
487 let claims = Claims {
488 root: "live".into(),
489 publish: patterns(&["camera1"]),
490 subscribe: patterns(&["camera1", "camera2"]),
491 ..Default::default()
492 };
493 assert_eq!(
494 serde_json::to_string(&claims).unwrap(),
495 r#"{"root":"live","publish":["camera1"],"subscribe":["camera1","camera2"]}"#
496 );
497 }
498
499 #[test]
500 fn test_claims_read_legacy_prefixes_as_subtrees() {
501 let claims: Claims =
502 serde_json::from_str(r#"{"root":"test","put":["pub1","/a//b/"],"get":"","exp":1700000000}"#).unwrap();
503 assert_eq!(claims.publish, patterns(&["pub1/**", "a/b/**"]));
504 assert_eq!(claims.subscribe, patterns(&["**"]));
505 assert!(claims.expires.is_some());
506 }
507
508 #[test]
509 fn test_claims_write_legacy_prefixes_only_when_faithful() {
510 let subtrees = Claims {
512 root: "live".into(),
513 publish: patterns(&["camera1/**"]),
514 subscribe: patterns(&["**"]),
515 ..Default::default()
516 };
517 let json = serde_json::to_string(&subtrees).unwrap();
518 assert_eq!(json, r#"{"root":"live","put":["camera1"],"get":[""]}"#);
519 let back: Claims = serde_json::from_str(&json).unwrap();
520 assert_eq!(back.publish, subtrees.publish);
521 assert_eq!(back.subscribe, subtrees.subscribe);
522
523 let mixed = Claims {
525 root: "live".into(),
526 publish: patterns(&["camera1/**"]),
527 subscribe: patterns(&["*/chat"]),
528 ..Default::default()
529 };
530 assert_eq!(
531 serde_json::to_string(&mixed).unwrap(),
532 r#"{"root":"live","publish":["camera1/**"],"subscribe":["*/chat"]}"#
533 );
534 }
535
536 #[test]
537 fn test_claims_refuse_mixed_or_unknown_fields() {
538 for json in [
539 r#"{"root":"test","publish":["pub1"],"get":["sub1"]}"#,
540 r#"{"root":"test","put":[],"subscribe":["sub1"]}"#,
541 r#"{"root":"test","put":["pub1"],"cluster":true}"#,
542 r#"{"root":"test","put":null,"publish":["pub1"]}"#,
543 r#"{"root":"test","publish":null,"subscribe":["sub1"]}"#,
544 ] {
545 assert!(serde_json::from_str::<Claims>(json).is_err(), "{json}");
546 }
547 }
548
549 #[test]
550 fn test_claims_refuse_a_wildcard_in_a_legacy_prefix() {
551 assert!(serde_json::from_str::<Claims>(r#"{"put":["a/*"]}"#).is_err());
553 }
554
555 #[test]
556 fn test_claims_refuse_a_bad_pattern() {
557 let err = serde_json::from_str::<Claims>(r#"{"publish":["a/**/b/**"]}"#).unwrap_err();
558 assert!(err.to_string().contains("**"), "{err}");
559 }
560
561 #[test]
562 fn test_claims_default() {
563 let claims = Claims::default();
564 assert_eq!(claims.root, "");
565 assert!(claims.publish.is_empty());
566 assert!(claims.subscribe.is_empty());
567 assert_eq!(claims.expires, None);
568 assert_eq!(claims.issued, None);
569 }
570
571 fn authorize_claims(root: &str, subscribe: &[&str], publish: &[&str]) -> Claims {
572 Claims {
573 root: root.to_string(),
574 subscribe: patterns(subscribe),
575 publish: patterns(publish),
576 ..Default::default()
577 }
578 }
579
580 #[test]
581 fn test_authorize_path_equals_root() {
582 let claims = authorize_claims("room/123", &["**"], &["alice/**"]);
583 let permissions = claims.authorize("room/123").unwrap();
584
585 assert_eq!(permissions.subscribe, patterns(&["**"]));
586 assert_eq!(permissions.publish, patterns(&["alice/**"]));
587 }
588
589 #[test]
590 fn test_authorize_path_extends_root() {
591 let claims = authorize_claims("room/123", &["bob/**"], &["alice/**"]);
593 let permissions = claims.authorize("room/123/alice").unwrap();
594
595 assert_eq!(permissions.subscribe, Patterns::new());
596 assert_eq!(permissions.publish, patterns(&["**"]));
597 }
598
599 #[test]
600 fn test_authorize_literal_becomes_the_path_itself() {
601 let claims = authorize_claims("room", &[], &["alice"]);
603 let permissions = claims.authorize("room/alice").unwrap();
604
605 assert_eq!(permissions.publish, patterns(&[""]));
606 }
607
608 #[test]
609 fn test_authorize_path_is_parent_of_root() {
610 let claims = authorize_claims("demo", &["**"], &["alice/**"]);
612 let permissions = claims.authorize("/").unwrap();
613
614 assert_eq!(permissions.subscribe, patterns(&["demo/**"]));
615 assert_eq!(permissions.publish, patterns(&["demo/alice/**"]));
616 }
617
618 #[test]
619 fn test_authorize_empty_root() {
620 let claims = authorize_claims("", &["demo/**"], &[]);
622 let permissions = claims.authorize("demo/room").unwrap();
623
624 assert_eq!(permissions.subscribe, patterns(&["**"]));
625 assert_eq!(permissions.publish, Patterns::new());
626 }
627
628 #[test]
629 fn test_authorize_slashes_are_implicit() {
630 let claims = authorize_claims("/room/123/", &["bob/**"], &[]);
631 let permissions = claims.authorize("//room/123//").unwrap();
632
633 assert_eq!(permissions.subscribe, patterns(&["bob/**"]));
634 }
635
636 #[test]
637 fn test_authorize_respects_segment_boundaries() {
638 let claims = authorize_claims("foo", &["**"], &["**"]);
640 assert!(matches!(claims.authorize("foobar"), Err(crate::Error::RootMismatch(_))));
641 }
642
643 #[test]
644 fn test_authorize_unrelated_path() {
645 let claims = authorize_claims("demo", &["**"], &["**"]);
646 assert!(matches!(claims.authorize("other"), Err(crate::Error::RootMismatch(_))));
647 }
648
649 #[test]
650 fn test_authorize_no_access_at_path() {
651 let claims = authorize_claims("", &["demo/**"], &[]);
653 assert!(matches!(claims.authorize("other"), Err(crate::Error::NoAccess(_))));
654 }
655
656 #[test]
657 fn test_authorize_wildcards_rebase_as_a_set() {
658 let claims = authorize_claims("", &["**/chat"], &[]);
660 let permissions = claims.authorize("chat").unwrap();
661 assert_eq!(permissions.subscribe.len(), 2);
662 assert_eq!(permissions.subscribe, patterns(&["", "**/chat"]));
663 }
664}