use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use crate::{
ast::{Effect, EntityUID, Expr, Policy, PolicyID, PolicySet, Request, RequestSchema},
authorizer::{Authorizer, Decision},
entities::{conformance::EntitySchemaConformanceChecker, Entities},
extensions::Extensions,
tpe::{
entities::PartialEntities, err::ReauthorizationError, request::PartialRequest,
residual::Residual,
},
validator::{CoreSchema, ValidatorSchema},
};
#[derive(Debug, Clone)]
pub struct ResidualPolicy {
residual: Arc<Residual>,
policy: Arc<Policy>,
}
impl ResidualPolicy {
pub fn new(residual: Arc<Residual>, policy: Arc<Policy>) -> Self {
Self { residual, policy }
}
pub fn get_effect(&self) -> Effect {
self.policy.effect()
}
pub fn get_residual(&self) -> Arc<Residual> {
self.residual.clone()
}
pub fn get_policy_id(&self) -> &PolicyID {
self.policy.id()
}
pub fn all_literal_uids(&self) -> HashSet<EntityUID> {
self.residual.all_literal_uids()
}
}
impl From<ResidualPolicy> for Policy {
fn from(value: ResidualPolicy) -> Self {
Self::from_when_clause_annos(
value.policy.effect(),
Arc::new(Expr::from(value.residual.as_ref().clone())),
value.policy.id().clone(),
None,
value.policy.annotations_arc().clone(),
)
}
}
#[derive(Debug, Clone)]
pub struct Response<'a> {
decision: Option<Decision>,
residuals: HashMap<PolicyID, ResidualPolicy>,
true_permits: HashSet<PolicyID>,
false_permits: HashSet<PolicyID>,
error_permits: HashSet<PolicyID>,
residual_permits: HashSet<PolicyID>,
true_forbids: HashSet<PolicyID>,
false_forbids: HashSet<PolicyID>,
error_forbids: HashSet<PolicyID>,
residual_forbids: HashSet<PolicyID>,
request: &'a PartialRequest,
entities: &'a PartialEntities,
schema: &'a ValidatorSchema,
}
impl<'a> Response<'a> {
pub fn new(
residuals: impl Iterator<Item = ResidualPolicy>,
request: &'a PartialRequest,
entities: &'a PartialEntities,
schema: &'a ValidatorSchema,
) -> Self {
let mut residual_map = HashMap::new();
let mut true_permits = HashSet::new();
let mut false_permits = HashSet::new();
let mut error_permits = HashSet::new();
let mut residual_permits = HashSet::new();
let mut true_forbids = HashSet::new();
let mut false_forbids = HashSet::new();
let mut error_forbids = HashSet::new();
let mut residual_forbids = HashSet::new();
for rp in residuals {
let r = rp.get_residual();
let id = rp.get_policy_id();
residual_map.insert(id.clone(), rp.clone());
match rp.get_effect() {
Effect::Forbid => {
if r.is_true() {
true_forbids.insert(id.clone());
} else if r.is_false() {
false_forbids.insert(id.clone());
} else if r.is_error() {
error_forbids.insert(id.clone());
} else {
residual_forbids.insert(id.clone());
}
}
Effect::Permit => {
if r.is_true() {
true_permits.insert(id.clone());
} else if r.is_false() {
false_permits.insert(id.clone());
} else if r.is_error() {
error_permits.insert(id.clone());
} else {
residual_permits.insert(id.clone());
}
}
}
}
let decision = match (
!true_forbids.is_empty(),
!true_permits.is_empty(),
!residual_permits.is_empty(),
!residual_forbids.is_empty(),
) {
(true, _, _, _) => Some(Decision::Deny),
(_, false, false, _) => Some(Decision::Deny),
(false, _, _, true) => None,
(false, false, true, false) => None,
(false, true, _, false) => Some(Decision::Allow),
};
Self {
decision,
residuals: residual_map,
true_permits,
false_permits,
error_permits,
residual_permits,
true_forbids,
false_forbids,
error_forbids,
residual_forbids,
request,
entities,
schema,
}
}
pub fn true_permits(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.true_permits
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn true_forbids(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.true_forbids
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn false_permits(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.false_permits
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn error_permits(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.error_permits
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn false_forbids(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.false_forbids
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn error_forbids(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.error_forbids
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn residual_permits(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.residual_permits
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn residual_forbids(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.residual_forbids
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn get_residual_policy(&self, id: &PolicyID) -> Option<&ResidualPolicy> {
self.residuals.get(id)
}
pub fn decision(&self) -> Option<Decision> {
self.decision
}
pub fn reason(&self) -> Option<impl Iterator<Item = &PolicyID>> {
match self.decision? {
Decision::Allow => Some(self.true_permits.iter()),
Decision::Deny => Some(self.true_forbids.iter()),
}
}
pub fn reauthorize(
&self,
request: &Request,
entities: &Entities,
) -> Result<crate::authorizer::Response, ReauthorizationError> {
self.schema
.validate_request(request, Extensions::all_available())?;
let core_schema = CoreSchema::new(self.schema);
let entities_checker =
EntitySchemaConformanceChecker::new(&core_schema, Extensions::all_available());
for entity in entities.iter() {
entities_checker.validate_entity(entity)?;
}
self.entities.check_consistency(entities)?;
self.request.check_consistency(request)?;
let authorizer = Authorizer::new();
Ok(authorizer.is_authorized(request.clone(), &self.policy_set(), entities))
}
pub fn policies(&self) -> impl Iterator<Item = &ResidualPolicy> {
self.residuals.values()
}
pub fn policy_set(&self) -> PolicySet {
let mut ps = PolicySet::new();
for p in self.policies() {
#[expect(
clippy::unwrap_used,
reason = "`PolicySet::add` only fails on duplicate ids, but all residual policies will have unique ids"
)]
ps.add(p.policy.as_ref().clone()).unwrap()
}
ps
}
}