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().clone()
}
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>,
satisfied_permits: HashSet<PolicyID>,
false_permits: HashSet<PolicyID>,
residual_permits: HashSet<PolicyID>,
satisfied_forbids: HashSet<PolicyID>,
false_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 satisfied_permits = HashSet::new();
let mut false_permits = HashSet::new();
let mut residual_permits = HashSet::new();
let mut satisfied_forbids = HashSet::new();
let mut false_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() {
satisfied_forbids.insert(id);
} else if r.is_false() || r.is_error() {
false_forbids.insert(id);
} else {
residual_forbids.insert(id);
}
}
Effect::Permit => {
if r.is_true() {
satisfied_permits.insert(id);
} else if r.is_false() || r.is_error() {
false_permits.insert(id);
} else {
residual_permits.insert(id);
}
}
}
}
let decision = match (
!satisfied_forbids.is_empty(),
!satisfied_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,
satisfied_permits,
false_permits,
residual_permits,
satisfied_forbids,
false_forbids,
residual_forbids,
request,
entities,
schema,
}
}
pub fn satisfied_permits(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.satisfied_permits
.iter()
.map(|id| self.residuals.get(id).unwrap())
}
pub fn satisfied_forbids(&self) -> impl Iterator<Item = &ResidualPolicy> {
#[expect(
clippy::unwrap_used,
reason = "we know that the policy ids are in the residuals map"
)]
self.satisfied_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 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 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(&self, id: &PolicyID) -> Option<&Residual> {
self.residuals.get(id).map(|rp| rp.residual.as_ref())
}
pub fn decision(&self) -> Option<Decision> {
self.decision
}
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();
#[expect(clippy::unwrap_used, reason = "policy ids should not clash")]
Ok(authorizer.is_authorized(
request.clone(),
&PolicySet::try_from_iter(self.residuals.values().map(|rp| rp.clone().into())).unwrap(),
entities,
))
}
pub fn residual_policies(&self) -> impl Iterator<Item = &ResidualPolicy> {
self.residuals.values()
}
}