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