Skip to main content

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}