use haste_fhir_model::r4::generated::{resources::ResourceType, terminology::IssueType};
use haste_fhir_operation_error::OperationOutcomeError;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum OIDCScope {
OpenId,
Profile,
Email,
OfflineAccess,
OnlineAccess,
}
impl From<OIDCScope> for String {
fn from(value: OIDCScope) -> Self {
match value {
OIDCScope::OpenId => "openid".to_string(),
OIDCScope::Profile => "profile".to_string(),
OIDCScope::Email => "email".to_string(),
OIDCScope::OfflineAccess => "offline_access".to_string(),
OIDCScope::OnlineAccess => "online_access".to_string(),
}
}
}
impl TryFrom<&str> for OIDCScope {
type Error = OperationOutcomeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"openid" => Ok(Self::OpenId),
"profile" => Ok(Self::Profile),
"email" => Ok(Self::Email),
"offline_access" => Ok(Self::OfflineAccess),
"online_access" => Ok(Self::OnlineAccess),
_ => Err(OperationOutcomeError::error(
IssueType::NotSupported(None),
format!("OIDC Scope '{}' not supported.", value),
)),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct LaunchSystemScope;
impl From<LaunchSystemScope> for String {
fn from(_: LaunchSystemScope) -> Self {
"launch".to_string()
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum LaunchType {
Encounter,
Patient,
}
impl TryFrom<&str> for LaunchType {
type Error = OperationOutcomeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"encounter" => Ok(LaunchType::Encounter),
"patient" => Ok(LaunchType::Patient),
_ => Err(OperationOutcomeError::error(
IssueType::NotSupported(None),
format!("Launch type '{}' not supported.", value),
)),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct LaunchTypeScope {
pub launch_type: LaunchType,
}
impl From<LaunchTypeScope> for String {
fn from(value: LaunchTypeScope) -> Self {
match value.launch_type {
LaunchType::Encounter => "launch/encounter".to_string(),
LaunchType::Patient => "launch/patient".to_string(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum SmartResourceScopeUser {
User,
System,
Patient,
}
impl TryFrom<&str> for SmartResourceScopeUser {
type Error = OperationOutcomeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"user" => Ok(SmartResourceScopeUser::User),
"system" => Ok(SmartResourceScopeUser::System),
"patient" => Ok(SmartResourceScopeUser::Patient),
_ => Err(OperationOutcomeError::error(
IssueType::NotSupported(None),
format!("Smart resource scope level '{}' not supported.", value),
)),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum SmartResourceScopeLevel {
ResourceType(ResourceType),
AllResources,
}
impl TryFrom<&str> for SmartResourceScopeLevel {
type Error = OperationOutcomeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"*" => Ok(SmartResourceScopeLevel::AllResources),
resource_type => {
let resource_type = ResourceType::try_from(value).map_err(|_e| {
OperationOutcomeError::error(
IssueType::NotSupported(None),
format!(
"Smart resource scope resource type '{}' not supported.",
resource_type,
),
)
})?;
Ok(SmartResourceScopeLevel::ResourceType(resource_type))
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum SmartResourceScopePermission {
Create,
Read,
Update,
Delete,
Search,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SmartResourceScopePermissions(Vec<SmartResourceScopePermission>);
impl SmartResourceScopePermissions {
pub fn new(permissions: Vec<SmartResourceScopePermission>) -> Self {
Self(permissions)
}
pub fn has_permission(&self, permission: &SmartResourceScopePermission) -> bool {
self.0.contains(permission)
}
pub fn add_permission(&mut self, permission: SmartResourceScopePermission) {
if !self.has_permission(&permission) {
self.0.push(permission);
}
}
}
static SMART_RESOURCE_SCOPE_PERMISSION_ORDER: &[char] = &['c', 'r', 'u', 'd', 's'];
impl TryFrom<&str> for SmartResourceScopePermissions {
type Error = OperationOutcomeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"*" => Ok(SmartResourceScopePermissions::new(vec![
SmartResourceScopePermission::Create,
SmartResourceScopePermission::Read,
SmartResourceScopePermission::Update,
SmartResourceScopePermission::Delete,
SmartResourceScopePermission::Search,
])),
"write" => Ok(SmartResourceScopePermissions::new(vec![
SmartResourceScopePermission::Create,
SmartResourceScopePermission::Update,
SmartResourceScopePermission::Delete,
])),
"read" => Ok(SmartResourceScopePermissions::new(vec![
SmartResourceScopePermission::Read,
SmartResourceScopePermission::Search,
])),
methods => {
let mut methods_obj = SmartResourceScopePermissions::new(vec![]);
let mut current_index: i8 = -1;
for method in methods.chars() {
let found_index = SMART_RESOURCE_SCOPE_PERMISSION_ORDER
.iter()
.position(|o| *o == method)
.map(|p| p as i8);
if found_index <= Some(current_index) || found_index.is_none() {
return Err(OperationOutcomeError::error(
IssueType::NotSupported(None),
format!(
"Invalid scope access type methods: '{}' not supported or in wrong place must be in 'cruds' order.",
method
),
));
}
current_index = found_index.unwrap_or(0);
match method {
'c' => {
methods_obj.add_permission(SmartResourceScopePermission::Create);
}
'r' => {
methods_obj.add_permission(SmartResourceScopePermission::Read);
}
'u' => {
methods_obj.add_permission(SmartResourceScopePermission::Update);
}
'd' => {
methods_obj.add_permission(SmartResourceScopePermission::Delete);
}
's' => {
methods_obj.add_permission(SmartResourceScopePermission::Search);
}
_ => {}
}
}
Ok(methods_obj)
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SMARTResourceScope {
pub user: SmartResourceScopeUser,
pub level: SmartResourceScopeLevel,
pub permissions: SmartResourceScopePermissions,
}
impl From<SMARTResourceScope> for String {
fn from(value: SMARTResourceScope) -> Self {
let user_str = match value.user {
SmartResourceScopeUser::User => "user",
SmartResourceScopeUser::System => "system",
SmartResourceScopeUser::Patient => "patient",
};
let level_str = match value.level {
SmartResourceScopeLevel::AllResources => "*".to_string(),
SmartResourceScopeLevel::ResourceType(resource_type) => {
resource_type.as_ref().to_string()
}
};
let mut permissions_str = String::new();
if value
.permissions
.has_permission(&SmartResourceScopePermission::Create)
{
permissions_str.push('c');
}
if value
.permissions
.has_permission(&SmartResourceScopePermission::Read)
{
permissions_str.push('r');
}
if value
.permissions
.has_permission(&SmartResourceScopePermission::Update)
{
permissions_str.push('u');
}
if value
.permissions
.has_permission(&SmartResourceScopePermission::Delete)
{
permissions_str.push('d');
}
if value
.permissions
.has_permission(&SmartResourceScopePermission::Search)
{
permissions_str.push('s');
}
format!("{}/{}.{}", user_str, level_str, permissions_str)
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum SmartScope {
LaunchSystem(LaunchSystemScope),
LaunchType(LaunchTypeScope),
Resource(SMARTResourceScope),
FHIRUser,
}
impl From<SmartScope> for String {
fn from(value: SmartScope) -> Self {
match value {
SmartScope::FHIRUser => "fhirUser".to_string(),
SmartScope::LaunchSystem(launch_system) => String::from(launch_system),
SmartScope::LaunchType(launch_type) => String::from(launch_type),
SmartScope::Resource(resource) => String::from(resource),
}
}
}
impl TryFrom<&str> for SmartScope {
type Error = OperationOutcomeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"fhirUser" => Ok(SmartScope::FHIRUser),
"launch" => Ok(SmartScope::LaunchSystem(LaunchSystemScope)),
_ if value.starts_with("launch/") => {
let chunks: Vec<&str> = value.split('/').collect();
if chunks.len() != 2 {
return Err(OperationOutcomeError::error(
IssueType::NotSupported(None),
format!("Invalid launch scope: '{}'.", value),
));
}
let launch_type = LaunchType::try_from(chunks[1])?;
Ok(SmartScope::LaunchType(LaunchTypeScope { launch_type }))
}
_ if value.starts_with("user/")
|| value.starts_with("system/")
|| value.starts_with("patient/") =>
{
let parts: Vec<&str> = value.split('/').collect();
if parts.len() != 2 {
return Err(OperationOutcomeError::error(
IssueType::NotSupported(None),
format!("Invalid smart resource scope: '{}'.", value),
));
}
let user = SmartResourceScopeUser::try_from(parts[0])?;
let permissions_parts: Vec<&str> = parts[1].split('.').collect();
if permissions_parts.len() != 2 {
return Err(OperationOutcomeError::error(
IssueType::NotSupported(None),
format!("Invalid smart resource scope: '{}'.", value),
));
}
let level = SmartResourceScopeLevel::try_from(permissions_parts[0])?;
let permissions = SmartResourceScopePermissions::try_from(permissions_parts[1])?;
Ok(SmartScope::Resource(SMARTResourceScope {
user,
level,
permissions,
}))
}
_ => Err(OperationOutcomeError::error(
IssueType::NotSupported(None),
format!("Smart Scope '{}' not supported.", value),
)),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Scope {
OIDC(OIDCScope),
SMART(SmartScope),
}
impl TryFrom<&str> for Scope {
type Error = OperationOutcomeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if let Ok(oidc_scope) = OIDCScope::try_from(value) {
Ok(Self::OIDC(oidc_scope))
} else {
Ok(Self::SMART(SmartScope::try_from(value)?))
}
}
}
impl From<Scope> for String {
fn from(value: Scope) -> Self {
match value {
Scope::OIDC(oidc_scope) => String::from(oidc_scope),
Scope::SMART(smart_scope) => String::from(smart_scope),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Scopes(pub Vec<Scope>);
impl Scopes {
pub fn contains_scope(&self, scope: &Scope) -> bool {
self.0.contains(scope)
}
}
impl Default for Scopes {
fn default() -> Self {
Scopes(vec![])
}
}
impl TryFrom<&str> for Scopes {
type Error = OperationOutcomeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let scopes: Result<Vec<Scope>, OperationOutcomeError> = value
.split_whitespace()
.map(|s| Scope::try_from(s))
.collect();
Ok(Scopes(scopes?))
}
}
impl From<String> for Scopes {
fn from(value: String) -> Self {
let scopes = Self::try_from(value.as_str()).expect("Invalid scopes string");
scopes
}
}
impl<'de> Deserialize<'de> for Scopes {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Scopes::try_from(s.as_str()).map_err(serde::de::Error::custom)
}
}
impl From<Scopes> for String {
fn from(value: Scopes) -> Self {
value
.0
.into_iter()
.map(|s| String::from(s))
.collect::<Vec<_>>()
.join(" ")
}
}
impl Serialize for Scopes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&String::from(self.clone()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use haste_fhir_model::r4::generated::resources::ResourceType;
#[test]
fn test_multiple_correct() {
assert_eq!(
Scopes::try_from("openid profile email offline_access launch/patient user/*.*")
.unwrap(),
Scopes(vec![
Scope::OIDC(OIDCScope::OpenId),
Scope::OIDC(OIDCScope::Profile),
Scope::OIDC(OIDCScope::Email),
Scope::OIDC(OIDCScope::OfflineAccess),
Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
launch_type: LaunchType::Patient,
})),
Scope::SMART(SmartScope::Resource(SMARTResourceScope {
user: SmartResourceScopeUser::User,
level: SmartResourceScopeLevel::AllResources,
permissions: SmartResourceScopePermissions::new(vec![
SmartResourceScopePermission::Create,
SmartResourceScopePermission::Read,
SmartResourceScopePermission::Update,
SmartResourceScopePermission::Delete,
SmartResourceScopePermission::Search,
])
})),
]),
);
assert_eq!(
Scopes::try_from("launch/encounter system/Patient.cud").unwrap(),
Scopes(vec![
Scope::SMART(SmartScope::LaunchType(LaunchTypeScope {
launch_type: LaunchType::Encounter,
})),
Scope::SMART(SmartScope::Resource(SMARTResourceScope {
user: SmartResourceScopeUser::System,
level: SmartResourceScopeLevel::ResourceType(ResourceType::Patient),
permissions: SmartResourceScopePermissions::new(vec![
SmartResourceScopePermission::Create,
SmartResourceScopePermission::Update,
SmartResourceScopePermission::Delete,
])
})),
]),
);
}
#[test]
fn invalid_order() {
assert_eq!(
Scopes::try_from("launch/encounter system/Patient.duc").is_err(),
true
);
}
#[test]
fn invalid_system() {
assert_eq!(
Scopes::try_from("launch/encounter sytem/Patient.cud").is_err(),
true
);
}
#[test]
fn unknown_scope() {
assert_eq!(
Scopes::try_from("badscope sytem/Patient.cud").is_err(),
true
);
}
#[test]
fn test_roundtrip() {
assert_eq!(
String::from(
Scopes::try_from("openid profile email offline_access launch/patient user/*.*")
.unwrap()
),
"openid profile email offline_access launch/patient user/*.cruds".to_string(),
);
assert_eq!(
String::from(Scopes::try_from("launch/encounter system/Patient.cud").unwrap()),
"launch/encounter system/Patient.cud".to_string()
);
}
}