Skip to content

Commit d501251

Browse files
committed
Add evaluate_policies_partial API for Authorizer
1 parent eb2e728 commit d501251

File tree

3 files changed

+197
-9
lines changed

3 files changed

+197
-9
lines changed

cedar-policy-core/src/authorizer.rs

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,7 @@ impl Authorizer {
165165
pset: &PolicySet,
166166
entities: &Entities,
167167
) -> ResponseKind {
168-
let eval = Evaluator::new(q, entities, &self.extensions);
169-
170-
let results = self.evaluate_policies(pset, eval);
168+
let results = self.evaluate_policies_core(pset, q, entities);
171169

172170
let errors = results
173171
.errors
@@ -273,11 +271,56 @@ impl Authorizer {
273271
}
274272
}
275273

276-
fn evaluate_policies<'a>(
274+
/// Returns a policy evaluation response for `q`.
275+
pub fn evaluate_policies(
276+
&self,
277+
pset: &PolicySet,
278+
q: Request,
279+
entities: &Entities,
280+
) -> EvaluationResponse {
281+
let EvaluationResults {
282+
satisfied_permits,
283+
satisfied_forbids,
284+
global_deny_policies: _,
285+
errors,
286+
permit_residuals,
287+
forbid_residuals,
288+
} = self.evaluate_policies_core(pset, q, entities);
289+
290+
let errors = errors
291+
.into_iter()
292+
.map(|(pid, err)| AuthorizationError::PolicyEvaluationError {
293+
id: pid,
294+
error: err,
295+
})
296+
.collect();
297+
298+
let satisfied_permits = satisfied_permits.iter().map(|p| p.id().clone()).collect();
299+
let satisfied_forbids = satisfied_forbids.iter().map(|p| p.id().clone()).collect();
300+
301+
// PANIC SAFETY all policy IDs in the original policy are unique by construction
302+
#[allow(clippy::unwrap_used)]
303+
let permit_residuals = PolicySet::try_from_iter(permit_residuals).unwrap();
304+
// PANIC SAFETY all policy IDs in the original policy are unique by construction
305+
#[allow(clippy::unwrap_used)]
306+
let forbid_residuals = PolicySet::try_from_iter(forbid_residuals).unwrap();
307+
308+
EvaluationResponse {
309+
satisfied_permits,
310+
satisfied_forbids,
311+
errors,
312+
permit_residuals,
313+
forbid_residuals,
314+
}
315+
}
316+
317+
fn evaluate_policies_core<'a>(
277318
&'a self,
278319
pset: &'a PolicySet,
279-
eval: Evaluator<'_>,
320+
q: Request,
321+
entities: &Entities,
280322
) -> EvaluationResults<'a> {
323+
let eval = Evaluator::new(q, entities, &self.extensions);
281324
let mut results = EvaluationResults::default();
282325
let mut satisfied_policies = vec![];
283326

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

631-
let r = a.is_authorized_core(q, &pset, &es).decision();
674+
let r = a.is_authorized_core(q.clone(), &pset, &es).decision();
632675
assert_eq!(r, Some(Decision::Allow));
676+
677+
let r = a.evaluate_policies(&pset, q, &es);
678+
assert!(r.satisfied_permits.contains(&PolicyID::from_string("1")));
679+
assert!(r.satisfied_forbids.is_empty());
680+
assert!(r
681+
.permit_residuals
682+
.get(&PolicyID::from_string("3"))
683+
.is_some());
684+
assert!(r.forbid_residuals.is_empty());
685+
assert!(r.errors.is_empty());
633686
}
634687

635688
#[test]
@@ -687,10 +740,20 @@ mod test {
687740
)
688741
});
689742
let pset = PolicySet::try_from_iter(new).unwrap();
690-
let r = a.is_authorized(q, &pset, &es);
743+
let r = a.is_authorized(q.clone(), &pset, &es);
691744
assert_eq!(r.decision, Decision::Deny);
692745
}
693746
}
747+
748+
let r = a.evaluate_policies(&pset, q, &es);
749+
assert!(r.satisfied_permits.contains(&PolicyID::from_string("1")));
750+
assert!(r.satisfied_forbids.is_empty());
751+
assert!(r.errors.is_empty());
752+
assert!(r.permit_residuals.is_empty());
753+
assert!(r
754+
.forbid_residuals
755+
.get(&PolicyID::from_string("2"))
756+
.is_some());
694757
}
695758

696759
#[test]
@@ -740,8 +803,18 @@ mod test {
740803
.unwrap();
741804
pset.add_static(parser::parse_policy(Some("4".into()), src4).unwrap())
742805
.unwrap();
743-
let r = a.is_authorized_core(q, &pset, &es);
806+
let r = a.is_authorized_core(q.clone(), &pset, &es);
744807
assert_eq!(r.decision(), Some(Decision::Deny));
808+
809+
let r = a.evaluate_policies(&pset, q, &es);
810+
assert!(r.satisfied_permits.contains(&PolicyID::from_string("4")));
811+
assert!(r.satisfied_forbids.contains(&PolicyID::from_string("3")));
812+
assert!(r.errors.is_empty());
813+
assert!(r.permit_residuals.is_empty());
814+
assert!(r
815+
.forbid_residuals
816+
.get(&PolicyID::from_string("2"))
817+
.is_some());
745818
}
746819

747820
#[test]
@@ -808,8 +881,18 @@ mod test {
808881

809882
pset.add_static(parser::parse_policy(Some("3".into()), src3).unwrap())
810883
.unwrap();
811-
let r = a.is_authorized_core(q, &pset, &es);
884+
let r = a.is_authorized_core(q.clone(), &pset, &es);
812885
assert_eq!(r.decision(), Some(Decision::Deny));
886+
887+
let r = a.evaluate_policies(&pset, q, &es);
888+
assert!(r.satisfied_permits.is_empty());
889+
assert!(r.satisfied_forbids.contains(&PolicyID::from_string("3")));
890+
assert!(r.errors.is_empty());
891+
assert!(r
892+
.permit_residuals
893+
.get(&PolicyID::from_string("2"))
894+
.is_some());
895+
assert!(r.forbid_residuals.is_empty());
813896
}
814897
}
815898
// by default, Coverlay does not track coverage for lines after a line
@@ -850,6 +933,21 @@ impl PartialResponse {
850933
}
851934
}
852935

936+
/// Policy evaluation response returned from the `Authorizer`.
937+
#[derive(Debug, PartialEq, Clone)]
938+
pub struct EvaluationResponse {
939+
/// `PolicyID`s of the fully evaluated policies with a permit [`Effect`].
940+
pub satisfied_permits: HashSet<PolicyID>,
941+
/// `PolicyID`s of the fully evaluated policies with a forbid [`Effect`].
942+
pub satisfied_forbids: HashSet<PolicyID>,
943+
/// List of errors that occurred
944+
pub errors: Vec<AuthorizationError>,
945+
/// Residual policies with a permit [`Effect`].
946+
pub permit_residuals: PolicySet,
947+
/// Residual policies with a forbid [`Effect`].
948+
pub forbid_residuals: PolicySet,
949+
}
950+
853951
/// Diagnostics providing more information on how a `Decision` was reached
854952
#[derive(Debug, PartialEq, Clone)]
855953
pub struct Diagnostics {

cedar-policy/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4747
return a `RequestBuilder<&Schema>` so the `RequestBuilder<&Schema>::build`
4848
method checks the request against the schema provided and the
4949
`RequestBuilder<UnsetSchema>::build` method becomes infallible. (#559)
50+
- For the `partial-eval` experimental feature: added
51+
`Authorizer::evaluate_policies_partial` (#474)
5052

5153
### Fixed
5254

cedar-policy/src/api.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,33 @@ impl Authorizer {
733733
authorizer::ResponseKind::Partial(p) => PartialResponse::Residual(p.into()),
734734
}
735735
}
736+
737+
/// Evaluate an authorization request and respond with results that always includes
738+
/// residuals even if the [`Authorizer`] already reached a decision.
739+
#[cfg(feature = "partial-eval")]
740+
pub fn evaluate_policies_partial(
741+
&self,
742+
query: &Request,
743+
policy_set: &PolicySet,
744+
entities: &Entities,
745+
) -> EvaluationResponse {
746+
let authorizer::EvaluationResponse {
747+
satisfied_permits,
748+
satisfied_forbids,
749+
errors,
750+
permit_residuals,
751+
forbid_residuals,
752+
} = self
753+
.0
754+
.evaluate_policies(&policy_set.ast, query.0.clone(), &entities.0);
755+
EvaluationResponse {
756+
satisfied_permits: satisfied_permits.into_iter().map(PolicyId).collect(),
757+
satisfied_forbids: satisfied_forbids.into_iter().map(PolicyId).collect(),
758+
errors,
759+
permit_residuals: PolicySet::from_ast(permit_residuals),
760+
forbid_residuals: PolicySet::from_ast(forbid_residuals),
761+
}
762+
}
736763
}
737764

738765
/// Authorization response returned from the `Authorizer`
@@ -765,6 +792,22 @@ pub struct ResidualResponse {
765792
diagnostics: Diagnostics,
766793
}
767794

795+
/// A policy evaluation response obtained from `evaluate_policies_partial`.
796+
#[cfg(feature = "partial-eval")]
797+
#[derive(Debug, PartialEq, Eq, Clone)]
798+
pub struct EvaluationResponse {
799+
/// `PolicyId`s of fully evaluated policies with a permit [`Effect`]
800+
satisfied_permits: HashSet<PolicyId>,
801+
/// `PolicyId`s of fully evaluated policies with a forbid [`Effect`]
802+
satisfied_forbids: HashSet<PolicyId>,
803+
/// Errors that occurred during policy evaluation.
804+
errors: Vec<AuthorizationError>,
805+
/// Partially evaluated policies with a permit [`Effect`]
806+
permit_residuals: PolicySet,
807+
/// Partially evaluated policies with a forbid [`Effect`]
808+
forbid_residuals: PolicySet,
809+
}
810+
768811
/// Diagnostics providing more information on how a `Decision` was reached
769812
#[derive(Debug, PartialEq, Eq, Clone)]
770813
pub struct Diagnostics {
@@ -974,6 +1017,51 @@ impl From<authorizer::PartialResponse> for ResidualResponse {
9741017
}
9751018
}
9761019

1020+
#[cfg(feature = "partial-eval")]
1021+
impl EvaluationResponse {
1022+
/// Create a new `EvaluationResponse`.
1023+
pub fn new(
1024+
satisfied_permits: HashSet<PolicyId>,
1025+
satisfied_forbids: HashSet<PolicyId>,
1026+
errors: Vec<AuthorizationError>,
1027+
permit_residuals: PolicySet,
1028+
forbid_residuals: PolicySet,
1029+
) -> Self {
1030+
Self {
1031+
satisfied_permits,
1032+
satisfied_forbids,
1033+
errors,
1034+
permit_residuals,
1035+
forbid_residuals,
1036+
}
1037+
}
1038+
1039+
/// Get the `PolicyId`s of fully evaluated policies with a permit [`Effect`].
1040+
pub fn satisfied_permits(&self) -> impl Iterator<Item = &PolicyId> {
1041+
self.satisfied_permits.iter()
1042+
}
1043+
1044+
/// Get the `PolicyId`s of fully evaluated policies with a forbid [`Effect`].
1045+
pub fn satisfied_forbids(&self) -> impl Iterator<Item = &PolicyId> {
1046+
self.satisfied_forbids.iter()
1047+
}
1048+
1049+
/// Get the redisual policies with a permit [`Effect`].
1050+
pub fn permit_residuals(&self) -> &PolicySet {
1051+
&self.permit_residuals
1052+
}
1053+
1054+
/// Get the redisual policies with a permit [`Effect`].
1055+
pub fn forbid_residuals(&self) -> &PolicySet {
1056+
&self.forbid_residuals
1057+
}
1058+
1059+
/// Get the evaluation errors.
1060+
pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> {
1061+
self.errors.iter()
1062+
}
1063+
}
1064+
9771065
/// Used to select how a policy will be validated.
9781066
#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
9791067
#[non_exhaustive]

0 commit comments

Comments
 (0)