1use std::collections::{BTreeMap, BTreeSet};
21use std::fmt;
22use std::str::FromStr;
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum Scope {
27 Account(AccountScope),
29 Identity(IdentityScope),
31 Blob(BlobScope),
33 Repo(RepoScope),
35 Rpc(RpcScope),
37 Atproto,
39 Transition(TransitionScope),
41 OpenId,
43 Profile,
45 Email,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
51pub struct AccountScope {
52 pub resource: AccountResource,
54 pub action: AccountAction,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub enum AccountResource {
61 Email,
63 Repo,
65 Status,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub enum AccountAction {
72 Read,
74 Manage,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
80pub enum IdentityScope {
81 Handle,
83 All,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Hash)]
89pub enum TransitionScope {
90 Generic,
92 Email,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
98pub struct BlobScope {
99 pub accept: BTreeSet<MimePattern>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
105pub enum MimePattern {
106 All,
108 TypeWildcard(String),
110 Exact(String),
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Hash)]
116pub struct RepoScope {
117 pub collection: RepoCollection,
119 pub actions: BTreeSet<RepoAction>,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub enum RepoCollection {
126 All,
128 Nsid(String),
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
134pub enum RepoAction {
135 Create,
137 Update,
139 Delete,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Hash)]
145pub struct RpcScope {
146 pub lxm: BTreeSet<RpcLexicon>,
148 pub aud: BTreeSet<RpcAudience>,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
154pub enum RpcLexicon {
155 All,
157 Nsid(String),
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
163pub enum RpcAudience {
164 All,
166 Did(String),
168}
169
170impl Scope {
171 pub fn parse_multiple(s: &str) -> Result<Vec<Self>, ParseError> {
180 if s.trim().is_empty() {
181 return Ok(Vec::new());
182 }
183
184 let mut scopes = Vec::new();
185 for scope_str in s.trim().split_whitespace() {
186 scopes.push(Self::parse(scope_str)?);
187 }
188
189 Ok(scopes)
190 }
191
192 pub fn parse_multiple_reduced(s: &str) -> Result<Vec<Self>, ParseError> {
205 let all_scopes = Self::parse_multiple(s)?;
206
207 if all_scopes.is_empty() {
208 return Ok(Vec::new());
209 }
210
211 let mut result: Vec<Self> = Vec::new();
212
213 for scope in all_scopes {
214 let mut is_granted = false;
216 for existing in &result {
217 if existing.grants(&scope) && existing != &scope {
218 is_granted = true;
219 break;
220 }
221 }
222
223 if is_granted {
224 continue; }
226
227 let mut indices_to_remove = Vec::new();
229 for (i, existing) in result.iter().enumerate() {
230 if scope.grants(existing) && &scope != existing {
231 indices_to_remove.push(i);
232 }
233 }
234
235 for i in indices_to_remove.into_iter().rev() {
237 result.remove(i);
238 }
239
240 if !result.contains(&scope) {
242 result.push(scope);
243 }
244 }
245
246 Ok(result)
247 }
248
249 pub fn serialize_multiple(scopes: &[Self]) -> String {
266 if scopes.is_empty() {
267 return String::new();
268 }
269
270 let mut serialized: Vec<String> = scopes.iter().map(|scope| scope.to_string()).collect();
271
272 serialized.sort();
273 serialized.join(" ")
274 }
275
276 pub fn remove_scope(scopes: &[Self], scope_to_remove: &Self) -> Vec<Self> {
295 scopes
296 .iter()
297 .filter(|s| *s != scope_to_remove)
298 .cloned()
299 .collect()
300 }
301
302 pub fn parse(s: &str) -> Result<Self, ParseError> {
304 let prefixes = [
306 "account",
307 "identity",
308 "blob",
309 "repo",
310 "rpc",
311 "atproto",
312 "transition",
313 "openid",
314 "profile",
315 "email",
316 ];
317 let mut found_prefix = None;
318 let mut suffix = None;
319
320 for prefix in &prefixes {
321 if s.starts_with(prefix) {
322 let remainder = &s[prefix.len()..];
323 if remainder.is_empty() || remainder.starts_with(':') || remainder.starts_with('?')
324 {
325 found_prefix = Some(*prefix);
326 if remainder.starts_with(':') {
327 suffix = Some(&remainder[1..]);
328 } else if remainder.starts_with('?') {
329 suffix = Some(remainder);
330 } else {
331 suffix = None;
332 }
333 break;
334 }
335 }
336 }
337
338 let prefix = found_prefix.ok_or_else(|| {
339 let end = s.find(':').or_else(|| s.find('?')).unwrap_or(s.len());
341 ParseError::UnknownPrefix(s[..end].to_string())
342 })?;
343
344 match prefix {
345 "account" => Self::parse_account(suffix),
346 "identity" => Self::parse_identity(suffix),
347 "blob" => Self::parse_blob(suffix),
348 "repo" => Self::parse_repo(suffix),
349 "rpc" => Self::parse_rpc(suffix),
350 "atproto" => Self::parse_atproto(suffix),
351 "transition" => Self::parse_transition(suffix),
352 "openid" => Self::parse_openid(suffix),
353 "profile" => Self::parse_profile(suffix),
354 "email" => Self::parse_email(suffix),
355 _ => Err(ParseError::UnknownPrefix(prefix.to_string())),
356 }
357 }
358
359 fn parse_account(suffix: Option<&str>) -> Result<Self, ParseError> {
360 let (resource_str, params) = match suffix {
361 Some(s) => {
362 if let Some(pos) = s.find('?') {
363 (&s[..pos], Some(&s[pos + 1..]))
364 } else {
365 (s, None)
366 }
367 }
368 None => return Err(ParseError::MissingResource),
369 };
370
371 let resource = match resource_str {
372 "email" => AccountResource::Email,
373 "repo" => AccountResource::Repo,
374 "status" => AccountResource::Status,
375 _ => return Err(ParseError::InvalidResource(resource_str.to_string())),
376 };
377
378 let action = if let Some(params) = params {
379 let parsed_params = parse_query_string(params);
380 match parsed_params
381 .get("action")
382 .and_then(|v| v.first())
383 .map(|s| s.as_str())
384 {
385 Some("read") => AccountAction::Read,
386 Some("manage") => AccountAction::Manage,
387 Some(other) => return Err(ParseError::InvalidAction(other.to_string())),
388 None => AccountAction::Read,
389 }
390 } else {
391 AccountAction::Read
392 };
393
394 Ok(Scope::Account(AccountScope { resource, action }))
395 }
396
397 fn parse_identity(suffix: Option<&str>) -> Result<Self, ParseError> {
398 let scope = match suffix {
399 Some("handle") => IdentityScope::Handle,
400 Some("*") => IdentityScope::All,
401 Some(other) => return Err(ParseError::InvalidResource(other.to_string())),
402 None => return Err(ParseError::MissingResource),
403 };
404
405 Ok(Scope::Identity(scope))
406 }
407
408 fn parse_blob(suffix: Option<&str>) -> Result<Self, ParseError> {
409 let mut accept = BTreeSet::new();
410
411 match suffix {
412 Some(s) if s.starts_with('?') => {
413 let params = parse_query_string(&s[1..]);
414 if let Some(values) = params.get("accept") {
415 for value in values {
416 accept.insert(MimePattern::from_str(value)?);
417 }
418 }
419 }
420 Some(s) => {
421 accept.insert(MimePattern::from_str(s)?);
422 }
423 None => {
424 accept.insert(MimePattern::All);
425 }
426 }
427
428 if accept.is_empty() {
429 accept.insert(MimePattern::All);
430 }
431
432 Ok(Scope::Blob(BlobScope { accept }))
433 }
434
435 fn parse_repo(suffix: Option<&str>) -> Result<Self, ParseError> {
436 let (collection_str, params) = match suffix {
437 Some(s) => {
438 if let Some(pos) = s.find('?') {
439 (Some(&s[..pos]), Some(&s[pos + 1..]))
440 } else {
441 (Some(s), None)
442 }
443 }
444 None => (None, None),
445 };
446
447 let collection = match collection_str {
448 Some("*") | None => RepoCollection::All,
449 Some(nsid) => RepoCollection::Nsid(nsid.to_string()),
450 };
451
452 let mut actions = BTreeSet::new();
453 if let Some(params) = params {
454 let parsed_params = parse_query_string(params);
455 if let Some(values) = parsed_params.get("action") {
456 for value in values {
457 match value.as_str() {
458 "create" => {
459 actions.insert(RepoAction::Create);
460 }
461 "update" => {
462 actions.insert(RepoAction::Update);
463 }
464 "delete" => {
465 actions.insert(RepoAction::Delete);
466 }
467 "*" => {
468 actions.insert(RepoAction::Create);
469 actions.insert(RepoAction::Update);
470 actions.insert(RepoAction::Delete);
471 }
472 other => return Err(ParseError::InvalidAction(other.to_string())),
473 }
474 }
475 }
476 }
477
478 if actions.is_empty() {
479 actions.insert(RepoAction::Create);
480 actions.insert(RepoAction::Update);
481 actions.insert(RepoAction::Delete);
482 }
483
484 Ok(Scope::Repo(RepoScope {
485 collection,
486 actions,
487 }))
488 }
489
490 fn parse_rpc(suffix: Option<&str>) -> Result<Self, ParseError> {
491 let mut lxm = BTreeSet::new();
492 let mut aud = BTreeSet::new();
493
494 match suffix {
495 Some("*") => {
496 lxm.insert(RpcLexicon::All);
497 aud.insert(RpcAudience::All);
498 }
499 Some(s) if s.starts_with('?') => {
500 let params = parse_query_string(&s[1..]);
501
502 if let Some(values) = params.get("lxm") {
503 for value in values {
504 if value == "*" {
505 lxm.insert(RpcLexicon::All);
506 } else {
507 lxm.insert(RpcLexicon::Nsid(value.to_string()));
508 }
509 }
510 }
511
512 if let Some(values) = params.get("aud") {
513 for value in values {
514 if value == "*" {
515 aud.insert(RpcAudience::All);
516 } else {
517 aud.insert(RpcAudience::Did(value.to_string()));
518 }
519 }
520 }
521 }
522 Some(s) => {
523 if let Some(pos) = s.find('?') {
525 let nsid = &s[..pos];
526 let params = parse_query_string(&s[pos + 1..]);
527
528 lxm.insert(RpcLexicon::Nsid(nsid.to_string()));
529
530 if let Some(values) = params.get("aud") {
531 for value in values {
532 if value == "*" {
533 aud.insert(RpcAudience::All);
534 } else {
535 aud.insert(RpcAudience::Did(value.to_string()));
536 }
537 }
538 }
539 } else {
540 lxm.insert(RpcLexicon::Nsid(s.to_string()));
541 }
542 }
543 None => {}
544 }
545
546 if lxm.is_empty() {
547 lxm.insert(RpcLexicon::All);
548 }
549 if aud.is_empty() {
550 aud.insert(RpcAudience::All);
551 }
552
553 Ok(Scope::Rpc(RpcScope { lxm, aud }))
554 }
555
556 fn parse_atproto(suffix: Option<&str>) -> Result<Self, ParseError> {
557 if suffix.is_some() {
558 return Err(ParseError::InvalidResource(
559 "atproto scope does not accept suffixes".to_string(),
560 ));
561 }
562 Ok(Scope::Atproto)
563 }
564
565 fn parse_transition(suffix: Option<&str>) -> Result<Self, ParseError> {
566 let scope = match suffix {
567 Some("generic") => TransitionScope::Generic,
568 Some("email") => TransitionScope::Email,
569 Some(other) => return Err(ParseError::InvalidResource(other.to_string())),
570 None => return Err(ParseError::MissingResource),
571 };
572
573 Ok(Scope::Transition(scope))
574 }
575
576 fn parse_openid(suffix: Option<&str>) -> Result<Self, ParseError> {
577 if suffix.is_some() {
578 return Err(ParseError::InvalidResource(
579 "openid scope does not accept suffixes".to_string(),
580 ));
581 }
582 Ok(Scope::OpenId)
583 }
584
585 fn parse_profile(suffix: Option<&str>) -> Result<Self, ParseError> {
586 if suffix.is_some() {
587 return Err(ParseError::InvalidResource(
588 "profile scope does not accept suffixes".to_string(),
589 ));
590 }
591 Ok(Scope::Profile)
592 }
593
594 fn parse_email(suffix: Option<&str>) -> Result<Self, ParseError> {
595 if suffix.is_some() {
596 return Err(ParseError::InvalidResource(
597 "email scope does not accept suffixes".to_string(),
598 ));
599 }
600 Ok(Scope::Email)
601 }
602
603 pub fn to_string_normalized(&self) -> String {
605 match self {
606 Scope::Account(scope) => {
607 let resource = match scope.resource {
608 AccountResource::Email => "email",
609 AccountResource::Repo => "repo",
610 AccountResource::Status => "status",
611 };
612
613 match scope.action {
614 AccountAction::Read => format!("account:{}", resource),
615 AccountAction::Manage => format!("account:{}?action=manage", resource),
616 }
617 }
618 Scope::Identity(scope) => match scope {
619 IdentityScope::Handle => "identity:handle".to_string(),
620 IdentityScope::All => "identity:*".to_string(),
621 },
622 Scope::Blob(scope) => {
623 if scope.accept.len() == 1 {
624 if let Some(pattern) = scope.accept.iter().next() {
625 match pattern {
626 MimePattern::All => "blob:*/*".to_string(),
627 MimePattern::TypeWildcard(t) => format!("blob:{}/*", t),
628 MimePattern::Exact(mime) => format!("blob:{}", mime),
629 }
630 } else {
631 "blob:*/*".to_string()
632 }
633 } else {
634 let mut params = Vec::new();
635 for pattern in &scope.accept {
636 match pattern {
637 MimePattern::All => params.push("accept=*/*".to_string()),
638 MimePattern::TypeWildcard(t) => params.push(format!("accept={}/*", t)),
639 MimePattern::Exact(mime) => params.push(format!("accept={}", mime)),
640 }
641 }
642 params.sort();
643 format!("blob?{}", params.join("&"))
644 }
645 }
646 Scope::Repo(scope) => {
647 let collection = match &scope.collection {
648 RepoCollection::All => "*",
649 RepoCollection::Nsid(nsid) => nsid,
650 };
651
652 if scope.actions.len() == 3 {
653 format!("repo:{}", collection)
654 } else {
655 let mut params = Vec::new();
656 for action in &scope.actions {
657 match action {
658 RepoAction::Create => params.push("action=create"),
659 RepoAction::Update => params.push("action=update"),
660 RepoAction::Delete => params.push("action=delete"),
661 }
662 }
663 format!("repo:{}?{}", collection, params.join("&"))
664 }
665 }
666 Scope::Rpc(scope) => {
667 if scope.lxm.len() == 1
668 && scope.lxm.contains(&RpcLexicon::All)
669 && scope.aud.len() == 1
670 && scope.aud.contains(&RpcAudience::All)
671 {
672 "rpc:*".to_string()
673 } else if scope.lxm.len() == 1
674 && scope.aud.len() == 1
675 && scope.aud.contains(&RpcAudience::All)
676 {
677 if let Some(lxm) = scope.lxm.iter().next() {
678 match lxm {
679 RpcLexicon::All => "rpc:*".to_string(),
680 RpcLexicon::Nsid(nsid) => format!("rpc:{}", nsid),
681 }
682 } else {
683 "rpc:*".to_string()
684 }
685 } else {
686 let mut params = Vec::new();
687
688 for lxm in &scope.lxm {
689 match lxm {
690 RpcLexicon::All => params.push("lxm=*".to_string()),
691 RpcLexicon::Nsid(nsid) => params.push(format!("lxm={}", nsid)),
692 }
693 }
694
695 for aud in &scope.aud {
696 match aud {
697 RpcAudience::All => params.push("aud=*".to_string()),
698 RpcAudience::Did(did) => params.push(format!("aud={}", did)),
699 }
700 }
701
702 params.sort();
703
704 if params.is_empty() {
705 "rpc:*".to_string()
706 } else {
707 format!("rpc?{}", params.join("&"))
708 }
709 }
710 }
711 Scope::Atproto => "atproto".to_string(),
712 Scope::Transition(scope) => match scope {
713 TransitionScope::Generic => "transition:generic".to_string(),
714 TransitionScope::Email => "transition:email".to_string(),
715 },
716 Scope::OpenId => "openid".to_string(),
717 Scope::Profile => "profile".to_string(),
718 Scope::Email => "email".to_string(),
719 }
720 }
721
722 pub fn grants(&self, other: &Scope) -> bool {
724 match (self, other) {
725 (Scope::Atproto, Scope::Atproto) => true,
727 (Scope::Atproto, _) => false,
728 (_, Scope::Atproto) => false,
730 (Scope::Transition(a), Scope::Transition(b)) => a == b,
732 (_, Scope::Transition(_)) => false,
734 (Scope::Transition(_), _) => false,
735 (Scope::OpenId, Scope::OpenId) => true,
737 (Scope::OpenId, _) => false,
738 (_, Scope::OpenId) => false,
739 (Scope::Profile, Scope::Profile) => true,
740 (Scope::Profile, _) => false,
741 (_, Scope::Profile) => false,
742 (Scope::Email, Scope::Email) => true,
743 (Scope::Email, _) => false,
744 (_, Scope::Email) => false,
745 (Scope::Account(a), Scope::Account(b)) => {
746 a.resource == b.resource
747 && match (a.action, b.action) {
748 (AccountAction::Manage, _) => true,
749 (AccountAction::Read, AccountAction::Read) => true,
750 _ => false,
751 }
752 }
753 (Scope::Identity(a), Scope::Identity(b)) => match (a, b) {
754 (IdentityScope::All, _) => true,
755 (IdentityScope::Handle, IdentityScope::Handle) => true,
756 _ => false,
757 },
758 (Scope::Blob(a), Scope::Blob(b)) => {
759 for b_pattern in &b.accept {
760 let mut granted = false;
761 for a_pattern in &a.accept {
762 if a_pattern.grants(b_pattern) {
763 granted = true;
764 break;
765 }
766 }
767 if !granted {
768 return false;
769 }
770 }
771 true
772 }
773 (Scope::Repo(a), Scope::Repo(b)) => {
774 let collection_match = match (&a.collection, &b.collection) {
775 (RepoCollection::All, _) => true,
776 (RepoCollection::Nsid(a_nsid), RepoCollection::Nsid(b_nsid)) => {
777 a_nsid == b_nsid
778 }
779 _ => false,
780 };
781
782 if !collection_match {
783 return false;
784 }
785
786 b.actions.is_subset(&a.actions) || a.actions.len() == 3
787 }
788 (Scope::Rpc(a), Scope::Rpc(b)) => {
789 let lxm_match = if a.lxm.contains(&RpcLexicon::All) {
790 true
791 } else {
792 b.lxm.iter().all(|b_lxm| match b_lxm {
793 RpcLexicon::All => false,
794 RpcLexicon::Nsid(_) => a.lxm.contains(b_lxm),
795 })
796 };
797
798 let aud_match = if a.aud.contains(&RpcAudience::All) {
799 true
800 } else {
801 b.aud.iter().all(|b_aud| match b_aud {
802 RpcAudience::All => false,
803 RpcAudience::Did(_) => a.aud.contains(b_aud),
804 })
805 };
806
807 lxm_match && aud_match
808 }
809 _ => false,
810 }
811 }
812}
813
814impl MimePattern {
815 fn grants(&self, other: &MimePattern) -> bool {
816 match (self, other) {
817 (MimePattern::All, _) => true,
818 (MimePattern::TypeWildcard(a_type), MimePattern::TypeWildcard(b_type)) => {
819 a_type == b_type
820 }
821 (MimePattern::TypeWildcard(a_type), MimePattern::Exact(b_mime)) => {
822 b_mime.starts_with(&format!("{}/", a_type))
823 }
824 (MimePattern::Exact(a), MimePattern::Exact(b)) => a == b,
825 _ => false,
826 }
827 }
828}
829
830impl FromStr for MimePattern {
831 type Err = ParseError;
832
833 fn from_str(s: &str) -> Result<Self, Self::Err> {
834 if s == "*/*" {
835 Ok(MimePattern::All)
836 } else if s.ends_with("/*") {
837 Ok(MimePattern::TypeWildcard(s[..s.len() - 2].to_string()))
838 } else if s.contains('/') {
839 Ok(MimePattern::Exact(s.to_string()))
840 } else {
841 Err(ParseError::InvalidMimeType(s.to_string()))
842 }
843 }
844}
845
846impl FromStr for Scope {
847 type Err = ParseError;
848
849 fn from_str(s: &str) -> Result<Self, Self::Err> {
850 Self::parse(s)
851 }
852}
853
854impl fmt::Display for Scope {
855 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
856 write!(f, "{}", self.to_string_normalized())
857 }
858}
859
860fn parse_query_string(query: &str) -> BTreeMap<String, Vec<String>> {
862 let mut params = BTreeMap::new();
863
864 for pair in query.split('&') {
865 if let Some(pos) = pair.find('=') {
866 let key = &pair[..pos];
867 let value = &pair[pos + 1..];
868 params
869 .entry(key.to_string())
870 .or_insert_with(Vec::new)
871 .push(value.to_string());
872 }
873 }
874
875 params
876}
877
878#[derive(Debug, Clone, PartialEq, Eq)]
880pub enum ParseError {
881 UnknownPrefix(String),
883 MissingResource,
885 InvalidResource(String),
887 InvalidAction(String),
889 InvalidMimeType(String),
891}
892
893impl fmt::Display for ParseError {
894 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
895 match self {
896 ParseError::UnknownPrefix(prefix) => write!(f, "Unknown scope prefix: {}", prefix),
897 ParseError::MissingResource => write!(f, "Missing required resource"),
898 ParseError::InvalidResource(resource) => write!(f, "Invalid resource: {}", resource),
899 ParseError::InvalidAction(action) => write!(f, "Invalid action: {}", action),
900 ParseError::InvalidMimeType(mime) => write!(f, "Invalid MIME type: {}", mime),
901 }
902 }
903}
904
905impl std::error::Error for ParseError {}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910
911 #[test]
912 fn test_account_scope_parsing() {
913 let scope = Scope::parse("account:email").unwrap();
914 assert_eq!(
915 scope,
916 Scope::Account(AccountScope {
917 resource: AccountResource::Email,
918 action: AccountAction::Read,
919 })
920 );
921
922 let scope = Scope::parse("account:repo?action=manage").unwrap();
923 assert_eq!(
924 scope,
925 Scope::Account(AccountScope {
926 resource: AccountResource::Repo,
927 action: AccountAction::Manage,
928 })
929 );
930
931 let scope = Scope::parse("account:status?action=read").unwrap();
932 assert_eq!(
933 scope,
934 Scope::Account(AccountScope {
935 resource: AccountResource::Status,
936 action: AccountAction::Read,
937 })
938 );
939 }
940
941 #[test]
942 fn test_identity_scope_parsing() {
943 let scope = Scope::parse("identity:handle").unwrap();
944 assert_eq!(scope, Scope::Identity(IdentityScope::Handle));
945
946 let scope = Scope::parse("identity:*").unwrap();
947 assert_eq!(scope, Scope::Identity(IdentityScope::All));
948 }
949
950 #[test]
951 fn test_blob_scope_parsing() {
952 let scope = Scope::parse("blob:*/*").unwrap();
953 let mut accept = BTreeSet::new();
954 accept.insert(MimePattern::All);
955 assert_eq!(scope, Scope::Blob(BlobScope { accept }));
956
957 let scope = Scope::parse("blob:image/png").unwrap();
958 let mut accept = BTreeSet::new();
959 accept.insert(MimePattern::Exact("image/png".to_string()));
960 assert_eq!(scope, Scope::Blob(BlobScope { accept }));
961
962 let scope = Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap();
963 let mut accept = BTreeSet::new();
964 accept.insert(MimePattern::Exact("image/png".to_string()));
965 accept.insert(MimePattern::Exact("image/jpeg".to_string()));
966 assert_eq!(scope, Scope::Blob(BlobScope { accept }));
967
968 let scope = Scope::parse("blob:image/*").unwrap();
969 let mut accept = BTreeSet::new();
970 accept.insert(MimePattern::TypeWildcard("image".to_string()));
971 assert_eq!(scope, Scope::Blob(BlobScope { accept }));
972 }
973
974 #[test]
975 fn test_repo_scope_parsing() {
976 let scope = Scope::parse("repo:*?action=create").unwrap();
977 let mut actions = BTreeSet::new();
978 actions.insert(RepoAction::Create);
979 assert_eq!(
980 scope,
981 Scope::Repo(RepoScope {
982 collection: RepoCollection::All,
983 actions,
984 })
985 );
986
987 let scope = Scope::parse("repo:foo.bar?action=create&action=update").unwrap();
988 let mut actions = BTreeSet::new();
989 actions.insert(RepoAction::Create);
990 actions.insert(RepoAction::Update);
991 assert_eq!(
992 scope,
993 Scope::Repo(RepoScope {
994 collection: RepoCollection::Nsid("foo.bar".to_string()),
995 actions,
996 })
997 );
998
999 let scope = Scope::parse("repo:foo.bar").unwrap();
1000 let mut actions = BTreeSet::new();
1001 actions.insert(RepoAction::Create);
1002 actions.insert(RepoAction::Update);
1003 actions.insert(RepoAction::Delete);
1004 assert_eq!(
1005 scope,
1006 Scope::Repo(RepoScope {
1007 collection: RepoCollection::Nsid("foo.bar".to_string()),
1008 actions,
1009 })
1010 );
1011 }
1012
1013 #[test]
1014 fn test_rpc_scope_parsing() {
1015 let scope = Scope::parse("rpc:*").unwrap();
1016 let mut lxm = BTreeSet::new();
1017 let mut aud = BTreeSet::new();
1018 lxm.insert(RpcLexicon::All);
1019 aud.insert(RpcAudience::All);
1020 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1021
1022 let scope = Scope::parse("rpc:com.example.service").unwrap();
1023 let mut lxm = BTreeSet::new();
1024 let mut aud = BTreeSet::new();
1025 lxm.insert(RpcLexicon::Nsid("com.example.service".to_string()));
1026 aud.insert(RpcAudience::All);
1027 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1028
1029 let scope = Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap();
1030 let mut lxm = BTreeSet::new();
1031 let mut aud = BTreeSet::new();
1032 lxm.insert(RpcLexicon::Nsid("com.example.service".to_string()));
1033 aud.insert(RpcAudience::Did("did:example:123".to_string()));
1034 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1035
1036 let scope =
1037 Scope::parse("rpc?lxm=com.example.method1&lxm=com.example.method2&aud=did:example:123")
1038 .unwrap();
1039 let mut lxm = BTreeSet::new();
1040 let mut aud = BTreeSet::new();
1041 lxm.insert(RpcLexicon::Nsid("com.example.method1".to_string()));
1042 lxm.insert(RpcLexicon::Nsid("com.example.method2".to_string()));
1043 aud.insert(RpcAudience::Did("did:example:123".to_string()));
1044 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1045 }
1046
1047 #[test]
1048 fn test_scope_normalization() {
1049 let tests = vec![
1050 ("account:email", "account:email"),
1051 ("account:email?action=read", "account:email"),
1052 ("account:email?action=manage", "account:email?action=manage"),
1053 ("blob:image/png", "blob:image/png"),
1054 (
1055 "blob?accept=image/jpeg&accept=image/png",
1056 "blob?accept=image/jpeg&accept=image/png",
1057 ),
1058 ("repo:foo.bar", "repo:foo.bar"),
1059 ("repo:foo.bar?action=create", "repo:foo.bar?action=create"),
1060 ("rpc:*", "rpc:*"),
1061 ];
1062
1063 for (input, expected) in tests {
1064 let scope = Scope::parse(input).unwrap();
1065 assert_eq!(scope.to_string_normalized(), expected);
1066 }
1067 }
1068
1069 #[test]
1070 fn test_account_scope_grants() {
1071 let manage = Scope::parse("account:email?action=manage").unwrap();
1072 let read = Scope::parse("account:email?action=read").unwrap();
1073 let other_read = Scope::parse("account:repo?action=read").unwrap();
1074
1075 assert!(manage.grants(&read));
1076 assert!(manage.grants(&manage));
1077 assert!(!read.grants(&manage));
1078 assert!(read.grants(&read));
1079 assert!(!read.grants(&other_read));
1080 }
1081
1082 #[test]
1083 fn test_identity_scope_grants() {
1084 let all = Scope::parse("identity:*").unwrap();
1085 let handle = Scope::parse("identity:handle").unwrap();
1086
1087 assert!(all.grants(&handle));
1088 assert!(all.grants(&all));
1089 assert!(!handle.grants(&all));
1090 assert!(handle.grants(&handle));
1091 }
1092
1093 #[test]
1094 fn test_blob_scope_grants() {
1095 let all = Scope::parse("blob:*/*").unwrap();
1096 let image_all = Scope::parse("blob:image/*").unwrap();
1097 let image_png = Scope::parse("blob:image/png").unwrap();
1098 let text_plain = Scope::parse("blob:text/plain").unwrap();
1099
1100 assert!(all.grants(&image_all));
1101 assert!(all.grants(&image_png));
1102 assert!(all.grants(&text_plain));
1103 assert!(image_all.grants(&image_png));
1104 assert!(!image_all.grants(&text_plain));
1105 assert!(!image_png.grants(&image_all));
1106 }
1107
1108 #[test]
1109 fn test_repo_scope_grants() {
1110 let all_all = Scope::parse("repo:*").unwrap();
1111 let all_create = Scope::parse("repo:*?action=create").unwrap();
1112 let specific_all = Scope::parse("repo:foo.bar").unwrap();
1113 let specific_create = Scope::parse("repo:foo.bar?action=create").unwrap();
1114 let other_create = Scope::parse("repo:baz.qux?action=create").unwrap();
1115
1116 assert!(all_all.grants(&all_create));
1117 assert!(all_all.grants(&specific_all));
1118 assert!(all_all.grants(&specific_create));
1119 assert!(all_create.grants(&all_create));
1120 assert!(!all_create.grants(&specific_all));
1121 assert!(specific_all.grants(&specific_create));
1122 assert!(!specific_create.grants(&specific_all));
1123 assert!(!specific_create.grants(&other_create));
1124 }
1125
1126 #[test]
1127 fn test_rpc_scope_grants() {
1128 let all = Scope::parse("rpc:*").unwrap();
1129 let specific_lxm = Scope::parse("rpc:com.example.service").unwrap();
1130 let specific_both = Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap();
1131
1132 assert!(all.grants(&specific_lxm));
1133 assert!(all.grants(&specific_both));
1134 assert!(specific_lxm.grants(&specific_both));
1135 assert!(!specific_both.grants(&specific_lxm));
1136 assert!(!specific_both.grants(&all));
1137 }
1138
1139 #[test]
1140 fn test_cross_scope_grants() {
1141 let account = Scope::parse("account:email").unwrap();
1142 let identity = Scope::parse("identity:handle").unwrap();
1143
1144 assert!(!account.grants(&identity));
1145 assert!(!identity.grants(&account));
1146 }
1147
1148 #[test]
1149 fn test_parse_errors() {
1150 assert!(matches!(
1151 Scope::parse("unknown:test"),
1152 Err(ParseError::UnknownPrefix(_))
1153 ));
1154
1155 assert!(matches!(
1156 Scope::parse("account"),
1157 Err(ParseError::MissingResource)
1158 ));
1159
1160 assert!(matches!(
1161 Scope::parse("account:invalid"),
1162 Err(ParseError::InvalidResource(_))
1163 ));
1164
1165 assert!(matches!(
1166 Scope::parse("account:email?action=invalid"),
1167 Err(ParseError::InvalidAction(_))
1168 ));
1169 }
1170
1171 #[test]
1172 fn test_query_parameter_sorting() {
1173 let scope =
1174 Scope::parse("blob?accept=image/png&accept=application/pdf&accept=image/jpeg").unwrap();
1175 let normalized = scope.to_string_normalized();
1176 assert!(normalized.contains("accept=application/pdf"));
1177 assert!(normalized.contains("accept=image/jpeg"));
1178 assert!(normalized.contains("accept=image/png"));
1179 let pdf_pos = normalized.find("accept=application/pdf").unwrap();
1180 let jpeg_pos = normalized.find("accept=image/jpeg").unwrap();
1181 let png_pos = normalized.find("accept=image/png").unwrap();
1182 assert!(pdf_pos < jpeg_pos);
1183 assert!(jpeg_pos < png_pos);
1184 }
1185
1186 #[test]
1187 fn test_repo_action_wildcard() {
1188 let scope = Scope::parse("repo:foo.bar?action=*").unwrap();
1189 let mut actions = BTreeSet::new();
1190 actions.insert(RepoAction::Create);
1191 actions.insert(RepoAction::Update);
1192 actions.insert(RepoAction::Delete);
1193 assert_eq!(
1194 scope,
1195 Scope::Repo(RepoScope {
1196 collection: RepoCollection::Nsid("foo.bar".to_string()),
1197 actions,
1198 })
1199 );
1200 }
1201
1202 #[test]
1203 fn test_multiple_blob_accepts() {
1204 let scope = Scope::parse("blob?accept=image/*&accept=text/plain").unwrap();
1205 assert!(scope.grants(&Scope::parse("blob:image/png").unwrap()));
1206 assert!(scope.grants(&Scope::parse("blob:text/plain").unwrap()));
1207 assert!(!scope.grants(&Scope::parse("blob:application/json").unwrap()));
1208 }
1209
1210 #[test]
1211 fn test_rpc_default_wildcards() {
1212 let scope = Scope::parse("rpc").unwrap();
1213 let mut lxm = BTreeSet::new();
1214 let mut aud = BTreeSet::new();
1215 lxm.insert(RpcLexicon::All);
1216 aud.insert(RpcAudience::All);
1217 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1218 }
1219
1220 #[test]
1221 fn test_atproto_scope_parsing() {
1222 let scope = Scope::parse("atproto").unwrap();
1223 assert_eq!(scope, Scope::Atproto);
1224
1225 assert!(Scope::parse("atproto:something").is_err());
1227 assert!(Scope::parse("atproto?param=value").is_err());
1228 }
1229
1230 #[test]
1231 fn test_transition_scope_parsing() {
1232 let scope = Scope::parse("transition:generic").unwrap();
1233 assert_eq!(scope, Scope::Transition(TransitionScope::Generic));
1234
1235 let scope = Scope::parse("transition:email").unwrap();
1236 assert_eq!(scope, Scope::Transition(TransitionScope::Email));
1237
1238 assert!(matches!(
1240 Scope::parse("transition:invalid"),
1241 Err(ParseError::InvalidResource(_))
1242 ));
1243
1244 assert!(matches!(
1246 Scope::parse("transition"),
1247 Err(ParseError::MissingResource)
1248 ));
1249
1250 assert!(matches!(
1252 Scope::parse("transition:generic?param=value"),
1253 Err(ParseError::InvalidResource(_))
1254 ));
1255 }
1256
1257 #[test]
1258 fn test_atproto_scope_normalization() {
1259 let scope = Scope::parse("atproto").unwrap();
1260 assert_eq!(scope.to_string_normalized(), "atproto");
1261 }
1262
1263 #[test]
1264 fn test_transition_scope_normalization() {
1265 let tests = vec![
1266 ("transition:generic", "transition:generic"),
1267 ("transition:email", "transition:email"),
1268 ];
1269
1270 for (input, expected) in tests {
1271 let scope = Scope::parse(input).unwrap();
1272 assert_eq!(scope.to_string_normalized(), expected);
1273 }
1274 }
1275
1276 #[test]
1277 fn test_atproto_scope_grants() {
1278 let atproto = Scope::parse("atproto").unwrap();
1279 let account = Scope::parse("account:email").unwrap();
1280 let identity = Scope::parse("identity:handle").unwrap();
1281 let blob = Scope::parse("blob:image/png").unwrap();
1282 let repo = Scope::parse("repo:foo.bar").unwrap();
1283 let rpc = Scope::parse("rpc:com.example.service").unwrap();
1284 let transition_generic = Scope::parse("transition:generic").unwrap();
1285 let transition_email = Scope::parse("transition:email").unwrap();
1286
1287 assert!(atproto.grants(&atproto));
1289 assert!(!atproto.grants(&account));
1290 assert!(!atproto.grants(&identity));
1291 assert!(!atproto.grants(&blob));
1292 assert!(!atproto.grants(&repo));
1293 assert!(!atproto.grants(&rpc));
1294 assert!(!atproto.grants(&transition_generic));
1295 assert!(!atproto.grants(&transition_email));
1296
1297 assert!(!account.grants(&atproto));
1299 assert!(!identity.grants(&atproto));
1300 assert!(!blob.grants(&atproto));
1301 assert!(!repo.grants(&atproto));
1302 assert!(!rpc.grants(&atproto));
1303 assert!(!transition_generic.grants(&atproto));
1304 assert!(!transition_email.grants(&atproto));
1305 }
1306
1307 #[test]
1308 fn test_transition_scope_grants() {
1309 let transition_generic = Scope::parse("transition:generic").unwrap();
1310 let transition_email = Scope::parse("transition:email").unwrap();
1311 let account = Scope::parse("account:email").unwrap();
1312
1313 assert!(transition_generic.grants(&transition_generic));
1315 assert!(transition_email.grants(&transition_email));
1316 assert!(!transition_generic.grants(&transition_email));
1317 assert!(!transition_email.grants(&transition_generic));
1318
1319 assert!(!transition_generic.grants(&account));
1321 assert!(!transition_email.grants(&account));
1322
1323 assert!(!account.grants(&transition_generic));
1325 assert!(!account.grants(&transition_email));
1326 }
1327
1328 #[test]
1329 fn test_parse_multiple() {
1330 let scopes = Scope::parse_multiple("atproto repo:*").unwrap();
1332 assert_eq!(scopes.len(), 2);
1333 assert_eq!(scopes[0], Scope::Atproto);
1334 assert_eq!(
1335 scopes[1],
1336 Scope::Repo(RepoScope {
1337 collection: RepoCollection::All,
1338 actions: {
1339 let mut actions = BTreeSet::new();
1340 actions.insert(RepoAction::Create);
1341 actions.insert(RepoAction::Update);
1342 actions.insert(RepoAction::Delete);
1343 actions
1344 }
1345 })
1346 );
1347
1348 let scopes = Scope::parse_multiple("account:email identity:handle blob:image/png").unwrap();
1350 assert_eq!(scopes.len(), 3);
1351 assert!(matches!(scopes[0], Scope::Account(_)));
1352 assert!(matches!(scopes[1], Scope::Identity(_)));
1353 assert!(matches!(scopes[2], Scope::Blob(_)));
1354
1355 let scopes = Scope::parse_multiple(
1357 "account:email?action=manage repo:foo.bar?action=create transition:email",
1358 )
1359 .unwrap();
1360 assert_eq!(scopes.len(), 3);
1361
1362 let scopes = Scope::parse_multiple("").unwrap();
1364 assert_eq!(scopes.len(), 0);
1365
1366 let scopes = Scope::parse_multiple(" ").unwrap();
1368 assert_eq!(scopes.len(), 0);
1369
1370 let scopes = Scope::parse_multiple(" atproto repo:* ").unwrap();
1372 assert_eq!(scopes.len(), 2);
1373
1374 let scopes = Scope::parse_multiple("atproto").unwrap();
1376 assert_eq!(scopes.len(), 1);
1377 assert_eq!(scopes[0], Scope::Atproto);
1378
1379 assert!(Scope::parse_multiple("atproto invalid:scope").is_err());
1381 assert!(Scope::parse_multiple("account:invalid repo:*").is_err());
1382 }
1383
1384 #[test]
1385 fn test_parse_multiple_reduced() {
1386 let scopes = Scope::parse_multiple_reduced("atproto repo:foo.bar repo:*").unwrap();
1388 assert_eq!(scopes.len(), 2);
1389 assert!(scopes.contains(&Scope::Atproto));
1390 assert!(scopes.contains(&Scope::Repo(RepoScope {
1391 collection: RepoCollection::All,
1392 actions: {
1393 let mut actions = BTreeSet::new();
1394 actions.insert(RepoAction::Create);
1395 actions.insert(RepoAction::Update);
1396 actions.insert(RepoAction::Delete);
1397 actions
1398 }
1399 })));
1400
1401 let scopes = Scope::parse_multiple_reduced("atproto repo:* repo:foo.bar").unwrap();
1403 assert_eq!(scopes.len(), 2);
1404 assert!(scopes.contains(&Scope::Atproto));
1405 assert!(scopes.contains(&Scope::Repo(RepoScope {
1406 collection: RepoCollection::All,
1407 actions: {
1408 let mut actions = BTreeSet::new();
1409 actions.insert(RepoAction::Create);
1410 actions.insert(RepoAction::Update);
1411 actions.insert(RepoAction::Delete);
1412 actions
1413 }
1414 })));
1415
1416 let scopes =
1418 Scope::parse_multiple_reduced("account:email account:email?action=manage").unwrap();
1419 assert_eq!(scopes.len(), 1);
1420 assert_eq!(
1421 scopes[0],
1422 Scope::Account(AccountScope {
1423 resource: AccountResource::Email,
1424 action: AccountAction::Manage,
1425 })
1426 );
1427
1428 let scopes = Scope::parse_multiple_reduced("identity:handle identity:*").unwrap();
1430 assert_eq!(scopes.len(), 1);
1431 assert_eq!(scopes[0], Scope::Identity(IdentityScope::All));
1432
1433 let scopes = Scope::parse_multiple_reduced("blob:image/png blob:image/* blob:*/*").unwrap();
1435 assert_eq!(scopes.len(), 1);
1436 let mut accept = BTreeSet::new();
1437 accept.insert(MimePattern::All);
1438 assert_eq!(scopes[0], Scope::Blob(BlobScope { accept }));
1439
1440 let scopes =
1442 Scope::parse_multiple_reduced("account:email identity:handle blob:image/png").unwrap();
1443 assert_eq!(scopes.len(), 3);
1444
1445 let scopes =
1447 Scope::parse_multiple_reduced("repo:foo.bar?action=create repo:foo.bar").unwrap();
1448 assert_eq!(scopes.len(), 1);
1449 assert_eq!(
1450 scopes[0],
1451 Scope::Repo(RepoScope {
1452 collection: RepoCollection::Nsid("foo.bar".to_string()),
1453 actions: {
1454 let mut actions = BTreeSet::new();
1455 actions.insert(RepoAction::Create);
1456 actions.insert(RepoAction::Update);
1457 actions.insert(RepoAction::Delete);
1458 actions
1459 }
1460 })
1461 );
1462
1463 let scopes = Scope::parse_multiple_reduced(
1465 "rpc:com.example.service?aud=did:example:123 rpc:com.example.service rpc:*",
1466 )
1467 .unwrap();
1468 assert_eq!(scopes.len(), 1);
1469 assert_eq!(
1470 scopes[0],
1471 Scope::Rpc(RpcScope {
1472 lxm: {
1473 let mut lxm = BTreeSet::new();
1474 lxm.insert(RpcLexicon::All);
1475 lxm
1476 },
1477 aud: {
1478 let mut aud = BTreeSet::new();
1479 aud.insert(RpcAudience::All);
1480 aud
1481 }
1482 })
1483 );
1484
1485 let scopes = Scope::parse_multiple_reduced("atproto atproto atproto").unwrap();
1487 assert_eq!(scopes.len(), 1);
1488 assert_eq!(scopes[0], Scope::Atproto);
1489
1490 let scopes = Scope::parse_multiple_reduced("transition:generic transition:email").unwrap();
1492 assert_eq!(scopes.len(), 2);
1493 assert!(scopes.contains(&Scope::Transition(TransitionScope::Generic)));
1494 assert!(scopes.contains(&Scope::Transition(TransitionScope::Email)));
1495
1496 let scopes = Scope::parse_multiple_reduced("").unwrap();
1498 assert_eq!(scopes.len(), 0);
1499
1500 let scopes = Scope::parse_multiple_reduced(
1502 "account:email?action=manage account:email account:repo account:repo?action=read identity:* identity:handle"
1503 ).unwrap();
1504 assert_eq!(scopes.len(), 3);
1505 assert!(scopes.contains(&Scope::Account(AccountScope {
1507 resource: AccountResource::Email,
1508 action: AccountAction::Manage,
1509 })));
1510 assert!(scopes.contains(&Scope::Account(AccountScope {
1511 resource: AccountResource::Repo,
1512 action: AccountAction::Read,
1513 })));
1514 assert!(scopes.contains(&Scope::Identity(IdentityScope::All)));
1515
1516 let scopes = Scope::parse_multiple_reduced("atproto account:email repo:*").unwrap();
1518 assert_eq!(scopes.len(), 3);
1519 assert!(scopes.contains(&Scope::Atproto));
1520 assert!(scopes.contains(&Scope::Account(AccountScope {
1521 resource: AccountResource::Email,
1522 action: AccountAction::Read,
1523 })));
1524 assert!(scopes.contains(&Scope::Repo(RepoScope {
1525 collection: RepoCollection::All,
1526 actions: {
1527 let mut actions = BTreeSet::new();
1528 actions.insert(RepoAction::Create);
1529 actions.insert(RepoAction::Update);
1530 actions.insert(RepoAction::Delete);
1531 actions
1532 }
1533 })));
1534 }
1535
1536 #[test]
1537 fn test_openid_connect_scope_parsing() {
1538 let scope = Scope::parse("openid").unwrap();
1540 assert_eq!(scope, Scope::OpenId);
1541
1542 let scope = Scope::parse("profile").unwrap();
1544 assert_eq!(scope, Scope::Profile);
1545
1546 let scope = Scope::parse("email").unwrap();
1548 assert_eq!(scope, Scope::Email);
1549
1550 assert!(Scope::parse("openid:something").is_err());
1552 assert!(Scope::parse("profile:something").is_err());
1553 assert!(Scope::parse("email:something").is_err());
1554
1555 assert!(Scope::parse("openid?param=value").is_err());
1557 assert!(Scope::parse("profile?param=value").is_err());
1558 assert!(Scope::parse("email?param=value").is_err());
1559 }
1560
1561 #[test]
1562 fn test_openid_connect_scope_normalization() {
1563 let scope = Scope::parse("openid").unwrap();
1564 assert_eq!(scope.to_string_normalized(), "openid");
1565
1566 let scope = Scope::parse("profile").unwrap();
1567 assert_eq!(scope.to_string_normalized(), "profile");
1568
1569 let scope = Scope::parse("email").unwrap();
1570 assert_eq!(scope.to_string_normalized(), "email");
1571 }
1572
1573 #[test]
1574 fn test_openid_connect_scope_grants() {
1575 let openid = Scope::parse("openid").unwrap();
1576 let profile = Scope::parse("profile").unwrap();
1577 let email = Scope::parse("email").unwrap();
1578 let account = Scope::parse("account:email").unwrap();
1579
1580 assert!(openid.grants(&openid));
1582 assert!(!openid.grants(&profile));
1583 assert!(!openid.grants(&email));
1584 assert!(!openid.grants(&account));
1585
1586 assert!(profile.grants(&profile));
1587 assert!(!profile.grants(&openid));
1588 assert!(!profile.grants(&email));
1589 assert!(!profile.grants(&account));
1590
1591 assert!(email.grants(&email));
1592 assert!(!email.grants(&openid));
1593 assert!(!email.grants(&profile));
1594 assert!(!email.grants(&account));
1595
1596 assert!(!account.grants(&openid));
1598 assert!(!account.grants(&profile));
1599 assert!(!account.grants(&email));
1600 }
1601
1602 #[test]
1603 fn test_parse_multiple_with_openid_connect() {
1604 let scopes = Scope::parse_multiple("openid profile email atproto").unwrap();
1605 assert_eq!(scopes.len(), 4);
1606 assert_eq!(scopes[0], Scope::OpenId);
1607 assert_eq!(scopes[1], Scope::Profile);
1608 assert_eq!(scopes[2], Scope::Email);
1609 assert_eq!(scopes[3], Scope::Atproto);
1610
1611 let scopes = Scope::parse_multiple("openid account:email profile repo:*").unwrap();
1613 assert_eq!(scopes.len(), 4);
1614 assert!(scopes.contains(&Scope::OpenId));
1615 assert!(scopes.contains(&Scope::Profile));
1616 }
1617
1618 #[test]
1619 fn test_parse_multiple_reduced_with_openid_connect() {
1620 let scopes = Scope::parse_multiple_reduced("openid profile email openid").unwrap();
1622 assert_eq!(scopes.len(), 3);
1623 assert!(scopes.contains(&Scope::OpenId));
1624 assert!(scopes.contains(&Scope::Profile));
1625 assert!(scopes.contains(&Scope::Email));
1626
1627 let scopes = Scope::parse_multiple_reduced(
1629 "openid account:email account:email?action=manage profile",
1630 )
1631 .unwrap();
1632 assert_eq!(scopes.len(), 3);
1633 assert!(scopes.contains(&Scope::OpenId));
1634 assert!(scopes.contains(&Scope::Profile));
1635 assert!(scopes.contains(&Scope::Account(AccountScope {
1636 resource: AccountResource::Email,
1637 action: AccountAction::Manage,
1638 })));
1639 }
1640
1641 #[test]
1642 fn test_serialize_multiple() {
1643 let scopes: Vec<Scope> = vec![];
1645 assert_eq!(Scope::serialize_multiple(&scopes), "");
1646
1647 let scopes = vec![Scope::Atproto];
1649 assert_eq!(Scope::serialize_multiple(&scopes), "atproto");
1650
1651 let scopes = vec![
1653 Scope::parse("repo:*").unwrap(),
1654 Scope::Atproto,
1655 Scope::parse("account:email").unwrap(),
1656 ];
1657 assert_eq!(
1658 Scope::serialize_multiple(&scopes),
1659 "account:email atproto repo:*"
1660 );
1661
1662 let scopes = vec![
1664 Scope::parse("identity:handle").unwrap(),
1665 Scope::parse("blob:image/png").unwrap(),
1666 Scope::parse("account:repo?action=manage").unwrap(),
1667 ];
1668 assert_eq!(
1669 Scope::serialize_multiple(&scopes),
1670 "account:repo?action=manage blob:image/png identity:handle"
1671 );
1672
1673 let scopes = vec![Scope::Email, Scope::OpenId, Scope::Profile, Scope::Atproto];
1675 assert_eq!(
1676 Scope::serialize_multiple(&scopes),
1677 "atproto email openid profile"
1678 );
1679
1680 let scopes = vec![
1682 Scope::parse("rpc:com.example.service?aud=did:example:123&lxm=com.example.method")
1683 .unwrap(),
1684 Scope::parse("repo:foo.bar?action=create&action=update").unwrap(),
1685 Scope::parse("blob:image/*?accept=image/png&accept=image/jpeg").unwrap(),
1686 ];
1687 let result = Scope::serialize_multiple(&scopes);
1688 assert!(result.starts_with("blob:"));
1691 assert!(result.contains(" repo:"));
1692 assert!(result.contains("rpc?aud=did:example:123&lxm=com.example.service"));
1693
1694 let scopes = vec![
1696 Scope::Transition(TransitionScope::Email),
1697 Scope::Transition(TransitionScope::Generic),
1698 Scope::Atproto,
1699 ];
1700 assert_eq!(
1701 Scope::serialize_multiple(&scopes),
1702 "atproto transition:email transition:generic"
1703 );
1704
1705 let scopes = vec![
1707 Scope::Atproto,
1708 Scope::Atproto,
1709 Scope::parse("account:email").unwrap(),
1710 ];
1711 assert_eq!(
1712 Scope::serialize_multiple(&scopes),
1713 "account:email atproto atproto"
1714 );
1715
1716 let scopes = vec![Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap()];
1718 assert_eq!(
1720 Scope::serialize_multiple(&scopes),
1721 "blob?accept=image/jpeg&accept=image/png"
1722 );
1723 }
1724
1725 #[test]
1726 fn test_serialize_multiple_roundtrip() {
1727 let original = "account:email atproto blob:image/png identity:handle repo:*";
1729 let scopes = Scope::parse_multiple(original).unwrap();
1730 let serialized = Scope::serialize_multiple(&scopes);
1731 assert_eq!(serialized, original);
1732
1733 let original = "account:repo?action=manage blob?accept=image/jpeg&accept=image/png rpc:*";
1735 let scopes = Scope::parse_multiple(original).unwrap();
1736 let serialized = Scope::serialize_multiple(&scopes);
1737 let reparsed = Scope::parse_multiple(&serialized).unwrap();
1739 assert_eq!(scopes, reparsed);
1740
1741 let original = "email openid profile";
1743 let scopes = Scope::parse_multiple(original).unwrap();
1744 let serialized = Scope::serialize_multiple(&scopes);
1745 assert_eq!(serialized, original);
1746 }
1747
1748 #[test]
1749 fn test_remove_scope() {
1750 let scopes = vec![
1752 Scope::parse("repo:*").unwrap(),
1753 Scope::Atproto,
1754 Scope::parse("account:email").unwrap(),
1755 ];
1756 let to_remove = Scope::Atproto;
1757 let result = Scope::remove_scope(&scopes, &to_remove);
1758 assert_eq!(result.len(), 2);
1759 assert!(!result.contains(&to_remove));
1760 assert!(result.contains(&Scope::parse("repo:*").unwrap()));
1761 assert!(result.contains(&Scope::parse("account:email").unwrap()));
1762
1763 let scopes = vec![
1765 Scope::parse("repo:*").unwrap(),
1766 Scope::parse("account:email").unwrap(),
1767 ];
1768 let to_remove = Scope::parse("identity:handle").unwrap();
1769 let result = Scope::remove_scope(&scopes, &to_remove);
1770 assert_eq!(result.len(), 2);
1771 assert_eq!(result, scopes);
1772
1773 let scopes: Vec<Scope> = vec![];
1775 let to_remove = Scope::Atproto;
1776 let result = Scope::remove_scope(&scopes, &to_remove);
1777 assert_eq!(result.len(), 0);
1778
1779 let scopes = vec![
1781 Scope::Atproto,
1782 Scope::parse("account:email").unwrap(),
1783 Scope::Atproto,
1784 Scope::parse("repo:*").unwrap(),
1785 Scope::Atproto,
1786 ];
1787 let to_remove = Scope::Atproto;
1788 let result = Scope::remove_scope(&scopes, &to_remove);
1789 assert_eq!(result.len(), 2);
1790 assert!(!result.contains(&to_remove));
1791 assert!(result.contains(&Scope::parse("account:email").unwrap()));
1792 assert!(result.contains(&Scope::parse("repo:*").unwrap()));
1793
1794 let scopes = vec![
1796 Scope::parse("account:email?action=manage").unwrap(),
1797 Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap(),
1798 Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap(),
1799 ];
1800 let to_remove = Scope::parse("blob?accept=image/jpeg&accept=image/png").unwrap(); let result = Scope::remove_scope(&scopes, &to_remove);
1802 assert_eq!(result.len(), 2);
1803 assert!(!result.contains(&to_remove));
1804
1805 let scopes = vec![Scope::OpenId, Scope::Profile, Scope::Email, Scope::Atproto];
1807 let to_remove = Scope::Profile;
1808 let result = Scope::remove_scope(&scopes, &to_remove);
1809 assert_eq!(result.len(), 3);
1810 assert!(!result.contains(&to_remove));
1811 assert!(result.contains(&Scope::OpenId));
1812 assert!(result.contains(&Scope::Email));
1813 assert!(result.contains(&Scope::Atproto));
1814
1815 let scopes = vec![
1817 Scope::Transition(TransitionScope::Generic),
1818 Scope::Transition(TransitionScope::Email),
1819 Scope::Atproto,
1820 ];
1821 let to_remove = Scope::Transition(TransitionScope::Email);
1822 let result = Scope::remove_scope(&scopes, &to_remove);
1823 assert_eq!(result.len(), 2);
1824 assert!(!result.contains(&to_remove));
1825 assert!(result.contains(&Scope::Transition(TransitionScope::Generic)));
1826 assert!(result.contains(&Scope::Atproto));
1827
1828 let scopes = vec![
1830 Scope::parse("account:email").unwrap(),
1831 Scope::parse("account:email?action=manage").unwrap(),
1832 Scope::parse("account:repo").unwrap(),
1833 ];
1834 let to_remove = Scope::parse("account:email").unwrap();
1835 let result = Scope::remove_scope(&scopes, &to_remove);
1836 assert_eq!(result.len(), 2);
1837 assert!(!result.contains(&Scope::parse("account:email").unwrap()));
1838 assert!(result.contains(&Scope::parse("account:email?action=manage").unwrap()));
1839 assert!(result.contains(&Scope::parse("account:repo").unwrap()));
1840 }
1841}