use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::security::ENRICHED_NAMESPACE_PREFIX;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SubscriptionPolicy {
pub owner_path: String,
pub identity_field: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bypass_roles: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OwnerCondition {
Bypass,
Eq {
field: String,
value: Value,
},
Refuse(String),
}
impl SubscriptionPolicy {
#[must_use]
pub fn owner_field(&self) -> &str {
self.owner_path.trim_start_matches("$.")
}
pub fn validate(&self) -> Result<(), String> {
if self.identity_field.is_empty() {
return Err("subscription_policy.identity_field must not be empty".to_string());
}
let field = self.owner_field();
if field.is_empty() {
return Err("subscription_policy.owner_path must name a field (e.g. \"$.owner_id\")"
.to_string());
}
if field.contains('.') || field.contains('[') {
return Err(format!(
"subscription_policy.owner_path `{}` must be a single-level `$.<field>` — nested \
paths are not supported by the delivery matchers",
self.owner_path
));
}
Ok(())
}
#[must_use]
pub fn derive(&self, attributes: &HashMap<String, Value>, roles: &[String]) -> OwnerCondition {
if self.bypass_roles.iter().any(|bypass| roles.iter().any(|role| role == bypass)) {
return OwnerCondition::Bypass;
}
let key = format!("{ENRICHED_NAMESPACE_PREFIX}{}", self.identity_field);
match attributes.get(&key) {
Some(value) if !value.is_null() => OwnerCondition::Eq {
field: self.owner_field().to_string(),
value: value.clone(),
},
_ => OwnerCondition::Refuse(format!(
"subscription refused (fail-closed): enriched identity field `{}` is unresolvable \
— configure `[identity.enrichment]` so the owner boundary can be derived",
self.identity_field
)),
}
}
}
#[cfg(test)]
mod tests;