pub mod err;
use std::collections::{HashMap, HashSet};
use std::iter;
use std::sync::Arc;
use crate::ast::{Entity, EntityUID, EntityUIDEntry, Request};
use crate::authorizer::Decision;
use crate::batched_evaluator::err::{BatchedEvalError, InsufficientIterationsError};
use crate::entities::TCComputation;
use crate::tpe::entities::PartialEntity;
use crate::tpe::err::PartialRequestError;
use crate::tpe::request::{PartialEntityUID, PartialRequest};
use crate::tpe::residual_policies;
use crate::tpe::response::{decision_from_residuals, ResidualPolicy, Response};
use crate::validator::ValidatorSchema;
use crate::{ast::PolicySet, extensions::Extensions};
use crate::tpe::{entities::PartialEntities, evaluator::Evaluator};
pub trait EntityLoader {
fn load_entities(&mut self, uids: &HashSet<EntityUID>) -> HashMap<EntityUID, Option<Entity>>;
}
fn concrete_request_to_partial(
request: &Request,
schema: &ValidatorSchema,
) -> Result<PartialRequest, BatchedEvalError> {
let principal = match &request.principal {
EntityUIDEntry::Known { euid, .. } => PartialEntityUID::from(euid.as_ref().clone()),
EntityUIDEntry::Unknown { .. } => return Err(PartialRequestError {}.into()),
};
let action = match &request.action {
EntityUIDEntry::Known { euid, .. } => euid.as_ref().clone(),
EntityUIDEntry::Unknown { .. } => return Err(PartialRequestError {}.into()),
};
let resource = match &request.resource {
EntityUIDEntry::Known { euid, .. } => PartialEntityUID::from(euid.as_ref().clone()),
EntityUIDEntry::Unknown { .. } => return Err(PartialRequestError {}.into()),
};
let context = match &request.context {
Some(crate::ast::Context::Value(attrs)) => Some(attrs.clone()),
Some(crate::ast::Context::RestrictedResidual(_)) => {
return Err(PartialRequestError {}.into())
}
None => None,
};
Ok(PartialRequest::new(
principal, action, resource, context, schema,
)?)
}
pub fn is_authorized_batched(
request: &Request,
ps: &PolicySet,
schema: &ValidatorSchema,
loader: &mut dyn EntityLoader,
max_iters: u32,
) -> Result<Decision, BatchedEvalError> {
let request = concrete_request_to_partial(request, schema)?;
let mut entities = PartialEntities::from_entities(iter::empty(), schema)?;
let initial_evaluator = Evaluator {
request: &request,
entities: &entities,
extensions: Extensions::all_available(),
};
let mut residuals = residual_policies(&request, ps, schema, &initial_evaluator)?;
for _i in 0..max_iters {
if decision_from_residuals(&residuals).is_some() {
break;
}
let ids = residuals.iter().flat_map(|r| r.all_literal_uids());
let mut to_load = HashSet::new();
for uid in ids {
if !entities.contains_entity(&uid) {
to_load.insert(uid);
}
}
let loaded_entities = loader.load_entities(&to_load);
for (id, e_option) in loaded_entities {
match e_option {
Some(e) => {
entities.add_entities(
iter::once((id, PartialEntity::try_from(e)?)),
schema,
TCComputation::AssumeAlreadyComputed,
)?;
}
None => {
entities.add_entity_trusted(
id.clone(),
PartialEntity::try_from(Entity::with_uid(id))?,
)?;
}
}
}
let evaluator = Evaluator {
request: &request,
entities: &entities,
extensions: Extensions::all_available(),
};
residuals = residuals
.into_iter()
.map(|residual| {
#[expect(
clippy::unwrap_used,
reason = "residuals and policy set contain the same policy ids"
)]
ResidualPolicy::new(
Arc::new(evaluator.interpret(&residual.get_residual())),
Arc::new(ps.get(residual.get_policy_id()).unwrap().clone()),
)
})
.collect();
}
let response = Response::new(residuals.into_iter(), &request, &entities, schema);
match response.decision() {
Some(decision) => Ok(decision),
None => Err(InsufficientIterationsError {}.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Context, RequestSchemaAllPass};
use crate::extensions::Extensions;
use crate::parser::parse_policyset;
struct Loader {
requested: Vec<EntityUID>,
}
impl EntityLoader for Loader {
fn load_entities(
&mut self,
uids: &HashSet<EntityUID>,
) -> HashMap<EntityUID, Option<Entity>> {
self.requested.extend(uids.iter().cloned());
uids.iter().map(|u| (u.clone(), None)).collect()
}
}
#[track_caller]
fn run(policies: &str) -> (Decision, Loader) {
let schema = ValidatorSchema::from_cedarschema_str(
r#"
entity User { level: Long };
entity Document;
action all;
action read in [all] appliesTo { principal: [User], resource: [Document] };
"#,
Extensions::all_available(),
)
.expect("schema should parse")
.0;
let request = Request::new(
(r#"User::"alice""#.parse().expect("uid should parse"), None),
(r#"Action::"read""#.parse().expect("uid should parse"), None),
(
r#"Document::"doc""#.parse().expect("uid should parse"),
None,
),
Context::empty(),
Some(&RequestSchemaAllPass {}),
Extensions::all_available(),
)
.expect("request should be valid");
let ps = parse_policyset(policies).expect("policies should parse");
let mut loader = Loader {
requested: Vec::new(),
};
let decision = is_authorized_batched(&request, &ps, &schema, &mut loader, 3)
.expect("should reach a decision");
(decision, loader)
}
#[test]
fn actions_are_seeded_from_the_schema() {
let (decision, loader) = run(r#"permit(principal, action in Action::"all", resource);"#);
assert_eq!(decision, Decision::Allow);
assert_eq!(loader.requested, vec![]);
}
#[test]
fn nothing_is_loaded_once_the_decision_is_determined() {
let (decision, loader) = run(r#"
forbid(principal, action, resource);
permit(principal, action, resource) when { principal.level > 5 };
"#);
assert_eq!(decision, Decision::Deny);
assert_eq!(loader.requested, vec![]);
}
}