1pub mod permissions;
2pub mod smart_v2;
3
4pub use permissions::SmartPermissions;
5pub use smart_v2::{ResourceTypeSpec, ScopeContext, SmartScope};
6
7#[derive(Debug, Clone, Default)]
14pub struct ScopeSet {
15 scopes: Vec<SmartScope>,
16 raw: Vec<String>,
17}
18
19impl ScopeSet {
20 pub fn empty() -> Self {
22 Self {
23 scopes: Vec::new(),
24 raw: Vec::new(),
25 }
26 }
27
28 pub fn parse(scope_str: &str) -> Self {
33 let raw: Vec<String> = scope_str.split_whitespace().map(str::to_string).collect();
34 let scopes = raw.iter().filter_map(|s| SmartScope::parse(s)).collect();
35 Self { scopes, raw }
36 }
37
38 pub fn parse_array(scope_strs: &[String]) -> Self {
40 let raw: Vec<String> = scope_strs.to_vec();
41 let scopes = raw.iter().filter_map(|s| SmartScope::parse(s)).collect();
42 Self { scopes, raw }
43 }
44
45 pub fn is_permitted(&self, resource_type: &str, permission: SmartPermissions) -> bool {
47 self.scopes
48 .iter()
49 .any(|scope| scope.permits(resource_type, permission))
50 }
51
52 pub fn scopes(&self) -> &[SmartScope] {
54 &self.scopes
55 }
56
57 pub fn raw(&self) -> &[String] {
59 &self.raw
60 }
61
62 pub fn is_empty(&self) -> bool {
64 self.scopes.is_empty()
65 }
66
67 pub fn has_system_wildcard(&self) -> bool {
71 self.scopes.iter().any(|s| {
72 s.context == ScopeContext::System
73 && matches!(s.resource_type, ResourceTypeSpec::Wildcard)
74 })
75 }
76
77 pub fn grants_operation(&self, name: &str) -> bool {
83 let target = format!("system/{name}");
84 self.raw.iter().any(|s| s == &target) || self.has_system_wildcard()
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_parse_space_delimited() {
94 let set = ScopeSet::parse("system/Patient.rs system/Observation.r openid profile");
95 assert_eq!(set.scopes().len(), 2);
96 assert!(set.is_permitted("Patient", SmartPermissions::READ));
97 assert!(set.is_permitted("Patient", SmartPermissions::SEARCH));
98 assert!(set.is_permitted("Observation", SmartPermissions::READ));
99 assert!(!set.is_permitted("Observation", SmartPermissions::SEARCH));
100 }
101
102 #[test]
103 fn test_parse_array() {
104 let scopes = vec![
105 "system/Patient.rs".to_string(),
106 "system/*.crud".to_string(),
107 "openid".to_string(),
108 ];
109 let set = ScopeSet::parse_array(&scopes);
110 assert_eq!(set.scopes().len(), 2);
111 assert!(set.is_permitted("Patient", SmartPermissions::READ));
112 assert!(set.is_permitted("Condition", SmartPermissions::CREATE));
114 }
115
116 #[test]
117 fn test_empty_scope() {
118 let set = ScopeSet::parse("");
119 assert!(set.is_empty());
120 assert!(!set.is_permitted("Patient", SmartPermissions::READ));
121 }
122
123 #[test]
124 fn test_wildcard_scope() {
125 let set = ScopeSet::parse("system/*.cruds");
126 assert!(set.is_permitted("Patient", SmartPermissions::CREATE));
127 assert!(set.is_permitted("Observation", SmartPermissions::DELETE));
128 assert!(set.is_permitted("Condition", SmartPermissions::SEARCH));
129 }
130
131 #[test]
132 fn test_non_smart_scopes_ignored() {
133 let set = ScopeSet::parse("openid profile email launch/patient");
134 assert!(set.is_empty());
135 }
136
137 #[test]
138 fn test_grants_operation_literal() {
139 let set = ScopeSet::parse("openid system/bulk-submit profile");
140 assert!(set.is_empty());
142 assert!(set.grants_operation("bulk-submit"));
143 assert!(!set.grants_operation("export"));
144 assert!(!set.has_system_wildcard());
145 }
146
147 #[test]
148 fn test_grants_operation_via_wildcard() {
149 let set = ScopeSet::parse("system/*.cruds");
150 assert!(set.has_system_wildcard());
151 assert!(set.grants_operation("bulk-submit"));
153 }
154
155 #[test]
156 fn test_grants_operation_array_claim() {
157 let set = ScopeSet::parse_array(&[
158 "system/bulk-submit".to_string(),
159 "system/Patient.rs".to_string(),
160 ]);
161 assert!(set.grants_operation("bulk-submit"));
162 assert!(set.is_permitted("Patient", SmartPermissions::READ));
163 assert!(!set.has_system_wildcard());
164 }
165
166 #[test]
167 fn test_raw_retained() {
168 let set = ScopeSet::parse("system/Patient.rs system/bulk-submit");
169 assert_eq!(set.raw().len(), 2);
170 }
171}