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)]
84#[derive(Debug, Clone, PartialEq, Eq, RefCast)]
85pub struct Entity(ast::Entity);
86
87impl Entity {
88 pub fn new(
93 uid: EntityUid,
94 attrs: HashMap<String, RestrictedExpression>,
95 parents: HashSet<EntityUid>,
96 ) -> Self {
97 Self(ast::Entity::new(
100 uid.0,
101 attrs
102 .into_iter()
103 .map(|(k, v)| (SmolStr::from(k), v.0))
104 .collect(),
105 parents.into_iter().map(|uid| uid.0).collect(),
106 ))
107 }
108
109 pub fn with_uid(uid: EntityUid) -> Self {
111 Self(ast::Entity::with_uid(uid.0))
112 }
113
114 pub fn uid(&self) -> EntityUid {
116 EntityUid(self.0.uid())
117 }
118
119 pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, EvaluationError>> {
123 let expr = self.0.get(attr)?;
124 let all_ext = Extensions::all_available();
125 let evaluator = RestrictedEvaluator::new(&all_ext);
126 Some(
127 evaluator
128 .interpret(expr.as_borrowed())
129 .map(EvalResult::from)
130 .map_err(|e| EvaluationError::StringMessage(e.to_string())),
131 )
132 }
133}
134
135impl std::fmt::Display for Entity {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 write!(f, "{}", self.0)
138 }
139}
140
141#[repr(transparent)]
144#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
145pub struct Entities(pub(crate) entities::Entities);
146
147pub use entities::EntitiesError;
148
149impl Entities {
150 pub fn empty() -> Self {
152 Self(entities::Entities::new())
153 }
154
155 pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
157 match self.0.entity(&uid.0) {
158 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
159 Dereference::Data(e) => Some(Entity::ref_cast(e)),
160 }
161 }
162
163 #[must_use]
167 #[cfg(feature = "partial-eval")]
168 pub fn partial(self) -> Self {
169 Self(self.0.partial())
170 }
171
172 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
174 self.0.iter().map(Entity::ref_cast)
175 }
176
177 pub fn from_entities(
180 entities: impl IntoIterator<Item = Entity>,
181 ) -> Result<Self, entities::EntitiesError> {
182 entities::Entities::from_entities(
183 entities.into_iter().map(|e| e.0),
184 entities::TCComputation::ComputeNow,
185 )
186 .map(Entities)
187 }
188
189 pub fn from_json_str(
195 json: &str,
196 schema: Option<&Schema>,
197 ) -> Result<Self, entities::EntitiesError> {
198 let eparser = entities::EntityJsonParser::new(
199 schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
200 Extensions::all_available(),
201 entities::TCComputation::ComputeNow,
202 );
203 eparser.from_json_str(json).map(Entities)
204 }
205
206 pub fn from_json_value(
213 json: serde_json::Value,
214 schema: Option<&Schema>,
215 ) -> Result<Self, entities::EntitiesError> {
216 let eparser = entities::EntityJsonParser::new(
217 schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
218 Extensions::all_available(),
219 entities::TCComputation::ComputeNow,
220 );
221 eparser.from_json_value(json).map(Entities)
222 }
223
224 pub fn from_json_file(
231 json: impl std::io::Read,
232 schema: Option<&Schema>,
233 ) -> Result<Self, entities::EntitiesError> {
234 let eparser = entities::EntityJsonParser::new(
235 schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0)),
236 Extensions::all_available(),
237 entities::TCComputation::ComputeNow,
238 );
239 eparser.from_json_file(json).map(Entities)
240 }
241
242 pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
245 match self.0.entity(&b.0) {
246 Dereference::Data(b) => b.is_descendant_of(&a.0),
247 _ => a == b, }
249 }
250
251 pub fn ancestors<'a>(
254 &'a self,
255 euid: &EntityUid,
256 ) -> Option<impl Iterator<Item = &'a EntityUid>> {
257 let entity = match self.0.entity(&euid.0) {
258 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
259 Dereference::Data(e) => Some(e),
260 }?;
261 Some(entity.ancestors().map(EntityUid::ref_cast))
262 }
263
264 pub fn write_to_json(
272 &self,
273 f: impl std::io::Write,
274 ) -> std::result::Result<(), entities::EntitiesError> {
275 self.0.write_to_json(f)
276 }
277}
278
279#[repr(transparent)]
281#[derive(Debug, RefCast)]
282pub struct Authorizer(authorizer::Authorizer);
283
284impl Default for Authorizer {
285 fn default() -> Self {
286 Self::new()
287 }
288}
289
290impl Authorizer {
291 pub fn new() -> Self {
293 Self(authorizer::Authorizer::new())
294 }
295
296 pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
302 self.0.is_authorized(&r.0, &p.ast, &e.0).into()
303 }
304
305 #[cfg(feature = "partial-eval")]
310 pub fn is_authorized_partial(
311 &self,
312 query: &Request,
313 policy_set: &PolicySet,
314 entities: &Entities,
315 ) -> PartialResponse {
316 let response = self
317 .0
318 .is_authorized_core(&query.0, &policy_set.ast, &entities.0);
319 match response {
320 authorizer::ResponseKind::FullyEvaluated(a) => PartialResponse::Concrete(a.into()),
321 authorizer::ResponseKind::Partial(p) => PartialResponse::Residual(p.into()),
322 }
323 }
324}
325
326#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
328pub struct Response {
329 decision: Decision,
331 diagnostics: Diagnostics,
333}
334
335#[cfg(feature = "partial-eval")]
338#[derive(Debug, PartialEq, Clone)]
339pub enum PartialResponse {
340 Concrete(Response),
342 Residual(ResidualResponse),
344}
345
346#[cfg(feature = "partial-eval")]
348#[derive(Debug, PartialEq, Eq, Clone)]
349pub struct ResidualResponse {
350 residuals: PolicySet,
352 diagnostics: Diagnostics,
354}
355
356#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
358pub struct Diagnostics {
359 reason: HashSet<PolicyId>,
362 errors: HashSet<String>,
364}
365
366impl From<authorizer::Diagnostics> for Diagnostics {
367 fn from(diagnostics: authorizer::Diagnostics) -> Self {
368 Self {
369 reason: diagnostics.reason.into_iter().map(PolicyId).collect(),
370 errors: diagnostics.errors.iter().map(ToString::to_string).collect(),
371 }
372 }
373}
374
375impl Diagnostics {
376 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
378 self.reason.iter()
379 }
380
381 pub fn errors(&self) -> impl Iterator<Item = EvaluationError> + '_ {
383 self.errors
384 .iter()
385 .cloned()
386 .map(EvaluationError::StringMessage)
387 }
388}
389
390impl Response {
391 pub fn new(decision: Decision, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
393 Self {
394 decision,
395 diagnostics: Diagnostics { reason, errors },
396 }
397 }
398
399 pub fn decision(&self) -> Decision {
401 self.decision
402 }
403
404 pub fn diagnostics(&self) -> &Diagnostics {
406 &self.diagnostics
407 }
408}
409
410impl From<authorizer::Response> for Response {
411 fn from(a: authorizer::Response) -> Self {
412 Self {
413 decision: a.decision,
414 diagnostics: a.diagnostics.into(),
415 }
416 }
417}
418
419#[cfg(feature = "partial-eval")]
420impl ResidualResponse {
421 pub fn new(residuals: PolicySet, reason: HashSet<PolicyId>, errors: HashSet<String>) -> Self {
423 Self {
424 residuals,
425 diagnostics: Diagnostics { reason, errors },
426 }
427 }
428
429 pub fn residuals(&self) -> &PolicySet {
431 &self.residuals
432 }
433
434 pub fn diagnostics(&self) -> &Diagnostics {
436 &self.diagnostics
437 }
438}
439
440#[cfg(feature = "partial-eval")]
441impl From<authorizer::PartialResponse> for ResidualResponse {
442 fn from(p: authorizer::PartialResponse) -> Self {
443 Self {
444 residuals: PolicySet::from_ast(p.residuals),
445 diagnostics: p.diagnostics.into(),
446 }
447 }
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Error)]
453pub enum EvaluationError {
454 #[error("{0}")]
457 StringMessage(String),
458}
459
460#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
462#[non_exhaustive]
463pub enum ValidationMode {
464 #[default]
467 Strict,
468 Permissive,
470}
471
472impl From<ValidationMode> for cedar_policy_validator::ValidationMode {
473 fn from(mode: ValidationMode) -> Self {
474 match mode {
475 ValidationMode::Strict => Self::Strict,
476 ValidationMode::Permissive => Self::Permissive,
477 }
478 }
479}
480
481#[repr(transparent)]
483#[derive(Debug, RefCast)]
484pub struct Validator(cedar_policy_validator::Validator);
485
486impl Validator {
487 pub fn new(schema: Schema) -> Self {
490 Self(cedar_policy_validator::Validator::new(schema.0))
491 }
492
493 pub fn validate<'a>(
501 &'a self,
502 pset: &'a PolicySet,
503 mode: ValidationMode,
504 ) -> ValidationResult<'a> {
505 ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
506 }
507}
508
509#[derive(Debug)]
512pub struct SchemaFragment(cedar_policy_validator::ValidatorSchemaFragment);
513
514impl SchemaFragment {
515 pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
519 self.0
520 .namespaces()
521 .map(|ns| ns.as_ref().map(|ns| EntityNamespace(ns.clone())))
522 }
523
524 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
527 Ok(Self(
528 cedar_policy_validator::SchemaFragment::from_json_value(json)?.try_into()?,
529 ))
530 }
531
532 pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
534 Ok(Self(
535 cedar_policy_validator::SchemaFragment::from_file(file)?.try_into()?,
536 ))
537 }
538}
539
540impl TryInto<Schema> for SchemaFragment {
541 type Error = SchemaError;
542
543 fn try_into(self) -> Result<Schema, Self::Error> {
547 Ok(Schema(
548 cedar_policy_validator::ValidatorSchema::from_schema_fragments([self.0])?,
549 ))
550 }
551}
552
553impl FromStr for SchemaFragment {
554 type Err = SchemaError;
555 fn from_str(src: &str) -> Result<Self, Self::Err> {
562 Ok(Self(
563 serde_json::from_str::<cedar_policy_validator::SchemaFragment>(src)
564 .map_err(cedar_policy_validator::SchemaError::from)?
565 .try_into()?,
566 ))
567 }
568}
569
570#[repr(transparent)]
572#[derive(Debug, Clone, RefCast)]
573pub struct Schema(pub(crate) cedar_policy_validator::ValidatorSchema);
574
575impl FromStr for Schema {
576 type Err = SchemaError;
577
578 fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
585 Ok(Self(schema_src.parse()?))
586 }
587}
588
589impl Schema {
590 pub fn from_schema_fragments(
595 fragments: impl IntoIterator<Item = SchemaFragment>,
596 ) -> Result<Self, SchemaError> {
597 Ok(Self(
598 cedar_policy_validator::ValidatorSchema::from_schema_fragments(
599 fragments.into_iter().map(|f| f.0),
600 )?,
601 ))
602 }
603
604 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
607 Ok(Self(
608 cedar_policy_validator::ValidatorSchema::from_json_value(json)?,
609 ))
610 }
611
612 pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
614 Ok(Self(cedar_policy_validator::ValidatorSchema::from_file(
615 file,
616 )?))
617 }
618
619 pub fn action_entities(&self) -> Result<Entities, entities::EntitiesError> {
622 Ok(Entities(self.0.action_entities()?))
623 }
624}
625
626#[derive(Debug, Error)]
628pub enum SchemaError {
629 #[error("JSON Schema file could not be parsed: {0}")]
631 ParseJson(serde_json::Error),
632 #[error("Transitive closure error on action hierarchy: {0}")]
635 ActionTransitiveClosureError(String),
636 #[error("Transitive closure error on entity hierarchy: {0}")]
639 EntityTransitiveClosureError(String),
640 #[error("Unsupported feature used in schema: {0}")]
643 UnsupportedSchemaFeature(String),
644 #[error("Undeclared entity types: {0:?}")]
647 UndeclaredEntityTypes(HashSet<String>),
648 #[error("Undeclared actions: {0:?}")]
650 UndeclaredActions(HashSet<String>),
651 #[error("Undeclared common types: {0:?}")]
653 UndeclaredCommonType(HashSet<String>),
654 #[error("Duplicate entity type {0}")]
657 DuplicateEntityType(String),
658 #[error("Duplicate action {0}")]
661 DuplicateAction(String),
662 #[error("Duplicate common type {0}")]
665 DuplicateCommonType(String),
666 #[error("Cycle in action hierarchy")]
668 CycleInActionHierarchy,
669 #[error("Parse error in entity type: {0}")]
671 EntityTypeParse(ParseErrors),
672 #[error("Parse error in namespace identifier: {0}")]
674 NamespaceParse(ParseErrors),
675 #[error("Parse error in common type identifier: {0}")]
677 CommonTypeParseError(ParseErrors),
678 #[error("Parse error in extension type: {0}")]
680 ExtensionTypeParse(ParseErrors),
681 #[error("Entity type `Action` declared in `entityTypes` list.")]
686 ActionEntityTypeDeclared,
687 #[error("Actions declared with `attributes`: [{}]", .0.iter().map(String::as_str).join(", "))]
690 ActionEntityAttributes(Vec<String>),
691 #[error("Action context or entity type shape is not a record")]
694 ContextOrShapeNotRecord,
695 #[error("Action attribute is an empty set")]
697 ActionEntityAttributeEmptySet,
698 #[error(
700 "Action has an attribute of unsupported type (escaped expression, entity or extension)"
701 )]
702 ActionEntityAttributeUnsupportedType,
703}
704
705#[doc(hidden)]
706impl From<cedar_policy_validator::SchemaError> for SchemaError {
707 fn from(value: cedar_policy_validator::SchemaError) -> Self {
708 match value {
709 cedar_policy_validator::SchemaError::ParseFileFormat(e) => Self::ParseJson(e),
710 cedar_policy_validator::SchemaError::ActionTransitiveClosureError(e) => {
711 Self::ActionTransitiveClosureError(e.to_string())
712 }
713 cedar_policy_validator::SchemaError::EntityTransitiveClosureError(e) => {
714 Self::EntityTransitiveClosureError(e.to_string())
715 }
716 cedar_policy_validator::SchemaError::UnsupportedSchemaFeature(e) => {
717 Self::UnsupportedSchemaFeature(e.to_string())
718 }
719 cedar_policy_validator::SchemaError::UndeclaredEntityTypes(e) => {
720 Self::UndeclaredEntityTypes(e)
721 }
722 cedar_policy_validator::SchemaError::UndeclaredActions(e) => Self::UndeclaredActions(e),
723 cedar_policy_validator::SchemaError::UndeclaredCommonType(c) => {
724 Self::UndeclaredCommonType(c)
725 }
726 cedar_policy_validator::SchemaError::DuplicateEntityType(e) => {
727 Self::DuplicateEntityType(e)
728 }
729 cedar_policy_validator::SchemaError::DuplicateAction(e) => Self::DuplicateAction(e),
730 cedar_policy_validator::SchemaError::DuplicateCommonType(c) => {
731 Self::DuplicateCommonType(c)
732 }
733 cedar_policy_validator::SchemaError::CycleInActionHierarchy => {
734 Self::CycleInActionHierarchy
735 }
736 cedar_policy_validator::SchemaError::EntityTypeParseError(e) => {
737 Self::EntityTypeParse(ParseErrors(e))
738 }
739 cedar_policy_validator::SchemaError::NamespaceParseError(e) => {
740 Self::NamespaceParse(ParseErrors(e))
741 }
742 cedar_policy_validator::SchemaError::CommonTypeParseError(e) => {
743 Self::CommonTypeParseError(ParseErrors(e))
744 }
745 cedar_policy_validator::SchemaError::ExtensionTypeParseError(e) => {
746 Self::ExtensionTypeParse(ParseErrors(e))
747 }
748 cedar_policy_validator::SchemaError::ActionEntityTypeDeclared => {
749 Self::ActionEntityTypeDeclared
750 }
751 cedar_policy_validator::SchemaError::ActionEntityAttributes(e) => {
752 Self::ActionEntityAttributes(e)
753 }
754 cedar_policy_validator::SchemaError::ContextOrShapeNotRecord(_) => {
755 Self::ContextOrShapeNotRecord
756 }
757 cedar_policy_validator::SchemaError::ActionEntityAttributeEmptySet => {
758 Self::ActionEntityAttributeEmptySet
759 }
760 cedar_policy_validator::SchemaError::ActionEntityAttributeUnsupportedType => {
761 Self::ActionEntityAttributeUnsupportedType
762 }
763 }
764 }
765}
766
767#[derive(Debug)]
772pub struct ValidationResult<'a> {
773 validation_errors: Vec<ValidationError<'a>>,
774}
775
776impl<'a> ValidationResult<'a> {
777 pub fn validation_passed(&self) -> bool {
779 self.validation_errors.is_empty()
780 }
781
782 pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError<'a>> {
784 self.validation_errors.iter()
785 }
786}
787
788impl<'a> From<cedar_policy_validator::ValidationResult<'a>> for ValidationResult<'a> {
789 fn from(r: cedar_policy_validator::ValidationResult<'a>) -> Self {
790 Self {
791 validation_errors: r
792 .into_validation_errors()
793 .map(ValidationError::from)
794 .collect(),
795 }
796 }
797}
798
799#[derive(Debug, Error)]
804pub struct ValidationError<'a> {
805 location: SourceLocation<'a>,
806 error_kind: ValidationErrorKind,
807}
808
809impl<'a> ValidationError<'a> {
810 pub fn error_kind(&self) -> &ValidationErrorKind {
812 &self.error_kind
813 }
814
815 pub fn location(&self) -> &SourceLocation<'a> {
817 &self.location
818 }
819}
820
821impl<'a> From<cedar_policy_validator::ValidationError<'a>> for ValidationError<'a> {
822 fn from(err: cedar_policy_validator::ValidationError<'a>) -> Self {
823 let (location, error_kind) = err.into_location_and_error_kind();
824 Self {
825 location: SourceLocation::from(location),
826 error_kind,
827 }
828 }
829}
830
831impl<'a> std::fmt::Display for ValidationError<'a> {
832 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
833 write!(f, "Validation error on policy {}", self.location.policy_id)?;
834 if let (Some(range_start), Some(range_end)) =
835 (self.location().range_start(), self.location().range_end())
836 {
837 write!(f, " at offset {range_start}-{range_end}")?;
838 }
839 write!(f, ": {}", self.error_kind())
840 }
841}
842
843#[derive(Debug, Clone, Eq, PartialEq)]
845pub struct SourceLocation<'a> {
846 policy_id: &'a PolicyId,
847 source_range: Option<SourceInfo>,
848}
849
850impl<'a> SourceLocation<'a> {
851 pub fn policy_id(&self) -> &'a PolicyId {
853 self.policy_id
854 }
855
856 pub fn range_start(&self) -> Option<usize> {
859 self.source_range.as_ref().map(SourceInfo::range_start)
860 }
861
862 pub fn range_end(&self) -> Option<usize> {
865 self.source_range.as_ref().map(SourceInfo::range_end)
866 }
867}
868
869impl<'a> From<cedar_policy_validator::SourceLocation<'a>> for SourceLocation<'a> {
870 fn from(loc: cedar_policy_validator::SourceLocation<'a>) -> SourceLocation<'a> {
871 let policy_id: &'a PolicyId = PolicyId::ref_cast(loc.policy_id());
872 let source_range = loc.into_source_info();
873 Self {
874 policy_id,
875 source_range,
876 }
877 }
878}
879
880pub fn confusable_string_checker<'a>(
882 templates: impl Iterator<Item = &'a Template>,
883) -> impl Iterator<Item = ValidationWarning<'a>> {
884 cedar_policy_validator::confusable_string_checks(templates.map(|t| &t.ast))
885 .map(std::convert::Into::into)
886}
887
888#[derive(Debug, Error)]
889#[error("Warning on policy {}: {}", .location.policy_id, .kind)]
890pub struct ValidationWarning<'a> {
892 location: SourceLocation<'a>,
893 kind: ValidationWarningKind,
894}
895
896impl<'a> ValidationWarning<'a> {
897 pub fn warning_kind(&self) -> &ValidationWarningKind {
899 &self.kind
900 }
901
902 pub fn location(&self) -> &SourceLocation<'a> {
904 &self.location
905 }
906}
907
908#[doc(hidden)]
909impl<'a> From<cedar_policy_validator::ValidationWarning<'a>> for ValidationWarning<'a> {
910 fn from(w: cedar_policy_validator::ValidationWarning<'a>) -> Self {
911 let (loc, kind) = w.to_kind_and_location();
912 ValidationWarning {
913 location: SourceLocation {
914 policy_id: PolicyId::ref_cast(loc),
915 source_range: None,
916 },
917 kind,
918 }
919 }
920}
921
922#[repr(transparent)]
924#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
925pub struct EntityId(ast::Eid);
926
927impl FromStr for EntityId {
928 type Err = ParseErrors;
929 fn from_str(eid_str: &str) -> Result<Self, Self::Err> {
930 Ok(Self(ast::Eid::new(eid_str)))
931 }
932}
933
934impl AsRef<str> for EntityId {
935 fn as_ref(&self) -> &str {
936 self.0.as_ref()
937 }
938}
939
940impl std::fmt::Display for EntityId {
944 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
945 write!(f, "{}", self.0)
946 }
947}
948
949#[repr(transparent)]
951#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
952pub struct EntityTypeName(ast::Name);
953
954impl FromStr for EntityTypeName {
955 type Err = ParseErrors;
956
957 fn from_str(namespace_type_str: &str) -> Result<Self, Self::Err> {
958 ast::Name::from_normalized_str(namespace_type_str)
959 .map(EntityTypeName)
960 .map_err(ParseErrors)
961 }
962}
963
964impl std::fmt::Display for EntityTypeName {
965 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
966 write!(f, "{}", self.0)
967 }
968}
969
970#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
972pub struct EntityNamespace(ast::Name);
973
974impl FromStr for EntityNamespace {
977 type Err = ParseErrors;
978
979 fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
980 ast::Name::from_normalized_str(namespace_str)
981 .map(EntityNamespace)
982 .map_err(ParseErrors)
983 }
984}
985
986impl std::fmt::Display for EntityNamespace {
987 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
988 write!(f, "{}", self.0)
989 }
990}
991
992#[repr(transparent)]
994#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
995pub struct EntityUid(ast::EntityUID);
996
997impl EntityUid {
998 pub fn type_name(&self) -> &EntityTypeName {
1000 match self.0.entity_type() {
1001 ast::EntityType::Unspecified => panic!("Impossible to have an unspecified entity"),
1002 ast::EntityType::Concrete(name) => EntityTypeName::ref_cast(name),
1003 }
1004 }
1005
1006 pub fn id(&self) -> &EntityId {
1008 EntityId::ref_cast(self.0.eid())
1009 }
1010
1011 pub fn from_type_name_and_id(name: EntityTypeName, id: EntityId) -> Self {
1013 Self(ast::EntityUID::from_components(name.0, id.0))
1014 }
1015
1016 pub fn from_json(json: serde_json::Value) -> Result<Self, impl std::error::Error> {
1023 let parsed: entities::EntityUidJSON = serde_json::from_value(json)?;
1024 Ok::<Self, entities::JsonDeserializationError>(Self(
1025 parsed.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
1026 ))
1027 }
1028
1029 #[cfg(test)]
1031 pub(crate) fn from_strs(typename: &str, id: &str) -> Self {
1032 Self::from_type_name_and_id(
1033 EntityTypeName::from_str(typename).unwrap(),
1034 EntityId::from_str(id).unwrap(),
1035 )
1036 }
1037}
1038
1039impl FromStr for EntityUid {
1042 type Err = ParseErrors;
1043
1044 fn from_str(uid_str: &str) -> Result<Self, Self::Err> {
1051 ast::EntityUID::from_normalized_str(uid_str)
1052 .map(EntityUid)
1053 .map_err(ParseErrors)
1054 }
1055}
1056
1057impl std::fmt::Display for EntityUid {
1058 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1059 write!(f, "{}", self.0)
1060 }
1061}
1062
1063#[derive(Error, Debug)]
1065#[non_exhaustive]
1066pub enum PolicySetError {
1067 #[error("duplicate template or policy id")]
1070 AlreadyDefined,
1071 #[error("unable to link template: {0}")]
1073 LinkingError(#[from] ast::LinkingError),
1074 #[error("expected a static policy, but a template-linked policy was provided")]
1076 ExpectedStatic,
1077 #[error("expected a template, but a static policy was provided")]
1079 ExpectedTemplate,
1080}
1081
1082impl From<ast::PolicySetError> for PolicySetError {
1083 fn from(e: ast::PolicySetError) -> Self {
1084 match e {
1085 ast::PolicySetError::Occupied => Self::AlreadyDefined,
1086 }
1087 }
1088}
1089
1090impl From<ast::UnexpectedSlotError> for PolicySetError {
1091 fn from(_: ast::UnexpectedSlotError) -> Self {
1092 Self::ExpectedStatic
1093 }
1094}
1095
1096#[derive(Debug, Clone, Default)]
1098pub struct PolicySet {
1099 pub(crate) ast: ast::PolicySet,
1102 policies: HashMap<PolicyId, Policy>,
1104 templates: HashMap<PolicyId, Template>,
1106}
1107
1108impl PartialEq for PolicySet {
1109 fn eq(&self, other: &Self) -> bool {
1110 self.ast.eq(&other.ast)
1112 }
1113}
1114impl Eq for PolicySet {}
1115
1116impl FromStr for PolicySet {
1117 type Err = ParseErrors;
1118
1119 fn from_str(policies: &str) -> Result<Self, Self::Err> {
1126 let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
1127 let policies = pset.policies().map(|p|
1128 (
1129 PolicyId(p.id().clone()),
1130 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() }
1131 )
1132 ).collect();
1133 let templates = pset.templates().map(|t|
1134 (
1135 PolicyId(t.id().clone()),
1136 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() }
1137 )
1138 ).collect();
1139 Ok(Self {
1140 ast: pset,
1141 policies,
1142 templates,
1143 })
1144 }
1145}
1146
1147impl PolicySet {
1148 pub fn new() -> Self {
1150 Self {
1151 ast: ast::PolicySet::new(),
1152 policies: HashMap::new(),
1153 templates: HashMap::new(),
1154 }
1155 }
1156
1157 pub fn from_policies(
1159 policies: impl IntoIterator<Item = Policy>,
1160 ) -> Result<Self, PolicySetError> {
1161 let mut set = Self::new();
1162 for policy in policies {
1163 set.add(policy)?;
1164 }
1165 Ok(set)
1166 }
1167
1168 pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
1172 if policy.is_static() {
1173 let id = PolicyId(policy.ast.id().clone());
1174 self.ast.add(policy.ast.clone())?;
1175 self.policies.insert(id, policy);
1176 Ok(())
1177 } else {
1178 Err(PolicySetError::ExpectedStatic)
1179 }
1180 }
1181
1182 pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
1184 let id = PolicyId(template.ast.id().clone());
1185 self.ast.add_template(template.ast.clone())?;
1186 self.templates.insert(id, template);
1187 Ok(())
1188 }
1189
1190 pub fn policies(&self) -> impl Iterator<Item = &Policy> {
1194 self.policies.values()
1195 }
1196
1197 pub fn templates(&self) -> impl Iterator<Item = &Template> {
1199 self.templates.values()
1200 }
1201
1202 pub fn template(&self, id: &PolicyId) -> Option<&Template> {
1204 self.templates.get(id)
1205 }
1206
1207 pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
1209 self.policies.get(id)
1210 }
1211
1212 pub fn annotation<'a>(&'a self, id: &PolicyId, key: impl AsRef<str>) -> Option<&'a str> {
1214 self.ast
1215 .get(&id.0)?
1216 .annotation(&key.as_ref().parse().ok()?)
1217 .map(smol_str::SmolStr::as_str)
1218 }
1219
1220 pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<String> {
1222 self.ast
1223 .get_template(&id.0)?
1224 .annotation(&key.as_ref().parse().ok()?)
1225 .map(smol_str::SmolStr::to_string)
1226 }
1227
1228 pub fn is_empty(&self) -> bool {
1230 debug_assert_eq!(
1231 self.ast.is_empty(),
1232 self.policies.is_empty() && self.templates.is_empty()
1233 );
1234 self.ast.is_empty()
1235 }
1236
1237 #[allow(clippy::needless_pass_by_value)]
1243 pub fn link(
1244 &mut self,
1245 template_id: PolicyId,
1246 new_id: PolicyId,
1247 vals: HashMap<SlotId, EntityUid>,
1248 ) -> Result<(), PolicySetError> {
1249 let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
1250 .into_iter()
1251 .map(|(key, value)| (key.into(), value.0))
1252 .collect();
1253 self.ast
1254 .link(
1255 template_id.0.clone(),
1256 new_id.0.clone(),
1257 unwrapped_vals.clone(),
1258 )
1259 .map_err(PolicySetError::LinkingError)?;
1260 let linked_ast = self
1261 .ast
1262 .get(&new_id.0)
1263 .expect("ast.link() didn't fail above, so this shouldn't fail");
1264 let linked_lossless = self
1265 .templates
1266 .get(&template_id)
1267 .ok_or(PolicySetError::ExpectedTemplate)?
1273 .lossless
1274 .clone()
1275 .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
1276 .expect("ast.link() didn't fail above, so this shouldn't fail");
1281 self.policies.insert(
1282 new_id,
1283 Policy {
1284 ast: linked_ast.clone(),
1285 lossless: linked_lossless,
1286 },
1287 );
1288 Ok(())
1289 }
1290
1291 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1297 fn from_ast(ast: ast::PolicySet) -> Self {
1298 let policies = ast
1299 .policies()
1300 .map(|p| (PolicyId(p.id().clone()), Policy::from_ast(p.clone())))
1301 .collect();
1302 let templates = ast
1303 .templates()
1304 .map(|t| (PolicyId(t.id().clone()), Template::from_ast(t.clone())))
1305 .collect();
1306 Self {
1307 ast,
1308 policies,
1309 templates,
1310 }
1311 }
1312}
1313
1314impl std::fmt::Display for PolicySet {
1315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1316 write!(f, "{}", self.ast)
1317 }
1318}
1319
1320#[derive(Debug, Clone)]
1322pub struct Template {
1323 ast: ast::Template,
1326
1327 lossless: LosslessPolicy,
1338}
1339
1340impl PartialEq for Template {
1341 fn eq(&self, other: &Self) -> bool {
1342 self.ast.eq(&other.ast)
1344 }
1345}
1346impl Eq for Template {}
1347
1348impl Template {
1349 pub fn parse(id: Option<String>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
1354 let ast = parser::parse_policy_template(id, src.as_ref()).map_err(ParseErrors)?;
1355 Ok(Self {
1356 ast,
1357 lossless: LosslessPolicy::policy_or_template_text(src.as_ref()),
1358 })
1359 }
1360
1361 pub fn id(&self) -> &PolicyId {
1363 PolicyId::ref_cast(self.ast.id())
1364 }
1365
1366 #[must_use]
1368 pub fn new_id(&self, id: PolicyId) -> Self {
1369 Self {
1370 ast: self.ast.new_id(id.0),
1371 lossless: self.lossless.clone(), }
1373 }
1374
1375 pub fn effect(&self) -> Effect {
1377 self.ast.effect()
1378 }
1379
1380 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
1382 self.ast
1383 .annotation(&key.as_ref().parse().ok()?)
1384 .map(smol_str::SmolStr::as_str)
1385 }
1386
1387 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
1389 self.ast
1390 .annotations()
1391 .map(|(k, v)| (k.as_ref(), v.as_str()))
1392 }
1393
1394 pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
1396 self.ast.slots().map(SlotId::ref_cast)
1397 }
1398
1399 pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
1401 match self.ast.principal_constraint().as_inner() {
1402 ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
1403 ast::PrincipalOrResourceConstraint::In(eref) => {
1404 TemplatePrincipalConstraint::In(match eref {
1405 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1406 ast::EntityReference::Slot => None,
1407 })
1408 }
1409 ast::PrincipalOrResourceConstraint::Eq(eref) => {
1410 TemplatePrincipalConstraint::Eq(match eref {
1411 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1412 ast::EntityReference::Slot => None,
1413 })
1414 }
1415 }
1416 }
1417
1418 pub fn action_constraint(&self) -> ActionConstraint {
1420 match self.ast.action_constraint() {
1422 ast::ActionConstraint::Any => ActionConstraint::Any,
1423 ast::ActionConstraint::In(ids) => ActionConstraint::In(
1424 ids.iter()
1425 .map(|id| EntityUid(id.as_ref().clone()))
1426 .collect(),
1427 ),
1428 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid(id.as_ref().clone())),
1429 }
1430 }
1431
1432 pub fn resource_constraint(&self) -> TemplateResourceConstraint {
1434 match self.ast.resource_constraint().as_inner() {
1435 ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
1436 ast::PrincipalOrResourceConstraint::In(eref) => {
1437 TemplateResourceConstraint::In(match eref {
1438 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1439 ast::EntityReference::Slot => None,
1440 })
1441 }
1442 ast::PrincipalOrResourceConstraint::Eq(eref) => {
1443 TemplateResourceConstraint::Eq(match eref {
1444 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
1445 ast::EntityReference::Slot => None,
1446 })
1447 }
1448 }
1449 }
1450
1451 #[allow(dead_code)] fn from_json(
1457 id: Option<PolicyId>,
1458 json: serde_json::Value,
1459 ) -> Result<Self, cedar_policy_core::est::EstToAstError> {
1460 let est: est::Policy =
1461 serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
1462 Ok(Self {
1463 ast: est.clone().try_into_ast_template(id.map(|id| id.0))?,
1464 lossless: LosslessPolicy::Est(est),
1465 })
1466 }
1467
1468 #[allow(dead_code)] fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
1471 let est = self.lossless.est()?;
1472 let json = serde_json::to_value(est)?;
1473 Ok::<_, PolicyToJsonError>(json)
1474 }
1475
1476 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1482 fn from_ast(ast: ast::Template) -> Self {
1483 let text = ast.to_string(); Self {
1485 ast,
1486 lossless: LosslessPolicy::policy_or_template_text(text),
1487 }
1488 }
1489}
1490
1491impl FromStr for Template {
1492 type Err = ParseErrors;
1493
1494 fn from_str(src: &str) -> Result<Self, Self::Err> {
1495 Self::parse(None, src)
1496 }
1497}
1498
1499#[derive(Debug, Clone, PartialEq, Eq)]
1501pub enum PrincipalConstraint {
1502 Any,
1504 In(EntityUid),
1506 Eq(EntityUid),
1508}
1509
1510#[derive(Debug, Clone, PartialEq, Eq)]
1512pub enum TemplatePrincipalConstraint {
1513 Any,
1515 In(Option<EntityUid>),
1518 Eq(Option<EntityUid>),
1521}
1522
1523impl TemplatePrincipalConstraint {
1524 pub fn has_slot(&self) -> bool {
1526 match self {
1527 Self::Any => false,
1528 Self::In(o) | Self::Eq(o) => o.is_none(),
1529 }
1530 }
1531}
1532
1533#[derive(Debug, Clone, PartialEq, Eq)]
1535pub enum ActionConstraint {
1536 Any,
1538 In(Vec<EntityUid>),
1540 Eq(EntityUid),
1542}
1543
1544#[derive(Debug, Clone, PartialEq, Eq)]
1546pub enum ResourceConstraint {
1547 Any,
1549 In(EntityUid),
1551 Eq(EntityUid),
1553}
1554
1555#[derive(Debug, Clone, PartialEq, Eq)]
1557pub enum TemplateResourceConstraint {
1558 Any,
1560 In(Option<EntityUid>),
1563 Eq(Option<EntityUid>),
1566}
1567
1568impl TemplateResourceConstraint {
1569 pub fn has_slot(&self) -> bool {
1571 match self {
1572 Self::Any => false,
1573 Self::In(o) | Self::Eq(o) => o.is_none(),
1574 }
1575 }
1576}
1577
1578#[repr(transparent)]
1580#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize, RefCast)]
1581pub struct PolicyId(ast::PolicyID);
1582
1583impl FromStr for PolicyId {
1584 type Err = ParseErrors;
1585
1586 fn from_str(id: &str) -> Result<Self, Self::Err> {
1588 Ok(Self(ast::PolicyID::from_string(id)))
1589 }
1590}
1591
1592impl std::fmt::Display for PolicyId {
1593 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1594 write!(f, "{}", self.0)
1595 }
1596}
1597
1598#[derive(Debug, Clone)]
1600pub struct Policy {
1601 ast: ast::Policy,
1604 lossless: LosslessPolicy,
1612}
1613
1614impl PartialEq for Policy {
1615 fn eq(&self, other: &Self) -> bool {
1616 self.ast.eq(&other.ast)
1618 }
1619}
1620impl Eq for Policy {}
1621
1622impl Policy {
1623 pub fn template_id(&self) -> Option<&PolicyId> {
1626 if self.is_static() {
1627 None
1628 } else {
1629 Some(PolicyId::ref_cast(self.ast.template().id()))
1630 }
1631 }
1632
1633 pub fn effect(&self) -> Effect {
1635 self.ast.effect()
1636 }
1637
1638 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
1640 self.ast
1641 .annotation(&key.as_ref().parse().ok()?)
1642 .map(smol_str::SmolStr::as_str)
1643 }
1644
1645 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
1647 self.ast
1648 .annotations()
1649 .map(|(k, v)| (k.as_ref(), v.as_str()))
1650 }
1651
1652 pub fn id(&self) -> &PolicyId {
1654 PolicyId::ref_cast(self.ast.id())
1655 }
1656
1657 #[must_use]
1659 pub fn new_id(&self, id: PolicyId) -> Self {
1660 Self {
1661 ast: self.ast.new_id(id.0),
1662 lossless: self.lossless.clone(), }
1664 }
1665
1666 pub fn is_static(&self) -> bool {
1668 self.ast.is_static()
1669 }
1670
1671 pub fn principal_constraint(&self) -> PrincipalConstraint {
1673 let slot_id = ast::SlotId::principal();
1674 match self.ast.template().principal_constraint().as_inner() {
1675 ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
1676 ast::PrincipalOrResourceConstraint::In(eref) => {
1677 PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
1678 }
1679 ast::PrincipalOrResourceConstraint::Eq(eref) => {
1680 PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
1681 }
1682 }
1683 }
1684
1685 pub fn action_constraint(&self) -> ActionConstraint {
1687 match self.ast.template().action_constraint() {
1689 ast::ActionConstraint::Any => ActionConstraint::Any,
1690 ast::ActionConstraint::In(ids) => ActionConstraint::In(
1691 ids.iter()
1692 .map(|euid| EntityUid::ref_cast(euid.as_ref()))
1693 .cloned()
1694 .collect(),
1695 ),
1696 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
1697 }
1698 }
1699
1700 pub fn resource_constraint(&self) -> ResourceConstraint {
1702 let slot_id = ast::SlotId::resource();
1703 match self.ast.template().resource_constraint().as_inner() {
1704 ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
1705 ast::PrincipalOrResourceConstraint::In(eref) => {
1706 ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
1707 }
1708 ast::PrincipalOrResourceConstraint::Eq(eref) => {
1709 ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
1710 }
1711 }
1712 }
1713
1714 fn convert_entity_reference<'a>(
1715 &'a self,
1716 r: &'a ast::EntityReference,
1717 slot: ast::SlotId,
1718 ) -> &'a EntityUid {
1719 match r {
1720 ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
1721 ast::EntityReference::Slot => EntityUid::ref_cast(self.ast.env().get(&slot).unwrap()),
1723 }
1724 }
1725
1726 pub fn parse(id: Option<String>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
1731 let inline_ast = parser::parse_policy(id, policy_src.as_ref()).map_err(ParseErrors)?;
1732 let (_, ast) = ast::Template::link_static_policy(inline_ast);
1733 Ok(Self {
1734 ast,
1735 lossless: LosslessPolicy::policy_or_template_text(policy_src.as_ref()),
1736 })
1737 }
1738
1739 pub fn from_json(
1744 id: Option<PolicyId>,
1745 json: serde_json::Value,
1746 ) -> Result<Self, cedar_policy_core::est::EstToAstError> {
1747 let est: est::Policy =
1748 serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
1749 Ok(Self {
1750 ast: est.clone().try_into_ast_policy(id.map(|id| id.0))?,
1751 lossless: LosslessPolicy::Est(est),
1752 })
1753 }
1754
1755 pub fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
1757 let est = self.lossless.est()?;
1758 let json = serde_json::to_value(est)?;
1759 Ok::<_, PolicyToJsonError>(json)
1760 }
1761
1762 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
1768 fn from_ast(ast: ast::Policy) -> Self {
1769 let text = ast.to_string(); Self {
1771 ast,
1772 lossless: LosslessPolicy::policy_or_template_text(text),
1773 }
1774 }
1775}
1776
1777impl std::fmt::Display for Policy {
1778 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1779 self.ast.fmt(f)
1780 }
1781}
1782
1783impl FromStr for Policy {
1784 type Err = ParseErrors;
1785 fn from_str(policy: &str) -> Result<Self, Self::Err> {
1793 Self::parse(None, policy)
1794 }
1795}
1796
1797#[derive(Debug, Clone)]
1801enum LosslessPolicy {
1802 Est(est::Policy),
1804 Text {
1806 text: String,
1808 slots: HashMap<ast::SlotId, ast::EntityUID>,
1812 },
1813}
1814
1815impl LosslessPolicy {
1816 fn policy_or_template_text(text: impl Into<String>) -> Self {
1818 Self::Text {
1819 text: text.into(),
1820 slots: HashMap::new(),
1821 }
1822 }
1823
1824 fn est(&self) -> Result<est::Policy, PolicyToJsonError> {
1826 match self {
1827 Self::Est(est) => Ok(est.clone()),
1828 Self::Text { text, slots } => {
1829 let est = parser::parse_policy_or_template_to_est(text)?;
1830 if slots.is_empty() {
1831 Ok(est)
1832 } else {
1833 let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
1834 Ok(est.link(&unwrapped_vals)?)
1835 }
1836 }
1837 }
1838 }
1839
1840 fn link<'a>(
1841 self,
1842 vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
1843 ) -> Result<Self, est::InstantiationError> {
1844 match self {
1845 Self::Est(est) => {
1846 let unwrapped_est_vals: HashMap<ast::SlotId, entities::EntityUidJSON> =
1847 vals.into_iter().map(|(k, v)| (k, v.into())).collect();
1848 Ok(Self::Est(est.link(&unwrapped_est_vals)?))
1849 }
1850 Self::Text { text, slots } => {
1851 debug_assert!(
1852 slots.is_empty(),
1853 "shouldn't call link() on an already-linked policy"
1854 );
1855 let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
1856 Ok(Self::Text { text, slots })
1857 }
1858 }
1859 }
1860}
1861
1862#[derive(Debug, Error)]
1864pub enum PolicyToJsonError {
1865 #[error(transparent)]
1867 Parse(#[from] ParseErrors),
1868 #[error(transparent)]
1870 Link(#[from] est::InstantiationError),
1871 #[error(transparent)]
1873 Serde(#[from] serde_json::Error),
1874}
1875
1876#[repr(transparent)]
1878#[derive(Debug, Clone, RefCast)]
1879pub struct Expression(ast::Expr);
1880
1881impl Expression {
1882 pub fn new_string(value: String) -> Self {
1884 Self(ast::Expr::val(value))
1885 }
1886
1887 pub fn new_bool(value: bool) -> Self {
1889 Self(ast::Expr::val(value))
1890 }
1891
1892 pub fn new_long(value: i64) -> Self {
1894 Self(ast::Expr::val(value))
1895 }
1896
1897 pub fn new_record(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
1899 Self(ast::Expr::record(
1900 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
1901 ))
1902 }
1903
1904 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
1906 Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
1907 }
1908}
1909
1910impl FromStr for Expression {
1911 type Err = ParseErrors;
1912
1913 fn from_str(expression: &str) -> Result<Self, Self::Err> {
1915 ast::Expr::from_str(expression)
1916 .map_err(ParseErrors)
1917 .map(Expression)
1918 }
1919}
1920
1921#[repr(transparent)]
1937#[derive(Debug, Clone, RefCast)]
1938pub struct RestrictedExpression(ast::RestrictedExpr);
1939
1940impl RestrictedExpression {
1941 pub fn new_string(value: String) -> Self {
1943 Self(ast::RestrictedExpr::val(value))
1944 }
1945
1946 pub fn new_bool(value: bool) -> Self {
1948 Self(ast::RestrictedExpr::val(value))
1949 }
1950
1951 pub fn new_long(value: i64) -> Self {
1953 Self(ast::RestrictedExpr::val(value))
1954 }
1955
1956 pub fn new_record(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
1958 Self(ast::RestrictedExpr::record(
1959 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
1960 ))
1961 }
1962
1963 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
1965 Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
1966 }
1967}
1968
1969impl FromStr for RestrictedExpression {
1970 type Err = ParseErrors;
1971
1972 fn from_str(expression: &str) -> Result<Self, Self::Err> {
1974 ast::RestrictedExpr::from_str(expression)
1975 .map_err(ParseErrors)
1976 .map(RestrictedExpression)
1977 }
1978}
1979
1980#[repr(transparent)]
1982#[derive(Debug, RefCast)]
1983pub struct Request(pub(crate) ast::Request);
1984
1985impl Request {
1986 pub fn new(
1996 principal: Option<EntityUid>,
1997 action: Option<EntityUid>,
1998 resource: Option<EntityUid>,
1999 context: Context,
2000 ) -> Self {
2001 let p = match principal {
2002 Some(p) => p.0,
2003 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
2004 };
2005 let a = match action {
2006 Some(a) => a.0,
2007 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("action")),
2008 };
2009 let r = match resource {
2010 Some(r) => r.0,
2011 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
2012 };
2013 Self(ast::Request::new(p, a, r, context.0))
2014 }
2015
2016 pub fn principal(&self) -> Option<&EntityUid> {
2018 match self.0.principal() {
2019 ast::EntityUIDEntry::Concrete(euid) => Some(EntityUid::ref_cast(euid.as_ref())),
2020 ast::EntityUIDEntry::Unknown => None,
2021 }
2022 }
2023
2024 pub fn action(&self) -> Option<&EntityUid> {
2026 match self.0.action() {
2027 ast::EntityUIDEntry::Concrete(euid) => Some(EntityUid::ref_cast(euid.as_ref())),
2028 ast::EntityUIDEntry::Unknown => None,
2029 }
2030 }
2031
2032 pub fn resource(&self) -> Option<&EntityUid> {
2034 match self.0.resource() {
2035 ast::EntityUIDEntry::Concrete(euid) => Some(EntityUid::ref_cast(euid.as_ref())),
2036 ast::EntityUIDEntry::Unknown => None,
2037 }
2038 }
2039}
2040
2041#[repr(transparent)]
2043#[derive(Debug, Clone, RefCast)]
2044pub struct Context(ast::Context);
2045
2046impl Context {
2047 pub fn empty() -> Self {
2049 Self(ast::Context::empty())
2050 }
2051
2052 pub fn from_pairs(pairs: impl IntoIterator<Item = (String, RestrictedExpression)>) -> Self {
2056 Self(ast::Context::from_pairs(
2057 pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2058 ))
2059 }
2060
2061 pub fn from_json_str(
2072 json: &str,
2073 schema: Option<(&Schema, &EntityUid)>,
2074 ) -> Result<Self, ContextJsonError> {
2075 let schema = schema
2076 .map(|(s, uid)| Self::get_context_schema(s, uid))
2077 .transpose()?;
2078 let context =
2079 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2080 .from_json_str(json)?;
2081 Ok(Self(context))
2082 }
2083
2084 pub fn from_json_value(
2095 json: serde_json::Value,
2096 schema: Option<(&Schema, &EntityUid)>,
2097 ) -> Result<Self, ContextJsonError> {
2098 let schema = schema
2099 .map(|(s, uid)| Self::get_context_schema(s, uid))
2100 .transpose()?;
2101 let context =
2102 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2103 .from_json_value(json)?;
2104 Ok(Self(context))
2105 }
2106
2107 pub fn from_json_file(
2118 json: impl std::io::Read,
2119 schema: Option<(&Schema, &EntityUid)>,
2120 ) -> Result<Self, ContextJsonError> {
2121 let schema = schema
2122 .map(|(s, uid)| Self::get_context_schema(s, uid))
2123 .transpose()?;
2124 let context =
2125 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
2126 .from_json_file(json)?;
2127 Ok(Self(context))
2128 }
2129
2130 fn get_context_schema(
2132 schema: &Schema,
2133 action: &EntityUid,
2134 ) -> Result<impl ContextSchema, ContextJsonError> {
2135 schema
2136 .0
2137 .get_context_schema(&action.0)
2138 .ok_or_else(|| ContextJsonError::ActionDoesNotExist {
2139 action: action.clone(),
2140 })
2141 }
2142}
2143
2144#[derive(Debug, Error)]
2146pub enum ContextJsonError {
2147 #[error(transparent)]
2149 JsonDeserializationError(#[from] JsonDeserializationError),
2150 #[error("Action {action} doesn't exist in the supplied schema")]
2152 ActionDoesNotExist {
2153 action: EntityUid,
2155 },
2156}
2157
2158impl std::fmt::Display for Request {
2159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2160 write!(f, "{}", self.0)
2161 }
2162}
2163
2164#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
2166pub enum EvalResult {
2167 Bool(bool),
2169 Long(i64),
2171 String(String),
2173 EntityUid(EntityUid),
2175 Set(Set),
2177 Record(Record),
2179 ExtensionValue(String),
2181 }
2183
2184#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
2186pub struct Set(BTreeSet<EvalResult>);
2187
2188impl Set {
2189 pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
2191 self.0.iter()
2192 }
2193
2194 pub fn contains(&self, elem: &EvalResult) -> bool {
2196 self.0.contains(elem)
2197 }
2198
2199 pub fn len(&self) -> usize {
2201 self.0.len()
2202 }
2203
2204 pub fn is_empty(&self) -> bool {
2206 self.0.is_empty()
2207 }
2208}
2209
2210#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
2212pub struct Record(BTreeMap<String, EvalResult>);
2213
2214impl Record {
2215 pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
2217 self.0.iter()
2218 }
2219
2220 pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
2222 self.0.contains_key(key.as_ref())
2223 }
2224
2225 pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
2227 self.0.get(key.as_ref())
2228 }
2229
2230 pub fn len(&self) -> usize {
2232 self.0.len()
2233 }
2234
2235 pub fn is_empty(&self) -> bool {
2237 self.0.is_empty()
2238 }
2239}
2240
2241#[doc(hidden)]
2242impl From<ast::Value> for EvalResult {
2243 fn from(v: ast::Value) -> Self {
2244 match v {
2245 ast::Value::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
2246 ast::Value::Lit(ast::Literal::Long(i)) => Self::Long(i),
2247 ast::Value::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
2248 ast::Value::Lit(ast::Literal::EntityUID(e)) => {
2249 Self::EntityUid(EntityUid(ast::EntityUID::clone(&e)))
2250 }
2251 ast::Value::Set(s) => Self::Set(Set(s
2252 .authoritative
2253 .iter()
2254 .map(|v| v.clone().into())
2255 .collect())),
2256 ast::Value::Record(r) => Self::Record(Record(
2257 r.iter()
2258 .map(|(k, v)| (k.to_string(), v.clone().into()))
2259 .collect(),
2260 )),
2261 ast::Value::ExtensionValue(v) => Self::ExtensionValue(v.to_string()),
2262 }
2263 }
2264}
2265impl std::fmt::Display for EvalResult {
2266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2267 match self {
2268 Self::Bool(b) => write!(f, "{b}"),
2269 Self::Long(l) => write!(f, "{l}"),
2270 Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
2271 Self::EntityUid(uid) => write!(f, "{uid}"),
2272 Self::Set(s) => {
2273 write!(f, "[")?;
2274 for (i, ev) in s.iter().enumerate() {
2275 write!(f, "{ev}")?;
2276 if (i + 1) < s.len() {
2277 write!(f, ", ")?;
2278 }
2279 }
2280 write!(f, "]")?;
2281 Ok(())
2282 }
2283 Self::Record(r) => {
2284 write!(f, "{{")?;
2285 for (i, (k, v)) in r.iter().enumerate() {
2286 write!(f, "\"{}\": {v}", k.escape_debug())?;
2287 if (i + 1) < r.len() {
2288 write!(f, ", ")?;
2289 }
2290 }
2291 write!(f, "}}")?;
2292 Ok(())
2293 }
2294 Self::ExtensionValue(s) => write!(f, "{s}"),
2295 }
2296 }
2297}
2298
2299pub fn eval_expression(
2303 request: &Request,
2304 entities: &Entities,
2305 expr: &Expression,
2306) -> Result<EvalResult, EvaluationError> {
2307 let all_ext = Extensions::all_available();
2308 let eval = Evaluator::new(&request.0, &entities.0, &all_ext)
2309 .map_err(|e| EvaluationError::StringMessage(e.to_string()))?;
2310 Ok(EvalResult::from(
2311 eval.interpret(&expr.0, &ast::SlotEnv::new())
2313 .map_err(|e| EvaluationError::StringMessage(e.to_string()))?,
2314 ))
2315}
2316
2317#[cfg(test)]
2318#[cfg(feature = "partial-eval")]
2319mod partial_eval_test {
2320 use std::collections::HashSet;
2321
2322 use crate::{PolicyId, PolicySet, ResidualResponse};
2323
2324 #[test]
2325 fn test_pe_response_constructor() {
2326 let p: PolicySet = "permit(principal, action, resource);".parse().unwrap();
2327 let reason: HashSet<PolicyId> = std::iter::once("id1".parse().unwrap()).collect();
2328 let errors: HashSet<String> = std::iter::once("error".to_string()).collect();
2329 let a = ResidualResponse::new(p.clone(), reason.clone(), errors.clone());
2330 assert_eq!(a.diagnostics().errors, errors);
2331 assert_eq!(a.diagnostics().reason, reason);
2332 assert_eq!(a.residuals(), &p);
2333 }
2334}
2335
2336#[cfg(test)]
2337mod entity_uid_tests {
2338 use super::*;
2339
2340 #[test]
2342 fn entity_uid_from_parts() {
2343 let entity_id = EntityId::from_str("bobby").expect("failed at constructing EntityId");
2344 let entity_type_name = EntityTypeName::from_str("Chess::Master")
2345 .expect("failed at constructing EntityTypeName");
2346 let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
2347 assert_eq!(euid.id().as_ref(), "bobby");
2348 assert_eq!(euid.type_name().to_string(), "Chess::Master");
2349 }
2350
2351 #[test]
2353 fn entity_uid_with_escape() {
2354 let entity_id = EntityId::from_str(r#"bobby\'s sister:\nVeronica"#)
2356 .expect("failed at constructing EntityId");
2357 let entity_type_name = EntityTypeName::from_str("Hockey::Master")
2358 .expect("failed at constructing EntityTypeName");
2359 let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
2360 assert_eq!(euid.id().as_ref(), r#"bobby\'s sister:\nVeronica"#);
2363 assert_eq!(euid.type_name().to_string(), "Hockey::Master");
2364 }
2365
2366 #[test]
2368 fn entity_uid_with_backslashes() {
2369 let entity_id =
2371 EntityId::from_str(r#"\ \a \b \' \" \\"#).expect("failed at constructing EntityId");
2372 let entity_type_name =
2373 EntityTypeName::from_str("Test::User").expect("failed at constructing EntityTypeName");
2374 let euid = EntityUid::from_type_name_and_id(entity_type_name, entity_id);
2375 assert_eq!(euid.id().as_ref(), r#"\ \a \b \' \" \\"#);
2377 assert_eq!(euid.type_name().to_string(), "Test::User");
2378 }
2379
2380 #[test]
2382 fn entity_uid_with_quotes() {
2383 let euid: EntityUid = EntityUid::from_type_name_and_id(
2384 EntityTypeName::from_str("Test::User").unwrap(),
2385 EntityId::from_str(r#"b'ob"by\'s sis\"ter"#).unwrap(),
2386 );
2387 assert_eq!(euid.id().as_ref(), r#"b'ob"by\'s sis\"ter"#);
2390 assert_eq!(euid.type_name().to_string(), r#"Test::User"#);
2391 }
2392
2393 #[test]
2395 fn entity_uid_with_whitespace() {
2396 EntityTypeName::from_str("A :: B::C").expect_err("should fail due to RFC 9");
2397 EntityTypeName::from_str(" A :: B\n::C \n ::D\n").expect_err("should fail due to RFC 9");
2398
2399 let policy = Policy::from_str(
2401 r#"permit(principal == A :: B::C :: " hi there are spaces ", action, resource);"#,
2402 )
2403 .expect("should succeed, see RFC 9");
2404 let euid = match policy.principal_constraint() {
2405 PrincipalConstraint::Eq(euid) => euid,
2406 _ => panic!("expected Eq constraint"),
2407 };
2408 assert_eq!(euid.id().as_ref(), " hi there are spaces ");
2409 assert_eq!(euid.type_name().to_string(), "A::B::C"); let policy = Policy::from_str(
2412 r#"
2413permit(principal == A :: B
2414 ::C
2415 :: D
2416 :: " hi there are
2417 spaces and
2418 newlines ", action, resource);"#,
2419 )
2420 .expect("should succeed, see RFC 9");
2421 let euid = match policy.principal_constraint() {
2422 PrincipalConstraint::Eq(euid) => euid,
2423 _ => panic!("expected Eq constraint"),
2424 };
2425 assert_eq!(
2426 euid.id().as_ref(),
2427 " hi there are\n spaces and\n newlines "
2428 );
2429 assert_eq!(euid.type_name().to_string(), "A::B::C::D"); }
2431
2432 #[test]
2433 fn malformed_entity_type_name_should_fail() {
2434 let result = EntityTypeName::from_str("I'm an invalid name");
2435
2436 assert!(matches!(result, Err(ParseErrors(_))));
2437 let error = result.err().unwrap();
2438 assert!(error.to_string().contains("Unrecognized token `'`"));
2439 }
2440
2441 #[test]
2443 fn parse_euid() {
2444 let parsed_eid: EntityUid = r#"Test::User::"bobby""#.parse().expect("Failed to parse");
2445 assert_eq!(parsed_eid.id().as_ref(), r#"bobby"#);
2446 assert_eq!(parsed_eid.type_name().to_string(), r#"Test::User"#);
2447 }
2448
2449 #[test]
2451 fn parse_euid_with_escape() {
2452 let parsed_eid: EntityUid = r#"Test::User::"b\'ob\"by""#.parse().expect("Failed to parse");
2454 assert_eq!(parsed_eid.id().as_ref(), r#"b'ob"by"#);
2457 assert_eq!(parsed_eid.type_name().to_string(), r#"Test::User"#);
2458 }
2459
2460 #[test]
2462 fn parse_euid_single_quotes() {
2463 let euid_str = r#"Test::User::"b'obby\'s sister""#;
2465 EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
2466 let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
2468 let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
2469 let parsed_euid = match policy.principal_constraint() {
2470 PrincipalConstraint::Eq(euid) => euid,
2471 _ => panic!("Expected an Eq constraint"),
2472 };
2473 assert_eq!(parsed_euid.id().as_ref(), r#"b'obby's sister"#);
2476 assert_eq!(parsed_euid.type_name().to_string(), r#"Test::User"#);
2477 }
2478
2479 #[test]
2481 fn parse_euid_whitespace() {
2482 let euid_str = " A ::B :: C:: D \n :: \n E\n :: \"hi\"";
2483 EntityUid::from_str(euid_str).expect_err("Should fail, not normalized -- see RFC 9");
2484 let policy_str = "permit(principal == ".to_string() + euid_str + ", action, resource);";
2486 let policy = Policy::from_str(&policy_str).expect("Should parse; see RFC 9");
2487 let parsed_euid = match policy.principal_constraint() {
2488 PrincipalConstraint::Eq(euid) => euid,
2489 _ => panic!("Expected an Eq constraint"),
2490 };
2491 assert_eq!(parsed_euid.id().as_ref(), "hi");
2492 assert_eq!(parsed_euid.type_name().to_string(), "A::B::C::D::E"); }
2494
2495 #[test]
2497 fn euid_roundtrip() {
2498 let parsed_euid: EntityUid = r#"Test::User::"b\'ob""#.parse().expect("Failed to parse");
2499 assert_eq!(parsed_euid.id().as_ref(), r#"b'ob"#);
2500 let reparsed: EntityUid = format!("{parsed_euid}")
2501 .parse()
2502 .expect("failed to roundtrip");
2503 assert_eq!(reparsed.id().as_ref(), r#"b'ob"#);
2504 }
2505}
2506
2507#[cfg(test)]
2508mod head_constraints_tests {
2509 use super::*;
2510
2511 #[test]
2512 fn principal_constraint_inline() {
2513 let p = Policy::from_str("permit(principal,action,resource);").unwrap();
2514 assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
2515 let euid = EntityUid::from_strs("T", "a");
2516 assert_eq!(euid.id().as_ref(), "a");
2517 assert_eq!(
2518 euid.type_name(),
2519 &EntityTypeName::from_str("T").expect("Failed to parse EntityTypeName")
2520 );
2521 let p =
2522 Policy::from_str("permit(principal == T::\"a\",action,resource == T::\"b\");").unwrap();
2523 assert_eq!(
2524 p.principal_constraint(),
2525 PrincipalConstraint::Eq(euid.clone())
2526 );
2527 let p = Policy::from_str("permit(principal in T::\"a\",action,resource);").unwrap();
2528 assert_eq!(p.principal_constraint(), PrincipalConstraint::In(euid));
2529 }
2530
2531 #[test]
2532 fn action_constraint_inline() {
2533 let p = Policy::from_str("permit(principal,action,resource);").unwrap();
2534 assert_eq!(p.action_constraint(), ActionConstraint::Any);
2535 let euid = EntityUid::from_strs("NN::N::Action", "a");
2536 assert_eq!(
2537 euid.type_name(),
2538 &EntityTypeName::from_str("NN::N::Action").expect("Failed to parse EntityTypeName")
2539 );
2540 let p = Policy::from_str(
2541 "permit(principal == T::\"b\",action == NN::N::Action::\"a\",resource == T::\"c\");",
2542 )
2543 .unwrap();
2544 assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
2545 let p = Policy::from_str("permit(principal,action in [NN::N::Action::\"a\"],resource);")
2546 .unwrap();
2547 assert_eq!(p.action_constraint(), ActionConstraint::In(vec![euid]));
2548 }
2549
2550 #[test]
2551 fn resource_constraint_inline() {
2552 let p = Policy::from_str("permit(principal,action,resource);").unwrap();
2553 assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
2554 let euid = EntityUid::from_strs("NN::N::T", "a");
2555 assert_eq!(
2556 euid.type_name(),
2557 &EntityTypeName::from_str("NN::N::T").expect("Failed to parse EntityTypeName")
2558 );
2559 let p =
2560 Policy::from_str("permit(principal == T::\"b\",action,resource == NN::N::T::\"a\");")
2561 .unwrap();
2562 assert_eq!(
2563 p.resource_constraint(),
2564 ResourceConstraint::Eq(euid.clone())
2565 );
2566 let p = Policy::from_str("permit(principal,action,resource in NN::N::T::\"a\");").unwrap();
2567 assert_eq!(p.resource_constraint(), ResourceConstraint::In(euid));
2568 }
2569
2570 #[test]
2571 fn principal_constraint_link() {
2572 let p = link("permit(principal,action,resource);", HashMap::new());
2573 assert_eq!(p.principal_constraint(), PrincipalConstraint::Any);
2574 let euid = EntityUid::from_strs("T", "a");
2575 let p = link(
2576 "permit(principal == T::\"a\",action,resource);",
2577 HashMap::new(),
2578 );
2579 assert_eq!(
2580 p.principal_constraint(),
2581 PrincipalConstraint::Eq(euid.clone())
2582 );
2583 let p = link(
2584 "permit(principal in T::\"a\",action,resource);",
2585 HashMap::new(),
2586 );
2587 assert_eq!(
2588 p.principal_constraint(),
2589 PrincipalConstraint::In(euid.clone())
2590 );
2591 let map: HashMap<SlotId, EntityUid> =
2592 std::iter::once((SlotId::principal(), euid.clone())).collect();
2593 let p = link(
2594 "permit(principal in ?principal,action,resource);",
2595 map.clone(),
2596 );
2597 assert_eq!(
2598 p.principal_constraint(),
2599 PrincipalConstraint::In(euid.clone())
2600 );
2601 let p = link("permit(principal == ?principal,action,resource);", map);
2602 assert_eq!(p.principal_constraint(), PrincipalConstraint::Eq(euid));
2603 }
2604
2605 #[test]
2606 fn action_constraint_link() {
2607 let p = link("permit(principal,action,resource);", HashMap::new());
2608 assert_eq!(p.action_constraint(), ActionConstraint::Any);
2609 let euid = EntityUid::from_strs("Action", "a");
2610 let p = link(
2611 "permit(principal,action == Action::\"a\",resource);",
2612 HashMap::new(),
2613 );
2614 assert_eq!(p.action_constraint(), ActionConstraint::Eq(euid.clone()));
2615 let p = link(
2616 "permit(principal,action in [Action::\"a\",Action::\"b\"],resource);",
2617 HashMap::new(),
2618 );
2619 assert_eq!(
2620 p.action_constraint(),
2621 ActionConstraint::In(vec![euid, EntityUid::from_strs("Action", "b"),])
2622 );
2623 }
2624
2625 #[test]
2626 fn resource_constraint_link() {
2627 let p = link("permit(principal,action,resource);", HashMap::new());
2628 assert_eq!(p.resource_constraint(), ResourceConstraint::Any);
2629 let euid = EntityUid::from_strs("T", "a");
2630 let p = link(
2631 "permit(principal,action,resource == T::\"a\");",
2632 HashMap::new(),
2633 );
2634 assert_eq!(
2635 p.resource_constraint(),
2636 ResourceConstraint::Eq(euid.clone())
2637 );
2638 let p = link(
2639 "permit(principal,action,resource in T::\"a\");",
2640 HashMap::new(),
2641 );
2642 assert_eq!(
2643 p.resource_constraint(),
2644 ResourceConstraint::In(euid.clone())
2645 );
2646 let map: HashMap<SlotId, EntityUid> =
2647 std::iter::once((SlotId::resource(), euid.clone())).collect();
2648 let p = link(
2649 "permit(principal,action,resource in ?resource);",
2650 map.clone(),
2651 );
2652 assert_eq!(
2653 p.resource_constraint(),
2654 ResourceConstraint::In(euid.clone())
2655 );
2656 let p = link("permit(principal,action,resource == ?resource);", map);
2657 assert_eq!(p.resource_constraint(), ResourceConstraint::Eq(euid));
2658 }
2659
2660 fn link(src: &str, values: HashMap<SlotId, EntityUid>) -> Policy {
2661 let mut pset = PolicySet::new();
2662 let template = Template::parse(Some("Id".to_string()), src).unwrap();
2663
2664 pset.add_template(template).unwrap();
2665
2666 let link_id = PolicyId::from_str("link").unwrap();
2667 pset.link(PolicyId::from_str("Id").unwrap(), link_id.clone(), values)
2668 .unwrap();
2669 pset.policy(&link_id).unwrap().clone()
2670 }
2671}
2672
2673#[cfg(test)]
2675mod policy_set_tests {
2676 use super::*;
2677 use ast::LinkingError;
2678 use cool_asserts::assert_matches;
2679
2680 #[test]
2681 fn link_conflicts() {
2682 let mut pset = PolicySet::new();
2683 let p1 = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
2684 .expect("Failed to parse");
2685 pset.add(p1).expect("Failed to add");
2686 let template = Template::parse(
2687 Some("t".into()),
2688 "permit(principal == ?principal, action, resource);",
2689 )
2690 .expect("Failed to parse");
2691 pset.add_template(template).expect("Add failed");
2692
2693 let env: HashMap<SlotId, EntityUid> =
2694 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect();
2695
2696 let r = pset.link(
2697 PolicyId::from_str("t").unwrap(),
2698 PolicyId::from_str("id").unwrap(),
2699 env,
2700 );
2701
2702 match r {
2703 Ok(_) => panic!("Should have failed due to conflict"),
2704 Err(PolicySetError::LinkingError(LinkingError::PolicyIdConflict)) => (),
2705 Err(e) => panic!("Incorrect error: {e}"),
2706 };
2707 }
2708
2709 #[test]
2710 fn policyset_add() {
2711 let mut pset = PolicySet::new();
2712 let static_policy = Policy::parse(Some("id".into()), "permit(principal,action,resource);")
2713 .expect("Failed to parse");
2714 pset.add(static_policy).expect("Failed to add");
2715
2716 let template = Template::parse(
2717 Some("t".into()),
2718 "permit(principal == ?principal, action, resource);",
2719 )
2720 .expect("Failed to parse");
2721 pset.add_template(template).expect("Failed to add");
2722
2723 let env1: HashMap<SlotId, EntityUid> =
2724 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test1"))).collect();
2725 pset.link(
2726 PolicyId::from_str("t").unwrap(),
2727 PolicyId::from_str("link").unwrap(),
2728 env1,
2729 )
2730 .expect("Failed to link");
2731
2732 let env2: HashMap<SlotId, EntityUid> =
2733 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test2"))).collect();
2734
2735 let err = pset
2736 .link(
2737 PolicyId::from_str("t").unwrap(),
2738 PolicyId::from_str("link").unwrap(),
2739 env2.clone(),
2740 )
2741 .expect_err("Should have failed due to conflict with existing link id");
2742 match err {
2743 PolicySetError::LinkingError(_) => (),
2744 e => panic!("Wrong error: {e}"),
2745 }
2746
2747 pset.link(
2748 PolicyId::from_str("t").unwrap(),
2749 PolicyId::from_str("link2").unwrap(),
2750 env2,
2751 )
2752 .expect("Failed to link");
2753
2754 let template2 = Template::parse(
2755 Some("t".into()),
2756 "forbid(principal, action, resource == ?resource);",
2757 )
2758 .expect("Failed to parse");
2759 pset.add_template(template2)
2760 .expect_err("should have failed due to conflict on template id");
2761 let template2 = Template::parse(
2762 Some("t2".into()),
2763 "forbid(principal, action, resource == ?resource);",
2764 )
2765 .expect("Failed to parse");
2766 pset.add_template(template2)
2767 .expect("Failed to add template");
2768 let env3: HashMap<SlotId, EntityUid> =
2769 std::iter::once((SlotId::resource(), EntityUid::from_strs("Test", "test3"))).collect();
2770
2771 pset.link(
2772 PolicyId::from_str("t").unwrap(),
2773 PolicyId::from_str("unique3").unwrap(),
2774 env3.clone(),
2775 )
2776 .expect_err("should have failed due to conflict on template id");
2777
2778 pset.link(
2779 PolicyId::from_str("t2").unwrap(),
2780 PolicyId::from_str("unique3").unwrap(),
2781 env3,
2782 )
2783 .expect("should succeed with unique ids");
2784 }
2785
2786 #[test]
2787 fn pset_requests() {
2788 let template = Template::parse(
2789 Some("template".into()),
2790 "permit(principal == ?principal, action, resource);",
2791 )
2792 .expect("Template Parse Failure");
2793 let static_policy = Policy::parse(
2794 Some("static".into()),
2795 "permit(principal, action, resource);",
2796 )
2797 .expect("Static parse failure");
2798 let mut pset = PolicySet::new();
2799 pset.add_template(template).unwrap();
2800 pset.add(static_policy).unwrap();
2801 pset.link(
2802 PolicyId::from_str("template").unwrap(),
2803 PolicyId::from_str("linked").unwrap(),
2804 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
2805 )
2806 .expect("Link failure");
2807
2808 assert_eq!(pset.templates().count(), 1);
2809 assert_eq!(pset.policies().count(), 2);
2810 assert_eq!(pset.policies().filter(|p| p.is_static()).count(), 1);
2811
2812 assert_eq!(
2813 pset.template(&"template".parse().unwrap())
2814 .expect("lookup failed")
2815 .id(),
2816 &"template".parse().unwrap()
2817 );
2818 assert_eq!(
2819 pset.policy(&"static".parse().unwrap())
2820 .expect("lookup failed")
2821 .id(),
2822 &"static".parse().unwrap()
2823 );
2824 assert_eq!(
2825 pset.policy(&"linked".parse().unwrap())
2826 .expect("lookup failed")
2827 .id(),
2828 &"linked".parse().unwrap()
2829 );
2830 }
2831
2832 #[test]
2833 fn link_static_policy() {
2834 let static_policy = Policy::parse(
2837 Some("static".into()),
2838 "permit(principal, action, resource);",
2839 )
2840 .expect("Static parse failure");
2841 let mut pset = PolicySet::new();
2842 pset.add(static_policy).unwrap();
2843
2844 let result = pset.link(
2845 PolicyId::from_str("static").unwrap(),
2846 PolicyId::from_str("linked").unwrap(),
2847 HashMap::new(),
2848 );
2849 assert_matches!(result, Err(PolicySetError::ExpectedTemplate));
2850 }
2851
2852 #[test]
2853 fn link_linked_policy() {
2854 let template = Template::parse(
2855 Some("template".into()),
2856 "permit(principal == ?principal, action, resource);",
2857 )
2858 .expect("Template Parse Failure");
2859 let mut pset = PolicySet::new();
2860 pset.add_template(template).unwrap();
2861
2862 pset.link(
2863 PolicyId::from_str("template").unwrap(),
2864 PolicyId::from_str("linked").unwrap(),
2865 std::iter::once((SlotId::principal(), EntityUid::from_strs("Test", "test"))).collect(),
2866 )
2867 .unwrap();
2868
2869 let result = pset.link(
2870 PolicyId::from_str("linked").unwrap(),
2871 PolicyId::from_str("linked2").unwrap(),
2872 HashMap::new(),
2873 );
2874 assert_matches!(
2875 result,
2876 Err(PolicySetError::LinkingError(LinkingError::NoSuchTemplate(
2877 _
2878 )))
2879 );
2880 }
2881}
2882
2883#[cfg(test)]
2884mod schema_tests {
2885 use super::*;
2886 use cool_asserts::assert_matches;
2887 use serde_json::json;
2888
2889 #[test]
2891 fn valid_schema() {
2892 Schema::from_json_value(json!(
2893 { "": {
2894 "entityTypes": {
2895 "Photo": {
2896 "memberOfTypes": [ "Album" ],
2897 "shape": {
2898 "type": "Record",
2899 "attributes": {
2900 "foo": {
2901 "type": "Boolean",
2902 "required": false
2903 }
2904 }
2905 }
2906 },
2907 "Album": {
2908 "memberOfTypes": [ ],
2909 "shape": {
2910 "type": "Record",
2911 "attributes": {
2912 "foo": {
2913 "type": "Boolean",
2914 "required": false
2915 }
2916 }
2917 }
2918 }
2919 },
2920 "actions": {
2921 "view": {
2922 "appliesTo": {
2923 "principalTypes": ["Photo", "Album"],
2924 "resourceTypes": ["Photo"]
2925 }
2926 }
2927 }
2928 }}))
2929 .expect("schema should be valid");
2930 }
2931
2932 #[test]
2934 fn invalid_schema() {
2935 assert_matches!(
2936 Schema::from_json_value(json!(
2937 r#""{"": {
2940 "entityTypes": {
2941 "Photo": {
2942 "memberOfTypes": [ "Album" ],
2943 "shape": {
2944 "type": "Record",
2945 "attributes": {
2946 "foo": {
2947 "type": "Boolean",
2948 "required": false
2949 }
2950 }
2951 }
2952 },
2953 "Album": {
2954 "memberOfTypes": [ ],
2955 "shape": {
2956 "type": "Record",
2957 "attributes": {
2958 "foo": {
2959 "type": "Boolean",
2960 "required": false
2961 }
2962 }
2963 }
2964 },
2965 "Photo": {
2966 "memberOfTypes": [ "Album" ],
2967 "shape": {
2968 "type": "Record",
2969 "attributes": {
2970 "foo": {
2971 "type": "Boolean",
2972 "required": false
2973 }
2974 }
2975 }
2976 }
2977 },
2978 "actions": {
2979 "view": {
2980 "appliesTo": {
2981 "principalTypes": ["Photo", "Album"],
2982 "resourceTypes": ["Photo"]
2983 }
2984 }
2985 }
2986 }}"#
2987 )),
2988 Err(SchemaError::ParseJson(_))
2989 );
2990 }
2991}
2992
2993#[cfg(test)]
2994mod ancestors_tests {
2995 use super::*;
2996
2997 #[test]
2998 fn test_ancestors() {
2999 let a_euid: EntityUid = EntityUid::from_strs("test", "A");
3000 let b_euid: EntityUid = EntityUid::from_strs("test", "b");
3001 let c_euid: EntityUid = EntityUid::from_strs("test", "C");
3002 let a = Entity::new(a_euid.clone(), HashMap::new(), HashSet::new());
3003 let b = Entity::new(
3004 b_euid.clone(),
3005 HashMap::new(),
3006 std::iter::once(a_euid.clone()).collect(),
3007 );
3008 let c = Entity::new(
3009 c_euid.clone(),
3010 HashMap::new(),
3011 std::iter::once(b_euid.clone()).collect(),
3012 );
3013 let es = Entities::from_entities([a, b, c]).unwrap();
3014 let ans = es.ancestors(&c_euid).unwrap().collect::<HashSet<_>>();
3015 assert_eq!(ans.len(), 2);
3016 assert!(ans.contains(&b_euid));
3017 assert!(ans.contains(&a_euid));
3018 }
3019}
3020
3021#[cfg(test)]
3026mod schema_based_parsing_tests {
3027 use std::assert_eq;
3028
3029 use super::*;
3030 use cool_asserts::assert_matches;
3031 use serde_json::json;
3032
3033 #[test]
3035 #[allow(clippy::too_many_lines)]
3036 #[allow(clippy::cognitive_complexity)]
3037 fn attr_types() {
3038 let schema = Schema::from_json_value(json!(
3039 {"": {
3040 "entityTypes": {
3041 "Employee": {
3042 "memberOfTypes": [],
3043 "shape": {
3044 "type": "Record",
3045 "attributes": {
3046 "isFullTime": { "type": "Boolean" },
3047 "numDirectReports": { "type": "Long" },
3048 "department": { "type": "String" },
3049 "manager": { "type": "Entity", "name": "Employee" },
3050 "hr_contacts": { "type": "Set", "element": {
3051 "type": "Entity", "name": "HR" } },
3052 "json_blob": { "type": "Record", "attributes": {
3053 "inner1": { "type": "Boolean" },
3054 "inner2": { "type": "String" },
3055 "inner3": { "type": "Record", "attributes": {
3056 "innerinner": { "type": "Entity", "name": "Employee" }
3057 }}
3058 }},
3059 "home_ip": { "type": "Extension", "name": "ipaddr" },
3060 "work_ip": { "type": "Extension", "name": "ipaddr" },
3061 "trust_score": { "type": "Extension", "name": "decimal" },
3062 "tricky": { "type": "Record", "attributes": {
3063 "type": { "type": "String" },
3064 "id": { "type": "String" }
3065 }}
3066 }
3067 }
3068 },
3069 "HR": {
3070 "memberOfTypes": []
3071 }
3072 },
3073 "actions": {
3074 "view": { }
3075 }
3076 }}
3077 ))
3078 .expect("should be a valid schema");
3079
3080 let entitiesjson = json!(
3081 [
3082 {
3083 "uid": { "type": "Employee", "id": "12UA45" },
3084 "attrs": {
3085 "isFullTime": true,
3086 "numDirectReports": 3,
3087 "department": "Sales",
3088 "manager": { "type": "Employee", "id": "34FB87" },
3089 "hr_contacts": [
3090 { "type": "HR", "id": "aaaaa" },
3091 { "type": "HR", "id": "bbbbb" }
3092 ],
3093 "json_blob": {
3094 "inner1": false,
3095 "inner2": "-*/",
3096 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3097 },
3098 "home_ip": "222.222.222.101",
3099 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3100 "trust_score": "5.7",
3101 "tricky": { "type": "Employee", "id": "34FB87" }
3102 },
3103 "parents": []
3104 }
3105 ]
3106 );
3107 let parsed = Entities::from_json_value(entitiesjson.clone(), None)
3111 .expect("Should parse without error");
3112 assert_eq!(parsed.iter().count(), 1);
3113 let parsed = parsed
3114 .get(&EntityUid::from_strs("Employee", "12UA45"))
3115 .expect("that should be the employee id");
3116 assert_eq!(
3117 parsed.attr("home_ip"),
3118 Some(Ok(EvalResult::String("222.222.222.101".into())))
3119 );
3120 assert_eq!(
3121 parsed.attr("trust_score"),
3122 Some(Ok(EvalResult::String("5.7".into())))
3123 );
3124 assert!(matches!(
3125 parsed.attr("manager"),
3126 Some(Ok(EvalResult::Record(_)))
3127 ));
3128 assert!(matches!(
3129 parsed.attr("work_ip"),
3130 Some(Ok(EvalResult::Record(_)))
3131 ));
3132 {
3133 let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
3134 panic!("expected hr_contacts attr to exist and be a Set")
3135 };
3136 let contact = set.iter().next().expect("should be at least one contact");
3137 assert!(matches!(contact, EvalResult::Record(_)));
3138 };
3139 {
3140 let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
3141 panic!("expected json_blob attr to exist and be a Record")
3142 };
3143 let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
3144 let EvalResult::Record(rec) = inner3 else {
3145 panic!("expected inner3 to be a Record")
3146 };
3147 let innerinner = rec
3148 .get("innerinner")
3149 .expect("expected innerinner attr to exist");
3150 assert!(matches!(innerinner, EvalResult::Record(_)));
3151 };
3152 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3154 .expect("Should parse without error");
3155 assert_eq!(parsed.iter().count(), 1);
3156 let parsed = parsed
3157 .get(&EntityUid::from_strs("Employee", "12UA45"))
3158 .expect("that should be the employee id");
3159 assert_eq!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
3160 assert_eq!(
3161 parsed.attr("numDirectReports"),
3162 Some(Ok(EvalResult::Long(3)))
3163 );
3164 assert_eq!(
3165 parsed.attr("department"),
3166 Some(Ok(EvalResult::String("Sales".into())))
3167 );
3168 assert_eq!(
3169 parsed.attr("manager"),
3170 Some(Ok(EvalResult::EntityUid(EntityUid::from_strs(
3171 "Employee", "34FB87"
3172 ))))
3173 );
3174 {
3175 let Some(Ok(EvalResult::Set(set))) = parsed.attr("hr_contacts") else {
3176 panic!("expected hr_contacts attr to exist and be a Set")
3177 };
3178 let contact = set.iter().next().expect("should be at least one contact");
3179 assert!(matches!(contact, EvalResult::EntityUid(_)));
3180 };
3181 {
3182 let Some(Ok(EvalResult::Record(rec))) = parsed.attr("json_blob") else {
3183 panic!("expected json_blob attr to exist and be a Record")
3184 };
3185 let inner3 = rec.get("inner3").expect("expected inner3 attr to exist");
3186 let EvalResult::Record(rec) = inner3 else {
3187 panic!("expected inner3 to be a Record")
3188 };
3189 let innerinner = rec
3190 .get("innerinner")
3191 .expect("expected innerinner attr to exist");
3192 assert!(matches!(innerinner, EvalResult::EntityUid(_)));
3193 };
3194 assert_eq!(
3195 parsed.attr("home_ip"),
3196 Some(Ok(EvalResult::ExtensionValue("222.222.222.101/32".into())))
3197 );
3198 assert_eq!(
3199 parsed.attr("work_ip"),
3200 Some(Ok(EvalResult::ExtensionValue("2.2.2.0/24".into())))
3201 );
3202 assert_eq!(
3203 parsed.attr("trust_score"),
3204 Some(Ok(EvalResult::ExtensionValue("5.7000".into())))
3205 );
3206
3207 let entitiesjson = json!(
3209 [
3210 {
3211 "uid": { "type": "Employee", "id": "12UA45" },
3212 "attrs": {
3213 "isFullTime": true,
3214 "numDirectReports": "3",
3215 "department": "Sales",
3216 "manager": { "type": "Employee", "id": "34FB87" },
3217 "hr_contacts": [
3218 { "type": "HR", "id": "aaaaa" },
3219 { "type": "HR", "id": "bbbbb" }
3220 ],
3221 "json_blob": {
3222 "inner1": false,
3223 "inner2": "-*/",
3224 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3225 },
3226 "home_ip": "222.222.222.101",
3227 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3228 "trust_score": "5.7",
3229 "tricky": { "type": "Employee", "id": "34FB87" }
3230 },
3231 "parents": []
3232 }
3233 ]
3234 );
3235 let err = Entities::from_json_value(entitiesjson, Some(&schema))
3236 .expect_err("should fail due to type mismatch on numDirectReports");
3237 assert!(
3238 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"#),
3239 "actual error message was {err}"
3240 );
3241
3242 let entitiesjson = json!(
3244 [
3245 {
3246 "uid": { "type": "Employee", "id": "12UA45" },
3247 "attrs": {
3248 "isFullTime": true,
3249 "numDirectReports": 3,
3250 "department": "Sales",
3251 "manager": "34FB87",
3252 "hr_contacts": [
3253 { "type": "HR", "id": "aaaaa" },
3254 { "type": "HR", "id": "bbbbb" }
3255 ],
3256 "json_blob": {
3257 "inner1": false,
3258 "inner2": "-*/",
3259 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3260 },
3261 "home_ip": "222.222.222.101",
3262 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3263 "trust_score": "5.7",
3264 "tricky": { "type": "Employee", "id": "34FB87" }
3265 },
3266 "parents": []
3267 }
3268 ]
3269 );
3270 let err = Entities::from_json_value(entitiesjson, Some(&schema))
3271 .expect_err("should fail due to type mismatch on manager");
3272 assert!(
3273 err.to_string()
3274 .contains(r#"in attribute "manager" on Employee::"12UA45", expected a literal entity reference, but got "34FB87""#),
3275 "actual error message was {err}"
3276 );
3277
3278 let entitiesjson = json!(
3280 [
3281 {
3282 "uid": { "type": "Employee", "id": "12UA45" },
3283 "attrs": {
3284 "isFullTime": true,
3285 "numDirectReports": 3,
3286 "department": "Sales",
3287 "manager": { "type": "Employee", "id": "34FB87" },
3288 "hr_contacts": { "type": "HR", "id": "aaaaa" },
3289 "json_blob": {
3290 "inner1": false,
3291 "inner2": "-*/",
3292 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3293 },
3294 "home_ip": "222.222.222.101",
3295 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3296 "trust_score": "5.7",
3297 "tricky": { "type": "Employee", "id": "34FB87" }
3298 },
3299 "parents": []
3300 }
3301 ]
3302 );
3303 let err = Entities::from_json_value(entitiesjson, Some(&schema))
3304 .expect_err("should fail due to type mismatch on hr_contacts");
3305 assert!(
3306 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: ("#),
3307 "actual error message was {err}"
3308 );
3309
3310 let entitiesjson = json!(
3312 [
3313 {
3314 "uid": { "type": "Employee", "id": "12UA45" },
3315 "attrs": {
3316 "isFullTime": true,
3317 "numDirectReports": 3,
3318 "department": "Sales",
3319 "manager": { "type": "HR", "id": "34FB87" },
3320 "hr_contacts": [
3321 { "type": "HR", "id": "aaaaa" },
3322 { "type": "HR", "id": "bbbbb" }
3323 ],
3324 "json_blob": {
3325 "inner1": false,
3326 "inner2": "-*/",
3327 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3328 },
3329 "home_ip": "222.222.222.101",
3330 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3331 "trust_score": "5.7",
3332 "tricky": { "type": "Employee", "id": "34FB87" }
3333 },
3334 "parents": []
3335 }
3336 ]
3337 );
3338 let err = Entities::from_json_value(entitiesjson, Some(&schema))
3339 .expect_err("should fail due to type mismatch on manager");
3340 assert!(
3341 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)"#),
3342 "actual error message was {err}"
3343 );
3344
3345 let entitiesjson = json!(
3348 [
3349 {
3350 "uid": { "type": "Employee", "id": "12UA45" },
3351 "attrs": {
3352 "isFullTime": true,
3353 "numDirectReports": 3,
3354 "department": "Sales",
3355 "manager": { "type": "Employee", "id": "34FB87" },
3356 "hr_contacts": [
3357 { "type": "HR", "id": "aaaaa" },
3358 { "type": "HR", "id": "bbbbb" }
3359 ],
3360 "json_blob": {
3361 "inner1": false,
3362 "inner2": "-*/",
3363 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3364 },
3365 "home_ip": { "fn": "decimal", "arg": "3.33" },
3366 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3367 "trust_score": "5.7",
3368 "tricky": { "type": "Employee", "id": "34FB87" }
3369 },
3370 "parents": []
3371 }
3372 ]
3373 );
3374 let err = Entities::from_json_value(entitiesjson, Some(&schema))
3375 .expect_err("should fail due to type mismatch on home_ip");
3376 assert!(
3377 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"#),
3378 "actual error message was {err}"
3379 );
3380
3381 let entitiesjson = json!(
3383 [
3384 {
3385 "uid": { "type": "Employee", "id": "12UA45" },
3386 "attrs": {
3387 "isFullTime": true,
3388 "numDirectReports": 3,
3389 "department": "Sales",
3390 "manager": { "type": "Employee", "id": "34FB87" },
3391 "hr_contacts": [
3392 { "type": "HR", "id": "aaaaa" },
3393 { "type": "HR", "id": "bbbbb" }
3394 ],
3395 "json_blob": {
3396 "inner1": false,
3397 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3398 },
3399 "home_ip": "222.222.222.101",
3400 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3401 "trust_score": "5.7",
3402 "tricky": { "type": "Employee", "id": "34FB87" }
3403 },
3404 "parents": []
3405 }
3406 ]
3407 );
3408 let err = Entities::from_json_value(entitiesjson, Some(&schema))
3409 .expect_err("should fail due to missing attribute \"inner2\"");
3410 assert!(
3411 err.to_string().contains(r#"in attribute "json_blob" on Employee::"12UA45", expected the record to have an attribute "inner2", but it didn't"#),
3412 "actual error message was {err}"
3413 );
3414
3415 let entitiesjson = json!(
3417 [
3418 {
3419 "uid": { "type": "Employee", "id": "12UA45" },
3420 "attrs": {
3421 "isFullTime": true,
3422 "numDirectReports": 3,
3423 "department": "Sales",
3424 "manager": { "type": "Employee", "id": "34FB87" },
3425 "hr_contacts": [
3426 { "type": "HR", "id": "aaaaa" },
3427 { "type": "HR", "id": "bbbbb" }
3428 ],
3429 "json_blob": {
3430 "inner1": 33,
3431 "inner2": "-*/",
3432 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3433 },
3434 "home_ip": "222.222.222.101",
3435 "work_ip": { "fn": "ip", "arg": "2.2.2.0/24" },
3436 "trust_score": "5.7",
3437 "tricky": { "type": "Employee", "id": "34FB87" }
3438 },
3439 "parents": []
3440 }
3441 ]
3442 );
3443 let err = Entities::from_json_value(entitiesjson, Some(&schema))
3444 .expect_err("should fail due to type mismatch on attribute \"inner1\"");
3445 assert!(
3446 err.to_string().contains(r#"in attribute "json_blob" on Employee::"12UA45", type mismatch: attribute was expected to have type record with attributes: "#),
3447 "actual error message was {err}"
3448 );
3449
3450 let entitiesjson = json!(
3451 [
3452 {
3453 "uid": { "__entity": { "type": "Employee", "id": "12UA45" } },
3454 "attrs": {
3455 "isFullTime": true,
3456 "numDirectReports": 3,
3457 "department": "Sales",
3458 "manager": { "__entity": { "type": "Employee", "id": "34FB87" } },
3459 "hr_contacts": [
3460 { "type": "HR", "id": "aaaaa" },
3461 { "type": "HR", "id": "bbbbb" }
3462 ],
3463 "json_blob": {
3464 "inner1": false,
3465 "inner2": "-*/",
3466 "inner3": { "innerinner": { "type": "Employee", "id": "09AE76" }},
3467 },
3468 "home_ip": { "__extn": { "fn": "ip", "arg": "222.222.222.101" } },
3469 "work_ip": { "__extn": { "fn": "ip", "arg": "2.2.2.0/24" } },
3470 "trust_score": { "__extn": { "fn": "decimal", "arg": "5.7" } },
3471 "tricky": { "type": "Employee", "id": "34FB87" }
3472 },
3473 "parents": []
3474 }
3475 ]
3476 );
3477
3478 Entities::from_json_value(entitiesjson, Some(&schema))
3479 .expect("this version with explicit __entity and __extn escapes should also pass");
3480 }
3481
3482 #[test]
3484 fn namespaces() {
3485 let schema = Schema::from_str(
3486 r#"
3487 {"XYZCorp": {
3488 "entityTypes": {
3489 "Employee": {
3490 "memberOfTypes": [],
3491 "shape": {
3492 "type": "Record",
3493 "attributes": {
3494 "isFullTime": { "type": "Boolean" },
3495 "department": { "type": "String" },
3496 "manager": {
3497 "type": "Entity",
3498 "name": "XYZCorp::Employee"
3499 }
3500 }
3501 }
3502 }
3503 },
3504 "actions": {
3505 "view": {}
3506 }
3507 }}
3508 "#,
3509 )
3510 .expect("should be a valid schema");
3511
3512 let entitiesjson = json!(
3513 [
3514 {
3515 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
3516 "attrs": {
3517 "isFullTime": true,
3518 "department": "Sales",
3519 "manager": { "type": "XYZCorp::Employee", "id": "34FB87" }
3520 },
3521 "parents": []
3522 }
3523 ]
3524 );
3525 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3526 .expect("Should parse without error");
3527 assert_eq!(parsed.iter().count(), 1);
3528 let parsed = parsed
3529 .get(&EntityUid::from_strs("XYZCorp::Employee", "12UA45"))
3530 .expect("that should be the employee type and id");
3531 assert_eq!(parsed.attr("isFullTime"), Some(Ok(EvalResult::Bool(true))));
3532 assert_eq!(
3533 parsed.attr("department"),
3534 Some(Ok(EvalResult::String("Sales".into())))
3535 );
3536 assert_eq!(
3537 parsed.attr("manager"),
3538 Some(Ok(EvalResult::EntityUid(EntityUid::from_strs(
3539 "XYZCorp::Employee",
3540 "34FB87"
3541 ))))
3542 );
3543
3544 let entitiesjson = json!(
3545 [
3546 {
3547 "uid": { "type": "XYZCorp::Employee", "id": "12UA45" },
3548 "attrs": {
3549 "isFullTime": true,
3550 "department": "Sales",
3551 "manager": { "type": "Employee", "id": "34FB87" }
3552 },
3553 "parents": []
3554 }
3555 ]
3556 );
3557 let err = Entities::from_json_value(entitiesjson, Some(&schema))
3558 .expect_err("should fail due to manager being wrong entity type (missing namespace)");
3559 assert!(
3560 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)"#),
3561 "actual error message was {err}"
3562 );
3563 }
3564
3565 #[test]
3567 fn optional_attrs() {
3568 let schema = Schema::from_str(
3569 r#"
3570 {"": {
3571 "entityTypes": {
3572 "Employee": {
3573 "memberOfTypes": [],
3574 "shape": {
3575 "type": "Record",
3576 "attributes": {
3577 "isFullTime": { "type": "Boolean" },
3578 "department": { "type": "String", "required": false },
3579 "manager": { "type": "Entity", "name": "Employee" }
3580 }
3581 }
3582 }
3583 },
3584 "actions": {
3585 "view": {}
3586 }
3587 }}
3588 "#,
3589 )
3590 .expect("should be a valid schema");
3591
3592 let entitiesjson = json!(
3594 [
3595 {
3596 "uid": { "type": "Employee", "id": "12UA45" },
3597 "attrs": {
3598 "isFullTime": true,
3599 "department": "Sales",
3600 "manager": { "type": "Employee", "id": "34FB87" }
3601 },
3602 "parents": []
3603 }
3604 ]
3605 );
3606 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3607 .expect("Should parse without error");
3608 assert_eq!(parsed.iter().count(), 1);
3609
3610 let entitiesjson = json!(
3612 [
3613 {
3614 "uid": { "type": "Employee", "id": "12UA45" },
3615 "attrs": {
3616 "isFullTime": true,
3617 "manager": { "type": "Employee", "id": "34FB87" }
3618 },
3619 "parents": []
3620 }
3621 ]
3622 );
3623 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3624 .expect("Should parse without error");
3625 assert_eq!(parsed.iter().count(), 1);
3626 }
3627
3628 #[test]
3630 #[should_panic(
3631 expected = "UnsupportedSchemaFeature(\"Records and entities with additional attributes are not yet implemented.\")"
3632 )]
3633 fn open_entities() {
3634 let schema = Schema::from_str(
3635 r#"
3636 {"": {
3637 "entityTypes": {
3638 "Employee": {
3639 "memberOfTypes": [],
3640 "shape": {
3641 "type": "Record",
3642 "attributes": {
3643 "isFullTime": { "type": "Boolean" },
3644 "department": { "type": "String", "required": false },
3645 "manager": { "type": "Entity", "name": "Employee" }
3646 },
3647 "additionalAttributes": true
3648 }
3649 }
3650 },
3651 "actions": {
3652 "view": {}
3653 }
3654 }}
3655 "#,
3656 )
3657 .expect("should be a valid schema");
3658
3659 let entitiesjson = json!(
3661 [
3662 {
3663 "uid": { "type": "Employee", "id": "12UA45" },
3664 "attrs": {
3665 "isFullTime": true,
3666 "department": "Sales",
3667 "manager": { "type": "Employee", "id": "34FB87" }
3668 },
3669 "parents": []
3670 }
3671 ]
3672 );
3673 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3674 .expect("Should parse without error");
3675 assert_eq!(parsed.iter().count(), 1);
3676
3677 let entitiesjson = json!(
3679 [
3680 {
3681 "uid": { "type": "Employee", "id": "12UA45" },
3682 "attrs": {
3683 "isFullTime": true,
3684 "foobar": 234,
3685 "manager": { "type": "Employee", "id": "34FB87" }
3686 },
3687 "parents": []
3688 }
3689 ]
3690 );
3691 let parsed = Entities::from_json_value(entitiesjson, Some(&schema))
3692 .expect("Should parse without error");
3693 assert_eq!(parsed.iter().count(), 1);
3694 }
3695
3696 #[test]
3697 fn schema_sanity_check() {
3698 let src = "{ , .. }";
3699 assert_matches!(Schema::from_str(src), Err(super::SchemaError::ParseJson(_)));
3700 }
3701
3702 #[test]
3703 fn template_constraint_sanity_checks() {
3704 assert!(!TemplatePrincipalConstraint::Any.has_slot());
3705 assert!(!TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
3706 assert!(!TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
3707 assert!(TemplatePrincipalConstraint::In(None).has_slot());
3708 assert!(TemplatePrincipalConstraint::Eq(None).has_slot());
3709 assert!(!TemplateResourceConstraint::Any.has_slot());
3710 assert!(!TemplateResourceConstraint::In(Some(EntityUid::from_strs("a", "a"))).has_slot());
3711 assert!(!TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("a", "a"))).has_slot());
3712 assert!(TemplateResourceConstraint::In(None).has_slot());
3713 assert!(TemplateResourceConstraint::Eq(None).has_slot());
3714 }
3715
3716 #[test]
3717 fn template_principal_constraints() {
3718 let src = r#"
3719 permit(principal, action, resource);
3720 "#;
3721 let t = Template::parse(None, src).unwrap();
3722 assert_eq!(t.principal_constraint(), TemplatePrincipalConstraint::Any);
3723
3724 let src = r#"
3725 permit(principal == ?principal, action, resource);
3726 "#;
3727 let t = Template::parse(None, src).unwrap();
3728 assert_eq!(
3729 t.principal_constraint(),
3730 TemplatePrincipalConstraint::Eq(None)
3731 );
3732
3733 let src = r#"
3734 permit(principal == A::"a", action, resource);
3735 "#;
3736 let t = Template::parse(None, src).unwrap();
3737 assert_eq!(
3738 t.principal_constraint(),
3739 TemplatePrincipalConstraint::Eq(Some(EntityUid::from_strs("A", "a")))
3740 );
3741
3742 let src = r#"
3743 permit(principal in ?principal, action, resource);
3744 "#;
3745 let t = Template::parse(None, src).unwrap();
3746 assert_eq!(
3747 t.principal_constraint(),
3748 TemplatePrincipalConstraint::In(None)
3749 );
3750
3751 let src = r#"
3752 permit(principal in A::"a", action, resource);
3753 "#;
3754 let t = Template::parse(None, src).unwrap();
3755 assert_eq!(
3756 t.principal_constraint(),
3757 TemplatePrincipalConstraint::In(Some(EntityUid::from_strs("A", "a")))
3758 );
3759 }
3760
3761 #[test]
3762 fn template_action_constraints() {
3763 let src = r#"
3764 permit(principal, action, resource);
3765 "#;
3766 let t = Template::parse(None, src).unwrap();
3767 assert_eq!(t.action_constraint(), ActionConstraint::Any);
3768
3769 let src = r#"
3770 permit(principal, action == Action::"A", resource);
3771 "#;
3772 let t = Template::parse(None, src).unwrap();
3773 assert_eq!(
3774 t.action_constraint(),
3775 ActionConstraint::Eq(EntityUid::from_strs("Action", "A"))
3776 );
3777
3778 let src = r#"
3779 permit(principal, action in [Action::"A", Action::"B"], resource);
3780 "#;
3781 let t = Template::parse(None, src).unwrap();
3782 assert_eq!(
3783 t.action_constraint(),
3784 ActionConstraint::In(vec![
3785 EntityUid::from_strs("Action", "A"),
3786 EntityUid::from_strs("Action", "B")
3787 ])
3788 );
3789 }
3790
3791 #[test]
3792 fn template_resource_constraints() {
3793 let src = r#"
3794 permit(principal, action, resource);
3795 "#;
3796 let t = Template::parse(None, src).unwrap();
3797 assert_eq!(t.resource_constraint(), TemplateResourceConstraint::Any);
3798
3799 let src = r#"
3800 permit(principal, action, resource == ?resource);
3801 "#;
3802 let t = Template::parse(None, src).unwrap();
3803 assert_eq!(
3804 t.resource_constraint(),
3805 TemplateResourceConstraint::Eq(None)
3806 );
3807
3808 let src = r#"
3809 permit(principal, action, resource == A::"a");
3810 "#;
3811 let t = Template::parse(None, src).unwrap();
3812 assert_eq!(
3813 t.resource_constraint(),
3814 TemplateResourceConstraint::Eq(Some(EntityUid::from_strs("A", "a")))
3815 );
3816
3817 let src = r#"
3818 permit(principal, action, resource in ?resource);
3819 "#;
3820 let t = Template::parse(None, src).unwrap();
3821 assert_eq!(
3822 t.resource_constraint(),
3823 TemplateResourceConstraint::In(None)
3824 );
3825
3826 let src = r#"
3827 permit(principal, action, resource in A::"a");
3828 "#;
3829 let t = Template::parse(None, src).unwrap();
3830 assert_eq!(
3831 t.resource_constraint(),
3832 TemplateResourceConstraint::In(Some(EntityUid::from_strs("A", "a")))
3833 );
3834 }
3835
3836 #[test]
3837 fn schema_namespace() {
3838 let fragment: SchemaFragment = r#"
3839 {
3840 "Foo::Bar": {
3841 "entityTypes": {},
3842 "actions": {}
3843 }
3844 }
3845 "#
3846 .parse()
3847 .unwrap();
3848 let namespaces = fragment.namespaces().next().unwrap();
3849 assert_eq!(
3850 namespaces.map(|ns| ns.to_string()),
3851 Some("Foo::Bar".to_string())
3852 );
3853 let _schema: Schema = fragment.try_into().expect("Should convert to schema");
3854
3855 let fragment: SchemaFragment = r#"
3856 {
3857 "": {
3858 "entityTypes": {},
3859 "actions": {}
3860 }
3861 }
3862 "#
3863 .parse()
3864 .unwrap();
3865 let namespaces = fragment.namespaces().next().unwrap();
3866 assert_eq!(namespaces, None);
3867 let _schema: Schema = fragment.try_into().expect("Should convert to schema");
3868 }
3869
3870 #[test]
3871 fn load_multiple_namespaces() {
3872 let fragment = SchemaFragment::from_json_value(json!({
3873 "Foo::Bar": {
3874 "entityTypes": {
3875 "Baz": {
3876 "memberOfTypes": ["Bar::Foo::Baz"]
3877 }
3878 },
3879 "actions": {}
3880 },
3881 "Bar::Foo": {
3882 "entityTypes": {
3883 "Baz": {
3884 "memberOfTypes": ["Foo::Bar::Baz"]
3885 }
3886 },
3887 "actions": {}
3888 }
3889 }))
3890 .unwrap();
3891
3892 let schema = Schema::from_schema_fragments([fragment]).unwrap();
3893
3894 assert!(schema
3895 .0
3896 .get_entity_type(&"Foo::Bar::Baz".parse().unwrap())
3897 .is_some());
3898 assert!(schema
3899 .0
3900 .get_entity_type(&"Bar::Foo::Baz".parse().unwrap())
3901 .is_some());
3902 }
3903
3904 #[test]
3905 fn get_attributes_from_schema() {
3906 let fragment: SchemaFragment = SchemaFragment::from_json_value(json!({
3907 "": {
3908 "entityTypes": {},
3909 "actions": {
3910 "A": {},
3911 "B": {
3912 "memberOf": [{"id": "A"}]
3913 },
3914 "C": {
3915 "memberOf": [{"id": "A"}]
3916 },
3917 "D": {
3918 "memberOf": [{"id": "B"}, {"id": "C"}]
3919 },
3920 "E": {
3921 "memberOf": [{"id": "D"}]
3922 }
3923 }
3924 }}))
3925 .unwrap();
3926
3927 let schema = Schema::from_schema_fragments([fragment]).unwrap();
3928 let action_entities = schema.action_entities().unwrap();
3929
3930 let a_euid = EntityUid::from_strs("Action", "A");
3931 let b_euid = EntityUid::from_strs("Action", "B");
3932 let c_euid = EntityUid::from_strs("Action", "C");
3933 let d_euid = EntityUid::from_strs("Action", "D");
3934 let e_euid = EntityUid::from_strs("Action", "E");
3935
3936 assert_eq!(
3937 action_entities,
3938 Entities::from_entities([
3939 Entity::new(a_euid.clone(), HashMap::new(), HashSet::new()),
3940 Entity::new(
3941 b_euid.clone(),
3942 HashMap::new(),
3943 HashSet::from([a_euid.clone()])
3944 ),
3945 Entity::new(
3946 c_euid.clone(),
3947 HashMap::new(),
3948 HashSet::from([a_euid.clone()])
3949 ),
3950 Entity::new(
3951 d_euid.clone(),
3952 HashMap::new(),
3953 HashSet::from([a_euid.clone(), b_euid.clone(), c_euid.clone()])
3954 ),
3955 Entity::new(
3956 e_euid,
3957 HashMap::new(),
3958 HashSet::from([a_euid, b_euid, c_euid, d_euid])
3959 ),
3960 ])
3961 .unwrap()
3962 );
3963 }
3964}