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