use std::fmt;
#[derive(Debug, PartialEq, Clone)]
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: String,
reason: Option<String>,
provenance: Vec<FactProvenance>,
},
Denied {
policy_type: String,
reason: String,
provenance: Vec<FactProvenance>,
},
Combined {
policy_type: String,
operation: CombineOp,
children: Vec<PolicyEvalResult>,
outcome: bool,
},
}
#[derive(Debug, Clone)]
pub enum AccessEvaluation {
Granted {
policy_type: String,
reason: Option<String>,
trace: EvalTrace,
},
Denied {
trace: EvalTrace,
reason: String,
},
}
impl AccessEvaluation {
pub fn is_granted(&self) -> bool {
matches!(self, Self::Granted { .. })
}
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 = match self {
AccessEvaluation::Granted {
policy_type: _,
reason: _,
trace,
} => trace,
AccessEvaluation::Denied { reason: _, trace } => trace,
};
let trace_str = 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<String>, reason: Option<String>) -> Self {
Self::Granted {
policy_type: policy_type.into(),
reason,
provenance: Vec::new(),
}
}
pub fn denied(policy_type: impl Into<String>, 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<String>,
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<String>,
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> {
match self {
Self::Granted { reason, .. } => reason.clone(),
Self::Denied { reason, .. } => Some(reason.clone()),
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)
}
}