Skip to content

Commit

Permalink
Add evaluate_policies_partial API for Authorizer (#593)
Browse files Browse the repository at this point in the history
  • Loading branch information
0x00A5 authored Jan 29, 2024
1 parent 7de7333 commit 2bf3a1d
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 9 deletions.
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 @@ -49,6 +49,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 @@ -732,6 +732,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),
}
}
}

/// Errors that can occur during authorization
Expand Down Expand Up @@ -799,6 +826,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 @@ -1008,6 +1051,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

0 comments on commit 2bf3a1d

Please sign in to comment.