Skip to main content

confium_pki/delegation/
validate.rs

1//! Validation logic for delegated operations.
2
3use crate::delegation::constraint::{Constraint, ScopeValue};
4use crate::delegation::operation::Operation;
5use crate::delegation::scope::DelegationScope;
6
7/// Result of a delegation validation.
8#[derive(Debug, Clone, Default)]
9pub struct DelegationValidation {
10    /// True if the operation is permitted under the scope.
11    pub permitted: bool,
12    /// Reasons for denial (empty if permitted).
13    pub denials: Vec<DenialReason>,
14}
15
16/// Why an operation was denied.
17#[derive(Debug, Clone)]
18pub enum DenialReason {
19    /// Operation not in allowed list.
20    OperationNotAllowed(String),
21    /// Constraint violated.
22    ConstraintViolated(Constraint),
23}
24
25/// Validate whether `proposed_operation` is permitted under `scope`,
26/// given the `actual_values` that apply.
27pub fn validate_delegation(
28    scope: &DelegationScope,
29    proposed_operation: &Operation,
30    actual_values: &[ScopeValue<'_>],
31) -> DelegationValidation {
32    let mut denials = Vec::new();
33
34    let op_permitted = scope
35        .allowed_operations
36        .iter()
37        .any(|allowed| operations_match(allowed, proposed_operation));
38
39    if !op_permitted {
40        denials.push(DenialReason::OperationNotAllowed(format!(
41            "{:?}",
42            proposed_operation
43        )));
44    }
45
46    for constraint in &scope.constraints {
47        let satisfied = actual_values.iter().any(|v| constraint.satisfies(v));
48        if !satisfied {
49            denials.push(DenialReason::ConstraintViolated(constraint.clone()));
50        }
51    }
52
53    let permitted = denials.is_empty();
54    DelegationValidation { permitted, denials }
55}
56
57fn operations_match(allowed: &Operation, proposed: &Operation) -> bool {
58    matches!(
59        (allowed, proposed),
60        (Operation::SignCert(_), Operation::SignCert(_))
61            | (Operation::SignDocument(_), Operation::SignDocument(_))
62            | (Operation::ThresholdSign(_), Operation::ThresholdSign(_))
63            | (Operation::Encrypt(_), Operation::Encrypt(_))
64    )
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::delegation::operation::{SignCertSpec, SignDocSpec};
71    use chrono::Utc;
72
73    #[test]
74    fn permitted_operation_with_satisfied_constraints() {
75        let scope = DelegationScope::new()
76            .allow_operation(Operation::SignCert(SignCertSpec::default()))
77            .constrain(Constraint::ModelBound {
78                model_id: "FM-2026-A".into(),
79            });
80        let values = vec![ScopeValue::ModelId("FM-2026-A")];
81        let result = validate_delegation(
82            &scope,
83            &Operation::SignCert(SignCertSpec::default()),
84            &values,
85        );
86        assert!(result.permitted);
87        assert!(result.denials.is_empty());
88    }
89
90    #[test]
91    fn denied_when_operation_not_allowed() {
92        let scope =
93            DelegationScope::new().allow_operation(Operation::SignCert(SignCertSpec::default()));
94        let result = validate_delegation(
95            &scope,
96            &Operation::SignDocument(SignDocSpec::default()),
97            &[],
98        );
99        assert!(!result.permitted);
100    }
101
102    #[test]
103    fn denied_when_constraint_violated() {
104        let scope = DelegationScope::new()
105            .allow_operation(Operation::SignCert(SignCertSpec::default()))
106            .constrain(Constraint::ModelBound {
107                model_id: "FM-2026-A".into(),
108            });
109        let result = validate_delegation(
110            &scope,
111            &Operation::SignCert(SignCertSpec::default()),
112            &[ScopeValue::ModelId("FM-2026-B")],
113        );
114        assert!(!result.permitted);
115        assert_eq!(result.denials.len(), 1);
116    }
117
118    #[test]
119    fn time_bound_constraint_works() {
120        let now = Utc::now();
121        let scope = DelegationScope::new()
122            .allow_operation(Operation::SignCert(SignCertSpec::default()))
123            .constrain(Constraint::TimeBound {
124                not_before: now,
125                not_after: now,
126            });
127        let result = validate_delegation(
128            &scope,
129            &Operation::SignCert(SignCertSpec::default()),
130            &[ScopeValue::Time(now)],
131        );
132        assert!(result.permitted);
133    }
134}