Skip to main content

helios_auth/policy/
mod.rs

1use crate::error::{AuthError, FhirOperation};
2use crate::principal::Principal;
3use crate::scope::SmartPermissions;
4
5/// Checks whether a Principal's SMART scopes authorize a FHIR operation.
6pub struct SmartScopePolicy;
7
8impl SmartScopePolicy {
9    /// Check if the principal is authorized to perform the given operation
10    /// on the given resource type.
11    ///
12    /// Returns `Ok(())` if authorized, or `Err(AuthError::Forbidden)` if not.
13    pub fn check(
14        principal: &Principal,
15        resource_type: &str,
16        operation: FhirOperation,
17    ) -> Result<(), AuthError> {
18        let permission = Self::operation_to_permission(operation);
19
20        if principal.scopes.is_permitted(resource_type, permission) {
21            Ok(())
22        } else {
23            Err(AuthError::Forbidden {
24                resource_type: resource_type.to_string(),
25                operation: operation.to_string(),
26            })
27        }
28    }
29
30    /// Map a FHIR operation to the corresponding SMART permission bit.
31    fn operation_to_permission(operation: FhirOperation) -> SmartPermissions {
32        match operation {
33            FhirOperation::Read => SmartPermissions::READ,
34            FhirOperation::Search => SmartPermissions::SEARCH,
35            FhirOperation::Create => SmartPermissions::CREATE,
36            FhirOperation::Update => SmartPermissions::UPDATE,
37            FhirOperation::Delete => SmartPermissions::DELETE,
38        }
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::scope::ScopeSet;
46    use chrono::Utc;
47
48    fn make_principal(scope_str: &str) -> Principal {
49        Principal {
50            subject: "test-client".to_string(),
51            issuer: "https://idp.example.com".to_string(),
52            tenant_id: Some("tenant-1".to_string()),
53            scopes: ScopeSet::parse(scope_str),
54            jti: None,
55            expires_at: Utc::now() + chrono::Duration::hours(1),
56            custom_claims: serde_json::Map::new(),
57        }
58    }
59
60    #[test]
61    fn test_read_permitted() {
62        let principal = make_principal("system/Patient.rs");
63        assert!(SmartScopePolicy::check(&principal, "Patient", FhirOperation::Read).is_ok());
64    }
65
66    #[test]
67    fn test_search_permitted() {
68        let principal = make_principal("system/Patient.rs");
69        assert!(SmartScopePolicy::check(&principal, "Patient", FhirOperation::Search).is_ok());
70    }
71
72    #[test]
73    fn test_create_denied() {
74        let principal = make_principal("system/Patient.rs");
75        assert!(SmartScopePolicy::check(&principal, "Patient", FhirOperation::Create).is_err());
76    }
77
78    #[test]
79    fn test_wrong_resource_type() {
80        let principal = make_principal("system/Patient.rs");
81        assert!(SmartScopePolicy::check(&principal, "Observation", FhirOperation::Read).is_err());
82    }
83
84    #[test]
85    fn test_wildcard_full_access() {
86        let principal = make_principal("system/*.cruds");
87        assert!(SmartScopePolicy::check(&principal, "Patient", FhirOperation::Create).is_ok());
88        assert!(SmartScopePolicy::check(&principal, "Observation", FhirOperation::Delete).is_ok());
89        assert!(SmartScopePolicy::check(&principal, "Condition", FhirOperation::Search).is_ok());
90    }
91
92    #[test]
93    fn test_multiple_scopes() {
94        let principal = make_principal("system/Patient.rs system/Observation.crud");
95        assert!(SmartScopePolicy::check(&principal, "Patient", FhirOperation::Read).is_ok());
96        assert!(SmartScopePolicy::check(&principal, "Patient", FhirOperation::Create).is_err());
97        assert!(SmartScopePolicy::check(&principal, "Observation", FhirOperation::Create).is_ok());
98        assert!(SmartScopePolicy::check(&principal, "Observation", FhirOperation::Search).is_err());
99    }
100
101    #[test]
102    fn test_empty_scopes_deny_all() {
103        let principal = make_principal("");
104        assert!(SmartScopePolicy::check(&principal, "Patient", FhirOperation::Read).is_err());
105    }
106}