trace2e_core/traceability/services/
compliance.rs

1//! # Compliance Module
2//!
3//! This module implements the compliance system for traceability policies in the trace2e framework.
4//! It provides policy management and evaluation capabilities to control data flows between resources
5//! based on confidentiality, integrity, deletion status, and consent requirements.
6//!
7//! ## Overview
8//!
9//! The compliance system enforces policies on resources and evaluates whether data flows between
10//! resources are permitted based on these policies. It supports four main policy dimensions:
11//!
12//! - **Confidentiality**: Controls whether data is public or secret
13//! - **Integrity**: Numeric level indicating data trustworthiness (higher = more trusted)
14//! - **Deletion**: Tracks deletion status (not deleted, pending deletion, or deleted)
15//! - **Consent**: Boolean flag indicating whether the resource owner has given consent for flows
16//!
17//! ## Policy Evaluation Rules
18//!
19//! Data flows are permitted only when:
20//! 1. Neither source nor destination is deleted or pending deletion
21//! 2. Source integrity level >= destination integrity level
22//! 3. Secret data cannot flow to public destinations (but public can flow to secret)
23//! 4. Both source and destination have consent (when enforced)
24use std::{
25    collections::{HashMap, HashSet},
26    future::Future,
27    pin::Pin,
28    sync::Arc,
29    task::Poll,
30};
31
32use crate::traceability::infrastructure::naming::DisplayableResource;
33use dashmap::DashMap;
34use tokio::task::JoinSet;
35use tower::Service;
36use tracing::info;
37
38use crate::traceability::{
39    api::types::{ComplianceRequest, ComplianceResponse},
40    error::TraceabilityError,
41    infrastructure::naming::{LocalizedResource, Resource},
42    services::consent::{ConsentRequest, ConsentResponse, ConsentService},
43};
44
45/// Confidentiality policy defines the level of confidentiality of a resource.
46///
47/// This enum controls whether data associated with a resource is considered sensitive
48/// and restricts flows from secret resources to public destinations.
49///
50/// # Flow Rules
51///
52/// - `Public` → `Public`: ✅ Allowed
53/// - `Public` → `Secret`: ✅ Allowed (upgrading confidentiality)
54/// - `Secret` → `Secret`: ✅ Allowed
55/// - `Secret` → `Public`: ❌ Blocked (would leak sensitive data)
56#[derive(Default, PartialEq, Debug, Clone, Copy, Eq)]
57pub enum ConfidentialityPolicy {
58    /// Data that must be kept confidential and cannot flow to public destinations
59    Secret,
60    /// Data that can be shared publicly (default)
61    #[default]
62    Public,
63}
64
65/// Deletion policy defines the deletion status of a resource.
66///
67/// This enum tracks the lifecycle state of a resource with respect to deletion,
68/// supporting a two-phase deletion process where resources are first marked for
69/// deletion and then actually deleted.
70///
71/// # State Transitions
72///
73/// ```text
74/// NotDeleted → Pending → Deleted
75/// ```
76///
77/// # Flow Rules
78///
79/// Resources with `Pending` or `Deleted` status cannot be involved in data flows
80/// (both as source and destination).
81#[derive(Default, PartialEq, Debug, Clone, Eq, Copy)]
82pub enum DeletionPolicy {
83    /// Resource is active and can participate in flows (default)
84    #[default]
85    NotDeleted,
86    /// Resource is marked for deletion but not yet removed
87    Pending,
88    /// Resource has been fully deleted
89    Deleted,
90}
91
92impl From<bool> for DeletionPolicy {
93    fn from(deleted: bool) -> Self {
94        if deleted { DeletionPolicy::Deleted } else { DeletionPolicy::NotDeleted }
95    }
96}
97
98/// Policy for a resource that controls compliance checking for data flows.
99///
100/// A `Policy` combines multiple dimensions of access control and resource management
101/// to determine whether data flows involving a resource should be permitted.
102///
103/// # Fields
104///
105/// - **`confidentiality`**: Controls whether the resource contains sensitive data
106/// - **`integrity`**: Numeric trust level (0 = lowest, higher = more trusted)
107/// - **`deleted`**: Tracks deletion status through a multi-phase process
108/// - **`consent`**: Whether the resource owner consent is required for flows
109///
110/// # Policy Evaluation
111///
112/// When evaluating flows between resources, policies are checked to ensure:
113/// 1. No deleted resources are involved
114/// 2. Integrity levels are compatible (source >= destination)
115/// 3. Confidentiality is preserved (secret data doesn't leak to public)
116/// 4. All parties have given consent
117///
118/// This policy is used to check the compliance of input/output flows of the associated resource.
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct Policy {
121    /// Confidentiality level of the resource
122    confidentiality: ConfidentialityPolicy,
123    /// Integrity level (0 = lowest trust, higher values = more trusted)
124    integrity: u32,
125    /// Deletion status of the resource
126    deleted: DeletionPolicy,
127    /// Whether the resource owner consent is required for flows
128    consent: bool,
129}
130
131impl Default for Policy {
132    fn default() -> Self {
133        Policy {
134            confidentiality: ConfidentialityPolicy::Public,
135            integrity: 0,
136            deleted: DeletionPolicy::NotDeleted,
137            consent: false,
138        }
139    }
140}
141
142impl Policy {
143    /// Creates a new policy with the specified parameters.
144    ///
145    /// # Arguments
146    ///
147    /// * `confidentiality` - The confidentiality level for the resource
148    /// * `integrity` - The integrity level (0 = lowest, higher = more trusted)
149    /// * `deleted` - The deletion status
150    /// * `consent` - Whether the resource owner consent is required for flows
151    pub fn new(
152        confidentiality: ConfidentialityPolicy,
153        integrity: u32,
154        deleted: DeletionPolicy,
155        consent: bool,
156    ) -> Self {
157        Self { confidentiality, integrity, deleted, consent }
158    }
159
160    /// Returns true if the resource contains confidential data.
161    ///
162    /// This is a convenience method that checks if the confidentiality policy
163    /// is set to `Secret`.
164    pub fn is_confidential(&self) -> bool {
165        self.confidentiality == ConfidentialityPolicy::Secret
166    }
167
168    /// Returns true if the resource is deleted or pending deletion.
169    ///
170    /// Resources that are deleted cannot participate in data flows.
171    pub fn is_deleted(&self) -> bool {
172        self.deleted != DeletionPolicy::NotDeleted
173    }
174
175    /// Returns true if the resource is pending deletion.
176    ///
177    /// This indicates the resource has been marked for deletion but hasn't
178    /// been fully removed yet.
179    pub fn is_pending_deletion(&self) -> bool {
180        self.deleted == DeletionPolicy::Pending
181    }
182
183    /// Returns the integrity level of the resource.
184    ///
185    /// Higher values indicate more trusted data. For flows to be permitted,
186    /// the source integrity must be greater than or equal to the destination integrity.
187    pub fn get_integrity(&self) -> u32 {
188        self.integrity
189    }
190
191    /// Returns true if the resource owner has given consent for flows.
192    ///
193    /// When consent is false, flows involving this resource should be denied.
194    pub fn get_consent(&self) -> bool {
195        self.consent
196    }
197
198    /// Updates the consent flag for this policy.
199    ///
200    /// Returns `PolicyUpdated` if the consent was successfully changed,
201    /// or `PolicyNotUpdated` if the resource is deleted and cannot be modified.
202    pub fn with_consent(&mut self, consent: bool) -> ComplianceResponse {
203        if !self.is_deleted() {
204            self.consent = consent;
205            ComplianceResponse::PolicyUpdated
206        } else {
207            ComplianceResponse::PolicyNotUpdated
208        }
209    }
210
211    /// Updates the integrity level for this policy.
212    ///
213    /// Returns `PolicyUpdated` if the integrity was successfully changed,
214    /// or `PolicyNotUpdated` if the resource is deleted and cannot be modified.
215    ///
216    /// # Arguments
217    ///
218    /// * `integrity` - The new integrity level
219    pub fn with_integrity(&mut self, integrity: u32) -> ComplianceResponse {
220        if !self.is_deleted() {
221            self.integrity = integrity;
222            ComplianceResponse::PolicyUpdated
223        } else {
224            ComplianceResponse::PolicyNotUpdated
225        }
226    }
227
228    /// Updates the confidentiality level for this policy.
229    ///
230    /// Returns `PolicyUpdated` if the confidentiality was successfully changed,
231    /// or `PolicyNotUpdated` if the resource is deleted and cannot be modified.
232    ///
233    /// # Arguments
234    ///
235    /// * `confidentiality` - The new confidentiality level
236    pub fn with_confidentiality(
237        &mut self,
238        confidentiality: ConfidentialityPolicy,
239    ) -> ComplianceResponse {
240        if !self.is_deleted() {
241            self.confidentiality = confidentiality;
242            ComplianceResponse::PolicyUpdated
243        } else {
244            ComplianceResponse::PolicyNotUpdated
245        }
246    }
247
248    /// Marks the resource for deletion.
249    ///
250    /// This transitions the resource from `NotDeleted` to `Pending` deletion status.
251    /// Once marked for deletion, the policy cannot be further modified.
252    ///
253    /// Returns `PolicyUpdated` if the deletion was successfully marked as pending,
254    /// or `PolicyNotUpdated` if the resource is already deleted or pending deletion.
255    pub fn deleted(&mut self) -> ComplianceResponse {
256        if !self.is_deleted() {
257            self.deleted = DeletionPolicy::Pending;
258            ComplianceResponse::PolicyUpdated
259        } else {
260            ComplianceResponse::PolicyNotUpdated
261        }
262    }
263
264    /// Marks the deletion as enforced for a resource that is pending deletion.
265    ///
266    /// This transitions the resource from `Pending` to `Deleted` status.
267    /// This method should be called after the actual deletion has been performed.
268    ///
269    /// Returns `PolicyUpdated` if the deletion was successfully marked,
270    /// or `PolicyNotUpdated` if the resource is not pending deletion.
271    pub fn deletion_enforced(&mut self) -> ComplianceResponse {
272        if self.is_pending_deletion() {
273            self.deleted = DeletionPolicy::Deleted;
274            ComplianceResponse::PolicyUpdated
275        } else {
276            ComplianceResponse::PolicyNotUpdated
277        }
278    }
279}
280
281/// The main compliance service that manages policies and evaluates flows.
282///
283/// `ComplianceService` implements the `Service` trait from the Tower library,
284/// providing an asynchronous interface for handling compliance requests.
285/// It combines policy storage and evaluation logic in a single service.
286///
287/// # Features
288///
289/// - **Policy Management**: Store, retrieve, and update resource policies
290/// - **Flow Evaluation**: Check whether data flows comply with policies
291/// - **Thread Safety**: Safe for concurrent use across multiple threads
292/// - **Async Interface**: Non-blocking operations using Tower's Service trait
293///
294/// # Request Types
295///
296/// The service handles several types of compliance requests:
297///
298/// - `EvalPolicies` - Evaluate whether a flow is permitted
299/// - `GetPolicy` / `GetPolicies` - Retrieve existing policies
300/// - `SetPolicy` - Set complete policy for a resource
301/// - `SetConfidentiality` / `SetIntegrity` / `SetConsent` - Update specific policy fields
302/// - `SetDeleted` - Mark resources for deletion
303///
304/// # Operating Modes
305///
306/// ## Normal Mode (default)
307/// - Used in production
308/// - Unknown resources get default policies automatically
309/// - More forgiving for dynamic resource discovery
310///
311/// # Error Handling
312///
313/// The service returns `TraceabilityError` for various failure conditions:
314/// - `DirectPolicyViolation` - Flow violates compliance rules
315/// - `InternalTrace2eError` - Internal service errors
316#[derive(Clone, Debug)]
317pub struct ComplianceService<C = ConsentService> {
318    /// Node ID
319    node_id: String,
320    /// Thread-safe storage for resource policies
321    policies: Arc<DashMap<Resource, Policy>>,
322    /// Consent service
323    consent: C,
324}
325
326impl Default for ComplianceService {
327    fn default() -> Self {
328        Self {
329            node_id: String::new(),
330            policies: Arc::new(DashMap::new()),
331            consent: ConsentService::default(),
332        }
333    }
334}
335
336impl ComplianceService<ConsentService> {
337    /// Creates a new compliance service with the specified node ID and consent service.
338    ///
339    /// # Arguments
340    ///
341    /// * `node_id` - The node ID
342    /// * `consent` - The consent service
343    ///
344    /// # Returns
345    ///
346    /// A new compliance service.
347    pub fn new(node_id: String, consent: ConsentService) -> Self {
348        Self { node_id, policies: Arc::new(DashMap::new()), consent }
349    }
350
351    /// Evaluates whether a data flow is compliant with the given policies.
352    ///
353    /// This function implements the core compliance logic by checking multiple policy
354    /// dimensions against a set of rules that determine whether data can flow from
355    /// source resources to a destination resource.
356    ///
357    /// # Policy Evaluation Rules
358    ///
359    /// A flow is **permitted** only when ALL of the following conditions are met:
360    ///
361    /// 1. **No Deleted Resources**: Neither source nor destination is deleted or pending deletion
362    /// 2. **Integrity Preservation**: Source integrity ≥ destination integrity
363    /// 3. **Confidentiality Protection**: Secret data cannot flow to public destinations
364    /// 4. **Consent Required**: source resources must have consent (when enforced)
365    ///
366    /// # Arguments
367    ///
368    /// * `sources` - Set of source resources
369    /// * `destination` - Destination resource
370    ///
371    /// # Returns
372    ///
373    /// - `Ok(ComplianceResponse::Grant)` if the flow is permitted
374    /// - `Err(TraceabilityError::DirectPolicyViolation)` if any rule is violated
375    ///
376    /// # Flow Scenarios
377    ///
378    /// ## ✅ Permitted Flows
379    /// - Public (integrity 5) → Public (integrity 3) - integrity preserved
380    /// - Secret (integrity 5) → Secret (integrity 3) - confidentiality maintained
381    /// - Public (integrity 5) → Secret (integrity 3) - upgrading confidentiality
382    ///
383    /// ## ❌ Blocked Flows  
384    /// - Any deleted/pending resource involved
385    /// - Public (integrity 3) → Public (integrity 5) - integrity violation
386    /// - Secret (integrity 5) → Public (integrity 3) - confidentiality leak
387    /// - Any resource without consent (when enforced)
388    async fn eval_compliance(
389        &self,
390        sources: HashSet<Resource>,
391        destination: LocalizedResource,
392        destination_policy: Option<Policy>,
393    ) -> Result<ComplianceResponse, TraceabilityError> {
394        let mut result: Result<ComplianceResponse, TraceabilityError> =
395            Ok(ComplianceResponse::Grant);
396
397        // Get the destination policy if it is local or use the provided policy
398        let destination_policy = if let Some(destination) = self.as_local_resource(&destination) {
399            self.get_policy(&destination)
400        } else {
401            destination_policy.ok_or(TraceabilityError::DestinationPolicyNotFound)?
402        };
403
404        let source_policies = self.get_policies(sources);
405
406        // Collect all consent requests from source policies
407        let mut consent_tasks = JoinSet::new();
408
409        for (source, source_policy) in source_policies {
410            // Spawn consent request tasks in parallel
411            if source_policy.get_consent() {
412                let mut consent_service = self.consent.clone();
413                let destination = destination.clone().into();
414                let source_clone = source.clone();
415                consent_tasks.spawn(async move {
416                    consent_service
417                        .call(ConsentRequest::RequestConsent { source: source_clone, destination })
418                        .await
419                });
420            }
421
422            // If the source or destination policy is deleted, the flow is not compliant
423            if source_policy.is_deleted() || destination_policy.is_deleted() {
424                info!(
425                    source_deleted = source_policy.is_deleted(),
426                    destination_deleted = destination_policy.is_deleted(),
427                    "[compliance] EvalPolicies: deleted policies detected"
428                );
429                if source_policy.is_pending_deletion() {
430                    info!("[compliance] Enforcing deletion policy for source");
431                }
432                if destination_policy.is_pending_deletion() {
433                    info!("[compliance] Enforcing deletion policy for destination");
434                }
435
436                #[cfg(feature = "deletion_enforcement")]
437                if let Resource::Fd(crate::traceability::infrastructure::naming::Fd::File(file)) =
438                    &source
439                {
440                    let _ = std::fs::remove_file(&file.path);
441                }
442                #[cfg(feature = "deletion_enforcement")]
443                if let Resource::Fd(crate::traceability::infrastructure::naming::Fd::File(file)) =
444                    &destination.resource()
445                {
446                    let _ = std::fs::remove_file(&file.path);
447                }
448
449                result = Err(TraceabilityError::DirectPolicyViolation);
450            }
451
452            // Integrity check: Source integrity must be greater than or equal to destination
453            // integrity
454            if source_policy.integrity < destination_policy.integrity {
455                result = Err(TraceabilityError::DirectPolicyViolation);
456            }
457
458            // Confidentiality check: Secret data cannot flow to public destinations
459            if source_policy.confidentiality == ConfidentialityPolicy::Secret
460                && destination_policy.confidentiality == ConfidentialityPolicy::Public
461            {
462                result = Err(TraceabilityError::DirectPolicyViolation);
463            }
464        }
465
466        // Await all consent requests and verify they succeed
467        while let Some(result) = consent_tasks.join_next().await {
468            let consent_response = result
469                .map_err(|_| TraceabilityError::InternalTrace2eError)?
470                .map_err(|_| TraceabilityError::InternalTrace2eError)?;
471
472            match consent_response {
473                ConsentResponse::Consent(true) => continue,
474                ConsentResponse::Consent(false) => {
475                    return Err(TraceabilityError::DirectPolicyViolation);
476                }
477                _ => return Err(TraceabilityError::InternalTrace2eError),
478            }
479        }
480        result
481    }
482}
483
484impl ComplianceService {
485    /// Returns the resource if it is local to the node, otherwise returns None
486    fn as_local_resource(&self, resource: &LocalizedResource) -> Option<Resource> {
487        if *resource.node_id() == self.node_id {
488            Some(resource.resource().to_owned())
489        } else {
490            None
491        }
492    }
493
494    /// Retrieves the policy for a specific resource, inserts a default policy and returns it if not found
495    ///
496    /// # Arguments
497    ///
498    /// * `resource` - The resource to look up
499    fn get_policy(&self, resource: &Resource) -> Policy {
500        self.policies.entry(resource.to_owned()).or_default().to_owned()
501    }
502
503    /// Retrieves policies for a set of resources.
504    ///
505    /// # Arguments
506    ///
507    /// * `resources` - Set of resources to look up
508    ///
509    /// # Returns
510    ///
511    /// A map from resources to their policies, excluding stream resources.
512    fn get_policies(&self, resources: HashSet<Resource>) -> HashMap<Resource, Policy> {
513        let mut policies_set = HashMap::new();
514        for resource in resources {
515            // Get the policy from the local policies, streams have no policies
516            if !resource.is_stream() {
517                policies_set.insert(resource.to_owned(), self.get_policy(&resource));
518            }
519        }
520        policies_set
521    }
522
523    /// Retrieves policies for a set of resources.
524    ///
525    /// # Arguments
526    ///
527    /// * `resources` - Set of resources to look up
528    ///
529    /// # Returns
530    ///
531    /// A map from localized resources to their policies, excluding stream resources.
532    fn get_localized_policies(
533        &self,
534        resources: HashSet<Resource>,
535    ) -> HashMap<LocalizedResource, Policy> {
536        self.get_policies(resources)
537            .into_iter()
538            .map(|(resource, policy)| {
539                (LocalizedResource::new(self.node_id.clone(), resource), policy)
540            })
541            .collect()
542    }
543
544    /// Sets the complete policy for a specific resource.
545    ///
546    /// This replaces any existing policy for the resource. If the resource
547    /// is already deleted, the update is rejected.
548    ///
549    /// # Arguments
550    ///
551    /// * `resource` - The resource to update
552    /// * `policy` - The new policy to set
553    ///
554    /// # Returns
555    ///
556    /// - `PolicyUpdated` if the policy was successfully set
557    /// - `PolicyNotUpdated` if the resource is deleted and cannot be modified
558    fn set_policy(&self, resource: Resource, policy: Policy) -> ComplianceResponse {
559        // If the resource is not local or is deleted, return PolicyNotUpdated
560        if self.policies.get(&resource).is_some_and(|policy| policy.is_deleted()) {
561            ComplianceResponse::PolicyNotUpdated
562        } else {
563            self.policies.insert(resource, policy);
564            ComplianceResponse::PolicyUpdated
565        }
566    }
567
568    /// Sets the confidentiality level for a specific resource.
569    ///
570    /// Creates a default policy if the resource doesn't exist.
571    /// Updates are rejected if the resource is deleted.
572    ///
573    /// # Arguments
574    ///
575    /// * `resource` - The resource to update
576    /// * `confidentiality` - The new confidentiality level
577    fn set_confidentiality(
578        &self,
579        resource: Resource,
580        confidentiality: ConfidentialityPolicy,
581    ) -> ComplianceResponse {
582        let mut response = ComplianceResponse::PolicyNotUpdated;
583        self.policies
584            .entry(resource)
585            .and_modify(|policy| {
586                response = policy.with_confidentiality(confidentiality);
587            })
588            .or_insert_with(|| {
589                let mut policy = Policy::default();
590                response = policy.with_confidentiality(confidentiality);
591                policy
592            });
593        response
594    }
595
596    /// Sets the integrity level for a specific resource.
597    ///
598    /// Creates a default policy if the resource doesn't exist.
599    /// Updates are rejected if the resource is deleted.
600    ///
601    /// # Arguments
602    ///
603    /// * `resource` - The resource to update
604    /// * `integrity` - The new integrity level
605    fn set_integrity(&self, resource: Resource, integrity: u32) -> ComplianceResponse {
606        let mut response = ComplianceResponse::PolicyNotUpdated;
607        self.policies
608            .entry(resource)
609            .and_modify(|policy| {
610                response = policy.with_integrity(integrity);
611            })
612            .or_insert_with(|| {
613                let mut policy = Policy::default();
614                response = policy.with_integrity(integrity);
615                policy
616            });
617        response
618    }
619
620    /// Marks a specific resource for deletion.
621    ///
622    /// Creates a default policy if the resource doesn't exist, then marks it for deletion.
623    /// Updates are rejected if the resource is already deleted.
624    ///
625    /// # Arguments
626    ///
627    /// * `resource` - The resource to mark for deletion
628    fn set_deleted(&self, resource: Resource) -> ComplianceResponse {
629        let mut response = ComplianceResponse::PolicyNotUpdated;
630        self.policies
631            .entry(resource)
632            .and_modify(|policy| {
633                response = policy.deleted();
634            })
635            .or_insert_with(|| {
636                let mut policy = Policy::default();
637                response = policy.deleted();
638                policy
639            });
640        response
641    }
642
643    /// Sets the consent enforcement flag for a specific resource.
644    ///
645    /// Creates a default policy if the resource doesn't exist.
646    /// Updates are rejected if the resource is deleted.
647    ///
648    /// # Arguments
649    ///
650    /// * `resource` - The resource to update
651    /// * `consent` - The new consent enforcement value
652    fn enforce_consent(&self, resource: Resource, consent: bool) -> ComplianceResponse {
653        let mut response = ComplianceResponse::PolicyNotUpdated;
654        self.policies
655            .entry(resource)
656            .and_modify(|policy| {
657                response = policy.with_consent(consent);
658            })
659            .or_insert_with(|| {
660                let mut policy = Policy::default();
661                response = policy.with_consent(consent);
662                policy
663            });
664        response
665    }
666}
667
668impl Service<ComplianceRequest> for ComplianceService<ConsentService> {
669    type Response = ComplianceResponse;
670    type Error = TraceabilityError;
671    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
672
673    fn poll_ready(&mut self, _: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
674        Poll::Ready(Ok(()))
675    }
676
677    fn call(&mut self, request: ComplianceRequest) -> Self::Future {
678        let this = self.clone();
679        Box::pin(async move {
680            match request {
681                ComplianceRequest::EvalCompliance { sources, destination, destination_policy } => {
682                    info!(
683                        node_id = %this.node_id,
684                        sources = %DisplayableResource::from(&sources),
685                        destination = %destination,
686                        destination_policy = ?destination_policy,
687                        "[compliance] EvalCompliance"
688                    );
689                    this.eval_compliance(sources, destination, destination_policy).await
690                }
691                ComplianceRequest::GetPolicy(resource) => {
692                    info!(node_id = %this.node_id, resource = %resource, "[compliance] GetPolicy");
693                    Ok(ComplianceResponse::Policy(this.get_policy(&resource)))
694                }
695                ComplianceRequest::GetPolicies(resources) => {
696                    info!(
697                        node_id = %this.node_id,
698                        resources = %DisplayableResource::from(&resources),
699                        "[compliance] GetPolicies"
700                    );
701                    Ok(ComplianceResponse::Policies(this.get_localized_policies(resources)))
702                }
703                ComplianceRequest::SetPolicy { resource, policy } => {
704                    info!(
705                        node_id = %this.node_id,
706                        resource = %resource,
707                        policy = ?policy,
708                        "[compliance] SetPolicy"
709                    );
710                    Ok(this.set_policy(resource, policy))
711                }
712                ComplianceRequest::SetConfidentiality { resource, confidentiality } => {
713                    info!(
714                        node_id = %this.node_id,
715                        resource = %resource,
716                        confidentiality = ?confidentiality,
717                        "[compliance] SetConfidentiality"
718                    );
719                    Ok(this.set_confidentiality(resource, confidentiality))
720                }
721                ComplianceRequest::SetIntegrity { resource, integrity } => {
722                    info!(
723                        node_id = %this.node_id,
724                        resource = %resource,
725                        integrity = ?integrity,
726                        "[compliance] SetIntegrity"
727                    );
728                    Ok(this.set_integrity(resource, integrity))
729                }
730                ComplianceRequest::SetDeleted(resource) => {
731                    info!(node_id = %this.node_id, resource = %resource, "[compliance] SetDeleted");
732                    Ok(this.set_deleted(resource))
733                }
734                ComplianceRequest::EnforceConsent { resource, consent } => {
735                    info!(
736                        node_id = %this.node_id,
737                        resource = %resource,
738                        consent = ?consent,
739                        "[compliance] EnforceConsent"
740                    );
741                    Ok(this.enforce_consent(resource, consent))
742                }
743            }
744        })
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use crate::traceability::infrastructure::naming::Resource;
752
753    // Helper functions to reduce test code duplication
754    fn create_public_policy(integrity: u32) -> Policy {
755        Policy::new(ConfidentialityPolicy::Public, integrity, DeletionPolicy::NotDeleted, false)
756    }
757
758    fn create_secret_policy(integrity: u32) -> Policy {
759        Policy::new(ConfidentialityPolicy::Secret, integrity, DeletionPolicy::NotDeleted, false)
760    }
761
762    fn create_deleted_policy() -> Policy {
763        Policy::new(ConfidentialityPolicy::Public, 0, DeletionPolicy::Pending, false)
764    }
765
766    fn init_tracing() {
767        crate::trace2e_tracing::init();
768    }
769
770    #[tokio::test]
771    async fn unit_compliance_set_policy_basic() {
772        init_tracing();
773        let compliance = ComplianceService::default();
774        let process = Resource::new_process_mock(0);
775
776        let policy = create_secret_policy(5);
777
778        // First time setting policy should return PolicyUpdated
779        assert_eq!(
780            compliance.set_policy(process.clone(), policy),
781            ComplianceResponse::PolicyUpdated
782        );
783
784        let new_policy = create_secret_policy(3);
785
786        // Updating existing policy should also return PolicyUpdated
787        assert_eq!(compliance.set_policy(process, new_policy), ComplianceResponse::PolicyUpdated);
788    }
789
790    #[tokio::test]
791    async fn unit_compliance_policy_evaluation_scenarios() {
792        init_tracing();
793        let compliance = ComplianceService::default();
794        // Test integrity constraints
795        let test_cases = [
796            // (source_policy, dest_policy, should_pass, description)
797            (create_public_policy(5), create_public_policy(3), true, "integrity pass: 5 >= 3"),
798            (create_public_policy(3), create_public_policy(5), false, "integrity fail: 3 < 5"),
799            (
800                create_secret_policy(5),
801                create_secret_policy(3),
802                true,
803                "confidentiality pass: secret -> secret",
804            ),
805            (
806                create_secret_policy(5),
807                create_public_policy(3),
808                false,
809                "confidentiality fail: secret -> public",
810            ),
811            (
812                create_public_policy(5),
813                create_secret_policy(3),
814                true,
815                "confidentiality pass: public -> secret",
816            ),
817        ];
818        let mock_file =
819            LocalizedResource::new(String::new(), Resource::new_file("/tmp/dest".to_string()));
820        let mock_process = Resource::new_process_mock(0);
821
822        for (source_policy, dest_policy, should_pass, description) in test_cases {
823            compliance.set_policy(mock_process.clone(), source_policy);
824            compliance.set_policy(mock_file.resource().to_owned(), dest_policy);
825            let result = compliance
826                .eval_compliance(HashSet::from([mock_process.clone()]), mock_file.clone(), None)
827                .await;
828
829            if should_pass {
830                assert!(
831                    result.is_ok_and(|r| r == ComplianceResponse::Grant),
832                    "Test failed: {description}"
833                );
834            } else {
835                assert!(
836                    result.is_err_and(|e| e == TraceabilityError::DirectPolicyViolation),
837                    "Test failed: {description}"
838                );
839            }
840        }
841    }
842
843    #[tokio::test]
844    async fn unit_compliance_default_policies() {
845        init_tracing();
846        let compliance = ComplianceService::default();
847        let mock_local_file =
848            LocalizedResource::new(String::new(), Resource::new_file("/tmp/dest".to_string()));
849        let mock_process = Resource::new_process_mock(0);
850        let mock_process2 = Resource::new_process_mock(1);
851
852        // Test 1: All default local policies should pass
853        assert!(
854            compliance
855                .eval_compliance(
856                    HashSet::from([mock_process.clone(), mock_process2.clone()]),
857                    mock_local_file.clone(),
858                    None,
859                )
860                .await
861                .is_ok_and(|r| r == ComplianceResponse::Grant)
862        );
863
864        // Test 2: Mixed default and explicit policies - integrity violation
865        let dest_policy = create_public_policy(2);
866        compliance.set_policy(mock_local_file.resource().to_owned(), dest_policy);
867        assert!(
868            compliance
869                .eval_compliance(
870                    HashSet::from([mock_process.clone(), mock_process2.clone()]),
871                    mock_local_file, // integrity: 2, so 0 < 2 should fail
872                    None,
873                )
874                .await
875                .is_err_and(|e| e == TraceabilityError::DirectPolicyViolation)
876        );
877    }
878
879    #[tokio::test]
880    async fn unit_compliance_service_eval_policies_requests() {
881        init_tracing();
882        let mut compliance = ComplianceService::default();
883
884        let mock_process = Resource::new_process_mock(0);
885        let file =
886            LocalizedResource::new(String::new(), Resource::new_file("/tmp/dest".to_string()));
887
888        // Test case 1: Valid policy flow - should grant (process integrity 5 >= file integrity 3)
889        let process_policy = create_public_policy(5);
890        compliance.set_policy(mock_process.clone(), process_policy);
891        let file_policy = create_public_policy(3);
892        compliance.set_policy(file.resource().to_owned(), file_policy);
893
894        let grant_request = ComplianceRequest::EvalCompliance {
895            sources: HashSet::from([mock_process.clone()]),
896            destination: file.clone(),
897            destination_policy: None,
898        };
899        assert_eq!(compliance.call(grant_request).await.unwrap(), ComplianceResponse::Grant);
900
901        // Test case 2: Invalid policy flow - should deny (process integrity 2 < file integrity 3)
902        let low_process_policy = create_public_policy(2);
903        compliance.set_policy(mock_process.clone(), low_process_policy);
904
905        let deny_request = ComplianceRequest::EvalCompliance {
906            sources: HashSet::from([mock_process.clone()]),
907            destination: file,
908            destination_policy: None,
909        };
910        assert_eq!(
911            compliance.call(deny_request).await.unwrap_err(),
912            TraceabilityError::DirectPolicyViolation
913        );
914    }
915
916    #[test]
917    fn unit_compliance_get_policies_empty() {
918        init_tracing();
919        let compliance = ComplianceService::default();
920        let process = LocalizedResource::new(String::new(), Resource::new_process_mock(0));
921        let file =
922            LocalizedResource::new(String::new(), Resource::new_file("/tmp/test".to_string()));
923
924        assert_eq!(
925            compliance.get_localized_policies(HashSet::from([
926                process.resource().to_owned(),
927                file.resource().to_owned()
928            ])),
929            HashMap::from([(process, Policy::default()), (file, Policy::default())])
930        );
931    }
932
933    #[test]
934    fn unit_compliance_get_policies_scenarios() {
935        init_tracing();
936        let compliance = ComplianceService::default();
937
938        // Test resources
939        let process = Resource::new_process_mock(0);
940        let file = Resource::new_file("/tmp/test".to_string());
941
942        // Test policies
943        let process_policy = create_secret_policy(7);
944        let file_policy = create_public_policy(3);
945
946        // Test 1: Single resource with policy
947        compliance.set_policy(process.clone(), process_policy.clone());
948        assert_eq!(
949            compliance.get_policies(HashSet::from([process.clone()])),
950            HashMap::from([(process.clone(), process_policy.clone())])
951        );
952
953        // Test 2: Multiple resources with policies
954        compliance.set_policy(file.clone(), file_policy.clone());
955        assert_eq!(
956            compliance.get_policies(HashSet::from([process.clone(), file.clone()])),
957            HashMap::from([
958                (process.clone(), process_policy.clone()),
959                (file.clone(), file_policy.clone())
960            ])
961        );
962
963        // Test 3: Mixed existing and default policies
964        let new_file = Resource::new_file("/tmp/new.txt".to_string());
965        assert_eq!(
966            compliance.get_policies(HashSet::from([
967                process.clone(),
968                file.clone(),
969                new_file.clone()
970            ])),
971            HashMap::from([
972                (process.clone(), process_policy.clone()),
973                (file.clone(), file_policy.clone()),
974                (new_file.clone(), Policy::default())
975            ])
976        );
977    }
978
979    #[tokio::test]
980    async fn unit_compliance_deleted_policy_behavior() {
981        init_tracing();
982        let compliance = ComplianceService::default();
983        let mock_process0 = Resource::new_process_mock(0);
984        let mock_process1 = Resource::new_process_mock(1);
985        let mock_file = Resource::new_file("/tmp/dest".to_string());
986
987        // Create and set a deleted policy
988        let deleted_policy = create_deleted_policy();
989        compliance.set_policy(mock_process0.clone(), deleted_policy.clone());
990
991        // Test 1: Deleted policy is returned correctly
992        assert_eq!(
993            compliance.get_policies(HashSet::from([mock_process0.clone()])),
994            HashMap::from([(mock_process0.clone(), deleted_policy.clone())])
995        );
996
997        // Test 2: Cannot update deleted policy
998        assert_eq!(
999            compliance.set_policy(mock_process0.clone(), Policy::default()),
1000            ComplianceResponse::PolicyNotUpdated
1001        );
1002
1003        // Test 3: Policy remains deleted after update attempt
1004        assert_eq!(
1005            compliance.get_policies(HashSet::from([mock_process0.clone()])),
1006            HashMap::from([(mock_process0.clone(), deleted_policy.clone())])
1007        );
1008
1009        // Test 4: Deleted policies cause policy violations in evaluation in both directions
1010        assert!(
1011            compliance
1012                .eval_compliance(
1013                    HashSet::from([mock_process0.clone(), mock_process1.clone()]),
1014                    LocalizedResource::new(String::new(), mock_file.clone()),
1015                    None
1016                )
1017                .await
1018                .is_err_and(|e| e == TraceabilityError::DirectPolicyViolation)
1019        );
1020
1021        assert!(
1022            compliance
1023                .eval_compliance(
1024                    HashSet::from([mock_file.clone(), mock_process1.clone()]),
1025                    LocalizedResource::new(String::new(), mock_process0),
1026                    None
1027                )
1028                .await
1029                .is_err_and(|e| e == TraceabilityError::DirectPolicyViolation)
1030        );
1031    }
1032
1033    #[tokio::test]
1034    async fn unit_compliance_localized_destination() {
1035        init_tracing();
1036        let compliance = ComplianceService::default();
1037
1038        let local_process = Resource::new_process_mock(0);
1039        let local_file = Resource::new_file("/tmp/local.txt".to_string());
1040        let remote_process =
1041            LocalizedResource::new("10.0.0.1".to_string(), Resource::new_process_mock(1));
1042
1043        // Test 2: Non-local destination without policy should fail
1044        assert!(
1045            compliance
1046                .eval_compliance(
1047                    HashSet::from([local_file.clone(), local_process.clone()]),
1048                    remote_process.clone(),
1049                    None
1050                )
1051                .await
1052                .is_err_and(|e| e == TraceabilityError::DestinationPolicyNotFound)
1053        );
1054
1055        // Test 3: Non-local destination WITH policy should succeed (the exception)
1056        let remote_policy = create_public_policy(0);
1057        assert!(
1058            compliance
1059                .eval_compliance(
1060                    HashSet::from([local_file, local_process]),
1061                    remote_process.clone(),
1062                    Some(remote_policy.clone())
1063                )
1064                .await
1065                .is_ok_and(|r| r == ComplianceResponse::Grant)
1066        );
1067    }
1068
1069    #[tokio::test]
1070    async fn unit_compliance_service_deletion_policy_workflow() {
1071        init_tracing();
1072        let mut compliance = ComplianceService::default();
1073
1074        let process = Resource::new_process_mock(0);
1075        let file = Resource::new_file("/tmp/sensitive.txt".to_string());
1076
1077        // Test 1: Set initial policies for resources
1078        let process_policy = create_secret_policy(5);
1079        let file_policy = create_public_policy(3);
1080
1081        let set_process_request = ComplianceRequest::SetPolicy {
1082            resource: process.clone(),
1083            policy: process_policy.clone(),
1084        };
1085        assert_eq!(
1086            compliance.call(set_process_request).await.unwrap(),
1087            ComplianceResponse::PolicyUpdated
1088        );
1089
1090        let set_file_request =
1091            ComplianceRequest::SetPolicy { resource: file.clone(), policy: file_policy.clone() };
1092        assert_eq!(
1093            compliance.call(set_file_request).await.unwrap(),
1094            ComplianceResponse::PolicyUpdated
1095        );
1096
1097        // Test 2: Verify policies are set correctly
1098        let get_process_request = ComplianceRequest::GetPolicy(process.clone());
1099        if let ComplianceResponse::Policy(retrieved_policy) =
1100            compliance.call(get_process_request).await.unwrap()
1101        {
1102            assert_eq!(retrieved_policy, process_policy);
1103            assert!(!retrieved_policy.is_deleted());
1104        } else {
1105            panic!("Expected Policy response");
1106        }
1107
1108        // Test 3: Create a policy with pending deletion status
1109        let mut pending_deletion_policy = create_secret_policy(7);
1110        pending_deletion_policy.deleted();
1111        assert!(pending_deletion_policy.is_pending_deletion());
1112
1113        let set_pending_request = ComplianceRequest::SetPolicy {
1114            resource: file.clone(),
1115            policy: pending_deletion_policy.clone(),
1116        };
1117        assert_eq!(
1118            compliance.call(set_pending_request).await.unwrap(),
1119            ComplianceResponse::PolicyUpdated
1120        );
1121
1122        // Test 4: Verify pending deletion policy is set
1123        let get_file_request = ComplianceRequest::GetPolicy(file.clone());
1124        if let ComplianceResponse::Policy(retrieved_policy) =
1125            compliance.call(get_file_request).await.unwrap()
1126        {
1127            assert_eq!(retrieved_policy, pending_deletion_policy);
1128            assert!(retrieved_policy.is_deleted());
1129            assert!(retrieved_policy.is_pending_deletion());
1130        } else {
1131            panic!("Expected Policy response");
1132        }
1133
1134        // Test 5: Try to update policy on resource with pending deletion - should fail
1135        let new_policy = create_public_policy(1);
1136        let update_pending_request =
1137            ComplianceRequest::SetPolicy { resource: file.clone(), policy: new_policy };
1138        assert_eq!(
1139            compliance.call(update_pending_request).await.unwrap(),
1140            ComplianceResponse::PolicyNotUpdated
1141        );
1142
1143        // Test 6: Create fully deleted policy and test it
1144        let mut deleted_policy = create_public_policy(2);
1145        deleted_policy.deleted();
1146        deleted_policy.deletion_enforced();
1147        assert!(deleted_policy.is_deleted());
1148        assert!(!deleted_policy.is_pending_deletion());
1149
1150        let new_resource = Resource::new_process_mock(1);
1151        let set_deleted_request = ComplianceRequest::SetPolicy {
1152            resource: new_resource.clone(),
1153            policy: deleted_policy.clone(),
1154        };
1155        assert_eq!(
1156            compliance.call(set_deleted_request).await.unwrap(),
1157            ComplianceResponse::PolicyUpdated
1158        );
1159
1160        // Test 7: Verify deleted policy blocks updates
1161        let another_policy = create_secret_policy(10);
1162        let update_deleted_request =
1163            ComplianceRequest::SetPolicy { resource: new_resource.clone(), policy: another_policy };
1164        assert_eq!(
1165            compliance.call(update_deleted_request).await.unwrap(),
1166            ComplianceResponse::PolicyNotUpdated
1167        );
1168
1169        // Test 8: Test policy evaluation with deleted resources - should fail
1170        let eval_with_deleted_source = ComplianceRequest::EvalCompliance {
1171            sources: HashSet::from([new_resource]),
1172            destination: LocalizedResource::new(
1173                String::new(),
1174                Resource::new_file("/tmp/dest".to_string()),
1175            ),
1176            destination_policy: None,
1177        };
1178        assert_eq!(
1179            compliance.call(eval_with_deleted_source).await.unwrap_err(),
1180            TraceabilityError::DirectPolicyViolation
1181        );
1182
1183        // Test 9: Test policy evaluation with deleted destination - should fail
1184        let eval_with_deleted_dest = ComplianceRequest::EvalCompliance {
1185            sources: HashSet::from([process]),
1186            destination: LocalizedResource::new(String::new(), file),
1187            destination_policy: None,
1188        };
1189        assert_eq!(
1190            compliance.call(eval_with_deleted_dest).await.unwrap_err(),
1191            TraceabilityError::DirectPolicyViolation
1192        );
1193    }
1194
1195    #[tokio::test]
1196    async fn unit_compliance_eval_policies_empty_sources() {
1197        init_tracing();
1198        let compliance = ComplianceService::default();
1199
1200        let mock_file = LocalizedResource::new(
1201            compliance.node_id.clone(),
1202            Resource::new_file("/tmp/dest".to_string()),
1203        );
1204
1205        // Test: eval_compliance with empty sources set should grant access (no-op)
1206        // Nothing flows to destination, so there are no violations
1207        assert!(
1208            compliance
1209                .eval_compliance(HashSet::new(), mock_file, None)
1210                .await
1211                .is_ok_and(|r| r == ComplianceResponse::Grant)
1212        );
1213    }
1214}