1use haste_fhir_model::r4::generated::{resources::ResourceType, terminology::IssueType};
2use haste_fhir_operation_error::OperationOutcomeError;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, PartialEq, Eq, Clone)]
6pub enum OIDCScope {
7 OpenId,
8 Profile,
9 Email,
10 OfflineAccess,
11 OnlineAccess,
12}
13
14impl From<OIDCScope> for String {
15 fn from(value: OIDCScope) -> Self {
16 match value {
17 OIDCScope::OpenId => "openid".to_string(),
18 OIDCScope::Profile => "profile".to_string(),
19 OIDCScope::Email => "email".to_string(),
20 OIDCScope::OfflineAccess => "offline_access".to_string(),
21 OIDCScope::OnlineAccess => "online_access".to_string(),
22 }
23 }
24}
25
26impl TryFrom<&str> for OIDCScope {
27 type Error = OperationOutcomeError;
28
29 fn try_from(value: &str) -> Result<Self, Self::Error> {
30 match value {
31 "openid" => Ok(Self::OpenId),
32 "profile" => Ok(Self::Profile),
33 "email" => Ok(Self::Email),
34 "offline_access" => Ok(Self::OfflineAccess),
35 "online_access" => Ok(Self::OnlineAccess),
36 _ => Err(OperationOutcomeError::error(
37 IssueType::not_supported(),
38 format!("OIDC Scope '{value}' not supported."),
39 )),
40 }
41 }
42}
43
44#[derive(Debug, PartialEq, Eq, Clone)]
45pub struct LaunchSystemScope;
46
47impl From<LaunchSystemScope> for String {
48 fn from(_: LaunchSystemScope) -> Self {
49 "launch".to_string()
50 }
51}
52
53#[derive(Debug, PartialEq, Eq, Clone)]
54pub enum LaunchType {
55 Encounter,
56 Patient,
57}
58
59impl TryFrom<&str> for LaunchType {
60 type Error = OperationOutcomeError;
61
62 fn try_from(value: &str) -> Result<Self, Self::Error> {
63 match value {
64 "encounter" => Ok(LaunchType::Encounter),
65 "patient" => Ok(LaunchType::Patient),
66 _ => Err(OperationOutcomeError::error(
67 IssueType::not_supported(),
68 format!("Launch type '{value}' not supported."),
69 )),
70 }
71 }
72}
73
74#[derive(Debug, PartialEq, Eq, Clone)]
75pub struct LaunchTypeScope {
76 pub launch_type: LaunchType,
77}
78
79impl From<LaunchTypeScope> for String {
80 fn from(value: LaunchTypeScope) -> Self {
81 match value.launch_type {
82 LaunchType::Encounter => "launch/encounter",
83 LaunchType::Patient => "launch/patient",
84 }
85 .to_string()
86 }
87}
88
89#[derive(Debug, PartialEq, Eq, Clone)]
90pub enum SmartResourceScopeUser {
91 User,
92 System,
93 Patient,
94}
95
96impl TryFrom<&str> for SmartResourceScopeUser {
97 type Error = OperationOutcomeError;
98
99 fn try_from(value: &str) -> Result<Self, Self::Error> {
100 match value {
101 "user" => Ok(SmartResourceScopeUser::User),
102 "system" => Ok(SmartResourceScopeUser::System),
103 "patient" => Ok(SmartResourceScopeUser::Patient),
104 _ => Err(OperationOutcomeError::error(
105 IssueType::not_supported(),
106 format!("Smart resource scope level '{value}' not supported."),
107 )),
108 }
109 }
110}
111
112#[derive(Debug, PartialEq, Eq, Clone)]
113pub enum SmartResourceScopeLevel {
114 ResourceType(ResourceType),
115 AllResources,
116}
117
118impl TryFrom<&str> for SmartResourceScopeLevel {
119 type Error = OperationOutcomeError;
120
121 fn try_from(value: &str) -> Result<Self, Self::Error> {
122 match value {
123 "*" => Ok(SmartResourceScopeLevel::AllResources),
124 resource_type => {
125 let resource_type = ResourceType::try_from(value).map_err(|_e| {
126 OperationOutcomeError::error(
127 IssueType::not_supported(),
128 format!(
129 "Smart resource scope resource type '{resource_type}' not supported.",
130 ),
131 )
132 })?;
133 Ok(SmartResourceScopeLevel::ResourceType(resource_type))
134 }
135 }
136 }
137}
138
139impl SmartResourceScopeLevel {
140 #[must_use]
143 pub fn covers(&self, other: &Self) -> bool {
144 match (self, other) {
145 (Self::AllResources, _) => true,
146 (Self::ResourceType(_), Self::AllResources) => false,
147 (Self::ResourceType(held), Self::ResourceType(wanted)) => held == wanted,
148 }
149 }
150}
151
152#[derive(Debug, PartialEq, Eq, Clone)]
153pub enum SmartResourceScopePermission {
154 Create,
155 Read,
156 Update,
157 Delete,
158 Search,
159}
160
161#[derive(Debug, PartialEq, Eq, Clone)]
162pub struct SmartResourceScopePermissions(Vec<SmartResourceScopePermission>);
163
164impl SmartResourceScopePermissions {
165 #[must_use]
166 pub fn new(permissions: Vec<SmartResourceScopePermission>) -> Self {
167 Self(permissions)
168 }
169
170 #[must_use]
171 pub fn has_permission(&self, permission: &SmartResourceScopePermission) -> bool {
172 self.0.contains(permission)
173 }
174
175 pub fn add_permission(&mut self, permission: SmartResourceScopePermission) {
176 if !self.has_permission(&permission) {
177 self.0.push(permission);
178 }
179 }
180
181 pub fn iter(&self) -> std::slice::Iter<'_, SmartResourceScopePermission> {
182 self.0.iter()
183 }
184}
185
186impl<'a> IntoIterator for &'a SmartResourceScopePermissions {
187 type Item = &'a SmartResourceScopePermission;
188 type IntoIter = std::slice::Iter<'a, SmartResourceScopePermission>;
189
190 fn into_iter(self) -> Self::IntoIter {
191 self.0.iter()
192 }
193}
194
195static SMART_RESOURCE_SCOPE_PERMISSION_ORDER: &[char] = &['c', 'r', 'u', 'd', 's'];
196
197impl TryFrom<&str> for SmartResourceScopePermissions {
198 type Error = OperationOutcomeError;
199
200 fn try_from(value: &str) -> Result<Self, Self::Error> {
201 match value {
202 "*" => Ok(SmartResourceScopePermissions::new(vec![
203 SmartResourceScopePermission::Create,
204 SmartResourceScopePermission::Read,
205 SmartResourceScopePermission::Update,
206 SmartResourceScopePermission::Delete,
207 SmartResourceScopePermission::Search,
208 ])),
209 "write" => Ok(SmartResourceScopePermissions::new(vec![
210 SmartResourceScopePermission::Create,
211 SmartResourceScopePermission::Update,
212 SmartResourceScopePermission::Delete,
213 ])),
214 "read" => Ok(SmartResourceScopePermissions::new(vec![
215 SmartResourceScopePermission::Read,
216 SmartResourceScopePermission::Search,
217 ])),
218 methods => {
219 let mut methods_obj = SmartResourceScopePermissions::new(vec![]);
220
221 let mut current_index: i8 = -1;
224 for method in methods.chars() {
225 let found_index = SMART_RESOURCE_SCOPE_PERMISSION_ORDER
226 .iter()
227 .position(|o| *o == method)
228 .map(i8::try_from)
229 .transpose()
230 .map_err(|e| {
231 OperationOutcomeError::error(
232 IssueType::not_supported(),
233 format!("Permission order index overflow {e}"),
234 )
235 })?;
236
237 if found_index <= Some(current_index) || found_index.is_none() {
238 return Err(OperationOutcomeError::error(
239 IssueType::not_supported(),
240 format!(
241 "Invalid scope access type methods: '{method}' not supported or in wrong place must be in 'cruds' order.",
242 ),
243 ));
244 }
245
246 current_index = found_index.unwrap_or(0);
247
248 match method {
249 'c' => {
253 methods_obj.add_permission(SmartResourceScopePermission::Create);
254 }
255 'r' => {
261 methods_obj.add_permission(SmartResourceScopePermission::Read);
262 }
263 'u' => {
269 methods_obj.add_permission(SmartResourceScopePermission::Update);
270 }
271 'd' => {
275 methods_obj.add_permission(SmartResourceScopePermission::Delete);
276 }
277 's' => {
284 methods_obj.add_permission(SmartResourceScopePermission::Search);
285 }
286 _ => {}
287 }
288 }
289
290 Ok(methods_obj)
291 }
292 }
293 }
294}
295#[derive(Debug, PartialEq, Eq, Clone)]
296pub struct SMARTResourceScope {
297 pub user: SmartResourceScopeUser,
298 pub level: SmartResourceScopeLevel,
299 pub permissions: SmartResourceScopePermissions,
300}
301
302impl From<SMARTResourceScope> for String {
303 fn from(value: SMARTResourceScope) -> Self {
304 let user_str = match value.user {
305 SmartResourceScopeUser::User => "user",
306 SmartResourceScopeUser::System => "system",
307 SmartResourceScopeUser::Patient => "patient",
308 };
309
310 let level_str = match value.level {
311 SmartResourceScopeLevel::AllResources => "*".to_string(),
312 SmartResourceScopeLevel::ResourceType(resource_type) => {
313 resource_type.as_ref().to_string()
314 }
315 };
316
317 let mut permissions_str = String::new();
318 if value
319 .permissions
320 .has_permission(&SmartResourceScopePermission::Create)
321 {
322 permissions_str.push('c');
323 }
324 if value
325 .permissions
326 .has_permission(&SmartResourceScopePermission::Read)
327 {
328 permissions_str.push('r');
329 }
330 if value
331 .permissions
332 .has_permission(&SmartResourceScopePermission::Update)
333 {
334 permissions_str.push('u');
335 }
336 if value
337 .permissions
338 .has_permission(&SmartResourceScopePermission::Delete)
339 {
340 permissions_str.push('d');
341 }
342 if value
343 .permissions
344 .has_permission(&SmartResourceScopePermission::Search)
345 {
346 permissions_str.push('s');
347 }
348
349 format!("{user_str}/{level_str}.{permissions_str}")
350 }
351}
352
353#[derive(Debug, PartialEq, Eq, Clone)]
354pub enum SmartScope {
355 LaunchSystem(LaunchSystemScope),
356 LaunchType(LaunchTypeScope),
357 Resource(SMARTResourceScope),
358 FHIRUser,
359}
360
361impl From<SmartScope> for String {
362 fn from(value: SmartScope) -> Self {
363 match value {
364 SmartScope::FHIRUser => "fhirUser".to_string(),
365 SmartScope::LaunchSystem(launch_system) => String::from(launch_system),
366 SmartScope::LaunchType(launch_type) => String::from(launch_type),
367 SmartScope::Resource(resource) => String::from(resource),
368 }
369 }
370}
371
372impl TryFrom<&str> for SmartScope {
373 type Error = OperationOutcomeError;
374 fn try_from(value: &str) -> Result<Self, Self::Error> {
375 match value {
376 "fhirUser" => Ok(SmartScope::FHIRUser),
377 "launch" => Ok(SmartScope::LaunchSystem(LaunchSystemScope)),
378 _ if value.starts_with("launch/") => {
379 let chunks: Vec<&str> = value.split('/').collect();
380 if chunks.len() != 2 {
381 return Err(OperationOutcomeError::error(
382 IssueType::not_supported(),
383 format!("Invalid launch scope: '{value}'."),
384 ));
385 }
386
387 let launch_type = LaunchType::try_from(chunks[1])?;
388
389 Ok(SmartScope::LaunchType(LaunchTypeScope { launch_type }))
390 }
391 _ if value.starts_with("user/")
392 || value.starts_with("system/")
393 || value.starts_with("patient/") =>
394 {
395 let parts: Vec<&str> = value.split('/').collect();
396 if parts.len() != 2 {
397 return Err(OperationOutcomeError::error(
398 IssueType::not_supported(),
399 format!("Invalid smart resource scope: '{value}'."),
400 ));
401 }
402
403 let user = SmartResourceScopeUser::try_from(parts[0])?;
404 let permissions_parts: Vec<&str> = parts[1].split('.').collect();
405 if permissions_parts.len() != 2 {
406 return Err(OperationOutcomeError::error(
407 IssueType::not_supported(),
408 format!("Invalid smart resource scope: '{value}'."),
409 ));
410 }
411
412 let level = SmartResourceScopeLevel::try_from(permissions_parts[0])?;
413 let permissions = SmartResourceScopePermissions::try_from(permissions_parts[1])?;
414
415 Ok(SmartScope::Resource(SMARTResourceScope {
416 user,
417 level,
418 permissions,
419 }))
420 }
421 _ => Err(OperationOutcomeError::error(
422 IssueType::not_supported(),
423 format!("Smart Scope '{value}' not supported."),
424 )),
425 }
426 }
427}
428
429#[derive(Debug, PartialEq, Eq, Clone)]
430pub enum Scope {
431 OIDC(OIDCScope),
432 SMART(SmartScope),
433}
434
435impl TryFrom<&str> for Scope {
436 type Error = OperationOutcomeError;
437
438 fn try_from(value: &str) -> Result<Self, Self::Error> {
439 if let Ok(oidc_scope) = OIDCScope::try_from(value) {
440 Ok(Self::OIDC(oidc_scope))
441 } else {
442 Ok(Self::SMART(SmartScope::try_from(value)?))
443 }
444 }
445}
446
447impl From<Scope> for String {
448 fn from(value: Scope) -> Self {
449 match value {
450 Scope::OIDC(oidc_scope) => String::from(oidc_scope),
451 Scope::SMART(smart_scope) => String::from(smart_scope),
452 }
453 }
454}
455
456#[derive(Debug, Default, PartialEq, Eq, Clone)]
457pub struct Scopes(pub Vec<Scope>);
458
459impl Scopes {
460 #[must_use]
461 pub fn contains_scope(&self, scope: &Scope) -> bool {
462 self.0.contains(scope)
463 }
464
465 #[must_use]
477 pub fn covers_scope(&self, scope: &Scope) -> bool {
478 let Scope::SMART(SmartScope::Resource(wanted)) = scope else {
479 return self.contains_scope(scope);
480 };
481 let reaching = || {
482 self.0.iter().filter_map(|held| match held {
483 Scope::SMART(SmartScope::Resource(held))
484 if held.user == wanted.user && held.level.covers(&wanted.level) =>
485 {
486 Some(&held.permissions)
487 }
488 _ => None,
489 })
490 };
491 wanted
492 .permissions
493 .iter()
494 .all(|permission| reaching().any(|held| held.has_permission(permission)))
495 }
496}
497
498impl TryFrom<&str> for Scopes {
499 type Error = OperationOutcomeError;
500
501 fn try_from(value: &str) -> Result<Self, Self::Error> {
502 let scopes: Result<Vec<Scope>, OperationOutcomeError> =
503 value.split_whitespace().map(Scope::try_from).collect();
504
505 Ok(Scopes(scopes?))
506 }
507}
508
509impl From<String> for Scopes {
511 fn from(value: String) -> Self {
512 Self::try_from(value.as_str()).expect("Invalid scopes string")
513 }
514}
515
516impl<'de> Deserialize<'de> for Scopes {
517 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
518 where
519 D: serde::Deserializer<'de>,
520 {
521 let s = String::deserialize(deserializer)?;
522 Scopes::try_from(s.as_str()).map_err(serde::de::Error::custom)
523 }
524}
525
526impl From<Scopes> for String {
527 fn from(value: Scopes) -> Self {
528 value
529 .0
530 .into_iter()
531 .map(String::from)
532 .collect::<Vec<_>>()
533 .join(" ")
534 }
535}
536
537impl Serialize for Scopes {
538 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
539 where
540 S: serde::Serializer,
541 {
542 serializer.serialize_str(&String::from(self.clone()))
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549 use haste_fhir_model::r4::generated::resources::ResourceType;
550
551 #[test]
552 fn test_multiple_correct() {
553 assert_eq!(
554 Scopes::try_from("openid profile email offline_access launch/patient user/*.*")
555 .unwrap(),
556 Scopes(vec![
557 Scope::OIDC(OIDCScope::OpenId),
558 Scope::OIDC(OIDCScope::Profile),
559 Scope::OIDC(OIDCScope::Email),
560 Scope::OIDC(OIDCScope::OfflineAccess),
561 Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
562 launch_type: LaunchType::Patient,
563 })),
564 Scope::SMART(SmartScope::Resource(SMARTResourceScope {
565 user: SmartResourceScopeUser::User,
566 level: SmartResourceScopeLevel::AllResources,
567 permissions: SmartResourceScopePermissions::new(vec![
568 SmartResourceScopePermission::Create,
569 SmartResourceScopePermission::Read,
570 SmartResourceScopePermission::Update,
571 SmartResourceScopePermission::Delete,
572 SmartResourceScopePermission::Search,
573 ])
574 })),
575 ]),
576 );
577
578 assert_eq!(
579 Scopes::try_from("launch/encounter system/Patient.cud").unwrap(),
580 Scopes(vec![
581 Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
582 launch_type: LaunchType::Encounter,
583 })),
584 Scope::SMART(SmartScope::Resource(SMARTResourceScope {
585 user: SmartResourceScopeUser::System,
586 level: SmartResourceScopeLevel::ResourceType(ResourceType::Patient),
587 permissions: SmartResourceScopePermissions::new(vec![
588 SmartResourceScopePermission::Create,
589 SmartResourceScopePermission::Update,
590 SmartResourceScopePermission::Delete,
591 ])
592 })),
593 ]),
594 );
595 }
596
597 #[test]
598 fn invalid_order() {
599 assert!(Scopes::try_from("launch/encounter system/Patient.duc").is_err());
600 }
601
602 #[test]
603 fn invalid_system() {
604 assert!(Scopes::try_from("launch/encounter sytem/Patient.cud").is_err());
605 }
606
607 #[test]
608 fn unknown_scope() {
609 assert!(Scopes::try_from("badscope sytem/Patient.cud").is_err());
610 }
611
612 #[test]
613 fn test_roundtrip() {
614 assert_eq!(
615 String::from(
616 Scopes::try_from("openid profile email offline_access launch/patient user/*.*")
617 .unwrap()
618 ),
619 "openid profile email offline_access launch/patient user/*.cruds".to_string(),
620 );
621
622 assert_eq!(
623 String::from(Scopes::try_from("launch/encounter system/Patient.cud").unwrap()),
624 "launch/encounter system/Patient.cud".to_string()
625 );
626 }
627}