1use crate::ast::*;
18use crate::parser::{
19 err::parse_errors::{InvalidActionType, SlotsInConditionClause},
20 Loc,
21};
22use annotation::{Annotation, Annotations};
23use educe::Educe;
24use itertools::Itertools;
25use miette::Diagnostic;
26use nonempty::{nonempty, NonEmpty};
27use serde::{Deserialize, Serialize};
28use smol_str::SmolStr;
29use std::{
30 collections::{HashMap, HashSet},
31 str::FromStr,
32 sync::Arc,
33};
34use thiserror::Error;
35
36#[cfg(feature = "wasm")]
37extern crate tsify;
38
39macro_rules! cfg_tolerant_ast {
40 ($($item:item)*) => {
41 $(
42 #[cfg(feature = "tolerant-ast")]
43 $item
44 )*
45 };
46}
47
48cfg_tolerant_ast! {
49 use super::expr_allows_errors::AstExprErrorKind;
50 use crate::ast::expr_allows_errors::ExprWithErrsBuilder;
51 use crate::expr_builder::ExprBuilder;
52 use crate::parser::err::{ParseErrors, ToASTError, ToASTErrorKind};
53
54 static DEFAULT_ANNOTATIONS: std::sync::LazyLock<Arc<Annotations>> =
55 std::sync::LazyLock::new(|| Arc::new(Annotations::default()));
56
57 static DEFAULT_PRINCIPAL_CONSTRAINT: std::sync::LazyLock<PrincipalConstraint> =
58 std::sync::LazyLock::new(PrincipalConstraint::any);
59
60 static DEFAULT_RESOURCE_CONSTRAINT: std::sync::LazyLock<ResourceConstraint> =
61 std::sync::LazyLock::new(ResourceConstraint::any);
62
63 static DEFAULT_ACTION_CONSTRAINT: std::sync::LazyLock<ActionConstraint> =
64 std::sync::LazyLock::new(ActionConstraint::any);
65
66 static DEFAULT_ERROR_EXPR: std::sync::LazyLock<Arc<Expr>> = std::sync::LazyLock::new(|| {
67 Arc::new(
70 <ExprWithErrsBuilder as ExprBuilder>::new()
71 .error(ParseErrors::singleton(ToASTError::new(
72 ToASTErrorKind::ASTErrorNode,
73 Some(Loc::new(0..1, "ASTErrorNode".into())),
74 )))
75 .unwrap_infallible(),
76 )
77 });
78}
79
80#[derive(Clone, Hash, Eq, PartialEq, Debug)]
85pub struct Template {
86 body: TemplateBody,
87 slots: Vec<Slot>,
92}
93
94impl From<Template> for TemplateBody {
95 fn from(val: Template) -> Self {
96 val.body
97 }
98}
99
100impl Template {
101 fn check_slot_cache(&self) -> Result<(), TemplateValidationError> {
106 for slot in self.body.condition().slots() {
107 if !self.slots.contains(&slot) {
108 return Err(TemplateValidationError::SlotCacheMismatch);
109 }
110 }
111 for slot in &self.slots {
112 if !self.body.condition().slots().contains(slot) {
113 return Err(TemplateValidationError::SlotCacheMismatch);
114 }
115 }
116 Ok(())
117 }
118
119 pub fn check_invariant(&self) {
123 #[cfg(debug_assertions)]
124 {
125 assert!(self.check_slot_cache().is_ok());
126 }
127 }
128
129 #[expect(
131 clippy::too_many_arguments,
132 reason = "policies just have this many components"
133 )]
134 pub fn new(
135 id: PolicyID,
136 loc: Option<Loc>,
137 annotations: Annotations,
138 effect: Effect,
139 principal_constraint: PrincipalConstraint,
140 action_constraint: ActionConstraint,
141 resource_constraint: ResourceConstraint,
142 non_scope_constraint: Option<Expr>,
143 ) -> Self {
144 let body = TemplateBody::new(
145 id,
146 loc,
147 annotations,
148 effect,
149 principal_constraint,
150 action_constraint,
151 resource_constraint,
152 non_scope_constraint,
153 );
154 Template::from(body)
157 }
158
159 #[cfg(feature = "tolerant-ast")]
160 pub fn error(id: PolicyID, loc: Option<Loc>) -> Self {
162 let body = TemplateBody::error(id, loc);
163 Template::from(body)
164 }
165
166 #[expect(
168 clippy::too_many_arguments,
169 reason = "policies just have this many components"
170 )]
171 pub fn new_shared(
172 id: PolicyID,
173 loc: Option<Loc>,
174 annotations: Arc<Annotations>,
175 effect: Effect,
176 principal_constraint: PrincipalConstraint,
177 action_constraint: ActionConstraint,
178 resource_constraint: ResourceConstraint,
179 non_scope_constraint: Option<Arc<Expr>>,
180 ) -> Self {
181 let body = TemplateBody::new_shared(
182 id,
183 loc,
184 annotations,
185 effect,
186 principal_constraint,
187 action_constraint,
188 resource_constraint,
189 non_scope_constraint,
190 );
191 Template::from(body)
194 }
195
196 #[expect(clippy::type_complexity, reason = "policies just have many components")]
201 pub(crate) fn into_template_components_opt(
202 self,
203 ) -> Option<(
204 PolicyID,
205 Arc<Annotations>,
206 Effect,
207 PrincipalConstraint,
208 ActionConstraint,
209 ResourceConstraint,
210 Option<Arc<Expr>>,
211 )> {
212 self.body.into_components_opt()
213 }
214
215 pub fn principal_constraint(&self) -> &PrincipalConstraint {
217 self.body.principal_constraint()
218 }
219
220 pub fn action_constraint(&self) -> &ActionConstraint {
222 self.body.action_constraint()
223 }
224
225 pub fn resource_constraint(&self) -> &ResourceConstraint {
227 self.body.resource_constraint()
228 }
229
230 pub fn non_scope_constraints(&self) -> Option<&Expr> {
232 self.body.non_scope_constraints()
233 }
234
235 pub fn non_scope_constraints_arc(&self) -> Option<&Arc<Expr>> {
237 self.body.non_scope_constraints_arc()
238 }
239
240 pub fn id(&self) -> &PolicyID {
242 self.body.id()
243 }
244
245 pub fn new_id(&self, id: PolicyID) -> Self {
247 Template {
248 body: self.body.new_id(id),
249 slots: self.slots.clone(),
250 }
251 }
252
253 pub fn loc(&self) -> Option<&Loc> {
255 self.body.loc()
256 }
257
258 pub fn effect(&self) -> Effect {
260 self.body.effect()
261 }
262
263 pub fn annotation(&self, key: &AnyId) -> Option<&Annotation> {
265 self.body.annotation(key)
266 }
267
268 pub fn annotations(&self) -> impl Iterator<Item = (&AnyId, &Annotation)> {
270 self.body.annotations()
271 }
272
273 pub fn annotations_arc(&self) -> &Arc<Annotations> {
275 self.body.annotations_arc()
276 }
277
278 pub fn condition(&self) -> Expr {
284 self.body.condition()
285 }
286
287 pub fn slots(&self) -> impl Iterator<Item = &Slot> {
289 self.slots.iter()
290 }
291
292 pub fn is_static(&self) -> bool {
297 self.slots.is_empty()
298 }
299
300 pub fn check_binding(
304 template: &Template,
305 values: &HashMap<SlotId, EntityUID>,
306 ) -> Result<(), LinkingError> {
307 let unbound = template
309 .slots
310 .iter()
311 .filter(|slot| !values.contains_key(&slot.id))
312 .collect::<Vec<_>>();
313
314 let extra = values
315 .keys()
316 .filter(|slot| {
317 !template
318 .slots
319 .iter()
320 .any(|template_slot| template_slot.id == **slot)
321 })
322 .collect::<Vec<_>>();
323
324 if unbound.is_empty() && extra.is_empty() {
325 Ok(())
326 } else {
327 Err(LinkingError::from_unbound_and_extras(
328 unbound.into_iter().map(|slot| slot.id),
329 extra.into_iter().copied(),
330 ))
331 }
332 }
333
334 pub fn link(
338 template: Arc<Template>,
339 new_id: PolicyID,
340 values: HashMap<SlotId, EntityUID>,
341 ) -> Result<Policy, LinkingError> {
342 Template::check_binding(&template, &values)
344 .map(|_| Policy::new(template, Some(new_id), values))
345 }
346
347 pub fn try_as_policy(template: Arc<Template>) -> Result<Policy, LinkingError> {
350 Template::check_binding(&template, &HashMap::new())
352 .map(|_| Policy::new(template, None, HashMap::new()))
353 }
354
355 pub fn link_static_policy(p: StaticPolicy) -> (Arc<Template>, Policy) {
358 let body: TemplateBody = p.into();
359 let t = Arc::new(Self {
363 body,
364 slots: vec![],
365 });
366 t.check_invariant();
367 let p = Policy::new(Arc::clone(&t), None, HashMap::new());
368 (t, p)
369 }
370
371 pub fn try_validate(self) -> Result<Self, TemplateValidationError> {
381 if let Some(expr) = self.body.non_scope_constraints() {
383 if let Some(slot) = expr.slots().next() {
384 return Err(TemplateValidationError::SlotsInConditionClause(
385 SlotsInConditionClause {
386 slot,
387 clause_type: "when/unless", },
389 ));
390 }
391 }
392 if let Err(euids) = self.body.action_constraint().check_action_types() {
394 return Err(TemplateValidationError::InvalidActionType(
395 InvalidActionType { euids },
396 ));
397 }
398
399 self.check_slot_cache()?;
401 if let Some(expr) = self.body.non_scope_constraints() {
403 expr.clone().try_validate()?;
404 }
405 Ok(self)
406 }
407}
408
409#[derive(Debug, Clone, Diagnostic, Error)]
411pub enum TemplateValidationError {
412 #[error(transparent)]
414 #[diagnostic(transparent)]
415 InvalidActionType(#[from] crate::parser::err::parse_errors::InvalidActionType),
416 #[error(transparent)]
418 #[diagnostic(transparent)]
419 SlotsInConditionClause(#[from] crate::parser::err::parse_errors::SlotsInConditionClause),
420 #[error("template slot cache is inconsistent with condition expression")]
422 SlotCacheMismatch,
423 #[error(transparent)]
425 #[diagnostic(transparent)]
426 InvalidExpression(#[from] ExprValidationError),
427}
428
429impl From<TemplateBody> for Template {
430 fn from(body: TemplateBody) -> Self {
431 let slots = body.condition().slots().collect::<Vec<_>>();
434 Self { body, slots }
435 }
436}
437
438impl std::fmt::Display for Template {
439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440 write!(f, "{}", self.body)
441 }
442}
443
444#[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
446pub enum LinkingError {
447 #[error(fmt = describe_arity_error)]
450 ArityError {
451 unbound_values: Vec<SlotId>,
453 extra_values: Vec<SlotId>,
455 },
456
457 #[error("failed to find a template with id `{id}`")]
459 NoSuchTemplate {
460 id: PolicyID,
462 },
463
464 #[error("template-linked policy id `{id}` conflicts with an existing policy id")]
466 PolicyIdConflict {
467 id: PolicyID,
469 },
470}
471
472impl LinkingError {
473 fn from_unbound_and_extras(
474 unbound: impl Iterator<Item = SlotId>,
475 extra: impl Iterator<Item = SlotId>,
476 ) -> Self {
477 Self::ArityError {
478 unbound_values: unbound.collect(),
479 extra_values: extra.collect(),
480 }
481 }
482}
483
484fn describe_arity_error(
485 unbound_values: &[SlotId],
486 extra_values: &[SlotId],
487 fmt: &mut std::fmt::Formatter<'_>,
488) -> std::fmt::Result {
489 match (unbound_values.len(), extra_values.len()) {
490 #[expect(clippy::unreachable, reason = "0,0 case is not an error")]
491 (0,0) => unreachable!(),
492 (_unbound, 0) => write!(fmt, "the following slots were not provided as arguments: {}", unbound_values.iter().join(",")),
493 (0, _extra) => write!(fmt, "the following slots were provided as arguments, but did not exist in the template: {}", extra_values.iter().join(",")),
494 (_unbound, _extra) => write!(fmt, "the following slots were not provided as arguments: {}. The following slots were provided as arguments, but did not exist in the template: {}", unbound_values.iter().join(","), extra_values.iter().join(",")),
495 }
496}
497
498#[derive(Debug, Clone, Eq, PartialEq)]
506pub struct Policy {
507 template: Arc<Template>,
509 link: Option<PolicyID>,
513 values: HashMap<SlotId, EntityUID>,
520}
521
522impl Policy {
523 pub(crate) fn new(template: Arc<Template>, link_id: Option<PolicyID>, values: SlotEnv) -> Self {
527 #[cfg(debug_assertions)]
528 {
529 #[expect(
530 clippy::expect_used,
531 reason = "asserts (value total map invariant) which is justified at call sites"
532 )]
533 Template::check_binding(&template, &values).expect("(values total map) does not hold!");
534 }
535 Self {
536 template,
537 link: link_id,
538 values,
539 }
540 }
541
542 pub fn from_when_clause(effect: Effect, when: Expr, id: PolicyID, loc: Option<Loc>) -> Self {
544 Self::from_when_clause_annos(
545 effect,
546 Arc::new(when),
547 id,
548 loc,
549 Arc::new(Annotations::default()),
550 )
551 }
552
553 pub fn from_when_clause_annos(
555 effect: Effect,
556 when: Arc<Expr>,
557 id: PolicyID,
558 loc: Option<Loc>,
559 annotations: Arc<Annotations>,
560 ) -> Self {
561 let t = Template::new_shared(
562 id,
563 loc,
564 annotations,
565 effect,
566 PrincipalConstraint::any(),
567 ActionConstraint::any(),
568 ResourceConstraint::any(),
569 Some(when),
570 );
571 Self::new(Arc::new(t), None, SlotEnv::new())
572 }
573
574 pub(crate) fn into_components(self) -> (Arc<Template>, Option<PolicyID>, SlotEnv) {
577 (self.template, self.link, self.values)
578 }
579
580 pub fn template(&self) -> &Template {
582 &self.template
583 }
584
585 pub(crate) fn template_arc(&self) -> Arc<Template> {
587 Arc::clone(&self.template)
588 }
589
590 pub fn effect(&self) -> Effect {
592 self.template.effect()
593 }
594
595 pub fn annotation(&self, key: &AnyId) -> Option<&Annotation> {
597 self.template.annotation(key)
598 }
599
600 pub fn annotations(&self) -> impl Iterator<Item = (&AnyId, &Annotation)> {
602 self.template.annotations()
603 }
604
605 pub fn annotations_arc(&self) -> &Arc<Annotations> {
607 self.template.annotations_arc()
608 }
609
610 pub fn principal_constraint(&self) -> PrincipalConstraint {
616 let constraint = self.template.principal_constraint().clone();
617 match self.values.get(&SlotId::principal()) {
618 None => constraint,
619 Some(principal) => constraint.with_filled_slot(Arc::new(principal.clone())),
620 }
621 }
622
623 pub fn action_constraint(&self) -> &ActionConstraint {
625 self.template.action_constraint()
626 }
627
628 pub fn resource_constraint(&self) -> ResourceConstraint {
634 let constraint = self.template.resource_constraint().clone();
635 match self.values.get(&SlotId::resource()) {
636 None => constraint,
637 Some(resource) => constraint.with_filled_slot(Arc::new(resource.clone())),
638 }
639 }
640
641 pub fn non_scope_constraints(&self) -> Option<&Expr> {
643 self.template.non_scope_constraints()
644 }
645
646 pub fn non_scope_constraints_arc(&self) -> Option<&Arc<Expr>> {
648 self.template.non_scope_constraints_arc()
649 }
650
651 pub fn condition(&self) -> Expr {
653 self.template.condition()
654 }
655
656 pub fn env(&self) -> &SlotEnv {
659 &self.values
660 }
661
662 pub fn id(&self) -> &PolicyID {
664 self.link.as_ref().unwrap_or_else(|| self.template.id())
665 }
666
667 pub fn new_id(&self, id: PolicyID) -> Self {
669 match self.link {
670 None => Policy {
671 template: Arc::new(self.template.new_id(id)),
672 link: None,
673 values: self.values.clone(),
674 },
675 Some(_) => Policy {
676 template: self.template.clone(),
677 link: Some(id),
678 values: self.values.clone(),
679 },
680 }
681 }
682
683 pub(crate) fn new_template_id(self, id: PolicyID) -> Option<Self> {
686 self.link.map(|link| Policy {
687 template: Arc::new(self.template.new_id(id)),
688 link: Some(link),
689 values: self.values,
690 })
691 }
692
693 pub fn loc(&self) -> Option<&Loc> {
695 self.template.loc()
696 }
697
698 pub fn is_static(&self) -> bool {
700 self.link.is_none()
701 }
702
703 pub fn unknown_entities(&self) -> HashSet<EntityUID> {
705 self.condition()
706 .unknowns()
707 .filter_map(
708 |Unknown {
709 name,
710 type_annotation,
711 }| {
712 if matches!(type_annotation, Some(Type::Entity { .. })) {
713 EntityUID::from_str(name.as_str()).ok()
714 } else {
715 None
716 }
717 },
718 )
719 .collect()
720 }
721}
722
723fn display_slot_env(env: &SlotEnv) -> String {
724 env.iter()
725 .map(|(slot, value)| format!("{slot} -> {value}"))
726 .join(",")
727}
728
729impl std::fmt::Display for Policy {
730 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
731 if self.is_static() {
732 write!(f, "{}", self.template())
733 } else {
734 write!(
735 f,
736 "Template Instance of {}, slots: [{}]",
737 self.template().id(),
738 display_slot_env(self.env())
739 )
740 }
741 }
742}
743
744pub type SlotEnv = HashMap<SlotId, EntityUID>;
746
747#[derive(Clone, Hash, Eq, PartialEq, Debug)]
751pub struct StaticPolicy(TemplateBody);
752
753impl StaticPolicy {
754 pub fn id(&self) -> &PolicyID {
756 self.0.id()
757 }
758
759 pub fn new_id(&self, id: PolicyID) -> Self {
761 StaticPolicy(self.0.new_id(id))
762 }
763
764 pub fn loc(&self) -> Option<&Loc> {
766 self.0.loc()
767 }
768
769 pub fn effect(&self) -> Effect {
771 self.0.effect()
772 }
773
774 pub fn annotation(&self, key: &AnyId) -> Option<&Annotation> {
776 self.0.annotation(key)
777 }
778
779 pub fn annotations(&self) -> impl Iterator<Item = (&AnyId, &Annotation)> {
781 self.0.annotations()
782 }
783
784 pub fn principal_constraint(&self) -> &PrincipalConstraint {
786 self.0.principal_constraint()
787 }
788
789 pub fn principal_constraint_expr(&self) -> Expr {
793 self.0.principal_constraint_expr()
794 }
795
796 pub fn action_constraint(&self) -> &ActionConstraint {
798 self.0.action_constraint()
799 }
800
801 pub fn action_constraint_expr(&self) -> Expr {
805 self.0.action_constraint_expr()
806 }
807
808 pub fn resource_constraint(&self) -> &ResourceConstraint {
810 self.0.resource_constraint()
811 }
812
813 pub fn resource_constraint_expr(&self) -> Expr {
817 self.0.resource_constraint_expr()
818 }
819
820 pub fn non_scope_constraints(&self) -> Option<&Expr> {
825 self.0.non_scope_constraints()
826 }
827
828 pub fn condition(&self) -> Expr {
834 self.0.condition()
835 }
836
837 #[expect(
839 clippy::too_many_arguments,
840 reason = "policies just have this many components"
841 )]
842 pub fn new(
843 id: PolicyID,
844 loc: Option<Loc>,
845 annotations: Annotations,
846 effect: Effect,
847 principal_constraint: PrincipalConstraint,
848 action_constraint: ActionConstraint,
849 resource_constraint: ResourceConstraint,
850 non_scope_constraints: Option<Expr>,
851 ) -> Result<Self, UnexpectedSlotError> {
852 let body = TemplateBody::new(
853 id,
854 loc,
855 annotations,
856 effect,
857 principal_constraint,
858 action_constraint,
859 resource_constraint,
860 non_scope_constraints,
861 );
862 let first_slot = body.condition().slots().next();
863 match first_slot {
865 Some(slot) => Err(UnexpectedSlotError::FoundSlot(slot))?,
866 None => Ok(Self(body)),
867 }
868 }
869}
870
871impl TryFrom<Template> for StaticPolicy {
872 type Error = UnexpectedSlotError;
873
874 fn try_from(value: Template) -> Result<Self, Self::Error> {
875 let o = value.slots().next();
877 match o {
878 Some(slot_id) => Err(Self::Error::FoundSlot(slot_id.clone())),
879 None => Ok(Self(value.body)),
880 }
881 }
882}
883
884impl From<StaticPolicy> for Policy {
885 fn from(p: StaticPolicy) -> Policy {
886 let (_, policy) = Template::link_static_policy(p);
887 policy
888 }
889}
890
891impl From<StaticPolicy> for Arc<Template> {
892 fn from(p: StaticPolicy) -> Self {
893 let (t, _) = Template::link_static_policy(p);
894 t
895 }
896}
897
898#[derive(Educe, Clone, Debug)]
901#[educe(PartialEq, Eq, Hash)]
902pub struct TemplateBodyImpl {
903 id: PolicyID,
905 #[educe(PartialEq(ignore))]
907 #[educe(Hash(ignore))]
908 loc: Option<Loc>,
909 annotations: Arc<Annotations>,
913 effect: Effect,
915 principal_constraint: PrincipalConstraint,
919 action_constraint: ActionConstraint,
923 resource_constraint: ResourceConstraint,
927 non_scope_constraints: Option<Arc<Expr>>,
932}
933
934#[derive(Clone, Hash, Eq, PartialEq, Debug)]
937pub enum TemplateBody {
938 TemplateBody(TemplateBodyImpl),
940 #[cfg(feature = "tolerant-ast")]
941 TemplateBodyError(PolicyID, Option<Loc>),
943}
944
945impl TemplateBody {
946 pub fn id(&self) -> &PolicyID {
948 match self {
949 TemplateBody::TemplateBody(TemplateBodyImpl { id, .. }) => id,
950 #[cfg(feature = "tolerant-ast")]
951 TemplateBody::TemplateBodyError(id, _) => id,
952 }
953 }
954
955 pub fn loc(&self) -> Option<&Loc> {
957 match self {
958 TemplateBody::TemplateBody(TemplateBodyImpl { loc, .. }) => loc.as_ref(),
959 #[cfg(feature = "tolerant-ast")]
960 TemplateBody::TemplateBodyError(_, loc) => loc.as_ref(),
961 }
962 }
963
964 pub fn new_id(&self, id: PolicyID) -> Self {
966 match self {
967 TemplateBody::TemplateBody(t) => {
968 let mut new = t.clone();
969 new.id = id;
970 TemplateBody::TemplateBody(new)
971 }
972 #[cfg(feature = "tolerant-ast")]
973 TemplateBody::TemplateBodyError(_, loc) => {
974 TemplateBody::TemplateBodyError(id, loc.clone())
975 }
976 }
977 }
978
979 #[cfg(feature = "tolerant-ast")]
980 pub fn error(id: PolicyID, loc: Option<Loc>) -> Self {
982 TemplateBody::TemplateBodyError(id, loc)
983 }
984
985 pub fn effect(&self) -> Effect {
987 match self {
988 TemplateBody::TemplateBody(TemplateBodyImpl { effect, .. }) => *effect,
989 #[cfg(feature = "tolerant-ast")]
990 TemplateBody::TemplateBodyError(_, _) => Effect::Forbid,
991 }
992 }
993
994 pub fn annotation(&self, key: &AnyId) -> Option<&Annotation> {
996 match self {
997 TemplateBody::TemplateBody(TemplateBodyImpl { annotations, .. }) => {
998 annotations.get(key)
999 }
1000 #[cfg(feature = "tolerant-ast")]
1001 TemplateBody::TemplateBodyError(_, _) => None,
1002 }
1003 }
1004
1005 pub fn annotations_arc(&self) -> &Arc<Annotations> {
1007 match self {
1008 TemplateBody::TemplateBody(TemplateBodyImpl { annotations, .. }) => annotations,
1009 #[cfg(feature = "tolerant-ast")]
1010 TemplateBody::TemplateBodyError(_, _) => &DEFAULT_ANNOTATIONS,
1011 }
1012 }
1013
1014 pub fn annotations(&self) -> impl Iterator<Item = (&AnyId, &Annotation)> {
1016 match self {
1017 TemplateBody::TemplateBody(TemplateBodyImpl { annotations, .. }) => annotations.iter(),
1018 #[cfg(feature = "tolerant-ast")]
1019 TemplateBody::TemplateBodyError(_, _) => DEFAULT_ANNOTATIONS.iter(),
1020 }
1021 }
1022
1023 pub fn principal_constraint(&self) -> &PrincipalConstraint {
1025 match self {
1026 TemplateBody::TemplateBody(TemplateBodyImpl {
1027 principal_constraint,
1028 ..
1029 }) => principal_constraint,
1030 #[cfg(feature = "tolerant-ast")]
1031 TemplateBody::TemplateBodyError(_, _) => &DEFAULT_PRINCIPAL_CONSTRAINT,
1032 }
1033
1034 }
1036
1037 pub fn principal_constraint_expr(&self) -> Expr {
1041 match self {
1042 TemplateBody::TemplateBody(TemplateBodyImpl {
1043 principal_constraint,
1044 ..
1045 }) => principal_constraint.as_expr(),
1046 #[cfg(feature = "tolerant-ast")]
1047 TemplateBody::TemplateBodyError(_, _) => DEFAULT_PRINCIPAL_CONSTRAINT.as_expr(),
1048 }
1049 }
1050
1051 pub fn action_constraint(&self) -> &ActionConstraint {
1053 match self {
1054 TemplateBody::TemplateBody(TemplateBodyImpl {
1055 action_constraint, ..
1056 }) => action_constraint,
1057 #[cfg(feature = "tolerant-ast")]
1058 TemplateBody::TemplateBodyError(_, _) => &DEFAULT_ACTION_CONSTRAINT,
1059 }
1060 }
1061
1062 pub fn action_constraint_expr(&self) -> Expr {
1066 match self {
1067 TemplateBody::TemplateBody(TemplateBodyImpl {
1068 action_constraint, ..
1069 }) => action_constraint.as_expr(),
1070 #[cfg(feature = "tolerant-ast")]
1071 TemplateBody::TemplateBodyError(_, _) => DEFAULT_ACTION_CONSTRAINT.as_expr(),
1072 }
1073 }
1074
1075 pub fn resource_constraint(&self) -> &ResourceConstraint {
1077 match self {
1078 TemplateBody::TemplateBody(TemplateBodyImpl {
1079 resource_constraint,
1080 ..
1081 }) => resource_constraint,
1082 #[cfg(feature = "tolerant-ast")]
1083 TemplateBody::TemplateBodyError(_, _) => &DEFAULT_RESOURCE_CONSTRAINT,
1084 }
1085 }
1086
1087 pub fn resource_constraint_expr(&self) -> Expr {
1091 match self {
1092 TemplateBody::TemplateBody(TemplateBodyImpl {
1093 resource_constraint,
1094 ..
1095 }) => resource_constraint.as_expr(),
1096 #[cfg(feature = "tolerant-ast")]
1097 TemplateBody::TemplateBodyError(_, _) => DEFAULT_RESOURCE_CONSTRAINT.as_expr(),
1098 }
1099 }
1100
1101 pub fn non_scope_constraints(&self) -> Option<&Expr> {
1106 match self {
1107 TemplateBody::TemplateBody(TemplateBodyImpl {
1108 non_scope_constraints,
1109 ..
1110 }) => non_scope_constraints.as_ref().map(|e| e.as_ref()),
1111 #[cfg(feature = "tolerant-ast")]
1112 TemplateBody::TemplateBodyError(_, _) => Some(&DEFAULT_ERROR_EXPR),
1113 }
1114 }
1115
1116 pub fn non_scope_constraints_arc(&self) -> Option<&Arc<Expr>> {
1118 match self {
1119 TemplateBody::TemplateBody(TemplateBodyImpl {
1120 non_scope_constraints,
1121 ..
1122 }) => non_scope_constraints.as_ref(),
1123 #[cfg(feature = "tolerant-ast")]
1124 TemplateBody::TemplateBodyError(_, _) => Some(&DEFAULT_ERROR_EXPR),
1125 }
1126 }
1127
1128 #[expect(clippy::type_complexity, reason = "policies just have many components")]
1131 pub(crate) fn into_components_opt(
1132 self,
1133 ) -> Option<(
1134 PolicyID,
1135 Arc<Annotations>,
1136 Effect,
1137 PrincipalConstraint,
1138 ActionConstraint,
1139 ResourceConstraint,
1140 Option<Arc<Expr>>,
1141 )> {
1142 match self {
1143 TemplateBody::TemplateBody(TemplateBodyImpl {
1144 id,
1145 loc: _,
1146 annotations,
1147 effect,
1148 principal_constraint,
1149 action_constraint,
1150 resource_constraint,
1151 non_scope_constraints,
1152 }) => Some((
1153 id,
1154 annotations,
1155 effect,
1156 principal_constraint,
1157 action_constraint,
1158 resource_constraint,
1159 non_scope_constraints,
1160 )),
1161 #[cfg(feature = "tolerant-ast")]
1162 TemplateBody::TemplateBodyError(_, _) => None,
1163 }
1164 }
1165
1166 pub fn condition(&self) -> Expr {
1172 match self {
1173 TemplateBody::TemplateBody(TemplateBodyImpl { .. }) => {
1174 let loc = self.loc().cloned();
1175 Expr::and(
1176 self.principal_constraint_expr(),
1177 Expr::and(
1178 self.action_constraint_expr(),
1179 Expr::and(
1180 self.resource_constraint_expr(),
1181 self.non_scope_constraints()
1182 .cloned()
1183 .unwrap_or_else(|| Expr::val(true)),
1184 )
1185 .with_maybe_source_loc(loc.clone()),
1186 )
1187 .with_maybe_source_loc(loc.clone()),
1188 )
1189 .with_maybe_source_loc(loc)
1190 }
1191 #[cfg(feature = "tolerant-ast")]
1192 TemplateBody::TemplateBodyError(_, _) => DEFAULT_ERROR_EXPR.as_ref().clone(),
1193 }
1194 }
1195
1196 #[expect(
1198 clippy::too_many_arguments,
1199 reason = "policies just have this many components"
1200 )]
1201 pub fn new_shared(
1202 id: PolicyID,
1203 loc: Option<Loc>,
1204 annotations: Arc<Annotations>,
1205 effect: Effect,
1206 principal_constraint: PrincipalConstraint,
1207 action_constraint: ActionConstraint,
1208 resource_constraint: ResourceConstraint,
1209 non_scope_constraints: Option<Arc<Expr>>,
1210 ) -> Self {
1211 Self::TemplateBody(TemplateBodyImpl {
1212 id,
1213 loc,
1214 annotations,
1215 effect,
1216 principal_constraint,
1217 action_constraint,
1218 resource_constraint,
1219 non_scope_constraints,
1220 })
1221 }
1222
1223 #[expect(
1225 clippy::too_many_arguments,
1226 reason = "policies just have this many components"
1227 )]
1228 pub fn new(
1229 id: PolicyID,
1230 loc: Option<Loc>,
1231 annotations: Annotations,
1232 effect: Effect,
1233 principal_constraint: PrincipalConstraint,
1234 action_constraint: ActionConstraint,
1235 resource_constraint: ResourceConstraint,
1236 non_scope_constraints: Option<Expr>,
1237 ) -> Self {
1238 Self::TemplateBody(TemplateBodyImpl {
1239 id,
1240 loc,
1241 annotations: Arc::new(annotations),
1242 effect,
1243 principal_constraint,
1244 action_constraint,
1245 resource_constraint,
1246 non_scope_constraints: non_scope_constraints.map(Arc::new),
1247 })
1248 }
1249}
1250
1251impl From<StaticPolicy> for TemplateBody {
1252 fn from(p: StaticPolicy) -> Self {
1253 p.0
1254 }
1255}
1256
1257impl std::fmt::Display for TemplateBody {
1258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1259 match self {
1260 TemplateBody::TemplateBody(template_body_impl) => {
1261 template_body_impl.annotations.fmt(f)?;
1262 write!(
1263 f,
1264 "{}(\n {},\n {},\n {}\n)",
1265 self.effect(),
1266 self.principal_constraint(),
1267 self.action_constraint(),
1268 self.resource_constraint(),
1269 )?;
1270 if let Some(non_scope_constraints) = self.non_scope_constraints() {
1271 write!(f, " when {{\n {non_scope_constraints}\n}};")
1272 } else {
1273 write!(f, ";")
1274 }
1275 }
1276 #[cfg(feature = "tolerant-ast")]
1277 TemplateBody::TemplateBodyError(policy_id, _) => {
1278 write!(f, "TemplateBody::TemplateBodyError({policy_id})")
1279 }
1280 }
1281 }
1282}
1283
1284#[derive(Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
1286pub struct PrincipalConstraint {
1287 pub(crate) constraint: PrincipalOrResourceConstraint,
1288}
1289
1290impl PrincipalConstraint {
1291 pub fn new(constraint: PrincipalOrResourceConstraint) -> Self {
1293 PrincipalConstraint { constraint }
1294 }
1295
1296 pub fn as_inner(&self) -> &PrincipalOrResourceConstraint {
1298 &self.constraint
1299 }
1300
1301 pub fn into_inner(self) -> PrincipalOrResourceConstraint {
1303 self.constraint
1304 }
1305
1306 pub fn as_expr(&self) -> Expr {
1308 self.constraint.as_expr(PrincipalOrResource::Principal)
1309 }
1310
1311 pub fn any() -> Self {
1313 PrincipalConstraint {
1314 constraint: PrincipalOrResourceConstraint::any(),
1315 }
1316 }
1317
1318 pub fn is_eq(euid: Arc<EntityUID>) -> Self {
1320 PrincipalConstraint {
1321 constraint: PrincipalOrResourceConstraint::is_eq(euid),
1322 }
1323 }
1324
1325 pub fn is_eq_slot() -> Self {
1327 Self {
1328 constraint: PrincipalOrResourceConstraint::is_eq_slot(),
1329 }
1330 }
1331
1332 pub fn is_in(euid: Arc<EntityUID>) -> Self {
1334 PrincipalConstraint {
1335 constraint: PrincipalOrResourceConstraint::is_in(euid),
1336 }
1337 }
1338
1339 pub fn is_in_slot() -> Self {
1341 Self {
1342 constraint: PrincipalOrResourceConstraint::is_in_slot(),
1343 }
1344 }
1345
1346 pub fn is_entity_type_in_slot(entity_type: Arc<EntityType>) -> Self {
1348 Self {
1349 constraint: PrincipalOrResourceConstraint::is_entity_type_in_slot(entity_type),
1350 }
1351 }
1352
1353 pub fn is_entity_type_in(entity_type: Arc<EntityType>, in_entity: Arc<EntityUID>) -> Self {
1355 Self {
1356 constraint: PrincipalOrResourceConstraint::is_entity_type_in(entity_type, in_entity),
1357 }
1358 }
1359
1360 pub fn is_entity_type(entity_type: Arc<EntityType>) -> Self {
1362 Self {
1363 constraint: PrincipalOrResourceConstraint::is_entity_type(entity_type),
1364 }
1365 }
1366
1367 pub fn with_filled_slot(self, euid: Arc<EntityUID>) -> Self {
1369 match self.constraint {
1370 PrincipalOrResourceConstraint::Eq(EntityReference::Slot(_)) => Self {
1371 constraint: PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)),
1372 },
1373 PrincipalOrResourceConstraint::In(EntityReference::Slot(_)) => Self {
1374 constraint: PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)),
1375 },
1376 PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::Slot(_)) => Self {
1377 constraint: PrincipalOrResourceConstraint::IsIn(
1378 entity_type,
1379 EntityReference::EUID(euid),
1380 ),
1381 },
1382 PrincipalOrResourceConstraint::Is(_)
1384 | PrincipalOrResourceConstraint::Any
1385 | PrincipalOrResourceConstraint::Eq(EntityReference::EUID(_))
1386 | PrincipalOrResourceConstraint::In(EntityReference::EUID(_))
1387 | PrincipalOrResourceConstraint::IsIn(_, EntityReference::EUID(_)) => self,
1388 }
1389 }
1390}
1391
1392impl std::fmt::Display for PrincipalConstraint {
1393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1394 write!(
1395 f,
1396 "{}",
1397 self.constraint.display(PrincipalOrResource::Principal)
1398 )
1399 }
1400}
1401
1402#[derive(Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
1404pub struct ResourceConstraint {
1405 pub(crate) constraint: PrincipalOrResourceConstraint,
1406}
1407
1408impl ResourceConstraint {
1409 pub fn new(constraint: PrincipalOrResourceConstraint) -> Self {
1411 ResourceConstraint { constraint }
1412 }
1413
1414 pub fn as_inner(&self) -> &PrincipalOrResourceConstraint {
1416 &self.constraint
1417 }
1418
1419 pub fn into_inner(self) -> PrincipalOrResourceConstraint {
1421 self.constraint
1422 }
1423
1424 pub fn as_expr(&self) -> Expr {
1426 self.constraint.as_expr(PrincipalOrResource::Resource)
1427 }
1428
1429 pub fn any() -> Self {
1431 ResourceConstraint {
1432 constraint: PrincipalOrResourceConstraint::any(),
1433 }
1434 }
1435
1436 pub fn is_eq(euid: Arc<EntityUID>) -> Self {
1438 ResourceConstraint {
1439 constraint: PrincipalOrResourceConstraint::is_eq(euid),
1440 }
1441 }
1442
1443 pub fn is_eq_slot() -> Self {
1445 Self {
1446 constraint: PrincipalOrResourceConstraint::is_eq_slot(),
1447 }
1448 }
1449
1450 pub fn is_in_slot() -> Self {
1452 Self {
1453 constraint: PrincipalOrResourceConstraint::is_in_slot(),
1454 }
1455 }
1456
1457 pub fn is_in(euid: Arc<EntityUID>) -> Self {
1459 ResourceConstraint {
1460 constraint: PrincipalOrResourceConstraint::is_in(euid),
1461 }
1462 }
1463
1464 pub fn is_entity_type_in_slot(entity_type: Arc<EntityType>) -> Self {
1466 Self {
1467 constraint: PrincipalOrResourceConstraint::is_entity_type_in_slot(entity_type),
1468 }
1469 }
1470
1471 pub fn is_entity_type_in(entity_type: Arc<EntityType>, in_entity: Arc<EntityUID>) -> Self {
1473 Self {
1474 constraint: PrincipalOrResourceConstraint::is_entity_type_in(entity_type, in_entity),
1475 }
1476 }
1477
1478 pub fn is_entity_type(entity_type: Arc<EntityType>) -> Self {
1480 Self {
1481 constraint: PrincipalOrResourceConstraint::is_entity_type(entity_type),
1482 }
1483 }
1484
1485 pub fn with_filled_slot(self, euid: Arc<EntityUID>) -> Self {
1487 match self.constraint {
1488 PrincipalOrResourceConstraint::Eq(EntityReference::Slot(_)) => Self {
1489 constraint: PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)),
1490 },
1491 PrincipalOrResourceConstraint::In(EntityReference::Slot(_)) => Self {
1492 constraint: PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)),
1493 },
1494 PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::Slot(_)) => Self {
1495 constraint: PrincipalOrResourceConstraint::IsIn(
1496 entity_type,
1497 EntityReference::EUID(euid),
1498 ),
1499 },
1500 PrincipalOrResourceConstraint::Is(_)
1502 | PrincipalOrResourceConstraint::Any
1503 | PrincipalOrResourceConstraint::Eq(EntityReference::EUID(_))
1504 | PrincipalOrResourceConstraint::In(EntityReference::EUID(_))
1505 | PrincipalOrResourceConstraint::IsIn(_, EntityReference::EUID(_)) => self,
1506 }
1507 }
1508}
1509
1510impl std::fmt::Display for ResourceConstraint {
1511 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1512 write!(
1513 f,
1514 "{}",
1515 self.as_inner().display(PrincipalOrResource::Resource)
1516 )
1517 }
1518}
1519
1520#[derive(Educe, Clone, Debug, Eq)]
1522#[educe(Hash, PartialEq, PartialOrd, Ord)]
1523pub enum EntityReference {
1524 EUID(Arc<EntityUID>),
1526 Slot(
1528 #[educe(PartialEq(ignore))]
1529 #[educe(PartialOrd(ignore))]
1530 #[educe(Hash(ignore))]
1531 Option<Loc>,
1532 ),
1533}
1534
1535impl EntityReference {
1536 pub fn euid(euid: Arc<EntityUID>) -> Self {
1538 Self::EUID(euid)
1539 }
1540
1541 pub fn into_expr(&self, slot: SlotId) -> Expr {
1547 match self {
1548 EntityReference::EUID(euid) => Expr::val(euid.clone()),
1549 EntityReference::Slot(loc) => Expr::slot(slot).with_maybe_source_loc(loc.clone()),
1550 }
1551 }
1552}
1553
1554#[derive(Debug, Clone, PartialEq, Eq, Error)]
1556pub enum UnexpectedSlotError {
1557 #[error("found slot `{}` where slots are not allowed", .0.id)]
1559 FoundSlot(Slot),
1560}
1561
1562impl Diagnostic for UnexpectedSlotError {
1563 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
1564 match self {
1565 Self::FoundSlot(Slot { loc, .. }) => loc.as_ref().map(|loc| {
1566 let label = miette::LabeledSpan::underline(loc.span);
1567 Box::new(std::iter::once(label)) as _
1568 }),
1569 }
1570 }
1571
1572 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
1573 match self {
1574 Self::FoundSlot(Slot { loc, .. }) => loc.as_ref().map(|l| l as &dyn miette::SourceCode),
1575 }
1576 }
1577}
1578
1579impl From<EntityUID> for EntityReference {
1580 fn from(euid: EntityUID) -> Self {
1581 Self::EUID(Arc::new(euid))
1582 }
1583}
1584
1585#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
1587#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1588pub enum PrincipalOrResource {
1589 Principal,
1591 Resource,
1593}
1594
1595impl std::fmt::Display for PrincipalOrResource {
1596 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1597 let v = Var::from(*self);
1598 write!(f, "{v}")
1599 }
1600}
1601
1602impl TryFrom<Var> for PrincipalOrResource {
1603 type Error = Var;
1604
1605 fn try_from(value: Var) -> Result<Self, Self::Error> {
1606 match value {
1607 Var::Principal => Ok(Self::Principal),
1608 Var::Action => Err(Var::Action),
1609 Var::Resource => Ok(Self::Resource),
1610 Var::Context => Err(Var::Context),
1611 }
1612 }
1613}
1614
1615#[derive(Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
1618pub enum PrincipalOrResourceConstraint {
1619 Any,
1621 In(EntityReference),
1623 Eq(EntityReference),
1625 Is(Arc<EntityType>),
1627 IsIn(Arc<EntityType>, EntityReference),
1629}
1630
1631impl PrincipalOrResourceConstraint {
1632 pub fn any() -> Self {
1634 PrincipalOrResourceConstraint::Any
1635 }
1636
1637 pub fn is_eq(euid: Arc<EntityUID>) -> Self {
1639 PrincipalOrResourceConstraint::Eq(EntityReference::euid(euid))
1640 }
1641
1642 pub fn is_eq_slot() -> Self {
1644 PrincipalOrResourceConstraint::Eq(EntityReference::Slot(None))
1645 }
1646
1647 pub fn is_in_slot() -> Self {
1649 PrincipalOrResourceConstraint::In(EntityReference::Slot(None))
1650 }
1651
1652 pub fn is_in(euid: Arc<EntityUID>) -> Self {
1654 PrincipalOrResourceConstraint::In(EntityReference::euid(euid))
1655 }
1656
1657 pub fn is_entity_type_in_slot(entity_type: Arc<EntityType>) -> Self {
1659 PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::Slot(None))
1660 }
1661
1662 pub fn is_entity_type_in(entity_type: Arc<EntityType>, in_entity: Arc<EntityUID>) -> Self {
1664 PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::euid(in_entity))
1665 }
1666
1667 pub fn is_entity_type(entity_type: Arc<EntityType>) -> Self {
1669 PrincipalOrResourceConstraint::Is(entity_type)
1670 }
1671
1672 pub fn as_expr(&self, v: PrincipalOrResource) -> Expr {
1676 match self {
1677 PrincipalOrResourceConstraint::Any => Expr::val(true),
1678 PrincipalOrResourceConstraint::Eq(euid) => {
1679 Expr::is_eq(Expr::var(v.into()), euid.into_expr(v.into()))
1680 }
1681 PrincipalOrResourceConstraint::In(euid) => {
1682 Expr::is_in(Expr::var(v.into()), euid.into_expr(v.into()))
1683 }
1684 PrincipalOrResourceConstraint::IsIn(entity_type, euid) => Expr::and(
1685 Expr::is_entity_type(Expr::var(v.into()), entity_type.as_ref().clone()),
1686 Expr::is_in(Expr::var(v.into()), euid.into_expr(v.into())),
1687 ),
1688 PrincipalOrResourceConstraint::Is(entity_type) => {
1689 Expr::is_entity_type(Expr::var(v.into()), entity_type.as_ref().clone())
1690 }
1691 }
1692 }
1693
1694 pub fn display(&self, v: PrincipalOrResource) -> String {
1698 match self {
1699 PrincipalOrResourceConstraint::In(euid) => {
1700 format!("{} in {}", v, euid.into_expr(v.into()))
1701 }
1702 PrincipalOrResourceConstraint::Eq(euid) => {
1703 format!("{} == {}", v, euid.into_expr(v.into()))
1704 }
1705 PrincipalOrResourceConstraint::IsIn(entity_type, euid) => {
1706 format!("{} is {} in {}", v, entity_type, euid.into_expr(v.into()))
1707 }
1708 PrincipalOrResourceConstraint::Is(entity_type) => {
1709 format!("{v} is {entity_type}")
1710 }
1711 PrincipalOrResourceConstraint::Any => format!("{v}"),
1712 }
1713 }
1714
1715 pub fn get_euid(&self) -> Option<&Arc<EntityUID>> {
1717 match self {
1718 PrincipalOrResourceConstraint::Any => None,
1719 PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)) => Some(euid),
1720 PrincipalOrResourceConstraint::In(EntityReference::Slot(_)) => None,
1721 PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)) => Some(euid),
1722 PrincipalOrResourceConstraint::Eq(EntityReference::Slot(_)) => None,
1723 PrincipalOrResourceConstraint::IsIn(_, EntityReference::EUID(euid)) => Some(euid),
1724 PrincipalOrResourceConstraint::IsIn(_, EntityReference::Slot(_)) => None,
1725 PrincipalOrResourceConstraint::Is(_) => None,
1726 }
1727 }
1728
1729 pub fn iter_entity_type_names(&self) -> impl Iterator<Item = &'_ EntityType> {
1731 self.get_euid()
1732 .into_iter()
1733 .map(|euid| euid.entity_type())
1734 .chain(match self {
1735 PrincipalOrResourceConstraint::Is(entity_type)
1736 | PrincipalOrResourceConstraint::IsIn(entity_type, _) => Some(entity_type.as_ref()),
1737 _ => None,
1738 })
1739 }
1740}
1741
1742#[derive(Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
1745pub enum ActionConstraint {
1746 Any,
1748 In(Vec<Arc<EntityUID>>),
1750 Eq(Arc<EntityUID>),
1752 #[cfg(feature = "tolerant-ast")]
1753 ErrorConstraint,
1755}
1756
1757impl std::fmt::Display for ActionConstraint {
1758 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1759 let render_euids =
1760 |euids: &Vec<Arc<EntityUID>>| euids.iter().map(|euid| format!("{euid}")).join(",");
1761 match self {
1762 ActionConstraint::Any => write!(f, "action"),
1763 ActionConstraint::In(euids) => {
1764 write!(f, "action in [{}]", render_euids(euids))
1765 }
1766 ActionConstraint::Eq(euid) => write!(f, "action == {euid}"),
1767 #[cfg(feature = "tolerant-ast")]
1768 ActionConstraint::ErrorConstraint => write!(f, "<invalid_action_constraint>"),
1769 }
1770 }
1771}
1772
1773impl ActionConstraint {
1774 pub fn any() -> Self {
1776 ActionConstraint::Any
1777 }
1778
1779 pub fn is_in(euids: impl IntoIterator<Item = EntityUID>) -> Self {
1781 ActionConstraint::In(euids.into_iter().map(Arc::new).collect())
1782 }
1783
1784 pub fn is_eq(euid: EntityUID) -> Self {
1786 ActionConstraint::Eq(Arc::new(euid))
1787 }
1788
1789 fn euids_into_expr(euids: impl IntoIterator<Item = Arc<EntityUID>>) -> Expr {
1790 Expr::set(euids.into_iter().map(Expr::val))
1791 }
1792
1793 pub fn as_expr(&self) -> Expr {
1795 match self {
1796 ActionConstraint::Any => Expr::val(true),
1797 ActionConstraint::In(euids) => Expr::is_in(
1798 Expr::var(Var::Action),
1799 ActionConstraint::euids_into_expr(euids.iter().cloned()),
1800 ),
1801 ActionConstraint::Eq(euid) => {
1802 Expr::is_eq(Expr::var(Var::Action), Expr::val(euid.clone()))
1803 }
1804 #[cfg(feature = "tolerant-ast")]
1805 ActionConstraint::ErrorConstraint => Expr::new(
1806 ExprKind::Error {
1807 error_kind: AstExprErrorKind::InvalidExpr(
1808 "Invalid action constraint".to_string(),
1809 ),
1810 },
1811 None,
1812 (),
1813 ),
1814 }
1815 }
1816
1817 pub fn iter_euids(&self) -> impl Iterator<Item = &'_ EntityUID> {
1819 match self {
1820 ActionConstraint::Any => EntityIterator::None,
1821 ActionConstraint::In(euids) => {
1822 EntityIterator::Bunch(euids.iter().map(Arc::as_ref).collect())
1823 }
1824 ActionConstraint::Eq(euid) => EntityIterator::One(euid),
1825 #[cfg(feature = "tolerant-ast")]
1826 ActionConstraint::ErrorConstraint => EntityIterator::None,
1827 }
1828 }
1829
1830 pub fn iter_entity_type_names(&self) -> impl Iterator<Item = &'_ EntityType> {
1832 self.iter_euids().map(|euid| euid.entity_type())
1833 }
1834
1835 pub fn check_action_types(&self) -> Result<(), NonEmpty<Arc<EntityUID>>> {
1840 match self {
1841 ActionConstraint::Any => Ok(()),
1842 ActionConstraint::In(euids) => {
1843 if let Some(euids) =
1844 NonEmpty::collect(euids.iter().filter(|euid| !euid.is_action()).cloned())
1845 {
1846 Err(euids)
1847 } else {
1848 Ok(())
1849 }
1850 }
1851 ActionConstraint::Eq(euid) => {
1852 if euid.is_action() {
1853 Ok(())
1854 } else {
1855 Err(nonempty![euid.clone()])
1856 }
1857 }
1858 #[cfg(feature = "tolerant-ast")]
1859 ActionConstraint::ErrorConstraint => Ok(()),
1860 }
1861 }
1862
1863 pub fn contains_only_action_types(self) -> Result<Self, NonEmpty<Arc<EntityUID>>> {
1867 self.check_action_types()?;
1868 Ok(self)
1869 }
1870}
1871
1872impl std::fmt::Display for StaticPolicy {
1873 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1874 let policy_template = &self.0;
1875 match policy_template {
1876 TemplateBody::TemplateBody(template_body_impl) => {
1877 for (k, v) in template_body_impl.annotations.iter() {
1878 writeln!(f, "@{}(\"{}\")", k, v.val.escape_debug())?
1879 }
1880 write!(
1881 f,
1882 "{}(\n {},\n {},\n {}\n)",
1883 self.effect(),
1884 self.principal_constraint(),
1885 self.action_constraint(),
1886 self.resource_constraint(),
1887 )?;
1888 if let Some(non_scope_constraints) = self.non_scope_constraints() {
1889 write!(f, " when {{\n {non_scope_constraints}\n}};")
1890 } else {
1891 write!(f, ";")
1892 }
1893 }
1894 #[cfg(feature = "tolerant-ast")]
1895 TemplateBody::TemplateBodyError(policy_id, _) => {
1896 write!(f, "TemplateBody::TemplateBodyError({policy_id})")
1897 }
1898 }
1899 }
1900}
1901
1902#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
1904pub struct PolicyID(SmolStr);
1905
1906impl PolicyID {
1907 pub fn from_string(id: impl AsRef<str>) -> Self {
1909 Self(SmolStr::from(id.as_ref()))
1910 }
1911
1912 pub fn from_smolstr(id: SmolStr) -> Self {
1914 Self(id)
1915 }
1916
1917 pub fn into_smolstr(self) -> SmolStr {
1919 self.0
1920 }
1921}
1922
1923impl std::fmt::Display for PolicyID {
1924 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1925 write!(f, "{}", self.0.escape_debug())
1926 }
1927}
1928
1929impl AsRef<str> for PolicyID {
1930 fn as_ref(&self) -> &str {
1931 &self.0
1932 }
1933}
1934
1935#[cfg(feature = "arbitrary")]
1936impl arbitrary::Arbitrary<'_> for PolicyID {
1937 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<PolicyID> {
1938 let s: String = u.arbitrary()?;
1939 Ok(PolicyID::from_string(s))
1940 }
1941 fn size_hint(depth: usize) -> (usize, Option<usize>) {
1942 <String as arbitrary::Arbitrary>::size_hint(depth)
1943 }
1944}
1945
1946#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
1948#[serde(rename_all = "camelCase")]
1949#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1950#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1951#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1952pub enum Effect {
1953 Permit,
1955 Forbid,
1957}
1958
1959impl std::fmt::Display for Effect {
1960 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1961 match self {
1962 Self::Permit => write!(f, "permit"),
1963 Self::Forbid => write!(f, "forbid"),
1964 }
1965 }
1966}
1967
1968enum EntityIterator<'a> {
1969 None,
1970 One(&'a EntityUID),
1971 Bunch(Vec<&'a EntityUID>),
1972}
1973
1974impl<'a> Iterator for EntityIterator<'a> {
1975 type Item = &'a EntityUID;
1976
1977 fn next(&mut self) -> Option<Self::Item> {
1978 match self {
1979 EntityIterator::None => None,
1980 EntityIterator::One(euid) => {
1981 let eptr = *euid;
1982 let mut ptr = EntityIterator::None;
1983 std::mem::swap(self, &mut ptr);
1984 Some(eptr)
1985 }
1986 EntityIterator::Bunch(v) => v.pop(),
1987 }
1988 }
1989}
1990
1991#[cfg(test)]
1992pub(crate) mod test_generators {
1993 use super::*;
1994
1995 pub fn all_por_constraints() -> impl Iterator<Item = PrincipalOrResourceConstraint> {
1996 let euid = Arc::new(EntityUID::with_eid("test"));
1997 let v = vec![
1998 PrincipalOrResourceConstraint::any(),
1999 PrincipalOrResourceConstraint::is_eq(euid.clone()),
2000 PrincipalOrResourceConstraint::Eq(EntityReference::Slot(None)),
2001 PrincipalOrResourceConstraint::is_in(euid),
2002 PrincipalOrResourceConstraint::In(EntityReference::Slot(None)),
2003 ];
2004
2005 v.into_iter()
2006 }
2007
2008 pub fn all_principal_constraints() -> impl Iterator<Item = PrincipalConstraint> {
2009 all_por_constraints().map(|constraint| PrincipalConstraint { constraint })
2010 }
2011
2012 pub fn all_resource_constraints() -> impl Iterator<Item = ResourceConstraint> {
2013 all_por_constraints().map(|constraint| ResourceConstraint { constraint })
2014 }
2015
2016 pub fn all_actions_constraints() -> impl Iterator<Item = ActionConstraint> {
2017 let euid: EntityUID = "Action::\"test\""
2018 .parse()
2019 .expect("Invalid action constraint euid");
2020 let v = vec![
2021 ActionConstraint::any(),
2022 ActionConstraint::is_eq(euid.clone()),
2023 ActionConstraint::is_in([euid.clone()]),
2024 ActionConstraint::is_in([euid.clone(), euid]),
2025 ];
2026
2027 v.into_iter()
2028 }
2029
2030 pub fn all_templates() -> impl Iterator<Item = Template> {
2031 let mut buf = vec![];
2032 let permit = PolicyID::from_string("permit");
2033 let forbid = PolicyID::from_string("forbid");
2034 for principal in all_principal_constraints() {
2035 for action in all_actions_constraints() {
2036 for resource in all_resource_constraints() {
2037 let permit = Template::new(
2038 permit.clone(),
2039 None,
2040 Annotations::new(),
2041 Effect::Permit,
2042 principal.clone(),
2043 action.clone(),
2044 resource.clone(),
2045 None,
2046 );
2047 let forbid = Template::new(
2048 forbid.clone(),
2049 None,
2050 Annotations::new(),
2051 Effect::Forbid,
2052 principal.clone(),
2053 action.clone(),
2054 resource.clone(),
2055 None,
2056 );
2057 buf.push(permit);
2058 buf.push(forbid);
2059 }
2060 }
2061 }
2062 buf.into_iter()
2063 }
2064}
2065
2066#[cfg(test)]
2067mod test {
2068 use cool_asserts::assert_matches;
2069 use std::collections::HashSet;
2070
2071 use super::{test_generators::*, *};
2072 use crate::{
2073 parser::{
2074 parse_policy,
2075 test_utils::{expect_exactly_one_error, expect_some_error_matches},
2076 },
2077 test_utils::ExpectedErrorMessageBuilder,
2078 };
2079
2080 #[test]
2081 fn link_templates() {
2082 for template in all_templates() {
2083 template.check_invariant();
2084 let t = Arc::new(template);
2085 let env = t
2086 .slots()
2087 .map(|slot| (slot.id, EntityUID::with_eid("eid")))
2088 .collect();
2089 let _ = Template::link(t, PolicyID::from_string("id"), env).expect("Linking failed");
2090 }
2091 }
2092
2093 #[test]
2094 fn test_template_rebuild() {
2095 for template in all_templates() {
2096 let id = template.id().clone();
2097 let effect = template.effect();
2098 let p = template.principal_constraint().clone();
2099 let a = template.action_constraint().clone();
2100 let r = template.resource_constraint().clone();
2101 let non_scope = template.non_scope_constraints().cloned();
2102 let t2 = Template::new(id, None, Annotations::new(), effect, p, a, r, non_scope);
2103 assert_eq!(template, t2);
2104 }
2105 }
2106
2107 #[test]
2108 fn test_static_policy_rebuild() {
2109 for template in all_templates() {
2110 if let Ok(ip) = StaticPolicy::try_from(template.clone()) {
2111 let id = ip.id().clone();
2112 let e = ip.effect();
2113 let anno = ip
2114 .annotations()
2115 .map(|(k, v)| (k.clone(), v.clone()))
2116 .collect();
2117 let p = ip.principal_constraint().clone();
2118 let a = ip.action_constraint().clone();
2119 let r = ip.resource_constraint().clone();
2120 let non_scope = ip.non_scope_constraints();
2121 let ip2 = StaticPolicy::new(id, None, anno, e, p, a, r, non_scope.cloned())
2122 .expect("Policy Creation Failed");
2123 assert_eq!(ip, ip2);
2124 let (t2, inst) = Template::link_static_policy(ip2);
2125 assert!(inst.is_static());
2126 assert_eq!(&template, t2.as_ref());
2127 }
2128 }
2129 }
2130
2131 #[test]
2132 fn ir_binding_too_many() {
2133 let tid = PolicyID::from_string("tid");
2134 let iid = PolicyID::from_string("iid");
2135 let t = Arc::new(Template::new(
2136 tid,
2137 None,
2138 Annotations::new(),
2139 Effect::Forbid,
2140 PrincipalConstraint::is_eq_slot(),
2141 ActionConstraint::Any,
2142 ResourceConstraint::any(),
2143 None,
2144 ));
2145 let mut m = HashMap::new();
2146 m.insert(SlotId::resource(), EntityUID::with_eid("eid"));
2147 assert_matches!(Template::link(t, iid, m), Err(LinkingError::ArityError { unbound_values, extra_values }) => {
2148 assert_eq!(unbound_values, vec![SlotId::principal()]);
2149 assert_eq!(extra_values, vec![SlotId::resource()]);
2150 });
2151 }
2152
2153 #[test]
2154 fn ir_binding_too_few() {
2155 let tid = PolicyID::from_string("tid");
2156 let iid = PolicyID::from_string("iid");
2157 let t = Arc::new(Template::new(
2158 tid,
2159 None,
2160 Annotations::new(),
2161 Effect::Forbid,
2162 PrincipalConstraint::is_eq_slot(),
2163 ActionConstraint::Any,
2164 ResourceConstraint::is_in_slot(),
2165 None,
2166 ));
2167 assert_matches!(Template::link(t.clone(), iid.clone(), HashMap::new()), Err(LinkingError::ArityError { unbound_values, extra_values }) => {
2168 assert_eq!(unbound_values, vec![SlotId::resource(), SlotId::principal()]);
2169 assert_eq!(extra_values, vec![]);
2170 });
2171 let mut m = HashMap::new();
2172 m.insert(SlotId::principal(), EntityUID::with_eid("eid"));
2173 assert_matches!(Template::link(t, iid, m), Err(LinkingError::ArityError { unbound_values, extra_values }) => {
2174 assert_eq!(unbound_values, vec![SlotId::resource()]);
2175 assert_eq!(extra_values, vec![]);
2176 });
2177 }
2178
2179 #[test]
2180 fn ir_binding() {
2181 let tid = PolicyID::from_string("template");
2182 let iid = PolicyID::from_string("linked");
2183 let t = Arc::new(Template::new(
2184 tid,
2185 None,
2186 Annotations::new(),
2187 Effect::Permit,
2188 PrincipalConstraint::is_in_slot(),
2189 ActionConstraint::any(),
2190 ResourceConstraint::is_eq_slot(),
2191 None,
2192 ));
2193
2194 let mut m = HashMap::new();
2195 m.insert(SlotId::principal(), EntityUID::with_eid("theprincipal"));
2196 m.insert(SlotId::resource(), EntityUID::with_eid("theresource"));
2197
2198 let r = Template::link(t, iid.clone(), m).expect("Should Succeed");
2199 assert_eq!(r.id(), &iid);
2200 assert_eq!(
2201 r.env().get(&SlotId::principal()),
2202 Some(&EntityUID::with_eid("theprincipal"))
2203 );
2204 assert_eq!(
2205 r.env().get(&SlotId::resource()),
2206 Some(&EntityUID::with_eid("theresource"))
2207 );
2208 }
2209
2210 #[test]
2211 fn isnt_template_implies_from_succeeds() {
2212 for template in all_templates() {
2213 if template.slots().count() == 0 {
2214 StaticPolicy::try_from(template).expect("Should succeed");
2215 }
2216 }
2217 }
2218
2219 #[test]
2220 fn is_template_implies_from_fails() {
2221 for template in all_templates() {
2222 if template.slots().count() != 0 {
2223 assert!(
2224 StaticPolicy::try_from(template.clone()).is_err(),
2225 "Following template did convert {template}"
2226 );
2227 }
2228 }
2229 }
2230
2231 #[test]
2232 fn non_template_iso() {
2233 for template in all_templates() {
2234 if let Ok(p) = StaticPolicy::try_from(template.clone()) {
2235 let (t2, _) = Template::link_static_policy(p);
2236 assert_eq!(&template, t2.as_ref());
2237 }
2238 }
2239 }
2240
2241 #[test]
2242 fn template_into_expr() {
2243 for template in all_templates() {
2244 if let Ok(p) = StaticPolicy::try_from(template.clone()) {
2245 let t: Template = template;
2246 assert_eq!(p.condition(), t.condition());
2247 assert_eq!(p.effect(), t.effect());
2248 }
2249 }
2250 }
2251
2252 #[test]
2253 fn template_por_iter() {
2254 let e = Arc::new(EntityUID::with_eid("eid"));
2255 assert_eq!(PrincipalOrResourceConstraint::Any.get_euid(), None);
2256 assert_eq!(
2257 PrincipalOrResourceConstraint::In(EntityReference::EUID(e.clone())).get_euid(),
2258 Some(&e)
2259 );
2260 assert_eq!(
2261 PrincipalOrResourceConstraint::In(EntityReference::Slot(None)).get_euid(),
2262 None
2263 );
2264 assert_eq!(
2265 PrincipalOrResourceConstraint::Eq(EntityReference::EUID(e.clone())).get_euid(),
2266 Some(&e)
2267 );
2268 assert_eq!(
2269 PrincipalOrResourceConstraint::Eq(EntityReference::Slot(None)).get_euid(),
2270 None
2271 );
2272 assert_eq!(
2273 PrincipalOrResourceConstraint::IsIn(
2274 Arc::new("T".parse().unwrap()),
2275 EntityReference::EUID(e.clone())
2276 )
2277 .get_euid(),
2278 Some(&e)
2279 );
2280 assert_eq!(
2281 PrincipalOrResourceConstraint::Is(Arc::new("T".parse().unwrap())).get_euid(),
2282 None
2283 );
2284 assert_eq!(
2285 PrincipalOrResourceConstraint::IsIn(
2286 Arc::new("T".parse().unwrap()),
2287 EntityReference::Slot(None)
2288 )
2289 .get_euid(),
2290 None
2291 );
2292 }
2293
2294 #[test]
2295 fn action_iter() {
2296 assert_eq!(ActionConstraint::Any.iter_euids().count(), 0);
2297 let a = ActionConstraint::Eq(Arc::new(EntityUID::with_eid("test")));
2298 let v = a.iter_euids().collect::<Vec<_>>();
2299 assert_eq!(vec![&EntityUID::with_eid("test")], v);
2300 let a =
2301 ActionConstraint::is_in([EntityUID::with_eid("test1"), EntityUID::with_eid("test2")]);
2302 let set = a.iter_euids().collect::<HashSet<_>>();
2303 let e1 = EntityUID::with_eid("test1");
2304 let e2 = EntityUID::with_eid("test2");
2305 let correct = vec![&e1, &e2].into_iter().collect::<HashSet<_>>();
2306 assert_eq!(set, correct);
2307 }
2308
2309 #[test]
2310 fn test_iter_none() {
2311 let mut i = EntityIterator::None;
2312 assert_eq!(i.next(), None);
2313 }
2314
2315 #[test]
2316 fn test_iter_once() {
2317 let id = EntityUID::from_components(
2318 name::Name::parse_unqualified_name("s").unwrap().into(),
2319 entity::Eid::new("eid"),
2320 None,
2321 );
2322 let mut i = EntityIterator::One(&id);
2323 assert_eq!(i.next(), Some(&id));
2324 assert_eq!(i.next(), None);
2325 }
2326
2327 #[test]
2328 fn test_iter_mult() {
2329 let id1 = EntityUID::from_components(
2330 name::Name::parse_unqualified_name("s").unwrap().into(),
2331 entity::Eid::new("eid1"),
2332 None,
2333 );
2334 let id2 = EntityUID::from_components(
2335 name::Name::parse_unqualified_name("s").unwrap().into(),
2336 entity::Eid::new("eid2"),
2337 None,
2338 );
2339 let v = vec![&id1, &id2];
2340 let mut i = EntityIterator::Bunch(v);
2341 assert_eq!(i.next(), Some(&id2));
2342 assert_eq!(i.next(), Some(&id1));
2343 assert_eq!(i.next(), None)
2344 }
2345
2346 #[test]
2347 fn euid_into_expr() {
2348 let e = EntityReference::Slot(None);
2349 assert_eq!(
2350 e.into_expr(SlotId::principal()),
2351 Expr::slot(SlotId::principal())
2352 );
2353 let e = EntityReference::euid(Arc::new(EntityUID::with_eid("eid")));
2354 assert_eq!(
2355 e.into_expr(SlotId::principal()),
2356 Expr::val(EntityUID::with_eid("eid"))
2357 );
2358 }
2359
2360 #[test]
2361 fn por_constraint_display() {
2362 let t = PrincipalOrResourceConstraint::Eq(EntityReference::Slot(None));
2363 let s = t.display(PrincipalOrResource::Principal);
2364 assert_eq!(s, "principal == ?principal");
2365 let t = PrincipalOrResourceConstraint::Eq(EntityReference::euid(Arc::new(
2366 EntityUID::with_eid("test"),
2367 )));
2368 let s = t.display(PrincipalOrResource::Principal);
2369 assert_eq!(s, "principal == test_entity_type::\"test\"");
2370 }
2371
2372 #[test]
2373 fn unexpected_templates() {
2374 let policy_str = r#"permit(principal == ?principal, action, resource);"#;
2375 assert_matches!(parse_policy(Some(PolicyID::from_string("id")), policy_str), Err(e) => {
2376 expect_exactly_one_error(policy_str, &e, &ExpectedErrorMessageBuilder::error(
2377 "expected a static policy, got a template containing the slot ?principal"
2378 )
2379 .help("try removing the template slot(s) from this policy")
2380 .exactly_one_underline("?principal")
2381 .build()
2382 );
2383 });
2384
2385 let policy_str =
2386 r#"permit(principal == ?principal, action, resource) when { ?principal == 3 } ;"#;
2387 assert_matches!(parse_policy(Some(PolicyID::from_string("id")), policy_str), Err(e) => {
2388 expect_some_error_matches(policy_str, &e, &ExpectedErrorMessageBuilder::error(
2389 "expected a static policy, got a template containing the slot ?principal"
2390 )
2391 .help("try removing the template slot(s) from this policy")
2392 .exactly_one_underline("?principal")
2393 .build()
2394 );
2395 assert_eq!(e.len(), 2);
2396 });
2397 }
2398
2399 #[test]
2400 fn policy_to_expr() {
2401 let policy_str = r#"permit(principal is A, action, resource is B)
2402 when { 1 == 2 }
2403 unless { 2 == 1}
2404 when { 3 == 4}
2405 unless { 4 == 3};"#;
2406 assert_matches!(parse_policy(Some(PolicyID::from_string("id")), policy_str), Ok(p) => {
2407 assert_eq!(ToString::to_string(&p.condition()), "(principal is A) && (true && ((resource is B) && ((1 == 2) && ((!(2 == 1)) && ((3 == 4) && (!(4 == 3)))))))");
2408 });
2409 }
2410
2411 #[cfg(feature = "tolerant-ast")]
2412 #[test]
2413 fn template_body_error_methods() {
2414 use std::str::FromStr;
2415
2416 let policy_id = PolicyID::from_string("error_policy");
2417 let error_loc = Some(Loc::new(0..1, "ASTErrorNode".into()));
2418 let error_body = TemplateBody::TemplateBodyError(policy_id.clone(), error_loc.clone());
2419
2420 let expected_error = <ExprWithErrsBuilder as ExprBuilder>::new()
2421 .error(ParseErrors::singleton(ToASTError::new(
2422 ToASTErrorKind::ASTErrorNode,
2423 Some(Loc::new(0..1, "ASTErrorNode".into())),
2424 )))
2425 .unwrap();
2426
2427 assert_eq!(error_body.id(), &policy_id);
2429
2430 assert_eq!(error_body.loc(), error_loc.as_ref());
2432
2433 let new_policy_id = PolicyID::from_string("new_error_policy");
2435 let updated_error_body = error_body.new_id(new_policy_id.clone());
2436 assert_matches!(updated_error_body,
2437 TemplateBody::TemplateBodyError(id, loc) if id == new_policy_id && loc == error_loc
2438 );
2439
2440 assert_eq!(error_body.effect(), Effect::Forbid);
2442
2443 assert_eq!(
2445 error_body.annotation(&AnyId::from_str("test").unwrap()),
2446 None
2447 );
2448
2449 assert!(error_body.annotations().count() == 0);
2451
2452 assert_eq!(
2454 *error_body.principal_constraint(),
2455 PrincipalConstraint::any()
2456 );
2457
2458 assert_eq!(*error_body.action_constraint(), ActionConstraint::any());
2460
2461 assert_eq!(*error_body.resource_constraint(), ResourceConstraint::any());
2463
2464 assert_eq!(error_body.non_scope_constraints(), Some(&expected_error));
2466
2467 assert_eq!(error_body.condition(), expected_error);
2469
2470 let display_str = format!("{error_body}");
2472 assert!(display_str.contains("TemplateBodyError"));
2473 assert!(display_str.contains("error_policy"));
2474 }
2475
2476 #[cfg(feature = "tolerant-ast")]
2477 #[test]
2478 fn template_error_methods() {
2479 let policy_id = PolicyID::from_string("error_policy");
2480 let error_loc = Some(Loc::new(0..1, "ASTErrorNode".into()));
2481 let error_template = Template::error(policy_id.clone(), error_loc.clone());
2482
2483 assert_eq!(error_template.id(), &policy_id);
2485
2486 assert!(error_template.slots().count() == 0);
2488
2489 assert_matches!(error_template.body,
2491 TemplateBody::TemplateBodyError(ref id, ref loc) if id == &policy_id && loc == &error_loc
2492 );
2493
2494 assert_eq!(
2496 error_template.principal_constraint(),
2497 &PrincipalConstraint::any()
2498 );
2499
2500 assert_eq!(*error_template.action_constraint(), ActionConstraint::any());
2502
2503 assert_eq!(
2505 *error_template.resource_constraint(),
2506 ResourceConstraint::any()
2507 );
2508
2509 assert_eq!(error_template.effect(), Effect::Forbid);
2511
2512 assert_eq!(
2514 error_template.condition(),
2515 DEFAULT_ERROR_EXPR.as_ref().clone()
2516 );
2517
2518 assert_eq!(error_template.loc(), error_loc.as_ref());
2520
2521 assert!(error_template.annotations().count() == 0);
2523
2524 let display_str = format!("{error_template}");
2526 assert!(display_str.contains("TemplateBody::TemplateBodyError"));
2527 assert!(display_str.contains(&policy_id.to_string()));
2528 }
2529}
2530
2531#[cfg(test)]
2532mod template_validate_test {
2533 use super::*;
2534 use cool_asserts::assert_matches;
2535
2536 fn make_template(
2537 action_constraint: ActionConstraint,
2538 non_scope_constraint: Option<Expr>,
2539 ) -> Template {
2540 Template::new(
2541 PolicyID::from_string("test"),
2542 None,
2543 Annotations::new(),
2544 Effect::Permit,
2545 PrincipalConstraint::any(),
2546 action_constraint,
2547 ResourceConstraint::any(),
2548 non_scope_constraint,
2549 )
2550 }
2551
2552 fn action_euid() -> Arc<EntityUID> {
2553 Arc::new(EntityUID::with_eid_and_type("Action", "view").unwrap())
2554 }
2555
2556 fn non_action_euid() -> Arc<EntityUID> {
2557 Arc::new(EntityUID::with_eid_and_type("User", "alice").unwrap())
2558 }
2559
2560 #[test]
2561 fn valid_template_accepted() {
2562 let t = make_template(ActionConstraint::Eq(action_euid()), None);
2563 assert!(t.try_validate().is_ok());
2564 }
2565
2566 #[test]
2567 fn invalid_action_eq_rejected() {
2568 let t = make_template(ActionConstraint::Eq(non_action_euid()), None);
2569 assert_matches!(
2570 t.try_validate(),
2571 Err(TemplateValidationError::InvalidActionType(_))
2572 );
2573 }
2574
2575 #[test]
2576 fn invalid_action_in_rejected() {
2577 let t = make_template(
2578 ActionConstraint::In(vec![action_euid(), non_action_euid()]),
2579 None,
2580 );
2581 assert_matches!(
2582 t.try_validate(),
2583 Err(TemplateValidationError::InvalidActionType(_))
2584 );
2585 }
2586
2587 #[test]
2588 fn slot_in_condition_rejected() {
2589 let t = make_template(
2590 ActionConstraint::Eq(action_euid()),
2591 Some(Expr::slot(SlotId::principal())),
2592 );
2593 assert_matches!(
2594 t.try_validate(),
2595 Err(TemplateValidationError::SlotsInConditionClause(_))
2596 );
2597 }
2598
2599 #[test]
2600 fn slot_cache_mismatch_rejected() {
2601 let body = TemplateBody::new(
2603 PolicyID::from_string("test"),
2604 None,
2605 Annotations::new(),
2606 Effect::Permit,
2607 PrincipalConstraint::any(),
2608 ActionConstraint::Eq(action_euid()),
2609 ResourceConstraint::any(),
2610 None,
2611 );
2612 let t = Template {
2614 body,
2615 slots: vec![Slot {
2616 id: SlotId::principal(),
2617 loc: None,
2618 }],
2619 };
2620 assert_matches!(
2621 t.try_validate(),
2622 Err(TemplateValidationError::SlotCacheMismatch)
2623 );
2624 }
2625}