use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::{Disclosure, PASSPORT_FIELD_DISCLOSURE, ProductGroupCatalog};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProductGroupAccessPolicy {
pub name: String,
pub product_group: String,
pub field_disclosure: HashMap<String, Disclosure>,
#[serde(default)]
pub envelope_disclosure: HashMap<String, Disclosure>,
#[serde(default = "disclosure_public")]
pub default_disclosure: Disclosure,
}
fn disclosure_public() -> Disclosure {
Disclosure::Public
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DocumentScope {
Envelope,
ProductGroupData,
}
fn path_suffix_depth(policy_key: &str, path: &[&str]) -> Option<usize> {
let key_segments = policy_key.split('.').count();
if key_segments > path.len() {
let (Some(key_leaf), Some(query_leaf)) = (policy_key.split('.').next_back(), path.last())
else {
return None;
};
return keys_match_normalized(key_leaf, query_leaf).then_some(0);
}
let tail = &path[path.len() - key_segments..];
policy_key
.split('.')
.zip(tail.iter())
.all(|(k, p)| keys_match_normalized(k, p))
.then_some(key_segments)
}
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 common_conformity() -> HashMap<String, Disclosure> {
COMMON_CONFORMITY
.iter()
.map(|field| ((*field).to_owned(), Disclosure::Conformity))
.collect()
}
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 record_disclosure(out: &mut HashMap<String, Disclosure>, key: String, class: Disclosure) {
out.entry(key)
.and_modify(|existing| *existing = existing.most_restrictive(class))
.or_insert(class);
}
fn resolve_local_ref<'a>(
node: &serde_json::Map<String, serde_json::Value>,
root: &'a serde_json::Value,
) -> Option<(&'a serde_json::Value, String)> {
let pointer = node.get("$ref").and_then(serde_json::Value::as_str)?;
let path = pointer.strip_prefix('#')?;
let target = root.pointer(path)?;
Some((target, pointer.to_owned()))
}
fn collect_disclosures(
node: &serde_json::Value,
path: &str,
root: &serde_json::Value,
active_refs: &mut Vec<String>,
out: &mut HashMap<String, Disclosure>,
) {
let Some(object) = node.as_object() else {
return;
};
if let Some((target, pointer)) = resolve_local_ref(object, root)
&& !active_refs.iter().any(|seen| seen == &pointer)
{
active_refs.push(pointer);
collect_disclosures(target, path, root, active_refs, out);
active_refs.pop();
}
if let Some(properties) = object.get("properties").and_then(|p| p.as_object()) {
for (name, prop) in properties {
let child_path = if path.is_empty() {
name.clone()
} else {
format!("{path}.{name}")
};
if let Some(class) = prop
.get("x-disclosure")
.and_then(serde_json::Value::as_str)
.and_then(parse_disclosure)
{
record_disclosure(out, child_path.clone(), class);
}
collect_disclosures(prop, &child_path, root, active_refs, out);
}
}
if let Some(child) = object.get("items") {
collect_disclosures(child, path, root, active_refs, out);
}
if let Some(child) = object.get("additionalProperties") {
collect_disclosures(child, "", root, active_refs, 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, "", root, active_refs, 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, path, root, active_refs, out);
}
}
}
}
impl ProductGroupAccessPolicy {
#[must_use]
pub fn for_schema_version(product_group_key: &str, version: &str) -> Option<Self> {
let parsed: semver::Version = version.parse().ok()?;
let registry = crate::schemas::VersionedSchemaRegistry::new();
let json = registry.get(product_group_key, &parsed)?;
Self::from_schema(product_group_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: &ProductGroupCatalog, product_group_key: &str) -> Option<Self> {
let descriptor = catalog.get(product_group_key)?;
let field_disclosure: HashMap<String, Disclosure> = descriptor.disclosure.clone();
Some(Self {
name: format!("{product_group_key}-{}", descriptor.current_schema_version),
product_group: product_group_key.to_owned(),
field_disclosure,
envelope_disclosure: common_conformity(),
default_disclosure: Disclosure::Public,
})
}
#[must_use]
pub fn from_schema(product_group_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();
let mut active_refs: Vec<String> = Vec::new();
collect_disclosures(
&schema,
"",
&schema,
&mut active_refs,
&mut field_disclosure,
);
Some(Self {
name: format!("{product_group_key}-{version}"),
product_group: product_group_key.to_owned(),
field_disclosure,
envelope_disclosure: common_conformity(),
default_disclosure: Disclosure::Public,
})
}
pub fn passport_default() -> Self {
let mut envelope_disclosure = common_conformity();
for (field, class) in PASSPORT_FIELD_DISCLOSURE {
envelope_disclosure.insert((*field).to_owned(), *class);
}
Self {
name: "passport-v1.0".into(),
product_group: "passport".into(),
field_disclosure: HashMap::new(),
envelope_disclosure,
default_disclosure: Disclosure::Public,
}
}
#[must_use]
pub fn disclosure_for_key(&self, key: &str, scope: DocumentScope) -> Disclosure {
self.disclosure_for_path(&[key], scope)
}
#[must_use]
pub fn disclosure_for_path(&self, path: &[&str], scope: DocumentScope) -> Disclosure {
let scoped = match scope {
DocumentScope::ProductGroupData => Some(&self.field_disclosure),
DocumentScope::Envelope => None,
};
let mut best: Option<(usize, Disclosure)> = None;
for (policy_key, class) in scoped
.into_iter()
.chain(std::iter::once(&self.envelope_disclosure))
.flat_map(|map| map.iter())
{
let Some(depth) = path_suffix_depth(policy_key, path) else {
continue;
};
best = Some(match best {
None => (depth, *class),
Some((best_depth, _)) if depth > best_depth => (depth, *class),
Some((best_depth, best_class)) if depth == best_depth => {
(best_depth, best_class.most_restrictive(*class))
}
Some(kept) => kept,
});
}
best.map_or(self.default_disclosure, |(_, class)| class)
}
#[must_use]
pub fn disclosure_for_field(&self, field_name: &str) -> Disclosure {
self.disclosure_for_key(field_name, DocumentScope::ProductGroupData)
}
}