confium_pki/delegation/
validate.rs1use crate::delegation::constraint::{Constraint, ScopeValue};
4use crate::delegation::operation::Operation;
5use crate::delegation::scope::DelegationScope;
6
7#[derive(Debug, Clone, Default)]
9pub struct DelegationValidation {
10 pub permitted: bool,
12 pub denials: Vec<DenialReason>,
14}
15
16#[derive(Debug, Clone)]
18pub enum DenialReason {
19 OperationNotAllowed(String),
21 ConstraintViolated(Constraint),
23}
24
25pub 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}