Skip to main content

helios_persistence/tenant/
permissions.rs

1//! Tenant permission types.
2//!
3//! This module defines the permission model for tenant operations, controlling
4//! what actions a tenant context is allowed to perform.
5
6use std::collections::HashSet;
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11/// Operations that can be performed on resources.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum Operation {
15    /// Create new resources.
16    Create,
17    /// Read existing resources.
18    Read,
19    /// Update existing resources.
20    Update,
21    /// Delete resources (soft or hard).
22    Delete,
23    /// Read resource history.
24    History,
25    /// Search for resources.
26    Search,
27    /// Execute transactions/batches.
28    Transaction,
29    /// Perform bulk operations (export, import).
30    Bulk,
31}
32
33impl fmt::Display for Operation {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Operation::Create => write!(f, "create"),
37            Operation::Read => write!(f, "read"),
38            Operation::Update => write!(f, "update"),
39            Operation::Delete => write!(f, "delete"),
40            Operation::History => write!(f, "history"),
41            Operation::Search => write!(f, "search"),
42            Operation::Transaction => write!(f, "transaction"),
43            Operation::Bulk => write!(f, "bulk"),
44        }
45    }
46}
47
48/// Permissions granted to a tenant context.
49///
50/// `TenantPermissions` controls what operations a tenant can perform and
51/// on which resource types. Permissions can be:
52///
53/// - **Full access**: All operations on all resource types
54/// - **Operation-limited**: Only specific operations allowed
55/// - **Resource-limited**: Only specific resource types allowed
56/// - **Compartment-limited**: Only resources within a specific compartment
57///
58/// # Examples
59///
60/// ```
61/// use helios_persistence::tenant::{TenantPermissions, Operation};
62///
63/// // Full access
64/// let full = TenantPermissions::full_access();
65/// assert!(full.can_perform(Operation::Create, "Patient"));
66///
67/// // Read-only access
68/// let read_only = TenantPermissions::read_only();
69/// assert!(read_only.can_perform(Operation::Read, "Patient"));
70/// assert!(!read_only.can_perform(Operation::Create, "Patient"));
71///
72/// // Custom permissions
73/// let custom = TenantPermissions::builder()
74///     .allow_operations(vec![Operation::Read, Operation::Search])
75///     .allow_resource_types(vec!["Patient", "Observation"])
76///     .build();
77/// ```
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TenantPermissions {
80    /// Allowed operations. If None, all operations are allowed.
81    allowed_operations: Option<HashSet<Operation>>,
82
83    /// Allowed resource types. If None, all resource types are allowed.
84    allowed_resource_types: Option<HashSet<String>>,
85
86    /// Compartment restrictions. If Some, only resources in the specified
87    /// compartment are accessible.
88    compartment: Option<CompartmentRestriction>,
89
90    /// Whether this tenant can access system tenant resources.
91    can_access_system_tenant: bool,
92
93    /// Whether this tenant can access child tenant resources.
94    can_access_child_tenants: bool,
95}
96
97/// Restricts access to resources within a specific compartment.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct CompartmentRestriction {
100    /// The compartment type (e.g., "Patient", "Practitioner").
101    pub compartment_type: String,
102    /// The compartment owner resource ID.
103    pub compartment_id: String,
104}
105
106impl TenantPermissions {
107    /// Creates permissions with full access to all operations and resource types.
108    pub fn full_access() -> Self {
109        Self {
110            allowed_operations: None,
111            allowed_resource_types: None,
112            compartment: None,
113            can_access_system_tenant: true,
114            can_access_child_tenants: false,
115        }
116    }
117
118    /// Creates read-only permissions (read, history, search only).
119    pub fn read_only() -> Self {
120        let mut ops = HashSet::new();
121        ops.insert(Operation::Read);
122        ops.insert(Operation::History);
123        ops.insert(Operation::Search);
124
125        Self {
126            allowed_operations: Some(ops),
127            allowed_resource_types: None,
128            compartment: None,
129            can_access_system_tenant: true,
130            can_access_child_tenants: false,
131        }
132    }
133
134    /// Creates a builder for custom permissions.
135    pub fn builder() -> TenantPermissionsBuilder {
136        TenantPermissionsBuilder::new()
137    }
138
139    /// Returns `true` if the given operation is permitted on the given resource type.
140    pub fn can_perform(&self, operation: Operation, resource_type: &str) -> bool {
141        // Check operation permission
142        if let Some(ref allowed_ops) = self.allowed_operations {
143            if !allowed_ops.contains(&operation) {
144                return false;
145            }
146        }
147
148        // Check resource type permission
149        if let Some(ref allowed_types) = self.allowed_resource_types {
150            if !allowed_types.contains(resource_type) {
151                return false;
152            }
153        }
154
155        true
156    }
157
158    /// Returns `true` if access to system tenant resources is allowed.
159    pub fn can_access_system_tenant(&self) -> bool {
160        self.can_access_system_tenant
161    }
162
163    /// Returns `true` if access to child tenant resources is allowed.
164    pub fn can_access_child_tenants(&self) -> bool {
165        self.can_access_child_tenants
166    }
167
168    /// Returns the compartment restriction, if any.
169    pub fn compartment(&self) -> Option<&CompartmentRestriction> {
170        self.compartment.as_ref()
171    }
172
173    /// Returns the set of allowed operations, or None if all are allowed.
174    pub fn allowed_operations(&self) -> Option<&HashSet<Operation>> {
175        self.allowed_operations.as_ref()
176    }
177
178    /// Returns the set of allowed resource types, or None if all are allowed.
179    pub fn allowed_resource_types(&self) -> Option<&HashSet<String>> {
180        self.allowed_resource_types.as_ref()
181    }
182}
183
184impl Default for TenantPermissions {
185    fn default() -> Self {
186        Self::full_access()
187    }
188}
189
190/// Builder for creating custom tenant permissions.
191#[derive(Default)]
192pub struct TenantPermissionsBuilder {
193    allowed_operations: Option<HashSet<Operation>>,
194    allowed_resource_types: Option<HashSet<String>>,
195    compartment: Option<CompartmentRestriction>,
196    can_access_system_tenant: bool,
197    can_access_child_tenants: bool,
198}
199
200impl TenantPermissionsBuilder {
201    /// Creates a new builder with no permissions.
202    pub fn new() -> Self {
203        Self {
204            allowed_operations: None,
205            allowed_resource_types: None,
206            compartment: None,
207            can_access_system_tenant: true,
208            can_access_child_tenants: false,
209        }
210    }
211
212    /// Sets the allowed operations.
213    pub fn allow_operations(mut self, operations: Vec<Operation>) -> Self {
214        self.allowed_operations = Some(operations.into_iter().collect());
215        self
216    }
217
218    /// Sets the allowed resource types.
219    pub fn allow_resource_types(mut self, types: Vec<&str>) -> Self {
220        self.allowed_resource_types = Some(types.into_iter().map(String::from).collect());
221        self
222    }
223
224    /// Restricts access to a specific compartment.
225    pub fn restrict_to_compartment(mut self, compartment_type: &str, compartment_id: &str) -> Self {
226        self.compartment = Some(CompartmentRestriction {
227            compartment_type: compartment_type.to_string(),
228            compartment_id: compartment_id.to_string(),
229        });
230        self
231    }
232
233    /// Sets whether system tenant resources can be accessed.
234    pub fn can_access_system_tenant(mut self, can_access: bool) -> Self {
235        self.can_access_system_tenant = can_access;
236        self
237    }
238
239    /// Sets whether child tenant resources can be accessed.
240    pub fn can_access_child_tenants(mut self, can_access: bool) -> Self {
241        self.can_access_child_tenants = can_access;
242        self
243    }
244
245    /// Builds the permissions.
246    pub fn build(self) -> TenantPermissions {
247        TenantPermissions {
248            allowed_operations: self.allowed_operations,
249            allowed_resource_types: self.allowed_resource_types,
250            compartment: self.compartment,
251            can_access_system_tenant: self.can_access_system_tenant,
252            can_access_child_tenants: self.can_access_child_tenants,
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_full_access() {
263        let perms = TenantPermissions::full_access();
264        assert!(perms.can_perform(Operation::Create, "Patient"));
265        assert!(perms.can_perform(Operation::Read, "Observation"));
266        assert!(perms.can_perform(Operation::Delete, "Encounter"));
267        assert!(perms.can_access_system_tenant());
268    }
269
270    #[test]
271    fn test_read_only() {
272        let perms = TenantPermissions::read_only();
273        assert!(perms.can_perform(Operation::Read, "Patient"));
274        assert!(perms.can_perform(Operation::Search, "Observation"));
275        assert!(perms.can_perform(Operation::History, "Encounter"));
276        assert!(!perms.can_perform(Operation::Create, "Patient"));
277        assert!(!perms.can_perform(Operation::Update, "Patient"));
278        assert!(!perms.can_perform(Operation::Delete, "Patient"));
279    }
280
281    #[test]
282    fn test_custom_permissions() {
283        let perms = TenantPermissions::builder()
284            .allow_operations(vec![Operation::Read, Operation::Search])
285            .allow_resource_types(vec!["Patient", "Observation"])
286            .build();
287
288        // Allowed
289        assert!(perms.can_perform(Operation::Read, "Patient"));
290        assert!(perms.can_perform(Operation::Search, "Observation"));
291
292        // Not allowed - wrong operation
293        assert!(!perms.can_perform(Operation::Create, "Patient"));
294
295        // Not allowed - wrong resource type
296        assert!(!perms.can_perform(Operation::Read, "Encounter"));
297    }
298
299    #[test]
300    fn test_compartment_restriction() {
301        let perms = TenantPermissions::builder()
302            .restrict_to_compartment("Patient", "123")
303            .build();
304
305        let compartment = perms.compartment().unwrap();
306        assert_eq!(compartment.compartment_type, "Patient");
307        assert_eq!(compartment.compartment_id, "123");
308    }
309
310    #[test]
311    fn test_operation_display() {
312        assert_eq!(Operation::Create.to_string(), "create");
313        assert_eq!(Operation::Read.to_string(), "read");
314        assert_eq!(Operation::Update.to_string(), "update");
315        assert_eq!(Operation::Delete.to_string(), "delete");
316    }
317
318    #[test]
319    fn test_child_tenant_access() {
320        let perms = TenantPermissions::builder()
321            .can_access_child_tenants(true)
322            .build();
323        assert!(perms.can_access_child_tenants());
324
325        let perms2 = TenantPermissions::full_access();
326        assert!(!perms2.can_access_child_tenants());
327    }
328}