use std::borrow::Cow;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CombineOp {
And,
Or,
Not,
Delegate,
}
impl fmt::Display for CombineOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CombineOp::And => write!(f, "AND"),
CombineOp::Or => write!(f, "OR"),
CombineOp::Not => write!(f, "NOT"),
CombineOp::Delegate => write!(f, "DELEGATE"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FactOutcome {
Found,
Missing,
Error,
}
impl FactOutcome {
pub fn from_load_result<V>(result: &crate::FactLoadResult<V>) -> Self {
match result {
crate::FactLoadResult::Found(_) => Self::Found,
crate::FactLoadResult::Missing => Self::Missing,
crate::FactLoadResult::Error(_) => Self::Error,
}
}
}
impl fmt::Display for FactOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Found => write!(f, "found"),
Self::Missing => write!(f, "missing"),
Self::Error => write!(f, "error"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FactProvenance {
pub fact_name: &'static str,
pub key: String,
pub outcome: FactOutcome,
pub detail: Option<String>,
}
impl FactProvenance {
pub fn new(
fact_name: &'static str,
key: impl Into<String>,
outcome: FactOutcome,
detail: Option<String>,
) -> Self {
Self {
fact_name,
key: key.into(),
outcome,
detail,
}
}
}
impl fmt::Display for FactProvenance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"fact {} [{}]: {}",
self.fact_name, self.outcome, self.key
)?;
if let Some(detail) = &self.detail {
write!(f, " ({detail})")?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub enum PolicyEvalResult {
Granted {
policy_type: Cow<'static, str>,
reason: Option<String>,
provenance: Vec<FactProvenance>,
},
Denied {
policy_type: Cow<'static, str>,
reason: String,
provenance: Vec<FactProvenance>,
},
Combined {
policy_type: Cow<'static, str>,
operation: CombineOp,
children: Vec<PolicyEvalResult>,
outcome: bool,
},
}
#[derive(Debug, Clone)]
pub enum AccessEvaluation {
Granted {
policy_type: Cow<'static, str>,
reason: Option<String>,
trace: EvalTrace,
},
Denied {
trace: EvalTrace,
reason: String,
},
}
fn leaf_denial_matches(node: &PolicyEvalResult, expected: &str) -> bool {
match node {
PolicyEvalResult::Denied { policy_type, .. } => policy_type.as_ref() == expected,
PolicyEvalResult::Granted { .. } => false,
PolicyEvalResult::Combined { children, .. } => children
.iter()
.any(|child| leaf_denial_matches(child, expected)),
}
}
impl AccessEvaluation {
pub fn is_granted(&self) -> bool {
matches!(self, Self::Granted { .. })
}
pub fn trace(&self) -> &EvalTrace {
match self {
Self::Granted { trace, .. } | Self::Denied { trace, .. } => trace,
}
}
pub fn granted_policy_type(&self) -> Option<&str> {
match self {
Self::Granted { policy_type, .. } => Some(policy_type),
Self::Denied { .. } => None,
}
}
pub fn denied_reason(&self) -> Option<&str> {
match self {
Self::Denied { reason, .. } => Some(reason),
Self::Granted { .. } => None,
}
}
#[track_caller]
pub fn assert_granted_by(&self, expected: &str) {
match self {
Self::Granted { policy_type, .. } => {
assert_eq!(
policy_type.as_ref(),
expected,
"expected grant by policy `{expected}`, but the grant came from `{policy_type}`"
);
}
Self::Denied { reason, .. } => {
panic!("expected grant by policy `{expected}`, but access was denied: {reason}");
}
}
}
#[track_caller]
pub fn assert_denied(&self) {
if let Self::Granted {
policy_type,
reason,
..
} = self
{
panic!(
"expected denial, but access was granted by `{policy_type}`{}",
reason
.as_ref()
.map(|r| format!(": {r}"))
.unwrap_or_default()
);
}
}
#[track_caller]
pub fn assert_denied_with_reason_containing(&self, needle: &str) {
match self {
Self::Denied { reason, .. } => {
assert!(
reason.contains(needle),
"expected summary denial reason to contain `{needle}`, got `{reason}`"
);
}
Self::Granted { policy_type, .. } => {
panic!(
"expected denial containing `{needle}`, but access was granted by `{policy_type}`"
);
}
}
}
#[track_caller]
pub fn assert_denied_by(&self, expected: &str) {
match self {
Self::Granted { policy_type, .. } => {
panic!(
"expected denial by policy `{expected}`, but access was granted by `{policy_type}`"
);
}
Self::Denied { trace, .. } => {
let Some(root) = trace.root() else {
panic!("expected denial by `{expected}`, but the trace is empty");
};
if !leaf_denial_matches(root, expected) {
panic!(
"expected a denying leaf for policy `{expected}` in the trace; \
got:\n{}",
trace.format()
);
}
}
}
}
#[track_caller]
pub fn assert_trace_contains(&self, needle: &str) {
let rendered = self.display_trace();
assert!(
rendered.contains(needle),
"expected evaluation trace to contain `{needle}`; got:\n{rendered}"
);
}
pub fn to_result<E>(&self, error_fn: impl FnOnce(&str) -> E) -> Result<(), E> {
match self {
Self::Granted { .. } => Ok(()),
Self::Denied { reason, .. } => Err(error_fn(reason)),
}
}
pub fn display_trace(&self) -> String {
let trace_str = self.trace().format();
if trace_str == "No evaluation trace available" {
format!("{}\n(No evaluation trace available)", self)
} else {
format!("{}\nEvaluation Trace:\n{}", self, trace_str)
}
}
}
impl fmt::Display for AccessEvaluation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Granted {
policy_type,
reason,
trace: _,
} => {
match reason {
Some(r) => write!(f, "[GRANTED] by {} - {}", policy_type, r),
None => write!(f, "[GRANTED] by {}", policy_type),
}
}
Self::Denied { reason, trace: _ } => {
write!(f, "[Denied] - {}", reason)
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct EvalTrace {
root: Option<PolicyEvalResult>,
}
impl EvalTrace {
pub fn new() -> Self {
Self { root: None }
}
pub fn with_root(result: PolicyEvalResult) -> Self {
Self { root: Some(result) }
}
pub fn set_root(&mut self, result: PolicyEvalResult) {
self.root = Some(result);
}
pub fn root(&self) -> Option<&PolicyEvalResult> {
self.root.as_ref()
}
pub fn format(&self) -> String {
match &self.root {
Some(root) => root.format(0),
None => "No evaluation trace available".to_string(),
}
}
}
impl PolicyEvalResult {
pub fn granted(policy_type: impl Into<Cow<'static, str>>, reason: Option<String>) -> Self {
Self::Granted {
policy_type: policy_type.into(),
reason,
provenance: Vec::new(),
}
}
pub fn denied(policy_type: impl Into<Cow<'static, str>>, reason: impl Into<String>) -> Self {
Self::Denied {
policy_type: policy_type.into(),
reason: reason.into(),
provenance: Vec::new(),
}
}
pub fn granted_with_facts(
policy_type: impl Into<Cow<'static, str>>,
reason: Option<String>,
provenance: Vec<FactProvenance>,
) -> Self {
Self::Granted {
policy_type: policy_type.into(),
reason,
provenance,
}
}
pub fn denied_with_facts(
policy_type: impl Into<Cow<'static, str>>,
reason: impl Into<String>,
provenance: Vec<FactProvenance>,
) -> Self {
Self::Denied {
policy_type: policy_type.into(),
reason: reason.into(),
provenance,
}
}
pub fn is_granted(&self) -> bool {
match self {
Self::Granted { .. } => true,
Self::Denied { .. } => false,
Self::Combined { outcome, .. } => *outcome,
}
}
pub fn reason(&self) -> Option<String> {
self.reason_str().map(str::to_owned)
}
pub fn reason_str(&self) -> Option<&str> {
match self {
Self::Granted { reason, .. } => reason.as_deref(),
Self::Denied { reason, .. } => Some(reason),
Self::Combined { .. } => None,
}
}
pub fn provenance(&self) -> &[FactProvenance] {
match self {
Self::Granted { provenance, .. } | Self::Denied { provenance, .. } => provenance,
Self::Combined { .. } => &[],
}
}
pub fn format(&self, indent: usize) -> String {
let indent_str = " ".repeat(indent);
match self {
Self::Granted {
policy_type,
reason,
provenance,
} => {
let reason_text = reason
.as_ref()
.map_or("".to_string(), |r| format!(": {}", r));
let headline = format!("{}✔ {} GRANTED{}", indent_str, policy_type, reason_text);
Self::append_provenance(headline, &indent_str, provenance)
}
Self::Denied {
policy_type,
reason,
provenance,
} => {
let headline = format!("{}✘ {} DENIED: {}", indent_str, policy_type, reason);
Self::append_provenance(headline, &indent_str, provenance)
}
Self::Combined {
policy_type,
operation,
children,
outcome,
} => {
let outcome_char = if *outcome { "✔" } else { "✘" };
let mut result = format!(
"{}{} {} ({})",
indent_str, outcome_char, policy_type, operation
);
for child in children {
result.push_str(&format!("\n{}", child.format(indent + 2)));
}
result
}
}
}
fn append_provenance(
headline: String,
indent_str: &str,
provenance: &[FactProvenance],
) -> String {
let mut result = headline;
for fact in provenance {
result.push_str(&format!("\n{indent_str} ↳ {fact}"));
}
result
}
}
impl fmt::Display for PolicyEvalResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let tree = self.format(0);
write!(f, "{}", tree)
}
}