this repo has no description
1use std::collections::HashSet;
2
3pub struct ScopePreset {
4 pub name: &'static str,
5 pub label: &'static str,
6 pub description: &'static str,
7 pub scopes: &'static str,
8}
9
10pub const SCOPE_PRESETS: &[ScopePreset] = &[
11 ScopePreset {
12 name: "owner",
13 label: "Owner",
14 description: "Full control including delegation management",
15 scopes: "atproto",
16 },
17 ScopePreset {
18 name: "admin",
19 label: "Admin",
20 description: "Manage account settings, post content, upload media",
21 scopes: "atproto repo:* blob:*/* account:*?action=manage",
22 },
23 ScopePreset {
24 name: "editor",
25 label: "Editor",
26 description: "Post content and upload media",
27 scopes: "repo:*?action=create repo:*?action=update repo:*?action=delete blob:*/*",
28 },
29 ScopePreset {
30 name: "viewer",
31 label: "Viewer",
32 description: "Read-only access",
33 scopes: "",
34 },
35];
36
37pub fn intersect_scopes(requested: &str, granted: &str) -> String {
38 if granted.is_empty() {
39 return String::new();
40 }
41
42 let requested_set: HashSet<&str> = requested.split_whitespace().collect();
43 let granted_set: HashSet<&str> = granted.split_whitespace().collect();
44
45 let granted_has_atproto = granted_set.contains("atproto");
46 let requested_has_atproto = requested_set.contains("atproto");
47
48 if granted_has_atproto && requested_has_atproto {
49 return "atproto".to_string();
50 }
51
52 if granted_has_atproto {
53 return requested_set.into_iter().collect::<Vec<_>>().join(" ");
54 }
55
56 if requested_has_atproto {
57 return granted_set.into_iter().collect::<Vec<_>>().join(" ");
58 }
59
60 let mut result: Vec<&str> = Vec::new();
61
62 for requested_scope in &requested_set {
63 if granted_set.contains(requested_scope) {
64 result.push(requested_scope);
65 continue;
66 }
67
68 if let Some(match_result) = find_matching_scope(requested_scope, &granted_set) {
69 result.push(match_result);
70 }
71 }
72
73 result.sort();
74 result.join(" ")
75}
76
77fn find_matching_scope<'a>(requested: &str, granted: &HashSet<&'a str>) -> Option<&'a str> {
78 granted
79 .iter()
80 .find(|&granted_scope| scopes_compatible(granted_scope, requested))
81 .map(|v| v as _)
82}
83
84fn scopes_compatible(granted: &str, requested: &str) -> bool {
85 if granted == requested {
86 return true;
87 }
88
89 let (granted_base, _granted_params) = split_scope(granted);
90 let (requested_base, _requested_params) = split_scope(requested);
91
92 if granted_base.ends_with(":*")
93 && requested_base.starts_with(&granted_base[..granted_base.len() - 1])
94 {
95 return true;
96 }
97
98 if let Some(prefix) = granted_base.strip_suffix(".*")
99 && requested_base.starts_with(prefix)
100 && requested_base.len() > prefix.len()
101 {
102 return true;
103 }
104
105 false
106}
107
108fn split_scope(scope: &str) -> (&str, Option<&str>) {
109 if let Some(idx) = scope.find('?') {
110 (&scope[..idx], Some(&scope[idx + 1..]))
111 } else {
112 (scope, None)
113 }
114}
115
116pub fn validate_delegation_scopes(scopes: &str) -> Result<(), String> {
117 if scopes.is_empty() {
118 return Ok(());
119 }
120
121 for scope in scopes.split_whitespace() {
122 let (base, _) = split_scope(scope);
123
124 if !is_valid_scope_prefix(base) {
125 return Err(format!("Invalid scope: {}", scope));
126 }
127 }
128
129 Ok(())
130}
131
132fn is_valid_scope_prefix(base: &str) -> bool {
133 let valid_prefixes = [
134 "atproto",
135 "repo:",
136 "blob:",
137 "rpc:",
138 "account:",
139 "identity:",
140 "transition:",
141 ];
142
143 for prefix in valid_prefixes {
144 if base == prefix.trim_end_matches(':') || base.starts_with(prefix) {
145 return true;
146 }
147 }
148
149 false
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_intersect_both_atproto() {
158 assert_eq!(intersect_scopes("atproto", "atproto"), "atproto");
159 }
160
161 #[test]
162 fn test_intersect_granted_atproto() {
163 let result = intersect_scopes("repo:* blob:*/*", "atproto");
164 assert!(result.contains("repo:*"));
165 assert!(result.contains("blob:*/*"));
166 }
167
168 #[test]
169 fn test_intersect_requested_atproto() {
170 let result = intersect_scopes("atproto", "repo:* blob:*/*");
171 assert!(result.contains("repo:*"));
172 assert!(result.contains("blob:*/*"));
173 }
174
175 #[test]
176 fn test_intersect_exact_match() {
177 assert_eq!(
178 intersect_scopes("repo:*?action=create", "repo:*?action=create"),
179 "repo:*?action=create"
180 );
181 }
182
183 #[test]
184 fn test_intersect_empty_granted() {
185 assert_eq!(intersect_scopes("atproto", ""), "");
186 }
187
188 #[test]
189 fn test_validate_scopes_valid() {
190 assert!(validate_delegation_scopes("atproto").is_ok());
191 assert!(validate_delegation_scopes("repo:* blob:*/*").is_ok());
192 assert!(validate_delegation_scopes("").is_ok());
193 }
194
195 #[test]
196 fn test_validate_scopes_invalid() {
197 assert!(validate_delegation_scopes("invalid:scope").is_err());
198 }
199}