pic_continuity/authority/
indexed.rs1use super::{AuthorityValue, Invariant, LogicalAuthority};
32use crate::error::RejectReason;
33use serde::{Deserialize, Serialize};
34use std::collections::BTreeMap;
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(untagged)]
40pub enum TupleValue {
41 Text(String),
43 Membership(bool),
45}
46
47impl TupleValue {
48 pub fn validate(&self, key: &str) -> Result<(), RejectReason> {
51 match self {
52 TupleValue::Text(s) if !s.is_empty() => Ok(()),
53 TupleValue::Membership(true) => Ok(()),
54 _ => Err(RejectReason::InvalidAuthorityValue(key.to_string())),
55 }
56 }
57}
58
59pub type KvTuple = (String, TupleValue);
61
62#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
65pub struct InvariantTuple(pub String, pub String, pub String, pub String);
66
67impl From<&Invariant> for InvariantTuple {
68 fn from(i: &Invariant) -> Self {
69 InvariantTuple(
70 i.scope.clone(),
71 i.operation.clone(),
72 i.resource_type.clone(),
73 i.resource_id.clone(),
74 )
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
81pub struct IndexedAuthorityMap {
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub identity_context: Option<BTreeMap<u32, KvTuple>>,
85 pub invariants: BTreeMap<u32, InvariantTuple>,
87 pub execution_contract: BTreeMap<u32, KvTuple>,
89}
90
91fn denormalize_and_index(
93 map: &BTreeMap<String, AuthorityValue>,
94) -> Result<BTreeMap<u32, KvTuple>, RejectReason> {
95 let mut candidates: Vec<KvTuple> = Vec::new();
96 for (key, value) in map {
97 value.validate(key)?;
98 match value {
99 AuthorityValue::One(s) => candidates.push((key.clone(), TupleValue::Text(s.clone()))),
100 AuthorityValue::Many(members) => {
101 for m in members {
102 candidates.push((format!("{key}:{m}"), TupleValue::Membership(true)));
103 }
104 }
105 }
106 }
107 candidates.sort_by(|a, b| a.0.cmp(&b.0));
110 for pair in candidates.windows(2) {
112 if pair[0].0 == pair[1].0 {
113 return Err(RejectReason::DuplicateAdditionKey(pair[0].0.clone()));
114 }
115 }
116 Ok(candidates
117 .into_iter()
118 .enumerate()
119 .map(|(i, t)| (i as u32, t))
120 .collect())
121}
122
123impl IndexedAuthorityMap {
124 pub fn from_logical(logical: &LogicalAuthority) -> Result<Self, RejectReason> {
126 logical.validate()?;
127
128 let identity_context = match &logical.identity_context {
129 Some(map) if !map.is_empty() => Some(denormalize_and_index(map)?),
130 _ => None,
131 };
132
133 let mut invariants: Vec<InvariantTuple> = logical
134 .execution
135 .invariants
136 .iter()
137 .map(InvariantTuple::from)
138 .collect();
139 invariants.sort();
140 let invariants = invariants
141 .into_iter()
142 .enumerate()
143 .map(|(i, t)| (i as u32, t))
144 .collect();
145
146 let execution_contract = denormalize_and_index(&logical.execution.contract)?;
147
148 Ok(Self {
149 identity_context,
150 invariants,
151 execution_contract,
152 })
153 }
154
155 pub fn invariant_count(&self) -> u32 {
157 self.invariants.len() as u32
158 }
159
160 pub fn contains_invariant(&self, tuple: &InvariantTuple) -> bool {
162 self.invariants.values().any(|t| t == tuple)
163 }
164
165 pub fn contains_contract_entry(&self, entry: &KvTuple) -> bool {
167 self.execution_contract.values().any(|t| t == entry)
168 }
169
170 pub fn validate(&self) -> Result<(), RejectReason> {
174 validate_contiguous_indexes(&self.invariants, "invariants")?;
175 validate_contiguous_indexes(&self.execution_contract, "execution_contract")?;
176
177 if let Some(identity_context) = &self.identity_context {
178 if identity_context.is_empty() {
179 return Err(RejectReason::Malformed(
180 "identity_context must be omitted when empty".into(),
181 ));
182 }
183 validate_contiguous_indexes(identity_context, "identity_context")?;
184 validate_kv_section(identity_context)?;
185 }
186
187 if self.execution_contract.is_empty() {
188 return Err(RejectReason::EmptyExecutionContract);
189 }
190 validate_kv_section(&self.execution_contract)?;
191
192 for tuple in self.invariants.values() {
193 if tuple.0.is_empty() || tuple.1.is_empty() || tuple.2.is_empty() || tuple.3.is_empty()
194 {
195 return Err(RejectReason::Malformed(
196 "invariant tuple members must be non-empty".into(),
197 ));
198 }
199 }
200
201 Ok(())
202 }
203}
204
205fn validate_contiguous_indexes<T>(
206 section: &BTreeMap<u32, T>,
207 section_name: &'static str,
208) -> Result<(), RejectReason> {
209 for (expected, actual) in section.keys().enumerate() {
210 if *actual != expected as u32 {
211 return Err(RejectReason::Malformed(format!(
212 "{section_name} indexes must be contiguous from 0"
213 )));
214 }
215 }
216 Ok(())
217}
218
219fn validate_kv_section(section: &BTreeMap<u32, KvTuple>) -> Result<(), RejectReason> {
220 for (key, value) in section.values() {
221 if key.is_empty() {
222 return Err(RejectReason::InvalidAuthorityValue(key.clone()));
223 }
224 value.validate(key)?;
225 }
226 Ok(())
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::authority::AuthorityValue;
233
234 #[test]
237 fn canonicalization_matches_reference_example() {
238 let mut identity = BTreeMap::new();
239 identity.insert("type".into(), AuthorityValue::One("user".into()));
240 identity.insert("id".into(), AuthorityValue::One("user-123".into()));
241 identity.insert(
242 "roles".into(),
243 AuthorityValue::Many(vec!["payment-approver".into()]),
244 );
245 identity.insert(
246 "groups".into(),
247 AuthorityValue::Many(vec!["finance".into()]),
248 );
249 identity.insert(
250 "securityDomain".into(),
251 AuthorityValue::One("tenant-a".into()),
252 );
253
254 let mut contract = BTreeMap::new();
255 contract.insert(
256 "purpose".into(),
257 AuthorityValue::One("payment-approval".into()),
258 );
259 contract.insert("currency".into(), AuthorityValue::One("EUR".into()));
260
261 let logical = LogicalAuthority::new(
262 Some(identity),
263 vec![Invariant::new(
264 "payments:approve",
265 "approve",
266 "payments",
267 "*",
268 )],
269 contract,
270 );
271
272 let map = IndexedAuthorityMap::from_logical(&logical).unwrap();
273 let id = map.identity_context.unwrap();
274
275 assert_eq!(id[&0].0, "groups:finance");
276 assert_eq!(id[&0].1, TupleValue::Membership(true));
277 assert_eq!(id[&1].0, "id");
278 assert_eq!(id[&2].0, "roles:payment-approver");
279 assert_eq!(id[&3].0, "securityDomain");
280 assert_eq!(id[&4].0, "type");
281
282 assert_eq!(
283 map.invariants[&0],
284 InvariantTuple(
285 "payments:approve".into(),
286 "approve".into(),
287 "payments".into(),
288 "*".into()
289 )
290 );
291
292 assert_eq!(map.execution_contract[&0].0, "currency");
294 assert_eq!(map.execution_contract[&1].0, "purpose");
295 }
296
297 #[test]
298 fn invariants_sorted_by_tuple_elements() {
299 let mut contract = BTreeMap::new();
300 contract.insert("corporation".into(), AuthorityValue::One("ACME".into()));
301
302 let logical = LogicalAuthority::new(
303 None,
304 vec![
305 Invariant::new("storage:save", "save", "storage", "*"),
306 Invariant::new(
307 "documents:read:document-42",
308 "read",
309 "documents",
310 "document-42",
311 ),
312 ],
313 contract,
314 );
315
316 let map = IndexedAuthorityMap::from_logical(&logical).unwrap();
317 assert_eq!(map.invariants[&0].0, "documents:read:document-42");
318 assert_eq!(map.invariants[&1].0, "storage:save");
319 }
320
321 #[test]
322 fn rejects_empty_contract_and_invalid_values() {
323 let logical = LogicalAuthority::default();
324 assert_eq!(
325 logical.validate().unwrap_err(),
326 RejectReason::EmptyExecutionContract
327 );
328
329 let mut contract = BTreeMap::new();
330 contract.insert("corporation".into(), AuthorityValue::One("".into()));
331 let logical = LogicalAuthority::new(None, vec![], contract);
332 assert!(matches!(
333 logical.validate().unwrap_err(),
334 RejectReason::InvalidAuthorityValue(_)
335 ));
336
337 let mut contract = BTreeMap::new();
338 contract.insert("departments".into(), AuthorityValue::Many(vec![]));
339 let logical = LogicalAuthority::new(None, vec![], contract);
340 assert!(matches!(
341 logical.validate().unwrap_err(),
342 RejectReason::InvalidAuthorityValue(_)
343 ));
344 }
345
346 #[test]
347 fn cbor_roundtrip() {
348 let mut contract = BTreeMap::new();
349 contract.insert("corporation".into(), AuthorityValue::One("ACME".into()));
350 let logical = LogicalAuthority::new(
351 None,
352 vec![Invariant::new("storage:save", "save", "storage", "*")],
353 contract,
354 );
355 let map = IndexedAuthorityMap::from_logical(&logical).unwrap();
356
357 let mut buf = Vec::new();
358 ciborium::into_writer(&map, &mut buf).unwrap();
359 let decoded: IndexedAuthorityMap = ciborium::from_reader(buf.as_slice()).unwrap();
360 assert_eq!(map, decoded);
361 }
362}