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, PolicySet,
36 Request, RequestValidationError, RestrictedExpression, Schema, TpeReauthorizationError,
37};
38
39#[doc = include_str!("../../experimental_warning.md")]
42#[repr(transparent)]
43#[derive(Debug, Clone, RefCast)]
44pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
45
46#[doc(hidden)]
47impl AsRef<tpe::request::PartialEntityUID> for PartialEntityUid {
48 fn as_ref(&self) -> &tpe::request::PartialEntityUID {
49 &self.0
50 }
51}
52
53impl PartialEntityUid {
54 pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
56 Self(tpe::request::PartialEntityUID {
57 ty: ty.0,
58 eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
59 })
60 }
61
62 pub fn from_concrete(euid: EntityUid) -> Self {
64 let (ty, eid) = euid.0.components();
65 Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
66 }
67}
68
69#[doc = include_str!("../../experimental_warning.md")]
73#[repr(transparent)]
74#[derive(Debug, Clone, RefCast)]
75pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
76
77#[doc(hidden)]
78impl AsRef<tpe::request::PartialRequest> for PartialRequest {
79 fn as_ref(&self) -> &tpe::request::PartialRequest {
80 &self.0
81 }
82}
83
84impl PartialRequest {
85 pub fn new(
87 principal: PartialEntityUid,
88 action: EntityUid,
89 resource: PartialEntityUid,
90 context: Option<Context>,
91 schema: &Schema,
92 ) -> Result<Self, PartialRequestCreationError> {
93 let context = context
94 .map(|c| match c.0 {
95 ast::Context::RestrictedResidual(_) => {
96 Err(PartialRequestCreationError::ContextContainsUnknowns)
97 }
98 ast::Context::Value(m) => Ok(m),
99 })
100 .transpose()?;
101 tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
102 .map(Self)
103 .map_err(|e| PartialRequestCreationError::Validation(e.into()))
104 }
105}
106
107#[doc = include_str!("../../experimental_warning.md")]
109#[repr(transparent)]
110#[derive(Debug, Clone, RefCast)]
111pub struct ResourceQueryRequest(pub(crate) PartialRequest);
112
113impl ResourceQueryRequest {
114 pub fn new(
116 principal: EntityUid,
117 action: EntityUid,
118 resource: EntityTypeName,
119 context: Context,
120 schema: &Schema,
121 ) -> Result<Self, PartialRequestCreationError> {
122 PartialRequest::new(
123 PartialEntityUid(principal.0.into()),
124 action,
125 PartialEntityUid::new(resource, None),
126 Some(context),
127 schema,
128 )
129 .map(Self)
130 }
131
132 pub fn to_request(
134 &self,
135 resource_id: EntityId,
136 schema: Option<&Schema>,
137 ) -> Result<Request, RequestValidationError> {
138 #[expect(
139 clippy::unwrap_used,
140 reason = "various fields are validated through the constructor"
141 )]
142 Request::new(
143 EntityUid(self.0 .0.get_principal().try_into().unwrap()),
144 EntityUid(self.0 .0.get_action()),
145 EntityUid::from_type_name_and_id(
146 EntityTypeName(self.0 .0.get_resource_type()),
147 resource_id,
148 ),
149 Context::from_pairs(
150 self.0
151 .0
152 .get_context_attrs()
153 .unwrap()
154 .iter()
155 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
156 )
157 .unwrap(),
158 schema,
159 )
160 }
161}
162
163#[doc = include_str!("../../experimental_warning.md")]
165#[repr(transparent)]
166#[derive(Debug, Clone, RefCast)]
167pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
168
169impl PrincipalQueryRequest {
170 pub fn new(
172 principal: EntityTypeName,
173 action: EntityUid,
174 resource: EntityUid,
175 context: Context,
176 schema: &Schema,
177 ) -> Result<Self, PartialRequestCreationError> {
178 PartialRequest::new(
179 PartialEntityUid::new(principal, None),
180 action,
181 PartialEntityUid(resource.0.into()),
182 Some(context),
183 schema,
184 )
185 .map(Self)
186 }
187
188 pub fn to_request(
190 &self,
191 principal_id: EntityId,
192 schema: Option<&Schema>,
193 ) -> Result<Request, RequestValidationError> {
194 #[expect(
195 clippy::unwrap_used,
196 reason = "various fields are validated through the constructor"
197 )]
198 Request::new(
199 EntityUid::from_type_name_and_id(
200 EntityTypeName(self.0 .0.get_principal_type()),
201 principal_id,
202 ),
203 EntityUid(self.0 .0.get_action()),
204 EntityUid(self.0 .0.get_resource().try_into().unwrap()),
205 Context::from_pairs(
206 self.0
207 .0
208 .get_context_attrs()
209 .unwrap()
210 .iter()
211 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
212 )
213 .unwrap(),
214 schema,
215 )
216 }
217}
218
219#[doc = include_str!("../../experimental_warning.md")]
224#[derive(Debug, Clone)]
225pub struct ActionQueryRequest {
226 principal: PartialEntityUid,
227 resource: PartialEntityUid,
228 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
229 schema: Schema,
230}
231
232impl ActionQueryRequest {
233 pub fn new(
235 principal: PartialEntityUid,
236 resource: PartialEntityUid,
237 context: Option<Context>,
238 schema: Schema,
239 ) -> Result<Self, PartialRequestCreationError> {
240 let context = context
241 .map(|c| match c.0 {
242 ast::Context::RestrictedResidual(_) => {
243 Err(PartialRequestCreationError::ContextContainsUnknowns)
244 }
245 ast::Context::Value(m) => Ok(m),
246 })
247 .transpose()?;
248 Ok(Self {
249 principal,
250 resource,
251 context,
252 schema,
253 })
254 }
255
256 fn partial_request(
257 &self,
258 action: EntityUid,
259 ) -> Result<PartialRequest, cedar_policy_core::validator::RequestValidationError> {
260 tpe::request::PartialRequest::new(
261 self.principal.0.clone(),
262 action.0,
263 self.resource.0.clone(),
264 self.context.clone(),
265 &self.schema.0,
266 )
267 .map(PartialRequest)
268 }
269}
270
271#[doc = include_str!("../../experimental_warning.md")]
273#[repr(transparent)]
274#[derive(Debug, Clone, RefCast)]
275pub struct PartialEntity(pub(crate) tpe::entities::PartialEntity);
276
277impl PartialEntity {
278 pub fn new(
280 uid: EntityUid,
281 attrs: Option<BTreeMap<SmolStr, RestrictedExpression>>,
282 ancestors: Option<HashSet<EntityUid>>,
283 tags: Option<BTreeMap<SmolStr, RestrictedExpression>>,
284 schema: &Schema,
285 ) -> Result<Self, PartialEntityError> {
286 Ok(Self(tpe::entities::PartialEntity::new(
287 uid.0,
288 attrs
289 .map(|ps| {
290 ps.into_iter()
291 .map(|(k, v)| {
292 Ok((
293 k,
294 RestrictedEvaluator::new(Extensions::all_available())
295 .interpret(v.0.as_borrowed())?,
296 ))
297 })
298 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
299 })
300 .transpose()?,
301 ancestors.map(|s| s.into_iter().map(|e| e.0).collect()),
302 tags.map(|ps| {
303 ps.into_iter()
304 .map(|(k, v)| {
305 Ok((
306 k,
307 RestrictedEvaluator::new(Extensions::all_available())
308 .interpret(v.0.as_borrowed())?,
309 ))
310 })
311 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
312 })
313 .transpose()?,
314 &schema.0,
315 )?))
316 }
317}
318
319#[doc = include_str!("../../experimental_warning.md")]
321#[repr(transparent)]
322#[derive(Debug, Clone, RefCast)]
323pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
324
325#[doc(hidden)]
326impl AsRef<tpe::entities::PartialEntities> for PartialEntities {
327 fn as_ref(&self) -> &tpe::entities::PartialEntities {
328 &self.0
329 }
330}
331
332impl PartialEntities {
333 pub fn from_json_value(
337 value: serde_json::Value,
338 schema: &Schema,
339 ) -> Result<Self, tpe_err::EntitiesError> {
340 tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
341 }
342
343 pub fn from_concrete(
345 entities: Entities,
346 schema: &Schema,
347 ) -> Result<Self, tpe_err::EntitiesError> {
348 tpe::entities::PartialEntities::from_concrete(entities.0, &schema.0).map(Self)
349 }
350
351 pub fn empty() -> Self {
353 Self(tpe::entities::PartialEntities::new())
354 }
355
356 pub fn from_partial_entities(
358 entities: impl IntoIterator<Item = PartialEntity>,
359 schema: &Schema,
360 ) -> Result<Self, tpe_err::EntitiesError> {
361 Ok(Self(tpe::entities::PartialEntities::from_entities(
362 entities.into_iter().map(|entity| entity.0),
363 &schema.0,
364 )?))
365 }
366}
367
368#[doc = include_str!("../../experimental_warning.md")]
370#[repr(transparent)]
371#[derive(Debug, Clone, RefCast)]
372pub struct TpeResponse<'a>(pub(crate) tpe::response::Response<'a>);
373
374#[doc(hidden)]
375impl<'a> AsRef<tpe::response::Response<'a>> for TpeResponse<'a> {
376 fn as_ref(&self) -> &tpe::response::Response<'a> {
377 &self.0
378 }
379}
380
381impl TpeResponse<'_> {
382 pub fn decision(&self) -> Option<Decision> {
384 self.0.decision()
385 }
386
387 pub fn reauthorize(
389 &self,
390 request: &Request,
391 entities: &Entities,
392 ) -> Result<api::Response, TpeReauthorizationError> {
393 self.0
394 .reauthorize(&request.0, &entities.0)
395 .map(Into::into)
396 .map_err(Into::into)
397 }
398
399 pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
422 self.0
423 .residual_policies()
424 .map(|p| Policy::from_ast(p.clone().into()))
425 }
426
427 pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
440 self.0
441 .residual_permits()
442 .chain(self.0.residual_forbids())
443 .map(|p| Policy::from_ast(p.clone().into()))
444 }
445}
446
447#[doc = include_str!("../../experimental_warning.md")]
454pub trait EntityLoader {
455 fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>>;
459}
460
461struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
463
464impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
465 fn load_entities(
466 &mut self,
467 uids: &HashSet<ast::EntityUID>,
468 ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
469 let ids = uids
470 .iter()
471 .map(|id| EntityUid::ref_cast(id).clone())
472 .collect();
473 self.0
474 .load_entities(&ids)
475 .into_iter()
476 .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
477 .collect()
478 }
479}
480
481#[doc = include_str!("../../experimental_warning.md")]
483#[derive(Debug)]
484
485pub struct TestEntityLoader<'a> {
486 entities: &'a Entities,
487}
488
489impl<'a> TestEntityLoader<'a> {
490 pub fn new(entities: &'a Entities) -> Self {
492 Self { entities }
493 }
494}
495
496impl EntityLoader for TestEntityLoader<'_> {
497 fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>> {
498 uids.iter()
499 .map(|uid| {
500 let entity = self.entities.get(uid).cloned();
501 (uid.clone(), entity)
502 })
503 .collect()
504 }
505}
506
507impl PolicySet {
508 #[doc = include_str!("../../experimental_warning.md")]
518 pub fn tpe<'a>(
519 &self,
520 request: &'a PartialRequest,
521 entities: &'a PartialEntities,
522 schema: &'a Schema,
523 ) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
524 use cedar_policy_core::tpe::is_authorized;
525 let ps = &self.ast;
526 let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
527 Ok(TpeResponse(res))
528 }
529
530 #[doc = include_str!("../../experimental_warning.md")]
539 pub fn is_authorized_batched(
540 &self,
541 query: &Request,
542 schema: &Schema,
543 loader: &mut dyn EntityLoader,
544 max_iters: u32,
545 ) -> Result<Decision, BatchedEvalError> {
546 is_authorized_batched(
547 &query.0,
548 &self.ast,
549 &schema.0,
550 &mut EntityLoaderWrapper(loader),
551 max_iters,
552 )
553 }
554
555 #[doc = include_str!("../../experimental_warning.md")]
557 pub fn query_resource(
558 &self,
559 request: &ResourceQueryRequest,
560 entities: &Entities,
561 schema: &Schema,
562 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
563 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
564 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
565 #[expect(
566 clippy::unwrap_used,
567 reason = "policy set construction should succeed because there shouldn't be any policy id conflicts"
568 )]
569 let policies = &Self::from_policies(
570 residuals
571 .0
572 .residual_policies()
573 .map(|p| Policy::from_ast(p.clone().into())),
574 )
575 .unwrap();
576 #[expect(
577 clippy::unwrap_used,
578 reason = "request construction should succeed because each entity passes validation"
579 )]
580 match residuals.decision() {
581 Some(Decision::Allow) => Ok(entities
582 .iter()
583 .filter(|entity| entity.0.uid().entity_type() == &request.0 .0.get_resource_type())
584 .map(super::Entity::uid)
585 .collect_vec()
586 .into_iter()),
587 Some(Decision::Deny) => Ok(vec![].into_iter()),
588 None => Ok(entities
589 .iter()
590 .filter(|entity| entity.0.uid().entity_type() == &request.0 .0.get_resource_type())
591 .filter(|entity| {
592 let authorizer = Authorizer::new();
593 authorizer
594 .is_authorized(
595 &request.to_request(entity.uid().id().clone(), None).unwrap(),
596 policies,
597 entities,
598 )
599 .decision
600 == Decision::Allow
601 })
602 .map(super::Entity::uid)
603 .collect_vec()
604 .into_iter()),
605 }
606 }
607
608 #[doc = include_str!("../../experimental_warning.md")]
610 pub fn query_principal(
611 &self,
612 request: &PrincipalQueryRequest,
613 entities: &Entities,
614 schema: &Schema,
615 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
616 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
617 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
618 #[expect(
619 clippy::unwrap_used,
620 reason = "policy set construction should succeed because there shouldn't be any policy id conflicts"
621 )]
622 let policies = &Self::from_policies(
623 residuals
624 .0
625 .residual_policies()
626 .map(|p| Policy::from_ast(p.clone().into())),
627 )
628 .unwrap();
629 #[expect(
630 clippy::unwrap_used,
631 reason = "request construction should succeed because each entity passes validation"
632 )]
633 match residuals.decision() {
634 Some(Decision::Allow) => Ok(entities
635 .iter()
636 .filter(|entity| entity.0.uid().entity_type() == &request.0 .0.get_principal_type())
637 .map(super::Entity::uid)
638 .collect_vec()
639 .into_iter()),
640 Some(Decision::Deny) => Ok(vec![].into_iter()),
641 None => Ok(entities
642 .iter()
643 .filter(|entity| entity.0.uid().entity_type() == &request.0 .0.get_principal_type())
644 .filter(|entity| {
645 let authorizer = Authorizer::new();
646 authorizer
647 .is_authorized(
648 &request.to_request(entity.uid().id().clone(), None).unwrap(),
649 policies,
650 entities,
651 )
652 .decision
653 == Decision::Allow
654 })
655 .map(super::Entity::uid)
656 .collect_vec()
657 .into_iter()),
658 }
659 }
660
661 #[doc = include_str!("../../experimental_warning.md")]
727 pub fn query_action<'a>(
728 &self,
729 request: &'a ActionQueryRequest,
730 entities: &PartialEntities,
731 ) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError> {
732 let mut authorized_actions = Vec::new();
733 for action in request
739 .schema
740 .0
741 .actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
742 {
743 if let Ok(partial_request) = request.partial_request(action.clone().into()) {
747 let decision = self
748 .tpe(&partial_request, entities, &request.schema)?
749 .decision();
750 if decision != Some(Decision::Deny) {
751 authorized_actions.push((RefCast::ref_cast(action), decision));
752 }
753 }
754 }
755 Ok(authorized_actions.into_iter())
756 }
757}
758
759#[cfg(test)]
760mod tpe_tests {
761 use std::{
762 collections::{BTreeMap, HashSet},
763 str::FromStr,
764 };
765
766 use cedar_policy_core::tpe::err::EntitiesError;
767 use cool_asserts::assert_matches;
768
769 use crate::{PartialEntity, PartialEntityError, RestrictedExpression, Schema};
770
771 #[test]
772 fn entity_construction() {
773 let schema = Schema::from_str(
774 r"
775 entity A in B tags Long;
776 entity B;
777 ",
778 )
779 .unwrap();
780 PartialEntity::new(
781 r#"A::"foo""#.parse().unwrap(),
782 None,
783 Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
784 Some(BTreeMap::from_iter([(
785 "".into(),
786 RestrictedExpression::new_long(1),
787 )])),
788 &schema,
789 )
790 .unwrap();
791 assert_matches!(
792 PartialEntity::new(
793 r#"A::"foo""#.parse().unwrap(),
794 None,
795 Some(HashSet::from_iter([r#"C::"c""#.parse().unwrap()])),
796 Some(BTreeMap::from_iter([(
797 "".into(),
798 RestrictedExpression::new_long(1)
799 )])),
800 &schema
801 ),
802 Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
803 );
804
805 assert_matches!(
806 PartialEntity::new(
807 r#"A::"foo""#.parse().unwrap(),
808 None,
809 Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
810 Some(BTreeMap::from_iter([(
811 "".into(),
812 RestrictedExpression::new_bool(true)
813 )])),
814 &schema
815 ),
816 Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
817 );
818 }
819
820 mod streaming_service {
821 use std::{collections::BTreeMap, str::FromStr};
822
823 use cedar_policy_core::{authorizer::Decision, tpe::err::EntitiesError};
824 use cool_asserts::assert_matches;
825 use itertools::Itertools;
826 use similar_asserts::assert_eq;
827
828 use crate::{
829 ActionConstraint, ActionQueryRequest, Context, Entities, EntityId, EntityUid,
830 PartialEntities, PartialEntity, PartialEntityError, PartialEntityUid, PartialRequest,
831 PolicySet, PrincipalConstraint, PrincipalQueryRequest, Request, ResourceConstraint,
832 ResourceQueryRequest, RestrictedExpression, Schema,
833 };
834
835 #[test]
836 fn entities_construction() {
837 let schema = schema();
838 PartialEntity::new(
839 r#"Movie::"foo""#.parse().unwrap(),
840 None,
841 None,
842 None,
843 &schema,
844 )
845 .unwrap();
846 PartialEntity::new(
847 r#"Show::"foo""#.parse().unwrap(),
848 Some(BTreeMap::from_iter([
849 ("isFree".into(), RestrictedExpression::new_bool(true)),
850 (
851 "releaseDate".into(),
852 RestrictedExpression::new_datetime("2025-01-01"),
853 ),
854 (
855 "isEarlyAccess".into(),
856 RestrictedExpression::new_bool(false),
857 ),
858 ])),
859 None,
860 None,
861 &schema,
862 )
863 .unwrap();
864
865 assert_matches!(
866 PartialEntity::new(
867 r#"Show::"foo""#.parse().unwrap(),
868 Some(BTreeMap::from_iter([
869 ("isFree".into(), RestrictedExpression::new_bool(true)),
870 (
871 "isEarlyAccess".into(),
872 RestrictedExpression::new_bool(false)
873 ),
874 ])),
875 None,
876 None,
877 &schema
878 ),
879 Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
880 );
881
882 let e1 = PartialEntity::new(
883 r#"Show::"foo""#.parse().unwrap(),
884 Some(BTreeMap::from_iter([
885 ("isFree".into(), RestrictedExpression::new_bool(true)),
886 (
887 "releaseDate".into(),
888 RestrictedExpression::new_datetime("2025-01-01"),
889 ),
890 (
891 "isEarlyAccess".into(),
892 RestrictedExpression::new_bool(false),
893 ),
894 ])),
895 None,
896 None,
897 &schema,
898 )
899 .unwrap();
900 let e2 = PartialEntity::new(
901 r#"Subscriber::"a""#.parse().unwrap(),
902 None,
903 None,
904 None,
905 &schema,
906 )
907 .unwrap();
908 PartialEntities::from_partial_entities([e1.clone(), e2.clone()], &schema).unwrap();
909 let e3 = PartialEntity::new(
910 r#"Show::"foo""#.parse().unwrap(),
911 Some(BTreeMap::from_iter([
912 ("isFree".into(), RestrictedExpression::new_bool(true)),
913 (
914 "releaseDate".into(),
915 RestrictedExpression::new_datetime("2025-01-01"),
916 ),
917 ("isEarlyAccess".into(), RestrictedExpression::new_bool(true)),
918 ])),
919 None,
920 None,
921 &schema,
922 )
923 .unwrap();
924 assert_matches!(
925 PartialEntities::from_partial_entities([e1, e2, e3], &schema),
926 Err(EntitiesError::Duplicate(_)),
927 );
928 }
929
930 #[track_caller]
931 fn schema() -> Schema {
932 Schema::from_cedarschema_str(
933 r"
934 // Types
935type Subscription = {
936 tier: String
937};
938type Profile = {
939 isKid: Bool
940};
941
942// Entities
943entity FreeMember;
944entity Subscriber = {
945 subscription: Subscription,
946 profile: Profile
947};
948entity Movie = {
949 isFree: Bool,
950 needsRentOrBuy: Bool,
951 isOscarNominated: Bool
952};
953entity Show = {
954 isFree: Bool,
955 releaseDate: datetime,
956 isEarlyAccess: Bool
957};
958
959// Actions for content in general
960action watch
961 appliesTo {
962 principal: [FreeMember, Subscriber],
963 resource: [Movie, Show],
964 context: {
965 now: {
966 datetime: datetime,
967 localTimeOffset: duration
968 }
969 }
970 };
971
972// Actions for movies only
973action rent, buy
974 appliesTo {
975 principal: [FreeMember, Subscriber],
976 resource: Movie,
977 context: {
978 now: {
979 datetime: datetime
980 }
981 }
982 };
983 ",
984 )
985 .unwrap()
986 .0
987 }
988
989 #[track_caller]
990 fn policy_set() -> PolicySet {
991 PolicySet::from_str(
992 r#"
993 // Subscriber Content Access (Shows)
994@id("subscriber-content-access/show")
995permit (
996 principal is Subscriber,
997 action == Action::"watch",
998 resource is Show
999)
1000unless
1001{ resource.isEarlyAccess && context.now.datetime < resource.releaseDate };
1002
1003// Subscriber Content Access (Movies)
1004@id("subscriber-content-access/movie")
1005permit (
1006 principal is Subscriber,
1007 action == Action::"watch",
1008 resource is Movie
1009)
1010unless { resource.needsRentOrBuy };
1011
1012// Free Content Access
1013@id("free-content-access")
1014permit (
1015 principal is FreeMember,
1016 action == Action::"watch",
1017 resource
1018)
1019when { resource.isFree };
1020
1021// Promo: Rent/Buy Oscar-Nominated Movies Until the Oscars
1022@id("rent-buy-oscar-movie")
1023permit (
1024 principal is Subscriber,
1025 action in [Action::"rent", Action::"buy"],
1026 resource is Movie
1027)
1028when
1029{
1030 resource.isOscarNominated &&
1031 context.now.datetime >= datetime("2025-02-02T19:00:00-0500") &&
1032 context.now.datetime < datetime(
1033 "2025-03-02T19:00:00-0500"
1034 ) // Oscars Night
1035};
1036
1037// Early Access (24h) to Shows for Premium Subscribers
1038@id("early-access-show")
1039permit (
1040 principal is Subscriber,
1041 action == Action::"watch",
1042 resource is Show
1043)
1044when
1045{
1046 resource.isEarlyAccess &&
1047 principal.subscription.tier == "premium" &&
1048 context.now.datetime >= resource.releaseDate.offset(duration("-24h"))
1049};
1050
1051// Forbid Bedtime Access to Kid Profile
1052@id("forbid-bedtime-watch-kid-profile")
1053forbid (
1054 principal is Subscriber,
1055 action == Action::"watch",
1056 resource
1057)
1058when { principal.profile.isKid }
1059unless
1060{
1061 // `toTime()` returns the duration modulo one day (i.e., it ignores the "date"
1062 // component). Here, we use it to calculate the subscriber's local time and
1063 // compare the result against durations that represent 6:00AM and 9:00PM.
1064 duration("6h") <= context.now
1065 .datetime
1066 .offset
1067 (
1068 context.now.localTimeOffset
1069 )
1070 .toTime
1071 (
1072 ) &&
1073 context.now.datetime.offset(context.now.localTimeOffset).toTime() <= duration(
1074 "21h"
1075 )
1076};
1077 "#,
1078 )
1079 .unwrap()
1080 }
1081
1082 #[track_caller]
1083 fn entities() -> Entities {
1084 Entities::from_json_value(
1085 serde_json::json!(
1086 [
1087 {
1088 "uid": {
1089 "type": "Subscriber",
1090 "id": "Alice"
1091 },
1092 "attrs": {
1093 "subscription" : {
1094 "tier": "standard"
1095 },
1096 "profile" : {
1097 "isKid": false
1098 }
1099 },
1100 "parents": []
1101 },
1102 {
1103 "uid": {
1104 "type": "FreeMember",
1105 "id": "Bob"
1106 },
1107 "attrs": {},
1108 "parents": []
1109 },
1110 {
1111 "uid": {
1112 "type": "Subscriber",
1113 "id": "Charlie"
1114 },
1115 "attrs": {
1116 "subscription" : {
1117 "tier": "premium"
1118 },
1119 "profile" : {
1120 "isKid": false
1121 }
1122 },
1123 "parents": []
1124 },
1125 {
1126 "uid": {
1127 "type": "Subscriber",
1128 "id": "Dave"
1129 },
1130 "attrs": {
1131 "subscription" : {
1132 "tier": "standard"
1133 },
1134 "profile" : {
1135 "isKid": true
1136 }
1137 },
1138 "parents": []
1139 },
1140 {
1141 "uid": {
1142 "type": "Movie",
1143 "id": "The Godparent"
1144 },
1145 "attrs": {
1146 "isFree" : true,
1147 "needsRentOrBuy" : false,
1148 "isOscarNominated": true
1149 },
1150 "parents": []
1151 },
1152 {
1153 "uid": {
1154 "type": "Movie",
1155 "id": "The Gleaming"
1156 },
1157 "attrs": {
1158 "isFree" : false,
1159 "needsRentOrBuy" : false,
1160 "isOscarNominated": false
1161 },
1162 "parents": []
1163 },
1164 {
1165 "uid": {
1166 "type": "Movie",
1167 "id": "Devilish"
1168 },
1169 "attrs": {
1170 "isFree" : false,
1171 "needsRentOrBuy" : true,
1172 "isOscarNominated": true
1173 },
1174 "parents": []
1175 },
1176 {
1177 "uid": {
1178 "type": "Show",
1179 "id": "Buddies"
1180 },
1181 "attrs": {
1182 "isFree" : false,
1183 "releaseDate": "2024-10-10",
1184 "isEarlyAccess": false
1185 },
1186 "parents": []
1187 },
1188 {
1189 "uid": {
1190 "type": "Show",
1191 "id": "Breach"
1192 },
1193 "attrs": {
1194 "isFree" : false,
1195 "releaseDate": "2025-02-21",
1196 "isEarlyAccess": true
1197 },
1198 "parents": []
1199 }
1200 ]
1201 ),
1202 Some(&schema()),
1203 )
1204 .unwrap()
1205 }
1206
1207 #[test]
1208 fn run_tpe() {
1209 let schema = schema();
1210 let request = PartialRequest::new(
1211 PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1212 r#"Action::"watch""#.parse().unwrap(),
1213 PartialEntityUid::new("Movie".parse().unwrap(), None),
1214 Some(
1215 Context::from_pairs([(
1216 "now".into(),
1217 RestrictedExpression::new_record([
1218 (
1219 "datetime".into(),
1220 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#)
1221 .unwrap(),
1222 ),
1223 (
1224 "localTimeOffset".into(),
1225 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1226 ),
1227 ])
1228 .unwrap(),
1229 )])
1230 .unwrap(),
1231 ),
1232 &schema,
1233 )
1234 .unwrap();
1235 let policies = policy_set();
1236 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
1237
1238 let response = policies
1239 .tpe(&request, &partial_entities, &schema)
1240 .expect("tpe should succeed");
1241
1242 assert_eq!(
1243 response.residual_policies().count(),
1244 policies.num_of_policies()
1245 );
1246 for p in response.residual_policies() {
1247 assert_matches!(p.action_constraint(), ActionConstraint::Any);
1248 assert_matches!(p.principal_constraint(), PrincipalConstraint::Any);
1249 assert_matches!(p.resource_constraint(), ResourceConstraint::Any);
1250 }
1251 assert_eq!(
1252 response
1253 .nontrivial_residual_policies()
1254 .next()
1255 .unwrap()
1256 .annotation("id")
1257 .unwrap(),
1258 "subscriber-content-access/movie"
1259 );
1260
1261 assert_eq!(response.decision(), None);
1262
1263 let request = Request::new(
1264 EntityUid::from_type_name_and_id(
1265 "Subscriber".parse().unwrap(),
1266 EntityId::new("Alice"),
1267 ),
1268 r#"Action::"watch""#.parse().unwrap(),
1269 EntityUid::from_type_name_and_id(
1270 "Movie".parse().unwrap(),
1271 EntityId::new("The Godparent"),
1272 ),
1273 Context::from_pairs([(
1274 "now".into(),
1275 RestrictedExpression::new_record([
1276 (
1277 "datetime".into(),
1278 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1279 ),
1280 (
1281 "localTimeOffset".into(),
1282 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1283 ),
1284 ])
1285 .unwrap(),
1286 )])
1287 .unwrap(),
1288 Some(&schema),
1289 )
1290 .unwrap();
1291 assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
1292 assert_eq!(res.decision(), Decision::Allow);
1293 });
1294
1295 let request = Request::new(
1296 EntityUid::from_type_name_and_id(
1297 "Subscriber".parse().unwrap(),
1298 EntityId::new("Alice"),
1299 ),
1300 r#"Action::"watch""#.parse().unwrap(),
1301 EntityUid::from_type_name_and_id(
1302 "Movie".parse().unwrap(),
1303 EntityId::new("Devilish"),
1304 ),
1305 Context::from_pairs([(
1306 "now".into(),
1307 RestrictedExpression::new_record([
1308 (
1309 "datetime".into(),
1310 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1311 ),
1312 (
1313 "localTimeOffset".into(),
1314 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1315 ),
1316 ])
1317 .unwrap(),
1318 )])
1319 .unwrap(),
1320 Some(&schema),
1321 )
1322 .unwrap();
1323 assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
1324 assert_eq!(res.decision(), Decision::Deny);
1325 });
1326 }
1327
1328 #[test]
1329 fn query_resource() {
1330 let schema = schema();
1331 let policies = policy_set();
1332 let request = ResourceQueryRequest::new(
1333 r#"Subscriber::"Alice""#.parse().unwrap(),
1334 r#"Action::"watch""#.parse().unwrap(),
1335 "Movie".parse().unwrap(),
1336 Context::from_pairs([(
1337 "now".into(),
1338 RestrictedExpression::new_record([
1339 (
1340 "datetime".into(),
1341 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1342 ),
1343 (
1344 "localTimeOffset".into(),
1345 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1346 ),
1347 ])
1348 .unwrap(),
1349 )])
1350 .unwrap(),
1351 &schema,
1352 )
1353 .unwrap();
1354
1355 let movies = policies
1358 .query_resource(&request, &entities(), &schema)
1359 .unwrap()
1360 .sorted()
1361 .collect_vec();
1362 assert_eq!(
1363 movies,
1364 &[
1365 EntityUid::from_str(r#"Movie::"The Gleaming""#).unwrap(),
1366 EntityUid::from_str(r#"Movie::"The Godparent""#).unwrap(),
1367 ]
1368 );
1369 }
1370
1371 #[test]
1372 fn query_principal() {
1373 let schema = schema();
1374 let policies = policy_set();
1375
1376 let request = PrincipalQueryRequest::new(
1377 "Subscriber".parse().unwrap(),
1378 r#"Action::"watch""#.parse().unwrap(),
1379 r#"Movie::"The Godparent""#.parse().unwrap(),
1380 Context::from_pairs([(
1381 "now".into(),
1382 RestrictedExpression::new_record([
1383 (
1384 "datetime".into(),
1385 RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
1386 ),
1387 (
1388 "localTimeOffset".into(),
1389 RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
1390 ),
1391 ])
1392 .unwrap(),
1393 )])
1394 .unwrap(),
1395 &schema,
1396 )
1397 .unwrap();
1398
1399 let subscribers = policies
1400 .query_principal(&request, &entities(), &schema)
1401 .unwrap()
1402 .sorted()
1403 .collect_vec();
1404 assert_eq!(
1405 subscribers,
1406 &[
1407 EntityUid::from_str(r#"Subscriber::"Alice""#).unwrap(),
1408 EntityUid::from_str(r#"Subscriber::"Charlie""#).unwrap(),
1409 ]
1410 );
1411 }
1412
1413 #[test]
1414 fn query_action_alice() {
1415 let schema = schema();
1416 let request = ActionQueryRequest::new(
1417 PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
1418 PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
1419 None,
1420 schema.clone(),
1421 )
1422 .unwrap();
1423
1424 let policies = policy_set();
1425 let mut actions: Vec<_> = policies
1426 .query_action(
1427 &request,
1428 &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1429 )
1430 .unwrap()
1431 .collect();
1432 actions.sort_by_key(|(a, _)| *a);
1433 assert_eq!(
1434 actions,
1435 vec![
1436 (&r#"Action::"buy""#.parse().unwrap(), None),
1437 (&r#"Action::"rent""#.parse().unwrap(), None),
1438 (
1439 &r#"Action::"watch""#.parse().unwrap(),
1440 Some(Decision::Allow)
1441 ),
1442 ]
1443 );
1444 }
1445
1446 #[test]
1447 fn query_action_bob_free() {
1448 let schema = schema();
1449 let request = ActionQueryRequest::new(
1450 PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
1451 PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
1452 None,
1453 schema.clone(),
1454 )
1455 .unwrap();
1456
1457 let policies = policy_set();
1458 let actions: Vec<_> = policies
1459 .query_action(
1460 &request,
1461 &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1462 )
1463 .unwrap()
1464 .collect();
1465 assert_eq!(
1466 actions,
1467 vec![(
1468 &r#"Action::"watch""#.parse().unwrap(),
1469 Some(Decision::Allow)
1470 ),]
1471 );
1472 }
1473
1474 #[test]
1475 fn query_action_bob_not_free() {
1476 let schema = schema();
1477 let request = ActionQueryRequest::new(
1478 PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
1479 PartialEntityUid::from_concrete(r#"Movie::"The Gleaming""#.parse().unwrap()),
1480 None,
1481 schema.clone(),
1482 )
1483 .unwrap();
1484
1485 let policies = policy_set();
1486 let actions: Vec<_> = policies
1487 .query_action(
1488 &request,
1489 &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1490 )
1491 .unwrap()
1492 .collect();
1493 assert_eq!(actions, vec![]);
1494 }
1495 }
1496
1497 mod github {
1498 use std::{
1499 collections::{HashMap, HashSet},
1500 str::FromStr,
1501 };
1502
1503 use cedar_policy_core::tpe::err::TpeError;
1504 use cedar_policy_core::{authorizer::Decision, batched_evaluator::err::BatchedEvalError};
1505 use cool_asserts::assert_matches;
1506 use itertools::Itertools;
1507 use similar_asserts::assert_eq;
1508
1509 use crate::{
1510 ActionQueryRequest, Context, Entities, EntityUid, PartialEntities, PartialEntityUid,
1511 PolicySet, PrincipalQueryRequest, Request, ResourceQueryRequest, RestrictedExpression,
1512 Schema, TestEntityLoader,
1513 };
1514
1515 #[track_caller]
1516 fn schema() -> Schema {
1517 Schema::from_str(
1518 r#"
1519 entity Team, UserGroup in [UserGroup];
1520entity Issue = {
1521 "repo": Repository,
1522 "reporter": User,
1523};
1524entity Org = {
1525 "members": UserGroup,
1526 "owners": UserGroup,
1527};
1528entity Repository = {
1529 "admins": UserGroup,
1530 "maintainers": UserGroup,
1531 "readers": UserGroup,
1532 "triagers": UserGroup,
1533 "writers": UserGroup,
1534};
1535entity User in [UserGroup, Team];
1536
1537action push, pull, fork appliesTo {
1538 principal: [User],
1539 resource: [Repository]
1540};
1541action assign_issue, delete_issue, edit_issue appliesTo {
1542 principal: [User],
1543 resource: [Issue]
1544};
1545action add_reader, add_writer, add_maintainer, add_admin, add_triager appliesTo {
1546 principal: [User],
1547 resource: [Repository]
1548};
1549 "#,
1550 )
1551 .unwrap()
1552 }
1553
1554 fn policy_set() -> PolicySet {
1555 PolicySet::from_str(
1556 r#"
1557 //Actions for readers
1558permit (
1559 principal,
1560 action == Action::"pull",
1561 resource
1562)
1563when { principal in resource.readers };
1564
1565permit (
1566 principal,
1567 action == Action::"fork",
1568 resource
1569)
1570when { principal in resource.readers };
1571
1572permit (
1573 principal,
1574 action == Action::"delete_issue",
1575 resource
1576)
1577when { principal in resource.repo.readers && principal == resource.reporter };
1578
1579permit (
1580 principal,
1581 action == Action::"edit_issue",
1582 resource
1583)
1584when { principal in resource.repo.readers && principal == resource.reporter };
1585
1586//Actions for triagers
1587permit (
1588 principal,
1589 action == Action::"assign_issue",
1590 resource
1591)
1592when { principal in resource.repo.triagers };
1593
1594//Actions for writers
1595permit (
1596 principal,
1597 action == Action::"push",
1598 resource
1599)
1600when { principal in resource.writers };
1601
1602permit (
1603 principal,
1604 action == Action::"edit_issue",
1605 resource
1606)
1607when { principal in resource.repo.writers };
1608
1609//Actions for maintainers
1610permit (
1611 principal,
1612 action == Action::"delete_issue",
1613 resource
1614)
1615when { principal in resource.repo.maintainers };
1616
1617//Actions for admins
1618permit (
1619 principal,
1620 action in
1621 [Action::"add_reader",
1622 Action::"add_triager",
1623 Action::"add_writer",
1624 Action::"add_maintainer",
1625 Action::"add_admin"],
1626 resource
1627)
1628when { principal in resource.admins };
1629//We use the same permissions for org owners, and rely on placing them in the admins group for every repository in the org
1630//The other option is to duplicate all policies for the org base permissions (with a separate heirarchy for each org)
1631"#,
1632 )
1633 .unwrap()
1634 }
1635
1636 #[track_caller]
1637 fn entities() -> Entities {
1638 Entities::from_json_value(serde_json::json!(
1639
1640 [
1641 {
1642 "uid": { "__entity": { "type": "User", "id": "alice"} },
1643 "attrs": {},
1644 "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} } ]
1645 },
1646 {
1647 "uid": { "__entity": { "type": "User", "id": "jane"} },
1648 "attrs": {},
1649 "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} }, { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} }]
1650 },
1651 {
1652 "uid": { "__entity": { "type": "User", "id": "bob"} },
1653 "attrs": {},
1654 "parents": []
1655 },
1656 {
1657 "uid": { "__entity": { "type": "Repository", "id": "common_knowledge"} },
1658 "attrs": {
1659 "readers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
1660 "triagers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
1661 "writers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
1662 "maintainers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
1663 "admins" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} }
1664 },
1665 "parents": []
1666 },
1667 {
1668 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
1669 "attrs": {
1670 },
1671 "parents": [ ]
1672 },
1673 {
1674 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
1675 "attrs": {
1676 },
1677 "parents": [ { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} } ]
1678 },
1679 {
1680 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
1681 "attrs": {
1682 },
1683 "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"}} ]
1684 },
1685 {
1686 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
1687 "attrs": {
1688 },
1689 "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_writers"}} ]
1690 },
1691 {
1692 "uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} },
1693 "attrs": {
1694 },
1695 "parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"}} ]
1696 },
1697 {
1698 "uid": { "__entity": { "type": "Repository", "id": "secret"} },
1699 "attrs": {
1700 "readers" : { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
1701 "triagers" : { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
1702 "writers" : { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
1703 "maintainers" : { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
1704 "admins" : { "__entity": { "type": "UserGroup", "id": "secret_admins"} }
1705 },
1706 "parents": []
1707 },
1708 {
1709 "uid": { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
1710 "attrs": {
1711 },
1712 "parents": [ ]
1713 },
1714 {
1715 "uid": { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
1716 "attrs": {
1717 },
1718 "parents": [ { "__entity": { "type": "UserGroup", "id": "secret_readers"} } ]
1719 },
1720 {
1721 "uid": { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
1722 "attrs": {
1723 },
1724 "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_triagers"}} ]
1725 },
1726 {
1727 "uid": { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
1728 "attrs": {
1729 },
1730 "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_writers"}} ]
1731 },
1732 {
1733 "uid": { "__entity": { "type": "UserGroup", "id": "secret_admins"} },
1734 "attrs": {
1735 },
1736 "parents": [ {"__entity": { "type": "UserGroup", "id": "secret_maintainers"}} ]
1737 },
1738 {
1739 "uid": { "__entity": { "type": "Repository", "id": "uncommon_knowledge"} },
1740 "attrs": {
1741 "readers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
1742 "triagers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
1743 "writers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
1744 "maintainers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
1745 "admins" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} }
1746 },
1747 "parents": []
1748 },
1749 {
1750 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
1751 "attrs": {
1752 },
1753 "parents": [ ]
1754 },
1755 {
1756 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
1757 "attrs": {
1758 },
1759 "parents": [ { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} } ]
1760 },
1761 {
1762 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
1763 "attrs": {
1764 },
1765 "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"}} ]
1766 },
1767 {
1768 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
1769 "attrs": {
1770 },
1771 "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"}} ]
1772 },
1773 {
1774 "uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} },
1775 "attrs": {
1776 },
1777 "parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"}} ]
1778 },
1779 {
1780 "uid": { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} },
1781 "attrs": {},
1782 "parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} }, { "__entity": { "type": "UserGroup", "id": "secret_readers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} }]
1783 },
1784]
1785 ), Some(&schema())).unwrap()
1786 }
1787
1788 #[test]
1789 fn query_resource() {
1790 let schema = schema();
1791 let request = ResourceQueryRequest::new(
1792 r#"User::"jane""#.parse().unwrap(),
1793 r#"Action::"push""#.parse().unwrap(),
1794 "Repository".parse().unwrap(),
1795 Context::empty(),
1796 &schema,
1797 )
1798 .unwrap();
1799 let policies = policy_set();
1800 assert_matches!(&policies.query_resource(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
1801 assert_eq!(uid, &r#"Repository::"common_knowledge""#.parse().unwrap());
1802 });
1803 }
1804
1805 #[test]
1806 fn query_principal() {
1807 let schema = schema();
1808 let request = PrincipalQueryRequest::new(
1809 r"User".parse().unwrap(),
1810 r#"Action::"pull""#.parse().unwrap(),
1811 r#"Repository::"secret""#.parse().unwrap(),
1812 Context::empty(),
1813 &schema,
1814 )
1815 .unwrap();
1816 let policies = policy_set();
1817 assert_matches!(&policies.query_principal(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
1818 assert_eq!(uid, &r#"User::"jane""#.parse().unwrap());
1819 });
1820 }
1821
1822 #[test]
1823 fn query_action() {
1824 let schema = schema();
1825 let request = ActionQueryRequest::new(
1826 PartialEntityUid::from_concrete(r#"User::"jane""#.parse().unwrap()),
1827 PartialEntityUid::from_concrete(r#"Repository::"secret""#.parse().unwrap()),
1828 None,
1829 schema.clone(),
1830 )
1831 .unwrap();
1832
1833 let policies = policy_set();
1834 let mut actions: Vec<_> = policies
1835 .query_action(
1836 &request,
1837 &PartialEntities::from_concrete(entities(), &schema).unwrap(),
1838 )
1839 .unwrap()
1840 .collect();
1841 actions.sort_by_key(|(a, _)| *a);
1842 assert_eq!(
1843 actions,
1844 vec![
1845 (&r#"Action::"fork""#.parse().unwrap(), Some(Decision::Allow)),
1846 (&r#"Action::"pull""#.parse().unwrap(), Some(Decision::Allow)),
1847 ]
1848 );
1849 }
1850
1851 #[test]
1852 fn test_is_authorized_vs_is_authorized_batched() {
1853 use crate::{Authorizer, Request};
1854
1855 let schema = schema();
1856 let policies = policy_set();
1857 let entities = entities();
1858 let authorizer = Authorizer::new();
1859
1860 let test_requests = vec![
1862 Request::new(
1864 r#"User::"alice""#.parse().unwrap(),
1865 r#"Action::"push""#.parse().unwrap(),
1866 r#"Repository::"common_knowledge""#.parse().unwrap(),
1867 Context::empty(),
1868 Some(&schema),
1869 )
1870 .unwrap(),
1871 Request::new(
1873 r#"User::"jane""#.parse().unwrap(),
1874 r#"Action::"pull""#.parse().unwrap(),
1875 r#"Repository::"secret""#.parse().unwrap(),
1876 Context::empty(),
1877 Some(&schema),
1878 )
1879 .unwrap(),
1880 Request::new(
1882 r#"User::"bob""#.parse().unwrap(),
1883 r#"Action::"push""#.parse().unwrap(),
1884 r#"Repository::"common_knowledge""#.parse().unwrap(),
1885 Context::empty(),
1886 Some(&schema),
1887 )
1888 .unwrap(),
1889 Request::new(
1891 r#"User::"alice""#.parse().unwrap(),
1892 r#"Action::"fork""#.parse().unwrap(),
1893 r#"Repository::"common_knowledge""#.parse().unwrap(),
1894 Context::empty(),
1895 Some(&schema),
1896 )
1897 .unwrap(),
1898 ];
1899
1900 for (i, request) in test_requests.iter().enumerate() {
1902 let standard_response = authorizer.is_authorized(request, &policies, &entities);
1904
1905 let mut loader = TestEntityLoader::new(&entities);
1907 let batched_decision = policies
1908 .is_authorized_batched(request, &schema, &mut loader, u32::MAX)
1909 .unwrap();
1910
1911 let standard_decision = standard_response.decision();
1913
1914 assert_eq!(
1915 standard_decision,
1916 batched_decision,
1917 "Request {}: is_authorized returned {:?} but is_authorized_batched returned {:?}",
1918 i + 1,
1919 standard_decision,
1920 batched_decision
1921 );
1922 }
1923 }
1924
1925 #[test]
1926 fn test_batched_evaluation_error_validation() {
1927 let schema = schema();
1928 let policies = PolicySet::from_str(
1929 r#"permit(principal, action, resource) when { principal.nonexistent_attr == "value" };"#
1930 ).unwrap();
1931
1932 let request = Request::new(
1933 EntityUid::from_str("User::\"alice\"").unwrap(),
1934 EntityUid::from_str("Action::\"push\"").unwrap(),
1935 EntityUid::from_str("Repository::\"repo\"").unwrap(),
1936 Context::empty(),
1937 Some(&schema),
1938 )
1939 .unwrap();
1940
1941 let entities = entities();
1942 let mut loader = TestEntityLoader::new(&entities);
1943 let result = policies.is_authorized_batched(&request, &schema, &mut loader, 10);
1944
1945 assert!(matches!(
1946 result,
1947 Err(BatchedEvalError::TPE(TpeError::Validation(_)))
1948 ));
1949 }
1950
1951 #[test]
1952 #[cfg(feature = "partial-eval")]
1953 fn test_batched_evaluation_error_partial_request() {
1954 let context_with_unknown = Context::from_pairs([(
1955 "key".to_string(),
1956 RestrictedExpression::new_unknown("test_unknown"),
1957 )])
1958 .unwrap();
1959
1960 let request = Request::new(
1961 EntityUid::from_str("User::\"alice\"").unwrap(),
1962 EntityUid::from_str("Action::\"view\"").unwrap(),
1963 EntityUid::from_str("Resource::\"doc\"").unwrap(),
1964 context_with_unknown,
1965 None,
1966 )
1967 .unwrap();
1968 let schema = schema();
1969
1970 let pset = PolicySet::from_str("permit(principal, action, resource);").unwrap();
1971 let entities = Entities::empty();
1972 let mut loader = TestEntityLoader::new(&entities);
1973 let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
1974
1975 assert_matches!(result, Err(BatchedEvalError::PartialRequest(_)));
1976 }
1977
1978 #[test]
1979 fn test_batched_evaluation_error_invalid_entity() {
1980 struct InvalidEntityLoader;
1982 impl crate::EntityLoader for InvalidEntityLoader {
1983 fn load_entities(
1984 &mut self,
1985 _uids: &HashSet<EntityUid>,
1986 ) -> HashMap<EntityUid, Option<crate::Entity>> {
1987 let mut result = HashMap::new();
1988 let uid = EntityUid::from_strs("Org", "myorg");
1989 let entity = crate::Entity::new(
1990 uid.clone(),
1991 [
1992 (
1993 "members".to_string(),
1994 RestrictedExpression::new_string("not_a_usergroup".to_string()),
1995 ),
1996 (
1997 "owners".to_string(),
1998 RestrictedExpression::new_entity_uid(EntityUid::from_strs(
1999 "UserGroup",
2000 "2",
2001 )),
2002 ),
2003 ]
2004 .into(),
2005 HashSet::new(),
2006 )
2007 .unwrap();
2008 result.insert(uid, Some(entity));
2009 result
2010 }
2011 }
2012
2013 let schema = schema();
2014 let pset = PolicySet::from_str(
2015 "permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
2016 )
2017 .unwrap();
2018
2019 let request = Request::new(
2020 r#"User::"alice""#.parse().unwrap(),
2021 r#"Action::"push""#.parse().unwrap(),
2022 r#"Repository::"common_knowledge""#.parse().unwrap(),
2023 Context::empty(),
2024 Some(&schema),
2025 )
2026 .unwrap();
2027
2028 let mut loader = InvalidEntityLoader;
2029 let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2030
2031 assert_matches!(result, Err(BatchedEvalError::Entities(_)));
2032 }
2033
2034 #[test]
2035 #[cfg(feature = "partial-eval")]
2036 fn test_batched_evaluation_error_partial_entity() {
2037 struct PartialEntityLoader;
2039 impl crate::EntityLoader for PartialEntityLoader {
2040 fn load_entities(
2041 &mut self,
2042 _uids: &HashSet<EntityUid>,
2043 ) -> HashMap<EntityUid, Option<crate::Entity>> {
2044 let mut result = HashMap::new();
2045 let uid = EntityUid::from_strs("Org", "myorg");
2046 let entity = crate::Entity::new(
2047 uid.clone(),
2048 [
2049 (
2050 "members".to_string(),
2051 RestrictedExpression::new_unknown("partial_members"),
2052 ),
2053 (
2054 "owners".to_string(),
2055 RestrictedExpression::new_entity_uid(EntityUid::from_strs(
2056 "UserGroup",
2057 "2",
2058 )),
2059 ),
2060 ]
2061 .into(),
2062 HashSet::new(),
2063 )
2064 .unwrap();
2065 result.insert(uid, Some(entity));
2066 result
2067 }
2068 }
2069
2070 let schema = schema();
2071 let pset = PolicySet::from_str(
2072 "permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
2073 )
2074 .unwrap();
2075
2076 let request = Request::new(
2077 r#"User::"alice""#.parse().unwrap(),
2078 r#"Action::"push""#.parse().unwrap(),
2079 r#"Repository::"common_knowledge""#.parse().unwrap(),
2080 Context::empty(),
2081 Some(&schema),
2082 )
2083 .unwrap();
2084
2085 let mut loader = PartialEntityLoader;
2086 let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
2087
2088 assert_matches!(result, Err(BatchedEvalError::PartialValueToValue(_)));
2089 }
2090
2091 #[test]
2092 fn test_batched_evaluation_error_insufficient_iters() {
2093 let schema = schema();
2094 let policies = policy_set();
2095 let entities = entities();
2096
2097 let request = Request::new(
2098 r#"User::"alice""#.parse().unwrap(),
2099 r#"Action::"push""#.parse().unwrap(),
2100 r#"Repository::"common_knowledge""#.parse().unwrap(),
2101 Context::empty(),
2102 Some(&schema),
2103 )
2104 .unwrap();
2105
2106 let mut loader = TestEntityLoader::new(&entities);
2107 let result = policies.is_authorized_batched(&request, &schema, &mut loader, 0);
2108
2109 assert_matches!(result, Err(BatchedEvalError::InsufficientIterations(_)));
2110 }
2111 }
2112
2113 mod trivial {
2114 use cedar_policy_core::authorizer::Decision;
2115 use itertools::Itertools;
2116
2117 use crate::{
2118 Context, Entities, PartialEntities, PartialEntityUid, PartialRequest, PolicySet,
2119 PrincipalQueryRequest, ResourceQueryRequest, Schema,
2120 };
2121 use std::{i64, str::FromStr};
2122
2123 fn schema() -> Schema {
2124 Schema::from_str("entity P, R; action A appliesTo { principal: P, resource: R };")
2125 .unwrap()
2126 }
2127
2128 fn entities() -> Entities {
2129 Entities::from_json_value(
2130 serde_json::json!([
2131 { "uid": { "__entity": { "type": "P", "id": ""} }, "attrs": {}, "parents": [] },
2132 { "uid": { "__entity": { "type": "R", "id": ""} }, "attrs": {}, "parents": [] },
2133 ]),
2134 None,
2135 )
2136 .unwrap()
2137 }
2138
2139 #[test]
2140 fn trivial_permit_tpe() {
2141 let schema = schema();
2142 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2143 let req = PartialRequest::new(
2144 PartialEntityUid::new("P".parse().unwrap(), None),
2145 r#"Action::"A""#.parse().unwrap(),
2146 PartialEntityUid::new("R".parse().unwrap(), None),
2147 None,
2148 &schema,
2149 )
2150 .unwrap();
2151 let response = PolicySet::from_str(r"permit(principal, action, resource);")
2152 .unwrap()
2153 .tpe(&req, &partial_entities, &schema)
2154 .unwrap();
2155 assert_eq!(response.decision(), Some(Decision::Allow));
2156 }
2157
2158 #[test]
2159 fn trivial_permit_query_principal() {
2160 let schema = schema();
2161 let entities = entities();
2162 let req = PrincipalQueryRequest::new(
2163 "P".parse().unwrap(),
2164 r#"Action::"A""#.parse().unwrap(),
2165 r#"R::"""#.parse().unwrap(),
2166 Context::empty(),
2167 &schema,
2168 )
2169 .unwrap();
2170
2171 let principals = PolicySet::from_str(r#"permit(principal, action, resource);"#)
2172 .unwrap()
2173 .query_principal(&req, &entities, &schema)
2174 .unwrap()
2175 .collect_vec();
2176 assert_eq!(&principals, &[r#"P::"""#.parse().unwrap()]);
2177 }
2178
2179 #[test]
2180 fn trivial_permit_query_resource() {
2181 let schema = schema();
2182 let entities = entities();
2183 let req = ResourceQueryRequest::new(
2184 r#"P::"""#.parse().unwrap(),
2185 r#"Action::"A""#.parse().unwrap(),
2186 "R".parse().unwrap(),
2187 Context::empty(),
2188 &schema,
2189 )
2190 .unwrap();
2191
2192 let resources = PolicySet::from_str(r#"permit(principal, action, resource);"#)
2193 .unwrap()
2194 .query_resource(&req, &entities, &schema)
2195 .unwrap()
2196 .collect_vec();
2197 assert_eq!(&resources, &[r#"R::"""#.parse().unwrap()]);
2198 }
2199
2200 #[test]
2201 fn trivial_forbid_tpe() {
2202 let schema = schema();
2203 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2204 let req = PartialRequest::new(
2205 PartialEntityUid::new("P".parse().unwrap(), None),
2206 r#"Action::"A""#.parse().unwrap(),
2207 PartialEntityUid::new("R".parse().unwrap(), None),
2208 None,
2209 &schema,
2210 )
2211 .unwrap();
2212 let response = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2213 .unwrap()
2214 .tpe(&req, &partial_entities, &schema)
2215 .unwrap();
2216 assert_eq!(response.decision(), Some(Decision::Deny));
2217 }
2218
2219 #[test]
2220 fn trivial_forbid_query_principal() {
2221 let schema = schema();
2222 let entities = entities();
2223 let req = PrincipalQueryRequest::new(
2224 "P".parse().unwrap(),
2225 r#"Action::"A""#.parse().unwrap(),
2226 r#"R::"""#.parse().unwrap(),
2227 Context::empty(),
2228 &schema,
2229 )
2230 .unwrap();
2231
2232 let principals = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2233 .unwrap()
2234 .query_principal(&req, &entities, &schema)
2235 .unwrap()
2236 .collect_vec();
2237 assert_eq!(&principals, &[]);
2238 }
2239
2240 #[test]
2241 fn trivial_forbid_query_resource() {
2242 let schema = schema();
2243 let entities = entities();
2244 let req = ResourceQueryRequest::new(
2245 r#"P::"""#.parse().unwrap(),
2246 r#"Action::"A""#.parse().unwrap(),
2247 "R".parse().unwrap(),
2248 Context::empty(),
2249 &schema,
2250 )
2251 .unwrap();
2252
2253 let resources = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
2254 .unwrap()
2255 .query_resource(&req, &entities, &schema)
2256 .unwrap()
2257 .collect_vec();
2258 assert_eq!(&resources, &[]);
2259 }
2260
2261 #[test]
2262 fn error_tpe() {
2263 let schema = schema();
2264 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2265 let req = PartialRequest::new(
2266 PartialEntityUid::new("P".parse().unwrap(), None),
2267 r#"Action::"A""#.parse().unwrap(),
2268 PartialEntityUid::new("R".parse().unwrap(), None),
2269 None,
2270 &schema,
2271 )
2272 .unwrap();
2273 let response = PolicySet::from_str(&format!(
2274 r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2275 i64::MAX
2276 ))
2277 .unwrap()
2278 .tpe(&req, &partial_entities, &schema)
2279 .unwrap();
2280 assert_eq!(response.decision(), Some(Decision::Deny));
2281 }
2282
2283 #[test]
2284 fn error_query_principal() {
2285 let schema = schema();
2286 let entities = entities();
2287 let req = PrincipalQueryRequest::new(
2288 "P".parse().unwrap(),
2289 r#"Action::"A""#.parse().unwrap(),
2290 r#"R::"""#.parse().unwrap(),
2291 Context::empty(),
2292 &schema,
2293 )
2294 .unwrap();
2295
2296 let principals = PolicySet::from_str(&format!(
2297 r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2298 i64::MAX
2299 ))
2300 .unwrap()
2301 .query_principal(&req, &entities, &schema)
2302 .unwrap()
2303 .collect_vec();
2304 assert_eq!(&principals, &[]);
2305 }
2306
2307 #[test]
2308 fn error_query_resource() {
2309 let schema = schema();
2310 let entities = entities();
2311 let req = ResourceQueryRequest::new(
2312 r#"P::"""#.parse().unwrap(),
2313 r#"Action::"A""#.parse().unwrap(),
2314 "R".parse().unwrap(),
2315 Context::empty(),
2316 &schema,
2317 )
2318 .unwrap();
2319
2320 let resources = PolicySet::from_str(&format!(
2321 r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
2322 i64::MAX
2323 ))
2324 .unwrap()
2325 .query_resource(&req, &entities, &schema)
2326 .unwrap()
2327 .collect_vec();
2328 assert_eq!(&resources, &[]);
2329 }
2330
2331 #[test]
2332 fn empty_tpe() {
2333 let schema = schema();
2334 let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
2335 let req = PartialRequest::new(
2336 PartialEntityUid::new("P".parse().unwrap(), None),
2337 r#"Action::"A""#.parse().unwrap(),
2338 PartialEntityUid::new("R".parse().unwrap(), None),
2339 None,
2340 &schema,
2341 )
2342 .unwrap();
2343 let response = PolicySet::from_str(r#""#)
2344 .unwrap()
2345 .tpe(&req, &partial_entities, &schema)
2346 .unwrap();
2347 assert_eq!(response.decision(), Some(Decision::Deny));
2348 }
2349
2350 #[test]
2351 fn empty_query_principal() {
2352 let schema = schema();
2353 let entities = entities();
2354 let req = PrincipalQueryRequest::new(
2355 "P".parse().unwrap(),
2356 r#"Action::"A""#.parse().unwrap(),
2357 r#"R::"""#.parse().unwrap(),
2358 Context::empty(),
2359 &schema,
2360 )
2361 .unwrap();
2362
2363 let principals = PolicySet::from_str(r#""#)
2364 .unwrap()
2365 .query_principal(&req, &entities, &schema)
2366 .unwrap()
2367 .collect_vec();
2368 assert_eq!(&principals, &[]);
2369 }
2370
2371 #[test]
2372 fn empty_query_resource() {
2373 let schema = schema();
2374 let entities = entities();
2375 let req = ResourceQueryRequest::new(
2376 r#"P::"""#.parse().unwrap(),
2377 r#"Action::"A""#.parse().unwrap(),
2378 "R".parse().unwrap(),
2379 Context::empty(),
2380 &schema,
2381 )
2382 .unwrap();
2383
2384 let resources = PolicySet::from_str(r#""#)
2385 .unwrap()
2386 .query_resource(&req, &entities, &schema)
2387 .unwrap()
2388 .collect_vec();
2389 assert_eq!(&resources, &[]);
2390 }
2391 }
2392
2393 mod query_action {
2394 use cedar_policy_core::authorizer::Decision;
2395
2396 use crate::{
2397 ActionQueryRequest, Context, PartialEntities, PartialEntityUid, PolicySet, Schema,
2398 };
2399 use similar_asserts::assert_eq;
2400 use std::str::FromStr;
2401
2402 #[test]
2403 fn test() {
2404 let policies = PolicySet::from_str(
2405 r#"
2406 // Edit might be alowed, depending on context
2407 permit(principal, action == Action::"edit", resource)
2408 when {
2409 context.ip.isInRange(resource.allowed_edit_range)
2410 };
2411
2412 // We pass a concrete resource, so we know this will be allowed
2413 permit(principal, action == Action::"view", resource)
2414 when {
2415 resource.public
2416 };
2417
2418 // never allowed for any request
2419 forbid(principal, action == Action::"delete", resource);
2420
2421 // allowed for this action, but it doesn't apply to the request types
2422 permit(principal, action == Action::"not_on_photo", resource);
2423 "#,
2424 )
2425 .unwrap();
2426 let schema = Schema::from_str(
2427 "
2428 entity User, Other;
2429 entity Photo {
2430 public: Bool,
2431 allowed_edit_range: ipaddr,
2432 };
2433 action view, edit, delete appliesTo {
2434 principal: User,
2435 resource: Photo,
2436 context: {
2437 ip: ipaddr,
2438 }
2439 };
2440 action not_on_photo appliesTo {
2441 principal: User,
2442 resource: Other
2443 };
2444 ",
2445 )
2446 .unwrap();
2447 let entities = PartialEntities::from_json_value(
2448 serde_json::json!([
2449 {
2450 "uid": { "__entity": { "type": "Photo", "id": "vacation.jpg"} },
2451 "attrs": {
2452 "public": true,
2453 "allowed_edit_range": "192.0.2.0/24"
2454 },
2455 "parents": []
2456 },
2457 ]),
2458 &schema,
2459 )
2460 .unwrap();
2461
2462 let request = ActionQueryRequest::new(
2463 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2464 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2465 None,
2466 schema,
2467 )
2468 .unwrap();
2469
2470 let mut actions: Vec<_> = policies
2471 .query_action(&request, &entities)
2472 .unwrap()
2473 .collect();
2474 actions.sort_by_key(|(a, _)| *a);
2475 assert_eq!(
2476 actions,
2477 vec![
2478 (&r#"Action::"edit""#.parse().unwrap(), None),
2479 (&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow)),
2480 ]
2481 )
2482 }
2483
2484 #[test]
2485 fn permitted_action() {
2486 let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2487 let schema = Schema::from_str(
2488 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2489 )
2490 .unwrap();
2491 let entities = PartialEntities::empty();
2492
2493 let request = ActionQueryRequest::new(
2494 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2495 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2496 None,
2497 schema,
2498 )
2499 .unwrap();
2500
2501 let actions: Vec<_> = policies
2502 .query_action(&request, &entities)
2503 .unwrap()
2504 .collect();
2505 assert_eq!(
2506 actions,
2507 vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
2508 );
2509 }
2510
2511 #[test]
2512 fn maybe_permitted_action() {
2513 let policies = PolicySet::from_str(
2514 "permit(principal, action, resource) when { context.should_allow };",
2515 )
2516 .unwrap();
2517 let schema = Schema::from_str(
2518 "entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {should_allow: Bool}};",
2519 )
2520 .unwrap();
2521 let entities = PartialEntities::empty();
2522
2523 let request = ActionQueryRequest::new(
2524 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2525 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2526 None,
2527 schema,
2528 )
2529 .unwrap();
2530
2531 let actions: Vec<_> = policies
2532 .query_action(&request, &entities)
2533 .unwrap()
2534 .collect();
2535 assert_eq!(actions, vec![(&r#"Action::"view""#.parse().unwrap(), None)]);
2536 }
2537
2538 #[test]
2539 fn forbidden_action() {
2540 let policies = PolicySet::from_str("forbid(principal, action, resource);").unwrap();
2541 let schema = Schema::from_str(
2542 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2543 )
2544 .unwrap();
2545 let entities = PartialEntities::empty();
2546
2547 let request = ActionQueryRequest::new(
2548 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2549 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2550 None,
2551 schema,
2552 )
2553 .unwrap();
2554
2555 let actions: Vec<_> = policies
2556 .query_action(&request, &entities)
2557 .unwrap()
2558 .collect();
2559 assert_eq!(actions, Vec::new(),);
2560 }
2561
2562 #[test]
2563 fn invalid_permitted_action() {
2564 let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2565 let schema = Schema::from_str("entity User, Photo, Other; action view appliesTo { principal: User, resource: Other};").unwrap();
2566 let entities = PartialEntities::empty();
2567
2568 let request = ActionQueryRequest::new(
2569 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2570 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2571 None,
2572 schema,
2573 )
2574 .unwrap();
2575
2576 let actions: Vec<_> = policies
2577 .query_action(&request, &entities)
2578 .unwrap()
2579 .collect();
2580 assert_eq!(actions, Vec::new());
2581 }
2582
2583 #[test]
2584 fn invalid_context_permitted_action() {
2585 let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2586 let schema = Schema::from_str("entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {a: Long}};").unwrap();
2587 let entities = PartialEntities::empty();
2588
2589 let request = ActionQueryRequest::new(
2590 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2591 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2592 Some(Context::empty()),
2593 schema,
2594 )
2595 .unwrap();
2596
2597 let actions: Vec<_> = policies
2598 .query_action(&request, &entities)
2599 .unwrap()
2600 .collect();
2601 assert_eq!(actions, Vec::new());
2602 }
2603
2604 #[test]
2605 fn no_actions_in_schema() {
2606 let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
2607 let schema = Schema::from_str("entity User, Photo;").unwrap();
2608 let entities = PartialEntities::empty();
2609
2610 let request = ActionQueryRequest::new(
2611 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2612 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2613 None,
2614 schema,
2615 )
2616 .unwrap();
2617
2618 let actions: Vec<_> = policies
2619 .query_action(&request, &entities)
2620 .unwrap()
2621 .collect();
2622 assert_eq!(actions, Vec::new());
2623 }
2624
2625 #[test]
2626 fn permitted_action_error_permit() {
2627 let policies = PolicySet::from_str(&format!("permit(principal, action, resource);permit(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
2628 let schema = Schema::from_str(
2629 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2630 )
2631 .unwrap();
2632 let entities = PartialEntities::empty();
2633
2634 let request = ActionQueryRequest::new(
2635 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2636 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2637 None,
2638 schema,
2639 )
2640 .unwrap();
2641
2642 let actions: Vec<_> = policies
2643 .query_action(&request, &entities)
2644 .unwrap()
2645 .collect();
2646 assert_eq!(
2647 actions,
2648 vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
2649 );
2650 }
2651
2652 #[test]
2653 fn permitted_action_error_forbid() {
2654 let policies = PolicySet::from_str(&format!("permit(principal, action, resource);forbid(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
2655 let schema = Schema::from_str(
2656 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2657 )
2658 .unwrap();
2659 let entities = PartialEntities::empty();
2660
2661 let request = ActionQueryRequest::new(
2662 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2663 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2664 None,
2665 schema,
2666 )
2667 .unwrap();
2668
2669 let actions: Vec<_> = policies
2670 .query_action(&request, &entities)
2671 .unwrap()
2672 .collect();
2673 assert_eq!(
2674 actions,
2675 vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
2676 );
2677 }
2678
2679 #[test]
2680 fn forbidden_action_error_permit() {
2681 let policies = PolicySet::from_str(&format!(
2682 "permit(principal, action, resource) when {{ {} + 1 == 0 || true }};",
2683 i64::MAX
2684 ))
2685 .unwrap();
2686 let schema = Schema::from_str(
2687 "entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
2688 )
2689 .unwrap();
2690 let entities = PartialEntities::empty();
2691
2692 let request = ActionQueryRequest::new(
2693 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2694 PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
2695 None,
2696 schema,
2697 )
2698 .unwrap();
2699
2700 let actions: Vec<_> = policies
2701 .query_action(&request, &entities)
2702 .unwrap()
2703 .collect();
2704 assert_eq!(actions, Vec::new(),);
2705 }
2706 }
2707
2708 #[test]
2713 fn residual_error_to_pst_and_json() {
2714 use cedar_policy_core::pst;
2715 use std::str::FromStr;
2716
2717 let (schema, _) = crate::Schema::from_cedarschema_str(
2718 r#"
2719 entity User = { name: String };
2720 entity Account = { name: String, assignedTo?: User };
2721 action RevealCredentials appliesTo {
2722 principal: [User],
2723 resource: [Account],
2724 context: { flag: Bool },
2725 };
2726 "#,
2727 )
2728 .unwrap();
2729
2730 let policies = crate::PolicySet::from_str(
2731 r#"
2732 permit(
2733 principal is User,
2734 action == Action::"RevealCredentials",
2735 resource is Account
2736 ) when {
2737 context.flag &&
2738 resource has assignedTo &&
2739 resource.assignedTo == principal
2740 };
2741 "#,
2742 )
2743 .unwrap();
2744
2745 let entities = crate::Entities::from_json_value(
2748 serde_json::json!([
2749 {
2750 "uid": { "type": "User", "id": "u1" },
2751 "attrs": { "name": "alice" },
2752 "parents": []
2753 },
2754 {
2755 "uid": { "type": "Account", "id": "a1" },
2756 "attrs": { "name": "shared" },
2757 "parents": []
2758 }
2759 ]),
2760 Some(&schema),
2761 )
2762 .unwrap();
2763
2764 let partial_entities = crate::PartialEntities::from_concrete(entities, &schema).unwrap();
2765
2766 let request = crate::PartialRequest::new(
2768 crate::PartialEntityUid::from_concrete(r#"User::"u1""#.parse().unwrap()),
2769 r#"Action::"RevealCredentials""#.parse().unwrap(),
2770 crate::PartialEntityUid::from_concrete(r#"Account::"a1""#.parse().unwrap()),
2771 None,
2772 &schema,
2773 )
2774 .unwrap();
2775
2776 let response = policies
2777 .tpe(&request, &partial_entities, &schema)
2778 .expect("tpe should succeed");
2779 let residual_policies: Vec<_> = response.nontrivial_residual_policies().collect();
2781 assert_eq!(
2782 residual_policies.len(),
2783 1,
2784 "decision={:?}, all residuals: {:?}",
2785 response.decision(),
2786 response
2787 .residual_policies()
2788 .map(|p| p.to_string())
2789 .collect::<Vec<_>>()
2790 );
2791
2792 let policy = &residual_policies[0];
2793
2794 let json_res = policy.to_json();
2796 assert!(json_res.is_ok());
2797 assert!(json_res.unwrap().to_string().contains(r#"{"error":[]}"#));
2798
2799 let pst_policy = policy.to_pst().expect("to_pst should succeed");
2801 let clauses = pst_policy.body().clauses();
2802 assert_eq!(clauses.len(), 1);
2803
2804 let expr = match &clauses[0] {
2805 pst::Clause::When(e) => e,
2806 pst::Clause::Unless(_) => panic!("expected when clause"),
2807 };
2808
2809 assert!(
2812 expr.has_error(),
2813 "residual expression should contain an error node"
2814 );
2815 }
2816
2817 mod template_links {
2818 use std::{collections::HashMap, str::FromStr};
2819
2820 use crate::{
2821 pst, Decision, EntityUid, PartialEntities, PartialEntityUid, PartialRequest, Policy,
2822 PolicyId, PolicySet, Schema, SlotId, Template,
2823 };
2824
2825 fn schema() -> Schema {
2826 Schema::from_str(
2827 "entity User { age: Long }; entity Photo; action view appliesTo { principal: User, resource: Photo};",
2828 )
2829 .unwrap()
2830 }
2831
2832 fn template_policy_set() -> PolicySet {
2833 let mut policies = PolicySet::new();
2834 let template = Template::parse(
2835 Some(PolicyId::new("t0").clone()),
2836 "permit(principal == ?principal, action, resource);",
2837 )
2838 .unwrap();
2839 policies.add_template(template).unwrap();
2840 let template = Template::parse(
2841 Some(PolicyId::new("t1").clone()),
2842 "permit(principal, action, resource == ?resource);",
2843 )
2844 .unwrap();
2845 policies.add_template(template).unwrap();
2846 policies
2847 }
2848
2849 fn partial_req() -> PartialRequest {
2850 PartialRequest::new(
2851 PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
2852 r#"Action::"view""#.parse().unwrap(),
2853 PartialEntityUid::new("Photo".parse().unwrap(), None),
2854 None,
2855 &schema(),
2856 )
2857 .unwrap()
2858 }
2859
2860 #[test]
2861 fn concrete_allow() {
2862 let schema = schema();
2863 let mut policies = template_policy_set();
2864 policies
2865 .link(
2866 PolicyId::new("t0"),
2867 PolicyId::new("l"),
2868 HashMap::from([(
2869 SlotId::principal(),
2870 EntityUid::from_str(r#"User::"alice""#).unwrap(),
2871 )]),
2872 )
2873 .unwrap();
2874
2875 let request = partial_req();
2876 let es = PartialEntities::empty();
2877 let response = policies.tpe(&request, &es, &schema).unwrap();
2878
2879 assert_eq!(response.decision(), Some(Decision::Allow));
2880 }
2881
2882 #[test]
2883 fn templates_no_links_deny() {
2884 let schema = schema();
2885 let policies = template_policy_set();
2886
2887 let request = partial_req();
2888 let es = PartialEntities::empty();
2889 let response = policies.tpe(&request, &es, &schema).unwrap();
2890
2891 assert_eq!(response.decision(), Some(Decision::Deny));
2892 }
2893
2894 #[test]
2895 fn concrete_deny() {
2896 let schema = schema();
2897 let mut policies = template_policy_set();
2898 policies
2899 .link(
2900 PolicyId::new("t0"),
2901 PolicyId::new("l"),
2902 HashMap::from([(
2903 SlotId::principal(),
2904 EntityUid::from_str(r#"User::"bob""#).unwrap(),
2905 )]),
2906 )
2907 .unwrap();
2908
2909 let request = partial_req();
2910 let es = PartialEntities::empty();
2911 let response = policies.tpe(&request, &es, &schema).unwrap();
2912
2913 assert_eq!(response.decision(), Some(Decision::Deny));
2914 }
2915
2916 #[test]
2917 fn residual() {
2918 let schema = schema();
2919 let mut policies = template_policy_set();
2920 policies
2921 .link(
2922 PolicyId::new("t1"),
2923 PolicyId::new("l"),
2924 HashMap::from([(
2925 SlotId::resource(),
2926 EntityUid::from_str(r#"Photo::"p""#).unwrap(),
2927 )]),
2928 )
2929 .unwrap();
2930
2931 let request = partial_req();
2932 let es = PartialEntities::empty();
2933 let response = policies.tpe(&request, &es, &schema).unwrap();
2934
2935 let expected: pst::Policy = Policy::parse(
2936 Some(PolicyId::new("l")),
2937 r#"permit(principal, action, resource) when { resource == Photo::"p" };"#,
2938 )
2939 .unwrap()
2940 .to_pst()
2941 .unwrap();
2942
2943 let residuals: Vec<_> = response.nontrivial_residual_policies().collect();
2944 assert_eq!(residuals[0].to_pst().unwrap().body(), expected.body());
2945 assert_eq!(response.decision(), None);
2946 assert_eq!(residuals.len(), 1);
2947 }
2948 }
2949}