Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 107 additions & 9 deletions cedar-policy-core/src/authorizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,7 @@ impl Authorizer {
pset: &PolicySet,
entities: &Entities,
) -> ResponseKind {
let eval = Evaluator::new(q, entities, &self.extensions);

let results = self.evaluate_policies(pset, eval);
let results = self.evaluate_policies_core(pset, q, entities);

let errors = results
.errors
Expand Down Expand Up @@ -273,11 +271,56 @@ impl Authorizer {
}
}

fn evaluate_policies<'a>(
/// Returns a policy evaluation response for `q`.
pub fn evaluate_policies(
&self,
pset: &PolicySet,
q: Request,
entities: &Entities,
) -> EvaluationResponse {
let EvaluationResults {
satisfied_permits,
satisfied_forbids,
global_deny_policies: _,
errors,
permit_residuals,
forbid_residuals,
} = self.evaluate_policies_core(pset, q, entities);

let errors = errors
.into_iter()
.map(|(pid, err)| AuthorizationError::PolicyEvaluationError {
id: pid,
error: err,
})
.collect();

let satisfied_permits = satisfied_permits.iter().map(|p| p.id().clone()).collect();
let satisfied_forbids = satisfied_forbids.iter().map(|p| p.id().clone()).collect();

// PANIC SAFETY all policy IDs in the original policy are unique by construction
#[allow(clippy::unwrap_used)]
let permit_residuals = PolicySet::try_from_iter(permit_residuals).unwrap();
// PANIC SAFETY all policy IDs in the original policy are unique by construction
#[allow(clippy::unwrap_used)]
let forbid_residuals = PolicySet::try_from_iter(forbid_residuals).unwrap();

EvaluationResponse {
satisfied_permits,
satisfied_forbids,
errors,
permit_residuals,
forbid_residuals,
}
}

fn evaluate_policies_core<'a>(
&'a self,
pset: &'a PolicySet,
eval: Evaluator<'_>,
q: Request,
entities: &Entities,
) -> EvaluationResults<'a> {
let eval = Evaluator::new(q, entities, &self.extensions);
let mut results = EvaluationResults::default();
let mut satisfied_policies = vec![];

Expand Down Expand Up @@ -628,8 +671,18 @@ mod test {
pset.add_static(parser::parse_policy(Some("3".to_string()), src3).unwrap())
.unwrap();

let r = a.is_authorized_core(q, &pset, &es).decision();
let r = a.is_authorized_core(q.clone(), &pset, &es).decision();
assert_eq!(r, Some(Decision::Allow));

let r = a.evaluate_policies(&pset, q, &es);
assert!(r.satisfied_permits.contains(&PolicyID::from_string("1")));
assert!(r.satisfied_forbids.is_empty());
assert!(r
.permit_residuals
.get(&PolicyID::from_string("3"))
.is_some());
assert!(r.forbid_residuals.is_empty());
assert!(r.errors.is_empty());
}

#[test]
Expand Down Expand Up @@ -687,10 +740,20 @@ mod test {
)
});
let pset = PolicySet::try_from_iter(new).unwrap();
let r = a.is_authorized(q, &pset, &es);
let r = a.is_authorized(q.clone(), &pset, &es);
assert_eq!(r.decision, Decision::Deny);
}
}

let r = a.evaluate_policies(&pset, q, &es);
assert!(r.satisfied_permits.contains(&PolicyID::from_string("1")));
assert!(r.satisfied_forbids.is_empty());
assert!(r.errors.is_empty());
assert!(r.permit_residuals.is_empty());
assert!(r
.forbid_residuals
.get(&PolicyID::from_string("2"))
.is_some());
}

#[test]
Expand Down Expand Up @@ -740,8 +803,18 @@ mod test {
.unwrap();
pset.add_static(parser::parse_policy(Some("4".into()), src4).unwrap())
.unwrap();
let r = a.is_authorized_core(q, &pset, &es);
let r = a.is_authorized_core(q.clone(), &pset, &es);
assert_eq!(r.decision(), Some(Decision::Deny));

let r = a.evaluate_policies(&pset, q, &es);
assert!(r.satisfied_permits.contains(&PolicyID::from_string("4")));
assert!(r.satisfied_forbids.contains(&PolicyID::from_string("3")));
assert!(r.errors.is_empty());
assert!(r.permit_residuals.is_empty());
assert!(r
.forbid_residuals
.get(&PolicyID::from_string("2"))
.is_some());
}

#[test]
Expand Down Expand Up @@ -808,8 +881,18 @@ mod test {

pset.add_static(parser::parse_policy(Some("3".into()), src3).unwrap())
.unwrap();
let r = a.is_authorized_core(q, &pset, &es);
let r = a.is_authorized_core(q.clone(), &pset, &es);
assert_eq!(r.decision(), Some(Decision::Deny));

let r = a.evaluate_policies(&pset, q, &es);
assert!(r.satisfied_permits.is_empty());
assert!(r.satisfied_forbids.contains(&PolicyID::from_string("3")));
assert!(r.errors.is_empty());
assert!(r
.permit_residuals
.get(&PolicyID::from_string("2"))
.is_some());
assert!(r.forbid_residuals.is_empty());
}
}
// by default, Coverlay does not track coverage for lines after a line
Expand Down Expand Up @@ -850,6 +933,21 @@ impl PartialResponse {
}
}

/// Policy evaluation response returned from the `Authorizer`.
#[derive(Debug, PartialEq, Clone)]
pub struct EvaluationResponse {
/// `PolicyID`s of the fully evaluated policies with a permit [`Effect`].
pub satisfied_permits: HashSet<PolicyID>,
/// `PolicyID`s of the fully evaluated policies with a forbid [`Effect`].
pub satisfied_forbids: HashSet<PolicyID>,
/// List of errors that occurred
pub errors: Vec<AuthorizationError>,
/// Residual policies with a permit [`Effect`].
pub permit_residuals: PolicySet,
/// Residual policies with a forbid [`Effect`].
pub forbid_residuals: PolicySet,
}

/// Diagnostics providing more information on how a `Decision` was reached
#[derive(Debug, PartialEq, Clone)]
pub struct Diagnostics {
Expand Down
2 changes: 2 additions & 0 deletions cedar-policy/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
return a `RequestBuilder<&Schema>` so the `RequestBuilder<&Schema>::build`
method checks the request against the schema provided and the
`RequestBuilder<UnsetSchema>::build` method becomes infallible. (#559)
- For the `partial-eval` experimental feature: added
`Authorizer::evaluate_policies_partial` (#474)

### Fixed

Expand Down
88 changes: 88 additions & 0 deletions cedar-policy/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,33 @@ impl Authorizer {
authorizer::ResponseKind::Partial(p) => PartialResponse::Residual(p.into()),
}
}

/// Evaluate an authorization request and respond with results that always includes
/// residuals even if the [`Authorizer`] already reached a decision.
#[cfg(feature = "partial-eval")]
pub fn evaluate_policies_partial(
&self,
query: &Request,
policy_set: &PolicySet,
entities: &Entities,
) -> EvaluationResponse {
let authorizer::EvaluationResponse {
satisfied_permits,
satisfied_forbids,
errors,
permit_residuals,
forbid_residuals,
} = self
.0
.evaluate_policies(&policy_set.ast, query.0.clone(), &entities.0);
EvaluationResponse {
satisfied_permits: satisfied_permits.into_iter().map(PolicyId).collect(),
satisfied_forbids: satisfied_forbids.into_iter().map(PolicyId).collect(),
errors,
permit_residuals: PolicySet::from_ast(permit_residuals),
forbid_residuals: PolicySet::from_ast(forbid_residuals),
}
}
}

/// Authorization response returned from the `Authorizer`
Expand Down Expand Up @@ -765,6 +792,22 @@ pub struct ResidualResponse {
diagnostics: Diagnostics,
}

/// A policy evaluation response obtained from `evaluate_policies_partial`.
#[cfg(feature = "partial-eval")]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EvaluationResponse {
/// `PolicyId`s of fully evaluated policies with a permit [`Effect`]
satisfied_permits: HashSet<PolicyId>,
/// `PolicyId`s of fully evaluated policies with a forbid [`Effect`]
satisfied_forbids: HashSet<PolicyId>,
/// Errors that occurred during policy evaluation.
errors: Vec<AuthorizationError>,
/// Partially evaluated policies with a permit [`Effect`]
permit_residuals: PolicySet,
/// Partially evaluated policies with a forbid [`Effect`]
forbid_residuals: PolicySet,
}

/// Diagnostics providing more information on how a `Decision` was reached
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Diagnostics {
Expand Down Expand Up @@ -974,6 +1017,51 @@ impl From<authorizer::PartialResponse> for ResidualResponse {
}
}

#[cfg(feature = "partial-eval")]
impl EvaluationResponse {
/// Create a new `EvaluationResponse`.
pub fn new(
satisfied_permits: HashSet<PolicyId>,
satisfied_forbids: HashSet<PolicyId>,
errors: Vec<AuthorizationError>,
permit_residuals: PolicySet,
forbid_residuals: PolicySet,
) -> Self {
Self {
satisfied_permits,
satisfied_forbids,
errors,
permit_residuals,
forbid_residuals,
}
}

/// Get the `PolicyId`s of fully evaluated policies with a permit [`Effect`].
pub fn satisfied_permits(&self) -> impl Iterator<Item = &PolicyId> {
self.satisfied_permits.iter()
}

/// Get the `PolicyId`s of fully evaluated policies with a forbid [`Effect`].
pub fn satisfied_forbids(&self) -> impl Iterator<Item = &PolicyId> {
self.satisfied_forbids.iter()
}

/// Get the redisual policies with a permit [`Effect`].
pub fn permit_residuals(&self) -> &PolicySet {
&self.permit_residuals
}

/// Get the redisual policies with a permit [`Effect`].
pub fn forbid_residuals(&self) -> &PolicySet {
&self.forbid_residuals
}

/// Get the evaluation errors.
pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> {
self.errors.iter()
}
}

/// Used to select how a policy will be validated.
#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
#[non_exhaustive]
Expand Down