pub mod permissions;
pub mod smart_v2;
pub use permissions::SmartPermissions;
pub use smart_v2::{ResourceTypeSpec, ScopeContext, SmartScope};
#[derive(Debug, Clone, Default)]
pub struct ScopeSet {
scopes: Vec<SmartScope>,
raw: Vec<String>,
}
impl ScopeSet {
pub fn empty() -> Self {
Self {
scopes: Vec::new(),
raw: Vec::new(),
}
}
pub fn parse(scope_str: &str) -> Self {
let raw: Vec<String> = scope_str.split_whitespace().map(str::to_string).collect();
let scopes = raw.iter().filter_map(|s| SmartScope::parse(s)).collect();
Self { scopes, raw }
}
pub fn parse_array(scope_strs: &[String]) -> Self {
let raw: Vec<String> = scope_strs.to_vec();
let scopes = raw.iter().filter_map(|s| SmartScope::parse(s)).collect();
Self { scopes, raw }
}
pub fn is_permitted(&self, resource_type: &str, permission: SmartPermissions) -> bool {
self.scopes
.iter()
.any(|scope| scope.permits(resource_type, permission))
}
pub fn scopes(&self) -> &[SmartScope] {
&self.scopes
}
pub fn raw(&self) -> &[String] {
&self.raw
}
pub fn is_empty(&self) -> bool {
self.scopes.is_empty()
}
pub fn has_system_wildcard(&self) -> bool {
self.scopes.iter().any(|s| {
s.context == ScopeContext::System
&& matches!(s.resource_type, ResourceTypeSpec::Wildcard)
})
}
pub fn grants_operation(&self, name: &str) -> bool {
let target = format!("system/{name}");
self.raw.iter().any(|s| s == &target) || self.has_system_wildcard()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_space_delimited() {
let set = ScopeSet::parse("system/Patient.rs system/Observation.r openid profile");
assert_eq!(set.scopes().len(), 2);
assert!(set.is_permitted("Patient", SmartPermissions::READ));
assert!(set.is_permitted("Patient", SmartPermissions::SEARCH));
assert!(set.is_permitted("Observation", SmartPermissions::READ));
assert!(!set.is_permitted("Observation", SmartPermissions::SEARCH));
}
#[test]
fn test_parse_array() {
let scopes = vec![
"system/Patient.rs".to_string(),
"system/*.crud".to_string(),
"openid".to_string(),
];
let set = ScopeSet::parse_array(&scopes);
assert_eq!(set.scopes().len(), 2);
assert!(set.is_permitted("Patient", SmartPermissions::READ));
assert!(set.is_permitted("Condition", SmartPermissions::CREATE));
}
#[test]
fn test_empty_scope() {
let set = ScopeSet::parse("");
assert!(set.is_empty());
assert!(!set.is_permitted("Patient", SmartPermissions::READ));
}
#[test]
fn test_wildcard_scope() {
let set = ScopeSet::parse("system/*.cruds");
assert!(set.is_permitted("Patient", SmartPermissions::CREATE));
assert!(set.is_permitted("Observation", SmartPermissions::DELETE));
assert!(set.is_permitted("Condition", SmartPermissions::SEARCH));
}
#[test]
fn test_non_smart_scopes_ignored() {
let set = ScopeSet::parse("openid profile email launch/patient");
assert!(set.is_empty());
}
#[test]
fn test_grants_operation_literal() {
let set = ScopeSet::parse("openid system/bulk-submit profile");
assert!(set.is_empty());
assert!(set.grants_operation("bulk-submit"));
assert!(!set.grants_operation("export"));
assert!(!set.has_system_wildcard());
}
#[test]
fn test_grants_operation_via_wildcard() {
let set = ScopeSet::parse("system/*.cruds");
assert!(set.has_system_wildcard());
assert!(set.grants_operation("bulk-submit"));
}
#[test]
fn test_grants_operation_array_claim() {
let set = ScopeSet::parse_array(&[
"system/bulk-submit".to_string(),
"system/Patient.rs".to_string(),
]);
assert!(set.grants_operation("bulk-submit"));
assert!(set.is_permitted("Patient", SmartPermissions::READ));
assert!(!set.has_system_wildcard());
}
#[test]
fn test_raw_retained() {
let set = ScopeSet::parse("system/Patient.rs system/bulk-submit");
assert_eq!(set.raw().len(), 2);
}
}