use crate::SecurityRequestView;
#[must_use = "authorization decisions must be checked; Ok(Deny(_)) rejects the operation"]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthorizationDecision {
Allow,
Deny(AuthorizationDenial),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthorizationDenial {
SubjectUnknown,
ResourceUnknown,
PermissionDenied,
PolicyNotApplicable,
MaintenanceRestricted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IngressDecision {
AllowToContinue,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetailedDecision {
Allow,
Deny,
Abstain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayerFailureKind {
Unavailable,
Error,
Timeout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayerRequirement {
Required,
Optional,
}
pub type LayerEvaluation<T> = Result<T, LayerFailureKind>;
pub trait IngressPolicy: Send + Sync {
fn evaluate_ingress(&self, request: SecurityRequestView<'_>) -> LayerEvaluation<IngressDecision>;
}
pub fn combine_layered_authorization<F>(
ingress: LayerEvaluation<IngressDecision>,
requirement: LayerRequirement,
detailed: F,
) -> LayerEvaluation<AuthorizationDecision>
where
F: FnOnce() -> LayerEvaluation<DetailedDecision>,
{
match ingress {
Ok(IngressDecision::AllowToContinue) => match detailed() {
Ok(DetailedDecision::Allow) => Ok(AuthorizationDecision::Allow),
Ok(DetailedDecision::Deny) => Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied)),
Ok(DetailedDecision::Abstain) if requirement == LayerRequirement::Optional => {
Ok(AuthorizationDecision::Allow)
}
Ok(DetailedDecision::Abstain) => Ok(AuthorizationDecision::Deny(AuthorizationDenial::PolicyNotApplicable)),
Err(failure) => Err(failure),
},
Ok(IngressDecision::Deny) => Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied)),
Err(failure) => Err(failure),
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use super::*;
fn is_allow(decision: LayerEvaluation<AuthorizationDecision>) -> bool {
matches!(decision, Ok(AuthorizationDecision::Allow))
}
#[test]
fn complete_truth_table_is_fail_closed() {
for ingress in [IngressDecision::AllowToContinue, IngressDecision::Deny] {
for requirement in [LayerRequirement::Required, LayerRequirement::Optional] {
for detailed in [
DetailedDecision::Allow,
DetailedDecision::Deny,
DetailedDecision::Abstain,
] {
let detailed_called = Cell::new(false);
let result = combine_layered_authorization(Ok(ingress), requirement, || {
detailed_called.set(true);
Ok(detailed)
});
let expected_allow = matches!(
(ingress, requirement, detailed),
(IngressDecision::AllowToContinue, _, DetailedDecision::Allow)
| (
IngressDecision::AllowToContinue,
LayerRequirement::Optional,
DetailedDecision::Abstain
)
);
assert_eq!(is_allow(result), expected_allow);
assert_eq!(detailed_called.get(), ingress == IngressDecision::AllowToContinue);
}
}
}
}
#[test]
fn coarse_deny_is_sticky_and_skips_detailed_evaluation() {
let detailed_called = Cell::new(false);
let result = combine_layered_authorization(Ok(IngressDecision::Deny), LayerRequirement::Optional, || {
detailed_called.set(true);
Ok(DetailedDecision::Allow)
});
assert_eq!(
result,
Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied))
);
assert!(!detailed_called.get());
}
#[test]
fn every_layer_failure_is_preserved() {
for failure in [
LayerFailureKind::Unavailable,
LayerFailureKind::Error,
LayerFailureKind::Timeout,
] {
assert_eq!(
combine_layered_authorization(Err(failure), LayerRequirement::Optional, || {
Ok(DetailedDecision::Allow)
}),
Err(failure)
);
assert_eq!(
combine_layered_authorization(
Ok(IngressDecision::AllowToContinue),
LayerRequirement::Optional,
|| Err(failure),
),
Err(failure)
);
}
}
#[test]
fn detailed_deny_and_required_abstention_have_closed_reasons() {
let denied =
combine_layered_authorization(Ok(IngressDecision::AllowToContinue), LayerRequirement::Required, || {
Ok(DetailedDecision::Deny)
});
let abstained =
combine_layered_authorization(Ok(IngressDecision::AllowToContinue), LayerRequirement::Required, || {
Ok(DetailedDecision::Abstain)
});
assert_eq!(
denied,
Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied))
);
assert_eq!(
abstained,
Ok(AuthorizationDecision::Deny(AuthorizationDenial::PolicyNotApplicable))
);
}
#[test]
fn every_denial_reason_is_closed_and_value_free() {
let reasons = [
AuthorizationDenial::SubjectUnknown,
AuthorizationDenial::ResourceUnknown,
AuthorizationDenial::PermissionDenied,
AuthorizationDenial::PolicyNotApplicable,
AuthorizationDenial::MaintenanceRestricted,
];
for reason in reasons {
let decision = AuthorizationDecision::Deny(reason);
assert_eq!(decision, AuthorizationDecision::Deny(reason));
}
}
}