use std::collections::BTreeSet;
use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::claims::ClaimCeiling;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum PolicyOutcome {
Allow,
Warn,
BreakGlass,
Quarantine,
Reject,
}
impl PolicyOutcome {
#[must_use]
pub const fn claim_ceiling(self) -> ClaimCeiling {
match self {
Self::Allow | Self::Warn | Self::BreakGlass => ClaimCeiling::AuthorityGrade,
Self::Quarantine | Self::Reject => ClaimCeiling::DevOnly,
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
pub struct PolicyRuleId(String);
impl PolicyRuleId {
pub fn new(value: impl Into<String>) -> Result<Self, PolicyError> {
let value = value.into();
if value.trim().is_empty() {
return Err(PolicyError::EmptyRuleId);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PolicyContribution {
pub rule_id: PolicyRuleId,
pub outcome: PolicyOutcome,
pub reason: String,
pub break_glass_override_allowed: bool,
}
impl PolicyContribution {
pub fn new(
rule_id: impl Into<String>,
outcome: PolicyOutcome,
reason: impl Into<String>,
) -> Result<Self, PolicyError> {
let reason = reason.into();
if reason.trim().is_empty() {
return Err(PolicyError::EmptyReason);
}
Ok(Self {
rule_id: PolicyRuleId::new(rule_id)?,
outcome,
reason,
break_glass_override_allowed: false,
})
}
#[must_use]
pub const fn allow_break_glass_override(mut self) -> Self {
self.break_glass_override_allowed = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BreakGlassReasonCode {
IncidentResponse,
RestoreRecovery,
OperatorCorrection,
DataMigration,
DiagnosticOnly,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BreakGlassScope {
pub operation_type: String,
pub artifact_refs: Vec<String>,
pub not_before: Option<DateTime<Utc>>,
pub not_after: Option<DateTime<Utc>>,
}
impl BreakGlassScope {
#[must_use]
pub fn is_bound(&self) -> bool {
!self.operation_type.trim().is_empty()
&& !self.artifact_refs.is_empty()
&& self
.artifact_refs
.iter()
.all(|artifact_ref| !artifact_ref.trim().is_empty())
&& self
.not_before
.zip(self.not_after)
.is_none_or(|(not_before, not_after)| not_before <= not_after)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BreakGlassAuthorization {
pub permitted: bool,
pub attested: bool,
pub scope: BreakGlassScope,
pub reason_code: BreakGlassReasonCode,
}
impl BreakGlassAuthorization {
#[must_use]
pub fn is_valid(&self) -> bool {
self.permitted && self.attested && self.scope.is_bound()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BreakGlassAuditShape {
pub scope: BreakGlassScope,
pub reason_code: BreakGlassReasonCode,
pub contributing_outcomes: Vec<PolicyContribution>,
pub expires_at: Option<DateTime<Utc>>,
}
impl BreakGlassAuditShape {
#[must_use]
pub fn from_decision(decision: &PolicyDecision) -> Option<Self> {
let authorization = decision.break_glass.as_ref()?;
if decision.final_outcome != PolicyOutcome::BreakGlass || !authorization.is_valid() {
return None;
}
Some(Self {
scope: authorization.scope.clone(),
reason_code: authorization.reason_code,
contributing_outcomes: decision.contributing.clone(),
expires_at: authorization.scope.not_after,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PolicyDecision {
pub final_outcome: PolicyOutcome,
pub contributing: Vec<PolicyContribution>,
pub discarded: Vec<PolicyContribution>,
pub break_glass: Option<BreakGlassAuthorization>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PolicyEngine {
registered_rules: BTreeSet<PolicyRuleId>,
}
impl PolicyEngine {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register_rule(&mut self, rule_id: impl Into<String>) -> Result<(), PolicyError> {
self.registered_rules.insert(PolicyRuleId::new(rule_id)?);
Ok(())
}
pub fn compose(
&self,
contributions: Vec<PolicyContribution>,
break_glass: Option<BreakGlassAuthorization>,
) -> Result<PolicyDecision, PolicyError> {
if contributions.is_empty() {
return Err(PolicyError::NoContributions);
}
for contribution in &contributions {
if !self.registered_rules.contains(&contribution.rule_id) {
return Err(PolicyError::UnregisteredRule(
contribution.rule_id.as_str().to_string(),
));
}
}
Ok(compose_policy_outcomes(contributions, break_glass))
}
}
#[must_use]
pub fn compose_policy_outcomes(
mut contributions: Vec<PolicyContribution>,
break_glass: Option<BreakGlassAuthorization>,
) -> PolicyDecision {
contributions.sort_by(|a, b| {
b.outcome
.cmp(&a.outcome)
.then_with(|| a.rule_id.cmp(&b.rule_id))
});
let strongest = contributions
.first()
.map(|contribution| contribution.outcome)
.unwrap_or(PolicyOutcome::Allow);
let has_break_glass = contributions
.iter()
.any(|contribution| contribution.outcome == PolicyOutcome::BreakGlass);
let every_blocking_rule_allows_break_glass = contributions
.iter()
.filter(|contribution| {
matches!(
contribution.outcome,
PolicyOutcome::Reject | PolicyOutcome::Quarantine
)
})
.all(|contribution| contribution.break_glass_override_allowed);
let break_glass_valid = break_glass
.as_ref()
.is_some_and(BreakGlassAuthorization::is_valid);
let final_outcome = if matches!(strongest, PolicyOutcome::Reject | PolicyOutcome::Quarantine)
&& has_break_glass
&& every_blocking_rule_allows_break_glass
&& break_glass_valid
{
PolicyOutcome::BreakGlass
} else {
strongest
};
let (contributing, discarded) = contributions
.into_iter()
.partition(|contribution| contribution.outcome == final_outcome);
PolicyDecision {
final_outcome,
contributing,
discarded,
break_glass: break_glass.filter(BreakGlassAuthorization::is_valid),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PolicyError {
EmptyRuleId,
EmptyReason,
NoContributions,
UnregisteredRule(String),
}
#[cfg(test)]
mod tests {
use super::*;
fn contribution(rule_id: &str, outcome: PolicyOutcome) -> PolicyContribution {
PolicyContribution::new(rule_id, outcome, format!("{rule_id} reason")).unwrap()
}
fn break_glassable_contribution(rule_id: &str, outcome: PolicyOutcome) -> PolicyContribution {
contribution(rule_id, outcome).allow_break_glass_override()
}
fn break_glass_scope() -> BreakGlassScope {
BreakGlassScope {
operation_type: "memory.override".into(),
artifact_refs: vec!["mem_01".into()],
not_before: None,
not_after: None,
}
}
#[test]
fn policy_outcome_order_matches_adr_0026() {
assert!(PolicyOutcome::Reject > PolicyOutcome::Quarantine);
assert!(PolicyOutcome::Quarantine > PolicyOutcome::BreakGlass);
assert!(PolicyOutcome::BreakGlass > PolicyOutcome::Warn);
assert!(PolicyOutcome::Warn > PolicyOutcome::Allow);
}
#[test]
fn reject_and_quarantine_cap_claims_to_dev_only() {
assert_eq!(PolicyOutcome::Reject.claim_ceiling(), ClaimCeiling::DevOnly);
assert_eq!(
PolicyOutcome::Quarantine.claim_ceiling(),
ClaimCeiling::DevOnly
);
assert_eq!(
PolicyOutcome::BreakGlass.claim_ceiling(),
ClaimCeiling::AuthorityGrade
);
}
#[test]
fn policy_engine_rejects_unregistered_rules() {
let engine = PolicyEngine::new();
let err = engine
.compose(
vec![contribution("memory.closure", PolicyOutcome::Reject)],
None,
)
.unwrap_err();
assert_eq!(err, PolicyError::UnregisteredRule("memory.closure".into()));
}
#[test]
fn strongest_outcome_wins_with_deterministic_explainability() {
let mut engine = PolicyEngine::new();
engine.register_rule("pack.strict").unwrap();
engine.register_rule("memory.closure").unwrap();
engine.register_rule("tier.warn").unwrap();
let decision = engine
.compose(
vec![
contribution("tier.warn", PolicyOutcome::Warn),
contribution("pack.strict", PolicyOutcome::Reject),
contribution("memory.closure", PolicyOutcome::Reject),
],
None,
)
.unwrap();
assert_eq!(decision.final_outcome, PolicyOutcome::Reject);
assert_eq!(decision.contributing.len(), 2);
assert_eq!(decision.contributing[0].rule_id.as_str(), "memory.closure");
assert_eq!(decision.contributing[1].rule_id.as_str(), "pack.strict");
assert_eq!(decision.discarded[0].outcome, PolicyOutcome::Warn);
}
#[test]
fn break_glass_cannot_override_without_attestation() {
let decision = compose_policy_outcomes(
vec![
contribution("memory.closure", PolicyOutcome::Reject),
contribution("operator.override", PolicyOutcome::BreakGlass),
],
Some(BreakGlassAuthorization {
permitted: true,
attested: false,
scope: break_glass_scope(),
reason_code: BreakGlassReasonCode::OperatorCorrection,
}),
);
assert_eq!(decision.final_outcome, PolicyOutcome::Reject);
assert!(decision.break_glass.is_none());
}
#[test]
fn valid_break_glass_can_be_final_when_explicitly_authorized() {
let decision = compose_policy_outcomes(
vec![
break_glassable_contribution("memory.closure", PolicyOutcome::Quarantine),
contribution("operator.override", PolicyOutcome::BreakGlass),
],
Some(BreakGlassAuthorization {
permitted: true,
attested: true,
scope: break_glass_scope(),
reason_code: BreakGlassReasonCode::IncidentResponse,
}),
);
assert_eq!(decision.final_outcome, PolicyOutcome::BreakGlass);
assert_eq!(decision.contributing.len(), 1);
assert_eq!(decision.contributing[0].outcome, PolicyOutcome::BreakGlass);
assert!(decision.break_glass.is_some());
let audit_shape = BreakGlassAuditShape::from_decision(&decision).unwrap();
assert_eq!(
audit_shape.reason_code,
BreakGlassReasonCode::IncidentResponse
);
assert_eq!(audit_shape.scope.operation_type, "memory.override");
}
#[test]
fn break_glass_cannot_bypass_non_overridable_reject() {
for rule_id in [
"actor.attestation.0010",
"canonical.hash.0022",
"trust.tier.0019",
] {
let decision = compose_policy_outcomes(
vec![
contribution(rule_id, PolicyOutcome::Reject),
break_glassable_contribution("operator.override", PolicyOutcome::BreakGlass),
],
Some(BreakGlassAuthorization {
permitted: true,
attested: true,
scope: break_glass_scope(),
reason_code: BreakGlassReasonCode::IncidentResponse,
}),
);
assert_eq!(
decision.final_outcome,
PolicyOutcome::Reject,
"{rule_id} must remain non-overridable"
);
assert!(decision.break_glass.is_some());
assert!(BreakGlassAuditShape::from_decision(&decision).is_none());
}
}
#[test]
fn break_glass_scope_requires_operation_and_artifact_refs() {
let mut scope = break_glass_scope();
assert!(scope.is_bound());
scope.artifact_refs.clear();
assert!(!scope.is_bound());
scope.artifact_refs.push("mem_01".into());
scope.operation_type.clear();
assert!(!scope.is_bound());
}
}