1pub mod identity;
24pub mod policy;
25
26use std::time::SystemTime;
27
28use serde::{Deserialize, Serialize};
29use sha2::{Digest, Sha256};
30
31pub use identity::MeshIdentity;
32pub use policy::{MeshPolicyEnforcer, PolicyDecision, StaticPolicyEnforcer};
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
37pub struct Namespace {
38 pub tenant: String,
39 pub scope: String,
40}
41
42impl Namespace {
43 pub fn new(tenant: impl Into<String>, scope: impl Into<String>) -> Self {
44 Self {
45 tenant: tenant.into(),
46 scope: scope.into(),
47 }
48 }
49
50 pub fn as_label(&self) -> String {
51 format!("{}/{}", self.tenant, self.scope)
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
59pub enum MemOp {
60 Recall,
61 Write,
62 Forget,
63 Branch,
64 ReplayAsOf,
65 ExportProvenance,
66}
67
68impl MemOp {
69 pub fn as_str(&self) -> &'static str {
70 match self {
71 MemOp::Recall => "recall",
72 MemOp::Write => "write",
73 MemOp::Forget => "forget",
74 MemOp::Branch => "branch",
75 MemOp::ReplayAsOf => "replay_as_of",
76 MemOp::ExportProvenance => "export_provenance",
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct MeshAuditEnvelope {
87 pub caller_spiffe: String,
88 pub op: MemOp,
89 pub namespace: Namespace,
90 pub decided: PolicyDecision,
91 pub prev_chain_head: [u8; 32],
94 pub envelope_at: SystemTime,
95}
96
97impl MeshAuditEnvelope {
98 pub fn next_chain_head(&self, prev_head: &[u8; 32]) -> [u8; 32] {
102 let mut h = Sha256::new();
103 h.update(prev_head);
104 h.update(self.caller_spiffe.as_bytes());
105 h.update(b"|");
106 h.update(self.op.as_str().as_bytes());
107 h.update(b"|");
108 h.update(self.namespace.as_label().as_bytes());
109 h.update(b"|");
110 h.update(self.decided.as_str().as_bytes());
111 h.update(b"|");
112 h.update(
113 chrono::DateTime::<chrono::Utc>::from(self.envelope_at)
114 .to_rfc3339()
115 .as_bytes(),
116 );
117 h.finalize().into()
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn env(decision: PolicyDecision) -> MeshAuditEnvelope {
126 MeshAuditEnvelope {
127 caller_spiffe: "spiffe://t1/a1".into(),
128 op: MemOp::Recall,
129 namespace: Namespace::new("t1", "shared"),
130 decided: decision,
131 prev_chain_head: [0u8; 32],
132 envelope_at: SystemTime::UNIX_EPOCH,
133 }
134 }
135
136 #[test]
137 fn next_chain_head_is_deterministic() {
138 let e = env(PolicyDecision::Allow);
139 let h1 = e.next_chain_head(&[0u8; 32]);
140 let h2 = e.next_chain_head(&[0u8; 32]);
141 assert_eq!(h1, h2);
142 }
143
144 #[test]
145 fn next_chain_head_changes_with_decision() {
146 let allow = env(PolicyDecision::Allow);
147 let deny = env(PolicyDecision::DenyMissingIdentity);
148 assert_ne!(
149 allow.next_chain_head(&[0u8; 32]),
150 deny.next_chain_head(&[0u8; 32])
151 );
152 }
153
154 #[test]
155 fn next_chain_head_changes_with_namespace() {
156 let mut e = env(PolicyDecision::Allow);
157 let h1 = e.next_chain_head(&[1u8; 32]);
158 e.namespace = Namespace::new("t1", "private");
159 let h2 = e.next_chain_head(&[1u8; 32]);
160 assert_ne!(h1, h2);
161 }
162
163 #[test]
164 fn memop_strings_round_trip() {
165 for op in [
166 MemOp::Recall,
167 MemOp::Write,
168 MemOp::Forget,
169 MemOp::Branch,
170 MemOp::ReplayAsOf,
171 MemOp::ExportProvenance,
172 ] {
173 let s = op.as_str();
174 assert!(!s.is_empty());
175 assert_eq!(s, s.to_lowercase());
176 }
177 }
178}