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 for granted_scope in granted {
79 if scopes_compatible(granted_scope, requested) {
80 return Some(granted_scope);
81 }
82 }
83 None
84}
85
86fn scopes_compatible(granted: &str, requested: &str) -> bool {
87 if granted == requested {
88 return true;
89 }
90
91 let (granted_base, _granted_params) = split_scope(granted);
92 let (requested_base, _requested_params) = split_scope(requested);
93
94 if granted_base.ends_with(":*")
95 && requested_base.starts_with(&granted_base[..granted_base.len() - 1])
96 {
97 return true;
98 }
99
100 if granted_base.ends_with(".*") {
101 let prefix = &granted_base[..granted_base.len() - 2];
102 if requested_base.starts_with(prefix) && requested_base.len() > prefix.len() {
103 return true;
104 }
105 }
106
107 false
108}
109
110fn split_scope(scope: &str) -> (&str, Option<&str>) {
111 if let Some(idx) = scope.find('?') {
112 (&scope[..idx], Some(&scope[idx + 1..]))
113 } else {
114 (scope, None)
115 }
116}
117
118pub fn validate_delegation_scopes(scopes: &str) -> Result<(), String> {
119 if scopes.is_empty() {
120 return Ok(());
121 }
122
123 for scope in scopes.split_whitespace() {
124 let (base, _) = split_scope(scope);
125
126 if !is_valid_scope_prefix(base) {
127 return Err(format!("Invalid scope: {}", scope));
128 }
129 }
130
131 Ok(())
132}
133
134fn is_valid_scope_prefix(base: &str) -> bool {
135 let valid_prefixes = [
136 "atproto",
137 "repo:",
138 "blob:",
139 "rpc:",
140 "account:",
141 "identity:",
142 "transition:",
143 ];
144
145 for prefix in valid_prefixes {
146 if base == prefix.trim_end_matches(':') || base.starts_with(prefix) {
147 return true;
148 }
149 }
150
151 false
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_intersect_both_atproto() {
160 assert_eq!(intersect_scopes("atproto", "atproto"), "atproto");
161 }
162
163 #[test]
164 fn test_intersect_granted_atproto() {
165 let result = intersect_scopes("repo:* blob:*/*", "atproto");
166 assert!(result.contains("repo:*"));
167 assert!(result.contains("blob:*/*"));
168 }
169
170 #[test]
171 fn test_intersect_requested_atproto() {
172 let result = intersect_scopes("atproto", "repo:* blob:*/*");
173 assert!(result.contains("repo:*"));
174 assert!(result.contains("blob:*/*"));
175 }
176
177 #[test]
178 fn test_intersect_exact_match() {
179 assert_eq!(
180 intersect_scopes("repo:*?action=create", "repo:*?action=create"),
181 "repo:*?action=create"
182 );
183 }
184
185 #[test]
186 fn test_intersect_empty_granted() {
187 assert_eq!(intersect_scopes("atproto", ""), "");
188 }
189
190 #[test]
191 fn test_validate_scopes_valid() {
192 assert!(validate_delegation_scopes("atproto").is_ok());
193 assert!(validate_delegation_scopes("repo:* blob:*/*").is_ok());
194 assert!(validate_delegation_scopes("").is_ok());
195 }
196
197 #[test]
198 fn test_validate_scopes_invalid() {
199 assert!(validate_delegation_scopes("invalid:scope").is_err());
200 }
201}