helios_auth/scope/
smart_v2.rs1use super::permissions::SmartPermissions;
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub enum ScopeContext {
7 System,
9 User,
11 Patient,
13}
14
15impl fmt::Display for ScopeContext {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 ScopeContext::System => write!(f, "system"),
19 ScopeContext::User => write!(f, "user"),
20 ScopeContext::Patient => write!(f, "patient"),
21 }
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub enum ResourceTypeSpec {
28 Specific(String),
30 Wildcard,
32}
33
34impl fmt::Display for ResourceTypeSpec {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 ResourceTypeSpec::Specific(t) => write!(f, "{}", t),
38 ResourceTypeSpec::Wildcard => write!(f, "*"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub struct SmartScope {
53 pub context: ScopeContext,
55 pub resource_type: ResourceTypeSpec,
57 pub permissions: SmartPermissions,
59}
60
61impl SmartScope {
62 pub fn parse(scope_str: &str) -> Option<Self> {
67 let (context_str, rest) = scope_str.split_once('/')?;
69
70 let context = match context_str {
71 "system" => ScopeContext::System,
72 "user" => ScopeContext::User,
73 "patient" => ScopeContext::Patient,
74 _ => return None,
75 };
76
77 let (resource_str, perm_str) = rest.split_once('.')?;
79
80 if resource_str.is_empty() || perm_str.is_empty() {
81 return None;
82 }
83
84 let resource_type = if resource_str == "*" {
85 ResourceTypeSpec::Wildcard
86 } else {
87 ResourceTypeSpec::Specific(resource_str.to_string())
88 };
89
90 let permissions = SmartPermissions::from_permission_str(perm_str)?;
91
92 Some(SmartScope {
93 context,
94 resource_type,
95 permissions,
96 })
97 }
98
99 pub fn permits(&self, resource_type: &str, permission: SmartPermissions) -> bool {
101 let type_matches = match &self.resource_type {
102 ResourceTypeSpec::Wildcard => true,
103 ResourceTypeSpec::Specific(t) => t == resource_type,
104 };
105
106 type_matches && self.permissions.contains(permission)
107 }
108}
109
110impl fmt::Display for SmartScope {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(
113 f,
114 "{}/{}.{}",
115 self.context, self.resource_type, self.permissions
116 )
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_parse_system_patient_rs() {
126 let scope = SmartScope::parse("system/Patient.rs").unwrap();
127 assert_eq!(scope.context, ScopeContext::System);
128 assert_eq!(
129 scope.resource_type,
130 ResourceTypeSpec::Specific("Patient".to_string())
131 );
132 assert_eq!(
133 scope.permissions,
134 SmartPermissions::READ | SmartPermissions::SEARCH
135 );
136 }
137
138 #[test]
139 fn test_parse_wildcard_cruds() {
140 let scope = SmartScope::parse("system/*.cruds").unwrap();
141 assert_eq!(scope.context, ScopeContext::System);
142 assert_eq!(scope.resource_type, ResourceTypeSpec::Wildcard);
143 assert!(scope.permissions.contains(SmartPermissions::CREATE));
144 assert!(scope.permissions.contains(SmartPermissions::READ));
145 assert!(scope.permissions.contains(SmartPermissions::UPDATE));
146 assert!(scope.permissions.contains(SmartPermissions::DELETE));
147 assert!(scope.permissions.contains(SmartPermissions::SEARCH));
148 }
149
150 #[test]
151 fn test_parse_user_context() {
152 let scope = SmartScope::parse("user/Observation.r").unwrap();
153 assert_eq!(scope.context, ScopeContext::User);
154 assert_eq!(
155 scope.resource_type,
156 ResourceTypeSpec::Specific("Observation".to_string())
157 );
158 assert_eq!(scope.permissions, SmartPermissions::READ);
159 }
160
161 #[test]
162 fn test_parse_patient_context() {
163 let scope = SmartScope::parse("patient/MedicationRequest.crud").unwrap();
164 assert_eq!(scope.context, ScopeContext::Patient);
165 }
166
167 #[test]
168 fn test_parse_invalid_scopes() {
169 assert!(SmartScope::parse("").is_none());
170 assert!(SmartScope::parse("system").is_none());
171 assert!(SmartScope::parse("system/").is_none());
172 assert!(SmartScope::parse("system/Patient").is_none());
173 assert!(SmartScope::parse("system/.rs").is_none());
174 assert!(SmartScope::parse("system/Patient.").is_none());
175 assert!(SmartScope::parse("system/Patient.xyz").is_none());
176 assert!(SmartScope::parse("unknown/Patient.rs").is_none());
177 assert!(SmartScope::parse("foo/bar/baz").is_none());
178 }
179
180 #[test]
181 fn test_permits() {
182 let scope = SmartScope::parse("system/Patient.rs").unwrap();
183 assert!(scope.permits("Patient", SmartPermissions::READ));
184 assert!(scope.permits("Patient", SmartPermissions::SEARCH));
185 assert!(!scope.permits("Patient", SmartPermissions::CREATE));
186 assert!(!scope.permits("Observation", SmartPermissions::READ));
187 }
188
189 #[test]
190 fn test_permits_wildcard() {
191 let scope = SmartScope::parse("system/*.rs").unwrap();
192 assert!(scope.permits("Patient", SmartPermissions::READ));
193 assert!(scope.permits("Observation", SmartPermissions::SEARCH));
194 assert!(!scope.permits("Patient", SmartPermissions::DELETE));
195 }
196
197 #[test]
198 fn test_display_roundtrip() {
199 let original = "system/Patient.rs";
200 let scope = SmartScope::parse(original).unwrap();
201 assert_eq!(format!("{}", scope), original);
202 }
203}