use std::{
ops::ControlFlow,
sync::Arc,
};
use super::{
AllowRule,
FieldClassification,
FieldMatchKind,
FieldNameMatching,
RedactionFloor,
ResolvedField,
SensitiveFieldRule,
Sensitivity,
UnknownFieldPolicy,
internal::{
RedactionPolicyInner,
visit_canonical_field_candidates,
},
};
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedactionRules {
application: Arc<RedactionPolicyInner>,
floor: Option<RedactionFloor>,
}
impl RedactionRules {
pub(crate) fn new(
application: RedactionPolicyInner,
floor: Option<RedactionFloor>,
) -> Self {
Self {
application: Arc::new(application),
floor,
}
}
#[inline]
pub fn floor(&self) -> Option<&RedactionFloor> {
self.floor.as_ref()
}
pub fn with_floor(mut self, floor: RedactionFloor) -> Self {
self.floor = Some(floor);
self
}
pub fn disable_floor(mut self) -> Self {
self.floor = None;
self
}
pub fn classify_field<'a>(
&'a self,
field: &str,
) -> FieldClassification<'a> {
classify_inner(
&self.application,
field,
self.application.matching,
true,
)
}
#[inline]
pub fn sensitivity_for(&self, field: &str) -> Option<Sensitivity> {
match self.resolve_field(field) {
ResolvedField::Sensitive { sensitivity } => Some(sensitivity),
ResolvedField::PassThrough => None,
}
}
pub(crate) fn sensitivity_for_exact(
&self,
field: &str,
) -> Option<Sensitivity> {
match self.resolve_field_exact(field) {
ResolvedField::Sensitive { sensitivity } => Some(sensitivity),
ResolvedField::PassThrough => None,
}
}
pub(crate) fn resolve_field_exact(&self, field: &str) -> ResolvedField {
let application = sensitivity_inner(
&self.application,
field,
FieldNameMatching::Exact,
true,
);
let floor = self.floor.as_ref().and_then(|floor| {
sensitivity_inner(
&floor.inner,
field,
FieldNameMatching::Exact,
false,
)
});
match self.floor.as_ref().zip(floor) {
Some((_floor, floor_level)) => ResolvedField::Sensitive {
sensitivity: application
.map_or(floor_level, |level| level.max(floor_level)),
},
None => match application {
Some(sensitivity) => ResolvedField::Sensitive { sensitivity },
None => ResolvedField::PassThrough,
},
}
}
#[inline]
pub(crate) fn resolve_field(&self, field: &str) -> ResolvedField {
self.resolve_field_with_matching(field, self.application.matching)
}
fn resolve_field_with_matching(
&self,
field: &str,
matching: FieldNameMatching,
) -> ResolvedField {
let application =
sensitivity_inner(&self.application, field, matching, true);
let floor = self.floor.as_ref().and_then(|floor| {
sensitivity_inner(&floor.inner, field, floor.inner.matching, false)
});
match floor {
Some(floor_level) => ResolvedField::Sensitive {
sensitivity: application
.map_or(floor_level, |level| level.max(floor_level)),
},
None => match application {
Some(sensitivity) => ResolvedField::Sensitive { sensitivity },
None => ResolvedField::PassThrough,
},
}
}
#[inline]
pub fn matching(&self) -> FieldNameMatching {
self.application.matching
}
#[inline]
pub fn unknown_field_policy(&self) -> UnknownFieldPolicy {
self.application.unknown_field_policy
}
pub fn application_sensitive_rules(
&self,
) -> impl Iterator<Item = SensitiveFieldRule<'_>> {
self.application
.sensitive
.iter()
.map(|(field, level)| SensitiveFieldRule::new(field, *level))
}
pub fn application_allow_rules(
&self,
) -> impl Iterator<Item = AllowRule<'_>> {
self.application
.allow_exact
.iter()
.map(|field| AllowRule::new(field, FieldNameMatching::Exact))
.chain(self.application.allow_suffix.iter().map(|field| {
AllowRule::new(field, FieldNameMatching::ExactOrTokenSuffix)
}))
}
pub(crate) fn clone_application(&self) -> RedactionPolicyInner {
(*self.application).clone()
}
}
fn classify_inner<'a>(
inner: &'a RedactionPolicyInner,
field: &str,
matching: FieldNameMatching,
allow: bool,
) -> FieldClassification<'a> {
match visit_canonical_field_candidates(
field,
matching,
|is_exact, candidate| {
let match_kind = if is_exact {
FieldMatchKind::Exact
} else {
FieldMatchKind::TokenSuffix
};
if allow
&& is_exact
&& let Some(field) = inner.allow_exact.get(candidate)
{
return ControlFlow::Break(FieldClassification::Allowed {
rule: AllowRule::new(field, FieldNameMatching::Exact),
match_kind,
});
}
if allow && let Some(field) = inner.allow_suffix.get(candidate) {
return ControlFlow::Break(FieldClassification::Allowed {
rule: AllowRule::new(
field,
FieldNameMatching::ExactOrTokenSuffix,
),
match_kind,
});
}
if let Some((field, sensitivity)) =
inner.sensitive.get_key_value(candidate)
{
return ControlFlow::Break(FieldClassification::Sensitive {
rule: SensitiveFieldRule::new(field, *sensitivity),
match_kind,
});
}
ControlFlow::Continue(())
},
) {
ControlFlow::Break(classification) => classification,
ControlFlow::Continue(()) => FieldClassification::Unknown,
}
}
fn sensitivity_inner(
inner: &RedactionPolicyInner,
field: &str,
matching: FieldNameMatching,
allow: bool,
) -> Option<Sensitivity> {
match classify_inner(inner, field, matching, allow) {
FieldClassification::Allowed { .. } => None,
FieldClassification::Sensitive { .. }
| FieldClassification::Unknown => {
strongest_sensitive_match(inner, field, matching)
.or_else(|| inner.unknown_field_policy.sensitivity())
}
}
}
fn strongest_sensitive_match(
inner: &RedactionPolicyInner,
field: &str,
matching: FieldNameMatching,
) -> Option<Sensitivity> {
let mut strongest: Option<Sensitivity> = None;
let _ = visit_canonical_field_candidates(
field,
matching,
|_is_exact, candidate| {
if let Some(level) = inner.sensitive.get(candidate) {
strongest = Some(
strongest.map_or(*level, |current| current.max(*level)),
);
}
ControlFlow::<()>::Continue(())
},
);
strongest
}