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