1use crate::path;
2use moq_pattern::Patterns;
3use serde::{Deserialize, Serialize};
4use serde_with::{TimestampSeconds, serde_as};
5
6#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
17#[serde(default, deny_unknown_fields)]
18pub struct Scope {
19 #[serde(skip_serializing_if = "String::is_empty")]
21 pub root: String,
22
23 #[serde(skip_serializing_if = "Patterns::is_empty")]
25 pub publish: Patterns,
26
27 #[serde(skip_serializing_if = "Patterns::is_empty")]
29 pub subscribe: Patterns,
30}
31
32impl Scope {
33 pub fn validate(&self) -> crate::Result<()> {
35 if self.publish.is_empty() && self.subscribe.is_empty() {
36 return Err(crate::Error::UselessScope);
37 }
38
39 Ok(())
40 }
41
42 pub fn allows(&self, claims: &Claims) -> bool {
53 let covers = |granted: &Patterns, requested: &Patterns| {
54 match (granted.rooted(&self.root), requested.rooted(&claims.root)) {
55 (Ok(granted), Ok(requested)) => granted.covers(&requested),
56 _ => false,
58 }
59 };
60
61 covers(&self.publish, &claims.publish) && covers(&self.subscribe, &claims.subscribe)
62 }
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq)]
72pub struct Permissions {
73 pub subscribe: Patterns,
75
76 pub publish: Patterns,
78}
79
80impl Permissions {
81 pub fn new(publish: Patterns, subscribe: Patterns) -> Self {
83 Self { publish, subscribe }
84 }
85
86 pub fn is_empty(&self) -> bool {
88 self.publish.is_empty() && self.subscribe.is_empty()
89 }
90}
91
92#[serde_with::skip_serializing_none]
109#[serde_as]
110#[derive(Debug, Serialize, Deserialize, Default, Clone)]
111#[serde(default, deny_unknown_fields)]
112#[non_exhaustive]
113pub struct Claims {
114 #[serde(skip_serializing_if = "String::is_empty")]
117 pub root: String,
118
119 #[serde(skip_serializing_if = "Patterns::is_empty")]
122 pub publish: Patterns,
123
124 #[serde(skip_serializing_if = "Patterns::is_empty")]
127 pub subscribe: Patterns,
128
129 #[serde(rename = "exp")]
131 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
132 pub expires: Option<std::time::SystemTime>,
133
134 #[serde(rename = "iat")]
136 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
137 pub issued: Option<std::time::SystemTime>,
138}
139
140impl Claims {
141 pub fn with_root(mut self, root: impl Into<String>) -> Self {
143 self.root = root.into();
144 self
145 }
146
147 pub fn with_publish(mut self, patterns: impl IntoIterator<Item = moq_pattern::Pattern>) -> Self {
149 self.publish = patterns.into_iter().collect();
150 self
151 }
152
153 pub fn with_subscribe(mut self, patterns: impl IntoIterator<Item = moq_pattern::Pattern>) -> Self {
155 self.subscribe = patterns.into_iter().collect();
156 self
157 }
158
159 pub fn with_expires(mut self, at: impl Into<Option<std::time::SystemTime>>) -> Self {
163 self.expires = at.into();
164 self
165 }
166
167 pub fn with_issued(mut self, at: impl Into<Option<std::time::SystemTime>>) -> Self {
171 self.issued = at.into();
172 self
173 }
174
175 pub fn validate(&self) -> crate::Result<()> {
177 if self.publish.is_empty() && self.subscribe.is_empty() {
178 return Err(crate::Error::UselessToken);
179 }
180
181 Ok(())
182 }
183
184 pub fn authorize(&self, path: &str) -> crate::Result<Permissions> {
204 let path = path::normalize(path);
205 let root = path::normalize(&self.root);
206
207 let (suffix, prefix) = if let Some(suffix) = path::strip_prefix(&path, &root) {
210 (suffix, "")
211 } else if let Some(prefix) = path::strip_prefix(&root, &path) {
212 ("", prefix)
213 } else {
214 return Err(crate::Error::RootMismatch(path));
215 };
216
217 let scope = |patterns: &Patterns| -> crate::Result<Patterns> {
218 if prefix.is_empty() {
219 Ok(patterns.rebase(suffix))
221 } else {
222 Ok(patterns.rooted(prefix)?)
224 }
225 };
226
227 let permissions = Permissions {
228 subscribe: scope(&self.subscribe)?,
229 publish: scope(&self.publish)?,
230 };
231
232 if permissions.subscribe.is_empty() && permissions.publish.is_empty() {
233 return Err(crate::Error::NoAccess(path));
234 }
235
236 Ok(permissions)
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 use std::time::{Duration, SystemTime};
245
246 fn patterns(texts: &[&str]) -> Patterns {
247 texts.iter().map(|text| text.parse().unwrap()).collect()
248 }
249
250 fn create_test_claims() -> Claims {
251 Claims {
252 root: "test-path".to_string(),
253 publish: patterns(&["test-pub/**"]),
254 subscribe: patterns(&["test-sub/**"]),
255 expires: Some(SystemTime::now() + Duration::from_secs(3600)),
256 issued: Some(SystemTime::now()),
257 }
258 }
259
260 #[test]
261 fn scope_allows_contained_claims() {
262 let scope = Scope {
263 root: "project".into(),
264 publish: patterns(&["live/**"]),
265 subscribe: patterns(&["watch/**"]),
266 };
267 let claims = Claims {
268 root: "project/live/room".into(),
269 publish: patterns(&["**"]),
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: patterns(&["live/**"]),
280 subscribe: Patterns::new(),
281 };
282 let sibling = Claims {
283 root: "project/lively".into(),
284 publish: patterns(&["**"]),
285 ..Default::default()
286 };
287 let role = Claims {
288 root: "project/live".into(),
289 subscribe: patterns(&["**"]),
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: patterns(&["live/**"]),
302 subscribe: Patterns::new(),
303 };
304
305 for claims in [
306 Claims {
307 root: "project".into(),
308 publish: patterns(&["live/room/**"]),
309 ..Default::default()
310 },
311 Claims {
312 root: String::new(),
313 publish: patterns(&["project/live/room/**"]),
314 ..Default::default()
315 },
316 Claims {
317 root: "/project/live/".into(),
318 publish: patterns(&["room/**"]),
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: patterns(&["live/**"]),
331 subscribe: Patterns::new(),
332 };
333
334 let claims = Claims {
337 root: String::new(),
338 publish: patterns(&["**"]),
339 ..Default::default()
340 };
341 assert!(!scope.allows(&claims));
342 }
343
344 #[test]
345 fn scope_globstar_grants_everything_beneath_it() {
346 let scope = Scope {
347 root: "project".into(),
348 publish: patterns(&["**"]),
349 subscribe: Patterns::new(),
350 };
351 let claims = Claims {
352 root: "project/anything/deep".into(),
353 publish: patterns(&["**"]),
354 ..Default::default()
355 };
356 assert!(scope.allows(&claims));
357 }
358
359 #[test]
360 fn scope_requires_every_requested_pattern() {
361 let scope = Scope {
363 root: "project".into(),
364 publish: patterns(&["live/**"]),
365 subscribe: Patterns::new(),
366 };
367 let claims = Claims {
368 root: "project".into(),
369 publish: patterns(&["live/room/**", "other/**"]),
370 ..Default::default()
371 };
372 assert!(!scope.allows(&claims));
373 }
374
375 #[test]
376 fn scope_is_exact_about_a_literal() {
377 let scope = Scope {
379 root: "project".into(),
380 publish: patterns(&["live"]),
381 subscribe: Patterns::new(),
382 };
383 let exact = Claims {
384 root: "project".into(),
385 publish: patterns(&["live"]),
386 ..Default::default()
387 };
388 let subtree = Claims {
389 root: "project".into(),
390 publish: patterns(&["live/**"]),
391 ..Default::default()
392 };
393 assert!(scope.allows(&exact));
394 assert!(!scope.allows(&subtree));
395 }
396
397 #[test]
398 fn scope_without_grants_is_useless() {
399 assert!(matches!(Scope::default().validate(), Err(crate::Error::UselessScope)));
400 }
401
402 #[test]
403 fn scope_refuses_the_old_prefix_fields() {
404 let err = serde_json::from_str::<Scope>(r#"{"root":"demo","put":["room"]}"#).unwrap_err();
405 assert!(err.to_string().contains("unknown field `put`"), "{err}");
406 }
407
408 #[test]
409 fn test_claims_validation_success() {
410 let claims = create_test_claims();
411 assert!(claims.validate().is_ok());
412 }
413
414 #[test]
415 fn test_claims_validation_no_publish_or_subscribe() {
416 let claims = Claims {
417 root: "test-path".to_string(),
418 ..Default::default()
419 };
420
421 let result = claims.validate();
422 assert!(result.is_err());
423 assert!(
424 result
425 .unwrap_err()
426 .to_string()
427 .contains("no publish or subscribe allowed; token is useless")
428 );
429 }
430
431 #[test]
432 fn test_claims_validation_only_publish() {
433 let claims = Claims {
434 root: "test-path".to_string(),
435 publish: patterns(&["test-pub"]),
436 ..Default::default()
437 };
438
439 assert!(claims.validate().is_ok());
440 }
441
442 #[test]
443 fn test_claims_validation_only_subscribe() {
444 let claims = Claims {
445 root: "test-path".to_string(),
446 subscribe: patterns(&["test-sub"]),
447 ..Default::default()
448 };
449
450 assert!(claims.validate().is_ok());
451 }
452
453 #[test]
454 fn test_claims_serde() {
455 let claims = create_test_claims();
456 let json = serde_json::to_string(&claims).unwrap();
457 let deserialized: Claims = serde_json::from_str(&json).unwrap();
458
459 assert_eq!(deserialized.root, claims.root);
460 assert_eq!(deserialized.publish, claims.publish);
461 assert_eq!(deserialized.subscribe, claims.subscribe);
462 }
463
464 #[test]
465 fn test_claims_serde_names() {
466 let claims = Claims {
467 root: "live".into(),
468 publish: patterns(&["camera1"]),
469 subscribe: patterns(&["camera1", "camera2"]),
470 ..Default::default()
471 };
472 assert_eq!(
473 serde_json::to_string(&claims).unwrap(),
474 r#"{"root":"live","publish":["camera1"],"subscribe":["camera1","camera2"]}"#
475 );
476 }
477
478 #[test]
479 fn test_claims_refuse_the_old_prefix_fields() {
480 for json in [
481 r#"{"root":"test","put":["pub1"]}"#,
482 r#"{"root":"test","get":"sub1"}"#,
483 r#"{"root":"test","publish":["pub1"],"get":["sub1"]}"#,
484 ] {
485 let err = serde_json::from_str::<Claims>(json).unwrap_err();
486 assert!(err.to_string().contains("unknown field"), "{json}: {err}");
487 }
488 }
489
490 #[test]
491 fn test_claims_refuse_a_bad_pattern() {
492 let err = serde_json::from_str::<Claims>(r#"{"publish":["a/**/b/**"]}"#).unwrap_err();
493 assert!(err.to_string().contains("**"), "{err}");
494 }
495
496 #[test]
497 fn test_claims_default() {
498 let claims = Claims::default();
499 assert_eq!(claims.root, "");
500 assert!(claims.publish.is_empty());
501 assert!(claims.subscribe.is_empty());
502 assert_eq!(claims.expires, None);
503 assert_eq!(claims.issued, None);
504 }
505
506 fn authorize_claims(root: &str, subscribe: &[&str], publish: &[&str]) -> Claims {
507 Claims {
508 root: root.to_string(),
509 subscribe: patterns(subscribe),
510 publish: patterns(publish),
511 ..Default::default()
512 }
513 }
514
515 #[test]
516 fn test_authorize_path_equals_root() {
517 let claims = authorize_claims("room/123", &["**"], &["alice/**"]);
518 let permissions = claims.authorize("room/123").unwrap();
519
520 assert_eq!(permissions.subscribe, patterns(&["**"]));
521 assert_eq!(permissions.publish, patterns(&["alice/**"]));
522 }
523
524 #[test]
525 fn test_authorize_path_extends_root() {
526 let claims = authorize_claims("room/123", &["bob/**"], &["alice/**"]);
528 let permissions = claims.authorize("room/123/alice").unwrap();
529
530 assert_eq!(permissions.subscribe, Patterns::new());
531 assert_eq!(permissions.publish, patterns(&["**"]));
532 }
533
534 #[test]
535 fn test_authorize_literal_becomes_the_path_itself() {
536 let claims = authorize_claims("room", &[], &["alice"]);
538 let permissions = claims.authorize("room/alice").unwrap();
539
540 assert_eq!(permissions.publish, patterns(&[""]));
541 }
542
543 #[test]
544 fn test_authorize_path_is_parent_of_root() {
545 let claims = authorize_claims("demo", &["**"], &["alice/**"]);
547 let permissions = claims.authorize("/").unwrap();
548
549 assert_eq!(permissions.subscribe, patterns(&["demo/**"]));
550 assert_eq!(permissions.publish, patterns(&["demo/alice/**"]));
551 }
552
553 #[test]
554 fn test_authorize_empty_root() {
555 let claims = authorize_claims("", &["demo/**"], &[]);
557 let permissions = claims.authorize("demo/room").unwrap();
558
559 assert_eq!(permissions.subscribe, patterns(&["**"]));
560 assert_eq!(permissions.publish, Patterns::new());
561 }
562
563 #[test]
564 fn test_authorize_slashes_are_implicit() {
565 let claims = authorize_claims("/room/123/", &["bob/**"], &[]);
566 let permissions = claims.authorize("//room/123//").unwrap();
567
568 assert_eq!(permissions.subscribe, patterns(&["bob/**"]));
569 }
570
571 #[test]
572 fn test_authorize_respects_segment_boundaries() {
573 let claims = authorize_claims("foo", &["**"], &["**"]);
575 assert!(matches!(claims.authorize("foobar"), Err(crate::Error::RootMismatch(_))));
576 }
577
578 #[test]
579 fn test_authorize_unrelated_path() {
580 let claims = authorize_claims("demo", &["**"], &["**"]);
581 assert!(matches!(claims.authorize("other"), Err(crate::Error::RootMismatch(_))));
582 }
583
584 #[test]
585 fn test_authorize_no_access_at_path() {
586 let claims = authorize_claims("", &["demo/**"], &[]);
588 assert!(matches!(claims.authorize("other"), Err(crate::Error::NoAccess(_))));
589 }
590
591 #[test]
592 fn test_authorize_wildcards_rebase_as_a_set() {
593 let claims = authorize_claims("", &["**/chat"], &[]);
595 let permissions = claims.authorize("chat").unwrap();
596 assert_eq!(permissions.subscribe.len(), 2);
597 assert_eq!(permissions.subscribe, patterns(&["", "**/chat"]));
598 }
599}