1#![allow(
19 clippy::missing_panics_doc,
20 clippy::missing_errors_doc,
21 clippy::similar_names
22)]
23pub use ast::Effect;
24pub use authorizer::Decision;
25use cedar_policy_core::ast;
26use cedar_policy_core::authorizer;
27use cedar_policy_core::entities;
28use cedar_policy_core::entities::JsonDeserializationErrorContext;
29use cedar_policy_core::entities::{ContextSchema, Dereference, JsonDeserializationError};
30use cedar_policy_core::est;
31use cedar_policy_core::evaluator::{Evaluator, RestrictedEvaluator};
32pub use cedar_policy_core::extensions;
33use cedar_policy_core::extensions::Extensions;
34use cedar_policy_core::parser;
35pub use cedar_policy_core::parser::err::ParseErrors;
36use cedar_policy_core::parser::SourceInfo;
37use cedar_policy_core::FromNormalizedStr;
38pub use cedar_policy_validator::{TypeErrorKind, ValidationErrorKind, ValidationWarningKind};
39use itertools::Itertools;
40use ref_cast::RefCast;
41use serde::{Deserialize, Serialize};
42use smol_str::SmolStr;
43use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
44use std::str::FromStr;
45use thiserror::Error;
46
47#[repr(transparent)]
49#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, RefCast)]
50pub struct SlotId(ast::SlotId);
51
52impl SlotId {
53 pub fn principal() -> Self {
55 Self(ast::SlotId::principal())
56 }
57
58 pub fn resource() -> Self {
60 Self(ast::SlotId::resource())
61 }
62}
63
64impl std::fmt::Display for SlotId {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}", self.0)
67 }
68}
69
70impl From<ast::SlotId> for SlotId {
71 fn from(a: ast::SlotId) -> Self {
72 Self(a)
73 }
74}
75
76impl From<SlotId> for ast::SlotId {
77 fn from(s: SlotId) -> Self {
78 s.0
79 }
80}
81
82#[repr(transparent)]
85#[derive(Debug, Clone, PartialEq, Eq, RefCast)]
86pub struct Entity(ast::Entity);
87
88impl Entity {
89 pub fn new(
111 uid: EntityUid,
112 attrs: HashMap<String, RestrictedExpression>,
113 parents: HashSet<EntityUid>,
114 ) -> Self {
115 Self(ast::Entity::new(
119 uid.0,
120 attrs
121 .into_iter()
122 .map(|(k, v)| (SmolStr::from(k), v.0))
123 .collect(),
124 parents.into_iter().map(|uid| uid.0).collect(),
125 ))
126 }
127
128 pub fn with_uid(uid: EntityUid) -> Self {
139 Self(ast::Entity::with_uid(uid.0))
141 }
142
143 pub fn uid(&self) -> EntityUid {
154 EntityUid(self.0.uid())
156 }
157
158 pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, EvaluationError>> {
178 let expr = self.0.get(attr)?;
179 let all_ext = Extensions::all_available();
180 let evaluator = RestrictedEvaluator::new(&all_ext);
181 Some(
182 evaluator
183 .interpret(expr.as_borrowed())
184 .map(EvalResult::from)
185 .map_err(|e| EvaluationError::StringMessage(e.to_string())),
186 )
187 }
188}
189
190impl std::fmt::Display for Entity {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(f, "{}", self.0)
193 }
194}
195
196#[repr(transparent)]
199#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
200pub struct Entities(pub(crate) entities::Entities);
201
202pub use entities::EntitiesError;
203
204impl Entities {
205 pub fn empty() -> Self {
211 Self(entities::Entities::new())
212 }
213
214 pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
216 match self.0.entity(&uid.0) {
217 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
218 Dereference::Data(e) => Some(Entity::ref_cast(e)),
219 }
220 }
221
222 #[must_use]
226 #[cfg(feature = "partial-eval")]
227 pub fn partial(self) -> Self {
228 Self(self.0.partial())
229 }
230
231 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
233 self.0.iter().map(Entity::ref_cast)
234 }
235
236 pub fn from_entities(
239 entities: impl IntoIterator<Item = Entity>,
240 ) -> Result<Self, entities::EntitiesError> {
241 entities::Entities::from_entities(
242 entities.into_iter().map(|e| e.0),
243 entities::TCComputation::ComputeNow,
244 )
245 .map(Entities)
246 }
247
248 pub fn from_json_str(
284 json: &str,
285 schema: Option<&Schema>,
286 ) -> Result<Self, entities::EntitiesError> {
287 let eparser = entities::EntityJsonParser::new(
288 schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
289 Extensions::all_available(),
290 entities::TCComputation::ComputeNow,
291 );
292 eparser.from_json_str(json).map(Entities)
293 }
294
295 pub fn from_json_value(
325 json: serde_json::Value,
326 schema: Option<&Schema>,
327 ) -> Result<Self, entities::EntitiesError> {
328 let eparser = entities::EntityJsonParser::new(
329 schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
330 Extensions::all_available(),
331 entities::TCComputation::ComputeNow,
332 );
333 eparser.from_json_value(json).map(Entities)
334 }
335
336 pub fn from_json_file(
343 json: impl std::io::Read,
344 schema: Option<&Schema>,
345 ) -> Result<Self, entities::EntitiesError> {
346 let eparser = entities::EntityJsonParser::new(
347 schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
348 Extensions::all_available(),
349 entities::TCComputation::ComputeNow,
350 );
351 eparser.from_json_file(json).map(Entities)
352 }
353
354 pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
357 match self.0.entity(&b.0) {
358 Dereference::Data(b) => b.is_descendant_of(&a.0),
359 _ => a == b, }
361 }
362
363 pub fn ancestors<'a>(
366 &'a self,
367 euid: &EntityUid,
368 ) -> Option<impl Iterator<Item = &'a EntityUid>> {
369 let entity = match self.0.entity(&euid.0) {
370 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
371 Dereference::Data(e) => Some(e),
372 }?;
373 Some(entity.ancestors().map(EntityUid::ref_cast))
375 }
376
377 pub fn write_to_json(
385 &self,
386 f: impl std::io::Write,
387 ) -> std::result::Result<(), entities::EntitiesError> {
388 self.0.write_to_json(f)
389 }
390}
391
392#[repr(transparent)]
394#[derive(Debug, RefCast)]
395pub struct Authorizer(authorizer::Authorizer);
396
397impl Default for Authorizer {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403impl Authorizer {
404 pub fn new() -> Self {
456 Self(authorizer::Authorizer::new())
457 }
458
459 pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
515 self.0.is_authorized(&r.0, &p.ast, &e.0).into()
516 }
517
518 #[cfg(feature = "partial-eval")]
523 pub fn is_authorized_partial(
524 &self,
525 query: &Request,
526 policy_set: &PolicySet,
527 entities: &Entities,
528 ) -> PartialResponse {
529 let response = self
530 .0
531 .is_authorized_core(&query.0, &policy_set.ast, &entities.0);
532 match response {
533 authorizer::ResponseKind::FullyEvaluated(a) => PartialResponse::Concrete(a.into()),
534 authorizer::ResponseKind::Partial(p) => PartialResponse::Residual(p.into()),
535 }
536 }
537}
538
539#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
541pub struct Response {
542 decision: Decision,
544 diagnostics: Diagnostics,
546}
547
548#[cfg(feature = "partial-eval")]
551#[derive(Debug, PartialEq, Clone)]
552pub enum PartialResponse {
553 Concrete(Response),
555 Residual(ResidualResponse),
557}
558
559#[cfg(feature = "partial-eval")]
561#[derive(Debug, PartialEq, Eq, Clone)]
562pub struct ResidualResponse {
563 residuals: PolicySet,
565 diagnostics: Diagnostics,
567}
568
569#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
571pub struct Diagnostics {
572 reason: HashSet<PolicyId>,
575 errors: HashSet<String>,
577}
578
579impl From<authorizer::Diagnostics> for Diagnostics {
580 fn from(diagnostics: authorizer::Diagnostics) -> Self {
581 Self {
582 reason: diagnostics.reason.into_iter().map(PolicyId).collect(),
583 errors: diagnostics.errors.iter().map(ToString::to_string).collect(),
584 }
585 }
586}
587
588impl Diagnostics {
589 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
645 self.reason.iter()
646 }
647
648 pub fn errors(&self) -> impl Iterator<Item = EvaluationError> + '_ {
703 self.errors
704 .iter()
705 .cloned()
706 .map(EvaluationError::StringMessage)
707 }
708}
709
710impl Response {
711 pub fn new(decision: Decision, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
713 Self {
714 decision,
715 diagnostics: Diagnostics { reason, errors },
716 }
717 }
718
719 pub fn decision(&self) -> Decision {
721 self.decision
722 }
723
724 pub fn diagnostics(&self) -> &Diagnostics {
726 &self.diagnostics
727 }
728}
729
730impl From<authorizer::Response> for Response {
731 fn from(a: authorizer::Response) -> Self {
732 Self {
733 decision: a.decision,
734 diagnostics: a.diagnostics.into(),
735 }
736 }
737}
738
739#[cfg(feature = "partial-eval")]
740impl ResidualResponse {
741 pub fn new(residuals: PolicySet, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
743 Self {
744 residuals,
745 diagnostics: Diagnostics { reason, errors },
746 }
747 }
748
749 pub fn residuals(&self) -> &PolicySet {
751 &self.residuals
752 }
753
754 pub fn diagnostics(&self) -> &Diagnostics {
756 &self.diagnostics
757 }
758}
759
760#[cfg(feature = "partial-eval")]
761impl From<authorizer::PartialResponse> for ResidualResponse {
762 fn from(p: authorizer::PartialResponse) -> Self {
763 Self {
764 residuals: PolicySet::from_ast(p.residuals),
765 diagnostics: p.diagnostics.into(),
766 }
767 }
768}
769
770#[derive(Debug, Clone, PartialEq, Eq, Error)]
773pub enum EvaluationError {
774 #[error("{0}")]
777 StringMessage(String),
778}
779
780#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
782#[non_exhaustive]
783pub enum ValidationMode {
784 #[default]
787 Strict,
788 Permissive,
790}
791
792impl From<ValidationMode> for cedar_policy_validator::ValidationMode {
793 fn from(mode: ValidationMode) -> Self {
794 match mode {
795 ValidationMode::Strict => Self::Strict,
796 ValidationMode::Permissive => Self::Permissive,
797 }
798 }
799}
800
801#[repr(transparent)]
803#[derive(Debug, RefCast)]
804pub struct Validator(cedar_policy_validator::Validator);
805
806impl Validator {
807 pub fn new(schema: Schema) -> Self {
810 Self(cedar_policy_validator::Validator::new(schema.0))
811 }
812
813 pub fn validate<'a>(
821 &'a self,
822 pset: &'a PolicySet,
823 mode: ValidationMode,
824 ) -> ValidationResult<'a> {
825 ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
826 }
827}
828
829#[derive(Debug)]
832pub struct SchemaFragment(cedar_policy_validator::ValidatorSchemaFragment);
833
834impl SchemaFragment {
835 pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
839 self.0
840 .namespaces()
841 .map(|ns| ns.as_ref().map(|ns| EntityNamespace(ns.clone())))
842 }
843
844 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
847 Ok(Self(
848 cedar_policy_validator::SchemaFragment::from_json_value(json)?.try_into()?,
849 ))
850 }
851
852 pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
854 Ok(Self(
855 cedar_policy_validator::SchemaFragment::from_file(file)?.try_into()?,
856 ))
857 }
858}
859
860impl TryInto<Schema> for SchemaFragment {
861 type Error = SchemaError;
862
863 fn try_into(self) -> Result<Schema, Self::Error> {
867 Ok(Schema(
868 cedar_policy_validator::ValidatorSchema::from_schema_fragments([self.0])?,
869 ))
870 }
871}
872
873impl FromStr for SchemaFragment {
874 type Err = SchemaError;
875 fn from_str(src: &str) -> Result<Self, Self::Err> {
882 Ok(Self(
883 serde_json::from_str::<cedar_policy_validator::SchemaFragment>(src)
884 .map_err(cedar_policy_validator::SchemaError::from)?
885 .try_into()?,
886 ))
887 }
888}
889
890#[repr(transparent)]
892#[derive(Debug, Clone, RefCast)]
893pub struct Schema(pub(crate) cedar_policy_validator::ValidatorSchema);
894
895impl FromStr for Schema {
896 type Err = SchemaError;
897
898 fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
905 Ok(Self(schema_src.parse()?))
906 }
907}
908
909impl Schema {
910 pub fn from_schema_fragments(
915 fragments: impl IntoIterator<Item = SchemaFragment>,
916 ) -> Result<Self, SchemaError> {
917 Ok(Self(
918 cedar_policy_validator::ValidatorSchema::from_schema_fragments(
919 fragments.into_iter().map(|f| f.0),
920 )?,
921 ))
922 }
923
924 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
927 Ok(Self(
928 cedar_policy_validator::ValidatorSchema::from_json_value(json)?,
929 ))
930 }
931
932 pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
934 Ok(Self(cedar_policy_validator::ValidatorSchema::from_file(
935 file,
936 )?))
937 }
938
939 pub fn action_entities(&self) -> Result<Entities, entities::EntitiesError> {
942 Ok(Entities(self.0.action_entities()?))
943 }
944}
945
946#[derive(Debug, Error)]
948pub enum SchemaError {
949 #[error("JSON Schema file could not be parsed: {0}")]
951 ParseJson(serde_json::Error),
952 #[error("Transitive closure error on action hierarchy: {0}")]
955 ActionTransitiveClosureError(String),
956 #[error("Transitive closure error on entity hierarchy: {0}")]
959 EntityTransitiveClosureError(String),
960 #[error("Unsupported feature used in schema: {0}")]
963 UnsupportedSchemaFeature(String),
964 #[error("Undeclared entity types: {0:?}")]
967 UndeclaredEntityTypes(HashSet<String>),
968 #[error("Undeclared actions: {0:?}")]
970 UndeclaredActions(HashSet<String>),
971 #[error("Undeclared common types: {0:?}")]
973 UndeclaredCommonType(HashSet<String>),
974 #[error("Duplicate entity type {0}")]
977 DuplicateEntityType(String),
978 #[error("Duplicate action {0}")]
981 DuplicateAction(String),
982 #[error("Duplicate common type {0}")]
985 DuplicateCommonType(String),
986 #[error("Cycle in action hierarchy")]
988 CycleInActionHierarchy,
989 #[error("Parse error in entity type: {0}")]
991 EntityTypeParse(ParseErrors),
992 #[error("Parse error in namespace identifier: {0}")]
994 NamespaceParse(ParseErrors),
995 #[error("Parse error in common type identifier: {0}")]
997 CommonTypeParseError(ParseErrors),
998 #[error("Parse error in extension type: {0}")]
1000 ExtensionTypeParse(ParseErrors),
1001 #[error("Entity type `Action` declared in `entityTypes` list.")]
1006 ActionEntityTypeDeclared,
1007 #[error("Actions declared with `attributes`: [{}]", .0.iter().map(String::as_str).join(", "))]
1010 ActionEntityAttributes(Vec<String>),
1011 #[error("Action context or entity type shape is not a record")]
1014 ContextOrShapeNotRecord,
1015 #[error("Action attribute is an empty set")]
1017 ActionEntityAttributeEmptySet,
1018 #[error(
1020 "Action has an attribute of unsupported type (escaped expression, entity or extension)"
1021 )]
1022 ActionEntityAttributeUnsupportedType,
1023}
1024
1025#[doc(hidden)]
1026impl From<cedar_policy_validator::SchemaError> for SchemaError {
1027 fn from(value: cedar_policy_validator::SchemaError) -> Self {
1028 match value {
1029 cedar_policy_validator::SchemaError::Serde(e) => Self::ParseJson(e),
1030 cedar_policy_validator::SchemaError::ActionTransitiveClosure(e) => {
1031 Self::ActionTransitiveClosureError(e.to_string())
1032 }
1033 cedar_policy_validator::SchemaError::EntityTypeTransitiveClosure(e) => {
1034 Self::EntityTransitiveClosureError(e.to_string())
1035 }
1036 cedar_policy_validator::SchemaError::UnsupportedFeature(e) => {
1037 Self::UnsupportedSchemaFeature(e.to_string())
1038 }
1039 cedar_policy_validator::SchemaError::UndeclaredEntityTypes(e) => {
1040 Self::UndeclaredEntityTypes(e)
1041 }
1042 cedar_policy_validator::SchemaError::UndeclaredActions(e) => Self::UndeclaredActions(e),
1043 cedar_policy_validator::SchemaError::UndeclaredCommonTypes(c) => {
1044 Self::UndeclaredCommonType(c)
1045 }
1046 cedar_policy_validator::SchemaError::DuplicateEntityType(e) => {
1047 Self::DuplicateEntityType(e)
1048 }
1049 cedar_policy_validator::SchemaError::DuplicateAction(e) => Self::DuplicateAction(e),
1050 cedar_policy_validator::SchemaError::DuplicateCommonType(c) => {
1051 Self::DuplicateCommonType(c)
1052 }
1053 cedar_policy_validator::SchemaError::CycleInActionHierarchy => {
1054 Self::CycleInActionHierarchy
1055 }
1056 cedar_policy_validator::SchemaError::EntityTypeParseError(e) => {
1057 Self::EntityTypeParse(e)
1058 }
1059 cedar_policy_validator::SchemaError::NamespaceParseError(e) => Self::NamespaceParse(e),
1060 cedar_policy_validator::SchemaError::CommonTypeParseError(e) => {
1061 Self::CommonTypeParseError(e)
1062 }
1063 cedar_policy_validator::SchemaError::ExtensionTypeParseError(e) => {
1064 Self::ExtensionTypeParse(e)
1065 }
1066 cedar_policy_validator::SchemaError::ActionEntityTypeDeclared => {
1067 Self::ActionEntityTypeDeclared
1068 }
1069 cedar_policy_validator::SchemaError::ActionHasAttributes(e) => {
1070 Self::ActionEntityAttributes(e)
1071 }
1072 cedar_policy_validator::SchemaError::ContextOrShapeNotRecord(_) => {
1073 Self::ContextOrShapeNotRecord
1074 }
1075 cedar_policy_validator::SchemaError::ActionAttributesContainEmptySet(_) => {
1076 Self::ActionEntityAttributeEmptySet
1077 }
1078 cedar_policy_validator::SchemaError::UnsupportedActionAttributeType(_) => {
1079 Self::ActionEntityAttributeUnsupportedType
1080 }
1081 }
1082 }
1083}
1084
1085#[derive(Debug)]
1090pub struct ValidationResult<'a> {
1091 validation_errors: Vec<ValidationError<'a>>,
1092}
1093
1094impl<'a> ValidationResult<'a> {
1095 pub fn validation_passed(&self) -> bool {
1097 self.validation_errors.is_empty()
1098 }
1099
1100 pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError<'a>> {
1102 self.validation_errors.iter()
1103 }
1104}
1105
1106impl<'a> From<cedar_policy_validator::ValidationResult<'a>> for ValidationResult<'a> {
1107 fn from(r: cedar_policy_validator::ValidationResult<'a>) -> Self {
1108 Self {
1109 validation_errors: r
1110 .into_validation_errors()
1111 .map(ValidationError::from)
1112 .collect(),
1113 }
1114 }
1115}
1116
1117#[derive(Debug, Error)]
1122pub struct ValidationError<'a> {
1123 location: SourceLocation<'a>,
1124 error_kind: ValidationErrorKind,
1125}
1126
1127impl<'a> ValidationError<'a> {
1128 pub fn error_kind(&self) -> &ValidationErrorKind {
1130 &self.error_kind
1131 }
1132
1133 pub fn location(&self) -> &SourceLocation<'a> {
1135 &self.location
1136 }
1137}
1138
1139impl<'a> From<cedar_policy_validator::ValidationError<'a>> for ValidationError<'a> {
1140 fn from(err: cedar_policy_validator::ValidationError<'a>) -> Self {
1141 let (location, error_kind) = err.into_location_and_error_kind();
1142 Self {
1143 location: SourceLocation::from(location),
1144 error_kind,
1145 }
1146 }
1147}
1148
1149impl<'a> std::fmt::Display for ValidationError<'a> {
1150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1151 write!(f, "Validation error on policy {}", self.location.policy_id)?;
1152 if let (Some(range_start), Some(range_end)) =
1153 (self.location().range_start(), self.location().range_end())
1154 {
1155 write!(f, " at offset {range_start}-{range_end}")?;
1156 }
1157 write!(f, ": {}", self.error_kind())
1158 }
1159}
1160
1161#[derive(Debug, Clone, Eq, PartialEq)]
1163pub struct SourceLocation<'a> {
1164 policy_id: &'a PolicyId,
1165 source_range: Option<SourceInfo>,
1166}
1167
1168impl<'a> SourceLocation<'a> {
1169 pub fn policy_id(&self) -> &'a PolicyId {
1171 self.policy_id
1172 }
1173
1174 pub fn range_start(&self) -> Option<usize> {
1177 self.source_range.as_ref().map(SourceInfo::range_start)
1178 }
1179
1180 pub fn range_end(&self) -> Option<usize> {
1183 self.source_range.as_ref().map(SourceInfo::range_end)
1184 }
1185}
1186
1187impl<'a> From<cedar_policy_validator::SourceLocation<'a>> for SourceLocation<'a> {
1188 fn from(loc: cedar_policy_validator::SourceLocation<'a>) -> SourceLocation<'a> {
1189 let policy_id: &'a PolicyId = PolicyId::ref_cast(loc.policy_id());
1190 let source_range = loc.into_source_info();
1191 Self {
1192 policy_id,
1193 source_range,
1194 }
1195 }
1196}
1197
1198pub fn confusable_string_checker<'a>(
1200 templates: impl Iterator<Item = &'a Template>,
1201) -> impl Iterator<Item = ValidationWarning<'a>> {
1202 cedar_policy_validator::confusable_string_checks(templates.map(|t| &t.ast))
1203 .map(std::convert::Into::into)
1204}
1205
1206#[derive(Debug, Error)]
1207#[error("Warning on policy {}: {}", .location.policy_id, .kind)]
1208pub struct ValidationWarning<'a> {
1210 location: SourceLocation<'a>,
1211 kind: ValidationWarningKind,
1212}
1213
1214impl<'a> ValidationWarning<'a> {
1215 pub fn warning_kind(&self) -> &ValidationWarningKind {
1217 &self.kind
1218 }
1219
1220 pub fn location(&self) -> &SourceLocation<'a> {
1222 &self.location
1223 }
1224}
1225
1226#[doc(hidden)]
1227impl<'a> From<cedar_policy_validator::ValidationWarning<'a>> for ValidationWarning<'a> {
1228 fn from(w: cedar_policy_validator::ValidationWarning<'a>) -> Self {
1229 let (loc, kind) = w.to_kind_and_location();
1230 ValidationWarning {
1231 location: SourceLocation {
1232 policy_id: PolicyId::ref_cast(loc),
1233 source_range: None,
1234 },
1235 kind,
1236 }
1237 }
1238}
1239
1240#[repr(transparent)]
1242#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1243pub struct EntityId(ast::Eid);
1244
1245impl FromStr for EntityId {
1246 type Err = ParseErrors;
1247 fn from_str(eid_str: &str) -> Result<Self, Self::Err> {
1248 Ok(Self(ast::Eid::new(eid_str)))
1249 }
1250}
1251
1252impl AsRef<str> for EntityId {
1253 fn as_ref(&self) -> &str {
1254 self.0.as_ref()
1255 }
1256}
1257
1258impl std::fmt::Display for EntityId {
1262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1263 write!(f, "{}", self.0)
1264 }
1265}
1266
1267#[repr(transparent)]
1269#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1270pub struct EntityTypeName(ast::Name);
1271
1272impl EntityTypeName {
1273 pub fn basename(&self) -> &str {
1281 self.0.basename().as_ref()
1282 }
1283
1284 pub fn namespace_components(&self) -> impl Iterator<Item = &str> {
1295 self.0.namespace_components().map(AsRef::as_ref)
1296 }
1297
1298 pub fn namespace(&self) -> String {
1307 self.0.namespace()
1308 }
1309}
1310
1311impl FromStr for EntityTypeName {
1312 type Err = ParseErrors;
1313
1314 fn from_str(namespace_type_str: &str) -> Result<Self, Self::Err> {
1315 ast::Name::from_normalized_str(namespace_type_str).map(EntityTypeName)
1316 }
1317}
1318
1319impl std::fmt::Display for EntityTypeName {
1320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1321 write!(f, "{}", self.0)
1322 }
1323}
1324
1325#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
1327pub struct EntityNamespace(ast::Name);
1328
1329impl FromStr for EntityNamespace {
1332 type Err = ParseErrors;
1333
1334 fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
1335 ast::Name::from_normalized_str(namespace_str).map(EntityNamespace)
1336 }
1337}
1338
1339impl std::fmt::Display for EntityNamespace {
1340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1341 write!(f, "{}", self.0)
1342 }
1343}
1344
1345#[repr(transparent)]
1348#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1349pub struct EntityUid(ast::EntityUID);
1350
1351impl EntityUid {
1352 pub fn type_name(&self) -> &EntityTypeName {
1361 #[allow(clippy::panic)]
1363 match self.0.entity_type() {
1364 ast::EntityType::Unspecified => panic!("Impossible to have an unspecified entity"),
1365 ast::EntityType::Concrete(name) => EntityTypeName::ref_cast(name),
1366 }
1367 }
1368
1369 pub fn id(&self) -> &EntityId {
1378 EntityId::ref_cast(self.0.eid())
1379 }
1380
1381 pub fn from_type_name_and_id(name: EntityTypeName, id: EntityId) -> Self {
1393 Self(ast::EntityUID::from_components(name.0, id.0))
1395 }
1396
1397 pub fn from_json(json: serde_json::Value) -> Result<Self, impl std::error::Error> {
1407 let parsed: entities::EntityUidJSON = serde_json::from_value(json)?;
1408 Ok::<Self, entities::JsonDeserializationError>(Self(
1410 parsed.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
1411 ))
1412 }
1413
1414 #[cfg(test)]
1416 pub(crate) fn from_strs(typename: &str, id: &str) -> Self {
1417 Self::from_type_name_and_id(
1418 EntityTypeName::from_str(typename).unwrap(),
1419 EntityId::from_str(id).unwrap(),
1420 )
1421 }
1422}
1423
1424impl FromStr for EntityUid {
1425 type Err = ParseErrors;
1426
1427 fn from_str(uid_str: &str) -> Result<Self, Self::Err> {
1451 ast::EntityUID::from_normalized_str(uid_str).map(EntityUid)
1453 }
1454}
1455
1456impl std::fmt::Display for EntityUid {
1457 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1458 write!(f, "{}", self.0)
1459 }
1460}
1461
1462#[derive(Error, Debug)]
1464#[non_exhaustive]
1465pub enum PolicySetError {
1466 #[error("duplicate template or policy id")]
1469 AlreadyDefined,
1470 #[error("unable to link template: {0}")]
1472 LinkingError(#[from] ast::LinkingError),
1473 #[error("expected a static policy, but a template-linked policy was provided")]
1475 ExpectedStatic,
1476 #[error("expected a template, but a static policy was provided")]
1478 ExpectedTemplate,
1479}
1480
1481impl From<ast::PolicySetError> for PolicySetError {
1482 fn from(e: ast::PolicySetError) -> Self {
1483 match e {
1484 ast::PolicySetError::Occupied => Self::AlreadyDefined,
1485 }
1486 }
1487}
1488
1489impl From<ast::UnexpectedSlotError> for PolicySetError {
1490 fn from(_: ast::UnexpectedSlotError) -> Self {
1491 Self::ExpectedStatic
1492 }
1493}
1494
1495#[derive(Debug, Clone, Default)]
1497pub struct PolicySet {
1498 pub(crate) ast: ast::PolicySet,
1501 policies: HashMap<PolicyId, Policy>,
1503 templates: HashMap<PolicyId, Template>,
1505}
1506
1507impl PartialEq for PolicySet {
1508 fn eq(&self, other: &Self) -> bool {
1509 self.ast.eq(&other.ast)
1511 }
1512}
1513impl Eq for PolicySet {}
1514
1515impl FromStr for PolicySet {
1516 type Err = ParseErrors;
1517
1518 fn from_str(policies: &str) -> Result<Self, Self::Err> {
1525 let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
1526 #[allow(clippy::expect_used)]
1528 let policies = pset.policies().map(|p|
1529 (
1530 PolicyId(p.id().clone()),
1531 Policy { lossless: LosslessPolicy::policy_or_template_text(*texts.get(p.id()).expect("internal invariant violation: policy id exists in asts but not texts")), ast: p.clone() }
1532 )
1533 ).collect();
1534 #[allow(clippy::expect_used)]
1536 let templates = pset.templates().map(|t|
1537 (
1538 PolicyId(t.id().clone()),
1539 Template { lossless: LosslessPolicy::policy_or_template_text(*texts.get(t.id()).expect("internal invariant violation: template id exists in asts but not ests")), ast: t.clone() }
1540 )
1541 ).collect();
1542 Ok(Self {
1543 ast: pset,
1544 policies,
1545 templates,
1546 })
1547 }
1548}
1549
1550impl PolicySet {
1551 pub fn new() -> Self {
1553 Self {
1554 ast: ast::PolicySet::new(),
1555 policies: HashMap::new(),
1556 templates: HashMap::new(),
1557 }
1558 }
1559
1560 pub fn from_policies(
1562 policies: impl IntoIterator<Item = Policy>,
1563 ) -> Result<Self, PolicySetError> {
1564 let mut set = Self::new();
1565 for policy in policies {
1566 set.add(policy)?;
1567 }
1568 Ok(set)
1569 }
1570
1571 pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
1576 if policy.is_static() {
1577 let id = PolicyId(policy.ast.id().clone());
1578 self.ast.add(policy.ast.clone())?;
1579 self.policies.insert(id, policy);
1580 Ok(())
1581 } else {
1582 Err(PolicySetError::ExpectedStatic)
1583 }
1584 }
1585
1586 pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
1589 let id = PolicyId(template.ast.id().clone());
1590 self.ast.add_template(template.ast.clone())?;
1591 self.templates.insert(id, template);
1592 Ok(())
1593 }
1594
1595 pub fn policies(&self) -> impl Iterator<Item = &Policy> {
1599 self.policies.values()
1600 }
1601
1602 pub fn templates(&self) -> impl Iterator<Item = &Template> {
1604 self.templates.values()
1605 }
1606
1607 pub fn template(&self, id: &PolicyId) -> Option<&Template> {
1609 self.templates.get(id)
1610 }
1611
1612 pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
1614 self.policies.get(id)
1615 }
1616
1617 pub fn annotation<'a>(&'a self, id: &PolicyId, key: impl AsRef<str>) -> Option<&'a str> {
1619 self.ast
1620 .get(&id.0)?
1621 .annotation(&key.as_ref().parse().ok()?)
1622 .map(smol_str::SmolStr::as_str)
1623 }
1624
1625 pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<String> {
1627 self.ast
1628 .get_template(&id.0)?
1629 .annotation(&key.as_ref().parse().ok()?)
1630 .map(smol_str::SmolStr::to_string)
1631 }
1632
1633 pub fn is_empty(&self) -> bool {
1635 debug_assert_eq!(
1636 self.ast.is_empty(),
1637 self.policies.is_empty() && self.templates.is_empty()
1638 );
1639 self.ast.is_empty()
1640 }
1641
1642 #[allow(clippy::needless_pass_by_value)]
1649 pub fn link(
1650 &mut self,
1651 template_id: PolicyId,
1652 new_id: PolicyId,
1653 vals: HashMap<SlotId, EntityUid>,
1654 ) -> Result<(), PolicySetError> {
1655 let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
1656 .into_iter()
1657 .map(|(key, value)| (key.into(), value.0))
1658 .collect();
1659
1660 let template = match self.templates.get(&template_id) {
1665 Some(template) => template,
1666 None => {
1667 return Err(if self.policies.contains_key(&template_id) {
1668 PolicySetError::ExpectedTemplate
1669 } else {
1670 PolicySetError::LinkingError(ast::LinkingError::NoSuchTemplate(template_id.0))
1671 });
1672 }
1673 };
1674
1675 let linked_ast = self
1676 .ast
1677 .link(
1678 template_id.0.clone(),
1679 new_id.0.clone(),
1680 unwrapped_vals.clone(),
1681 )
1682 .map_err(PolicySetError::LinkingError)?;
1683
1684 #[allow(clippy::expect_used)]
1686 let linked_lossless = template
1687 .lossless
1688 .clone()
1689 .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
1690 .expect("ast.link() didn't fail above, so this shouldn't fail");
1695 self.policies.insert(
1696 new_id,
1697 Policy {
1698 ast: linked_ast.clone(),
1699 lossless: linked_lossless,
1700 },
1701 );
1702 Ok(())
1703 }
1704
1705 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1711 fn from_ast(ast: ast::PolicySet) -> Self {
1712 let policies = ast
1713 .policies()
1714 .map(|p| (PolicyId(p.id().clone()), Policy::from_ast(p.clone())))
1715 .collect();
1716 let templates = ast
1717 .templates()
1718 .map(|t| (PolicyId(t.id().clone()), Template::from_ast(t.clone())))
1719 .collect();
1720 Self {
1721 ast,
1722 policies,
1723 templates,
1724 }
1725 }
1726}
1727
1728impl std::fmt::Display for PolicySet {
1729 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1730 write!(f, "{}", self.ast)
1731 }
1732}
1733
1734#[derive(Debug, Clone)]
1736pub struct Template {
1737 ast: ast::Template,
1740
1741 lossless: LosslessPolicy,
1752}
1753
1754impl PartialEq for Template {
1755 fn eq(&self, other: &Self) -> bool {
1756 self.ast.eq(&other.ast)
1758 }
1759}
1760impl Eq for Template {}
1761
1762impl Template {
1763 pub fn parse(id: Option<String>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
1768 let ast = parser::parse_policy_template(id, src.as_ref())?;
1769 Ok(Self {
1770 ast,
1771 lossless: LosslessPolicy::policy_or_template_text(src.as_ref()),
1772 })
1773 }
1774
1775 pub fn id(&self) -> &PolicyId {
1777 PolicyId::ref_cast(self.ast.id())
1778 }
1779
1780 #[must_use]
1782 pub fn new_id(&self, id: PolicyId) -> Self {
1783 Self {
1784 ast: self.ast.new_id(id.0),
1785 lossless: self.lossless.clone(), }
1787 }
1788
1789 pub fn effect(&self) -> Effect {
1791 self.ast.effect()
1792 }
1793
1794 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
1796 self.ast
1797 .annotation(&key.as_ref().parse().ok()?)
1798 .map(smol_str::SmolStr::as_str)
1799 }
1800
1801 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
1803 self.ast
1804 .annotations()
1805 .map(|(k, v)| (k.as_ref(), v.as_str()))
1806 }
1807
1808 pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
1810 self.ast.slots().map(SlotId::ref_cast)
1811 }
1812
1813 pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
1815 match self.ast.principal_constraint().as_inner() {
1816 ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
1817 ast::PrincipalOrResourceConstraint::In(eref) => {
1818 TemplatePrincipalConstraint::In(match eref {
1819 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1820 ast::EntityReference::Slot => None,
1821 })
1822 }
1823 ast::PrincipalOrResourceConstraint::Eq(eref) => {
1824 TemplatePrincipalConstraint::Eq(match eref {
1825 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1826 ast::EntityReference::Slot => None,
1827 })
1828 }
1829 }
1830 }
1831
1832 pub fn action_constraint(&self) -> ActionConstraint {
1834 match self.ast.action_constraint() {
1836 ast::ActionConstraint::Any => ActionConstraint::Any,
1837 ast::ActionConstraint::In(ids) => ActionConstraint::In(
1838 ids.iter()
1839 .map(|id| EntityUid(id.as_ref().clone()))
1840 .collect(),
1841 ),
1842 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid(id.as_ref().clone())),
1843 }
1844 }
1845
1846 pub fn resource_constraint(&self) -> TemplateResourceConstraint {
1848 match self.ast.resource_constraint().as_inner() {
1849 ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
1850 ast::PrincipalOrResourceConstraint::In(eref) => {
1851 TemplateResourceConstraint::In(match eref {
1852 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1853 ast::EntityReference::Slot => None,
1854 })
1855 }
1856 ast::PrincipalOrResourceConstraint::Eq(eref) => {
1857 TemplateResourceConstraint::Eq(match eref {
1858 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1859 ast::EntityReference::Slot => None,
1860 })
1861 }
1862 }
1863 }
1864
1865 #[allow(dead_code)] fn from_json(
1871 id: Option<PolicyId>,
1872 json: serde_json::Value,
1873 ) -> Result<Self, cedar_policy_core::est::EstToAstError> {
1874 let est: est::Policy =
1875 serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
1876 Ok(Self {
1877 ast: est.clone().try_into_ast_template(id.map(|id| id.0))?,
1878 lossless: LosslessPolicy::Est(est),
1879 })
1880 }
1881
1882 #[allow(dead_code)] fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
1885 let est = self.lossless.est()?;
1886 let json = serde_json::to_value(est)?;
1887 Ok::<_, PolicyToJsonError>(json)
1888 }
1889
1890 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1896 fn from_ast(ast: ast::Template) -> Self {
1897 let text = ast.to_string(); Self {
1899 ast,
1900 lossless: LosslessPolicy::policy_or_template_text(text),
1901 }
1902 }
1903}
1904
1905impl FromStr for Template {
1906 type Err = ParseErrors;
1907
1908 fn from_str(src: &str) -> Result<Self, Self::Err> {
1909 Self::parse(None, src)
1910 }
1911}
1912
1913#[derive(Debug, Clone, PartialEq, Eq)]
1915pub enum PrincipalConstraint {
1916 Any,
1918 In(EntityUid),
1920 Eq(EntityUid),
1922}
1923
1924#[derive(Debug, Clone, PartialEq, Eq)]
1926pub enum TemplatePrincipalConstraint {
1927 Any,
1929 In(Option<EntityUid>),
1932 Eq(Option<EntityUid>),
1935}
1936
1937impl TemplatePrincipalConstraint {
1938 pub fn has_slot(&self) -> bool {
1940 match self {
1941 Self::Any => false,
1942 Self::In(o) | Self::Eq(o) => o.is_none(),
1943 }
1944 }
1945}
1946
1947#[derive(Debug, Clone, PartialEq, Eq)]
1949pub enum ActionConstraint {
1950 Any,
1952 In(Vec<EntityUid>),
1954 Eq(EntityUid),
1956}
1957
1958#[derive(Debug, Clone, PartialEq, Eq)]
1960pub enum ResourceConstraint {
1961 Any,
1963 In(EntityUid),
1965 Eq(EntityUid),
1967}
1968
1969#[derive(Debug, Clone, PartialEq, Eq)]
1971pub enum TemplateResourceConstraint {
1972 Any,
1974 In(Option<EntityUid>),
1977 Eq(Option<EntityUid>),
1980}
1981
1982impl TemplateResourceConstraint {
1983 pub fn has_slot(&self) -> bool {
1985 match self {
1986 Self::Any => false,
1987 Self::In(o) | Self::Eq(o) => o.is_none(),
1988 }
1989 }
1990}
1991
1992#[repr(transparent)]
1994#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize, RefCast)]
1995pub struct PolicyId(ast::PolicyID);
1996
1997impl FromStr for PolicyId {
1998 type Err = ParseErrors;
1999
2000 fn from_str(id: &str) -> Result<Self, Self::Err> {
2002 Ok(Self(ast::PolicyID::from_string(id)))
2003 }
2004}
2005
2006impl std::fmt::Display for PolicyId {
2007 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2008 write!(f, "{}", self.0)
2009 }
2010}
2011
2012#[derive(Debug, Clone)]
2014pub struct Policy {
2015 ast: ast::Policy,
2018 lossless: LosslessPolicy,
2026}
2027
2028impl PartialEq for Policy {
2029 fn eq(&self, other: &Self) -> bool {
2030 self.ast.eq(&other.ast)
2032 }
2033}
2034impl Eq for Policy {}
2035
2036impl Policy {
2037 pub fn template_id(&self) -> Option<&PolicyId> {
2040 if self.is_static() {
2041 None
2042 } else {
2043 Some(PolicyId::ref_cast(self.ast.template().id()))
2044 }
2045 }
2046
2047 pub fn effect(&self) -> Effect {
2049 self.ast.effect()
2050 }
2051
2052 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
2054 self.ast
2055 .annotation(&key.as_ref().parse().ok()?)
2056 .map(smol_str::SmolStr::as_str)
2057 }
2058
2059 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
2061 self.ast
2062 .annotations()
2063 .map(|(k, v)| (k.as_ref(), v.as_str()))
2064 }
2065
2066 pub fn id(&self) -> &PolicyId {
2068 PolicyId::ref_cast(self.ast.id())
2069 }
2070
2071 #[must_use]
2073 pub fn new_id(&self, id: PolicyId) -> Self {
2074 Self {
2075 ast: self.ast.new_id(id.0),
2076 lossless: self.lossless.clone(), }
2078 }
2079
2080 pub fn is_static(&self) -> bool {
2082 self.ast.is_static()
2083 }
2084
2085 pub fn principal_constraint(&self) -> PrincipalConstraint {
2087 let slot_id = ast::SlotId::principal();
2088 match self.ast.template().principal_constraint().as_inner() {
2089 ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
2090 ast::PrincipalOrResourceConstraint::In(eref) => {
2091 PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
2092 }
2093 ast::PrincipalOrResourceConstraint::Eq(eref) => {
2094 PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
2095 }
2096 }
2097 }
2098
2099 pub fn action_constraint(&self) -> ActionConstraint {
2101 match self.ast.template().action_constraint() {
2104 ast::ActionConstraint::Any => ActionConstraint::Any,
2105 ast::ActionConstraint::In(ids) => ActionConstraint::In(
2106 ids.iter()
2107 .map(|euid| EntityUid::ref_cast(euid.as_ref()))
2108 .cloned()
2109 .collect(),
2110 ),
2111 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
2112 }
2113 }
2114
2115 pub fn resource_constraint(&self) -> ResourceConstraint {
2117 let slot_id = ast::SlotId::resource();
2118 match self.ast.template().resource_constraint().as_inner() {
2119 ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
2120 ast::PrincipalOrResourceConstraint::In(eref) => {
2121 ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
2122 }
2123 ast::PrincipalOrResourceConstraint::Eq(eref) => {
2124 ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
2125 }
2126 }
2127 }
2128
2129 fn convert_entity_reference<'a>(
2136 &'a self,
2137 r: &'a ast::EntityReference,
2138 slot: ast::SlotId,
2139 ) -> &'a EntityUid {
2140 match r {
2141 ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
2143 #[allow(clippy::unwrap_used)]
2145 ast::EntityReference::Slot => EntityUid::ref_cast(self.ast.env().get(&slot).unwrap()),
2146 }
2147 }
2148
2149 pub fn parse(id: Option<String>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
2154 let inline_ast = parser::parse_policy(id, policy_src.as_ref())?;
2155 let (_, ast) = ast::Template::link_static_policy(inline_ast);
2156 Ok(Self {
2157 ast,
2158 lossless: LosslessPolicy::policy_or_template_text(policy_src.as_ref()),
2159 })
2160 }
2161
2162 pub fn from_json(
2229 id: Option<PolicyId>,
2230 json: serde_json::Value,
2231 ) -> Result<Self, cedar_policy_core::est::EstToAstError> {
2232 let est: est::Policy =
2233 serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
2234 Ok(Self {
2235 ast: est.clone().try_into_ast_policy(id.map(|id| id.0))?,
2236 lossless: LosslessPolicy::Est(est),
2237 })
2238 }
2239
2240 pub fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
2259 let est = self.lossless.est()?;
2260 let json = serde_json::to_value(est)?;
2261 Ok::<_, PolicyToJsonError>(json)
2262 }
2263
2264 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
2270 fn from_ast(ast: ast::Policy) -> Self {
2271 let text = ast.to_string(); Self {
2273 ast,
2274 lossless: LosslessPolicy::policy_or_template_text(text),
2275 }
2276 }
2277}
2278
2279impl std::fmt::Display for Policy {
2280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2281 self.ast.fmt(f)
2282 }
2283}
2284
2285impl FromStr for Policy {
2286 type Err = ParseErrors;
2287 fn from_str(policy: &str) -> Result<Self, Self::Err> {
2295 Self::parse(None, policy)
2296 }
2297}
2298
2299#[derive(Debug, Clone)]
2303enum LosslessPolicy {
2304 Est(est::Policy),
2306 Text {
2308 text: String,
2310 slots: HashMap<ast::SlotId, ast::EntityUID>,
2314 },
2315}
2316
2317impl LosslessPolicy {
2318 fn policy_or_template_text(text: impl Into<String>) -> Self {
2320 Self::Text {
2321 text: text.into(),
2322 slots: HashMap::new(),
2323 }
2324 }
2325
2326 fn est(&self) -> Result<est::Policy, PolicyToJsonError> {
2328 match self {
2329 Self::Est(est) => Ok(est.clone()),
2330 Self::Text { text, slots } => {
2331 let est = parser::parse_policy_or_template_to_est(text)?;
2332 if slots.is_empty() {
2333 Ok(est)
2334 } else {
2335 let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
2336 Ok(est.link(&unwrapped_vals)?)
2337 }
2338 }
2339 }
2340 }
2341
2342 fn link<'a>(
2343 self,
2344 vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
2345 ) -> Result<Self, est::InstantiationError> {
2346 match self {
2347 Self::Est(est) => {
2348 let unwrapped_est_vals: HashMap<ast::SlotId, entities::EntityUidJSON> =
2349 vals.into_iter().map(|(k, v)| (k, v.into())).collect();
2350 Ok(Self::Est(est.link(&unwrapped_est_vals)?))
2351 }
2352 Self::Text { text, slots } => {
2353 debug_assert!(
2354 slots.is_empty(),
2355 "shouldn't call link() on an already-linked policy"
2356 );
2357 let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
2358 Ok(Self::Text { text, slots })
2359 }
2360 }
2361 }
2362}
2363
2364#[derive(Debug, Error)]
2366pub enum PolicyToJsonError {
2367 #[error(transparent)]
2369 Parse(#[from] ParseErrors),
2370 #[error(transparent)]
2372 Link(#[from] est::InstantiationError),
2373 #[error(transparent)]
2375 Serde(#[from] serde_json::Error),
2376}
2377
2378#[repr(transparent)]
2380#[derive(Debug, Clone, RefCast)]
2381pub struct Expression(ast::Expr);
2382
2383impl Expression {
2384 pub fn new_string(value: String) -> Self {
2386 Self(ast::Expr::val(value))
2387 }
2388
2389 pub fn new_bool(value: bool) -> Self {
2391 Self(ast::Expr::val(value))
2392 }
2393
2394 pub fn new_long(value: i64) -> Self {
2396 Self(ast::Expr::val(value))
2397 }
2398
2399 pub fn new_record(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
2401 Self(ast::Expr::record(
2402 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2403 ))
2404 }
2405
2406 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
2408 Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
2409 }
2410}
2411
2412impl FromStr for Expression {
2413 type Err = ParseErrors;
2414
2415 fn from_str(expression: &str) -> Result<Self, Self::Err> {
2417 ast::Expr::from_str(expression).map(Expression)
2418 }
2419}
2420
2421#[repr(transparent)]
2437#[derive(Debug, Clone, RefCast)]
2438pub struct RestrictedExpression(ast::RestrictedExpr);
2439
2440impl RestrictedExpression {
2441 pub fn new_string(value: String) -> Self {
2443 Self(ast::RestrictedExpr::val(value))
2444 }
2445
2446 pub fn new_bool(value: bool) -> Self {
2448 Self(ast::RestrictedExpr::val(value))
2449 }
2450
2451 pub fn new_long(value: i64) -> Self {
2453 Self(ast::RestrictedExpr::val(value))
2454 }
2455
2456 pub fn new_record(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
2458 Self(ast::RestrictedExpr::record(
2459 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2460 ))
2461 }
2462
2463 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
2465 Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
2466 }
2467}
2468
2469impl FromStr for RestrictedExpression {
2470 type Err = ParseErrors;
2471
2472 fn from_str(expression: &str) -> Result<Self, Self::Err> {
2474 ast::RestrictedExpr::from_str(expression).map(RestrictedExpression)
2475 }
2476}
2477
2478#[cfg(feature = "partial-eval")]
2490#[derive(Debug, Default)]
2491pub struct RequestBuilder {
2492 principal: Option<ast::EntityUIDEntry>,
2493 action: Option<ast::EntityUIDEntry>,
2494 resource: Option<ast::EntityUIDEntry>,
2495 context: Option<ast::Context>,
2496}
2497
2498#[cfg(feature = "partial-eval")]
2499impl RequestBuilder {
2500 pub fn principal(self, principal: Option<EntityUid>) -> Self {
2502 Self {
2503 principal: Some(match principal {
2504 Some(p) => ast::EntityUIDEntry::concrete(p.0),
2505 None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
2506 ast::Eid::new("principal"),
2507 )),
2508 }),
2509 ..self
2510 }
2511 }
2512
2513 pub fn action(self, action: Option<EntityUid>) -> Self {
2515 Self {
2516 action: Some(match action {
2517 Some(a) => ast::EntityUIDEntry::concrete(a.0),
2518 None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
2519 ast::Eid::new("action"),
2520 )),
2521 }),
2522 ..self
2523 }
2524 }
2525
2526 pub fn resource(self, resource: Option<EntityUid>) -> Self {
2528 Self {
2529 resource: Some(match resource {
2530 Some(r) => ast::EntityUIDEntry::concrete(r.0),
2531 None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
2532 ast::Eid::new("resource"),
2533 )),
2534 }),
2535 ..self
2536 }
2537 }
2538
2539 pub fn context(self, context: Context) -> Self {
2541 Self {
2542 context: Some(context.0),
2543 ..self
2544 }
2545 }
2546
2547 pub fn build(self) -> Request {
2549 let p = match self.principal {
2550 Some(p) => p,
2551 None => ast::EntityUIDEntry::Unknown,
2552 };
2553 let a = match self.action {
2554 Some(a) => a,
2555 None => ast::EntityUIDEntry::Unknown,
2556 };
2557 let r = match self.resource {
2558 Some(r) => r,
2559 None => ast::EntityUIDEntry::Unknown,
2560 };
2561 Request(ast::Request::new_with_unknowns(p, a, r, self.context))
2562 }
2563}
2564
2565#[repr(transparent)]
2567#[derive(Debug, RefCast)]
2568pub struct Request(pub(crate) ast::Request);
2569
2570impl Request {
2571 #[cfg(feature = "partial-eval")]
2573 pub fn builder() -> RequestBuilder {
2574 RequestBuilder::default()
2575 }
2576
2577 pub fn new(
2587 principal: Option<EntityUid>,
2588 action: Option<EntityUid>,
2589 resource: Option<EntityUid>,
2590 context: Context,
2591 ) -> Self {
2592 let p = match principal {
2593 Some(p) => p.0,
2594 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
2595 };
2596 let a = match action {
2597 Some(a) => a.0,
2598 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("action")),
2599 };
2600 let r = match resource {
2601 Some(r) => r.0,
2602 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
2603 };
2604 Self(ast::Request::new(p, a, r, context.0))
2605 }
2606
2607 pub fn principal(&self) -> Option<&EntityUid> {
2611 match self.0.principal() {
2612 ast::EntityUIDEntry::Concrete(euid) => match euid.entity_type() {
2613 ast::EntityType::Concrete(_) => Some(EntityUid::ref_cast(euid.as_ref())),
2615 ast::EntityType::Unspecified => None,
2616 },
2617 ast::EntityUIDEntry::Unknown => None,
2618 }
2619 }
2620
2621 pub fn action(&self) -> Option<&EntityUid> {
2625 match self.0.action() {
2626 ast::EntityUIDEntry::Concrete(euid) => match euid.entity_type() {
2627 ast::EntityType::Concrete(_) => Some(EntityUid::ref_cast(euid.as_ref())),
2629 ast::EntityType::Unspecified => None,
2630 },
2631 ast::EntityUIDEntry::Unknown => None,
2632 }
2633 }
2634
2635 pub fn resource(&self) -> Option<&EntityUid> {
2639 match self.0.resource() {
2640 ast::EntityUIDEntry::Concrete(euid) => match euid.entity_type() {
2641 ast::EntityType::Concrete(_) => Some(EntityUid::ref_cast(euid.as_ref())),
2643 ast::EntityType::Unspecified => None,
2644 },
2645 ast::EntityUIDEntry::Unknown => None,
2646 }
2647 }
2648}
2649
2650#[repr(transparent)]
2652#[derive(Debug, Clone, RefCast)]
2653pub struct Context(ast::Context);
2654
2655impl Context {
2656 pub fn empty() -> Self {
2663 Self(ast::Context::empty())
2664 }
2665
2666 pub fn from_pairs(pairs: impl IntoIterator<Item = (String, RestrictedExpression)>) -> Self {
2701 Self(ast::Context::from_pairs(
2702 pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2703 ))
2704 }
2705
2706 pub fn from_json_str(
2745 json: &str,
2746 schema: Option<(&Schema, &EntityUid)>,
2747 ) -> Result<Self, ContextJsonError> {
2748 let schema = schema
2749 .map(|(s, uid)| Self::get_context_schema(s, uid))
2750 .transpose()?;
2751 let context =
2752 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2753 .from_json_str(json)?;
2754 Ok(Self(context))
2755 }
2756
2757 pub fn from_json_value(
2811 json: serde_json::Value,
2812 schema: Option<(&Schema, &EntityUid)>,
2813 ) -> Result<Self, ContextJsonError> {
2814 let schema = schema
2815 .map(|(s, uid)| Self::get_context_schema(s, uid))
2816 .transpose()?;
2817 let context =
2818 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2819 .from_json_value(json)?;
2820 Ok(Self(context))
2821 }
2822
2823 pub fn from_json_file(
2855 json: impl std::io::Read,
2856 schema: Option<(&Schema, &EntityUid)>,
2857 ) -> Result<Self, ContextJsonError> {
2858 let schema = schema
2859 .map(|(s, uid)| Self::get_context_schema(s, uid))
2860 .transpose()?;
2861 let context =
2862 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2863 .from_json_file(json)?;
2864 Ok(Self(context))
2865 }
2866
2867 fn get_context_schema(
2869 schema: &Schema,
2870 action: &EntityUid,
2871 ) -> Result<impl ContextSchema, ContextJsonError> {
2872 schema
2873 .0
2874 .get_context_schema(&action.0)
2875 .ok_or_else(|| ContextJsonError::ActionDoesNotExist {
2876 action: action.clone(),
2877 })
2878 }
2879}
2880
2881#[derive(Debug, Error)]
2883pub enum ContextJsonError {
2884 #[error(transparent)]
2886 JsonDeserializationError(#[from] JsonDeserializationError),
2887 #[error("Action {action} doesn't exist in the supplied schema")]
2889 ActionDoesNotExist {
2890 action: EntityUid,
2892 },
2893}
2894
2895impl std::fmt::Display for Request {
2896 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2897 write!(f, "{}", self.0)
2898 }
2899}
2900
2901#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
2903pub enum EvalResult {
2904 Bool(bool),
2906 Long(i64),
2908 String(String),
2910 EntityUid(EntityUid),
2912 Set(Set),
2914 Record(Record),
2916 ExtensionValue(String),
2918 }
2920
2921#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
2923pub struct Set(BTreeSet<EvalResult>);
2924
2925impl Set {
2926 pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
2928 self.0.iter()
2929 }
2930
2931 pub fn contains(&self, elem: &EvalResult) -> bool {
2933 self.0.contains(elem)
2934 }
2935
2936 pub fn len(&self) -> usize {
2938 self.0.len()
2939 }
2940
2941 pub fn is_empty(&self) -> bool {
2943 self.0.is_empty()
2944 }
2945}
2946
2947#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
2949pub struct Record(BTreeMap<String, EvalResult>);
2950
2951impl Record {
2952 pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
2954 self.0.iter()
2955 }
2956
2957 pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
2959 self.0.contains_key(key.as_ref())
2960 }
2961
2962 pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
2964 self.0.get(key.as_ref())
2965 }
2966
2967 pub fn len(&self) -> usize {
2969 self.0.len()
2970 }
2971
2972 pub fn is_empty(&self) -> bool {
2974 self.0.is_empty()
2975 }
2976}
2977
2978#[doc(hidden)]
2979impl From<ast::Value> for EvalResult {
2980 fn from(v: ast::Value) -> Self {
2981 match v {
2982 ast::Value::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
2983 ast::Value::Lit(ast::Literal::Long(i)) => Self::Long(i),
2984 ast::Value::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
2985 ast::Value::Lit(ast::Literal::EntityUID(e)) => {
2986 Self::EntityUid(EntityUid(ast::EntityUID::clone(&e)))
2987 }
2988 ast::Value::Set(s) => Self::Set(Set(s
2989 .authoritative
2990 .iter()
2991 .map(|v| v.clone().into())
2992 .collect())),
2993 ast::Value::Record(r) => Self::Record(Record(
2994 r.iter()
2995 .map(|(k, v)| (k.to_string(), v.clone().into()))
2996 .collect(),
2997 )),
2998 ast::Value::ExtensionValue(v) => Self::ExtensionValue(v.to_string()),
2999 }
3000 }
3001}
3002impl std::fmt::Display for EvalResult {
3003 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3004 match self {
3005 Self::Bool(b) => write!(f, "{b}"),
3006 Self::Long(l) => write!(f, "{l}"),
3007 Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
3008 Self::EntityUid(uid) => write!(f, "{uid}"),
3009 Self::Set(s) => {
3010 write!(f, "[")?;
3011 for (i, ev) in s.iter().enumerate() {
3012 write!(f, "{ev}")?;
3013 if (i + 1) < s.len() {
3014 write!(f, ", ")?;
3015 }
3016 }
3017 write!(f, "]")?;
3018 Ok(())
3019 }
3020 Self::Record(r) => {
3021 write!(f, "{{")?;
3022 for (i, (k, v)) in r.iter().enumerate() {
3023 write!(f, "\"{}\": {v}", k.escape_debug())?;
3024 if (i + 1) < r.len() {
3025 write!(f, ", ")?;
3026 }
3027 }
3028 write!(f, "}}")?;
3029 Ok(())
3030 }
3031 Self::ExtensionValue(s) => write!(f, "{s}"),
3032 }
3033 }
3034}
3035
3036pub fn eval_expression(
3040 request: &Request,
3041 entities: &Entities,
3042 expr: &Expression,
3043) -> Result<EvalResult, EvaluationError> {
3044 let all_ext = Extensions::all_available();
3045 let eval = Evaluator::new(&request.0, &entities.0, &all_ext)
3046 .map_err(|e| EvaluationError::StringMessage(e.to_string()))?;
3047 Ok(EvalResult::from(
3048 eval.interpret(&expr.0, &ast::SlotEnv::new())
3050 .map_err(|e| EvaluationError::StringMessage(e.to_string()))?,
3051 ))
3052}
3053
3054#[cfg(test)]
3055#[cfg(feature = "partial-eval")]
3056mod partial_eval_test {
3057 use std::collections::HashSet;
3058
3059 use crate::{PolicyId, PolicySet, ResidualResponse};
3060
3061 #[test]
3062 fn test_pe_response_constructor() {
3063 let p: PolicySet = "permit(principal, action, resource);".parse().unwrap();
3064 let reason: HashSet<PolicyId> = std::iter::once("id1".parse().unwrap()).collect();
3065 let errors: HashSet<String> = std::iter::once("error".to_string()).collect();
3066 let a = ResidualResponse::new(p.clone(), reason.clone(), errors.clone());
3067 assert_eq!(a.diagnostics().errors, errors);
3068 assert_eq!(a.diagnostics().reason, reason);
3069 assert_eq!(a.residuals(), &p);
3070 }
3071}
3072
3073#[allow(clippy::panic)]
3075#[cfg(test)]
3076mod entity_uid_tests {
3077 use super::*;
3078
3079 #[test]
3081 fn entity_uid_from_parts() {
3082 let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
3083 let entity_type_name = EntityTypeName::from_str("Chess::Master")
3084 .expect("failed at constructing EntityTypeName");
3085 let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3086 assert_eq!(euid.id().as_ref(), "bobby");
3087 assert_eq!(euid.type_name().to_string(), "Chess::Master");
3088 assert_eq!(euid.type_name().basename(), "Master");
3089 assert_eq!(euid.type_name().namespace(), "Chess");
3090 assert_eq!(euid.type_name().namespace_components().count(), 1);
3091 }
3092
3093 #[test]
3095 fn entity_uid_no_namespace() {
3096 let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
3097 let entity_type_name =
3098 EntityTypeName::from_str("User").expect("failed at constructing EntityTypeName");
3099 let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3100 assert_eq!(euid.id().as_ref(), "bobby");
3101 assert_eq!(euid.type_name().to_string(), "User");
3102 assert_eq!(euid.type_name().basename(), "User");
3103 assert_eq!(euid.type_name().namespace(), String::new());
3104 assert_eq!(euid.type_name().namespace_components().count(), 0);
3105 }
3106
3107 #[test]
3109 fn entity_uid_nested_namespaces() {
3110 let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
3111 let entity_type_name = EntityTypeName::from_str("A::B::C::D::Z")
3112 .expect("failed at constructing EntityTypeName");
3113 let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3114 assert_eq!(euid.id().as_ref(), "bobby");
3115 assert_eq!(euid.type_name().to_string(), "A::B::C::D::Z");
3116 assert_eq!(euid.type_name().basename(), "Z");
3117 assert_eq!(euid.type_name().namespace(), "A::B::C::D");
3118 assert_eq!(euid.type_name().namespace_components().count(), 4);
3119 }
3120
3121 #[test]
3123 fn entity_uid_with_escape() {
3124 let entity_id = EntityId::from_str(r"bobby\'s sister:\nVeronica")
3126 .expect("failed at constructing EntityId");
3127 let entity_type_name = EntityTypeName::from_str("Hockey::Master")
3128 .expect("failed at constructing EntityTypeName");
3129 let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3130 assert_eq!(euid.id().as_ref(), r"bobby\'s sister:\nVeronica");
3133 assert_eq!(euid.type_name().to_string(), "Hockey::Master");
3134 assert_eq!(euid.type_name().basename(), "Master");
3135 assert_eq!(euid.type_name().namespace(), "Hockey");
3136 assert_eq!(euid.type_name().namespace_components().count(), 1);
3137 }
3138
3139 #[test]
3141 fn entity_uid_with_backslashes() {
3142 let entity_id =
3144 EntityId::from_str(r#"\ \a \b \' \" \\"#).expect("failed at constructing EntityId");
3145 let entity_type_name =
3146 EntityTypeName::from_str("Test::User").expect("failed at constructing EntityTypeName");
3147 let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
3148 assert_eq!(euid.id().as_ref(), r#"\ \a \b \' \" \\"#);
3150 assert_eq!(euid.type_name().to_string(), "Test::User");
3151 }
3152
3153 #[test]
3155 fn entity_uid_with_quotes() {
3156 let euid: EntityUid = EntityUid::from_type_name_and_id(
3157 EntityTypeName::from_str("Test::User").unwrap(),
3158 EntityId::from_str(r#"b'ob"by\'s sis\"ter"#).unwrap(),
3159 );
3160 assert_eq!(euid.id().as_ref(), r#"b'ob"by\'s sis\"ter"#);
3163 assert_eq!(euid.type_name().to_string(), r"Test::User");
3164 }
3165
3166 #[test]
3168 fn entity_uid_with_whitespace() {
3169 EntityTypeName::from_str("A :: B::C").expect_err("should fail due to RFC 9");
3170 EntityTypeName::from_str(" A :: B\n::C \n ::D\n").expect_err("should fail due to RFC 9");
3171
3172 let policy = Policy::from_str(
3174 r#"permit(principal == A :: B::C :: " hi there are spaces ", action, resource);"#,
3175 )
3176 .expect("should succeed, see RFC 9");
3177 let euid = match policy.principal_constraint() {
3178 PrincipalConstraint::Eq(euid) => euid,
3179 _ => panic!("expected Eq constraint"),
3180 };
3181 assert_eq!(euid.id().as_ref(), " hi there are spaces ");
3182 assert_eq!(euid.type_name().to_string(), "A::B::C"); assert_eq!(euid.type_name().basename(), "C");
3184 assert_eq!(euid.type_name().namespace(), "A::B");
3185 assert_eq!(euid.type_name().namespace_components().count(), 2);
3186
3187 let policy = Policy::from_str(
3188 r#"
3189permit(principal == A :: B
3190 ::C
3191 :: D
3192 :: " hi there are
3193 spaces and
3194 newlines ", action, resource);"#,
3195 )
3196 .expect("should succeed, see RFC 9");
3197 let euid = match policy.principal_constraint() {
3198 PrincipalConstraint::Eq(euid) => euid,
3199 _ => panic!("expected Eq constraint"),
3200 };
3201 assert_eq!(
3202 euid.id().as_ref(),
3203 " hi there are\n spaces and\n newlines "
3204 );
3205 assert_eq!(euid.type_name().to_string(), "A::B::C::D"); assert_eq!(euid.type_name().basename(), "D");
3207 assert_eq!(euid.type_name().namespace(), "A::B::C");
3208 assert_eq!(euid.type_name().namespace_components().count(), 3);
3209 }
3210
3211 #[test]
3212 fn malformed_entity_type_name_should_fail() {
3213 let result = EntityTypeName::from_str("I'm an invalid name");
3214
3215 assert!(matches!(result, Err(ParseErrors(_))));
3216 let error = result.err().unwrap();
3217 assert!(error.to_string().contains("invalid token"));
3218 }
3219
3220 #[test]
3222 fn parse_euid() {
3223 let parsed_eid: EntityUid = r#"Test::User::"bobby""#.parse().expect("Failed to parse");
3224 assert_eq!(parsed_eid.id().as_ref(), r"bobby");
3225 assert_eq!(parsed_eid.type_name().to_string(), r"Test::User");
3226 }
3227
3228 #[test]
3230 fn parse_euid_with_escape() {
3231 let parsed_eid: EntityUid = r#"Test::User::"b\'ob\"by""#.parse().expect("Failed to parse");
3233 assert_eq!(parsed_eid.id().as_ref(), r#"b'ob"by"#);
3236 assert_eq!(parsed_eid.type_name().to_string(), r"Test::User");
3237 }
3238
3239 #[test]
3241 fn parse_euid_single_quotes() {
3242 let euid_str = r#"Test::User::"b'obby\'s sister""#;
3244 EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
3245 let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
3247 let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
3248 let parsed_euid = match policy.principal_constraint() {
3249 PrincipalConstraint::Eq(euid) => euid,
3250 _ => panic!("Expected an Eq constraint"),
3251 };
3252 assert_eq!(parsed_euid.id().as_ref(), r"b'obby's sister");
3255 assert_eq!(parsed_euid.type_name().to_string(), r"Test::User");
3256 }
3257
3258 #[test]
3260 fn parse_euid_whitespace() {
3261 let euid_str = " A ::B :: C:: D \n :: \n E\n :: \"hi\"";
3262 EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
3263 let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
3265 let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
3266 let parsed_euid = match policy.principal_constraint() {
3267 PrincipalConstraint::Eq(euid) => euid,
3268 _ => panic!("Expected an Eq constraint"),
3269 };
3270 assert_eq!(parsed_euid.id().as_ref(), "hi");
3271 assert_eq!(parsed_euid.type_name().to_string(), "A::B::C::D::E"); assert_eq!(parsed_euid.type_name().basename(), "E");
3273 assert_eq!(parsed_euid.type_name().namespace(), "A::B::C::D");
3274 assert_eq!(parsed_euid.type_name().namespace_components().count(), 4);
3275 }
3276
3277 #[test]
3279 fn euid_roundtrip() {
3280 let parsed_euid: EntityUid = r#"Test::User::"b\'ob""#.parse().expect("Failed to parse");
3281 assert_eq!(parsed_euid.id().as_ref(), r"b'ob");
3282 let reparsed: EntityUid = format!("{parsed_euid}")
3283 .parse()
3284 .expect("failed to roundtrip");
3285 assert_eq!(reparsed.id().as_ref(), r"b'ob");
3286 }
3287
3288 #[test]
3289 fn accessing_unspecified_entity_returns_none() {
3290 let c = Context::empty();
3291 let request: Request = Request::new(None, None, None, c);
3292 let p = request.principal();
3293 let a = request.action();
3294 let r = request.resource();
3295 assert!(p.is_none());
3296 assert!(a.is_none());
3297 assert!(r.is_none());
3298 }
3299}
3300
3301#[cfg(test)]
3302mod head_constraints_tests {
3303 use super::*;
3304
3305 #[test]
3306 fn principal_constraint_inline() {
3307 let p = Policy::from_str("permit(principal,action,resource);").unwrap();
3308 assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
3309 let euid = EntityUid::from_strs("T", "a");
3310 assert_eq!(euid.id().as_ref(), "a");
3311 assert_eq!(
3312 euid.type_name(),
3313 &EntityTypeName::from_str("T").expect("Failed to parse EntityTypeName")
3314 );
3315 let p =
3316 Policy::from_str("permit(principal == T::\"a\",action,resource == T::\"b\");").unwrap();
3317 assert_eq!(
3318 p.principal_constraint(),
3319 PrincipalConstraint::Eq(euid.clone())
3320 );
3321 let p = Policy::from_str("permit(principal in T::\"a\",action,resource);").unwrap();
3322 assert_eq!(p.principal_constraint(), PrincipalConstraint::In(euid));
3323 }
3324
3325 #[test]
3326 fn action_constraint_inline() {
3327 let p = Policy::from_str("permit(principal,action,resource);").unwrap();
3328 assert_eq!(p.action_constraint(), ActionConstraint::Any);
3329 let euid = EntityUid::from_strs("NN::N::Action", "a");
3330 assert_eq!(
3331 euid.type_name(),
3332 &EntityTypeName::from_str("NN::N::Action").expect("Failed to parse EntityTypeName")
3333 );
3334 let p = Policy::from_str(
3335 "permit(principal == T::\"b\",action == NN::N::Action::\"a\",resource == T::\"c\");",
3336 )
3337 .unwrap();
3338 assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
3339 let p = Policy::from_str("permit(principal,action in [NN::N::Action::\"a\"],resource);")
3340 .unwrap();
3341 assert_eq!(p.action_constraint(), ActionConstraint::In(vec![euid]));
3342 }
3343
3344 #[test]
3345 fn resource_constraint_inline() {
3346 let p = Policy::from_str("permit(principal,action,resource);").unwrap();
3347 assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
3348 let euid = EntityUid::from_strs("NN::N::T", "a");
3349 assert_eq!(
3350 euid.type_name(),
3351 &EntityTypeName::from_str("NN::N::T").expect("Failed to parse EntityTypeName")
3352 );
3353 let p =
3354 Policy::from_str("permit(principal == T::\"b\",action,resource == NN::N::T::\"a\");")
3355 .unwrap();
3356 assert_eq!(
3357 p.resource_constraint(),
3358 ResourceConstraint::Eq(euid.clone())
3359 );
3360 let p = Policy::from_str("permit(principal,action,resource in NN::N::T::\"a\");").unwrap();
3361 assert_eq!(p.resource_constraint(), ResourceConstraint::In(euid));
3362 }
3363
3364 #[test]
3365 fn principal_constraint_link() {
3366 let p = link("permit(principal,action,resource);", HashMap::new());
3367 assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
3368 let euid = EntityUid::from_strs("T", "a");
3369 let p = link(
3370 "permit(principal == T::\"a\",action,resource);",
3371 HashMap::new(),
3372 );
3373 assert_eq!(
3374 p.principal_constraint(),
3375 PrincipalConstraint::Eq(euid.clone())
3376 );
3377 let p = link(
3378 "permit(principal in T::\"a\",action,resource);",
3379 HashMap::new(),
3380 );
3381 assert_eq!(
3382 p.principal_constraint(),
3383 PrincipalConstraint::In(euid.clone())
3384 );
3385 let map: HashMap<SlotId, EntityUid> =
3386 std::iter::once((SlotId::principal(), euid.clone())).collect();
3387 let p = link(
3388 "permit(principal in ?principal,action,resource);",
3389 map.clone(),
3390 );
3391 assert_eq!(
3392 p.principal_constraint(),
3393 PrincipalConstraint::In(euid.clone())
3394 );
3395 let p = link("permit(principal == ?principal,action,resource);", map);
3396 assert_eq!(p.principal_constraint(), PrincipalConstraint::Eq(euid));
3397 }
3398
3399 #[test]
3400 fn action_constraint_link() {
3401 let p = link("permit(principal,action,resource);", HashMap::new());
3402 assert_eq!(p.action_constraint(), ActionConstraint::Any);
3403 let euid = EntityUid::from_strs("Action", "a");
3404 let p = link(
3405 "permit(principal,action == Action::\"a\",resource);",
3406 HashMap::new(),
3407 );
3408 assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
3409 let p = link(
3410 "permit(principal,action in [Action::\"a\",Action::\"b\"],resource);",
3411 HashMap::new(),
3412 );
3413 assert_eq!(
3414 p.action_constraint(),
3415 ActionConstraint::In(vec![euid, EntityUid::from_strs("Action", "b"),])
3416 );
3417 }
3418
3419 #[test]
3420 fn resource_constraint_link() {
3421 let p = link("permit(principal,action,resource);", HashMap::new());
3422 assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
3423 let euid = EntityUid::from_strs("T", "a");
3424 let p = link(
3425 "permit(principal,action,resource == T::\"a\");",
3426 HashMap::new(),
3427 );
3428 assert_eq!(
3429 p.resource_constraint(),
3430 ResourceConstraint::Eq(euid.clone())
3431 );
3432 let p = link(
3433 "permit(principal,action,resource in T::\"a\");",
3434 HashMap::new(),
3435 );
3436 assert_eq!(
3437 p.resource_constraint(),
3438 ResourceConstraint::In(euid.clone())
3439 );
3440 let map: HashMap<SlotId, EntityUid> =
3441 std::iter::once((SlotId::resource(), euid.clone())).collect();
3442 let p = link(
3443 "permit(principal,action,resource in ?resource);",
3444 map.clone(),
3445 );
3446 assert_eq!(
3447 p.resource_constraint(),
3448 ResourceConstraint::In(euid.clone())
3449 );
3450 let p = link("permit(principal,action,resource == ?resource);", map);
3451 assert_eq!(p.resource_constraint(), ResourceConstraint::Eq(euid));
3452 }
3453
3454 fn link(src: &str, values: HashMap<SlotId, EntityUid>) -> Policy {
3455 let mut pset = PolicySet::new();
3456 let template = Template::parse(Some("Id".to_string()), src).unwrap();
3457
3458 pset.add_template(template).unwrap();
3459
3460 let link_id = PolicyId::from_str("link").unwrap();
3461 pset.link(PolicyId::from_str("Id").unwrap(), link_id.clone(), values)
3462 .unwrap();
3463 pset.policy(&link_id).unwrap().clone()
3464 }
3465}
3466
3467#[allow(clippy::panic)]
3469#[cfg(test)]
3471mod policy_set_tests {
3472 use super::*;
3473 use ast::LinkingError;
3474 use cool_asserts::assert_matches;
3475
3476 #[test]
3477 fn link_conflicts() {
3478 let mut pset = PolicySet::new();
3479 let p1 = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
3480 .expect("Failed to parse");
3481 pset.add(p1).expect("Failed to add");
3482 let template = Template::parse(
3483 Some("t".into()),
3484 "permit(principal == ?principal, action, resource);",
3485 )
3486 .expect("Failed to parse");
3487 pset.add_template(template).expect("Add failed");
3488
3489 let env: HashMap<SlotId, EntityUid> =
3490 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
3491
3492 let before_link = pset.clone();
3493 let r = pset.link(
3494 PolicyId::from_str("t").unwrap(),
3495 PolicyId::from_str("id").unwrap(),
3496 env,
3497 );
3498
3499 assert_matches!(
3500 r,
3501 Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict)) => (),
3502 );
3503 assert_eq!(
3504 pset, before_link,
3505 "A failed link shouldn't mutate the policy set"
3506 );
3507 }
3508
3509 #[test]
3510 fn policyset_add() {
3511 let mut pset = PolicySet::new();
3512 let static_policy = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
3513 .expect("Failed to parse");
3514 pset.add(static_policy).expect("Failed to add");
3515
3516 let template = Template::parse(
3517 Some("t".into()),
3518 "permit(principal == ?principal, action, resource);",
3519 )
3520 .expect("Failed to parse");
3521 pset.add_template(template).expect("Failed to add");
3522
3523 let env1: HashMap<SlotId, EntityUid> =
3524 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test1"))).collect();
3525 pset.link(
3526 PolicyId::from_str("t").unwrap(),
3527 PolicyId::from_str("link").unwrap(),
3528 env1,
3529 )
3530 .expect("Failed to link");
3531
3532 let env2: HashMap<SlotId, EntityUid> =
3533 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test2"))).collect();
3534
3535 let err = pset
3536 .link(
3537 PolicyId::from_str("t").unwrap(),
3538 PolicyId::from_str("link").unwrap(),
3539 env2.clone(),
3540 )
3541 .expect_err("Should have failed due to conflict with existing link id");
3542 match err {
3543 PolicySetError::LinkingError(_) => (),
3544 e => panic!("Wrong error: {e}"),
3545 }
3546
3547 pset.link(
3548 PolicyId::from_str("t").unwrap(),
3549 PolicyId::from_str("link2").unwrap(),
3550 env2,
3551 )
3552 .expect("Failed to link");
3553
3554 let template2 = Template::parse(
3555 Some("t".into()),
3556 "forbid(principal, action, resource == ?resource);",
3557 )
3558 .expect("Failed to parse");
3559 pset.add_template(template2)
3560 .expect_err("should have failed due to conflict on template id");
3561 let template2 = Template::parse(
3562 Some("t2".into()),
3563 "forbid(principal, action, resource == ?resource);",
3564 )
3565 .expect("Failed to parse");
3566 pset.add_template(template2)
3567 .expect("Failed to add template");
3568 let env3: HashMap<SlotId, EntityUid> =
3569 std::iter::once((SlotId::resource(), EntityUid::from_strs("Test", "test3"))).collect();
3570
3571 pset.link(
3572 PolicyId::from_str("t").unwrap(),
3573 PolicyId::from_str("unique3").unwrap(),
3574 env3.clone(),
3575 )
3576 .expect_err("should have failed due to conflict on template id");
3577
3578 pset.link(
3579 PolicyId::from_str("t2").unwrap(),
3580 PolicyId::from_str("unique3").unwrap(),
3581 env3,
3582 )
3583 .expect("should succeed with unique ids");
3584 }
3585
3586 #[test]
3587 fn pset_requests() {
3588 let template = Template::parse(
3589 Some("template".into()),
3590 "permit(principal == ?principal, action, resource);",
3591 )
3592 .expect("Template Parse Failure");
3593 let static_policy = Policy::parse(
3594 Some("static".into()),
3595 "permit(principal, action, resource);",
3596 )
3597 .expect("Static parse failure");
3598 let mut pset = PolicySet::new();
3599 pset.add_template(template).unwrap();
3600 pset.add(static_policy).unwrap();
3601 pset.link(
3602 PolicyId::from_str("template").unwrap(),
3603 PolicyId::from_str("linked").unwrap(),
3604 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
3605 )
3606 .expect("Link failure");
3607
3608 assert_eq!(pset.templates().count(), 1);
3609 assert_eq!(pset.policies().count(), 2);
3610 assert_eq!(pset.policies().filter(|p| p.is_static()).count(), 1);
3611
3612 assert_eq!(
3613 pset.template(&"template".parse().unwrap())
3614 .expect("lookup failed")
3615 .id(),
3616 &"template".parse().unwrap()
3617 );
3618 assert_eq!(
3619 pset.policy(&"static".parse().unwrap())
3620 .expect("lookup failed")
3621 .id(),
3622 &"static".parse().unwrap()
3623 );
3624 assert_eq!(
3625 pset.policy(&"linked".parse().unwrap())
3626 .expect("lookup failed")
3627 .id(),
3628 &"linked".parse().unwrap()
3629 );
3630 }
3631
3632 #[test]
3633 fn link_static_policy() {
3634 let static_policy = Policy::parse(
3637 Some("static".into()),
3638 "permit(principal, action, resource);",
3639 )
3640 .expect("Static parse failure");
3641 let mut pset = PolicySet::new();
3642 pset.add(static_policy).unwrap();
3643
3644 let before_link = pset.clone();
3645 let result = pset.link(
3646 PolicyId::from_str("static").unwrap(),
3647 PolicyId::from_str("linked").unwrap(),
3648 HashMap::new(),
3649 );
3650 assert_matches!(result, Err(PolicySetError::ExpectedTemplate));
3651 assert_eq!(
3652 pset, before_link,
3653 "A failed link shouldn't mutate the policy set"
3654 );
3655 }
3656
3657 #[test]
3658 fn link_linked_policy() {
3659 let template = Template::parse(
3660 Some("template".into()),
3661 "permit(principal == ?principal, action, resource);",
3662 )
3663 .expect("Template Parse Failure");
3664 let mut pset = PolicySet::new();
3665 pset.add_template(template).unwrap();
3666
3667 pset.link(
3668 PolicyId::from_str("template").unwrap(),
3669 PolicyId::from_str("linked").unwrap(),
3670 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
3671 )
3672 .unwrap();
3673
3674 let before_link = pset.clone();
3675 let result = pset.link(
3676 PolicyId::from_str("linked").unwrap(),
3677 PolicyId::from_str("linked2").unwrap(),
3678 HashMap::new(),
3679 );
3680 assert_matches!(result, Err(PolicySetError::ExpectedTemplate));
3681 assert_eq!(
3682 pset, before_link,
3683 "A failed link shouldn't mutate the policy set"
3684 );
3685 }
3686
3687 #[test]
3688 fn pset_add_conflict() {
3689 let template = Template::parse(
3690 Some("policy0".into()),
3691 "permit(principal == ?principal, action, resource);",
3692 )
3693 .expect("Template Parse Failure");
3694 let mut pset = PolicySet::new();
3695 pset.add_template(template).unwrap();
3696 let env: HashMap<SlotId, EntityUid> =
3697 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
3698 pset.link(
3699 PolicyId::from_str("policy0").unwrap(),
3700 PolicyId::from_str("policy1").unwrap(),
3701 env,
3702 )
3703 .unwrap();
3704
3705 let static_policy = Policy::parse(
3707 Some("policy0".into()),
3708 "permit(principal, action, resource);",
3709 )
3710 .expect("Static parse failure");
3711 assert_matches!(pset.add(static_policy), Err(PolicySetError::AlreadyDefined));
3712
3713 let static_policy = Policy::parse(
3715 Some("policy1".into()),
3716 "permit(principal, action, resource);",
3717 )
3718 .expect("Static parse failure");
3719 assert_matches!(pset.add(static_policy), Err(PolicySetError::AlreadyDefined));
3720
3721 let static_policy = Policy::parse(
3723 Some("policy2".into()),
3724 "permit(principal, action, resource);",
3725 )
3726 .expect("Static parse failure");
3727 pset.add(static_policy.clone()).unwrap();
3728 assert_matches!(pset.add(static_policy), Err(PolicySetError::AlreadyDefined));
3729 }
3730
3731 #[test]
3732 fn pset_add_template_conflict() {
3733 let template = Template::parse(
3734 Some("policy0".into()),
3735 "permit(principal == ?principal, action, resource);",
3736 )
3737 .expect("Template Parse Failure");
3738 let mut pset = PolicySet::new();
3739 pset.add_template(template).unwrap();
3740 let env: HashMap<SlotId, EntityUid> =
3741 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
3742 pset.link(
3743 PolicyId::from_str("policy0").unwrap(),
3744 PolicyId::from_str("policy3").unwrap(),
3745 env,
3746 )
3747 .unwrap();
3748
3749 let template = Template::parse(
3751 Some("policy3".into()),
3752 "permit(principal == ?principal, action, resource);",
3753 )
3754 .expect("Template Parse Failure");
3755 assert_matches!(
3756 pset.add_template(template),
3757 Err(PolicySetError::AlreadyDefined)
3758 );
3759
3760 let template = Template::parse(
3762 Some("policy0".into()),
3763 "permit(principal == ?principal, action, resource);",
3764 )
3765 .expect("Template Parse Failure");
3766 assert_matches!(
3767 pset.add_template(template),
3768 Err(PolicySetError::AlreadyDefined)
3769 );
3770
3771 let static_policy = Policy::parse(
3773 Some("policy1".into()),
3774 "permit(principal, action, resource);",
3775 )
3776 .expect("Static parse failure");
3777 pset.add(static_policy).unwrap();
3778 let template = Template::parse(
3779 Some("policy1".into()),
3780 "permit(principal == ?principal, action, resource);",
3781 )
3782 .expect("Template Parse Failure");
3783 assert_matches!(
3784 pset.add_template(template),
3785 Err(PolicySetError::AlreadyDefined)
3786 );
3787 }
3788
3789 #[test]
3790 fn pset_link_conflict() {
3791 let template = Template::parse(
3792 Some("policy0".into()),
3793 "permit(principal == ?principal, action, resource);",
3794 )
3795 .expect("Template Parse Failure");
3796 let mut pset = PolicySet::new();
3797 pset.add_template(template).unwrap();
3798 let env: HashMap<SlotId, EntityUid> =
3799 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
3800
3801 pset.link(
3803 PolicyId::from_str("policy0").unwrap(),
3804 PolicyId::from_str("policy3").unwrap(),
3805 env.clone(),
3806 )
3807 .unwrap();
3808 assert_matches!(
3809 pset.link(
3810 PolicyId::from_str("policy0").unwrap(),
3811 PolicyId::from_str("policy3").unwrap(),
3812 env.clone(),
3813 ),
3814 Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict))
3815 );
3816
3817 assert_matches!(
3819 pset.link(
3820 PolicyId::from_str("policy0").unwrap(),
3821 PolicyId::from_str("policy0").unwrap(),
3822 env.clone(),
3823 ),
3824 Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict))
3825 );
3826
3827 let static_policy = Policy::parse(
3829 Some("policy1".into()),
3830 "permit(principal, action, resource);",
3831 )
3832 .expect("Static parse failure");
3833 pset.add(static_policy).unwrap();
3834 assert_matches!(
3835 pset.link(
3836 PolicyId::from_str("policy0").unwrap(),
3837 PolicyId::from_str("policy1").unwrap(),
3838 env,
3839 ),
3840 Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict))
3841 );
3842 }
3843}
3844
3845#[cfg(test)]
3846mod schema_tests {
3847 use super::*;
3848 use cool_asserts::assert_matches;
3849 use serde_json::json;
3850
3851 #[test]
3853 fn valid_schema() {
3854 Schema::from_json_value(json!(
3855 { "": {
3856 "entityTypes": {
3857 "Photo": {
3858 "memberOfTypes": [ "Album" ],
3859 "shape": {
3860 "type": "Record",
3861 "attributes": {
3862 "foo": {
3863 "type": "Boolean",
3864 "required": false
3865 }
3866 }
3867 }
3868 },
3869 "Album": {
3870 "memberOfTypes": [ ],
3871 "shape": {
3872 "type": "Record",
3873 "attributes": {
3874 "foo": {
3875 "type": "Boolean",
3876 "required": false
3877 }
3878 }
3879 }
3880 }
3881 },
3882 "actions": {
3883 "view": {
3884 "appliesTo": {
3885 "principalTypes": ["Photo", "Album"],
3886 "resourceTypes": ["Photo"]
3887 }
3888 }
3889 }
3890 }}))
3891 .expect("schema should be valid");
3892 }
3893
3894 #[test]
3896 fn invalid_schema() {
3897 assert_matches!(
3898 Schema::from_json_value(json!(
3899 r#""{"": {
3902 "entityTypes": {
3903 "Photo": {
3904 "memberOfTypes": [ "Album" ],
3905 "shape": {
3906 "type": "Record",
3907 "attributes": {
3908 "foo": {
3909 "type": "Boolean",
3910 "required": false
3911 }
3912 }
3913 }
3914 },
3915 "Album": {
3916 "memberOfTypes": [ ],
3917 "shape": {
3918 "type": "Record",
3919 "attributes": {
3920 "foo": {
3921 "type": "Boolean",
3922 "required": false
3923 }
3924 }
3925 }
3926 },
3927 "Photo": {
3928 "memberOfTypes": [ "Album" ],
3929 "shape": {
3930 "type": "Record",
3931 "attributes": {
3932 "foo": {
3933 "type": "Boolean",
3934 "required": false
3935 }
3936 }
3937 }
3938 }
3939 },
3940 "actions": {
3941 "view": {
3942 "appliesTo": {
3943 "principalTypes": ["Photo", "Album"],
3944 "resourceTypes": ["Photo"]
3945 }
3946 }
3947 }
3948 }}"#
3949 )),
3950 Err(SchemaError::ParseJson(_))
3951 );
3952 }
3953}
3954
3955#[cfg(test)]
3956mod ancestors_tests {
3957 use super::*;
3958
3959 #[test]
3960 fn test_ancestors() {
3961 let a_euid: EntityUid = EntityUid::from_strs("test", "A");
3962 let b_euid: EntityUid = EntityUid::from_strs("test", "b");
3963 let c_euid: EntityUid = EntityUid::from_strs("test", "C");
3964 let a = Entity::new(a_euid.clone(), HashMap::new(), HashSet::new());
3965 let b = Entity::new(
3966 b_euid.clone(),
3967 HashMap::new(),
3968 std::iter::once(a_euid.clone()).collect(),
3969 );
3970 let c = Entity::new(
3971 c_euid.clone(),
3972 HashMap::new(),
3973 std::iter::once(b_euid.clone()).collect(),
3974 );
3975 let es = Entities::from_entities([a, b, c]).unwrap();
3976 let ans = es.ancestors(&c_euid).unwrap().collect::<HashSet<_>>();
3977 assert_eq!(ans.len(), 2);
3978 assert!(ans.contains(&b_euid));
3979 assert!(ans.contains(&a_euid));
3980 }
3981}
3982
3983#[allow(clippy::panic)]
3989#[cfg(test)]
3990mod schema_based_parsing_tests {
3991 use std::assert_eq;
3992
3993 use super::*;
3994 use cool_asserts::assert_matches;
3995 use serde_json::json;
3996
3997 #[test]
3999 #[allow(clippy::too_many_lines)]
4000 #[allow(clippy::cognitive_complexity)]
4001 fn attr_types() {
4002 let schema = Schema::from_json_value(json!(
4003 {"": {
4004 "entityTypes": {
4005 "Employee": {
4006 "memberOfTypes": [],
4007 "shape": {
4008 "type": "Record",
4009 "attributes": {
4010 "isFullTime": { "type": "Boolean" },
4011 "numDirectReports": { "type": "Long" },
4012 "department": { "type": "String" },
4013 "manager": { "type": "Entity", "name": "Employee" },
4014 "hr_contacts": { "type": "Set", "element": {
4015 "type": "Entity", "name": "HR" } },
4016 "json_blob": { "type": "Record", "attributes": {
4017 "inner1": { "type": "Boolean" },
4018 "inner2": { "type": "String" },
4019 "inner3": { "type": "Record", "attributes": {
4020 "innerinner": { "type": "Entity", "name": "Employee" }
4021 }}
4022 }},
4023 "home_ip": { "type": "Extension", "name": "ipaddr" },
4024 "work_ip": { "type": "Extension", "name": "ipaddr" },
4025 "trust_score": { "type": "Extension", "name": "decimal" },
4026 "tricky": { "type": "Record", "attributes": {
4027 "type": { "type": "String" },
4028 "id": { "type": "String" }
4029 }}
4030 }
4031 }
4032 },
4033 "HR": {
4034 "memberOfTypes": []
4035 }
4036 },
4037 "actions": {
4038 "view": { }
4039 }
4040 }}
4041 ))
4042 .expect("should be a valid schema");
4043
4044 let entitiesjson = json!(
4045 [
4046 {
4047 "uid": { "type": "Employee", "id": "12UA45" },
4048 "attrs": {
4049 "isFullTime": true,
4050 "numDirectReports": 3,
4051 "department": "Sales",
4052 "manager": { "type": "Employee", "id": "34FB87" },
4053 "hr_contacts": [
4054 { "type": "HR", "id": "aaaaa" },
4055 { "type": "HR", "id": "bbbbb" }
4056 ],
4057 "json_blob": {
4058 "inner1": false,
4059 "inner2": "-*/",
4060 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4061 },
4062 "home_ip": "222.222.222.101",
4063 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4064 "trust_score": "5.7",
4065 "tricky": { "type": "Employee", "id": "34FB87" }
4066 },
4067 "parents": []
4068 }
4069 ]
4070 );
4071 let parsed = Entities::from_json_value(entitiesjson.clone(), None)
4075 .expect("Should parse without error");
4076 assert_eq!(parsed.iter().count(), 1);
4077 let parsed = parsed
4078 .get(&EntityUid::from_strs("Employee", "12UA45"))
4079 .expect("that should be the employee id");
4080 assert_eq!(
4081 parsed.attr("home_ip"),
4082 Some(Ok(EvalResult::String("222.222.222.101".into())))
4083 );
4084 assert_eq!(
4085 parsed.attr("trust_score"),
4086 Some(Ok(EvalResult::String("5.7".into())))
4087 );
4088 assert!(matches!(
4089 parsed.attr("manager"),
4090 Some(Ok(EvalResult::Record(_)))
4091 ));
4092 assert!(matches!(
4093 parsed.attr("work_ip"),
4094 Some(Ok(EvalResult::Record(_)))
4095 ));
4096 {
4097 let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
4098 panic!("expected hr_contacts attr to exist and be a Set")
4099 };
4100 let contact = set.iter().next().expect("should be at least one contact");
4101 assert!(matches!(contact, EvalResult::Record(_)));
4102 };
4103 {
4104 let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
4105 panic!("expected json_blob attr to exist and be a Record")
4106 };
4107 let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
4108 let EvalResult::Record(rec) = inner3 else {
4109 panic!("expected inner3 to be a Record")
4110 };
4111 let innerinner = rec
4112 .get("innerinner")
4113 .expect("expected innerinner attr to exist");
4114 assert!(matches!(innerinner, EvalResult::Record(_)));
4115 };
4116 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4118 .expect("Should parse without error");
4119 assert_eq!(parsed.iter().count(), 1);
4120 let parsed = parsed
4121 .get(&EntityUid::from_strs("Employee", "12UA45"))
4122 .expect("that should be the employee id");
4123 assert_eq!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
4124 assert_eq!(
4125 parsed.attr("numDirectReports"),
4126 Some(Ok(EvalResult::Long(3)))
4127 );
4128 assert_eq!(
4129 parsed.attr("department"),
4130 Some(Ok(EvalResult::String("Sales".into())))
4131 );
4132 assert_eq!(
4133 parsed.attr("manager"),
4134 Some(Ok(EvalResult::EntityUid(EntityUid::from_strs(
4135 "Employee", "34FB87"
4136 ))))
4137 );
4138 {
4139 let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
4140 panic!("expected hr_contacts attr to exist and be a Set")
4141 };
4142 let contact = set.iter().next().expect("should be at least one contact");
4143 assert!(matches!(contact, EvalResult::EntityUid(_)));
4144 };
4145 {
4146 let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
4147 panic!("expected json_blob attr to exist and be a Record")
4148 };
4149 let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
4150 let EvalResult::Record(rec) = inner3 else {
4151 panic!("expected inner3 to be a Record")
4152 };
4153 let innerinner = rec
4154 .get("innerinner")
4155 .expect("expected innerinner attr to exist");
4156 assert!(matches!(innerinner, EvalResult::EntityUid(_)));
4157 };
4158 assert_eq!(
4159 parsed.attr("home_ip"),
4160 Some(Ok(EvalResult::ExtensionValue("222.222.222.101/32".into())))
4161 );
4162 assert_eq!(
4163 parsed.attr("work_ip"),
4164 Some(Ok(EvalResult::ExtensionValue("2.2.2.0/24".into())))
4165 );
4166 assert_eq!(
4167 parsed.attr("trust_score"),
4168 Some(Ok(EvalResult::ExtensionValue("5.7000".into())))
4169 );
4170
4171 let entitiesjson = json!(
4173 [
4174 {
4175 "uid": { "type": "Employee", "id": "12UA45" },
4176 "attrs": {
4177 "isFullTime": true,
4178 "numDirectReports": "3",
4179 "department": "Sales",
4180 "manager": { "type": "Employee", "id": "34FB87" },
4181 "hr_contacts": [
4182 { "type": "HR", "id": "aaaaa" },
4183 { "type": "HR", "id": "bbbbb" }
4184 ],
4185 "json_blob": {
4186 "inner1": false,
4187 "inner2": "-*/",
4188 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4189 },
4190 "home_ip": "222.222.222.101",
4191 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4192 "trust_score": "5.7",
4193 "tricky": { "type": "Employee", "id": "34FB87" }
4194 },
4195 "parents": []
4196 }
4197 ]
4198 );
4199 let err = Entities::from_json_value(entitiesjson, Some(&schema))
4200 .expect_err("should fail due to type mismatch on numDirectReports");
4201 assert!(
4202 err.to_string().contains(r#"in attribute "numDirectReports" on Employee::"12UA45", type mismatch: attribute was expected to have type long, but actually has type string"#),
4203 "actual error message was {err}"
4204 );
4205
4206 let entitiesjson = json!(
4208 [
4209 {
4210 "uid": { "type": "Employee", "id": "12UA45" },
4211 "attrs": {
4212 "isFullTime": true,
4213 "numDirectReports": 3,
4214 "department": "Sales",
4215 "manager": "34FB87",
4216 "hr_contacts": [
4217 { "type": "HR", "id": "aaaaa" },
4218 { "type": "HR", "id": "bbbbb" }
4219 ],
4220 "json_blob": {
4221 "inner1": false,
4222 "inner2": "-*/",
4223 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4224 },
4225 "home_ip": "222.222.222.101",
4226 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4227 "trust_score": "5.7",
4228 "tricky": { "type": "Employee", "id": "34FB87" }
4229 },
4230 "parents": []
4231 }
4232 ]
4233 );
4234 let err = Entities::from_json_value(entitiesjson, Some(&schema))
4235 .expect_err("should fail due to type mismatch on manager");
4236 assert!(
4237 err.to_string()
4238 .contains(r#"in attribute "manager" on Employee::"12UA45", expected a literal entity reference, but got: "34FB87""#),
4239 "actual error message was {err}"
4240 );
4241
4242 let entitiesjson = json!(
4244 [
4245 {
4246 "uid": { "type": "Employee", "id": "12UA45" },
4247 "attrs": {
4248 "isFullTime": true,
4249 "numDirectReports": 3,
4250 "department": "Sales",
4251 "manager": { "type": "Employee", "id": "34FB87" },
4252 "hr_contacts": { "type": "HR", "id": "aaaaa" },
4253 "json_blob": {
4254 "inner1": false,
4255 "inner2": "-*/",
4256 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4257 },
4258 "home_ip": "222.222.222.101",
4259 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4260 "trust_score": "5.7",
4261 "tricky": { "type": "Employee", "id": "34FB87" }
4262 },
4263 "parents": []
4264 }
4265 ]
4266 );
4267 let err = Entities::from_json_value(entitiesjson, Some(&schema))
4268 .expect_err("should fail due to type mismatch on hr_contacts");
4269 assert!(
4270 err.to_string().contains(r#"in attribute "hr_contacts" on Employee::"12UA45", type mismatch: attribute was expected to have type (set of (entity of type HR)), but actually has type record with attributes: ("#),
4271 "actual error message was {err}"
4272 );
4273
4274 let entitiesjson = json!(
4276 [
4277 {
4278 "uid": { "type": "Employee", "id": "12UA45" },
4279 "attrs": {
4280 "isFullTime": true,
4281 "numDirectReports": 3,
4282 "department": "Sales",
4283 "manager": { "type": "HR", "id": "34FB87" },
4284 "hr_contacts": [
4285 { "type": "HR", "id": "aaaaa" },
4286 { "type": "HR", "id": "bbbbb" }
4287 ],
4288 "json_blob": {
4289 "inner1": false,
4290 "inner2": "-*/",
4291 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4292 },
4293 "home_ip": "222.222.222.101",
4294 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4295 "trust_score": "5.7",
4296 "tricky": { "type": "Employee", "id": "34FB87" }
4297 },
4298 "parents": []
4299 }
4300 ]
4301 );
4302 let err = Entities::from_json_value(entitiesjson, Some(&schema))
4303 .expect_err("should fail due to type mismatch on manager");
4304 assert!(
4305 err.to_string().contains(r#"in attribute "manager" on Employee::"12UA45", type mismatch: attribute was expected to have type (entity of type Employee), but actually has type (entity of type HR)"#),
4306 "actual error message was {err}"
4307 );
4308
4309 let entitiesjson = json!(
4312 [
4313 {
4314 "uid": { "type": "Employee", "id": "12UA45" },
4315 "attrs": {
4316 "isFullTime": true,
4317 "numDirectReports": 3,
4318 "department": "Sales",
4319 "manager": { "type": "Employee", "id": "34FB87" },
4320 "hr_contacts": [
4321 { "type": "HR", "id": "aaaaa" },
4322 { "type": "HR", "id": "bbbbb" }
4323 ],
4324 "json_blob": {
4325 "inner1": false,
4326 "inner2": "-*/",
4327 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4328 },
4329 "home_ip": { "fn": "decimal", "arg": "3.33" },
4330 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4331 "trust_score": "5.7",
4332 "tricky": { "type": "Employee", "id": "34FB87" }
4333 },
4334 "parents": []
4335 }
4336 ]
4337 );
4338 let err = Entities::from_json_value(entitiesjson, Some(&schema))
4339 .expect_err("should fail due to type mismatch on home_ip");
4340 assert!(
4341 err.to_string().contains(r#"in attribute "home_ip" on Employee::"12UA45", type mismatch: attribute was expected to have type ipaddr, but actually has type decimal"#),
4342 "actual error message was {err}"
4343 );
4344
4345 let entitiesjson = json!(
4347 [
4348 {
4349 "uid": { "type": "Employee", "id": "12UA45" },
4350 "attrs": {
4351 "isFullTime": true,
4352 "numDirectReports": 3,
4353 "department": "Sales",
4354 "manager": { "type": "Employee", "id": "34FB87" },
4355 "hr_contacts": [
4356 { "type": "HR", "id": "aaaaa" },
4357 { "type": "HR", "id": "bbbbb" }
4358 ],
4359 "json_blob": {
4360 "inner1": false,
4361 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4362 },
4363 "home_ip": "222.222.222.101",
4364 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4365 "trust_score": "5.7",
4366 "tricky": { "type": "Employee", "id": "34FB87" }
4367 },
4368 "parents": []
4369 }
4370 ]
4371 );
4372 let err = Entities::from_json_value(entitiesjson, Some(&schema))
4373 .expect_err("should fail due to missing attribute \"inner2\"");
4374 assert!(
4375 err.to_string().contains(r#"in attribute "json_blob" on Employee::"12UA45", expected the record to have an attribute "inner2", but it doesn't"#),
4376 "actual error message was {err}"
4377 );
4378
4379 let entitiesjson = json!(
4381 [
4382 {
4383 "uid": { "type": "Employee", "id": "12UA45" },
4384 "attrs": {
4385 "isFullTime": true,
4386 "numDirectReports": 3,
4387 "department": "Sales",
4388 "manager": { "type": "Employee", "id": "34FB87" },
4389 "hr_contacts": [
4390 { "type": "HR", "id": "aaaaa" },
4391 { "type": "HR", "id": "bbbbb" }
4392 ],
4393 "json_blob": {
4394 "inner1": 33,
4395 "inner2": "-*/",
4396 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4397 },
4398 "home_ip": "222.222.222.101",
4399 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
4400 "trust_score": "5.7",
4401 "tricky": { "type": "Employee", "id": "34FB87" }
4402 },
4403 "parents": []
4404 }
4405 ]
4406 );
4407 let err = Entities::from_json_value(entitiesjson, Some(&schema))
4408 .expect_err("should fail due to type mismatch on attribute \"inner1\"");
4409 assert!(
4410 err.to_string().contains(r#"in attribute "json_blob" on Employee::"12UA45", type mismatch: attribute was expected to have type record with attributes: "#),
4411 "actual error message was {err}"
4412 );
4413
4414 let entitiesjson = json!(
4415 [
4416 {
4417 "uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
4418 "attrs": {
4419 "isFullTime": true,
4420 "numDirectReports": 3,
4421 "department": "Sales",
4422 "manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
4423 "hr_contacts": [
4424 { "type": "HR", "id": "aaaaa" },
4425 { "type": "HR", "id": "bbbbb" }
4426 ],
4427 "json_blob": {
4428 "inner1": false,
4429 "inner2": "-*/",
4430 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
4431 },
4432 "home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
4433 "work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
4434 "trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
4435 "tricky": { "type": "Employee", "id": "34FB87" }
4436 },
4437 "parents": []
4438 }
4439 ]
4440 );
4441
4442 Entities::from_json_value(entitiesjson, Some(&schema))
4443 .expect("this version with explicit __entity and __extn escapes should also pass");
4444 }
4445
4446 #[test]
4448 fn namespaces() {
4449 let schema = Schema::from_str(
4450 r#"
4451 {"XYZCorp": {
4452 "entityTypes": {
4453 "Employee": {
4454 "memberOfTypes": [],
4455 "shape": {
4456 "type": "Record",
4457 "attributes": {
4458 "isFullTime": { "type": "Boolean" },
4459 "department": { "type": "String" },
4460 "manager": {
4461 "type": "Entity",
4462 "name": "XYZCorp::Employee"
4463 }
4464 }
4465 }
4466 }
4467 },
4468 "actions": {
4469 "view": {}
4470 }
4471 }}
4472 "#,
4473 )
4474 .expect("should be a valid schema");
4475
4476 let entitiesjson = json!(
4477 [
4478 {
4479 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
4480 "attrs": {
4481 "isFullTime": true,
4482 "department": "Sales",
4483 "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
4484 },
4485 "parents": []
4486 }
4487 ]
4488 );
4489 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4490 .expect("Should parse without error");
4491 assert_eq!(parsed.iter().count(), 1);
4492 let parsed = parsed
4493 .get(&EntityUid::from_strs("XYZCorp::Employee", "12UA45"))
4494 .expect("that should be the employee type and id");
4495 assert_eq!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
4496 assert_eq!(
4497 parsed.attr("department"),
4498 Some(Ok(EvalResult::String("Sales".into())))
4499 );
4500 assert_eq!(
4501 parsed.attr("manager"),
4502 Some(Ok(EvalResult::EntityUid(EntityUid::from_strs(
4503 "XYZCorp::Employee",
4504 "34FB87"
4505 ))))
4506 );
4507
4508 let entitiesjson = json!(
4509 [
4510 {
4511 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
4512 "attrs": {
4513 "isFullTime": true,
4514 "department": "Sales",
4515 "manager": { "type": "Employee", "id": "34FB87" }
4516 },
4517 "parents": []
4518 }
4519 ]
4520 );
4521 let err = Entities::from_json_value(entitiesjson, Some(&schema))
4522 .expect_err("should fail due to manager being wrong entity type (missing namespace)");
4523 assert!(
4524 err.to_string().contains(r#"in attribute "manager" on XYZCorp::Employee::"12UA45", type mismatch: attribute was expected to have type (entity of type XYZCorp::Employee), but actually has type (entity of type Employee)"#),
4525 "actual error message was {err}"
4526 );
4527 }
4528
4529 #[test]
4531 fn optional_attrs() {
4532 let schema = Schema::from_str(
4533 r#"
4534 {"": {
4535 "entityTypes": {
4536 "Employee": {
4537 "memberOfTypes": [],
4538 "shape": {
4539 "type": "Record",
4540 "attributes": {
4541 "isFullTime": { "type": "Boolean" },
4542 "department": { "type": "String", "required": false },
4543 "manager": { "type": "Entity", "name": "Employee" }
4544 }
4545 }
4546 }
4547 },
4548 "actions": {
4549 "view": {}
4550 }
4551 }}
4552 "#,
4553 )
4554 .expect("should be a valid schema");
4555
4556 let entitiesjson = json!(
4558 [
4559 {
4560 "uid": { "type": "Employee", "id": "12UA45" },
4561 "attrs": {
4562 "isFullTime": true,
4563 "department": "Sales",
4564 "manager": { "type": "Employee", "id": "34FB87" }
4565 },
4566 "parents": []
4567 }
4568 ]
4569 );
4570 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4571 .expect("Should parse without error");
4572 assert_eq!(parsed.iter().count(), 1);
4573
4574 let entitiesjson = json!(
4576 [
4577 {
4578 "uid": { "type": "Employee", "id": "12UA45" },
4579 "attrs": {
4580 "isFullTime": true,
4581 "manager": { "type": "Employee", "id": "34FB87" }
4582 },
4583 "parents": []
4584 }
4585 ]
4586 );
4587 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4588 .expect("Should parse without error");
4589 assert_eq!(parsed.iter().count(), 1);
4590 }
4591
4592 #[test]
4594 #[should_panic(
4595 expected = "UnsupportedSchemaFeature(\"Records and entities with additional attributes are not yet implemented.\")"
4596 )]
4597 fn open_entities() {
4598 let schema = Schema::from_str(
4599 r#"
4600 {"": {
4601 "entityTypes": {
4602 "Employee": {
4603 "memberOfTypes": [],
4604 "shape": {
4605 "type": "Record",
4606 "attributes": {
4607 "isFullTime": { "type": "Boolean" },
4608 "department": { "type": "String", "required": false },
4609 "manager": { "type": "Entity", "name": "Employee" }
4610 },
4611 "additionalAttributes": true
4612 }
4613 }
4614 },
4615 "actions": {
4616 "view": {}
4617 }
4618 }}
4619 "#,
4620 )
4621 .expect("should be a valid schema");
4622
4623 let entitiesjson = json!(
4625 [
4626 {
4627 "uid": { "type": "Employee", "id": "12UA45" },
4628 "attrs": {
4629 "isFullTime": true,
4630 "department": "Sales",
4631 "manager": { "type": "Employee", "id": "34FB87" }
4632 },
4633 "parents": []
4634 }
4635 ]
4636 );
4637 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4638 .expect("Should parse without error");
4639 assert_eq!(parsed.iter().count(), 1);
4640
4641 let entitiesjson = json!(
4643 [
4644 {
4645 "uid": { "type": "Employee", "id": "12UA45" },
4646 "attrs": {
4647 "isFullTime": true,
4648 "foobar": 234,
4649 "manager": { "type": "Employee", "id": "34FB87" }
4650 },
4651 "parents": []
4652 }
4653 ]
4654 );
4655 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
4656 .expect("Should parse without error");
4657 assert_eq!(parsed.iter().count(), 1);
4658 }
4659
4660 #[test]
4661 fn schema_sanity_check() {
4662 let src = "{ , .. }";
4663 assert_matches!(Schema::from_str(src), Err(super::SchemaError::ParseJson(_)));
4664 }
4665
4666 #[test]
4667 fn template_constraint_sanity_checks() {
4668 assert!(!TemplatePrincipalConstraint::Any.has_slot());
4669 assert!(!TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
4670 assert!(!TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
4671 assert!(TemplatePrincipalConstraint::In(None).has_slot());
4672 assert!(TemplatePrincipalConstraint::Eq(None).has_slot());
4673 assert!(!TemplateResourceConstraint::Any.has_slot());
4674 assert!(!TemplateResourceConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
4675 assert!(!TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
4676 assert!(TemplateResourceConstraint::In(None).has_slot());
4677 assert!(TemplateResourceConstraint::Eq(None).has_slot());
4678 }
4679
4680 #[test]
4681 fn template_principal_constraints() {
4682 let src = r"
4683 permit(principal, action, resource);
4684 ";
4685 let t = Template::parse(None, src).unwrap();
4686 assert_eq!(t.principal_constraint(), TemplatePrincipalConstraint::Any);
4687
4688 let src = r"
4689 permit(principal == ?principal, action, resource);
4690 ";
4691 let t = Template::parse(None, src).unwrap();
4692 assert_eq!(
4693 t.principal_constraint(),
4694 TemplatePrincipalConstraint::Eq(None)
4695 );
4696
4697 let src = r#"
4698 permit(principal == A::"a", action, resource);
4699 "#;
4700 let t = Template::parse(None, src).unwrap();
4701 assert_eq!(
4702 t.principal_constraint(),
4703 TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("A", "a")))
4704 );
4705
4706 let src = r"
4707 permit(principal in ?principal, action, resource);
4708 ";
4709 let t = Template::parse(None, src).unwrap();
4710 assert_eq!(
4711 t.principal_constraint(),
4712 TemplatePrincipalConstraint::In(None)
4713 );
4714
4715 let src = r#"
4716 permit(principal in A::"a", action, resource);
4717 "#;
4718 let t = Template::parse(None, src).unwrap();
4719 assert_eq!(
4720 t.principal_constraint(),
4721 TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("A", "a")))
4722 );
4723 }
4724
4725 #[test]
4726 fn template_action_constraints() {
4727 let src = r"
4728 permit(principal, action, resource);
4729 ";
4730 let t = Template::parse(None, src).unwrap();
4731 assert_eq!(t.action_constraint(), ActionConstraint::Any);
4732
4733 let src = r#"
4734 permit(principal, action == Action::"A", resource);
4735 "#;
4736 let t = Template::parse(None, src).unwrap();
4737 assert_eq!(
4738 t.action_constraint(),
4739 ActionConstraint::Eq(EntityUid::from_strs("Action", "A"))
4740 );
4741
4742 let src = r#"
4743 permit(principal, action in [Action::"A", Action::"B"], resource);
4744 "#;
4745 let t = Template::parse(None, src).unwrap();
4746 assert_eq!(
4747 t.action_constraint(),
4748 ActionConstraint::In(vec![
4749 EntityUid::from_strs("Action", "A"),
4750 EntityUid::from_strs("Action", "B")
4751 ])
4752 );
4753 }
4754
4755 #[test]
4756 fn template_resource_constraints() {
4757 let src = r"
4758 permit(principal, action, resource);
4759 ";
4760 let t = Template::parse(None, src).unwrap();
4761 assert_eq!(t.resource_constraint(), TemplateResourceConstraint::Any);
4762
4763 let src = r"
4764 permit(principal, action, resource == ?resource);
4765 ";
4766 let t = Template::parse(None, src).unwrap();
4767 assert_eq!(
4768 t.resource_constraint(),
4769 TemplateResourceConstraint::Eq(None)
4770 );
4771
4772 let src = r#"
4773 permit(principal, action, resource == A::"a");
4774 "#;
4775 let t = Template::parse(None, src).unwrap();
4776 assert_eq!(
4777 t.resource_constraint(),
4778 TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("A", "a")))
4779 );
4780
4781 let src = r"
4782 permit(principal, action, resource in ?resource);
4783 ";
4784 let t = Template::parse(None, src).unwrap();
4785 assert_eq!(
4786 t.resource_constraint(),
4787 TemplateResourceConstraint::In(None)
4788 );
4789
4790 let src = r#"
4791 permit(principal, action, resource in A::"a");
4792 "#;
4793 let t = Template::parse(None, src).unwrap();
4794 assert_eq!(
4795 t.resource_constraint(),
4796 TemplateResourceConstraint::In(Some(EntityUid::from_strs("A", "a")))
4797 );
4798 }
4799
4800 #[test]
4801 fn schema_namespace() {
4802 let fragment: SchemaFragment = r#"
4803 {
4804 "Foo::Bar": {
4805 "entityTypes": {},
4806 "actions": {}
4807 }
4808 }
4809 "#
4810 .parse()
4811 .unwrap();
4812 let namespaces = fragment.namespaces().next().unwrap();
4813 assert_eq!(
4814 namespaces.map(|ns| ns.to_string()),
4815 Some("Foo::Bar".to_string())
4816 );
4817 let _schema: Schema = fragment.try_into().expect("Should convert to schema");
4818
4819 let fragment: SchemaFragment = r#"
4820 {
4821 "": {
4822 "entityTypes": {},
4823 "actions": {}
4824 }
4825 }
4826 "#
4827 .parse()
4828 .unwrap();
4829 let namespaces = fragment.namespaces().next().unwrap();
4830 assert_eq!(namespaces, None);
4831 let _schema: Schema = fragment.try_into().expect("Should convert to schema");
4832 }
4833
4834 #[test]
4835 fn load_multiple_namespaces() {
4836 let fragment = SchemaFragment::from_json_value(json!({
4837 "Foo::Bar": {
4838 "entityTypes": {
4839 "Baz": {
4840 "memberOfTypes": ["Bar::Foo::Baz"]
4841 }
4842 },
4843 "actions": {}
4844 },
4845 "Bar::Foo": {
4846 "entityTypes": {
4847 "Baz": {
4848 "memberOfTypes": ["Foo::Bar::Baz"]
4849 }
4850 },
4851 "actions": {}
4852 }
4853 }))
4854 .unwrap();
4855
4856 let schema = Schema::from_schema_fragments([fragment]).unwrap();
4857
4858 assert!(schema
4859 .0
4860 .get_entity_type(&"Foo::Bar::Baz".parse().unwrap())
4861 .is_some());
4862 assert!(schema
4863 .0
4864 .get_entity_type(&"Bar::Foo::Baz".parse().unwrap())
4865 .is_some());
4866 }
4867
4868 #[test]
4869 fn get_attributes_from_schema() {
4870 let fragment: SchemaFragment = SchemaFragment::from_json_value(json!({
4871 "": {
4872 "entityTypes": {},
4873 "actions": {
4874 "A": {},
4875 "B": {
4876 "memberOf": [{"id": "A"}]
4877 },
4878 "C": {
4879 "memberOf": [{"id": "A"}]
4880 },
4881 "D": {
4882 "memberOf": [{"id": "B"}, {"id": "C"}]
4883 },
4884 "E": {
4885 "memberOf": [{"id": "D"}]
4886 }
4887 }
4888 }}))
4889 .unwrap();
4890
4891 let schema = Schema::from_schema_fragments([fragment]).unwrap();
4892 let action_entities = schema.action_entities().unwrap();
4893
4894 let a_euid = EntityUid::from_strs("Action", "A");
4895 let b_euid = EntityUid::from_strs("Action", "B");
4896 let c_euid = EntityUid::from_strs("Action", "C");
4897 let d_euid = EntityUid::from_strs("Action", "D");
4898 let e_euid = EntityUid::from_strs("Action", "E");
4899
4900 assert_eq!(
4901 action_entities,
4902 Entities::from_entities([
4903 Entity::new(a_euid.clone(), HashMap::new(), HashSet::new()),
4904 Entity::new(
4905 b_euid.clone(),
4906 HashMap::new(),
4907 HashSet::from([a_euid.clone()])
4908 ),
4909 Entity::new(
4910 c_euid.clone(),
4911 HashMap::new(),
4912 HashSet::from([a_euid.clone()])
4913 ),
4914 Entity::new(
4915 d_euid.clone(),
4916 HashMap::new(),
4917 HashSet::from([a_euid.clone(), b_euid.clone(), c_euid.clone()])
4918 ),
4919 Entity::new(
4920 e_euid,
4921 HashMap::new(),
4922 HashSet::from([a_euid, b_euid, c_euid, d_euid])
4923 ),
4924 ])
4925 .unwrap()
4926 );
4927 }
4928
4929 #[test]
4933 fn issue_285() {
4934 let schema = Schema::from_json_value(json!(
4935 {"": {
4936 "entityTypes": {},
4937 "actions": {
4938 "A": {},
4939 "B": {
4940 "memberOf": [{"id": "A"}]
4941 },
4942 "C": {
4943 "memberOf": [{"id": "B"}]
4944 }
4945 }
4946 }}
4947 ))
4948 .expect("should be a valid schema");
4949
4950 let entitiesjson_tc = json!(
4951 [
4952 {
4953 "uid": { "type": "Action", "id": "A" },
4954 "attrs": {},
4955 "parents": []
4956 },
4957 {
4958 "uid": { "type": "Action", "id": "B" },
4959 "attrs": {},
4960 "parents": [
4961 { "type": "Action", "id": "A" }
4962 ]
4963 },
4964 {
4965 "uid": { "type": "Action", "id": "C" },
4966 "attrs": {},
4967 "parents": [
4968 { "type": "Action", "id": "A" },
4969 { "type": "Action", "id": "B" }
4970 ]
4971 }
4972 ]
4973 );
4974
4975 let entitiesjson_no_tc = json!(
4976 [
4977 {
4978 "uid": { "type": "Action", "id": "A" },
4979 "attrs": {},
4980 "parents": []
4981 },
4982 {
4983 "uid": { "type": "Action", "id": "B" },
4984 "attrs": {},
4985 "parents": [
4986 { "type": "Action", "id": "A" }
4987 ]
4988 },
4989 {
4990 "uid": { "type": "Action", "id": "C" },
4991 "attrs": {},
4992 "parents": [
4993 { "type": "Action", "id": "B" }
4994 ]
4995 }
4996 ]
4997 );
4998
4999 assert!(Entities::from_json_value(entitiesjson_tc, Some(&schema)).is_ok());
5001 assert!(Entities::from_json_value(entitiesjson_no_tc.clone(), Some(&schema)).is_ok());
5002
5003 let entitiesjson_bad = json!(
5005 [
5006 {
5007 "uid": { "type": "Action", "id": "A" },
5008 "attrs": {},
5009 "parents": []
5010 },
5011 {
5012 "uid": { "type": "Action", "id": "B" },
5013 "attrs": {},
5014 "parents": [
5015 { "type": "Action", "id": "A" }
5016 ]
5017 },
5018 {
5019 "uid": { "type": "Action", "id": "C" },
5020 "attrs": {},
5021 "parents": [
5022 { "type": "Action", "id": "A" }
5023 ]
5024 }
5025 ]
5026 );
5027 assert!(matches!(
5028 Entities::from_json_value(entitiesjson_bad, Some(&schema)),
5029 Err(EntitiesError::Deserialization(
5030 entities::JsonDeserializationError::ActionDeclarationMismatch { uid: _ }
5031 ))
5032 ));
5033
5034 let parser_assume_computed = entities::EntityJsonParser::new(
5036 Some(cedar_policy_validator::CoreSchema::new(&schema.0)),
5037 Extensions::all_available(),
5038 entities::TCComputation::AssumeAlreadyComputed,
5039 );
5040 assert!(matches!(
5041 parser_assume_computed.from_json_value(entitiesjson_no_tc.clone()),
5042 Err(EntitiesError::Deserialization(
5043 entities::JsonDeserializationError::ActionDeclarationMismatch { uid: _ }
5044 ))
5045 ));
5046
5047 let parser_enforce_computed = entities::EntityJsonParser::new(
5048 Some(cedar_policy_validator::CoreSchema::new(&schema.0)),
5049 extensions::Extensions::all_available(),
5050 entities::TCComputation::EnforceAlreadyComputed,
5051 );
5052 assert!(matches!(
5053 parser_enforce_computed.from_json_value(entitiesjson_no_tc),
5054 Err(EntitiesError::TransitiveClosureError(_))
5055 ));
5056 }
5057}
5058#[cfg(test)]
5059#[allow(clippy::unwrap_used)]
5061mod test {
5062 use super::*;
5063
5064 #[test]
5065 fn test_all_ints() {
5066 test_single_int(0);
5067 test_single_int(i64::MAX);
5068 test_single_int(i64::MIN);
5069 test_single_int(7);
5070 test_single_int(-7);
5071 }
5072
5073 fn test_single_int(x: i64) {
5074 for i in 0..4 {
5075 test_single_int_with_dashes(x, i);
5076 }
5077 }
5078
5079 fn test_single_int_with_dashes(x: i64, num_dashes: usize) {
5080 let dashes = vec!['-'; num_dashes].into_iter().collect::<String>();
5081 let src = format!(r#"permit(principal, action, resource) when {{ {dashes}{x} }};"#);
5082 let p: Policy = src.parse().unwrap();
5083 let json = p.to_json().unwrap();
5084 let round_trip = Policy::from_json(None, json).unwrap();
5085 let pretty_print = format!("{round_trip}");
5086 assert!(pretty_print.contains(&x.to_string()));
5087 if x != 0 {
5088 let expected_dashes = if x < 0 { num_dashes + 1 } else { num_dashes };
5089 assert_eq!(
5090 pretty_print.chars().filter(|c| *c == '-').count(),
5091 expected_dashes
5092 );
5093 }
5094 }
5095
5096 #[test]
5098 fn json_bignum_1() {
5099 let src = r#"
5100 permit(
5101 principal,
5102 action == Action::"action",
5103 resource
5104 ) when {
5105 -9223372036854775808
5106 };"#;
5107 let p: Policy = src.parse().unwrap();
5108 p.to_json().unwrap();
5109 }
5110
5111 #[test]
5112 fn json_bignum_1a() {
5113 let src = r"
5114 permit(principal, action, resource) when {
5115 (true && (-90071992547409921)) && principal
5116 };";
5117 let p: Policy = src.parse().unwrap();
5118 let v = p.to_json().unwrap();
5119 let s = serde_json::to_string(&v).unwrap();
5120 assert!(s.contains("90071992547409921"));
5121 }
5122
5123 #[test]
5125 fn json_bignum_2() {
5126 let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":90071992547409921}}}}]}"#;
5127 let v: serde_json::Value = serde_json::from_str(src).unwrap();
5128 let p = Policy::from_json(None, v).unwrap();
5129 let pretty = format!("{p}");
5130 assert!(pretty.contains("90071992547409921"));
5132 }
5133
5134 #[test]
5136 fn json_bignum_2a() {
5137 let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":-9223372036854775808}}}}]}"#;
5138 let v: serde_json::Value = serde_json::from_str(src).unwrap();
5139 let p = Policy::from_json(None, v).unwrap();
5140 let pretty = format!("{p}");
5141 assert!(pretty.contains("-9223372036854775808"));
5143 }
5144
5145 #[test]
5148 fn json_bignum_3() {
5149 let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":9223372036854775808}}}}]}"#;
5150 let v: serde_json::Value = serde_json::from_str(src).unwrap();
5151 assert!(Policy::from_json(None, v).is_err());
5152 }
5153}
5154
5155#[cfg(test)]
5156mod issue_606 {
5157 use std::str::FromStr;
5158
5159 use cedar_policy_core::est::EstToAstError;
5160
5161 use crate::{PolicyId, Template};
5162
5163 #[test]
5164 fn est_template() {
5165 let est_json = serde_json::json!({
5166 "effect": "permit",
5167 "principal": { "op": "All" },
5168 "action": { "op": "All" },
5169 "resource": { "op": "All" },
5170 "conditions": [
5171 {
5172 "kind": "when",
5173 "body": {
5174 "==": {
5175 "left": { "Var": "principal" },
5176 "right": { "Slot": "?principal" }
5177 }
5178 }
5179 }
5180 ]
5181 });
5182
5183 let tid = PolicyId::from_str("t0").unwrap();
5184 let template = Template::from_json(Some(tid), est_json);
5186 assert!(matches!(
5187 template,
5188 Err(EstToAstError::SlotsInConditionClause {
5189 slot: _,
5190 clausetype: "when"
5191 })
5192 ));
5193 }
5194}
5195
5196#[cfg(test)]
5197mod issue_604 {
5198 use crate::Policy;
5199 use cedar_policy_core::parser::parse_policy_or_template_to_est;
5200 use cool_asserts::assert_matches;
5201 #[track_caller]
5202 fn to_json_is_ok(text: &str) {
5203 let policy = Policy::parse(None, text).unwrap();
5204 let json = policy.to_json();
5205 assert_matches!(json, Ok(_));
5206 }
5207
5208 #[track_caller]
5209 fn make_policy_with_get_attr(attr: &str) -> String {
5210 format!(
5211 r#"
5212 permit(principal, action, resource) when {{ principal == resource.{attr} }};
5213 "#
5214 )
5215 }
5216
5217 #[track_caller]
5218 fn make_policy_with_has_attr(attr: &str) -> String {
5219 format!(
5220 r#"
5221 permit(principal, action, resource) when {{ resource has {attr} }};
5222 "#
5223 )
5224 }
5225
5226 #[test]
5227 fn var_as_attribute_name() {
5228 for attr in ["principal", "action", "resource", "context"] {
5229 to_json_is_ok(&make_policy_with_get_attr(attr));
5230 to_json_is_ok(&make_policy_with_has_attr(attr));
5231 }
5232 }
5233
5234 #[track_caller]
5235 fn is_valid_est(text: &str) {
5236 let est = parse_policy_or_template_to_est(text);
5237 assert_matches!(est, Ok(_));
5238 }
5239
5240 #[track_caller]
5241 fn is_invalid_est(text: &str) {
5242 let est = parse_policy_or_template_to_est(text);
5243 assert_matches!(est, Err(_));
5244 }
5245
5246 #[test]
5247 fn keyword_as_attribute_name_err() {
5248 for attr in ["true", "false", "if", "then", "else", "in", "like", "has"] {
5249 is_invalid_est(&make_policy_with_get_attr(attr));
5250 is_invalid_est(&make_policy_with_has_attr(attr));
5251 }
5252 }
5253
5254 #[test]
5255 fn keyword_as_attribute_name_ok() {
5256 for attr in ["permit", "forbid", "when", "unless", "_"] {
5257 is_valid_est(&make_policy_with_get_attr(attr));
5258 is_valid_est(&make_policy_with_has_attr(attr));
5259 }
5260 }
5261}