Skip to main content

riffdb_types/
principal_fact.rs

1//! Canonical bounded principal facts for compiler-owned row policy.
2
3use std::{error::Error, fmt};
4
5use crate::{CanonicalValue, decode_canonical_value, encode_canonical_value};
6
7/// Immutable principal-fact encoding version.
8pub const PRINCIPAL_FACT_SET_VERSION_V1: u32 = 1;
9/// Maximum fact schemas and values carried by one capability.
10pub const MAX_PRINCIPAL_FACTS_V1: usize = 32;
11/// Maximum scalar members of one list-valued fact.
12pub const MAX_PRINCIPAL_FACT_VALUES_V1: usize = 64;
13/// Maximum canonical bytes across one complete fact set.
14pub const MAX_PRINCIPAL_FACT_BYTES_V1: usize = 64 * 1024;
15const MAX_PRINCIPAL_FACT_NAME_BYTES_V1: usize = 256;
16const FACT_SET_MAGIC: &[u8] = b"RIFFDB-PRINCIPAL-FACTS\0";
17
18/// Safe failure to construct a bounded principal-fact set.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum PrincipalFactError {
21    /// A fact name is not a compiler-visible source identifier.
22    InvalidName,
23    /// A fact value is not one scalar or one bounded scalar set.
24    InvalidValue,
25    /// A count or encoded byte ceiling was exceeded.
26    LimitExceeded,
27    /// A fact or set member occurs more than once.
28    Duplicate,
29    /// Canonical encoding failed or overflowed.
30    InvalidEncoding,
31}
32
33impl fmt::Display for PrincipalFactError {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        formatter.write_str(match self {
36            Self::InvalidName => "principal fact name is invalid",
37            Self::InvalidValue => "principal fact value has an unsupported shape",
38            Self::LimitExceeded => "principal fact value exceeds a hard limit",
39            Self::Duplicate => "principal fact value contains a duplicate identity",
40            Self::InvalidEncoding => "principal fact canonical encoding failed",
41        })
42    }
43}
44
45impl Error for PrincipalFactError {}
46
47/// One canonical named authorization fact.
48#[derive(Clone, Eq, PartialEq)]
49pub struct CapabilityPrincipalFactV1 {
50    name: String,
51    value: CanonicalValue,
52    encoded_value: crate::OwnedCapacitySliceV1<u8>,
53}
54impl crate::InspectOwnedCapacityV1 for CapabilityPrincipalFactV1 {
55    fn inspect_owned_capacity(
56        &self,
57        inspection: &mut crate::OwnedCapacityInspectionV1,
58    ) -> Result<(), crate::OwnedCapacityInspectionErrorV1> {
59        let Self {
60            name,
61            value,
62            encoded_value,
63        } = self;
64        inspection.visit(name)?;
65        inspection.visit(value)?;
66        inspection.visit(encoded_value)?;
67        Ok(())
68    }
69}
70
71impl CapabilityPrincipalFactV1 {
72    /// Constructs one scalar fact or one canonical bounded scalar set.
73    pub fn new(name: impl Into<String>, value: CanonicalValue) -> Result<Self, PrincipalFactError> {
74        let name = name.into();
75        validate_name(&name)?;
76        let value = canonicalize_value(value)?;
77        let encoded_value =
78            encode_canonical_value(&value).map_err(|_| PrincipalFactError::InvalidEncoding)?;
79        if encoded_value.len() > MAX_PRINCIPAL_FACT_BYTES_V1 {
80            return Err(PrincipalFactError::LimitExceeded);
81        }
82        Ok(Self {
83            name,
84            value,
85            encoded_value: encoded_value.into(),
86        })
87    }
88
89    /// Compiler-visible symbolic fact name.
90    #[doc(hidden)]
91    #[must_use]
92    pub fn name(&self) -> &str {
93        &self.name
94    }
95
96    /// Exact value for trusted policy evaluation. Formatting remains redacted.
97    #[doc(hidden)]
98    #[must_use]
99    pub const fn internal_value(&self) -> &CanonicalValue {
100        &self.value
101    }
102
103    /// Exact canonical value bytes for persistence and narrowing comparison.
104    #[doc(hidden)]
105    #[must_use]
106    pub fn internal_encoded_value(&self) -> &[u8] {
107        &self.encoded_value
108    }
109
110    fn is_narrowing_of(&self, parent: &Self) -> bool {
111        if self.name != parent.name {
112            return false;
113        }
114        match (&self.value, &parent.value) {
115            (CanonicalValue::List(child), CanonicalValue::List(parent)) => child
116                .values()
117                .iter()
118                .all(|candidate| parent.values().iter().any(|value| value == candidate)),
119            _ => self.encoded_value == parent.encoded_value,
120        }
121    }
122}
123
124impl fmt::Debug for CapabilityPrincipalFactV1 {
125    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126        formatter.write_str("CapabilityPrincipalFactV1([REDACTED])")
127    }
128}
129
130/// Complete canonical fact set bound to one current capability revision.
131#[derive(Clone, Eq, PartialEq)]
132pub struct CapabilityPrincipalFactsV1 {
133    facts: crate::OwnedCapacitySliceV1<CapabilityPrincipalFactV1>,
134    canonical_bytes: crate::OwnedCapacitySliceV1<u8>,
135}
136impl crate::InspectOwnedCapacityV1 for CapabilityPrincipalFactsV1 {
137    fn inspect_owned_capacity(
138        &self,
139        inspection: &mut crate::OwnedCapacityInspectionV1,
140    ) -> Result<(), crate::OwnedCapacityInspectionErrorV1> {
141        let Self {
142            facts,
143            canonical_bytes,
144        } = self;
145        inspection.visit(facts)?;
146        inspection.visit(canonical_bytes)?;
147        Ok(())
148    }
149}
150
151impl CapabilityPrincipalFactsV1 {
152    /// Sorts facts by name, rejects duplicates, and enforces the complete byte ceiling.
153    pub fn new(mut facts: Vec<CapabilityPrincipalFactV1>) -> Result<Self, PrincipalFactError> {
154        if facts.len() > MAX_PRINCIPAL_FACTS_V1 {
155            return Err(PrincipalFactError::LimitExceeded);
156        }
157        facts.sort_unstable_by(|left, right| left.name.cmp(&right.name));
158        if facts.windows(2).any(|pair| pair[0].name == pair[1].name) {
159            return Err(PrincipalFactError::Duplicate);
160        }
161        let canonical_bytes = encode_set(&facts)?;
162        if canonical_bytes.len() > MAX_PRINCIPAL_FACT_BYTES_V1 {
163            return Err(PrincipalFactError::LimitExceeded);
164        }
165        Ok(Self {
166            facts: facts.into(),
167            canonical_bytes: canonical_bytes.into(),
168        })
169    }
170
171    /// Empty facts for roles whose policies require no principal attributes.
172    #[must_use]
173    pub fn empty() -> Self {
174        Self::new(Vec::new()).expect("the empty principal fact set is bounded")
175    }
176
177    /// Compiler-visible fact names without values.
178    #[doc(hidden)]
179    pub fn names(&self) -> impl ExactSizeIterator<Item = &str> {
180        self.facts.iter().map(|fact| fact.name.as_str())
181    }
182
183    /// Strictly decodes one canonical bounded capability fact set.
184    #[doc(hidden)]
185    pub fn decode_canonical(bytes: &[u8]) -> Result<Self, PrincipalFactError> {
186        if bytes.len() > MAX_PRINCIPAL_FACT_BYTES_V1 {
187            return Err(PrincipalFactError::LimitExceeded);
188        }
189        let mut reader = FactReader::new(bytes);
190        if reader.take(FACT_SET_MAGIC.len())? != FACT_SET_MAGIC
191            || reader.u32()? != PRINCIPAL_FACT_SET_VERSION_V1
192        {
193            return Err(PrincipalFactError::InvalidEncoding);
194        }
195        let count = reader.len(MAX_PRINCIPAL_FACTS_V1)?;
196        let mut facts = Vec::with_capacity(count);
197        for _ in 0..count {
198            let name = std::str::from_utf8(reader.bytes(MAX_PRINCIPAL_FACT_NAME_BYTES_V1)?)
199                .map_err(|_| PrincipalFactError::InvalidEncoding)?;
200            let value = decode_canonical_value(reader.bytes(MAX_PRINCIPAL_FACT_BYTES_V1)?)
201                .map_err(|_| PrincipalFactError::InvalidEncoding)?;
202            facts.push(CapabilityPrincipalFactV1::new(name, value)?);
203        }
204        if !reader.is_empty() {
205            return Err(PrincipalFactError::InvalidEncoding);
206        }
207        let decoded = Self::new(facts)?;
208        if decoded.canonical_bytes.as_ref() != bytes {
209            return Err(PrincipalFactError::InvalidEncoding);
210        }
211        Ok(decoded)
212    }
213
214    /// Looks up one trusted fact for authoritative policy evaluation.
215    #[doc(hidden)]
216    #[must_use]
217    pub fn internal_fact(&self, name: &str) -> Option<&CapabilityPrincipalFactV1> {
218        self.facts
219            .binary_search_by(|fact| fact.name.as_str().cmp(name))
220            .ok()
221            .map(|index| &self.facts[index])
222    }
223
224    /// Complete versioned canonical bytes for storage and capability identity.
225    #[doc(hidden)]
226    #[must_use]
227    pub fn internal_canonical_bytes(&self) -> &[u8] {
228        &self.canonical_bytes
229    }
230
231    /// Returns true only when every child fact is equal to or a set-subset of its parent fact.
232    /// Dropping facts is narrowing; adding, changing, or widening facts is not.
233    #[must_use]
234    pub fn is_narrowing_of(&self, parent: &Self) -> bool {
235        self.facts.iter().all(|child| {
236            parent
237                .facts
238                .binary_search_by(|fact| fact.name.cmp(&child.name))
239                .ok()
240                .is_some_and(|index| child.is_narrowing_of(&parent.facts[index]))
241        })
242    }
243}
244
245impl fmt::Debug for CapabilityPrincipalFactsV1 {
246    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
247        formatter
248            .debug_struct("CapabilityPrincipalFactsV1")
249            .field("facts", &"[REDACTED]")
250            .field("fact_count", &self.facts.len())
251            .finish()
252    }
253}
254
255fn canonicalize_value(value: CanonicalValue) -> Result<CanonicalValue, PrincipalFactError> {
256    match value {
257        CanonicalValue::List(list) => {
258            if list.len() > MAX_PRINCIPAL_FACT_VALUES_V1 {
259                return Err(PrincipalFactError::LimitExceeded);
260            }
261            let mut members = list
262                .values()
263                .iter()
264                .cloned()
265                .map(|member| {
266                    if !is_scalar(&member) {
267                        return Err(PrincipalFactError::InvalidValue);
268                    }
269                    let bytes = encode_canonical_value(&member)
270                        .map_err(|_| PrincipalFactError::InvalidEncoding)?;
271                    Ok((bytes, member))
272                })
273                .collect::<Result<Vec<_>, PrincipalFactError>>()?;
274            members.sort_unstable_by(|left, right| left.0.cmp(&right.0));
275            if members.windows(2).any(|pair| pair[0].0 == pair[1].0) {
276                return Err(PrincipalFactError::Duplicate);
277            }
278            CanonicalValue::list(members.into_iter().map(|(_, member)| member).collect())
279                .map_err(|_| PrincipalFactError::InvalidValue)
280        }
281        scalar if is_scalar(&scalar) => Ok(scalar),
282        _ => Err(PrincipalFactError::InvalidValue),
283    }
284}
285
286fn is_scalar(value: &CanonicalValue) -> bool {
287    matches!(
288        value,
289        CanonicalValue::Bool(_)
290            | CanonicalValue::I64(_)
291            | CanonicalValue::U64(_)
292            | CanonicalValue::Decimal(_)
293            | CanonicalValue::Money(_)
294            | CanonicalValue::String(_)
295            | CanonicalValue::Bytes(_)
296            | CanonicalValue::Timestamp(_)
297            | CanonicalValue::Date(_)
298            | CanonicalValue::Uuid(_)
299            | CanonicalValue::Enum { .. }
300    )
301}
302
303fn validate_name(name: &str) -> Result<(), PrincipalFactError> {
304    let mut bytes = name.bytes();
305    let first = bytes.next().ok_or(PrincipalFactError::InvalidName)?;
306    if name.len() > MAX_PRINCIPAL_FACT_NAME_BYTES_V1
307        || !(first == b'_' || first.is_ascii_alphabetic())
308        || bytes.any(|byte| !(byte == b'_' || byte.is_ascii_alphanumeric()))
309    {
310        return Err(PrincipalFactError::InvalidName);
311    }
312    Ok(())
313}
314
315fn encode_set(facts: &[CapabilityPrincipalFactV1]) -> Result<Vec<u8>, PrincipalFactError> {
316    let mut bytes = Vec::new();
317    bytes.extend_from_slice(FACT_SET_MAGIC);
318    bytes.extend_from_slice(&PRINCIPAL_FACT_SET_VERSION_V1.to_be_bytes());
319    push_len(&mut bytes, facts.len())?;
320    for fact in facts {
321        push_bytes(&mut bytes, fact.name.as_bytes())?;
322        push_bytes(&mut bytes, &fact.encoded_value)?;
323        if bytes.len() > MAX_PRINCIPAL_FACT_BYTES_V1 {
324            return Err(PrincipalFactError::LimitExceeded);
325        }
326    }
327    Ok(bytes)
328}
329
330fn push_bytes(bytes: &mut Vec<u8>, value: &[u8]) -> Result<(), PrincipalFactError> {
331    push_len(bytes, value.len())?;
332    bytes.extend_from_slice(value);
333    Ok(())
334}
335
336fn push_len(bytes: &mut Vec<u8>, value: usize) -> Result<(), PrincipalFactError> {
337    let value = u32::try_from(value).map_err(|_| PrincipalFactError::LimitExceeded)?;
338    bytes.extend_from_slice(&value.to_be_bytes());
339    Ok(())
340}
341
342struct FactReader<'a> {
343    bytes: &'a [u8],
344    offset: usize,
345}
346
347impl<'a> FactReader<'a> {
348    const fn new(bytes: &'a [u8]) -> Self {
349        Self { bytes, offset: 0 }
350    }
351
352    fn take(&mut self, len: usize) -> Result<&'a [u8], PrincipalFactError> {
353        let end = self
354            .offset
355            .checked_add(len)
356            .ok_or(PrincipalFactError::InvalidEncoding)?;
357        let value = self
358            .bytes
359            .get(self.offset..end)
360            .ok_or(PrincipalFactError::InvalidEncoding)?;
361        self.offset = end;
362        Ok(value)
363    }
364
365    fn u32(&mut self) -> Result<u32, PrincipalFactError> {
366        let bytes: [u8; 4] = self
367            .take(4)?
368            .try_into()
369            .map_err(|_| PrincipalFactError::InvalidEncoding)?;
370        Ok(u32::from_be_bytes(bytes))
371    }
372
373    fn len(&mut self, maximum: usize) -> Result<usize, PrincipalFactError> {
374        let len = usize::try_from(self.u32()?).map_err(|_| PrincipalFactError::LimitExceeded)?;
375        if len > maximum {
376            return Err(PrincipalFactError::LimitExceeded);
377        }
378        Ok(len)
379    }
380
381    fn bytes(&mut self, maximum: usize) -> Result<&'a [u8], PrincipalFactError> {
382        let len = self.len(maximum)?;
383        self.take(len)
384    }
385
386    fn is_empty(&self) -> bool {
387        self.offset == self.bytes.len()
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    fn text(value: &str) -> CanonicalValue {
396        CanonicalValue::string(value).expect("bounded text")
397    }
398
399    #[test]
400    fn fact_sets_are_canonical_redacted_and_narrowing_only() {
401        let parent = CapabilityPrincipalFactsV1::new(vec![
402            CapabilityPrincipalFactV1::new(
403                "groups",
404                CanonicalValue::list(vec![text("operators"), text("authors")])
405                    .expect("bounded list"),
406            )
407            .expect("fact"),
408            CapabilityPrincipalFactV1::new("region", text("us-central")).expect("fact"),
409        ])
410        .expect("parent facts");
411        let child = CapabilityPrincipalFactsV1::new(vec![
412            CapabilityPrincipalFactV1::new(
413                "groups",
414                CanonicalValue::list(vec![text("authors")]).expect("bounded list"),
415            )
416            .expect("fact"),
417        ])
418        .expect("child facts");
419        let widened = CapabilityPrincipalFactsV1::new(vec![
420            CapabilityPrincipalFactV1::new(
421                "groups",
422                CanonicalValue::list(vec![text("authors"), text("owners")]).expect("bounded list"),
423            )
424            .expect("fact"),
425        ])
426        .expect("widened facts");
427
428        assert!(child.is_narrowing_of(&parent));
429        assert!(!widened.is_narrowing_of(&parent));
430        assert_eq!(parent.names().collect::<Vec<_>>(), ["groups", "region"]);
431        assert_eq!(
432            format!("{parent:?}"),
433            "CapabilityPrincipalFactsV1 { facts: \"[REDACTED]\", fact_count: 2 }"
434        );
435        assert!(!format!("{parent:?}").contains("authors"));
436        assert_eq!(
437            CapabilityPrincipalFactsV1::decode_canonical(parent.internal_canonical_bytes())
438                .expect("strict round trip"),
439            parent
440        );
441    }
442
443    #[test]
444    fn ordering_duplicates_and_limits_fail_closed() {
445        let first = CapabilityPrincipalFactsV1::new(vec![
446            CapabilityPrincipalFactV1::new("zeta", CanonicalValue::Bool(true)).expect("fact"),
447            CapabilityPrincipalFactV1::new("alpha", CanonicalValue::U64(7)).expect("fact"),
448        ])
449        .expect("facts");
450        let second = CapabilityPrincipalFactsV1::new(vec![
451            CapabilityPrincipalFactV1::new("alpha", CanonicalValue::U64(7)).expect("fact"),
452            CapabilityPrincipalFactV1::new("zeta", CanonicalValue::Bool(true)).expect("fact"),
453        ])
454        .expect("facts");
455        assert_eq!(
456            first.internal_canonical_bytes(),
457            second.internal_canonical_bytes()
458        );
459        assert_eq!(
460            CapabilityPrincipalFactsV1::new(vec![
461                CapabilityPrincipalFactV1::new("same", CanonicalValue::Bool(true)).expect("fact"),
462                CapabilityPrincipalFactV1::new("same", CanonicalValue::Bool(false)).expect("fact"),
463            ])
464            .expect_err("duplicate name"),
465            PrincipalFactError::Duplicate
466        );
467        assert_eq!(
468            CapabilityPrincipalFactV1::new("bad-name", CanonicalValue::Bool(true))
469                .expect_err("invalid name"),
470            PrincipalFactError::InvalidName
471        );
472        let mut trailing = first.internal_canonical_bytes().to_vec();
473        trailing.push(0);
474        assert_eq!(
475            CapabilityPrincipalFactsV1::decode_canonical(&trailing).expect_err("trailing bytes"),
476            PrincipalFactError::InvalidEncoding
477        );
478        for end in 0..first.internal_canonical_bytes().len() {
479            assert!(
480                CapabilityPrincipalFactsV1::decode_canonical(
481                    &first.internal_canonical_bytes()[..end]
482                )
483                .is_err()
484            );
485        }
486    }
487}