1use std::collections::{BTreeMap, HashMap, HashSet};
18use std::sync::Arc;
19
20use cedar_policy_core::ast::{self, Value};
21use cedar_policy_core::authorizer::Decision;
22use cedar_policy_core::batched_evaluator::is_authorized_batched;
23use cedar_policy_core::batched_evaluator::{
24 err::BatchedEvalError, EntityLoader as EntityLoaderInternal,
25};
26use cedar_policy_core::evaluator::{EvaluationError, RestrictedEvaluator};
27use cedar_policy_core::extensions::Extensions;
28use cedar_policy_core::tpe;
29use itertools::Itertools;
30use ref_cast::RefCast;
31use smol_str::SmolStr;
32
33use crate::{
34 api, tpe_err, Authorizer, Context, Entities, Entity, EntityId, EntityTypeName, EntityUid,
35 PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicyId,
36 PolicySet, Request, RequestValidationError, RestrictedExpression, Schema,
37 TpeReauthorizationError,
38};
39
40#[doc = include_str!("../../experimental_warning.md")]
43#[repr(transparent)]
44#[derive(Debug, Clone, RefCast)]
45pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
46
47#[doc(hidden)]
48impl AsRef<tpe::request::PartialEntityUID> for PartialEntityUid {
49 fn as_ref(&self) -> &tpe::request::PartialEntityUID {
50 &self.0
51 }
52}
53
54impl PartialEntityUid {
55 pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
57 Self(tpe::request::PartialEntityUID {
58 ty: ty.0,
59 eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
60 })
61 }
62
63 pub fn from_concrete(euid: EntityUid) -> Self {
65 let (ty, eid) = euid.0.components();
66 Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
67 }
68}
69
70#[doc = include_str!("../../experimental_warning.md")]
74#[repr(transparent)]
75#[derive(Debug, Clone, RefCast)]
76pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
77
78#[doc(hidden)]
79impl AsRef<tpe::request::PartialRequest> for PartialRequest {
80 fn as_ref(&self) -> &tpe::request::PartialRequest {
81 &self.0
82 }
83}
84
85impl PartialRequest {
86 pub fn new(
88 principal: PartialEntityUid,
89 action: EntityUid,
90 resource: PartialEntityUid,
91 context: Option<Context>,
92 schema: &Schema,
93 ) -> Result<Self, PartialRequestCreationError> {
94 let context = context
95 .map(|c| match c.0 {
96 ast::Context::RestrictedResidual(_) => {
97 Err(PartialRequestCreationError::ContextContainsUnknowns)
98 }
99 ast::Context::Value(m) => Ok(m),
100 })
101 .transpose()?;
102 tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
103 .map(Self)
104 .map_err(|e| PartialRequestCreationError::Validation(e.into()))
105 }
106}
107
108#[doc = include_str!("../../experimental_warning.md")]
112#[repr(transparent)]
113#[derive(Debug, Clone, RefCast)]
114pub struct ResourceQueryRequest(pub(crate) PartialRequest);
115
116impl ResourceQueryRequest {
117 pub fn new(
119 principal: EntityUid,
120 action: EntityUid,
121 resource: EntityTypeName,
122 context: Context,
123 schema: &Schema,
124 ) -> Result<Self, PartialRequestCreationError> {
125 PartialRequest::new(
126 PartialEntityUid(principal.0.into()),
127 action,
128 PartialEntityUid::new(resource, None),
129 Some(context),
130 schema,
131 )
132 .map(Self)
133 }
134
135 fn principal(&self) -> EntityUid {
136 #[expect(
137 clippy::unwrap_used,
138 reason = "constructor requires concrete principal"
139 )]
140 EntityUid(self.0 .0.principal().clone().try_into().unwrap())
141 }
142
143 fn context(&self) -> Context {
144 #[expect(clippy::unwrap_used, reason = "constructor requires concrete context")]
145 let context_attrs = self.0 .0.context_attrs().unwrap();
146 #[expect(
147 clippy::unwrap_used,
148 reason = "building context from BTreeMap iter, so no duplicates are possible"
149 )]
150 Context::from_pairs(
151 context_attrs
152 .iter()
153 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
154 )
155 .unwrap()
156 }
157
158 pub fn to_request(
165 &self,
166 resource_id: EntityId,
167 schema: Option<&Schema>,
168 ) -> Result<Request, RequestValidationError> {
169 Request::new(
170 self.principal(),
171 EntityUid(self.0 .0.action().clone()),
172 EntityUid::from_type_name_and_id(
173 EntityTypeName(self.0 .0.resource_type().clone()),
174 resource_id,
175 ),
176 self.context(),
177 schema,
178 )
179 }
180}
181
182#[doc = include_str!("../../experimental_warning.md")]
186#[repr(transparent)]
187#[derive(Debug, Clone, RefCast)]
188pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
189
190impl PrincipalQueryRequest {
191 pub fn new(
193 principal: EntityTypeName,
194 action: EntityUid,
195 resource: EntityUid,
196 context: Context,
197 schema: &Schema,
198 ) -> Result<Self, PartialRequestCreationError> {
199 PartialRequest::new(
200 PartialEntityUid::new(principal, None),
201 action,
202 PartialEntityUid(resource.0.into()),
203 Some(context),
204 schema,
205 )
206 .map(Self)
207 }
208
209 fn resource(&self) -> EntityUid {
210 #[expect(clippy::unwrap_used, reason = "constructor requires concrete resource")]
211 EntityUid(self.0 .0.resource().clone().try_into().unwrap())
212 }
213
214 fn context(&self) -> Context {
215 #[expect(clippy::unwrap_used, reason = "constructor requires concrete context")]
216 let context_attrs = self.0 .0.context_attrs().unwrap();
217 #[expect(
218 clippy::unwrap_used,
219 reason = "building context from BTreeMap iter, so no duplicates are possible"
220 )]
221 Context::from_pairs(
222 context_attrs
223 .iter()
224 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
225 )
226 .unwrap()
227 }
228
229 pub fn to_request(
236 &self,
237 principal_id: EntityId,
238 schema: Option<&Schema>,
239 ) -> Result<Request, RequestValidationError> {
240 Request::new(
241 EntityUid::from_type_name_and_id(
242 EntityTypeName(self.0 .0.principal_type().clone()),
243 principal_id,
244 ),
245 EntityUid(self.0 .0.action().clone()),
246 self.resource(),
247 self.context(),
248 schema,
249 )
250 }
251}
252
253#[doc = include_str!("../../experimental_warning.md")]
258#[derive(Debug, Clone)]
259pub struct ActionQueryRequest {
260 principal: PartialEntityUid,
261 resource: PartialEntityUid,
262 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
263 schema: Schema,
264}
265
266impl ActionQueryRequest {
267 pub fn new(
276 principal: PartialEntityUid,
277 resource: PartialEntityUid,
278 context: Option<Context>,
279 schema: Schema,
280 ) -> Result<Self, PartialRequestCreationError> {
281 let context = context
282 .map(|c| match c.0 {
283 ast::Context::RestrictedResidual(_) => {
284 Err(PartialRequestCreationError::ContextContainsUnknowns)
285 }
286 ast::Context::Value(m) => Ok(m),
287 })
288 .transpose()?;
289 Ok(Self {
290 principal,
291 resource,
292 context,
293 schema,
294 })
295 }
296
297 fn partial_request(
298 &self,
299 action: EntityUid,
300 ) -> Result<PartialRequest, cedar_policy_core::validator::RequestValidationError> {
301 tpe::request::PartialRequest::new(
302 self.principal.0.clone(),
303 action.0,
304 self.resource.0.clone(),
305 self.context.clone(),
306 &self.schema.0,
307 )
308 .map(PartialRequest)
309 }
310}
311
312#[doc = include_str!("../../experimental_warning.md")]
314#[repr(transparent)]
315#[derive(Debug, Clone, RefCast)]
316pub struct PartialEntity(pub(crate) tpe::entities::PartialEntity);
317
318impl PartialEntity {
319 pub fn new(
321 uid: EntityUid,
322 attrs: Option<BTreeMap<SmolStr, RestrictedExpression>>,
323 ancestors: Option<HashSet<EntityUid>>,
324 tags: Option<BTreeMap<SmolStr, RestrictedExpression>>,
325 schema: &Schema,
326 ) -> Result<Self, PartialEntityError> {
327 Ok(Self(tpe::entities::PartialEntity::new(
328 uid.0,
329 attrs
330 .map(|ps| {
331 ps.into_iter()
332 .map(|(k, v)| {
333 Ok((
334 k,
335 RestrictedEvaluator::new(Extensions::all_available())
336 .interpret(v.0.as_borrowed())?,
337 ))
338 })
339 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
340 })
341 .transpose()?,
342 ancestors.map(|s| s.into_iter().map(|e| e.0).collect()),
343 tags.map(|ps| {
344 ps.into_iter()
345 .map(|(k, v)| {
346 Ok((
347 k,
348 RestrictedEvaluator::new(Extensions::all_available())
349 .interpret(v.0.as_borrowed())?,
350 ))
351 })
352 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
353 })
354 .transpose()?,
355 &schema.0,
356 )?))
357 }
358}
359
360#[doc = include_str!("../../experimental_warning.md")]
362#[repr(transparent)]
363#[derive(Debug, Clone, RefCast)]
364pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
365
366#[doc(hidden)]
367impl AsRef<tpe::entities::PartialEntities> for PartialEntities {
368 fn as_ref(&self) -> &tpe::entities::PartialEntities {
369 &self.0
370 }
371}
372
373impl PartialEntities {
374 pub fn from_json_value(
378 value: serde_json::Value,
379 schema: &Schema,
380 ) -> Result<Self, tpe_err::EntitiesError> {
381 tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
382 }
383
384 pub fn from_concrete(
386 entities: Entities,
387 schema: &Schema,
388 ) -> Result<Self, tpe_err::EntitiesError> {
389 tpe::entities::PartialEntities::from_concrete(entities.0, &schema.0).map(Self)
390 }
391
392 pub fn empty() -> Self {
394 Self(tpe::entities::PartialEntities::new())
395 }
396
397 pub fn from_partial_entities(
399 entities: impl IntoIterator<Item = PartialEntity>,
400 schema: &Schema,
401 ) -> Result<Self, tpe_err::EntitiesError> {
402 Ok(Self(tpe::entities::PartialEntities::from_entities(
403 entities.into_iter().map(|entity| entity.0),
404 &schema.0,
405 )?))
406 }
407}
408
409#[doc = include_str!("../../experimental_warning.md")]
416#[repr(transparent)]
417#[derive(Debug, Clone, RefCast)]
418pub struct TpeResponse<'a>(pub(crate) tpe::response::Response<'a>);
419
420#[doc(hidden)]
421impl<'a> AsRef<tpe::response::Response<'a>> for TpeResponse<'a> {
422 fn as_ref(&self) -> &tpe::response::Response<'a> {
423 &self.0
424 }
425}
426
427impl TpeResponse<'_> {
428 pub fn decision(&self) -> Option<Decision> {
439 self.0.decision()
440 }
441
442 pub fn reason(&self) -> Option<impl Iterator<Item = &PolicyId>> {
455 Some(self.0.reason()?.map(PolicyId::ref_cast))
456 }
457
458 pub fn residual_permits(&self) -> impl Iterator<Item = &PolicyId> {
468 self.0
469 .residual_permits()
470 .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
471 }
472
473 pub fn true_permits(&self) -> impl Iterator<Item = &PolicyId> {
489 self.0
490 .true_permits()
491 .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
492 }
493
494 pub fn false_permits(&self) -> impl Iterator<Item = &PolicyId> {
500 self.0
501 .false_permits()
502 .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
503 }
504
505 pub fn error_permits(&self) -> impl Iterator<Item = &PolicyId> {
512 self.0
513 .error_permits()
514 .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
515 }
516
517 pub fn residual_forbids(&self) -> impl Iterator<Item = &PolicyId> {
526 self.0
527 .residual_forbids()
528 .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
529 }
530
531 pub fn true_forbids(&self) -> impl Iterator<Item = &PolicyId> {
537 self.0
538 .true_forbids()
539 .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
540 }
541
542 pub fn false_forbids(&self) -> impl Iterator<Item = &PolicyId> {
548 self.0
549 .false_forbids()
550 .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
551 }
552
553 pub fn error_forbids(&self) -> impl Iterator<Item = &PolicyId> {
560 self.0
561 .error_forbids()
562 .map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
563 }
564
565 pub fn reauthorize(
573 &self,
574 request: &Request,
575 entities: &Entities,
576 ) -> Result<api::Response, TpeReauthorizationError> {
577 self.0
578 .reauthorize(&request.0, &entities.0)
579 .map(Into::into)
580 .map_err(Into::into)
581 }
582
583 pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
621 self.0
622 .residual_permits()
623 .chain(self.0.residual_forbids())
624 .map(|p| Policy::from_ast(p.clone().into()))
625 }
626
627 pub fn policies(&self) -> impl Iterator<Item = Policy> + '_ {
638 self.0
639 .policies()
640 .map(|p| Policy::from_ast(p.clone().into()))
641 }
642
643 pub fn policy_set(&self) -> PolicySet {
647 PolicySet::from_ast(self.0.policy_set())
648 }
649
650 #[deprecated(
652 since = "4.12.0",
653 note = "TpeResponse::residual_policies now returns only non-trivial residual policies"
654 )]
655 pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
656 self.residual_policies()
657 }
658
659 pub fn get_policy(&self, id: &PolicyId) -> Option<Policy> {
663 self.0
664 .get_residual_policy(id.as_ref())
665 .map(|p| Policy::from_ast(p.clone().into()))
666 }
667}
668
669#[doc = include_str!("../../experimental_warning.md")]
676pub trait EntityLoader {
677 fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>>;
681}
682
683struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
685
686impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
687 fn load_entities(
688 &mut self,
689 uids: &HashSet<ast::EntityUID>,
690 ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
691 let ids = uids
692 .iter()
693 .map(|id| EntityUid::ref_cast(id).clone())
694 .collect();
695 self.0
696 .load_entities(&ids)
697 .into_iter()
698 .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
699 .collect()
700 }
701}
702
703#[doc = include_str!("../../experimental_warning.md")]
705#[derive(Debug)]
706
707pub struct TestEntityLoader<'a> {
708 entities: &'a Entities,
709}
710
711impl<'a> TestEntityLoader<'a> {
712 pub fn new(entities: &'a Entities) -> Self {
714 Self { entities }
715 }
716}
717
718impl EntityLoader for TestEntityLoader<'_> {
719 fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>> {
720 uids.iter()
721 .map(|uid| {
722 let entity = self.entities.get(uid).cloned();
723 (uid.clone(), entity)
724 })
725 .collect()
726 }
727}
728
729impl PolicySet {
730 #[doc = include_str!("../../experimental_warning.md")]
738 pub fn tpe<'a>(
739 &self,
740 request: &'a PartialRequest,
741 entities: &'a PartialEntities,
742 schema: &'a Schema,
743 ) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
744 use cedar_policy_core::tpe::is_authorized;
745 let ps = &self.ast;
746 let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
747 Ok(TpeResponse(res))
748 }
749
750 #[doc = include_str!("../../experimental_warning.md")]
759 pub fn is_authorized_batched(
760 &self,
761 query: &Request,
762 schema: &Schema,
763 loader: &mut dyn EntityLoader,
764 max_iters: u32,
765 ) -> Result<Decision, BatchedEvalError> {
766 is_authorized_batched(
767 &query.0,
768 &self.ast,
769 &schema.0,
770 &mut EntityLoaderWrapper(loader),
771 max_iters,
772 )
773 }
774
775 #[doc = include_str!("../../experimental_warning.md")]
777 pub fn query_resource(
778 &self,
779 request: &ResourceQueryRequest,
780 entities: &Entities,
781 schema: &Schema,
782 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
783 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
784 let tpe_response = self.tpe(&request.0, &partial_entities, schema)?;
785 let policies = tpe_response.policy_set();
786 match tpe_response.decision() {
787 Some(Decision::Allow) => Ok(entities
788 .iter()
789 .filter(|entity| entity.0.uid().entity_type() == request.0.0.resource_type())
790 .map(Entity::uid)
791 .collect_vec()
792 .into_iter()),
793 Some(Decision::Deny) => Ok(vec![].into_iter()),
794 None => Ok(entities
795 .iter()
796 .filter(|entity| entity.0.uid().entity_type() == request.0.0.resource_type())
797 .filter(|entity| {
798 #[expect(
799 clippy::unwrap_used, reason = "`to_request` cannot panic because we do not pass a schema. However, the correctness of the authorization
800 decision depends on having valid a request and entities, but we do not do any validation here. Entities were already validated by
801 `PartialEntities::from_concrete`. The request was _mostly_ validated by its constructor, but the concrete request could still be invalid
802 if the resource entity is an enum entity and the id is not an instance of that enum. This cannot happen here because we draw candidate
803 resources from the entities, which we know are valid."
804 )]
805 let req = request.to_request(entity.uid().id().clone(), None).unwrap();
806 let authorizer = Authorizer::new();
807 let auth_response = authorizer
808 .is_authorized(
809 &req,
810 &policies,
811 entities,
812 );
813 auth_response.decision() == Decision::Allow
814 })
815 .map(Entity::uid)
816 .collect_vec()
817 .into_iter()),
818 }
819 }
820
821 #[doc = include_str!("../../experimental_warning.md")]
823 pub fn query_principal(
824 &self,
825 request: &PrincipalQueryRequest,
826 entities: &Entities,
827 schema: &Schema,
828 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
829 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
830 let tpe_response = self.tpe(&request.0, &partial_entities, schema)?;
831 let policies = tpe_response.policy_set();
832 match tpe_response.decision() {
833 Some(Decision::Allow) => Ok(entities
834 .iter()
835 .filter(|entity| entity.0.uid().entity_type() == request.0.0.principal_type())
836 .map(Entity::uid)
837 .collect_vec()
838 .into_iter()),
839 Some(Decision::Deny) => Ok(vec![].into_iter()),
840 None => Ok(entities
841 .iter()
842 .filter(|entity| entity.0.uid().entity_type() == request.0.0.principal_type())
843 .filter(|entity| {
844 #[expect(
845 clippy::unwrap_used, reason = "`to_request` cannot panic because we do not pass a schema. However, the correctness of the authorization
846 decision depends on having valid a request and entities, but we do not do any validation here. Entities were already validated by
847 `PartialEntities::from_concrete`. The request was _mostly_ validated by its constructor, but the concrete request could still be invalid
848 if the principal entity is an enum entity and the id is not an instance of that enum. This cannot happen here because we draw candidate
849 principals from the entities, which we know are valid."
850 )]
851 let req = request.to_request(entity.uid().id().clone(), None).unwrap();
852 let authorizer = Authorizer::new();
853 let auth_response = authorizer
854 .is_authorized(
855 &req,
856 &policies,
857 entities,
858 );
859 auth_response.decision() == Decision::Allow
860 })
861 .map(Entity::uid)
862 .collect_vec()
863 .into_iter()),
864 }
865 }
866
867 #[doc = include_str!("../../experimental_warning.md")]
933 pub fn query_action<'a>(
934 &self,
935 request: &'a ActionQueryRequest,
936 entities: &PartialEntities,
937 ) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError> {
938 let mut authorized_actions = Vec::new();
939 for action in request
945 .schema
946 .0
947 .actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
948 {
949 if let Ok(partial_request) = request.partial_request(action.clone().into()) {
953 let decision = self
954 .tpe(&partial_request, entities, &request.schema)?
955 .decision();
956 if decision != Some(Decision::Deny) {
957 authorized_actions.push((RefCast::ref_cast(action), decision));
958 }
959 }
960 }
961 Ok(authorized_actions.into_iter())
962 }
963}
964
965#[cfg(test)]
966mod tpe_tests {
967 use std::{
968 collections::{BTreeMap, HashSet},
969 str::FromStr,
970 };
971
972 use cedar_policy_core::tpe::err::EntitiesError;
973 use cool_asserts::assert_matches;
974
975 use crate::{PartialEntity, PartialEntityError, RestrictedExpression, Schema};
976
977 #[test]
978 fn entity_construction() {
979 let schema = Schema::from_str(
980 r"
981 entity A in B tags Long;
982 entity B;
983 ",
984 )
985 .unwrap();
986 PartialEntity::new(
987 r#"A::"foo""#.parse().unwrap(),
988 None,
989 Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
990 Some(BTreeMap::from_iter([(
991 "".into(),
992 RestrictedExpression::new_long(1),
993 )])),
994 &schema,
995 )
996 .unwrap();
997 assert_matches!(
998 PartialEntity::new(
999 r#"A::"foo""#.parse().unwrap(),
1000 None,
1001 Some(HashSet::from_iter([r#"C::"c""#.parse().unwrap()])),
1002 Some(BTreeMap::from_iter([(
1003 "".into(),
1004 RestrictedExpression::new_long(1)
1005 )])),
1006 &schema
1007 ),
1008 Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1009 );
1010
1011 assert_matches!(
1012 PartialEntity::new(
1013 r#"A::"foo""#.parse().unwrap(),
1014 None,
1015 Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
1016 Some(BTreeMap::from_iter([(
1017 "".into(),
1018 RestrictedExpression::new_bool(true)
1019 )])),
1020 &schema
1021 ),
1022 Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1023 );
1024 }
1025
1026 mod streaming_service {
1027 use std::{collections::BTreeMap, str::FromStr};
1028
1029 use cedar_policy_core::{authorizer::Decision, tpe::err::EntitiesError};
1030 use cool_asserts::assert_matches;
1031 use itertools::Itertools;
1032 use similar_asserts::assert_eq;
1033
1034 use crate::{
1035 ActionConstraint, ActionQueryRequest, Context, Entities, EntityId, EntityUid,
1036 PartialEntities, PartialEntity, PartialEntityError, PartialEntityUid, PartialRequest,
1037 PolicySet, PrincipalConstraint, PrincipalQueryRequest, Request, ResourceConstraint,
1038 ResourceQueryRequest, RestrictedExpression, Schema,
1039 };
1040
1041 #[test]
1042 fn entities_construction() {
1043 let schema = schema();
1044 PartialEntity::new(
1045 r#"Movie::"foo""#.parse().unwrap(),
1046 None,
1047 None,
1048 None,
1049 &schema,
1050 )
1051 .unwrap();
1052 PartialEntity::new(
1053 r#"Show::"foo""#.parse().unwrap(),
1054 Some(BTreeMap::from_iter([
1055 ("isFree".into(), RestrictedExpression::new_bool(true)),
1056 (
1057 "releaseDate".into(),
1058 RestrictedExpression::new_datetime("2025-01-01"),
1059 ),
1060 (
1061 "isEarlyAccess".into(),
1062 RestrictedExpression::new_bool(false),
1063 ),
1064 ])),
1065 None,
1066 None,
1067 &schema,
1068 )
1069 .unwrap();
1070
1071 assert_matches!(
1072 PartialEntity::new(
1073 r#"Show::"foo""#.parse().unwrap(),
1074 Some(BTreeMap::from_iter([
1075 ("isFree".into(), RestrictedExpression::new_bool(true)),
1076 (
1077 "isEarlyAccess".into(),
1078 RestrictedExpression::new_bool(false)
1079 ),
1080 ])),
1081 None,
1082 None,
1083 &schema
1084 ),
1085 Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
1086 );
1087
1088 let e1 = PartialEntity::new(
1089 r#"Show::"foo""#.parse().unwrap(),
1090 Some(BTreeMap::from_iter([
1091 ("isFree".into(), RestrictedExpression::new_bool(true)),
1092 (
1093 "releaseDate".into(),
1094 RestrictedExpression::new_datetime("2025-01-01"),
1095 ),
1096 (
1097 "isEarlyAccess".into(),
1098 RestrictedExpression::new_bool(false),
1099 ),
1100 ])),
1101 None,
1102 None,
1103 &schema,
1104 )
1105 .unwrap();
1106 let e2 = PartialEntity::new(
1107 r#"Subscriber::"a""#.parse().unwrap(),
1108 None,
1109 None,
1110 None,
1111 &schema,
1112 )
1113 .unwrap();
1114 PartialEntities::from_partial_entities([e1.clone(), e2.clone()], &schema).unwrap();
1115 let e3 = PartialEntity::new(
1116 r#"Show::"foo""#.parse().unwrap(),
1117 Some(BTreeMap::from_iter([
1118 ("isFree".into(), RestrictedExpression::new_bool(true)),
1119 (
1120 "releaseDate".into(),
1121 RestrictedExpression::new_datetime("2025-01-01"),
1122 ),
1123 ("isEarlyAccess".into(), RestrictedExpression::new_bool(true)),
1124 ])),
1125 None,
1126 None,
1127 &schema,
1128 )
1129 .unwrap();
1130 assert_matches!(
1131 PartialEntities::from_partial_entities([e1, e2, e3], &schema),
1132 Err(EntitiesError::Duplicate(_)),
1133 );
1134 }
1135
1136 #[track_caller]
1137 fn schema() -> Schema {
1138 Schema::from_cedarschema_str(
1139 r"
1140 // Types
1141type Subscription = {
1142 tier: String
1143};
1144type Profile = {
1145 isKid: Bool
1146};
1147
1148// Entities
1149entity FreeMember;
1150entity Subscriber = {
1151 subscription: Subscription,
1152 profile: Profile
1153};
1154entity Movie = {
1155 isFree: Bool,
1156 needsRentOrBuy: Bool,
1157 isOscarNominated: Bool
1158};
1159entity Show = {
1160 isFree: Bool,
1161 releaseDate: datetime,
1162 isEarlyAccess: Bool
1163};
1164
1165// Actions for content in general
1166action watch
1167 appliesTo {
1168 principal: [FreeMember, Subscriber],
1169 resource: [Movie, Show],
1170 context: {
1171 now: {
1172 datetime: datetime,
1173 localTimeOffset: duration
1174 }
1175 }
1176 };
1177
1178// Actions for movies only
1179action rent, buy
1180 appliesTo {
1181 principal: [FreeMember, Subscriber],
1182 resource: Movie,
1183 context: {
1184 now: {
1185 datetime: datetime
1186 }
1187 }
1188 };
1189 ",
1190 )
1191 .unwrap()
1192 .0
1193 }
1194
1195 #[track_caller]
1196 fn policy_set() -> PolicySet {
1197 PolicySet::from_str(
1198 r#"
1199 // Subscriber Content Access (Shows)
1200@id("subscriber-content-access/show")
1201permit (
1202 principal is Subscriber,
1203 action == Action::"watch",
1204 resource is Show
1205)
1206unless
1207{ resource.isEarlyAccess && context.now.datetime < resource.releaseDate };
1208
1209// Subscriber Content Access (Movies)
1210@id("subscriber-content-access/movie")
1211permit (
1212 principal is Subscriber,
1213 action == Action::"watch",
1214 resource is Movie
1215)
1216unless { resource.needsRentOrBuy };
1217
1218// Free Content Access
1219@id("free-content-access")
1220permit (
1221 principal is FreeMember,
1222 action == Action::"watch",
1223 resource
1224)
1225when { resource.isFree };
1226
1227// Promo: Rent/Buy Oscar-Nominated Movies Until the Oscars
1228@id("rent-buy-oscar-movie")
1229permit (
1230 principal is Subscriber,
1231 action in [Action::"rent", Action::"buy"],
1232 resource is Movie
1233)
1234when
1235{
1236 resource.isOscarNominated &&
1237 context.now.datetime >= datetime("2025-02-02T19:00:00-0500") &&
1238 context.now.datetime < datetime(
1239 "2025-03-02T19:00:00-0500"
1240 ) // Oscars Night
1241};
1242
1243// Early Access (24h) to Shows for Premium Subscribers
1244@id("early-access-show")
1245permit (
1246 principal is Subscriber,
1247 action == Action::"watch",
1248 resource is Show
1249)
1250when
1251{
1252 resource.isEarlyAccess &&
1253 principal.subscription.tier == "premium" &&
1254 context.now.datetime >= resource.releaseDate.offset(duration("-24h"))
1255};
1256
1257// Forbid Bedtime Access to Kid Profile
1258@id("forbid-bedtime-watch-kid-profile")
1259forbid (
1260 principal is Subscriber,
1261 action == Action::"watch",
1262 resource
1263)
1264when { principal.profile.isKid }
1265unless
1266{
1267 // `toTime()` returns the duration modulo one day (i.e., it ignores the "date"
1268 // component). Here, we use it to calculate the subscriber's local time and
1269 // compare the result against durations that represent 6:00AM and 9:00PM.
1270 duration("6h") <= context.now
1271 .datetime
1272 .offset
1273 (
1274 context.now.localTimeOffset
1275 )
1276 .toTime
1277 (
1278 ) &&
1279 context.now.datetime.offset(context.now.localTimeOffset).toTime() <= duration(
1280 "21h"
1281 )
1282};
1283 "#,
1284 )
1285 .unwrap()
1286 }
1287
1288 #[track_caller]
1289 fn entities() -> Entities {
1290 Entities::from_json_value(
1291 serde_json::json!(
1292 [
1293 {
1294 "uid": {
1295 "type": "Subscriber",
1296 "id": "Alice"
1297 },
1298 "attrs": {
1299 "subscription" : {
1300 "tier": "standard"
1301 },
1302 "profile" : {
1303 "isKid": false
1304 }
1305 },
1306 "parents": []
1307 },
1308 {
1309 "uid": {
1310 "type": "FreeMember",
1311 "id": "Bob"
1312 },
1313 "attrs": {},
1314 "parents": []
1315 },
1316 {
1317 "uid": {
1318 "type": "Subscriber",
1319 "id": "Charlie"
1320 },
1321 "attrs": {
1322 "subscription" : {
1323 "tier": "premium"
1324 },
1325 "profile" : {
1326 "isKid": false
1327 }
1328 },
1329 "parents": []
1330 },
1331 {
1332 "uid": {
1333 "type": "Subscriber",
1334 "id": "Dave"
1335 },
1336 "attrs": {
1337 "subscription" : {
1338 "tier": "standard"
1339 },
1340 "profile" : {
1341 "isKid": true
1342 }
1343 },
1344 "parents": []
1345 },
1346 {
1347 "uid": {
1348 "type": "Movie",
1349 "id": "The Godparent"
1350 },
1351 "attrs": {
1352 "isFree" : true,
1353 "needsRentOrBuy" : false,
1354 "isOscarNominated": true
1355 },
1356 "parents": []
1357 },
1358 {
1359 "uid": {
1360 "type": "Movie",
1361 "id": "The Gleaming"
1362 },
1363 "attrs": {
1364 "isFree" : false,
1365 "needsRentOrBuy" : false,
1366 "isOscarNominated": false
1367 },
1368 "parents": []
1369 },
1370 {
1371 "uid": {
1372 "type": "Movie",
1373 "id": "Devilish"
1374 },
1375 "attrs": {
1376 "isFree" : false,
1377 "needsRentOrBuy" : true,
1378 "isOscarNominated": true
1379 },
1380 "parents": []
1381 },
1382 {
1383 "uid": {
1384 "type": "Show",
1385 "id": "Buddies"
1386 },
1387 "attrs": {
1388 "isFree" : false,
1389 "releaseDate": "2024-10-10",
1390 "isEarlyAccess": false
1391 },
1392 "parents": []
1393 },
1394 {
1395 "uid": {
1396 "type": "Show",
1397 "id": "Breach"
1398 },
1399 "attrs": {
1400 "isFree" : false,
1401 "releaseDate": "2025-02-21",
1402 "isEarlyAccess": true
1403 },
1404 "parents": []
1405 }
1406 ]
1407 ),
1408 Some(&schema()),
1409 )
1410 .unwrap()
1411 }
1412
1413 #[test]
1414 fn run_tpe() {
1415 let schema = schema();
1416 let request = PartialRequest::new(
1417 PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1418 r#"Action::"watch""#.parse().unwrap(),
1419 PartialEntityUid::new("Movie".parse().unwrap(), None),
1420 Some(
1421 Context::from_pairs([(
1422 "now".into(),
1423 RestrictedExpression::new_record([
1424 (
1425 "datetime".into(),
1426 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#)
1427 .unwrap(),
1428 ),
1429 (
1430 "localTimeOffset".into(),
1431 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1432 ),
1433 ])
1434 .unwrap(),
1435 )])
1436 .unwrap(),
1437 ),
1438 &schema,
1439 )
1440 .unwrap();
1441 let policies = policy_set();
1442 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
1443
1444 let response = policies
1445 .tpe(&request, &partial_entities, &schema)
1446 .expect("tpe should succeed");
1447
1448 assert_eq!(response.policies().count(), policies.num_of_policies());
1449 for p in response.residual_policies() {
1450 assert_matches!(p.action_constraint(), ActionConstraint::Any);
1451 assert_matches!(p.principal_constraint(), PrincipalConstraint::Any);
1452 assert_matches!(p.resource_constraint(), ResourceConstraint::Any);
1453 }
1454 assert_eq!(
1455 response
1456 .residual_policies()
1457 .next()
1458 .unwrap()
1459 .annotation("id")
1460 .unwrap(),
1461 "subscriber-content-access/movie"
1462 );
1463
1464 assert_eq!(response.decision(), None);
1465 assert!(response.reason().is_none());
1466
1467 let request = Request::new(
1468 EntityUid::from_type_name_and_id(
1469 "Subscriber".parse().unwrap(),
1470 EntityId::new("Alice"),
1471 ),
1472 r#"Action::"watch""#.parse().unwrap(),
1473 EntityUid::from_type_name_and_id(
1474 "Movie".parse().unwrap(),
1475 EntityId::new("The Godparent"),
1476 ),
1477 Context::from_pairs([(
1478 "now".into(),
1479 RestrictedExpression::new_record([
1480 (
1481 "datetime".into(),
1482 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1483 ),
1484 (
1485 "localTimeOffset".into(),
1486 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1487 ),
1488 ])
1489 .unwrap(),
1490 )])
1491 .unwrap(),
1492 Some(&schema),
1493 )
1494 .unwrap();
1495 assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
1496 assert_eq!(res.decision(), Decision::Allow);
1497 });
1498
1499 let request = Request::new(
1500 EntityUid::from_type_name_and_id(
1501 "Subscriber".parse().unwrap(),
1502 EntityId::new("Alice"),
1503 ),
1504 r#"Action::"watch""#.parse().unwrap(),
1505 EntityUid::from_type_name_and_id(
1506 "Movie".parse().unwrap(),
1507 EntityId::new("Devilish"),
1508 ),
1509 Context::from_pairs([(
1510 "now".into(),
1511 RestrictedExpression::new_record([
1512 (
1513 "datetime".into(),
1514 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1515 ),
1516 (
1517 "localTimeOffset".into(),
1518 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1519 ),
1520 ])
1521 .unwrap(),
1522 )])
1523 .unwrap(),
1524 Some(&schema),
1525 )
1526 .unwrap();
1527 assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
1528 assert_eq!(res.decision(), Decision::Deny);
1529 });
1530 }
1531
1532 #[test]
1535 fn policy_set_returns_residuals() {
1536 let schema = schema();
1537 let request = PartialRequest::new(
1538 PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1539 r#"Action::"watch""#.parse().unwrap(),
1540 PartialEntityUid::new("Movie".parse().unwrap(), None),
1542 Some(
1543 Context::from_pairs([(
1544 "now".into(),
1545 RestrictedExpression::new_record([
1546 (
1547 "datetime".into(),
1548 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#)
1549 .unwrap(),
1550 ),
1551 (
1552 "localTimeOffset".into(),
1553 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1554 ),
1555 ])
1556 .unwrap(),
1557 )])
1558 .unwrap(),
1559 ),
1560 &schema,
1561 )
1562 .unwrap();
1563 let policies = policy_set();
1564 let partial_entities = PartialEntities::from_json_value(
1565 serde_json::json!([
1566 {
1567 "uid": { "type": "Subscriber", "id": "Alice" },
1568 "attrs": {
1569 "subscription": { "tier": "standard" },
1570 "profile": { "isKid": false }
1571 },
1572 "parents": []
1573 }
1574 ]),
1575 &schema,
1576 )
1577 .unwrap();
1578
1579 let response = policies
1580 .tpe(&request, &partial_entities, &schema)
1581 .expect("tpe should succeed");
1582 assert_eq!(response.decision(), None);
1583
1584 let residual_set = response.policy_set();
1585
1586 assert_eq!(residual_set.num_of_policies(), policies.num_of_policies());
1588
1589 for p in residual_set.policies() {
1592 assert_matches!(p.action_constraint(), ActionConstraint::Any);
1593 assert_matches!(p.principal_constraint(), PrincipalConstraint::Any);
1594 assert_matches!(p.resource_constraint(), ResourceConstraint::Any);
1595 }
1596
1597 let mut from_policies: Vec<(String, String)> = response
1599 .policies()
1600 .map(|p| (p.id().to_string(), p.to_cedar().unwrap()))
1601 .collect();
1602 from_policies.sort();
1603 let mut from_set: Vec<(String, String)> = residual_set
1604 .policies()
1605 .map(|p| (p.id().to_string(), p.to_cedar().unwrap()))
1606 .collect();
1607 from_set.sort();
1608 assert_eq!(from_set, from_policies);
1609 }
1610
1611 #[test]
1612 fn query_resource() {
1613 let schema = schema();
1614 let policies = policy_set();
1615 let request = ResourceQueryRequest::new(
1616 r#"Subscriber::"Alice""#.parse().unwrap(),
1617 r#"Action::"watch""#.parse().unwrap(),
1618 "Movie".parse().unwrap(),
1619 Context::from_pairs([(
1620 "now".into(),
1621 RestrictedExpression::new_record([
1622 (
1623 "datetime".into(),
1624 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1625 ),
1626 (
1627 "localTimeOffset".into(),
1628 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1629 ),
1630 ])
1631 .unwrap(),
1632 )])
1633 .unwrap(),
1634 &schema,
1635 )
1636 .unwrap();
1637
1638 let movies = policies
1641 .query_resource(&request, &entities(), &schema)
1642 .unwrap()
1643 .sorted()
1644 .collect_vec();
1645 assert_eq!(
1646 movies,
1647 &[
1648 EntityUid::from_str(r#"Movie::"The Gleaming""#).unwrap(),
1649 EntityUid::from_str(r#"Movie::"The Godparent""#).unwrap(),
1650 ]
1651 );
1652 }
1653
1654 #[test]
1655 fn query_principal() {
1656 let schema = schema();
1657 let policies = policy_set();
1658
1659 let request = PrincipalQueryRequest::new(
1660 "Subscriber".parse().unwrap(),
1661 r#"Action::"watch""#.parse().unwrap(),
1662 r#"Movie::"The Godparent""#.parse().unwrap(),
1663 Context::from_pairs([(
1664 "now".into(),
1665 RestrictedExpression::new_record([
1666 (
1667 "datetime".into(),
1668 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1669 ),
1670 (
1671 "localTimeOffset".into(),
1672 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1673 ),
1674 ])
1675 .unwrap(),
1676 )])
1677 .unwrap(),
1678 &schema,
1679 )
1680 .unwrap();
1681
1682 let subscribers = policies
1683 .query_principal(&request, &entities(), &schema)
1684 .unwrap()
1685 .sorted()
1686 .collect_vec();
1687 assert_eq!(
1688 subscribers,
1689 &[
1690 EntityUid::from_str(r#"Subscriber::"Alice""#).unwrap(),
1691 EntityUid::from_str(r#"Subscriber::"Charlie""#).unwrap(),
1692 ]
1693 );
1694 }
1695
1696 #[test]
1697 fn query_action_alice() {
1698 let schema = schema();
1699 let request = ActionQueryRequest::new(
1700 PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1701 PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
1702 None,
1703 schema.clone(),
1704 )
1705 .unwrap();
1706
1707 let policies = policy_set();
1708 let mut actions: Vec<_> = policies
1709 .query_action(
1710 &request,
1711 &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1712 )
1713 .unwrap()
1714 .collect();
1715 actions.sort_by_key(|(a, _)| *a);
1716 assert_eq!(
1717 actions,
1718 vec![
1719 (&r#"Action::"buy""#.parse().unwrap(), None),
1720 (&r#"Action::"rent""#.parse().unwrap(), None),
1721 (
1722 &r#"Action::"watch""#.parse().unwrap(),
1723 Some(Decision::Allow)
1724 ),
1725 ]
1726 );
1727 }
1728
1729 #[test]
1730 fn query_action_bob_free() {
1731 let schema = schema();
1732 let request = ActionQueryRequest::new(
1733 PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
1734 PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
1735 None,
1736 schema.clone(),
1737 )
1738 .unwrap();
1739
1740 let policies = policy_set();
1741 let actions: Vec<_> = policies
1742 .query_action(
1743 &request,
1744 &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1745 )
1746 .unwrap()
1747 .collect();
1748 assert_eq!(
1749 actions,
1750 vec![(
1751 &r#"Action::"watch""#.parse().unwrap(),
1752 Some(Decision::Allow)
1753 ),]
1754 );
1755 }
1756
1757 #[test]
1758 fn query_action_bob_not_free() {
1759 let schema = schema();
1760 let request = ActionQueryRequest::new(
1761 PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
1762 PartialEntityUid::from_concrete(r#"Movie::"The Gleaming""#.parse().unwrap()),
1763 None,
1764 schema.clone(),
1765 )
1766 .unwrap();
1767
1768 let policies = policy_set();
1769 let actions: Vec<_> = policies
1770 .query_action(
1771 &request,
1772 &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1773 )
1774 .unwrap()
1775 .collect();
1776 assert_eq!(actions, vec![]);
1777 }
1778 }
1779
1780 mod github {
1781 use std::{
1782 collections::{HashMap, HashSet},
1783 str::FromStr,
1784 };
1785
1786 use cedar_policy_core::tpe::err::TpeError;
1787 use cedar_policy_core::{authorizer::Decision, batched_evaluator::err::BatchedEvalError};
1788 use cool_asserts::assert_matches;
1789 use itertools::Itertools;
1790 use similar_asserts::assert_eq;
1791
1792 use crate::{
1793 ActionQueryRequest, Context, Entities, EntityUid, PartialEntities, PartialEntityUid,
1794 PolicySet, PrincipalQueryRequest, Request, ResourceQueryRequest, RestrictedExpression,
1795 Schema, TestEntityLoader,
1796 };
1797
1798 #[track_caller]
1799 fn schema() -> Schema {
1800 Schema::from_str(
1801 r#"
1802 entity Team, UserGroup in [UserGroup];
1803entity Issue = {
1804 "repo": Repository,
1805 "reporter": User,
1806};
1807entity Org = {
1808 "members": UserGroup,
1809 "owners": UserGroup,
1810};
1811entity Repository = {
1812 "admins": UserGroup,
1813 "maintainers": UserGroup,
1814 "readers": UserGroup,
1815 "triagers": UserGroup,
1816 "writers": UserGroup,
1817};
1818entity User in [UserGroup, Team];
1819
1820action push, pull, fork appliesTo {
1821 principal: [User],
1822 resource: [Repository]
1823};
1824action assign_issue, delete_issue, edit_issue appliesTo {
1825 principal: [User],
1826 resource: [Issue]
1827};
1828action add_reader, add_writer, add_maintainer, add_admin, add_triager appliesTo {
1829 principal: [User],
1830 resource: [Repository]
1831};
1832 "#,
1833 )
1834 .unwrap()
1835 }
1836
1837 fn policy_set() -> PolicySet {
1838 PolicySet::from_str(
1839 r#"
1840 //Actions for readers
1841permit (
1842 principal,
1843 action == Action::"pull",
1844 resource
1845)
1846when { principal in resource.readers };
1847
1848permit (
1849 principal,
1850 action == Action::"fork",
1851 resource
1852)
1853when { principal in resource.readers };
1854
1855permit (
1856 principal,
1857 action == Action::"delete_issue",
1858 resource
1859)
1860when { principal in resource.repo.readers && principal == resource.reporter };
1861
1862permit (
1863 principal,
1864 action == Action::"edit_issue",
1865 resource
1866)
1867when { principal in resource.repo.readers && principal == resource.reporter };
1868
1869//Actions for triagers
1870permit (
1871 principal,
1872 action == Action::"assign_issue",
1873 resource
1874)
1875when { principal in resource.repo.triagers };
1876
1877//Actions for writers
1878permit (
1879 principal,
1880 action == Action::"push",
1881 resource
1882)
1883when { principal in resource.writers };
1884
1885permit (
1886 principal,
1887 action == Action::"edit_issue",
1888 resource
1889)
1890when { principal in resource.repo.writers };
1891
1892//Actions for maintainers
1893permit (
1894 principal,
1895 action == Action::"delete_issue",
1896 resource
1897)
1898when { principal in resource.repo.maintainers };
1899
1900//Actions for admins
1901permit (
1902 principal,
1903 action in
1904 [Action::"add_reader",
1905 Action::"add_triager",
1906 Action::"add_writer",
1907 Action::"add_maintainer",
1908 Action::"add_admin"],
1909 resource
1910)
1911when { principal in resource.admins };
1912//We use the same permissions for org owners, and rely on placing them in the admins group for every repository in the org
1913//The other option is to duplicate all policies for the org base permissions (with a separate heirarchy for each org)
1914"#,
1915 )
1916 .unwrap()
1917 }
1918
1919 #[track_caller]
1920 fn entities() -> Entities {
1921 Entities::from_json_value(serde_json::json!(
1922
1923 [
1924 {
1925 "uid": { "__entity": { "type": "User", "id": "alice"} },
1926 "attrs": {},
1927 "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} } ]
1928 },
1929 {
1930 "uid": { "__entity": { "type": "User", "id": "jane"} },
1931 "attrs": {},
1932 "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} }, { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} }]
1933 },
1934 {
1935 "uid": { "__entity": { "type": "User", "id": "bob"} },
1936 "attrs": {},
1937 "parents": []
1938 },
1939 {
1940 "uid": { "__entity": { "type": "Repository", "id": "common_knowledge"} },
1941 "attrs": {
1942 "readers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
1943 "triagers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
1944 "writers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
1945 "maintainers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
1946 "admins" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} }
1947 },
1948 "parents": []
1949 },
1950 {
1951 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
1952 "attrs": {
1953 },
1954 "parents": [ ]
1955 },
1956 {
1957 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
1958 "attrs": {
1959 },
1960 "parents": [ { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} } ]
1961 },
1962 {
1963 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
1964 "attrs": {
1965 },
1966 "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"}} ]
1967 },
1968 {
1969 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
1970 "attrs": {
1971 },
1972 "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_writers"}} ]
1973 },
1974 {
1975 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} },
1976 "attrs": {
1977 },
1978 "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"}} ]
1979 },
1980 {
1981 "uid": { "__entity": { "type": "Repository", "id": "secret"} },
1982 "attrs": {
1983 "readers" : { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
1984 "triagers" : { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
1985 "writers" : { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
1986 "maintainers" : { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
1987 "admins" : { "__entity": { "type": "UserGroup", "id": "secret_admins"} }
1988 },
1989 "parents": []
1990 },
1991 {
1992 "uid": { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
1993 "attrs": {
1994 },
1995 "parents": [ ]
1996 },
1997 {
1998 "uid": { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
1999 "attrs": {
2000 },
2001 "parents": [ { "__entity": { "type": "UserGroup", "id": "secret_readers"} } ]
2002 },
2003 {
2004 "uid": { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
2005 "attrs": {
2006 },
2007 "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_triagers"}} ]
2008 },
2009 {
2010 "uid": { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
2011 "attrs": {
2012 },
2013 "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_writers"}} ]
2014 },
2015 {
2016 "uid": { "__entity": { "type": "UserGroup", "id": "secret_admins"} },
2017 "attrs": {
2018 },
2019 "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_maintainers"}} ]
2020 },
2021 {
2022 "uid": { "__entity": { "type": "Repository", "id": "uncommon_knowledge"} },
2023 "attrs": {
2024 "readers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
2025 "triagers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
2026 "writers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
2027 "maintainers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
2028 "admins" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} }
2029 },
2030 "parents": []
2031 },
2032 {
2033 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
2034 "attrs": {
2035 },
2036 "parents": [ ]
2037 },
2038 {
2039 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
2040 "attrs": {
2041 },
2042 "parents": [ { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} } ]
2043 },
2044 {
2045 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
2046 "attrs": {
2047 },
2048 "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"}} ]
2049 },
2050 {
2051 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
2052 "attrs": {
2053 },
2054 "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"}} ]
2055 },
2056 {
2057 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} },
2058 "attrs": {
2059 },
2060 "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"}} ]
2061 },
2062 {
2063 "uid": { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} },
2064 "attrs": {},
2065 "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} }, { "__entity": { "type": "UserGroup", "id": "secret_readers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} }]
2066 },
2067]
2068 ), Some(&schema())).unwrap()
2069 }
2070
2071 #[test]
2072 fn query_resource() {
2073 let schema = schema();
2074 let request = ResourceQueryRequest::new(
2075 r#"User::"jane""#.parse().unwrap(),
2076 r#"Action::"push""#.parse().unwrap(),
2077 "Repository".parse().unwrap(),
2078 Context::empty(),
2079 &schema,
2080 )
2081 .unwrap();
2082 let policies = policy_set();
2083 assert_matches!(&policies.query_resource(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
2084 assert_eq!(uid, &r#"Repository::"common_knowledge""#.parse().unwrap());
2085 });
2086 }
2087
2088 #[test]
2089 fn query_principal() {
2090 let schema = schema();
2091 let request = PrincipalQueryRequest::new(
2092 r"User".parse().unwrap(),
2093 r#"Action::"pull""#.parse().unwrap(),
2094 r#"Repository::"secret""#.parse().unwrap(),
2095 Context::empty(),
2096 &schema,
2097 )
2098 .unwrap();
2099 let policies = policy_set();
2100 assert_matches!(&policies.query_principal(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
2101 assert_eq!(uid, &r#"User::"jane""#.parse().unwrap());
2102 });
2103 }
2104
2105 #[test]
2106 fn query_action() {
2107 let schema = schema();
2108 let request = ActionQueryRequest::new(
2109 PartialEntityUid::from_concrete(r#"User::"jane""#.parse().unwrap()),
2110 PartialEntityUid::from_concrete(r#"Repository::"secret""#.parse().unwrap()),
2111 None,
2112 schema.clone(),
2113 )
2114 .unwrap();
2115
2116 let policies = policy_set();
2117 let mut actions: Vec<_> = policies
2118 .query_action(
2119 &request,
2120 &PartialEntities::from_concrete(entities(), &schema).unwrap(),
2121 )
2122 .unwrap()
2123 .collect();
2124 actions.sort_by_key(|(a, _)| *a);
2125 assert_eq!(
2126 actions,
2127 vec![
2128 (&r#"Action::"fork""#.parse().unwrap(), Some(Decision::Allow)),
2129 (&r#"Action::"pull""#.parse().unwrap(), Some(Decision::Allow)),
2130 ]
2131 );
2132 }
2133
2134 #[test]
2135 fn test_is_authorized_vs_is_authorized_batched() {
2136 use crate::{Authorizer, Request};
2137
2138 let schema = schema();
2139 let policies = policy_set();
2140 let entities = entities();
2141 let authorizer = Authorizer::new();
2142
2143 let test_requests = vec![
2145 Request::new(
2147 r#"User::"alice""#.parse().unwrap(),
2148 r#"Action::"push""#.parse().unwrap(),
2149 r#"Repository::"common_knowledge""#.parse().unwrap(),
2150 Context::empty(),
2151 Some(&schema),
2152 )
2153 .unwrap(),
2154 Request::new(
2156 r#"User::"jane""#.parse().unwrap(),
2157 r#"Action::"pull""#.parse().unwrap(),
2158 r#"Repository::"secret""#.parse().unwrap(),
2159 Context::empty(),
2160 Some(&schema),
2161 )
2162 .unwrap(),
2163 Request::new(
2165 r#"User::"bob""#.parse().unwrap(),
2166 r#"Action::"push""#.parse().unwrap(),
2167 r#"Repository::"common_knowledge""#.parse().unwrap(),
2168 Context::empty(),
2169 Some(&schema),
2170 )
2171 .unwrap(),
2172 Request::new(
2174 r#"User::"alice""#.parse().unwrap(),
2175 r#"Action::"fork""#.parse().unwrap(),
2176 r#"Repository::"common_knowledge""#.parse().unwrap(),
2177 Context::empty(),
2178 Some(&schema),
2179 )
2180 .unwrap(),
2181 ];
2182
2183 for (i, request) in test_requests.iter().enumerate() {
2185 let standard_response = authorizer.is_authorized(request, &policies, &entities);
2187
2188 let mut loader = TestEntityLoader::new(&entities);
2190 let batched_decision = policies
2191 .is_authorized_batched(request, &schema, &mut loader, u32::MAX)
2192 .unwrap();
2193
2194 let standard_decision = standard_response.decision();
2196
2197 assert_eq!(
2198 standard_decision,
2199 batched_decision,
2200 "Request {}: is_authorized returned {:?} but is_authorized_batched returned {:?}",
2201 i + 1,
2202 standard_decision,
2203 batched_decision
2204 );
2205 }
2206 }
2207
2208 #[test]
2209 fn test_batched_evaluation_error_validation() {
2210 let schema = schema();
2211 let policies = PolicySet::from_str(
2212 r#"permit(principal, action, resource) when { principal.nonexistent_attr == "value" };"#
2213 ).unwrap();
2214
2215 let request = Request::new(
2216 EntityUid::from_str("User::\"alice\"").unwrap(),
2217 EntityUid::from_str("Action::\"push\"").unwrap(),
2218 EntityUid::from_str("Repository::\"repo\"").unwrap(),
2219 Context::empty(),
2220 Some(&schema),
2221 )
2222 .unwrap();
2223
2224 let entities = entities();
2225 let mut loader = TestEntityLoader::new(&entities);
2226 let result = policies.is_authorized_batched(&request, &schema, &mut loader, 10);
2227
2228 assert!(matches!(
2229 result,
2230 Err(BatchedEvalError::TPE(TpeError::Validation(_)))
2231 ));
2232 }
2233
2234 #[test]
2235 #[cfg(feature = "partial-eval")]
2236 fn test_batched_evaluation_error_partial_request() {
2237 let context_with_unknown = Context::from_pairs([(
2238 "key".to_string(),
2239 RestrictedExpression::new_unknown("test_unknown"),
2240 )])
2241 .unwrap();
2242
2243 let request = Request::new(
2244 EntityUid::from_str("User::\"alice\"").unwrap(),
2245 EntityUid::from_str("Action::\"view\"").unwrap(),
2246 EntityUid::from_str("Resource::\"doc\"").unwrap(),
2247 context_with_unknown,
2248 None,
2249 )
2250 .unwrap();
2251 let schema = schema();
2252
2253 let pset = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2254 let entities = Entities::empty();
2255 let mut loader = TestEntityLoader::new(&entities);
2256 let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2257
2258 assert_matches!(result, Err(BatchedEvalError::PartialRequest(_)));
2259 }
2260
2261 #[test]
2262 fn test_batched_evaluation_error_invalid_entity() {
2263 struct InvalidEntityLoader;
2265 impl crate::EntityLoader for InvalidEntityLoader {
2266 fn load_entities(
2267 &mut self,
2268 _uids: &HashSet<EntityUid>,
2269 ) -> HashMap<EntityUid, Option<crate::Entity>> {
2270 let mut result = HashMap::new();
2271 let uid = EntityUid::from_strs("Org", "myorg");
2272 let entity = crate::Entity::new(
2273 uid.clone(),
2274 [
2275 (
2276 "members".to_string(),
2277 RestrictedExpression::new_string("not_a_usergroup".to_string()),
2278 ),
2279 (
2280 "owners".to_string(),
2281 RestrictedExpression::new_entity_uid(EntityUid::from_strs(
2282 "UserGroup",
2283 "2",
2284 )),
2285 ),
2286 ]
2287 .into(),
2288 HashSet::new(),
2289 )
2290 .unwrap();
2291 result.insert(uid, Some(entity));
2292 result
2293 }
2294 }
2295
2296 let schema = schema();
2297 let pset = PolicySet::from_str(
2298 "permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
2299 )
2300 .unwrap();
2301
2302 let request = Request::new(
2303 r#"User::"alice""#.parse().unwrap(),
2304 r#"Action::"push""#.parse().unwrap(),
2305 r#"Repository::"common_knowledge""#.parse().unwrap(),
2306 Context::empty(),
2307 Some(&schema),
2308 )
2309 .unwrap();
2310
2311 let mut loader = InvalidEntityLoader;
2312 let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2313
2314 assert_matches!(result, Err(BatchedEvalError::Entities(_)));
2315 }
2316
2317 #[test]
2318 #[cfg(feature = "partial-eval")]
2319 fn test_batched_evaluation_error_partial_entity() {
2320 struct PartialEntityLoader;
2322 impl crate::EntityLoader for PartialEntityLoader {
2323 fn load_entities(
2324 &mut self,
2325 _uids: &HashSet<EntityUid>,
2326 ) -> HashMap<EntityUid, Option<crate::Entity>> {
2327 let mut result = HashMap::new();
2328 let uid = EntityUid::from_strs("Org", "myorg");
2329 let entity = crate::Entity::new(
2330 uid.clone(),
2331 [
2332 (
2333 "members".to_string(),
2334 RestrictedExpression::new_unknown("partial_members"),
2335 ),
2336 (
2337 "owners".to_string(),
2338 RestrictedExpression::new_entity_uid(EntityUid::from_strs(
2339 "UserGroup",
2340 "2",
2341 )),
2342 ),
2343 ]
2344 .into(),
2345 HashSet::new(),
2346 )
2347 .unwrap();
2348 result.insert(uid, Some(entity));
2349 result
2350 }
2351 }
2352
2353 let schema = schema();
2354 let pset = PolicySet::from_str(
2355 "permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
2356 )
2357 .unwrap();
2358
2359 let request = Request::new(
2360 r#"User::"alice""#.parse().unwrap(),
2361 r#"Action::"push""#.parse().unwrap(),
2362 r#"Repository::"common_knowledge""#.parse().unwrap(),
2363 Context::empty(),
2364 Some(&schema),
2365 )
2366 .unwrap();
2367
2368 let mut loader = PartialEntityLoader;
2369 let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2370
2371 assert_matches!(result, Err(BatchedEvalError::PartialValueToValue(_)));
2372 }
2373
2374 #[test]
2375 fn test_batched_evaluation_error_insufficient_iters() {
2376 let schema = schema();
2377 let policies = policy_set();
2378 let entities = entities();
2379
2380 let request = Request::new(
2381 r#"User::"alice""#.parse().unwrap(),
2382 r#"Action::"push""#.parse().unwrap(),
2383 r#"Repository::"common_knowledge""#.parse().unwrap(),
2384 Context::empty(),
2385 Some(&schema),
2386 )
2387 .unwrap();
2388
2389 let mut loader = TestEntityLoader::new(&entities);
2390 let result = policies.is_authorized_batched(&request, &schema, &mut loader, 0);
2391
2392 assert_matches!(result, Err(BatchedEvalError::InsufficientIterations(_)));
2393 }
2394 }
2395
2396 mod trivial {
2397 use cedar_policy_core::authorizer::Decision;
2398 use itertools::Itertools;
2399
2400 use crate::{
2401 Context, Entities, PartialEntities, PartialEntityUid, PartialRequest, PolicyId,
2402 PolicySet, PrincipalQueryRequest, ResourceQueryRequest, Schema,
2403 };
2404 use std::{i64, str::FromStr};
2405
2406 fn schema() -> Schema {
2407 Schema::from_str("entity P, R; action A appliesTo { principal: P, resource: R };")
2408 .unwrap()
2409 }
2410
2411 fn entities() -> Entities {
2412 Entities::from_json_value(
2413 serde_json::json!([
2414 { "uid": { "__entity": { "type": "P", "id": ""} }, "attrs": {}, "parents": [] },
2415 { "uid": { "__entity": { "type": "R", "id": ""} }, "attrs": {}, "parents": [] },
2416 ]),
2417 None,
2418 )
2419 .unwrap()
2420 }
2421
2422 #[test]
2423 fn trivial_permit_tpe() {
2424 let schema = schema();
2425 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2426 let req = PartialRequest::new(
2427 PartialEntityUid::new("P".parse().unwrap(), None),
2428 r#"Action::"A""#.parse().unwrap(),
2429 PartialEntityUid::new("R".parse().unwrap(), None),
2430 None,
2431 &schema,
2432 )
2433 .unwrap();
2434 let response = PolicySet::from_str(r"permit(principal, action, resource);")
2435 .unwrap()
2436 .tpe(&req, &partial_entities, &schema)
2437 .unwrap();
2438 assert_eq!(response.decision(), Some(Decision::Allow));
2439 assert_eq!(
2440 response.reason().unwrap().collect::<Vec<_>>(),
2441 vec![&PolicyId::new("policy0")]
2442 );
2443 }
2444
2445 #[test]
2446 fn trivial_permit_query_principal() {
2447 let schema = schema();
2448 let entities = entities();
2449 let req = PrincipalQueryRequest::new(
2450 "P".parse().unwrap(),
2451 r#"Action::"A""#.parse().unwrap(),
2452 r#"R::"""#.parse().unwrap(),
2453 Context::empty(),
2454 &schema,
2455 )
2456 .unwrap();
2457
2458 let principals = PolicySet::from_str(r#"permit(principal, action, resource);"#)
2459 .unwrap()
2460 .query_principal(&req, &entities, &schema)
2461 .unwrap()
2462 .collect_vec();
2463 assert_eq!(&principals, &[r#"P::"""#.parse().unwrap()]);
2464 }
2465
2466 #[test]
2467 fn trivial_permit_query_resource() {
2468 let schema = schema();
2469 let entities = entities();
2470 let req = ResourceQueryRequest::new(
2471 r#"P::"""#.parse().unwrap(),
2472 r#"Action::"A""#.parse().unwrap(),
2473 "R".parse().unwrap(),
2474 Context::empty(),
2475 &schema,
2476 )
2477 .unwrap();
2478
2479 let resources = PolicySet::from_str(r#"permit(principal, action, resource);"#)
2480 .unwrap()
2481 .query_resource(&req, &entities, &schema)
2482 .unwrap()
2483 .collect_vec();
2484 assert_eq!(&resources, &[r#"R::"""#.parse().unwrap()]);
2485 }
2486
2487 #[test]
2488 fn trivial_forbid_tpe() {
2489 let schema = schema();
2490 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2491 let req = PartialRequest::new(
2492 PartialEntityUid::new("P".parse().unwrap(), None),
2493 r#"Action::"A""#.parse().unwrap(),
2494 PartialEntityUid::new("R".parse().unwrap(), None),
2495 None,
2496 &schema,
2497 )
2498 .unwrap();
2499 let response = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2500 .unwrap()
2501 .tpe(&req, &partial_entities, &schema)
2502 .unwrap();
2503 assert_eq!(response.decision(), Some(Decision::Deny));
2504 assert_eq!(
2505 response.reason().unwrap().collect::<Vec<_>>(),
2506 vec![&PolicyId::new("policy0")]
2507 );
2508 assert_eq!(
2509 response.true_forbids().collect::<Vec<_>>(),
2510 vec![&PolicyId::new("policy0")]
2511 );
2512 }
2513
2514 #[test]
2515 fn trivial_forbid_query_principal() {
2516 let schema = schema();
2517 let entities = entities();
2518 let req = PrincipalQueryRequest::new(
2519 "P".parse().unwrap(),
2520 r#"Action::"A""#.parse().unwrap(),
2521 r#"R::"""#.parse().unwrap(),
2522 Context::empty(),
2523 &schema,
2524 )
2525 .unwrap();
2526
2527 let principals = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2528 .unwrap()
2529 .query_principal(&req, &entities, &schema)
2530 .unwrap()
2531 .collect_vec();
2532 assert_eq!(&principals, &[]);
2533 }
2534
2535 #[test]
2536 fn trivial_forbid_query_resource() {
2537 let schema = schema();
2538 let entities = entities();
2539 let req = ResourceQueryRequest::new(
2540 r#"P::"""#.parse().unwrap(),
2541 r#"Action::"A""#.parse().unwrap(),
2542 "R".parse().unwrap(),
2543 Context::empty(),
2544 &schema,
2545 )
2546 .unwrap();
2547
2548 let resources = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2549 .unwrap()
2550 .query_resource(&req, &entities, &schema)
2551 .unwrap()
2552 .collect_vec();
2553 assert_eq!(&resources, &[]);
2554 }
2555
2556 #[test]
2557 fn error_tpe() {
2558 let schema = schema();
2559 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2560 let req = PartialRequest::new(
2561 PartialEntityUid::new("P".parse().unwrap(), None),
2562 r#"Action::"A""#.parse().unwrap(),
2563 PartialEntityUid::new("R".parse().unwrap(), None),
2564 None,
2565 &schema,
2566 )
2567 .unwrap();
2568 let response = PolicySet::from_str(&format!(
2569 r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2570 i64::MAX
2571 ))
2572 .unwrap()
2573 .tpe(&req, &partial_entities, &schema)
2574 .unwrap();
2575 assert_eq!(response.decision(), Some(Decision::Deny));
2576 assert_eq!(
2577 response.reason().unwrap().collect::<Vec<_>>(),
2578 Vec::<&PolicyId>::new()
2579 );
2580 }
2581
2582 #[test]
2583 fn error_query_principal() {
2584 let schema = schema();
2585 let entities = entities();
2586 let req = PrincipalQueryRequest::new(
2587 "P".parse().unwrap(),
2588 r#"Action::"A""#.parse().unwrap(),
2589 r#"R::"""#.parse().unwrap(),
2590 Context::empty(),
2591 &schema,
2592 )
2593 .unwrap();
2594
2595 let principals = PolicySet::from_str(&format!(
2596 r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2597 i64::MAX
2598 ))
2599 .unwrap()
2600 .query_principal(&req, &entities, &schema)
2601 .unwrap()
2602 .collect_vec();
2603 assert_eq!(&principals, &[]);
2604 }
2605
2606 #[test]
2607 fn error_query_resource() {
2608 let schema = schema();
2609 let entities = entities();
2610 let req = ResourceQueryRequest::new(
2611 r#"P::"""#.parse().unwrap(),
2612 r#"Action::"A""#.parse().unwrap(),
2613 "R".parse().unwrap(),
2614 Context::empty(),
2615 &schema,
2616 )
2617 .unwrap();
2618
2619 let resources = PolicySet::from_str(&format!(
2620 r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2621 i64::MAX
2622 ))
2623 .unwrap()
2624 .query_resource(&req, &entities, &schema)
2625 .unwrap()
2626 .collect_vec();
2627 assert_eq!(&resources, &[]);
2628 }
2629
2630 #[test]
2631 fn empty_tpe() {
2632 let schema = schema();
2633 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2634 let req = PartialRequest::new(
2635 PartialEntityUid::new("P".parse().unwrap(), None),
2636 r#"Action::"A""#.parse().unwrap(),
2637 PartialEntityUid::new("R".parse().unwrap(), None),
2638 None,
2639 &schema,
2640 )
2641 .unwrap();
2642 let response = PolicySet::from_str(r#""#)
2643 .unwrap()
2644 .tpe(&req, &partial_entities, &schema)
2645 .unwrap();
2646 assert_eq!(response.decision(), Some(Decision::Deny));
2647 assert_eq!(
2648 response.reason().unwrap().collect::<Vec<_>>(),
2649 Vec::<&PolicyId>::new()
2650 );
2651 }
2652
2653 #[test]
2654 fn empty_query_principal() {
2655 let schema = schema();
2656 let entities = entities();
2657 let req = PrincipalQueryRequest::new(
2658 "P".parse().unwrap(),
2659 r#"Action::"A""#.parse().unwrap(),
2660 r#"R::"""#.parse().unwrap(),
2661 Context::empty(),
2662 &schema,
2663 )
2664 .unwrap();
2665
2666 let principals = PolicySet::from_str(r#""#)
2667 .unwrap()
2668 .query_principal(&req, &entities, &schema)
2669 .unwrap()
2670 .collect_vec();
2671 assert_eq!(&principals, &[]);
2672 }
2673
2674 #[test]
2675 fn empty_query_resource() {
2676 let schema = schema();
2677 let entities = entities();
2678 let req = ResourceQueryRequest::new(
2679 r#"P::"""#.parse().unwrap(),
2680 r#"Action::"A""#.parse().unwrap(),
2681 "R".parse().unwrap(),
2682 Context::empty(),
2683 &schema,
2684 )
2685 .unwrap();
2686
2687 let resources = PolicySet::from_str(r#""#)
2688 .unwrap()
2689 .query_resource(&req, &entities, &schema)
2690 .unwrap()
2691 .collect_vec();
2692 assert_eq!(&resources, &[]);
2693 }
2694 }
2695
2696 mod response_iterators {
2697 use std::{i64, str::FromStr};
2698
2699 use cedar_policy_core::authorizer::Decision;
2700
2701 use crate::{
2702 PartialEntities, PartialEntityUid, PartialRequest, PolicyId, PolicySet, Schema,
2703 };
2704
2705 #[test]
2706 fn all_policy_categories() {
2707 let schema = Schema::from_str(
2708 "entity P, R; action A appliesTo { principal: P, resource: R, context: { flag: Bool } };",
2709 )
2710 .unwrap();
2711 let req = PartialRequest::new(
2712 PartialEntityUid::new("P".parse().unwrap(), None),
2713 r#"Action::"A""#.parse().unwrap(),
2714 PartialEntityUid::new("R".parse().unwrap(), None),
2715 None,
2716 &schema,
2717 )
2718 .unwrap();
2719
2720 let policies = PolicySet::from_str(&format!(
2721 r#"
2722 permit(principal, action, resource);
2723 permit(principal, action, resource) when {{ false }};
2724 permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};
2725 permit(principal, action, resource) when {{ context.flag }};
2726 forbid(principal, action, resource);
2727 forbid(principal, action, resource) when {{ false }};
2728 forbid(principal, action, resource) when {{ ({} + 1) == 0 || true }};
2729 forbid(principal, action, resource) when {{ context.flag }};
2730 "#,
2731 i64::MAX,
2732 i64::MAX
2733 ))
2734 .unwrap();
2735
2736 let entities = PartialEntities::empty();
2737 let response = policies.tpe(&req, &entities, &schema).unwrap();
2738
2739 assert_eq!(response.decision(), Some(Decision::Deny));
2740 assert_eq!(
2741 response.reason().unwrap().collect::<Vec<_>>(),
2742 vec![&PolicyId::new("policy4")]
2743 );
2744
2745 assert_eq!(
2746 response.true_permits().collect::<Vec<_>>(),
2747 vec![&PolicyId::new("policy0")]
2748 );
2749 assert_eq!(
2750 response.false_permits().collect::<Vec<_>>(),
2751 vec![&PolicyId::new("policy1")]
2752 );
2753 assert_eq!(
2754 response.error_permits().collect::<Vec<_>>(),
2755 vec![&PolicyId::new("policy2")]
2756 );
2757 assert_eq!(
2758 response.residual_permits().collect::<Vec<_>>(),
2759 vec![&PolicyId::new("policy3")]
2760 );
2761 assert_eq!(
2762 response.true_forbids().collect::<Vec<_>>(),
2763 vec![&PolicyId::new("policy4")]
2764 );
2765 assert_eq!(
2766 response.false_forbids().collect::<Vec<_>>(),
2767 vec![&PolicyId::new("policy5")]
2768 );
2769 assert_eq!(
2770 response.error_forbids().collect::<Vec<_>>(),
2771 vec![&PolicyId::new("policy6")]
2772 );
2773 assert_eq!(
2774 response.residual_forbids().collect::<Vec<_>>(),
2775 vec![&PolicyId::new("policy7")]
2776 );
2777 }
2778 }
2779
2780 mod query_action {
2781 use cedar_policy_core::authorizer::Decision;
2782
2783 use crate::{
2784 ActionQueryRequest, Context, PartialEntities, PartialEntityUid, PolicySet, Schema,
2785 };
2786 use similar_asserts::assert_eq;
2787 use std::str::FromStr;
2788
2789 #[test]
2790 fn test() {
2791 let policies = PolicySet::from_str(
2792 r#"
2793 // Edit might be alowed, depending on context
2794 permit(principal, action == Action::"edit", resource)
2795 when {
2796 context.ip.isInRange(resource.allowed_edit_range)
2797 };
2798
2799 // We pass a concrete resource, so we know this will be allowed
2800 permit(principal, action == Action::"view", resource)
2801 when {
2802 resource.public
2803 };
2804
2805 // never allowed for any request
2806 forbid(principal, action == Action::"delete", resource);
2807
2808 // allowed for this action, but it doesn't apply to the request types
2809 permit(principal, action == Action::"not_on_photo", resource);
2810 "#,
2811 )
2812 .unwrap();
2813 let schema = Schema::from_str(
2814 "
2815 entity User, Other;
2816 entity Photo {
2817 public: Bool,
2818 allowed_edit_range: ipaddr,
2819 };
2820 action view, edit, delete appliesTo {
2821 principal: User,
2822 resource: Photo,
2823 context: {
2824 ip: ipaddr,
2825 }
2826 };
2827 action not_on_photo appliesTo {
2828 principal: User,
2829 resource: Other
2830 };
2831 ",
2832 )
2833 .unwrap();
2834 let entities = PartialEntities::from_json_value(
2835 serde_json::json!([
2836 {
2837 "uid": { "__entity": { "type": "Photo", "id": "vacation.jpg"} },
2838 "attrs": {
2839 "public": true,
2840 "allowed_edit_range": "192.0.2.0/24"
2841 },
2842 "parents": []
2843 },
2844 ]),
2845 &schema,
2846 )
2847 .unwrap();
2848
2849 let request = ActionQueryRequest::new(
2850 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2851 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2852 None,
2853 schema,
2854 )
2855 .unwrap();
2856
2857 let mut actions: Vec<_> = policies
2858 .query_action(&request, &entities)
2859 .unwrap()
2860 .collect();
2861 actions.sort_by_key(|(a, _)| *a);
2862 assert_eq!(
2863 actions,
2864 vec![
2865 (&r#"Action::"edit""#.parse().unwrap(), None),
2866 (&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow)),
2867 ]
2868 )
2869 }
2870
2871 #[test]
2872 fn permitted_action() {
2873 let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2874 let schema = Schema::from_str(
2875 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2876 )
2877 .unwrap();
2878 let entities = PartialEntities::empty();
2879
2880 let request = ActionQueryRequest::new(
2881 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2882 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2883 None,
2884 schema,
2885 )
2886 .unwrap();
2887
2888 let actions: Vec<_> = policies
2889 .query_action(&request, &entities)
2890 .unwrap()
2891 .collect();
2892 assert_eq!(
2893 actions,
2894 vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
2895 );
2896 }
2897
2898 #[test]
2899 fn maybe_permitted_action() {
2900 let policies = PolicySet::from_str(
2901 "permit(principal, action, resource) when { context.should_allow };",
2902 )
2903 .unwrap();
2904 let schema = Schema::from_str(
2905 "entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {should_allow: Bool}};",
2906 )
2907 .unwrap();
2908 let entities = PartialEntities::empty();
2909
2910 let request = ActionQueryRequest::new(
2911 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2912 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2913 None,
2914 schema,
2915 )
2916 .unwrap();
2917
2918 let actions: Vec<_> = policies
2919 .query_action(&request, &entities)
2920 .unwrap()
2921 .collect();
2922 assert_eq!(actions, vec![(&r#"Action::"view""#.parse().unwrap(), None)]);
2923 }
2924
2925 #[test]
2926 fn forbidden_action() {
2927 let policies = PolicySet::from_str("forbid(principal, action, resource);").unwrap();
2928 let schema = Schema::from_str(
2929 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2930 )
2931 .unwrap();
2932 let entities = PartialEntities::empty();
2933
2934 let request = ActionQueryRequest::new(
2935 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2936 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2937 None,
2938 schema,
2939 )
2940 .unwrap();
2941
2942 let actions: Vec<_> = policies
2943 .query_action(&request, &entities)
2944 .unwrap()
2945 .collect();
2946 assert_eq!(actions, Vec::new(),);
2947 }
2948
2949 #[test]
2950 fn invalid_permitted_action() {
2951 let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2952 let schema = Schema::from_str("entity User, Photo, Other; action view appliesTo { principal: User, resource: Other};").unwrap();
2953 let entities = PartialEntities::empty();
2954
2955 let request = ActionQueryRequest::new(
2956 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2957 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2958 None,
2959 schema,
2960 )
2961 .unwrap();
2962
2963 let actions: Vec<_> = policies
2964 .query_action(&request, &entities)
2965 .unwrap()
2966 .collect();
2967 assert_eq!(actions, Vec::new());
2968 }
2969
2970 #[test]
2971 fn invalid_context_permitted_action() {
2972 let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2973 let schema = Schema::from_str("entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {a: Long}};").unwrap();
2974 let entities = PartialEntities::empty();
2975
2976 let request = ActionQueryRequest::new(
2977 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2978 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2979 Some(Context::empty()),
2980 schema,
2981 )
2982 .unwrap();
2983
2984 let actions: Vec<_> = policies
2985 .query_action(&request, &entities)
2986 .unwrap()
2987 .collect();
2988 assert_eq!(actions, Vec::new());
2989 }
2990
2991 #[test]
2992 fn no_actions_in_schema() {
2993 let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2994 let schema = Schema::from_str("entity User, Photo;").unwrap();
2995 let entities = PartialEntities::empty();
2996
2997 let request = ActionQueryRequest::new(
2998 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2999 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
3000 None,
3001 schema,
3002 )
3003 .unwrap();
3004
3005 let actions: Vec<_> = policies
3006 .query_action(&request, &entities)
3007 .unwrap()
3008 .collect();
3009 assert_eq!(actions, Vec::new());
3010 }
3011
3012 #[test]
3013 fn permitted_action_error_permit() {
3014 let policies = PolicySet::from_str(&format!("permit(principal, action, resource);permit(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
3015 let schema = Schema::from_str(
3016 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
3017 )
3018 .unwrap();
3019 let entities = PartialEntities::empty();
3020
3021 let request = ActionQueryRequest::new(
3022 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3023 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
3024 None,
3025 schema,
3026 )
3027 .unwrap();
3028
3029 let actions: Vec<_> = policies
3030 .query_action(&request, &entities)
3031 .unwrap()
3032 .collect();
3033 assert_eq!(
3034 actions,
3035 vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
3036 );
3037 }
3038
3039 #[test]
3040 fn permitted_action_error_forbid() {
3041 let policies = PolicySet::from_str(&format!("permit(principal, action, resource);forbid(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
3042 let schema = Schema::from_str(
3043 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
3044 )
3045 .unwrap();
3046 let entities = PartialEntities::empty();
3047
3048 let request = ActionQueryRequest::new(
3049 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3050 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
3051 None,
3052 schema,
3053 )
3054 .unwrap();
3055
3056 let actions: Vec<_> = policies
3057 .query_action(&request, &entities)
3058 .unwrap()
3059 .collect();
3060 assert_eq!(
3061 actions,
3062 vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
3063 );
3064 }
3065
3066 #[test]
3067 fn forbidden_action_error_permit() {
3068 let policies = PolicySet::from_str(&format!(
3069 "permit(principal, action, resource) when {{ {} + 1 == 0 || true }};",
3070 i64::MAX
3071 ))
3072 .unwrap();
3073 let schema = Schema::from_str(
3074 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
3075 )
3076 .unwrap();
3077 let entities = PartialEntities::empty();
3078
3079 let request = ActionQueryRequest::new(
3080 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3081 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
3082 None,
3083 schema,
3084 )
3085 .unwrap();
3086
3087 let actions: Vec<_> = policies
3088 .query_action(&request, &entities)
3089 .unwrap()
3090 .collect();
3091 assert_eq!(actions, Vec::new(),);
3092 }
3093 }
3094
3095 #[test]
3100 fn residual_error_to_pst_and_json() {
3101 use cedar_policy_core::pst;
3102 use std::str::FromStr;
3103
3104 let (schema, _) = crate::Schema::from_cedarschema_str(
3105 r#"
3106 entity User = { name: String };
3107 entity Account = { name: String, assignedTo?: User };
3108 action RevealCredentials appliesTo {
3109 principal: [User],
3110 resource: [Account],
3111 context: { flag: Bool },
3112 };
3113 "#,
3114 )
3115 .unwrap();
3116
3117 let policies = crate::PolicySet::from_str(
3118 r#"
3119 permit(
3120 principal is User,
3121 action == Action::"RevealCredentials",
3122 resource is Account
3123 ) when {
3124 context.flag &&
3125 resource has assignedTo &&
3126 resource.assignedTo == principal
3127 };
3128 "#,
3129 )
3130 .unwrap();
3131
3132 let entities = crate::Entities::from_json_value(
3135 serde_json::json!([
3136 {
3137 "uid": { "type": "User", "id": "u1" },
3138 "attrs": { "name": "alice" },
3139 "parents": []
3140 },
3141 {
3142 "uid": { "type": "Account", "id": "a1" },
3143 "attrs": { "name": "shared" },
3144 "parents": []
3145 }
3146 ]),
3147 Some(&schema),
3148 )
3149 .unwrap();
3150
3151 let partial_entities = crate::PartialEntities::from_concrete(entities, &schema).unwrap();
3152
3153 let request = crate::PartialRequest::new(
3155 crate::PartialEntityUid::from_concrete(r#"User::"u1""#.parse().unwrap()),
3156 r#"Action::"RevealCredentials""#.parse().unwrap(),
3157 crate::PartialEntityUid::from_concrete(r#"Account::"a1""#.parse().unwrap()),
3158 None,
3159 &schema,
3160 )
3161 .unwrap();
3162
3163 let response = policies
3164 .tpe(&request, &partial_entities, &schema)
3165 .expect("tpe should succeed");
3166 let residual_policies: Vec<_> = response.residual_policies().collect();
3168 assert_eq!(
3169 residual_policies.len(),
3170 1,
3171 "decision={:?}, all residuals: {:?}",
3172 response.decision(),
3173 response
3174 .residual_policies()
3175 .map(|p| p.to_string())
3176 .collect::<Vec<_>>()
3177 );
3178
3179 let policy = &residual_policies[0];
3180
3181 let json_res = policy.to_json();
3183 assert!(json_res.is_ok());
3184 assert!(json_res.unwrap().to_string().contains(r#"{"error":[]}"#));
3185
3186 let pst_policy = policy.to_pst().expect("to_pst should succeed");
3188 let clauses = pst_policy.body().clauses();
3189 assert_eq!(clauses.len(), 1);
3190
3191 let expr = match &clauses[0] {
3192 pst::Clause::When(e) => e,
3193 pst::Clause::Unless(_) => panic!("expected when clause"),
3194 };
3195
3196 assert!(
3199 expr.has_error(),
3200 "residual expression should contain an error node"
3201 );
3202 }
3203
3204 mod template_links {
3205 use std::{collections::HashMap, str::FromStr};
3206
3207 use crate::{
3208 pst, Decision, EntityUid, PartialEntities, PartialEntityUid, PartialRequest, Policy,
3209 PolicyId, PolicySet, Schema, SlotId, Template,
3210 };
3211
3212 fn schema() -> Schema {
3213 Schema::from_str(
3214 "entity User { age: Long }; entity Photo; action view appliesTo { principal: User, resource: Photo};",
3215 )
3216 .unwrap()
3217 }
3218
3219 fn template_policy_set() -> PolicySet {
3220 let mut policies = PolicySet::new();
3221 let template = Template::parse(
3222 Some(PolicyId::new("t0").clone()),
3223 "permit(principal == ?principal, action, resource);",
3224 )
3225 .unwrap();
3226 policies.add_template(template).unwrap();
3227 let template = Template::parse(
3228 Some(PolicyId::new("t1").clone()),
3229 "permit(principal, action, resource == ?resource);",
3230 )
3231 .unwrap();
3232 policies.add_template(template).unwrap();
3233 policies
3234 }
3235
3236 fn partial_req() -> PartialRequest {
3237 PartialRequest::new(
3238 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
3239 r#"Action::"view""#.parse().unwrap(),
3240 PartialEntityUid::new("Photo".parse().unwrap(), None),
3241 None,
3242 &schema(),
3243 )
3244 .unwrap()
3245 }
3246
3247 #[test]
3248 fn concrete_allow() {
3249 let schema = schema();
3250 let mut policies = template_policy_set();
3251 policies
3252 .link(
3253 PolicyId::new("t0"),
3254 PolicyId::new("l"),
3255 HashMap::from([(
3256 SlotId::principal(),
3257 EntityUid::from_str(r#"User::"alice""#).unwrap(),
3258 )]),
3259 )
3260 .unwrap();
3261
3262 let request = partial_req();
3263 let es = PartialEntities::empty();
3264 let response = policies.tpe(&request, &es, &schema).unwrap();
3265
3266 assert_eq!(response.decision(), Some(Decision::Allow));
3267 assert_eq!(
3268 response.reason().unwrap().collect::<Vec<_>>(),
3269 vec![&PolicyId::new("l")]
3270 );
3271 assert_eq!(
3272 response.true_permits().collect::<Vec<_>>(),
3273 vec![&PolicyId::new("l")]
3274 );
3275 }
3276
3277 #[test]
3278 fn templates_no_links_deny() {
3279 let schema = schema();
3280 let policies = template_policy_set();
3281
3282 let request = partial_req();
3283 let es = PartialEntities::empty();
3284 let response = policies.tpe(&request, &es, &schema).unwrap();
3285
3286 assert_eq!(response.decision(), Some(Decision::Deny));
3287 assert_eq!(
3288 response.reason().unwrap().collect::<Vec<_>>(),
3289 Vec::<&PolicyId>::new()
3290 );
3291 }
3292
3293 #[test]
3294 fn concrete_deny() {
3295 let schema = schema();
3296 let mut policies = template_policy_set();
3297 policies
3298 .link(
3299 PolicyId::new("t0"),
3300 PolicyId::new("l"),
3301 HashMap::from([(
3302 SlotId::principal(),
3303 EntityUid::from_str(r#"User::"bob""#).unwrap(),
3304 )]),
3305 )
3306 .unwrap();
3307
3308 let request = partial_req();
3309 let es = PartialEntities::empty();
3310 let response = policies.tpe(&request, &es, &schema).unwrap();
3311
3312 assert_eq!(response.decision(), Some(Decision::Deny));
3313 assert_eq!(
3314 response.reason().unwrap().collect::<Vec<_>>(),
3315 Vec::<&PolicyId>::new()
3316 );
3317 }
3318
3319 #[test]
3320 fn residual() {
3321 let schema = schema();
3322 let mut policies = template_policy_set();
3323 policies
3324 .link(
3325 PolicyId::new("t1"),
3326 PolicyId::new("l"),
3327 HashMap::from([(
3328 SlotId::resource(),
3329 EntityUid::from_str(r#"Photo::"p""#).unwrap(),
3330 )]),
3331 )
3332 .unwrap();
3333
3334 let request = partial_req();
3335 let es = PartialEntities::empty();
3336 let response = policies.tpe(&request, &es, &schema).unwrap();
3337
3338 let expected: pst::Policy = Policy::parse(
3339 Some(PolicyId::new("l")),
3340 r#"permit(principal, action, resource) when { resource == Photo::"p" };"#,
3341 )
3342 .unwrap()
3343 .to_pst()
3344 .unwrap();
3345
3346 let residuals: Vec<_> = response.residual_policies().collect();
3347 assert_eq!(residuals[0].to_pst().unwrap().body(), expected.body());
3348 assert_eq!(response.decision(), None);
3349 assert!(response.reason().is_none());
3350 assert_eq!(residuals.len(), 1);
3351 }
3352 }
3353}