use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::{Disclosure, PASSPORT_FIELD_DISCLOSURE, SectorCatalog};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SectorAccessPolicy {
pub name: String,
pub sector: String,
pub field_disclosure: HashMap<String, Disclosure>,
#[serde(default = "disclosure_public")]
pub default_disclosure: Disclosure,
}
fn disclosure_public() -> Disclosure {
Disclosure::Public
}
fn keys_match_normalized(a: &str, b: &str) -> bool {
let mut a_chars = a.chars().filter(char::is_ascii_alphanumeric);
let mut b_chars = b.chars().filter(char::is_ascii_alphanumeric);
loop {
match (a_chars.next(), b_chars.next()) {
(Some(x), Some(y)) if x.eq_ignore_ascii_case(&y) => {}
(None, None) => return true,
_ => return false,
}
}
}
const COMMON_CONFORMITY: &[&str] = &[
"jwsSignature",
"complianceReport",
"auditHistory",
"supplyChainTrace",
];
fn parse_disclosure(token: &str) -> Option<Disclosure> {
match token {
"public" => Some(Disclosure::Public),
"restricted" => Some(Disclosure::Restricted),
"conformity" => Some(Disclosure::Conformity),
"individual" => Some(Disclosure::Individual),
_ => None,
}
}
fn collect_disclosures(node: &serde_json::Value, out: &mut HashMap<String, Disclosure>) {
let Some(object) = node.as_object() else {
return;
};
if let Some(properties) = object.get("properties").and_then(|p| p.as_object()) {
for (name, prop) in properties {
if let Some(class) = prop
.get("x-disclosure")
.and_then(serde_json::Value::as_str)
.and_then(parse_disclosure)
{
out.entry(name.clone())
.and_modify(|existing| *existing = existing.most_restrictive(class))
.or_insert(class);
}
collect_disclosures(prop, out);
}
}
for key in ["items", "additionalProperties"] {
if let Some(child) = object.get(key) {
collect_disclosures(child, out);
}
}
for key in ["definitions", "$defs"] {
if let Some(block) = object.get(key).and_then(|b| b.as_object()) {
for definition in block.values() {
collect_disclosures(definition, out);
}
}
}
for key in ["allOf", "anyOf", "oneOf"] {
if let Some(branches) = object.get(key).and_then(|b| b.as_array()) {
for branch in branches {
collect_disclosures(branch, out);
}
}
}
}
impl SectorAccessPolicy {
#[must_use]
pub fn for_schema_version(sector_key: &str, version: &str) -> Option<Self> {
let parsed: semver::Version = version.parse().ok()?;
let registry = crate::schemas::VersionedSchemaRegistry::new();
let json = registry.get(sector_key, &parsed)?;
Self::from_schema(sector_key, version, json)
}
#[deprecated(
since = "0.17.0",
note = "use `for_schema_version` — the catalog map is unversioned, so it \
filters published passports by rules that may postdate their signatures"
)]
pub fn from_catalog(catalog: &SectorCatalog, sector_key: &str) -> Option<Self> {
let descriptor = catalog.get(sector_key)?;
let mut field_disclosure: HashMap<String, Disclosure> = descriptor.disclosure.clone();
for field in COMMON_CONFORMITY {
field_disclosure
.entry((*field).to_owned())
.or_insert(Disclosure::Conformity);
}
Some(Self {
name: format!("{sector_key}-{}", descriptor.current_schema_version),
sector: sector_key.to_owned(),
field_disclosure,
default_disclosure: Disclosure::Public,
})
}
#[must_use]
pub fn from_schema(sector_key: &str, version: &str, schema_json: &str) -> Option<Self> {
let schema: serde_json::Value = serde_json::from_str(schema_json).ok()?;
schema.get("properties")?.as_object()?;
let mut field_disclosure: HashMap<String, Disclosure> = HashMap::new();
collect_disclosures(&schema, &mut field_disclosure);
for field in COMMON_CONFORMITY {
field_disclosure
.entry((*field).to_owned())
.or_insert(Disclosure::Conformity);
}
Some(Self {
name: format!("{sector_key}-{version}"),
sector: sector_key.to_owned(),
field_disclosure,
default_disclosure: Disclosure::Public,
})
}
pub fn passport_default() -> Self {
let mut field_disclosure = HashMap::new();
for (field, class) in PASSPORT_FIELD_DISCLOSURE {
field_disclosure.insert((*field).to_owned(), *class);
}
Self {
name: "passport-v1.0".into(),
sector: "passport".into(),
field_disclosure,
default_disclosure: Disclosure::Public,
}
}
pub fn disclosure_for_field(&self, field_name: &str) -> Disclosure {
self.field_disclosure
.iter()
.filter(|(k, _)| keys_match_normalized(k, field_name))
.map(|(_, d)| *d)
.reduce(Disclosure::most_restrictive)
.unwrap_or(self.default_disclosure)
}
}