dpp_crypto/access/policy.rs
1//! Sector access policy types and tier lookup.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use dpp_domain::{AccessTier, SectorCatalog};
7
8/// Maps JSON field names to their minimum access tier.
9///
10/// Fields not listed fall back to [`Self::default_tier`]. Matching is by
11/// **normalized leaf key name** (case- and separator-insensitive), so a policy
12/// key `disassemblyInstructions` also covers a payload key
13/// `disassembly_instructions` — closing the casing/nesting drift that let
14/// elevated fields leak at the Public tier (crypto Gap 6).
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct SectorAccessPolicy {
18 /// Human-readable policy name (e.g., `"textile-v1.1"`).
19 pub name: String,
20 /// The sector this policy applies to.
21 pub sector: String,
22 /// Map of JSON field name → minimum access tier. A listed field is matched
23 /// wherever it appears in the document (any nesting depth), by normalized key.
24 pub field_tiers: HashMap<String, AccessTier>,
25 /// Tier applied to fields **not** listed in `field_tiers`. Defaults to
26 /// `Public` (backward-compatible: only elevated fields need listing). Set to
27 /// `Confidential` for a true default-deny (fail-closed) policy, where every
28 /// public field must be explicitly listed as `Public`.
29 #[serde(default = "tier_public")]
30 pub default_tier: AccessTier,
31}
32
33fn tier_public() -> AccessTier {
34 AccessTier::Public
35}
36
37/// Normalize a JSON key for tier matching: drop non-alphanumerics (`_`, `-`)
38/// and lowercase, so `disassemblyInstructions` == `disassembly_instructions`.
39pub(super) fn normalize_key(key: &str) -> String {
40 key.chars()
41 .filter(|c| c.is_ascii_alphanumeric())
42 .map(|c| c.to_ascii_lowercase())
43 .collect()
44}
45
46/// Universal confidential fields present on every published passport payload
47/// (signatures, audit trails). Folded into each sector's policy so they are not
48/// repeated in every manifest.
49const COMMON_CONFIDENTIAL: &[&str] = &[
50 "jwsSignature",
51 "complianceReport",
52 "auditHistory",
53 "supplyChainTrace",
54];
55
56impl SectorAccessPolicy {
57 /// Build a sector's access policy from the catalog's declared per-field
58 /// tiers, folding in the universal confidential fields.
59 ///
60 /// This works for **every** sector with no per-sector Rust code — the tiers
61 /// are data in the sector manifests (`access_tiers`). Returns `None` if
62 /// `sector_key` is not in the catalog.
63 pub fn from_catalog(catalog: &SectorCatalog, sector_key: &str) -> Option<Self> {
64 let descriptor = catalog.get(sector_key)?;
65 let mut field_tiers: HashMap<String, AccessTier> = descriptor.access_tiers.clone();
66 for field in COMMON_CONFIDENTIAL {
67 field_tiers
68 .entry((*field).to_owned())
69 .or_insert(AccessTier::Confidential);
70 }
71 Some(Self {
72 name: format!("{sector_key}-{}", descriptor.current_schema_version),
73 sector: sector_key.to_owned(),
74 field_tiers,
75 default_tier: AccessTier::Public,
76 })
77 }
78
79 /// Default access policy for top-level passport fields (sector-agnostic).
80 pub fn passport_default() -> Self {
81 let mut field_tiers = HashMap::new();
82
83 // Professional tier
84 field_tiers.insert("batchId".into(), AccessTier::Professional);
85
86 // Confidential tier — signature / internal
87 field_tiers.insert("jwsSignature".into(), AccessTier::Confidential);
88 field_tiers.insert("retentionLocked".into(), AccessTier::Confidential);
89
90 Self {
91 name: "passport-v1.0".into(),
92 sector: "passport".into(),
93 field_tiers,
94 default_tier: AccessTier::Public,
95 }
96 }
97
98 /// Get the minimum access tier for a field, matched by normalized key name
99 /// (case/separator-insensitive). Unlisted fields fall back to `default_tier`.
100 pub fn tier_for_field(&self, field_name: &str) -> AccessTier {
101 let target = normalize_key(field_name);
102 self.field_tiers
103 .iter()
104 .find(|(k, _)| normalize_key(k) == target)
105 .map(|(_, t)| *t)
106 .unwrap_or(self.default_tier)
107 }
108}