use crate::config::PolicyConfig;
#[derive(Clone, Default, Debug, PartialEq)]
pub struct Policy {
pub allow: Vec<String>,
pub deny: Vec<String>,
pub redact_keys: Vec<String>,
pub require_approval: Vec<String>,
pub approval_timeout_secs: u64,
pub webhook: Option<crate::config::WebhookConfig>,
}
impl Policy {
pub fn from_config(config: Option<PolicyConfig>) -> Self {
let Some(config) = config else {
return Self::default();
};
Self {
allow: config.allow,
deny: config.deny,
redact_keys: config.redact_keys,
require_approval: config.require_approval,
approval_timeout_secs: config.approval_timeout_secs.unwrap_or(300),
webhook: config.webhook,
}
}
pub fn allows(&self, id: impl AsRef<str>) -> bool {
let id_ref = id.as_ref();
if self
.deny
.iter()
.any(|pattern| wildcard_match(pattern, id_ref))
{
return false;
}
if self.allow.is_empty() {
return true;
}
self.allow
.iter()
.any(|pattern| wildcard_match(pattern, id_ref))
}
pub fn requires_approval(&self, id: impl AsRef<str>) -> bool {
let id_ref = id.as_ref();
self.require_approval
.iter()
.any(|pattern| wildcard_match(pattern, id_ref))
}
pub fn merge_with_profile(&self, profile_policy: Option<&Policy>) -> Policy {
let Some(prof_pol) = profile_policy else {
return self.clone();
};
let mut deny = self.deny.clone();
for d in &prof_pol.deny {
if !deny.contains(d) {
deny.push(d.clone());
}
}
let mut require_approval = self.require_approval.clone();
for r in &prof_pol.require_approval {
if !require_approval.contains(r) {
require_approval.push(r.clone());
}
}
let mut redact_keys = self.redact_keys.clone();
for k in &prof_pol.redact_keys {
if !redact_keys.contains(k) {
redact_keys.push(k.clone());
}
}
let allow = if !prof_pol.allow.is_empty() {
prof_pol.allow.clone()
} else {
self.allow.clone()
};
Policy {
allow,
deny,
redact_keys,
require_approval,
approval_timeout_secs: if prof_pol.approval_timeout_secs > 0
&& prof_pol.approval_timeout_secs != 300
{
prof_pol.approval_timeout_secs
} else {
self.approval_timeout_secs
},
webhook: prof_pol.webhook.clone().or_else(|| self.webhook.clone()),
}
}
}
pub fn wildcard_match(pattern: &str, value: &str) -> bool {
if pattern == "*" {
return true;
}
if !pattern.contains('*') {
return pattern == value;
}
let parts: Vec<&str> = pattern.split('*').collect();
let mut remaining = value;
if !pattern.starts_with('*') {
let first = parts[0];
if !remaining.starts_with(first) {
return false;
}
remaining = &remaining[first.len()..];
}
if !pattern.ends_with('*') {
let last = parts[parts.len() - 1];
if !remaining.ends_with(last) {
return false;
}
remaining = &remaining[..remaining.len() - last.len()];
}
if parts.len() > 2 {
for part in &parts[1..parts.len() - 1] {
if part.is_empty() {
continue;
}
if let Some(pos) = remaining.find(part) {
remaining = &remaining[pos + part.len()..];
} else {
return false;
}
}
}
true
}