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
139#[derive(Debug, PartialEq, Eq, Clone)]
140pub enum SmartResourceScopePermission {
141 Create,
142 Read,
143 Update,
144 Delete,
145 Search,
146}
147
148#[derive(Debug, PartialEq, Eq, Clone)]
149pub struct SmartResourceScopePermissions(Vec<SmartResourceScopePermission>);
150
151impl SmartResourceScopePermissions {
152 #[must_use]
153 pub fn new(permissions: Vec<SmartResourceScopePermission>) -> Self {
154 Self(permissions)
155 }
156
157 #[must_use]
158 pub fn has_permission(&self, permission: &SmartResourceScopePermission) -> bool {
159 self.0.contains(permission)
160 }
161
162 pub fn add_permission(&mut self, permission: SmartResourceScopePermission) {
163 if !self.has_permission(&permission) {
164 self.0.push(permission);
165 }
166 }
167}
168
169static SMART_RESOURCE_SCOPE_PERMISSION_ORDER: &[char] = &['c', 'r', 'u', 'd', 's'];
170
171impl TryFrom<&str> for SmartResourceScopePermissions {
172 type Error = OperationOutcomeError;
173
174 fn try_from(value: &str) -> Result<Self, Self::Error> {
175 match value {
176 "*" => Ok(SmartResourceScopePermissions::new(vec![
177 SmartResourceScopePermission::Create,
178 SmartResourceScopePermission::Read,
179 SmartResourceScopePermission::Update,
180 SmartResourceScopePermission::Delete,
181 SmartResourceScopePermission::Search,
182 ])),
183 "write" => Ok(SmartResourceScopePermissions::new(vec![
184 SmartResourceScopePermission::Create,
185 SmartResourceScopePermission::Update,
186 SmartResourceScopePermission::Delete,
187 ])),
188 "read" => Ok(SmartResourceScopePermissions::new(vec![
189 SmartResourceScopePermission::Read,
190 SmartResourceScopePermission::Search,
191 ])),
192 methods => {
193 let mut methods_obj = SmartResourceScopePermissions::new(vec![]);
194
195 let mut current_index: i8 = -1;
198 for method in methods.chars() {
199 let found_index = SMART_RESOURCE_SCOPE_PERMISSION_ORDER
200 .iter()
201 .position(|o| *o == method)
202 .map(i8::try_from)
203 .transpose()
204 .map_err(|e| {
205 OperationOutcomeError::error(
206 IssueType::not_supported(),
207 format!("Permission order index overflow {e}"),
208 )
209 })?;
210
211 if found_index <= Some(current_index) || found_index.is_none() {
212 return Err(OperationOutcomeError::error(
213 IssueType::not_supported(),
214 format!(
215 "Invalid scope access type methods: '{method}' not supported or in wrong place must be in 'cruds' order.",
216 ),
217 ));
218 }
219
220 current_index = found_index.unwrap_or(0);
221
222 match method {
223 'c' => {
227 methods_obj.add_permission(SmartResourceScopePermission::Create);
228 }
229 'r' => {
235 methods_obj.add_permission(SmartResourceScopePermission::Read);
236 }
237 'u' => {
243 methods_obj.add_permission(SmartResourceScopePermission::Update);
244 }
245 'd' => {
249 methods_obj.add_permission(SmartResourceScopePermission::Delete);
250 }
251 's' => {
258 methods_obj.add_permission(SmartResourceScopePermission::Search);
259 }
260 _ => {}
261 }
262 }
263
264 Ok(methods_obj)
265 }
266 }
267 }
268}
269#[derive(Debug, PartialEq, Eq, Clone)]
270pub struct SMARTResourceScope {
271 pub user: SmartResourceScopeUser,
272 pub level: SmartResourceScopeLevel,
273 pub permissions: SmartResourceScopePermissions,
274}
275
276impl From<SMARTResourceScope> for String {
277 fn from(value: SMARTResourceScope) -> Self {
278 let user_str = match value.user {
279 SmartResourceScopeUser::User => "user",
280 SmartResourceScopeUser::System => "system",
281 SmartResourceScopeUser::Patient => "patient",
282 };
283
284 let level_str = match value.level {
285 SmartResourceScopeLevel::AllResources => "*".to_string(),
286 SmartResourceScopeLevel::ResourceType(resource_type) => {
287 resource_type.as_ref().to_string()
288 }
289 };
290
291 let mut permissions_str = String::new();
292 if value
293 .permissions
294 .has_permission(&SmartResourceScopePermission::Create)
295 {
296 permissions_str.push('c');
297 }
298 if value
299 .permissions
300 .has_permission(&SmartResourceScopePermission::Read)
301 {
302 permissions_str.push('r');
303 }
304 if value
305 .permissions
306 .has_permission(&SmartResourceScopePermission::Update)
307 {
308 permissions_str.push('u');
309 }
310 if value
311 .permissions
312 .has_permission(&SmartResourceScopePermission::Delete)
313 {
314 permissions_str.push('d');
315 }
316 if value
317 .permissions
318 .has_permission(&SmartResourceScopePermission::Search)
319 {
320 permissions_str.push('s');
321 }
322
323 format!("{user_str}/{level_str}.{permissions_str}")
324 }
325}
326
327#[derive(Debug, PartialEq, Eq, Clone)]
328pub enum SmartScope {
329 LaunchSystem(LaunchSystemScope),
330 LaunchType(LaunchTypeScope),
331 Resource(SMARTResourceScope),
332 FHIRUser,
333}
334
335impl From<SmartScope> for String {
336 fn from(value: SmartScope) -> Self {
337 match value {
338 SmartScope::FHIRUser => "fhirUser".to_string(),
339 SmartScope::LaunchSystem(launch_system) => String::from(launch_system),
340 SmartScope::LaunchType(launch_type) => String::from(launch_type),
341 SmartScope::Resource(resource) => String::from(resource),
342 }
343 }
344}
345
346impl TryFrom<&str> for SmartScope {
347 type Error = OperationOutcomeError;
348 fn try_from(value: &str) -> Result<Self, Self::Error> {
349 match value {
350 "fhirUser" => Ok(SmartScope::FHIRUser),
351 "launch" => Ok(SmartScope::LaunchSystem(LaunchSystemScope)),
352 _ if value.starts_with("launch/") => {
353 let chunks: Vec<&str> = value.split('/').collect();
354 if chunks.len() != 2 {
355 return Err(OperationOutcomeError::error(
356 IssueType::not_supported(),
357 format!("Invalid launch scope: '{value}'."),
358 ));
359 }
360
361 let launch_type = LaunchType::try_from(chunks[1])?;
362
363 Ok(SmartScope::LaunchType(LaunchTypeScope { launch_type }))
364 }
365 _ if value.starts_with("user/")
366 || value.starts_with("system/")
367 || value.starts_with("patient/") =>
368 {
369 let parts: Vec<&str> = value.split('/').collect();
370 if parts.len() != 2 {
371 return Err(OperationOutcomeError::error(
372 IssueType::not_supported(),
373 format!("Invalid smart resource scope: '{value}'."),
374 ));
375 }
376
377 let user = SmartResourceScopeUser::try_from(parts[0])?;
378 let permissions_parts: Vec<&str> = parts[1].split('.').collect();
379 if permissions_parts.len() != 2 {
380 return Err(OperationOutcomeError::error(
381 IssueType::not_supported(),
382 format!("Invalid smart resource scope: '{value}'."),
383 ));
384 }
385
386 let level = SmartResourceScopeLevel::try_from(permissions_parts[0])?;
387 let permissions = SmartResourceScopePermissions::try_from(permissions_parts[1])?;
388
389 Ok(SmartScope::Resource(SMARTResourceScope {
390 user,
391 level,
392 permissions,
393 }))
394 }
395 _ => Err(OperationOutcomeError::error(
396 IssueType::not_supported(),
397 format!("Smart Scope '{value}' not supported."),
398 )),
399 }
400 }
401}
402
403#[derive(Debug, PartialEq, Eq, Clone)]
404pub enum Scope {
405 OIDC(OIDCScope),
406 SMART(SmartScope),
407}
408
409impl TryFrom<&str> for Scope {
410 type Error = OperationOutcomeError;
411
412 fn try_from(value: &str) -> Result<Self, Self::Error> {
413 if let Ok(oidc_scope) = OIDCScope::try_from(value) {
414 Ok(Self::OIDC(oidc_scope))
415 } else {
416 Ok(Self::SMART(SmartScope::try_from(value)?))
417 }
418 }
419}
420
421impl From<Scope> for String {
422 fn from(value: Scope) -> Self {
423 match value {
424 Scope::OIDC(oidc_scope) => String::from(oidc_scope),
425 Scope::SMART(smart_scope) => String::from(smart_scope),
426 }
427 }
428}
429
430#[derive(Debug, Default, PartialEq, Eq, Clone)]
431pub struct Scopes(pub Vec<Scope>);
432
433impl Scopes {
434 #[must_use]
435 pub fn contains_scope(&self, scope: &Scope) -> bool {
436 self.0.contains(scope)
437 }
438}
439
440impl TryFrom<&str> for Scopes {
441 type Error = OperationOutcomeError;
442
443 fn try_from(value: &str) -> Result<Self, Self::Error> {
444 let scopes: Result<Vec<Scope>, OperationOutcomeError> =
445 value.split_whitespace().map(Scope::try_from).collect();
446
447 Ok(Scopes(scopes?))
448 }
449}
450
451impl From<String> for Scopes {
453 fn from(value: String) -> Self {
454 Self::try_from(value.as_str()).expect("Invalid scopes string")
455 }
456}
457
458impl<'de> Deserialize<'de> for Scopes {
459 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
460 where
461 D: serde::Deserializer<'de>,
462 {
463 let s = String::deserialize(deserializer)?;
464 Scopes::try_from(s.as_str()).map_err(serde::de::Error::custom)
465 }
466}
467
468impl From<Scopes> for String {
469 fn from(value: Scopes) -> Self {
470 value
471 .0
472 .into_iter()
473 .map(String::from)
474 .collect::<Vec<_>>()
475 .join(" ")
476 }
477}
478
479impl Serialize for Scopes {
480 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
481 where
482 S: serde::Serializer,
483 {
484 serializer.serialize_str(&String::from(self.clone()))
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use haste_fhir_model::r4::generated::resources::ResourceType;
492
493 #[test]
494 fn test_multiple_correct() {
495 assert_eq!(
496 Scopes::try_from("openid profile email offline_access launch/patient user/*.*")
497 .unwrap(),
498 Scopes(vec![
499 Scope::OIDC(OIDCScope::OpenId),
500 Scope::OIDC(OIDCScope::Profile),
501 Scope::OIDC(OIDCScope::Email),
502 Scope::OIDC(OIDCScope::OfflineAccess),
503 Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
504 launch_type: LaunchType::Patient,
505 })),
506 Scope::SMART(SmartScope::Resource(SMARTResourceScope {
507 user: SmartResourceScopeUser::User,
508 level: SmartResourceScopeLevel::AllResources,
509 permissions: SmartResourceScopePermissions::new(vec![
510 SmartResourceScopePermission::Create,
511 SmartResourceScopePermission::Read,
512 SmartResourceScopePermission::Update,
513 SmartResourceScopePermission::Delete,
514 SmartResourceScopePermission::Search,
515 ])
516 })),
517 ]),
518 );
519
520 assert_eq!(
521 Scopes::try_from("launch/encounter system/Patient.cud").unwrap(),
522 Scopes(vec![
523 Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
524 launch_type: LaunchType::Encounter,
525 })),
526 Scope::SMART(SmartScope::Resource(SMARTResourceScope {
527 user: SmartResourceScopeUser::System,
528 level: SmartResourceScopeLevel::ResourceType(ResourceType::Patient),
529 permissions: SmartResourceScopePermissions::new(vec![
530 SmartResourceScopePermission::Create,
531 SmartResourceScopePermission::Update,
532 SmartResourceScopePermission::Delete,
533 ])
534 })),
535 ]),
536 );
537 }
538
539 #[test]
540 fn invalid_order() {
541 assert_eq!(
542 Scopes::try_from("launch/encounter system/Patient.duc").is_err(),
543 true
544 );
545 }
546
547 #[test]
548 fn invalid_system() {
549 assert_eq!(
550 Scopes::try_from("launch/encounter sytem/Patient.cud").is_err(),
551 true
552 );
553 }
554
555 #[test]
556 fn unknown_scope() {
557 assert_eq!(
558 Scopes::try_from("badscope sytem/Patient.cud").is_err(),
559 true
560 );
561 }
562
563 #[test]
564 fn test_roundtrip() {
565 assert_eq!(
566 String::from(
567 Scopes::try_from("openid profile email offline_access launch/patient user/*.*")
568 .unwrap()
569 ),
570 "openid profile email offline_access launch/patient user/*.cruds".to_string(),
571 );
572
573 assert_eq!(
574 String::from(Scopes::try_from("launch/encounter system/Patient.cud").unwrap()),
575 "launch/encounter system/Patient.cud".to_string()
576 );
577 }
578}