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.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 let Some(remainder) = s.strip_prefix(prefix)
322 && (remainder.is_empty()
323 || remainder.starts_with(':')
324 || remainder.starts_with('?'))
325 {
326 found_prefix = Some(*prefix);
327 if let Some(stripped) = remainder.strip_prefix(':') {
328 suffix = Some(stripped);
329 } else if remainder.starts_with('?') {
330 suffix = Some(remainder);
331 } else {
332 suffix = None;
333 }
334 break;
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 && matches!(
748 (a.action, b.action),
749 (AccountAction::Manage, _) | (AccountAction::Read, AccountAction::Read)
750 )
751 }
752 (Scope::Identity(a), Scope::Identity(b)) => matches!(
753 (a, b),
754 (IdentityScope::All, _) | (IdentityScope::Handle, IdentityScope::Handle)
755 ),
756 (Scope::Blob(a), Scope::Blob(b)) => {
757 for b_pattern in &b.accept {
758 let mut granted = false;
759 for a_pattern in &a.accept {
760 if a_pattern.grants(b_pattern) {
761 granted = true;
762 break;
763 }
764 }
765 if !granted {
766 return false;
767 }
768 }
769 true
770 }
771 (Scope::Repo(a), Scope::Repo(b)) => {
772 let collection_match = match (&a.collection, &b.collection) {
773 (RepoCollection::All, _) => true,
774 (RepoCollection::Nsid(a_nsid), RepoCollection::Nsid(b_nsid)) => {
775 a_nsid == b_nsid
776 }
777 _ => false,
778 };
779
780 if !collection_match {
781 return false;
782 }
783
784 b.actions.is_subset(&a.actions) || a.actions.len() == 3
785 }
786 (Scope::Rpc(a), Scope::Rpc(b)) => {
787 let lxm_match = if a.lxm.contains(&RpcLexicon::All) {
788 true
789 } else {
790 b.lxm.iter().all(|b_lxm| match b_lxm {
791 RpcLexicon::All => false,
792 RpcLexicon::Nsid(_) => a.lxm.contains(b_lxm),
793 })
794 };
795
796 let aud_match = if a.aud.contains(&RpcAudience::All) {
797 true
798 } else {
799 b.aud.iter().all(|b_aud| match b_aud {
800 RpcAudience::All => false,
801 RpcAudience::Did(_) => a.aud.contains(b_aud),
802 })
803 };
804
805 lxm_match && aud_match
806 }
807 _ => false,
808 }
809 }
810}
811
812impl MimePattern {
813 fn grants(&self, other: &MimePattern) -> bool {
814 match (self, other) {
815 (MimePattern::All, _) => true,
816 (MimePattern::TypeWildcard(a_type), MimePattern::TypeWildcard(b_type)) => {
817 a_type == b_type
818 }
819 (MimePattern::TypeWildcard(a_type), MimePattern::Exact(b_mime)) => {
820 b_mime.starts_with(&format!("{}/", a_type))
821 }
822 (MimePattern::Exact(a), MimePattern::Exact(b)) => a == b,
823 _ => false,
824 }
825 }
826}
827
828impl FromStr for MimePattern {
829 type Err = ParseError;
830
831 fn from_str(s: &str) -> Result<Self, Self::Err> {
832 if s == "*/*" {
833 Ok(MimePattern::All)
834 } else if let Some(stripped) = s.strip_suffix("/*") {
835 Ok(MimePattern::TypeWildcard(stripped.to_string()))
836 } else if s.contains('/') {
837 Ok(MimePattern::Exact(s.to_string()))
838 } else {
839 Err(ParseError::InvalidMimeType(s.to_string()))
840 }
841 }
842}
843
844impl FromStr for Scope {
845 type Err = ParseError;
846
847 fn from_str(s: &str) -> Result<Self, Self::Err> {
848 Self::parse(s)
849 }
850}
851
852impl fmt::Display for Scope {
853 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
854 write!(f, "{}", self.to_string_normalized())
855 }
856}
857
858fn parse_query_string(query: &str) -> BTreeMap<String, Vec<String>> {
860 let mut params = BTreeMap::new();
861
862 for pair in query.split('&') {
863 if let Some(pos) = pair.find('=') {
864 let key = &pair[..pos];
865 let value = &pair[pos + 1..];
866 params
867 .entry(key.to_string())
868 .or_insert_with(Vec::new)
869 .push(value.to_string());
870 }
871 }
872
873 params
874}
875
876#[derive(Debug, Clone, PartialEq, Eq)]
878pub enum ParseError {
879 UnknownPrefix(String),
881 MissingResource,
883 InvalidResource(String),
885 InvalidAction(String),
887 InvalidMimeType(String),
889}
890
891impl fmt::Display for ParseError {
892 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
893 match self {
894 ParseError::UnknownPrefix(prefix) => write!(f, "Unknown scope prefix: {}", prefix),
895 ParseError::MissingResource => write!(f, "Missing required resource"),
896 ParseError::InvalidResource(resource) => write!(f, "Invalid resource: {}", resource),
897 ParseError::InvalidAction(action) => write!(f, "Invalid action: {}", action),
898 ParseError::InvalidMimeType(mime) => write!(f, "Invalid MIME type: {}", mime),
899 }
900 }
901}
902
903impl std::error::Error for ParseError {}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908
909 #[test]
910 fn test_account_scope_parsing() {
911 let scope = Scope::parse("account:email").unwrap();
912 assert_eq!(
913 scope,
914 Scope::Account(AccountScope {
915 resource: AccountResource::Email,
916 action: AccountAction::Read,
917 })
918 );
919
920 let scope = Scope::parse("account:repo?action=manage").unwrap();
921 assert_eq!(
922 scope,
923 Scope::Account(AccountScope {
924 resource: AccountResource::Repo,
925 action: AccountAction::Manage,
926 })
927 );
928
929 let scope = Scope::parse("account:status?action=read").unwrap();
930 assert_eq!(
931 scope,
932 Scope::Account(AccountScope {
933 resource: AccountResource::Status,
934 action: AccountAction::Read,
935 })
936 );
937 }
938
939 #[test]
940 fn test_identity_scope_parsing() {
941 let scope = Scope::parse("identity:handle").unwrap();
942 assert_eq!(scope, Scope::Identity(IdentityScope::Handle));
943
944 let scope = Scope::parse("identity:*").unwrap();
945 assert_eq!(scope, Scope::Identity(IdentityScope::All));
946 }
947
948 #[test]
949 fn test_blob_scope_parsing() {
950 let scope = Scope::parse("blob:*/*").unwrap();
951 let mut accept = BTreeSet::new();
952 accept.insert(MimePattern::All);
953 assert_eq!(scope, Scope::Blob(BlobScope { accept }));
954
955 let scope = Scope::parse("blob:image/png").unwrap();
956 let mut accept = BTreeSet::new();
957 accept.insert(MimePattern::Exact("image/png".to_string()));
958 assert_eq!(scope, Scope::Blob(BlobScope { accept }));
959
960 let scope = Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap();
961 let mut accept = BTreeSet::new();
962 accept.insert(MimePattern::Exact("image/png".to_string()));
963 accept.insert(MimePattern::Exact("image/jpeg".to_string()));
964 assert_eq!(scope, Scope::Blob(BlobScope { accept }));
965
966 let scope = Scope::parse("blob:image/*").unwrap();
967 let mut accept = BTreeSet::new();
968 accept.insert(MimePattern::TypeWildcard("image".to_string()));
969 assert_eq!(scope, Scope::Blob(BlobScope { accept }));
970 }
971
972 #[test]
973 fn test_repo_scope_parsing() {
974 let scope = Scope::parse("repo:*?action=create").unwrap();
975 let mut actions = BTreeSet::new();
976 actions.insert(RepoAction::Create);
977 assert_eq!(
978 scope,
979 Scope::Repo(RepoScope {
980 collection: RepoCollection::All,
981 actions,
982 })
983 );
984
985 let scope = Scope::parse("repo:foo.bar?action=create&action=update").unwrap();
986 let mut actions = BTreeSet::new();
987 actions.insert(RepoAction::Create);
988 actions.insert(RepoAction::Update);
989 assert_eq!(
990 scope,
991 Scope::Repo(RepoScope {
992 collection: RepoCollection::Nsid("foo.bar".to_string()),
993 actions,
994 })
995 );
996
997 let scope = Scope::parse("repo:foo.bar").unwrap();
998 let mut actions = BTreeSet::new();
999 actions.insert(RepoAction::Create);
1000 actions.insert(RepoAction::Update);
1001 actions.insert(RepoAction::Delete);
1002 assert_eq!(
1003 scope,
1004 Scope::Repo(RepoScope {
1005 collection: RepoCollection::Nsid("foo.bar".to_string()),
1006 actions,
1007 })
1008 );
1009 }
1010
1011 #[test]
1012 fn test_rpc_scope_parsing() {
1013 let scope = Scope::parse("rpc:*").unwrap();
1014 let mut lxm = BTreeSet::new();
1015 let mut aud = BTreeSet::new();
1016 lxm.insert(RpcLexicon::All);
1017 aud.insert(RpcAudience::All);
1018 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1019
1020 let scope = Scope::parse("rpc:com.example.service").unwrap();
1021 let mut lxm = BTreeSet::new();
1022 let mut aud = BTreeSet::new();
1023 lxm.insert(RpcLexicon::Nsid("com.example.service".to_string()));
1024 aud.insert(RpcAudience::All);
1025 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1026
1027 let scope = Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap();
1028 let mut lxm = BTreeSet::new();
1029 let mut aud = BTreeSet::new();
1030 lxm.insert(RpcLexicon::Nsid("com.example.service".to_string()));
1031 aud.insert(RpcAudience::Did("did:example:123".to_string()));
1032 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1033
1034 let scope =
1035 Scope::parse("rpc?lxm=com.example.method1&lxm=com.example.method2&aud=did:example:123")
1036 .unwrap();
1037 let mut lxm = BTreeSet::new();
1038 let mut aud = BTreeSet::new();
1039 lxm.insert(RpcLexicon::Nsid("com.example.method1".to_string()));
1040 lxm.insert(RpcLexicon::Nsid("com.example.method2".to_string()));
1041 aud.insert(RpcAudience::Did("did:example:123".to_string()));
1042 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1043 }
1044
1045 #[test]
1046 fn test_scope_normalization() {
1047 let tests = vec![
1048 ("account:email", "account:email"),
1049 ("account:email?action=read", "account:email"),
1050 ("account:email?action=manage", "account:email?action=manage"),
1051 ("blob:image/png", "blob:image/png"),
1052 (
1053 "blob?accept=image/jpeg&accept=image/png",
1054 "blob?accept=image/jpeg&accept=image/png",
1055 ),
1056 ("repo:foo.bar", "repo:foo.bar"),
1057 ("repo:foo.bar?action=create", "repo:foo.bar?action=create"),
1058 ("rpc:*", "rpc:*"),
1059 ];
1060
1061 for (input, expected) in tests {
1062 let scope = Scope::parse(input).unwrap();
1063 assert_eq!(scope.to_string_normalized(), expected);
1064 }
1065 }
1066
1067 #[test]
1068 fn test_account_scope_grants() {
1069 let manage = Scope::parse("account:email?action=manage").unwrap();
1070 let read = Scope::parse("account:email?action=read").unwrap();
1071 let other_read = Scope::parse("account:repo?action=read").unwrap();
1072
1073 assert!(manage.grants(&read));
1074 assert!(manage.grants(&manage));
1075 assert!(!read.grants(&manage));
1076 assert!(read.grants(&read));
1077 assert!(!read.grants(&other_read));
1078 }
1079
1080 #[test]
1081 fn test_identity_scope_grants() {
1082 let all = Scope::parse("identity:*").unwrap();
1083 let handle = Scope::parse("identity:handle").unwrap();
1084
1085 assert!(all.grants(&handle));
1086 assert!(all.grants(&all));
1087 assert!(!handle.grants(&all));
1088 assert!(handle.grants(&handle));
1089 }
1090
1091 #[test]
1092 fn test_blob_scope_grants() {
1093 let all = Scope::parse("blob:*/*").unwrap();
1094 let image_all = Scope::parse("blob:image/*").unwrap();
1095 let image_png = Scope::parse("blob:image/png").unwrap();
1096 let text_plain = Scope::parse("blob:text/plain").unwrap();
1097
1098 assert!(all.grants(&image_all));
1099 assert!(all.grants(&image_png));
1100 assert!(all.grants(&text_plain));
1101 assert!(image_all.grants(&image_png));
1102 assert!(!image_all.grants(&text_plain));
1103 assert!(!image_png.grants(&image_all));
1104 }
1105
1106 #[test]
1107 fn test_repo_scope_grants() {
1108 let all_all = Scope::parse("repo:*").unwrap();
1109 let all_create = Scope::parse("repo:*?action=create").unwrap();
1110 let specific_all = Scope::parse("repo:foo.bar").unwrap();
1111 let specific_create = Scope::parse("repo:foo.bar?action=create").unwrap();
1112 let other_create = Scope::parse("repo:baz.qux?action=create").unwrap();
1113
1114 assert!(all_all.grants(&all_create));
1115 assert!(all_all.grants(&specific_all));
1116 assert!(all_all.grants(&specific_create));
1117 assert!(all_create.grants(&all_create));
1118 assert!(!all_create.grants(&specific_all));
1119 assert!(specific_all.grants(&specific_create));
1120 assert!(!specific_create.grants(&specific_all));
1121 assert!(!specific_create.grants(&other_create));
1122 }
1123
1124 #[test]
1125 fn test_rpc_scope_grants() {
1126 let all = Scope::parse("rpc:*").unwrap();
1127 let specific_lxm = Scope::parse("rpc:com.example.service").unwrap();
1128 let specific_both = Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap();
1129
1130 assert!(all.grants(&specific_lxm));
1131 assert!(all.grants(&specific_both));
1132 assert!(specific_lxm.grants(&specific_both));
1133 assert!(!specific_both.grants(&specific_lxm));
1134 assert!(!specific_both.grants(&all));
1135 }
1136
1137 #[test]
1138 fn test_cross_scope_grants() {
1139 let account = Scope::parse("account:email").unwrap();
1140 let identity = Scope::parse("identity:handle").unwrap();
1141
1142 assert!(!account.grants(&identity));
1143 assert!(!identity.grants(&account));
1144 }
1145
1146 #[test]
1147 fn test_parse_errors() {
1148 assert!(matches!(
1149 Scope::parse("unknown:test"),
1150 Err(ParseError::UnknownPrefix(_))
1151 ));
1152
1153 assert!(matches!(
1154 Scope::parse("account"),
1155 Err(ParseError::MissingResource)
1156 ));
1157
1158 assert!(matches!(
1159 Scope::parse("account:invalid"),
1160 Err(ParseError::InvalidResource(_))
1161 ));
1162
1163 assert!(matches!(
1164 Scope::parse("account:email?action=invalid"),
1165 Err(ParseError::InvalidAction(_))
1166 ));
1167 }
1168
1169 #[test]
1170 fn test_query_parameter_sorting() {
1171 let scope =
1172 Scope::parse("blob?accept=image/png&accept=application/pdf&accept=image/jpeg").unwrap();
1173 let normalized = scope.to_string_normalized();
1174 assert!(normalized.contains("accept=application/pdf"));
1175 assert!(normalized.contains("accept=image/jpeg"));
1176 assert!(normalized.contains("accept=image/png"));
1177 let pdf_pos = normalized.find("accept=application/pdf").unwrap();
1178 let jpeg_pos = normalized.find("accept=image/jpeg").unwrap();
1179 let png_pos = normalized.find("accept=image/png").unwrap();
1180 assert!(pdf_pos < jpeg_pos);
1181 assert!(jpeg_pos < png_pos);
1182 }
1183
1184 #[test]
1185 fn test_repo_action_wildcard() {
1186 let scope = Scope::parse("repo:foo.bar?action=*").unwrap();
1187 let mut actions = BTreeSet::new();
1188 actions.insert(RepoAction::Create);
1189 actions.insert(RepoAction::Update);
1190 actions.insert(RepoAction::Delete);
1191 assert_eq!(
1192 scope,
1193 Scope::Repo(RepoScope {
1194 collection: RepoCollection::Nsid("foo.bar".to_string()),
1195 actions,
1196 })
1197 );
1198 }
1199
1200 #[test]
1201 fn test_multiple_blob_accepts() {
1202 let scope = Scope::parse("blob?accept=image/*&accept=text/plain").unwrap();
1203 assert!(scope.grants(&Scope::parse("blob:image/png").unwrap()));
1204 assert!(scope.grants(&Scope::parse("blob:text/plain").unwrap()));
1205 assert!(!scope.grants(&Scope::parse("blob:application/json").unwrap()));
1206 }
1207
1208 #[test]
1209 fn test_rpc_default_wildcards() {
1210 let scope = Scope::parse("rpc").unwrap();
1211 let mut lxm = BTreeSet::new();
1212 let mut aud = BTreeSet::new();
1213 lxm.insert(RpcLexicon::All);
1214 aud.insert(RpcAudience::All);
1215 assert_eq!(scope, Scope::Rpc(RpcScope { lxm, aud }));
1216 }
1217
1218 #[test]
1219 fn test_atproto_scope_parsing() {
1220 let scope = Scope::parse("atproto").unwrap();
1221 assert_eq!(scope, Scope::Atproto);
1222
1223 assert!(Scope::parse("atproto:something").is_err());
1225 assert!(Scope::parse("atproto?param=value").is_err());
1226 }
1227
1228 #[test]
1229 fn test_transition_scope_parsing() {
1230 let scope = Scope::parse("transition:generic").unwrap();
1231 assert_eq!(scope, Scope::Transition(TransitionScope::Generic));
1232
1233 let scope = Scope::parse("transition:email").unwrap();
1234 assert_eq!(scope, Scope::Transition(TransitionScope::Email));
1235
1236 assert!(matches!(
1238 Scope::parse("transition:invalid"),
1239 Err(ParseError::InvalidResource(_))
1240 ));
1241
1242 assert!(matches!(
1244 Scope::parse("transition"),
1245 Err(ParseError::MissingResource)
1246 ));
1247
1248 assert!(matches!(
1250 Scope::parse("transition:generic?param=value"),
1251 Err(ParseError::InvalidResource(_))
1252 ));
1253 }
1254
1255 #[test]
1256 fn test_atproto_scope_normalization() {
1257 let scope = Scope::parse("atproto").unwrap();
1258 assert_eq!(scope.to_string_normalized(), "atproto");
1259 }
1260
1261 #[test]
1262 fn test_transition_scope_normalization() {
1263 let tests = vec![
1264 ("transition:generic", "transition:generic"),
1265 ("transition:email", "transition:email"),
1266 ];
1267
1268 for (input, expected) in tests {
1269 let scope = Scope::parse(input).unwrap();
1270 assert_eq!(scope.to_string_normalized(), expected);
1271 }
1272 }
1273
1274 #[test]
1275 fn test_atproto_scope_grants() {
1276 let atproto = Scope::parse("atproto").unwrap();
1277 let account = Scope::parse("account:email").unwrap();
1278 let identity = Scope::parse("identity:handle").unwrap();
1279 let blob = Scope::parse("blob:image/png").unwrap();
1280 let repo = Scope::parse("repo:foo.bar").unwrap();
1281 let rpc = Scope::parse("rpc:com.example.service").unwrap();
1282 let transition_generic = Scope::parse("transition:generic").unwrap();
1283 let transition_email = Scope::parse("transition:email").unwrap();
1284
1285 assert!(atproto.grants(&atproto));
1287 assert!(!atproto.grants(&account));
1288 assert!(!atproto.grants(&identity));
1289 assert!(!atproto.grants(&blob));
1290 assert!(!atproto.grants(&repo));
1291 assert!(!atproto.grants(&rpc));
1292 assert!(!atproto.grants(&transition_generic));
1293 assert!(!atproto.grants(&transition_email));
1294
1295 assert!(!account.grants(&atproto));
1297 assert!(!identity.grants(&atproto));
1298 assert!(!blob.grants(&atproto));
1299 assert!(!repo.grants(&atproto));
1300 assert!(!rpc.grants(&atproto));
1301 assert!(!transition_generic.grants(&atproto));
1302 assert!(!transition_email.grants(&atproto));
1303 }
1304
1305 #[test]
1306 fn test_transition_scope_grants() {
1307 let transition_generic = Scope::parse("transition:generic").unwrap();
1308 let transition_email = Scope::parse("transition:email").unwrap();
1309 let account = Scope::parse("account:email").unwrap();
1310
1311 assert!(transition_generic.grants(&transition_generic));
1313 assert!(transition_email.grants(&transition_email));
1314 assert!(!transition_generic.grants(&transition_email));
1315 assert!(!transition_email.grants(&transition_generic));
1316
1317 assert!(!transition_generic.grants(&account));
1319 assert!(!transition_email.grants(&account));
1320
1321 assert!(!account.grants(&transition_generic));
1323 assert!(!account.grants(&transition_email));
1324 }
1325
1326 #[test]
1327 fn test_parse_multiple() {
1328 let scopes = Scope::parse_multiple("atproto repo:*").unwrap();
1330 assert_eq!(scopes.len(), 2);
1331 assert_eq!(scopes[0], Scope::Atproto);
1332 assert_eq!(
1333 scopes[1],
1334 Scope::Repo(RepoScope {
1335 collection: RepoCollection::All,
1336 actions: {
1337 let mut actions = BTreeSet::new();
1338 actions.insert(RepoAction::Create);
1339 actions.insert(RepoAction::Update);
1340 actions.insert(RepoAction::Delete);
1341 actions
1342 }
1343 })
1344 );
1345
1346 let scopes = Scope::parse_multiple("account:email identity:handle blob:image/png").unwrap();
1348 assert_eq!(scopes.len(), 3);
1349 assert!(matches!(scopes[0], Scope::Account(_)));
1350 assert!(matches!(scopes[1], Scope::Identity(_)));
1351 assert!(matches!(scopes[2], Scope::Blob(_)));
1352
1353 let scopes = Scope::parse_multiple(
1355 "account:email?action=manage repo:foo.bar?action=create transition:email",
1356 )
1357 .unwrap();
1358 assert_eq!(scopes.len(), 3);
1359
1360 let scopes = Scope::parse_multiple("").unwrap();
1362 assert_eq!(scopes.len(), 0);
1363
1364 let scopes = Scope::parse_multiple(" ").unwrap();
1366 assert_eq!(scopes.len(), 0);
1367
1368 let scopes = Scope::parse_multiple(" atproto repo:* ").unwrap();
1370 assert_eq!(scopes.len(), 2);
1371
1372 let scopes = Scope::parse_multiple("atproto").unwrap();
1374 assert_eq!(scopes.len(), 1);
1375 assert_eq!(scopes[0], Scope::Atproto);
1376
1377 assert!(Scope::parse_multiple("atproto invalid:scope").is_err());
1379 assert!(Scope::parse_multiple("account:invalid repo:*").is_err());
1380 }
1381
1382 #[test]
1383 fn test_parse_multiple_reduced() {
1384 let scopes = Scope::parse_multiple_reduced("atproto repo:foo.bar repo:*").unwrap();
1386 assert_eq!(scopes.len(), 2);
1387 assert!(scopes.contains(&Scope::Atproto));
1388 assert!(scopes.contains(&Scope::Repo(RepoScope {
1389 collection: RepoCollection::All,
1390 actions: {
1391 let mut actions = BTreeSet::new();
1392 actions.insert(RepoAction::Create);
1393 actions.insert(RepoAction::Update);
1394 actions.insert(RepoAction::Delete);
1395 actions
1396 }
1397 })));
1398
1399 let scopes = Scope::parse_multiple_reduced("atproto repo:* repo:foo.bar").unwrap();
1401 assert_eq!(scopes.len(), 2);
1402 assert!(scopes.contains(&Scope::Atproto));
1403 assert!(scopes.contains(&Scope::Repo(RepoScope {
1404 collection: RepoCollection::All,
1405 actions: {
1406 let mut actions = BTreeSet::new();
1407 actions.insert(RepoAction::Create);
1408 actions.insert(RepoAction::Update);
1409 actions.insert(RepoAction::Delete);
1410 actions
1411 }
1412 })));
1413
1414 let scopes =
1416 Scope::parse_multiple_reduced("account:email account:email?action=manage").unwrap();
1417 assert_eq!(scopes.len(), 1);
1418 assert_eq!(
1419 scopes[0],
1420 Scope::Account(AccountScope {
1421 resource: AccountResource::Email,
1422 action: AccountAction::Manage,
1423 })
1424 );
1425
1426 let scopes = Scope::parse_multiple_reduced("identity:handle identity:*").unwrap();
1428 assert_eq!(scopes.len(), 1);
1429 assert_eq!(scopes[0], Scope::Identity(IdentityScope::All));
1430
1431 let scopes = Scope::parse_multiple_reduced("blob:image/png blob:image/* blob:*/*").unwrap();
1433 assert_eq!(scopes.len(), 1);
1434 let mut accept = BTreeSet::new();
1435 accept.insert(MimePattern::All);
1436 assert_eq!(scopes[0], Scope::Blob(BlobScope { accept }));
1437
1438 let scopes =
1440 Scope::parse_multiple_reduced("account:email identity:handle blob:image/png").unwrap();
1441 assert_eq!(scopes.len(), 3);
1442
1443 let scopes =
1445 Scope::parse_multiple_reduced("repo:foo.bar?action=create repo:foo.bar").unwrap();
1446 assert_eq!(scopes.len(), 1);
1447 assert_eq!(
1448 scopes[0],
1449 Scope::Repo(RepoScope {
1450 collection: RepoCollection::Nsid("foo.bar".to_string()),
1451 actions: {
1452 let mut actions = BTreeSet::new();
1453 actions.insert(RepoAction::Create);
1454 actions.insert(RepoAction::Update);
1455 actions.insert(RepoAction::Delete);
1456 actions
1457 }
1458 })
1459 );
1460
1461 let scopes = Scope::parse_multiple_reduced(
1463 "rpc:com.example.service?aud=did:example:123 rpc:com.example.service rpc:*",
1464 )
1465 .unwrap();
1466 assert_eq!(scopes.len(), 1);
1467 assert_eq!(
1468 scopes[0],
1469 Scope::Rpc(RpcScope {
1470 lxm: {
1471 let mut lxm = BTreeSet::new();
1472 lxm.insert(RpcLexicon::All);
1473 lxm
1474 },
1475 aud: {
1476 let mut aud = BTreeSet::new();
1477 aud.insert(RpcAudience::All);
1478 aud
1479 }
1480 })
1481 );
1482
1483 let scopes = Scope::parse_multiple_reduced("atproto atproto atproto").unwrap();
1485 assert_eq!(scopes.len(), 1);
1486 assert_eq!(scopes[0], Scope::Atproto);
1487
1488 let scopes = Scope::parse_multiple_reduced("transition:generic transition:email").unwrap();
1490 assert_eq!(scopes.len(), 2);
1491 assert!(scopes.contains(&Scope::Transition(TransitionScope::Generic)));
1492 assert!(scopes.contains(&Scope::Transition(TransitionScope::Email)));
1493
1494 let scopes = Scope::parse_multiple_reduced("").unwrap();
1496 assert_eq!(scopes.len(), 0);
1497
1498 let scopes = Scope::parse_multiple_reduced(
1500 "account:email?action=manage account:email account:repo account:repo?action=read identity:* identity:handle"
1501 ).unwrap();
1502 assert_eq!(scopes.len(), 3);
1503 assert!(scopes.contains(&Scope::Account(AccountScope {
1505 resource: AccountResource::Email,
1506 action: AccountAction::Manage,
1507 })));
1508 assert!(scopes.contains(&Scope::Account(AccountScope {
1509 resource: AccountResource::Repo,
1510 action: AccountAction::Read,
1511 })));
1512 assert!(scopes.contains(&Scope::Identity(IdentityScope::All)));
1513
1514 let scopes = Scope::parse_multiple_reduced("atproto account:email repo:*").unwrap();
1516 assert_eq!(scopes.len(), 3);
1517 assert!(scopes.contains(&Scope::Atproto));
1518 assert!(scopes.contains(&Scope::Account(AccountScope {
1519 resource: AccountResource::Email,
1520 action: AccountAction::Read,
1521 })));
1522 assert!(scopes.contains(&Scope::Repo(RepoScope {
1523 collection: RepoCollection::All,
1524 actions: {
1525 let mut actions = BTreeSet::new();
1526 actions.insert(RepoAction::Create);
1527 actions.insert(RepoAction::Update);
1528 actions.insert(RepoAction::Delete);
1529 actions
1530 }
1531 })));
1532 }
1533
1534 #[test]
1535 fn test_openid_connect_scope_parsing() {
1536 let scope = Scope::parse("openid").unwrap();
1538 assert_eq!(scope, Scope::OpenId);
1539
1540 let scope = Scope::parse("profile").unwrap();
1542 assert_eq!(scope, Scope::Profile);
1543
1544 let scope = Scope::parse("email").unwrap();
1546 assert_eq!(scope, Scope::Email);
1547
1548 assert!(Scope::parse("openid:something").is_err());
1550 assert!(Scope::parse("profile:something").is_err());
1551 assert!(Scope::parse("email:something").is_err());
1552
1553 assert!(Scope::parse("openid?param=value").is_err());
1555 assert!(Scope::parse("profile?param=value").is_err());
1556 assert!(Scope::parse("email?param=value").is_err());
1557 }
1558
1559 #[test]
1560 fn test_openid_connect_scope_normalization() {
1561 let scope = Scope::parse("openid").unwrap();
1562 assert_eq!(scope.to_string_normalized(), "openid");
1563
1564 let scope = Scope::parse("profile").unwrap();
1565 assert_eq!(scope.to_string_normalized(), "profile");
1566
1567 let scope = Scope::parse("email").unwrap();
1568 assert_eq!(scope.to_string_normalized(), "email");
1569 }
1570
1571 #[test]
1572 fn test_openid_connect_scope_grants() {
1573 let openid = Scope::parse("openid").unwrap();
1574 let profile = Scope::parse("profile").unwrap();
1575 let email = Scope::parse("email").unwrap();
1576 let account = Scope::parse("account:email").unwrap();
1577
1578 assert!(openid.grants(&openid));
1580 assert!(!openid.grants(&profile));
1581 assert!(!openid.grants(&email));
1582 assert!(!openid.grants(&account));
1583
1584 assert!(profile.grants(&profile));
1585 assert!(!profile.grants(&openid));
1586 assert!(!profile.grants(&email));
1587 assert!(!profile.grants(&account));
1588
1589 assert!(email.grants(&email));
1590 assert!(!email.grants(&openid));
1591 assert!(!email.grants(&profile));
1592 assert!(!email.grants(&account));
1593
1594 assert!(!account.grants(&openid));
1596 assert!(!account.grants(&profile));
1597 assert!(!account.grants(&email));
1598 }
1599
1600 #[test]
1601 fn test_parse_multiple_with_openid_connect() {
1602 let scopes = Scope::parse_multiple("openid profile email atproto").unwrap();
1603 assert_eq!(scopes.len(), 4);
1604 assert_eq!(scopes[0], Scope::OpenId);
1605 assert_eq!(scopes[1], Scope::Profile);
1606 assert_eq!(scopes[2], Scope::Email);
1607 assert_eq!(scopes[3], Scope::Atproto);
1608
1609 let scopes = Scope::parse_multiple("openid account:email profile repo:*").unwrap();
1611 assert_eq!(scopes.len(), 4);
1612 assert!(scopes.contains(&Scope::OpenId));
1613 assert!(scopes.contains(&Scope::Profile));
1614 }
1615
1616 #[test]
1617 fn test_parse_multiple_reduced_with_openid_connect() {
1618 let scopes = Scope::parse_multiple_reduced("openid profile email openid").unwrap();
1620 assert_eq!(scopes.len(), 3);
1621 assert!(scopes.contains(&Scope::OpenId));
1622 assert!(scopes.contains(&Scope::Profile));
1623 assert!(scopes.contains(&Scope::Email));
1624
1625 let scopes = Scope::parse_multiple_reduced(
1627 "openid account:email account:email?action=manage profile",
1628 )
1629 .unwrap();
1630 assert_eq!(scopes.len(), 3);
1631 assert!(scopes.contains(&Scope::OpenId));
1632 assert!(scopes.contains(&Scope::Profile));
1633 assert!(scopes.contains(&Scope::Account(AccountScope {
1634 resource: AccountResource::Email,
1635 action: AccountAction::Manage,
1636 })));
1637 }
1638
1639 #[test]
1640 fn test_serialize_multiple() {
1641 let scopes: Vec<Scope> = vec![];
1643 assert_eq!(Scope::serialize_multiple(&scopes), "");
1644
1645 let scopes = vec![Scope::Atproto];
1647 assert_eq!(Scope::serialize_multiple(&scopes), "atproto");
1648
1649 let scopes = vec![
1651 Scope::parse("repo:*").unwrap(),
1652 Scope::Atproto,
1653 Scope::parse("account:email").unwrap(),
1654 ];
1655 assert_eq!(
1656 Scope::serialize_multiple(&scopes),
1657 "account:email atproto repo:*"
1658 );
1659
1660 let scopes = vec![
1662 Scope::parse("identity:handle").unwrap(),
1663 Scope::parse("blob:image/png").unwrap(),
1664 Scope::parse("account:repo?action=manage").unwrap(),
1665 ];
1666 assert_eq!(
1667 Scope::serialize_multiple(&scopes),
1668 "account:repo?action=manage blob:image/png identity:handle"
1669 );
1670
1671 let scopes = vec![Scope::Email, Scope::OpenId, Scope::Profile, Scope::Atproto];
1673 assert_eq!(
1674 Scope::serialize_multiple(&scopes),
1675 "atproto email openid profile"
1676 );
1677
1678 let scopes = vec![
1680 Scope::parse("rpc:com.example.service?aud=did:example:123&lxm=com.example.method")
1681 .unwrap(),
1682 Scope::parse("repo:foo.bar?action=create&action=update").unwrap(),
1683 Scope::parse("blob:image/*?accept=image/png&accept=image/jpeg").unwrap(),
1684 ];
1685 let result = Scope::serialize_multiple(&scopes);
1686 assert!(result.starts_with("blob:"));
1689 assert!(result.contains(" repo:"));
1690 assert!(result.contains("rpc?aud=did:example:123&lxm=com.example.service"));
1691
1692 let scopes = vec![
1694 Scope::Transition(TransitionScope::Email),
1695 Scope::Transition(TransitionScope::Generic),
1696 Scope::Atproto,
1697 ];
1698 assert_eq!(
1699 Scope::serialize_multiple(&scopes),
1700 "atproto transition:email transition:generic"
1701 );
1702
1703 let scopes = vec![
1705 Scope::Atproto,
1706 Scope::Atproto,
1707 Scope::parse("account:email").unwrap(),
1708 ];
1709 assert_eq!(
1710 Scope::serialize_multiple(&scopes),
1711 "account:email atproto atproto"
1712 );
1713
1714 let scopes = vec![Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap()];
1716 assert_eq!(
1718 Scope::serialize_multiple(&scopes),
1719 "blob?accept=image/jpeg&accept=image/png"
1720 );
1721 }
1722
1723 #[test]
1724 fn test_serialize_multiple_roundtrip() {
1725 let original = "account:email atproto blob:image/png identity:handle repo:*";
1727 let scopes = Scope::parse_multiple(original).unwrap();
1728 let serialized = Scope::serialize_multiple(&scopes);
1729 assert_eq!(serialized, original);
1730
1731 let original = "account:repo?action=manage blob?accept=image/jpeg&accept=image/png rpc:*";
1733 let scopes = Scope::parse_multiple(original).unwrap();
1734 let serialized = Scope::serialize_multiple(&scopes);
1735 let reparsed = Scope::parse_multiple(&serialized).unwrap();
1737 assert_eq!(scopes, reparsed);
1738
1739 let original = "email openid profile";
1741 let scopes = Scope::parse_multiple(original).unwrap();
1742 let serialized = Scope::serialize_multiple(&scopes);
1743 assert_eq!(serialized, original);
1744 }
1745
1746 #[test]
1747 fn test_remove_scope() {
1748 let scopes = vec![
1750 Scope::parse("repo:*").unwrap(),
1751 Scope::Atproto,
1752 Scope::parse("account:email").unwrap(),
1753 ];
1754 let to_remove = Scope::Atproto;
1755 let result = Scope::remove_scope(&scopes, &to_remove);
1756 assert_eq!(result.len(), 2);
1757 assert!(!result.contains(&to_remove));
1758 assert!(result.contains(&Scope::parse("repo:*").unwrap()));
1759 assert!(result.contains(&Scope::parse("account:email").unwrap()));
1760
1761 let scopes = vec![
1763 Scope::parse("repo:*").unwrap(),
1764 Scope::parse("account:email").unwrap(),
1765 ];
1766 let to_remove = Scope::parse("identity:handle").unwrap();
1767 let result = Scope::remove_scope(&scopes, &to_remove);
1768 assert_eq!(result.len(), 2);
1769 assert_eq!(result, scopes);
1770
1771 let scopes: Vec<Scope> = vec![];
1773 let to_remove = Scope::Atproto;
1774 let result = Scope::remove_scope(&scopes, &to_remove);
1775 assert_eq!(result.len(), 0);
1776
1777 let scopes = vec![
1779 Scope::Atproto,
1780 Scope::parse("account:email").unwrap(),
1781 Scope::Atproto,
1782 Scope::parse("repo:*").unwrap(),
1783 Scope::Atproto,
1784 ];
1785 let to_remove = Scope::Atproto;
1786 let result = Scope::remove_scope(&scopes, &to_remove);
1787 assert_eq!(result.len(), 2);
1788 assert!(!result.contains(&to_remove));
1789 assert!(result.contains(&Scope::parse("account:email").unwrap()));
1790 assert!(result.contains(&Scope::parse("repo:*").unwrap()));
1791
1792 let scopes = vec![
1794 Scope::parse("account:email?action=manage").unwrap(),
1795 Scope::parse("blob?accept=image/png&accept=image/jpeg").unwrap(),
1796 Scope::parse("rpc:com.example.service?aud=did:example:123").unwrap(),
1797 ];
1798 let to_remove = Scope::parse("blob?accept=image/jpeg&accept=image/png").unwrap(); let result = Scope::remove_scope(&scopes, &to_remove);
1800 assert_eq!(result.len(), 2);
1801 assert!(!result.contains(&to_remove));
1802
1803 let scopes = vec![Scope::OpenId, Scope::Profile, Scope::Email, Scope::Atproto];
1805 let to_remove = Scope::Profile;
1806 let result = Scope::remove_scope(&scopes, &to_remove);
1807 assert_eq!(result.len(), 3);
1808 assert!(!result.contains(&to_remove));
1809 assert!(result.contains(&Scope::OpenId));
1810 assert!(result.contains(&Scope::Email));
1811 assert!(result.contains(&Scope::Atproto));
1812
1813 let scopes = vec![
1815 Scope::Transition(TransitionScope::Generic),
1816 Scope::Transition(TransitionScope::Email),
1817 Scope::Atproto,
1818 ];
1819 let to_remove = Scope::Transition(TransitionScope::Email);
1820 let result = Scope::remove_scope(&scopes, &to_remove);
1821 assert_eq!(result.len(), 2);
1822 assert!(!result.contains(&to_remove));
1823 assert!(result.contains(&Scope::Transition(TransitionScope::Generic)));
1824 assert!(result.contains(&Scope::Atproto));
1825
1826 let scopes = vec![
1828 Scope::parse("account:email").unwrap(),
1829 Scope::parse("account:email?action=manage").unwrap(),
1830 Scope::parse("account:repo").unwrap(),
1831 ];
1832 let to_remove = Scope::parse("account:email").unwrap();
1833 let result = Scope::remove_scope(&scopes, &to_remove);
1834 assert_eq!(result.len(), 2);
1835 assert!(!result.contains(&Scope::parse("account:email").unwrap()));
1836 assert!(result.contains(&Scope::parse("account:email?action=manage").unwrap()));
1837 assert!(result.contains(&Scope::parse("account:repo").unwrap()));
1838 }
1839}