1use super::err::{parse_errors, ParseError, ParseErrors, ToASTError, ToASTErrorKind};
37use super::node::Node;
38use super::unescape::{to_pattern, to_unescaped_string};
39use super::util::{flatten_tuple_2, flatten_tuple_3, flatten_tuple_4};
40use super::{cst, Loc};
41#[cfg(feature = "tolerant-ast")]
42use crate::ast::expr_allows_errors::ExprWithErrsBuilder;
43use crate::ast::{
44 self, ActionConstraint, Integer, PatternElem, PolicySetError, PrincipalConstraint,
45 PrincipalOrResourceConstraint, ResourceConstraint, UnreservedId, UnwrapInfallible,
46};
47use crate::expr_builder::{ExprBuilder, ExprBuilderInfallibleBuild};
48use crate::extensions::ExtStyles;
49use itertools::{Either, Itertools};
50use nonempty::nonempty;
51use nonempty::NonEmpty;
52use smol_str::{format_smolstr, SmolStr, ToSmolStr};
53use std::cmp::Ordering;
54use std::collections::BTreeMap;
55use std::mem;
56use std::sync::Arc;
57
58mod to_ref_or_refs;
62use to_ref_or_refs::OneOrMultipleRefs;
63
64const INVALID_SNIPPET: &str = "<invalid>";
65
66type Result<T> = std::result::Result<T, ParseErrors>;
68
69impl Node<Option<cst::Policies>> {
70 pub fn with_generated_policyids(
73 &self,
74 ) -> Result<impl Iterator<Item = (ast::PolicyID, &Node<Option<cst::Policy>>)>> {
75 let policies = self.try_as_inner()?;
76
77 Ok(policies.0.iter().enumerate().map(|(count, node)| {
78 (
79 ast::PolicyID::from_smolstr(format_smolstr!("policy{count}")),
80 node,
81 )
82 }))
83 }
84
85 pub fn to_policyset(&self) -> Result<ast::PolicySet> {
87 let mut pset = ast::PolicySet::new();
88 let mut all_errs: Vec<ParseErrors> = vec![];
89 for (policy_id, policy) in self.with_generated_policyids()? {
93 match policy.to_policy_or_template(policy_id) {
95 Ok(Either::Right(template)) => {
96 if let Err(e) = pset.add_template(template) {
97 match e {
98 PolicySetError::Occupied { id } => all_errs.push(
99 self.to_ast_err(ToASTErrorKind::DuplicateTemplateId(id))
100 .into(),
101 ),
102 };
103 }
104 }
105 Ok(Either::Left(static_policy)) => {
106 if let Err(e) = pset.add_static(static_policy) {
107 match e {
108 PolicySetError::Occupied { id } => all_errs.push(
109 self.to_ast_err(ToASTErrorKind::DuplicatePolicyId(id))
110 .into(),
111 ),
112 };
113 }
114 }
115 Err(errs) => {
116 all_errs.push(errs);
117 }
118 };
119 }
120
121 if let Some(errs) = ParseErrors::flatten(all_errs) {
123 Err(errs)
124 } else {
125 Ok(pset)
126 }
127 }
128
129 #[cfg(feature = "tolerant-ast")]
131 pub fn to_policyset_tolerant(&self) -> Result<ast::PolicySet> {
132 let mut pset = ast::PolicySet::new();
133 let mut all_errs: Vec<ParseErrors> = vec![];
134 for (policy_id, policy) in self.with_generated_policyids()? {
138 match policy.to_policy_or_template_tolerant(policy_id) {
140 Ok(Either::Right(template)) => {
141 if let Err(e) = pset.add_template(template) {
142 match e {
143 PolicySetError::Occupied { id } => all_errs.push(
144 self.to_ast_err(ToASTErrorKind::DuplicateTemplateId(id))
145 .into(),
146 ),
147 };
148 }
149 }
150 Ok(Either::Left(static_policy)) => {
151 if let Err(e) = pset.add_static(static_policy) {
152 match e {
153 PolicySetError::Occupied { id } => all_errs.push(
154 self.to_ast_err(ToASTErrorKind::DuplicatePolicyId(id))
155 .into(),
156 ),
157 };
158 }
159 }
160 Err(errs) => {
161 all_errs.push(errs);
162 }
163 };
164 }
165
166 if let Some(errs) = ParseErrors::flatten(all_errs) {
168 Err(errs)
169 } else {
170 Ok(pset)
171 }
172 }
173}
174
175impl Node<Option<cst::Policy>> {
176 pub fn to_template(&self, id: ast::PolicyID) -> Result<ast::Template> {
179 self.to_policy_template(id)
180 }
181
182 #[cfg(feature = "tolerant-ast")]
185 pub fn to_template_tolerant(&self, id: ast::PolicyID) -> Result<ast::Template> {
186 self.to_policy_template_tolerant(id)
187 }
188
189 pub fn to_policy_or_template(
191 &self,
192 id: ast::PolicyID,
193 ) -> Result<Either<ast::StaticPolicy, ast::Template>> {
194 let t = self.to_policy_template(id)?;
195 if t.slots().count() == 0 {
196 #[expect(clippy::expect_used, reason = "A `Template` with no slots will successfully convert to a `StaticPolicy`")]
197 let p = ast::StaticPolicy::try_from(t).expect("internal invariant violation: a template with no slots should be a valid static policy");
198 Ok(Either::Left(p))
199 } else {
200 Ok(Either::Right(t))
201 }
202 }
203
204 #[cfg(feature = "tolerant-ast")]
206 pub fn to_policy_or_template_tolerant(
207 &self,
208 id: ast::PolicyID,
209 ) -> Result<Either<ast::StaticPolicy, ast::Template>> {
210 let t = self.to_policy_template_tolerant(id)?;
211 if t.slots().count() == 0 {
212 #[expect(clippy::expect_used, reason = "A `Template` with no slots will successfully convert to a `StaticPolicy`")]
213 let p = ast::StaticPolicy::try_from(t).expect("internal invariant violation: a template with no slots should be a valid static policy");
214 Ok(Either::Left(p))
215 } else {
216 Ok(Either::Right(t))
217 }
218 }
219
220 pub fn to_policy(&self, id: ast::PolicyID) -> Result<ast::StaticPolicy> {
222 let maybe_template = self.to_policy_template(id);
223 let maybe_policy = maybe_template.map(ast::StaticPolicy::try_from);
224 match maybe_policy {
225 Ok(Ok(p)) => Ok(p),
227 Ok(Err(ast::UnexpectedSlotError::FoundSlot(slot))) => Err(ToASTError::new(
229 ToASTErrorKind::expected_static_policy(slot.clone()),
230 slot.loc.or_else(|| self.loc.clone()),
231 )
232 .into()),
233 Err(mut errs) => {
236 let new_errs = errs
237 .iter()
238 .filter_map(|err| match err {
239 ParseError::ToAST(err) => match err.kind() {
240 ToASTErrorKind::SlotsInConditionClause(inner) => Some(ToASTError::new(
241 ToASTErrorKind::expected_static_policy(inner.slot.clone()),
242 err.source_loc().cloned(),
243 )),
244 _ => None,
245 },
246 _ => None,
247 })
248 .collect::<Vec<_>>();
249 errs.extend(new_errs);
250 Err(errs)
251 }
252 }
253 }
254
255 pub fn to_policy_template(&self, id: ast::PolicyID) -> Result<ast::Template> {
258 let policy = self.try_as_inner()?;
259 let policy = match policy {
260 cst::Policy::Policy(policy_impl) => policy_impl,
261 #[cfg(feature = "tolerant-ast")]
262 cst::Policy::PolicyError => {
263 return Err(ParseErrors::singleton(ToASTError::new(
266 ToASTErrorKind::CSTErrorNode,
267 self.loc.clone(),
268 )));
269 }
270 };
271
272 let maybe_effect = policy.effect.to_effect();
274
275 let maybe_annotations = policy.get_ast_annotations(|value, loc| {
277 ast::Annotation::with_optional_value(value, loc.cloned())
278 });
279
280 let maybe_scope = policy.extract_scope();
282
283 let maybe_conds = ParseErrors::transpose(policy.conds.iter().map(|c| {
285 let (e, is_when) = c.to_expr::<ast::ExprBuilder<()>>()?;
286
287 let slot_errs = e.slots().map(|slot| {
288 ToASTError::new(
289 ToASTErrorKind::slots_in_condition_clause(
290 slot.clone(),
291 if is_when { "when" } else { "unless" },
292 ),
293 slot.loc.or_else(|| c.loc.clone()),
294 )
295 .into()
296 });
297 match ParseErrors::from_iter(slot_errs) {
298 Some(errs) => Err(errs),
299 None => Ok(e),
300 }
301 }));
302
303 let (effect, annotations, (principal, action, resource), conds) =
304 flatten_tuple_4(maybe_effect, maybe_annotations, maybe_scope, maybe_conds)?;
305 Ok(construct_template_policy(
306 id,
307 annotations.into(),
308 effect,
309 principal,
310 action,
311 resource,
312 conds,
313 self.loc(),
314 ))
315 }
316
317 #[cfg(feature = "tolerant-ast")]
322 pub fn to_policy_tolerant(&self, id: ast::PolicyID) -> Result<ast::StaticPolicy> {
323 let maybe_template = self.to_policy_template_tolerant(id);
324 let maybe_policy = maybe_template.map(ast::StaticPolicy::try_from);
325 match maybe_policy {
326 Ok(Ok(p)) => Ok(p),
328 Ok(Err(ast::UnexpectedSlotError::FoundSlot(slot))) => Err(ToASTError::new(
330 ToASTErrorKind::expected_static_policy(slot.clone()),
331 slot.loc.or_else(|| self.loc.clone()),
332 )
333 .into()),
334 Err(mut errs) => {
337 let new_errs = errs
338 .iter()
339 .filter_map(|err| match err {
340 ParseError::ToAST(err) => match err.kind() {
341 ToASTErrorKind::SlotsInConditionClause(inner) => Some(ToASTError::new(
342 ToASTErrorKind::expected_static_policy(inner.slot.clone()),
343 err.source_loc().cloned(),
344 )),
345 _ => None,
346 },
347 _ => None,
348 })
349 .collect::<Vec<_>>();
350 errs.extend(new_errs);
351 Err(errs)
352 }
353 }
354 }
355
356 #[cfg(feature = "tolerant-ast")]
362 pub fn to_policy_template_tolerant(&self, id: ast::PolicyID) -> Result<ast::Template> {
363 let policy = self.try_as_inner()?;
364 let policy = match policy {
365 cst::Policy::Policy(policy_impl) => policy_impl,
366 cst::Policy::PolicyError => {
367 return Ok(ast::Template::error(id, self.loc.clone()));
368 }
369 };
370 let maybe_effect = policy.effect.to_effect();
372
373 let maybe_annotations = policy.get_ast_annotations(|value, loc| {
375 ast::Annotation::with_optional_value(value, loc.cloned())
376 });
377
378 let maybe_scope = policy.extract_scope_tolerant_ast();
380
381 let maybe_conds = ParseErrors::transpose(policy.conds.iter().map(|c| {
383 let (e, is_when) = c.to_expr::<ExprWithErrsBuilder<()>>()?;
384 let slot_errs = e.slots().map(|slot| {
385 ToASTError::new(
386 ToASTErrorKind::slots_in_condition_clause(
387 slot.clone(),
388 if is_when { "when" } else { "unless" },
389 ),
390 slot.loc.or_else(|| c.loc.clone()),
391 )
392 .into()
393 });
394 match ParseErrors::from_iter(slot_errs) {
395 Some(errs) => Err(errs),
396 None => Ok(e),
397 }
398 }));
399
400 let (effect, annotations, (principal, action, resource), conds) =
401 flatten_tuple_4(maybe_effect, maybe_annotations, maybe_scope, maybe_conds)?;
402 Ok(construct_template_policy(
403 id,
404 annotations.into(),
405 effect,
406 principal,
407 action,
408 resource,
409 conds,
410 self.loc.as_ref(),
411 ))
412 }
413}
414
415impl cst::PolicyImpl {
416 pub fn extract_scope(
418 &self,
419 ) -> Result<(PrincipalConstraint, ActionConstraint, ResourceConstraint)> {
420 let mut end_of_last_var = self.effect.loc.as_ref().map(|loc| loc.end());
423
424 let mut vars = self.variables.iter();
425 let maybe_principal = if let Some(scope1) = vars.next() {
426 end_of_last_var = scope1.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
427 scope1.to_principal_constraint(TolerantAstSetting::NotTolerant)
428 } else {
429 let effect_span = self
430 .effect
431 .loc
432 .as_ref()
433 .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
434 Err(ToASTError::new(
435 ToASTErrorKind::MissingScopeVariable(ast::Var::Principal),
436 effect_span,
437 )
438 .into())
439 };
440 let maybe_action = if let Some(scope2) = vars.next() {
441 end_of_last_var = scope2.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
442 scope2.to_action_constraint(TolerantAstSetting::NotTolerant)
443 } else {
444 let effect_span = self
445 .effect
446 .loc
447 .as_ref()
448 .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
449 Err(ToASTError::new(
450 ToASTErrorKind::MissingScopeVariable(ast::Var::Action),
451 effect_span,
452 )
453 .into())
454 };
455 let maybe_resource = if let Some(scope3) = vars.next() {
456 scope3.to_resource_constraint(TolerantAstSetting::NotTolerant)
457 } else {
458 let effect_span = self
459 .effect
460 .loc
461 .as_ref()
462 .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
463 Err(ToASTError::new(
464 ToASTErrorKind::MissingScopeVariable(ast::Var::Resource),
465 effect_span,
466 )
467 .into())
468 };
469
470 let maybe_extra_vars = if let Some(errs) = ParseErrors::from_iter(
471 vars.map(|extra_var| {
473 extra_var
474 .try_as_inner()
475 .map(|def| {
476 extra_var
477 .to_ast_err(ToASTErrorKind::ExtraScopeElement(Box::new(def.clone())))
478 })
479 .unwrap_or_else(|e| e)
480 .into()
481 }),
482 ) {
483 Err(errs)
484 } else {
485 Ok(())
486 };
487 let (principal, action, resource, _) = flatten_tuple_4(
488 maybe_principal,
489 maybe_action,
490 maybe_resource,
491 maybe_extra_vars,
492 )?;
493 Ok((principal, action, resource))
494 }
495
496 #[cfg(feature = "tolerant-ast")]
498 pub fn extract_scope_tolerant_ast(
499 &self,
500 ) -> Result<(PrincipalConstraint, ActionConstraint, ResourceConstraint)> {
501 let mut end_of_last_var = self.effect.loc.as_ref().map(|loc| loc.end());
504
505 let mut vars = self.variables.iter();
506 let maybe_principal = if let Some(scope1) = vars.next() {
507 end_of_last_var = scope1.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
508 scope1.to_principal_constraint(TolerantAstSetting::Tolerant)
509 } else {
510 let effect_span = self
511 .effect
512 .loc
513 .as_ref()
514 .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
515 Err(ToASTError::new(
516 ToASTErrorKind::MissingScopeVariable(ast::Var::Principal),
517 effect_span,
518 )
519 .into())
520 };
521 let maybe_action = if let Some(scope2) = vars.next() {
522 end_of_last_var = scope2.loc.as_ref().map(|loc| loc.end()).or(end_of_last_var);
523 scope2.to_action_constraint(TolerantAstSetting::Tolerant)
524 } else {
525 let effect_span = self
526 .effect
527 .loc
528 .as_ref()
529 .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
530 Err(ToASTError::new(
531 ToASTErrorKind::MissingScopeVariable(ast::Var::Action),
532 effect_span,
533 )
534 .into())
535 };
536 let maybe_resource = if let Some(scope3) = vars.next() {
537 scope3.to_resource_constraint(TolerantAstSetting::Tolerant)
538 } else {
539 let effect_span = self
540 .effect
541 .loc
542 .as_ref()
543 .and_then(|loc| end_of_last_var.map(|end| loc.span(end)));
544 Err(ToASTError::new(
545 ToASTErrorKind::MissingScopeVariable(ast::Var::Resource),
546 effect_span,
547 )
548 .into())
549 };
550
551 let maybe_extra_vars = if let Some(errs) = ParseErrors::from_iter(
552 vars.map(|extra_var| {
554 extra_var
555 .try_as_inner()
556 .map(|def| {
557 extra_var
558 .to_ast_err(ToASTErrorKind::ExtraScopeElement(Box::new(def.clone())))
559 })
560 .unwrap_or_else(|e| e)
561 .into()
562 }),
563 ) {
564 Err(errs)
565 } else {
566 Ok(())
567 };
568 let (principal, action, resource, _) = flatten_tuple_4(
569 maybe_principal,
570 maybe_action,
571 maybe_resource,
572 maybe_extra_vars,
573 )?;
574 Ok((principal, action, resource))
575 }
576
577 pub fn get_ast_annotations<T>(
579 &self,
580 annotation_constructor: impl Fn(Option<SmolStr>, Option<&Loc>) -> T,
581 ) -> Result<BTreeMap<ast::AnyId, T>> {
582 let mut annotations = BTreeMap::new();
583 let mut all_errs: Vec<ParseErrors> = vec![];
584 for node in self.annotations.iter() {
585 match node.to_kv_pair(&annotation_constructor) {
586 Ok((k, v)) => {
587 use std::collections::btree_map::Entry;
588 match annotations.entry(k) {
589 Entry::Occupied(oentry) => {
590 all_errs.push(
591 ToASTError::new(
592 ToASTErrorKind::DuplicateAnnotation(oentry.key().clone()),
593 node.loc.clone(),
594 )
595 .into(),
596 );
597 }
598 Entry::Vacant(ventry) => {
599 ventry.insert(v);
600 }
601 }
602 }
603 Err(errs) => {
604 all_errs.push(errs);
605 }
606 }
607 }
608 match ParseErrors::flatten(all_errs) {
609 Some(errs) => Err(errs),
610 None => Ok(annotations),
611 }
612 }
613}
614
615impl Node<Option<cst::Annotation>> {
616 pub fn to_kv_pair<T>(
619 &self,
620 annotation_constructor: impl Fn(Option<SmolStr>, Option<&Loc>) -> T,
621 ) -> Result<(ast::AnyId, T)> {
622 let anno = self.try_as_inner()?;
623
624 let maybe_key = anno.key.to_any_ident();
625 let maybe_value = anno
626 .value
627 .as_ref()
628 .map(|a| {
629 a.as_valid_string().and_then(|s| {
630 to_unescaped_string(s).map_err(|unescape_errs| {
631 ParseErrors::new_from_nonempty(
632 unescape_errs.map(|e| self.to_ast_err(e).into()),
633 )
634 })
635 })
636 })
637 .transpose();
638
639 let (k, v) = flatten_tuple_2(maybe_key, maybe_value)?;
640 Ok((k, annotation_constructor(v, self.loc.as_ref())))
641 }
642}
643
644impl Node<Option<cst::Ident>> {
645 pub(crate) fn to_unreserved_ident(&self) -> Result<ast::UnreservedId> {
647 self.to_valid_ident()
648 .and_then(|id| id.try_into().map_err(|err| self.to_ast_err(err).into()))
649 }
650 pub fn to_valid_ident(&self) -> Result<ast::Id> {
652 let ident = self.try_as_inner()?;
653
654 match ident {
655 cst::Ident::If
656 | cst::Ident::True
657 | cst::Ident::False
658 | cst::Ident::Then
659 | cst::Ident::Else
660 | cst::Ident::In
661 | cst::Ident::Is
662 | cst::Ident::Has
663 | cst::Ident::Like => Err(self
664 .to_ast_err(ToASTErrorKind::ReservedIdentifier(ident.clone()))
665 .into()),
666 cst::Ident::Invalid(i) => Err(self
667 .to_ast_err(ToASTErrorKind::InvalidIdentifier(i.clone()))
668 .into()),
669 cst::Ident::Ident(i) => Ok(ast::Id::new_unchecked(i.clone())),
670 _ => Ok(ast::Id::new_unchecked(ident.to_smolstr())),
671 }
672 }
673
674 pub fn to_any_ident(&self) -> Result<ast::AnyId> {
680 let ident = self.try_as_inner()?;
681
682 match ident {
683 cst::Ident::Invalid(i) => Err(self
684 .to_ast_err(ToASTErrorKind::InvalidIdentifier(i.clone()))
685 .into()),
686 cst::Ident::Ident(i) => Ok(ast::AnyId::new_unchecked(i.clone())),
687 _ => Ok(ast::AnyId::new_unchecked(ident.to_smolstr())),
688 }
689 }
690
691 pub(crate) fn to_effect(&self) -> Result<ast::Effect> {
692 let effect = self.try_as_inner()?;
693
694 match effect {
695 cst::Ident::Permit => Ok(ast::Effect::Permit),
696 cst::Ident::Forbid => Ok(ast::Effect::Forbid),
697 _ => Err(self
698 .to_ast_err(ToASTErrorKind::InvalidEffect(effect.clone()))
699 .into()),
700 }
701 }
702
703 pub(crate) fn to_cond_is_when(&self) -> Result<bool> {
706 let cond = self.try_as_inner()?;
707
708 match cond {
709 cst::Ident::When => Ok(true),
710 cst::Ident::Unless => Ok(false),
711 _ => Err(self
712 .to_ast_err(ToASTErrorKind::InvalidCondition(cond.clone()))
713 .into()),
714 }
715 }
716
717 fn to_var(&self) -> Result<ast::Var> {
718 let ident = self.try_as_inner()?;
719
720 match ident {
721 cst::Ident::Principal => Ok(ast::Var::Principal),
722 cst::Ident::Action => Ok(ast::Var::Action),
723 cst::Ident::Resource => Ok(ast::Var::Resource),
724 ident => Err(self
725 .to_ast_err(ToASTErrorKind::InvalidScopeVariable(ident.clone()))
726 .into()),
727 }
728 }
729}
730
731impl ast::UnreservedId {
732 fn to_meth<Build: ExprBuilderInfallibleBuild>(
733 &self,
734 e: Build::Expr,
735 args: Vec<Build::Expr>,
736 loc: Option<&Loc>,
737 ) -> Result<Build::Expr> {
738 let builder = Build::new().with_maybe_source_loc(loc);
739 match self.as_ref() {
740 "contains" => extract_single_argument(args.into_iter(), "contains", loc)
741 .map(|arg| builder.contains(e, arg)),
742 "containsAll" => extract_single_argument(args.into_iter(), "containsAll", loc)
743 .map(|arg| builder.contains_all(e, arg)),
744 "containsAny" => extract_single_argument(args.into_iter(), "containsAny", loc)
745 .map(|arg| builder.contains_any(e, arg)),
746 "isEmpty" => {
747 require_zero_arguments(&args.into_iter(), "isEmpty", loc)?;
748 Ok(builder.is_empty(e))
749 }
750 "getTag" => extract_single_argument(args.into_iter(), "getTag", loc)
751 .map(|arg| builder.get_tag(e, arg)),
752 "hasTag" => extract_single_argument(args.into_iter(), "hasTag", loc)
753 .map(|arg| builder.has_tag(e, arg)),
754 _ => {
755 if ExtStyles::is_method(self) {
756 let args = NonEmpty {
757 head: e,
758 tail: args,
759 };
760 Ok(builder
761 .call_extension_fn(ast::Name::unqualified_name(self.clone()), args)
762 .unwrap_infallible())
763 } else {
764 let unqual_name = ast::Name::unqualified_name(self.clone());
765 if ExtStyles::is_function(&unqual_name) {
766 Err(ToASTError::new(
767 ToASTErrorKind::MethodCallOnFunction(unqual_name.basename()),
768 loc.cloned(),
769 )
770 .into())
771 } else {
772 convert_expr_error_to_parse_error::<Build>(
773 ToASTError::new(
774 ToASTErrorKind::UnknownMethod {
775 id: self.clone(),
776 hint: ExtStyles::suggest_method(self),
777 },
778 loc.cloned(),
779 )
780 .into(),
781 loc,
782 )
783 }
784 }
785 }
786 }
787 }
788}
789
790fn extract_single_argument<T>(
793 args: impl ExactSizeIterator<Item = T>,
794 fn_name: &'static str,
795 loc: Option<&Loc>,
796) -> Result<T> {
797 args.exactly_one().map_err(|args| {
798 ParseErrors::singleton(ToASTError::new(
799 ToASTErrorKind::wrong_arity(fn_name, 1, args.len()),
800 loc.cloned(),
801 ))
802 })
803}
804
805fn require_zero_arguments<T>(
807 args: &impl ExactSizeIterator<Item = T>,
808 fn_name: &'static str,
809 loc: Option<&Loc>,
810) -> Result<()> {
811 match args.len() {
812 0 => Ok(()),
813 n => Err(ParseErrors::singleton(ToASTError::new(
814 ToASTErrorKind::wrong_arity(fn_name, 0, n),
815 loc.cloned(),
816 ))),
817 }
818}
819
820#[derive(Debug)]
821enum PrincipalOrResource {
822 Principal(PrincipalConstraint),
823 Resource(ResourceConstraint),
824}
825
826#[derive(Debug, Clone, Copy)]
827enum TolerantAstSetting {
828 NotTolerant,
829 #[cfg(feature = "tolerant-ast")]
830 Tolerant,
831}
832
833impl Node<Option<cst::VariableDef>> {
834 fn to_principal_constraint(
835 &self,
836 tolerant_setting: TolerantAstSetting,
837 ) -> Result<PrincipalConstraint> {
838 match self.to_principal_or_resource_constraint(ast::Var::Principal, tolerant_setting)? {
839 PrincipalOrResource::Principal(p) => Ok(p),
840 PrincipalOrResource::Resource(_) => Err(self
841 .to_ast_err(ToASTErrorKind::IncorrectVariable {
842 expected: ast::Var::Principal,
843 got: ast::Var::Resource,
844 })
845 .into()),
846 }
847 }
848
849 fn to_resource_constraint(
850 &self,
851 tolerant_setting: TolerantAstSetting,
852 ) -> Result<ResourceConstraint> {
853 match self.to_principal_or_resource_constraint(ast::Var::Resource, tolerant_setting)? {
854 PrincipalOrResource::Principal(_) => Err(self
855 .to_ast_err(ToASTErrorKind::IncorrectVariable {
856 expected: ast::Var::Resource,
857 got: ast::Var::Principal,
858 })
859 .into()),
860 PrincipalOrResource::Resource(r) => Ok(r),
861 }
862 }
863
864 fn to_principal_or_resource_constraint(
865 &self,
866 expected: ast::Var,
867 tolerant_ast: TolerantAstSetting,
868 ) -> Result<PrincipalOrResource> {
869 let vardef = self.try_as_inner()?;
870 let var = vardef.variable.to_var()?;
871
872 if let Some(unused_typename) = vardef.unused_type_name.as_ref() {
873 unused_typename.to_type_constraint::<ast::ExprBuilder<()>>()?;
874 }
875
876 let c = if let Some((op, rel_expr)) = &vardef.ineq {
877 if op == &cst::RelOp::In {
879 if let Ok(expr) = rel_expr.to_expr::<ast::ExprBuilder<()>>() {
880 if matches!(expr.expr_kind(), ast::ExprKind::Is { .. }) {
881 return Err(self.to_ast_err(ToASTErrorKind::InvertedIsIn).into());
882 }
883 }
884 }
885 let eref = match tolerant_ast {
886 TolerantAstSetting::NotTolerant => rel_expr.to_ref_or_slot(var)?,
887 #[cfg(feature = "tolerant-ast")]
888 TolerantAstSetting::Tolerant => rel_expr.to_ref_or_slot_tolerant_ast(var)?,
889 };
890 match (op, &vardef.entity_type) {
891 (cst::RelOp::Eq, None) => Ok(PrincipalOrResourceConstraint::Eq(eref)),
892 (cst::RelOp::Eq, Some(_)) => Err(self.to_ast_err(ToASTErrorKind::IsWithEq)),
893 (cst::RelOp::In, None) => Ok(PrincipalOrResourceConstraint::In(eref)),
894 (cst::RelOp::In, Some(entity_type)) => {
895 match entity_type
896 .to_expr_or_special::<ast::ExprBuilder<()>>()?
897 .into_entity_type()
898 {
899 Ok(et) => Ok(PrincipalOrResourceConstraint::IsIn(Arc::new(et), eref)),
900 Err(eos) => Err(eos.to_ast_err(ToASTErrorKind::InvalidIsType {
901 lhs: var.to_string(),
902 rhs: eos
903 .loc()
904 .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
905 .unwrap_or(INVALID_SNIPPET)
906 .to_string(),
907 })),
908 }
909 }
910 (cst::RelOp::InvalidSingleEq, _) => {
911 Err(self.to_ast_err(ToASTErrorKind::InvalidSingleEq))
912 }
913 (op, _) => Err(self.to_ast_err(ToASTErrorKind::InvalidScopeOperator(*op))),
914 }
915 } else if let Some(entity_type) = &vardef.entity_type {
916 match entity_type
917 .to_expr_or_special::<ast::ExprBuilder<()>>()?
918 .into_entity_type()
919 {
920 Ok(et) => Ok(PrincipalOrResourceConstraint::Is(Arc::new(et))),
921 Err(eos) => Err(eos.to_ast_err(ToASTErrorKind::InvalidIsType {
922 lhs: var.to_string(),
923 rhs: eos
924 .loc()
925 .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
926 .unwrap_or(INVALID_SNIPPET)
927 .to_string(),
928 })),
929 }
930 } else {
931 Ok(PrincipalOrResourceConstraint::Any)
932 }?;
933 match var {
934 ast::Var::Principal => Ok(PrincipalOrResource::Principal(PrincipalConstraint::new(c))),
935 ast::Var::Resource => Ok(PrincipalOrResource::Resource(ResourceConstraint::new(c))),
936 got => Err(self
937 .to_ast_err(ToASTErrorKind::IncorrectVariable { expected, got })
938 .into()),
939 }
940 }
941
942 fn to_action_constraint(
943 &self,
944 tolerant_setting: TolerantAstSetting,
945 ) -> Result<ast::ActionConstraint> {
946 let vardef = self.try_as_inner()?;
947
948 match vardef.variable.to_var() {
949 Ok(ast::Var::Action) => Ok(()),
950 Ok(got) => Err(self
951 .to_ast_err(ToASTErrorKind::IncorrectVariable {
952 expected: ast::Var::Action,
953 got,
954 })
955 .into()),
956 Err(errs) => Err(errs),
957 }?;
958
959 if let Some(typename) = vardef.unused_type_name.as_ref() {
960 typename.to_type_constraint::<ast::ExprBuilder<()>>()?;
961 }
962
963 if vardef.entity_type.is_some() {
964 return Err(self.to_ast_err(ToASTErrorKind::IsInActionScope).into());
965 }
966
967 if let Some((op, rel_expr)) = &vardef.ineq {
968 let action_constraint = match op {
969 cst::RelOp::In => {
970 if let Ok(expr) = rel_expr.to_expr::<ast::ExprBuilder<()>>() {
972 if matches!(expr.expr_kind(), ast::ExprKind::Is { .. }) {
973 return Err(self.to_ast_err(ToASTErrorKind::IsInActionScope).into());
974 }
975 }
976 let one_or_multiple_refs = match tolerant_setting {
977 TolerantAstSetting::NotTolerant => rel_expr.to_refs(ast::Var::Action)?,
978 #[cfg(feature = "tolerant-ast")]
979 TolerantAstSetting::Tolerant => {
980 rel_expr.to_refs_tolerant_ast(ast::Var::Action)?
981 }
982 };
983 match one_or_multiple_refs {
984 OneOrMultipleRefs::Single(single_ref) => {
985 Ok(ActionConstraint::is_in([single_ref]))
986 }
987 OneOrMultipleRefs::Multiple(refs) => Ok(ActionConstraint::is_in(refs)),
988 }
989 }
990 cst::RelOp::Eq => {
991 let single_ref = match tolerant_setting {
992 TolerantAstSetting::NotTolerant => rel_expr.to_ref(ast::Var::Action)?,
993 #[cfg(feature = "tolerant-ast")]
994 TolerantAstSetting::Tolerant => {
995 rel_expr.to_ref_tolerant_ast(ast::Var::Action)?
996 }
997 };
998 Ok(ActionConstraint::is_eq(single_ref))
999 }
1000 cst::RelOp::InvalidSingleEq => {
1001 Err(self.to_ast_err(ToASTErrorKind::InvalidSingleEq))
1002 }
1003 op => Err(self.to_ast_err(ToASTErrorKind::InvalidActionScopeOperator(*op))),
1004 }?;
1005
1006 match tolerant_setting {
1007 TolerantAstSetting::NotTolerant => action_constraint
1008 .contains_only_action_types()
1009 .map_err(|non_action_euids| {
1010 rel_expr
1011 .to_ast_err(parse_errors::InvalidActionType {
1012 euids: non_action_euids,
1013 })
1014 .into()
1015 }),
1016 #[cfg(feature = "tolerant-ast")]
1017 TolerantAstSetting::Tolerant => {
1018 let action_constraint_res = action_constraint.contains_only_action_types();
1019 Ok(action_constraint_res.unwrap_or(ActionConstraint::ErrorConstraint))
1021 }
1022 }
1023 } else {
1024 Ok(ActionConstraint::Any)
1025 }
1026 }
1027}
1028
1029impl Node<Option<cst::Cond>> {
1030 fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<(Build::Expr, bool)> {
1035 let cond = self.try_as_inner()?;
1036 let is_when = cond.cond.to_cond_is_when()?;
1037
1038 let maybe_expr = match &cond.expr {
1039 Some(expr) => expr.to_expr::<Build>(),
1040 None => {
1041 let ident = match cond.cond.as_inner() {
1042 Some(ident) => ident.clone(),
1043 None => {
1044 if is_when {
1048 cst::Ident::Ident("when".into())
1049 } else {
1050 cst::Ident::Ident("unless".into())
1051 }
1052 }
1053 };
1054 convert_expr_error_to_parse_error::<Build>(
1055 self.to_ast_err(ToASTErrorKind::EmptyClause(Some(ident)))
1056 .into(),
1057 self.loc.as_ref(),
1058 )
1059 }
1060 };
1061
1062 maybe_expr.map(|e| {
1063 if is_when {
1064 (e, true)
1065 } else {
1066 (
1067 Build::new().with_maybe_source_loc(self.loc.as_ref()).not(e),
1068 false,
1069 )
1070 }
1071 })
1072 }
1073}
1074
1075impl Node<Option<cst::Str>> {
1076 pub(crate) fn as_valid_string(&self) -> Result<&SmolStr> {
1077 let id = self.try_as_inner()?;
1078
1079 match id {
1080 cst::Str::String(s) => Ok(s),
1081 cst::Str::Invalid(s) => Err(self
1083 .to_ast_err(ToASTErrorKind::InvalidString(s.to_string()))
1084 .into()),
1085 }
1086 }
1087}
1088
1089#[cfg(feature = "tolerant-ast")]
1090fn build_ast_error_node_if_possible<Build: ExprBuilderInfallibleBuild>(
1091 error: ParseErrors,
1092 loc: Option<&Loc>,
1093) -> Result<Build::Expr> {
1094 let res = Build::new().with_maybe_source_loc(loc).error(error.clone());
1095 match res {
1096 Ok(r) => Ok(r),
1097 Err(_) => Err(error),
1098 }
1099}
1100
1101#[cfg_attr(
1103 not(feature = "tolerant-ast"),
1104 expect(
1105 unused_variables,
1106 reason = "loc argument unused unless feature is enabled"
1107 )
1108)]
1109fn convert_expr_error_to_parse_error<Build: ExprBuilderInfallibleBuild>(
1110 error: ParseErrors,
1111 loc: Option<&Loc>,
1112) -> Result<Build::Expr> {
1113 #[cfg(feature = "tolerant-ast")]
1114 return build_ast_error_node_if_possible::<Build>(error, loc);
1115 #[cfg(not(feature = "tolerant-ast"))]
1116 Err(error)
1117}
1118
1119#[derive(Debug)]
1125pub(crate) enum ExprOrSpecial<'a, Expr> {
1126 Expr { expr: Expr, loc: Option<Loc> },
1128 Var { var: ast::Var, loc: Option<Loc> },
1130 Name { name: ast::Name, loc: Option<Loc> },
1132 StrLit { lit: &'a SmolStr, loc: Option<Loc> },
1135 BoolLit { val: bool, loc: Option<Loc> },
1137}
1138
1139impl<Expr> ExprOrSpecial<'_, Expr>
1140where
1141 Expr: std::fmt::Display,
1142{
1143 fn loc(&self) -> Option<&Loc> {
1144 match self {
1145 Self::Expr { loc, .. } => loc.as_ref(),
1146 Self::Var { loc, .. } => loc.as_ref(),
1147 Self::Name { loc, .. } => loc.as_ref(),
1148 Self::StrLit { loc, .. } => loc.as_ref(),
1149 Self::BoolLit { loc, .. } => loc.as_ref(),
1150 }
1151 }
1152
1153 fn to_ast_err(&self, kind: impl Into<ToASTErrorKind>) -> ToASTError {
1154 ToASTError::new(kind.into(), self.loc().cloned())
1155 }
1156
1157 fn into_expr<Build: ExprBuilderInfallibleBuild<Expr = Expr>>(self) -> Result<Expr> {
1158 match self {
1159 Self::Expr { expr, .. } => Ok(expr),
1160 Self::Var { var, loc } => Ok(Build::new().with_maybe_source_loc(loc.as_ref()).var(var)),
1161 Self::Name { name, loc } => convert_expr_error_to_parse_error::<Build>(
1162 ToASTError::new(
1163 ToASTErrorKind::ArbitraryVariable(name.to_string().into()),
1164 loc.clone(),
1165 )
1166 .into(),
1167 loc.as_ref(),
1168 ),
1169 Self::StrLit { lit, loc } => {
1170 match to_unescaped_string(lit) {
1171 Ok(s) => Ok(Build::new().with_maybe_source_loc(loc.as_ref()).val(s)),
1172 Err(escape_errs) => Err(ParseErrors::new_from_nonempty(escape_errs.map(|e| {
1173 ToASTError::new(ToASTErrorKind::Unescape(e), loc.clone()).into()
1174 }))),
1175 }
1176 }
1177 Self::BoolLit { val, loc } => {
1178 Ok(Build::new().with_maybe_source_loc(loc.as_ref()).val(val))
1179 }
1180 }
1181 }
1182
1183 pub(crate) fn into_valid_attr(self) -> Result<SmolStr> {
1185 match self {
1186 Self::Var { var, .. } => Ok(construct_string_from_var(var)),
1187 Self::Name { name, loc } => name.into_valid_attr(loc),
1188 Self::StrLit { lit, loc } => to_unescaped_string(lit).map_err(|escape_errs| {
1189 ParseErrors::new_from_nonempty(
1190 escape_errs
1191 .map(|e| ToASTError::new(ToASTErrorKind::Unescape(e), loc.clone()).into()),
1192 )
1193 }),
1194 Self::Expr { expr, loc } => Err(ToASTError::new(
1195 ToASTErrorKind::InvalidAttribute(expr.to_string().into()),
1196 loc,
1197 )
1198 .into()),
1199 Self::BoolLit { val, loc } => Err(ToASTError::new(
1200 ToASTErrorKind::ReservedIdentifier(if val {
1201 cst::Ident::True
1202 } else {
1203 cst::Ident::False
1204 }),
1205 loc,
1206 )
1207 .into()),
1208 }
1209 }
1210
1211 pub(crate) fn into_pattern(self) -> Result<Vec<PatternElem>> {
1212 match &self {
1213 Self::StrLit { lit, .. } => to_pattern(lit).map_err(|escape_errs| {
1214 ParseErrors::new_from_nonempty(
1215 escape_errs.map(|e| self.to_ast_err(ToASTErrorKind::Unescape(e)).into()),
1216 )
1217 }),
1218 Self::Var { var, .. } => Err(self
1219 .to_ast_err(ToASTErrorKind::InvalidPattern(var.to_string()))
1220 .into()),
1221 Self::Name { name, .. } => Err(self
1222 .to_ast_err(ToASTErrorKind::InvalidPattern(name.to_string()))
1223 .into()),
1224 Self::Expr { expr, .. } => Err(self
1225 .to_ast_err(ToASTErrorKind::InvalidPattern(expr.to_string()))
1226 .into()),
1227 Self::BoolLit { val, .. } => Err(self
1228 .to_ast_err(ToASTErrorKind::InvalidPattern(val.to_string()))
1229 .into()),
1230 }
1231 }
1232 fn into_string_literal(self) -> Result<SmolStr> {
1234 match &self {
1235 Self::StrLit { lit, .. } => to_unescaped_string(lit).map_err(|escape_errs| {
1236 ParseErrors::new_from_nonempty(
1237 escape_errs.map(|e| self.to_ast_err(ToASTErrorKind::Unescape(e)).into()),
1238 )
1239 }),
1240 Self::Var { var, .. } => Err(self
1241 .to_ast_err(ToASTErrorKind::InvalidString(var.to_string()))
1242 .into()),
1243 Self::Name { name, .. } => Err(self
1244 .to_ast_err(ToASTErrorKind::InvalidString(name.to_string()))
1245 .into()),
1246 Self::Expr { expr, .. } => Err(self
1247 .to_ast_err(ToASTErrorKind::InvalidString(expr.to_string()))
1248 .into()),
1249 Self::BoolLit { val, .. } => Err(self
1250 .to_ast_err(ToASTErrorKind::InvalidString(val.to_string()))
1251 .into()),
1252 }
1253 }
1254
1255 fn into_entity_type(self) -> std::result::Result<ast::EntityType, Self> {
1257 self.into_name().map(ast::EntityType::from)
1258 }
1259
1260 fn into_name(self) -> std::result::Result<ast::Name, Self> {
1262 match self {
1263 Self::Var { var, .. } => Ok(ast::Name::unqualified_name(var.into())),
1264 Self::Name { name, .. } => Ok(name),
1265 _ => Err(self),
1266 }
1267 }
1268}
1269
1270impl Node<Option<cst::Expr>> {
1271 pub fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1273 self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1274 }
1275 pub(crate) fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1276 &self,
1277 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1278 let expr_opt = self.try_as_inner()?;
1279
1280 let expr = match expr_opt {
1281 cst::Expr::Expr(expr_impl) => expr_impl,
1282 #[cfg(feature = "tolerant-ast")]
1283 cst::Expr::ErrorExpr => {
1284 let e = ToASTError::new(ToASTErrorKind::CSTErrorNode, self.loc.clone());
1285 return Ok(ExprOrSpecial::Expr {
1286 expr: convert_expr_error_to_parse_error::<Build>(e.into(), self.loc.as_ref())?,
1287 loc: self.loc.clone(),
1288 });
1289 }
1290 };
1291
1292 match &*expr.expr {
1293 cst::ExprData::Or(or) => or.to_expr_or_special::<Build>(),
1294 cst::ExprData::If(i, t, e) => {
1295 let maybe_guard = i.to_expr::<Build>();
1296 let maybe_then = t.to_expr::<Build>();
1297 let maybe_else = e.to_expr::<Build>();
1298
1299 let (i, t, e) = flatten_tuple_3(maybe_guard, maybe_then, maybe_else)?;
1300 Ok(ExprOrSpecial::Expr {
1301 expr: Build::new()
1302 .with_maybe_source_loc(self.loc.as_ref())
1303 .ite(i, t, e),
1304 loc: self.loc.clone(),
1305 })
1306 }
1307 }
1308 }
1309}
1310
1311impl Node<Option<cst::Or>> {
1312 fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1313 &self,
1314 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1315 let or = self.try_as_inner()?;
1316
1317 let maybe_first = or.initial.to_expr_or_special::<Build>();
1318 let maybe_rest = ParseErrors::transpose(or.extended.iter().map(|i| i.to_expr::<Build>()));
1319
1320 let (first, rest) = flatten_tuple_2(maybe_first, maybe_rest)?;
1321 if rest.is_empty() {
1322 Ok(first)
1325 } else {
1326 first.into_expr::<Build>().map(|first| ExprOrSpecial::Expr {
1327 expr: Build::new()
1328 .with_maybe_source_loc(self.loc.as_ref())
1329 .or_nary(first, rest),
1330 loc: self.loc.clone(),
1331 })
1332 }
1333 }
1334}
1335
1336impl Node<Option<cst::And>> {
1337 pub(crate) fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1338 self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1339 }
1340 fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1341 &self,
1342 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1343 let and = self.try_as_inner()?;
1344
1345 let maybe_first = and.initial.to_expr_or_special::<Build>();
1346 let maybe_rest = ParseErrors::transpose(and.extended.iter().map(|i| i.to_expr::<Build>()));
1347
1348 let (first, rest) = flatten_tuple_2(maybe_first, maybe_rest)?;
1349 if rest.is_empty() {
1350 Ok(first)
1353 } else {
1354 first.into_expr::<Build>().map(|first| ExprOrSpecial::Expr {
1355 expr: Build::new()
1356 .with_maybe_source_loc(self.loc.as_ref())
1357 .and_naryl(first, rest),
1358 loc: self.loc.clone(),
1359 })
1360 }
1361 }
1362}
1363
1364impl Node<Option<cst::Relation>> {
1365 fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1366 self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1367 }
1368 fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1369 &self,
1370 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1371 let rel = self.try_as_inner()?;
1372
1373 match rel {
1374 cst::Relation::Common { initial, extended } => {
1375 let maybe_first = initial.to_expr_or_special::<Build>();
1376 let maybe_rest = ParseErrors::transpose(
1377 extended
1378 .iter()
1379 .map(|(op, i)| i.to_expr::<Build>().map(|e| (op, e))),
1380 );
1381 let maybe_extra_elmts = if extended.len() > 1 {
1382 Err(self.to_ast_err(ToASTErrorKind::AmbiguousOperators).into())
1383 } else {
1384 Ok(())
1385 };
1386 let (first, rest, _) = flatten_tuple_3(maybe_first, maybe_rest, maybe_extra_elmts)?;
1387 let mut rest = rest.into_iter();
1388 let second = rest.next();
1389 match second {
1390 None => Ok(first),
1391 Some((&op, second)) => first.into_expr::<Build>().and_then(|first| {
1392 Ok(ExprOrSpecial::Expr {
1393 expr: construct_expr_rel::<Build>(first, op, second, self.loc.clone())?,
1394 loc: self.loc.clone(),
1395 })
1396 }),
1397 }
1398 }
1399 cst::Relation::Has { target, field } => {
1400 let maybe_target = target.to_expr::<Build>();
1401 let maybe_fields = Ok(match field.to_has_rhs::<Build>()? {
1402 Either::Left(s) => nonempty![s],
1403 Either::Right(ids) => ids.map(|id| id.into_smolstr()),
1404 });
1405 let (target, fields) = flatten_tuple_2(maybe_target, maybe_fields)?;
1406 Ok(ExprOrSpecial::Expr {
1407 expr: Build::new()
1408 .with_maybe_source_loc(self.loc.as_ref())
1409 .extended_has_attr(target, fields),
1410 loc: self.loc.clone(),
1411 })
1412 }
1413 cst::Relation::Like { target, pattern } => {
1414 let maybe_target = target.to_expr::<Build>();
1415 let maybe_pattern = pattern.to_expr_or_special::<Build>()?.into_pattern();
1416 let (target, pattern) = flatten_tuple_2(maybe_target, maybe_pattern)?;
1417 Ok(ExprOrSpecial::Expr {
1418 expr: Build::new()
1419 .with_maybe_source_loc(self.loc.as_ref())
1420 .like(target, pattern.into()),
1421 loc: self.loc.clone(),
1422 })
1423 }
1424 cst::Relation::IsIn {
1425 target,
1426 entity_type,
1427 in_entity,
1428 } => {
1429 let maybe_target = target.to_expr::<Build>();
1430 let maybe_entity_type = entity_type
1431 .to_expr_or_special::<Build>()?
1432 .into_entity_type()
1433 .map_err(|eos| {
1434 eos.to_ast_err(ToASTErrorKind::InvalidIsType {
1435 lhs: maybe_target
1436 .as_ref()
1437 .map(|expr| expr.to_string())
1438 .unwrap_or_else(|_| "..".to_string()),
1439 rhs: eos
1440 .loc()
1441 .map(|loc| loc.snippet().unwrap_or(INVALID_SNIPPET))
1442 .unwrap_or(INVALID_SNIPPET)
1443 .to_string(),
1444 })
1445 .into()
1446 });
1447 let (t, n) = flatten_tuple_2(maybe_target, maybe_entity_type)?;
1448 match in_entity {
1449 Some(in_entity) => {
1450 let in_expr = in_entity.to_expr::<Build>()?;
1451 Ok(ExprOrSpecial::Expr {
1452 expr: Build::new()
1453 .with_maybe_source_loc(self.loc.as_ref())
1454 .is_in_entity_type(t, n, in_expr),
1455 loc: self.loc.clone(),
1456 })
1457 }
1458 None => Ok(ExprOrSpecial::Expr {
1459 expr: Build::new()
1460 .with_maybe_source_loc(self.loc.as_ref())
1461 .is_entity_type(t, n),
1462 loc: self.loc.clone(),
1463 }),
1464 }
1465 }
1466 }
1467 }
1468}
1469
1470impl Node<Option<cst::Add>> {
1471 fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1472 self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1473 }
1474
1475 pub(crate) fn to_has_rhs<Build: ExprBuilderInfallibleBuild>(
1485 &self,
1486 ) -> Result<Either<SmolStr, NonEmpty<UnreservedId>>> {
1487 let inner @ cst::Add { initial, extended } = self.try_as_inner()?;
1488 let err = |loc| {
1489 ToASTError::new(ToASTErrorKind::InvalidHasRHS(inner.to_string().into()), loc).into()
1490 };
1491 let construct_attrs =
1492 |first, rest: &[Node<Option<cst::MemAccess>>]| -> Result<NonEmpty<UnreservedId>> {
1493 let mut acc = nonempty![first];
1494 rest.iter().try_for_each(|ma_node| {
1495 let ma = ma_node.try_as_inner()?;
1496 match ma {
1497 cst::MemAccess::Field(id) => {
1498 acc.push(id.to_unreserved_ident()?);
1499 Ok(())
1500 }
1501 _ => Err(err(ma_node.loc.clone())),
1502 }
1503 })?;
1504 Ok(acc)
1505 };
1506 if !extended.is_empty() {
1507 return Err(err(self.loc.clone()));
1508 }
1509 let cst::Mult { initial, extended } = initial.try_as_inner()?;
1510 if !extended.is_empty() {
1511 return Err(err(self.loc.clone()));
1512 }
1513 if let cst::Unary {
1514 op: None,
1515 item: item_node,
1516 } = initial.try_as_inner()?
1517 {
1518 let cst::Member { item, access } = item_node.try_as_inner()?;
1519 match item.try_as_inner()? {
1524 cst::Primary::EList(_)
1525 | cst::Primary::Expr(_)
1526 | cst::Primary::RInits(_)
1527 | cst::Primary::Ref(_)
1528 | cst::Primary::Slot(_) => Err(err(item.loc.clone())),
1529 cst::Primary::Literal(_) | cst::Primary::Name(_) => {
1530 let item = item.to_expr_or_special::<Build>()?;
1531 match (item, access.as_slice()) {
1532 (ExprOrSpecial::StrLit { lit, loc }, []) => Ok(Either::Left(
1533 to_unescaped_string(lit).map_err(|escape_errs| {
1534 ParseErrors::new_from_nonempty(escape_errs.map(|e| {
1535 ToASTError::new(ToASTErrorKind::Unescape(e), loc.clone()).into()
1536 }))
1537 })?,
1538 )),
1539 (ExprOrSpecial::Var { var, .. }, rest) => {
1540 #[expect(
1541 clippy::unwrap_used,
1542 reason = "any variable should be a valid identifier"
1543 )]
1544 let first = construct_string_from_var(var).parse().unwrap();
1545 Ok(Either::Right(construct_attrs(first, rest)?))
1546 }
1547 (ExprOrSpecial::Name { name, loc }, rest) => {
1548 if name.is_unqualified() {
1549 let first = name.basename();
1550
1551 Ok(Either::Right(construct_attrs(first, rest)?))
1552 } else {
1553 Err(ToASTError::new(
1554 ToASTErrorKind::PathAsAttribute(inner.to_string()),
1555 loc,
1556 )
1557 .into())
1558 }
1559 }
1560 (ExprOrSpecial::BoolLit { val, loc }, _) => Err(ToASTError::new(
1562 ToASTErrorKind::ReservedIdentifier(if val {
1563 cst::Ident::True
1564 } else {
1565 cst::Ident::False
1566 }),
1567 loc,
1568 )
1569 .into()),
1570 (ExprOrSpecial::Expr { loc, .. }, _) => Err(err(loc)),
1571 _ => Err(err(self.loc.clone())),
1572 }
1573 }
1574 }
1575 } else {
1576 Err(err(self.loc.clone()))
1577 }
1578 }
1579
1580 pub(crate) fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1581 &self,
1582 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1583 let add = self.try_as_inner()?;
1584
1585 let maybe_first = add.initial.to_expr_or_special::<Build>();
1586 let maybe_rest = ParseErrors::transpose(
1587 add.extended
1588 .iter()
1589 .map(|&(op, ref i)| i.to_expr::<Build>().map(|e| (op, e))),
1590 );
1591 let (first, rest) = flatten_tuple_2(maybe_first, maybe_rest)?;
1592 if !rest.is_empty() {
1593 let first = first.into_expr::<Build>()?;
1595 Ok(ExprOrSpecial::Expr {
1596 expr: Build::new()
1597 .with_maybe_source_loc(self.loc.as_ref())
1598 .add_nary(first, rest),
1599 loc: self.loc.clone(),
1600 })
1601 } else {
1602 Ok(first)
1603 }
1604 }
1605}
1606
1607impl Node<Option<cst::Mult>> {
1608 fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1609 self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1610 }
1611 fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1612 &self,
1613 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1614 let mult = self.try_as_inner()?;
1615
1616 let maybe_first = mult.initial.to_expr_or_special::<Build>();
1617 let maybe_rest = ParseErrors::transpose(mult.extended.iter().map(|&(op, ref i)| {
1618 i.to_expr::<Build>().and_then(|e| match op {
1619 cst::MultOp::Times => Ok(e),
1620 cst::MultOp::Divide => {
1621 Err(self.to_ast_err(ToASTErrorKind::UnsupportedDivision).into())
1622 }
1623 cst::MultOp::Mod => Err(self.to_ast_err(ToASTErrorKind::UnsupportedModulo).into()),
1624 })
1625 }));
1626
1627 let (first, rest) = flatten_tuple_2(maybe_first, maybe_rest)?;
1628 if !rest.is_empty() {
1629 let first = first.into_expr::<Build>()?;
1631 Ok(ExprOrSpecial::Expr {
1632 expr: Build::new()
1633 .with_maybe_source_loc(self.loc.as_ref())
1634 .mul_nary(first, rest),
1635 loc: self.loc.clone(),
1636 })
1637 } else {
1638 Ok(first)
1639 }
1640 }
1641}
1642
1643impl Node<Option<cst::Unary>> {
1644 fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1645 self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1646 }
1647 fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1648 &self,
1649 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1650 let unary = self.try_as_inner()?;
1651
1652 match unary.op {
1653 None => unary.item.to_expr_or_special::<Build>(),
1654 Some(cst::NegOp::Bang(n)) => {
1655 (0..n).fold(unary.item.to_expr_or_special::<Build>(), |inner, _| {
1656 inner
1657 .and_then(|e| e.into_expr::<Build>())
1658 .map(|expr| ExprOrSpecial::Expr {
1659 expr: Build::new()
1660 .with_maybe_source_loc(self.loc.as_ref())
1661 .not(expr),
1662 loc: self.loc.clone(),
1663 })
1664 })
1665 }
1666 Some(cst::NegOp::Dash(0)) => unary.item.to_expr_or_special::<Build>(),
1667 Some(cst::NegOp::Dash(c)) => {
1668 let (last, rc) = if let Some(cst::Literal::Num(n)) = unary.item.to_lit() {
1674 match n.cmp(&(i64::MAX as u64 + 1)) {
1675 Ordering::Equal => (
1676 Ok(Build::new()
1677 .with_maybe_source_loc(unary.item.loc.as_ref())
1678 .val(i64::MIN)),
1679 c - 1,
1680 ),
1681 Ordering::Less => (
1682 Ok(Build::new()
1683 .with_maybe_source_loc(unary.item.loc.as_ref())
1684 .val(-(*n as i64))),
1685 c - 1,
1686 ),
1687 Ordering::Greater => (
1688 Err(self
1689 .to_ast_err(ToASTErrorKind::IntegerLiteralTooLarge(*n))
1690 .into()),
1691 0,
1692 ),
1693 }
1694 } else {
1695 (
1698 unary
1699 .item
1700 .to_expr_or_special::<Build>()
1701 .and_then(|i| i.into_expr::<Build>()),
1702 c,
1703 )
1704 };
1705 (0..rc)
1707 .fold(last, |r, _| {
1708 r.map(|e| Build::new().with_maybe_source_loc(self.loc.as_ref()).neg(e))
1709 })
1710 .map(|expr| ExprOrSpecial::Expr {
1711 expr,
1712 loc: self.loc.clone(),
1713 })
1714 }
1715 Some(cst::NegOp::OverBang) => Err(self
1716 .to_ast_err(ToASTErrorKind::UnaryOpLimit(ast::UnaryOp::Not))
1717 .into()),
1718 Some(cst::NegOp::OverDash) => Err(self
1719 .to_ast_err(ToASTErrorKind::UnaryOpLimit(ast::UnaryOp::Neg))
1720 .into()),
1721 }
1722 }
1723}
1724
1725enum AstAccessor<Expr> {
1727 Field(ast::UnreservedId),
1728 Call(Vec<Expr>),
1729 Index(SmolStr),
1730}
1731
1732impl Node<Option<cst::Member>> {
1733 pub fn to_lit(&self) -> Option<&cst::Literal> {
1738 let m = self.as_ref().node.as_ref()?;
1739 if !m.access.is_empty() {
1740 return None;
1741 }
1742 match m.item.as_ref().node.as_ref()? {
1743 cst::Primary::Literal(lit) => lit.as_ref().node.as_ref(),
1744 _ => None,
1745 }
1746 }
1747
1748 #[expect(clippy::type_complexity, reason = "judged readable enough")]
1760 fn build_expr_accessor<'a, Build: ExprBuilderInfallibleBuild>(
1761 &self,
1762 head: Build::Expr,
1763 next: &mut AstAccessor<Build::Expr>,
1764 tail: &'a mut [AstAccessor<Build::Expr>],
1765 ) -> Result<(Build::Expr, &'a mut [AstAccessor<Build::Expr>])> {
1766 use AstAccessor::*;
1767 match (next, tail) {
1768 (Call(_), _) => Err(self.to_ast_err(ToASTErrorKind::ExpressionCall).into()),
1770
1771 (Field(id), [Call(args), rest @ ..]) => {
1773 let args = std::mem::take(args);
1775 let id = mem::replace(id, ast::UnreservedId::empty());
1777 Ok((id.to_meth::<Build>(head, args, self.loc.as_ref())?, rest))
1778 }
1779
1780 (Field(id), rest) => {
1782 let id = mem::replace(id, ast::UnreservedId::empty());
1783 Ok((
1784 Build::new()
1785 .with_maybe_source_loc(self.loc.as_ref())
1786 .get_attr(head, id.into_smolstr()),
1787 rest,
1788 ))
1789 }
1790
1791 (Index(i), rest) => {
1793 let i = mem::take(i);
1794 Ok((
1795 Build::new()
1796 .with_maybe_source_loc(self.loc.as_ref())
1797 .get_attr(head, i),
1798 rest,
1799 ))
1800 }
1801 }
1802 }
1803
1804 fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1805 &self,
1806 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1807 let mem = self.try_as_inner()?;
1808
1809 let maybe_prim = mem.item.to_expr_or_special::<Build>();
1810 let maybe_accessors =
1811 ParseErrors::transpose(mem.access.iter().map(|a| a.to_access::<Build>()));
1812
1813 let (prim, mut accessors) = flatten_tuple_2(maybe_prim, maybe_accessors)?;
1815
1816 let (mut head, mut tail) = {
1817 use AstAccessor::*;
1818 use ExprOrSpecial::*;
1819 match (prim, accessors.as_mut_slice()) {
1820 (prim, []) => return Ok(prim),
1822
1823 (prim @ (Expr { .. } | StrLit { .. } | BoolLit { .. }), [next, rest @ ..]) => {
1828 self.build_expr_accessor::<Build>(prim.into_expr::<Build>()?, next, rest)?
1829 }
1830
1831 (Name { name, .. }, [Call(args), rest @ ..]) => {
1833 let args = std::mem::take(args);
1835 (name.into_func::<Build>(args, self.loc.clone())?, rest)
1836 }
1837 (Var { var, .. }, [Call(_), ..]) => {
1839 return Err(self.to_ast_err(ToASTErrorKind::VariableCall(var)).into());
1840 }
1841
1842 (Name { name, .. }, [Field(f), Call(_), ..]) => {
1844 return Err(self
1845 .to_ast_err(ToASTErrorKind::NoMethods(name, f.clone()))
1846 .into());
1847 }
1848 (Var { var, loc: var_loc }, [Field(id), Call(args), rest @ ..]) => {
1850 let args = std::mem::take(args);
1851 let id = mem::replace(id, ast::UnreservedId::empty());
1853 (
1854 id.to_meth::<Build>(
1855 Build::new()
1856 .with_maybe_source_loc(var_loc.as_ref())
1857 .var(var),
1858 args,
1859 self.loc.as_ref(),
1860 )?,
1861 rest,
1862 )
1863 }
1864
1865 (Var { var, loc: var_loc }, [Field(i), rest @ ..]) => {
1867 let id = mem::replace(i, ast::UnreservedId::empty());
1868 (
1869 Build::new()
1870 .with_maybe_source_loc(self.loc.as_ref())
1871 .get_attr(
1872 Build::new()
1873 .with_maybe_source_loc(var_loc.as_ref())
1874 .var(var),
1875 id.into_smolstr(),
1876 ),
1877 rest,
1878 )
1879 }
1880 (Name { name, .. }, [Field(f), ..]) => {
1882 return Err(self
1883 .to_ast_err(ToASTErrorKind::InvalidAccess {
1884 lhs: name,
1885 field: f.clone().into_smolstr(),
1886 })
1887 .into());
1888 }
1889 (Name { name, .. }, [Index(i), ..]) => {
1891 return Err(self
1892 .to_ast_err(ToASTErrorKind::InvalidIndex {
1893 lhs: name,
1894 field: i.clone(),
1895 })
1896 .into());
1897 }
1898
1899 (Var { var, loc: var_loc }, [Index(i), rest @ ..]) => {
1901 let i = mem::take(i);
1902 (
1903 Build::new()
1904 .with_maybe_source_loc(self.loc.as_ref())
1905 .get_attr(
1906 Build::new()
1907 .with_maybe_source_loc(var_loc.as_ref())
1908 .var(var),
1909 i,
1910 ),
1911 rest,
1912 )
1913 }
1914 }
1915 };
1916
1917 while let [next, rest @ ..] = tail {
1922 (head, tail) = self.build_expr_accessor::<Build>(head, next, rest)?;
1923 }
1924 Ok(ExprOrSpecial::Expr {
1925 expr: head,
1926 loc: self.loc.clone(),
1927 })
1928 }
1929}
1930
1931impl Node<Option<cst::MemAccess>> {
1932 fn to_access<Build: ExprBuilderInfallibleBuild>(&self) -> Result<AstAccessor<Build::Expr>> {
1933 let acc = self.try_as_inner()?;
1934
1935 match acc {
1936 cst::MemAccess::Field(i) => {
1937 let maybe_ident = i.to_unreserved_ident();
1938 maybe_ident.map(AstAccessor::Field)
1939 }
1940 cst::MemAccess::Call(args) => {
1941 let maybe_args = ParseErrors::transpose(args.iter().map(|e| e.to_expr::<Build>()));
1942 maybe_args.map(AstAccessor::Call)
1943 }
1944 cst::MemAccess::Index(index) => {
1945 let maybe_index = index.to_expr_or_special::<Build>()?.into_string_literal();
1946 maybe_index.map(AstAccessor::Index)
1947 }
1948 }
1949 }
1950}
1951
1952impl Node<Option<cst::Primary>> {
1953 pub(crate) fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
1954 self.to_expr_or_special::<Build>()?.into_expr::<Build>()
1955 }
1956 fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
1957 &self,
1958 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
1959 let prim = self.try_as_inner()?;
1960
1961 match prim {
1962 cst::Primary::Literal(lit) => lit.to_expr_or_special::<Build>(),
1963 cst::Primary::Ref(r) => r.to_expr::<Build>().map(|expr| ExprOrSpecial::Expr {
1964 expr,
1965 loc: r.loc.clone(),
1966 }),
1967 cst::Primary::Slot(s) => {
1968 s.clone()
1969 .into_expr::<Build>()
1970 .map(|expr| ExprOrSpecial::Expr {
1971 expr,
1972 loc: s.loc.clone(),
1973 })
1974 }
1975 cst::Primary::Name(n) => {
1976 if let Some(var) = n.maybe_to_var() {
1978 Ok(ExprOrSpecial::Var {
1979 var,
1980 loc: self.loc.clone(),
1981 })
1982 } else {
1983 n.to_internal_name().and_then(|name| match name.try_into() {
1984 Ok(name) => Ok(ExprOrSpecial::Name {
1985 name,
1986 loc: self.loc.clone(),
1987 }),
1988 Err(err) => Err(ParseErrors::singleton(err)),
1989 })
1990 }
1991 }
1992 cst::Primary::Expr(e) => e.to_expr::<Build>().map(|expr| ExprOrSpecial::Expr {
1993 expr,
1994 loc: e.loc.clone(),
1995 }),
1996 cst::Primary::EList(es) => {
1997 let maybe_list = ParseErrors::transpose(es.iter().map(|e| e.to_expr::<Build>()));
1998 maybe_list.map(|list| ExprOrSpecial::Expr {
1999 expr: Build::new()
2000 .with_maybe_source_loc(self.loc.as_ref())
2001 .set(list),
2002 loc: self.loc.clone(),
2003 })
2004 }
2005 cst::Primary::RInits(is) => {
2006 let rec = ParseErrors::transpose(is.iter().map(|i| i.to_init::<Build>()))?;
2007 let expr = Build::new()
2008 .with_maybe_source_loc(self.loc.as_ref())
2009 .record(rec)
2010 .map_err(|e| {
2011 Into::<ParseErrors>::into(ToASTError::new(e.into(), self.loc.clone()))
2012 })?;
2013 Ok(ExprOrSpecial::Expr {
2014 expr,
2015 loc: self.loc.clone(),
2016 })
2017 }
2018 }
2019 }
2020
2021 pub fn to_string_literal<Build: ExprBuilderInfallibleBuild>(&self) -> Result<SmolStr> {
2023 let prim = self.try_as_inner()?;
2024
2025 match prim {
2026 cst::Primary::Literal(lit) => lit.to_expr_or_special::<Build>()?.into_string_literal(),
2027 _ => Err(self
2028 .to_ast_err(ToASTErrorKind::InvalidString(prim.to_string()))
2029 .into()),
2030 }
2031 }
2032}
2033
2034impl Node<Option<cst::Slot>> {
2035 fn into_expr<Build: ExprBuilderInfallibleBuild>(self) -> Result<Build::Expr> {
2036 match self.try_as_inner()?.try_into() {
2037 Ok(slot_id) => Ok(Build::new()
2038 .with_maybe_source_loc(self.loc.as_ref())
2039 .slot(slot_id)),
2040 Err(e) => Err(self.to_ast_err(e).into()),
2041 }
2042 }
2043}
2044
2045impl TryFrom<&cst::Slot> for ast::SlotId {
2046 type Error = ToASTErrorKind;
2047
2048 fn try_from(slot: &cst::Slot) -> std::result::Result<Self, Self::Error> {
2049 match slot {
2050 cst::Slot::Principal => Ok(ast::SlotId::principal()),
2051 cst::Slot::Resource => Ok(ast::SlotId::resource()),
2052 cst::Slot::Other(slot) => Err(ToASTErrorKind::InvalidSlot(slot.clone())),
2053 }
2054 }
2055}
2056
2057impl From<ast::SlotId> for cst::Slot {
2058 fn from(slot: ast::SlotId) -> cst::Slot {
2059 match slot {
2060 ast::SlotId(ast::ValidSlotId::Principal) => cst::Slot::Principal,
2061 ast::SlotId(ast::ValidSlotId::Resource) => cst::Slot::Resource,
2062 }
2063 }
2064}
2065
2066impl Node<Option<cst::Name>> {
2067 fn to_type_constraint<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
2069 match self.as_inner() {
2070 Some(_) => Err(self.to_ast_err(ToASTErrorKind::TypeConstraints).into()),
2071 None => Ok(Build::new()
2072 .with_maybe_source_loc(self.loc.as_ref())
2073 .val(true)),
2074 }
2075 }
2076
2077 pub(crate) fn to_name(&self) -> Result<ast::Name> {
2078 self.to_internal_name()
2079 .and_then(|n| n.try_into().map_err(ParseErrors::singleton))
2080 }
2081
2082 pub(crate) fn to_internal_name(&self) -> Result<ast::InternalName> {
2083 let name = self.try_as_inner()?;
2084
2085 let maybe_path = ParseErrors::transpose(name.path.iter().map(|i| i.to_valid_ident()));
2086 let maybe_name = name.name.to_valid_ident();
2087
2088 let (name, path) = flatten_tuple_2(maybe_name, maybe_path)?;
2090 Ok(construct_name(path, name, self.loc.clone()))
2091 }
2092
2093 fn maybe_to_var(&self) -> Option<ast::Var> {
2096 let name = self.as_inner()?;
2097 let ident = if name.path.is_empty() {
2098 name.name.as_inner()
2099 } else {
2100 None
2102 }?;
2103
2104 match ident {
2105 cst::Ident::Principal => Some(ast::Var::Principal),
2106 cst::Ident::Action => Some(ast::Var::Action),
2107 cst::Ident::Resource => Some(ast::Var::Resource),
2108 cst::Ident::Context => Some(ast::Var::Context),
2109 _ => None,
2110 }
2111 }
2112}
2113
2114impl ast::Name {
2115 fn into_valid_attr(self, loc: Option<Loc>) -> Result<SmolStr> {
2117 if !self.0.path.is_empty() {
2118 Err(ToASTError::new(ToASTErrorKind::PathAsAttribute(self.to_string()), loc).into())
2119 } else {
2120 Ok(self.0.id.into_smolstr())
2121 }
2122 }
2123
2124 fn into_func<Build: ExprBuilderInfallibleBuild>(
2125 self,
2126 args: Vec<Build::Expr>,
2127 loc: Option<Loc>,
2128 ) -> Result<Build::Expr> {
2129 if self.0.path.is_empty() {
2131 let id = self.basename();
2132 if ExtStyles::is_method(&id)
2133 || matches!(
2134 id.as_ref(),
2135 "contains" | "containsAll" | "containsAny" | "isEmpty" | "getTag" | "hasTag"
2136 )
2137 {
2138 return Err(ToASTError::new(
2139 ToASTErrorKind::FunctionCallOnMethod(self.basename()),
2140 loc,
2141 )
2142 .into());
2143 }
2144 }
2145 if ExtStyles::is_function(&self) {
2146 Ok(Build::new()
2147 .with_maybe_source_loc(loc.as_ref())
2148 .call_extension_fn(self, args)
2149 .unwrap_infallible())
2150 } else {
2151 let hint = ExtStyles::suggest_function(&self);
2152 Err(ToASTError::new(ToASTErrorKind::UnknownFunction { id: self, hint }, loc).into())
2153 }
2154 }
2155}
2156
2157impl Node<Option<cst::Ref>> {
2158 pub fn to_ref(&self) -> Result<ast::EntityUID> {
2160 let refr = self.try_as_inner()?;
2161
2162 match refr {
2163 cst::Ref::Uid { path, eid } => {
2164 let maybe_path = path.to_name().map(ast::EntityType::from);
2165 let maybe_eid = eid.as_valid_string().and_then(|s| {
2166 to_unescaped_string(s).map_err(|escape_errs| {
2167 ParseErrors::new_from_nonempty(
2168 escape_errs
2169 .map(|e| self.to_ast_err(ToASTErrorKind::Unescape(e)).into()),
2170 )
2171 })
2172 });
2173
2174 let (p, e) = flatten_tuple_2(maybe_path, maybe_eid)?;
2175 Ok({
2176 let loc = self.loc.clone();
2177 ast::EntityUID::from_components(p, ast::Eid::new(e), loc)
2178 })
2179 }
2180 r @ cst::Ref::Ref { .. } => Err(self
2181 .to_ast_err(ToASTErrorKind::InvalidEntityLiteral(r.to_string()))
2182 .into()),
2183 }
2184 }
2185 fn to_expr<Build: ExprBuilderInfallibleBuild>(&self) -> Result<Build::Expr> {
2186 self.to_ref().map(|euid| {
2187 Build::new()
2188 .with_maybe_source_loc(self.loc.as_ref())
2189 .val(euid)
2190 })
2191 }
2192}
2193
2194impl Node<Option<cst::Literal>> {
2195 fn to_expr_or_special<Build: ExprBuilderInfallibleBuild>(
2196 &self,
2197 ) -> Result<ExprOrSpecial<'_, Build::Expr>> {
2198 let lit = self.try_as_inner()?;
2199
2200 match lit {
2201 cst::Literal::True => Ok(ExprOrSpecial::BoolLit {
2202 val: true,
2203 loc: self.loc.clone(),
2204 }),
2205 cst::Literal::False => Ok(ExprOrSpecial::BoolLit {
2206 val: false,
2207 loc: self.loc.clone(),
2208 }),
2209 cst::Literal::Num(n) => match Integer::try_from(*n) {
2210 Ok(i) => Ok(ExprOrSpecial::Expr {
2211 expr: Build::new().with_maybe_source_loc(self.loc.as_ref()).val(i),
2212 loc: self.loc.clone(),
2213 }),
2214 Err(_) => Err(self
2215 .to_ast_err(ToASTErrorKind::IntegerLiteralTooLarge(*n))
2216 .into()),
2217 },
2218 cst::Literal::Str(s) => {
2219 let maybe_str = s.as_valid_string();
2220 maybe_str.map(|lit| ExprOrSpecial::StrLit {
2221 lit,
2222 loc: self.loc.clone(),
2223 })
2224 }
2225 }
2226 }
2227}
2228
2229impl Node<Option<cst::RecInit>> {
2230 fn to_init<Build: ExprBuilderInfallibleBuild>(&self) -> Result<(SmolStr, Build::Expr)> {
2231 let lit = self.try_as_inner()?;
2232
2233 let maybe_attr = lit.0.to_expr_or_special::<Build>()?.into_valid_attr();
2234 let maybe_value = lit.1.to_expr::<Build>();
2235
2236 flatten_tuple_2(maybe_attr, maybe_value)
2237 }
2238}
2239
2240#[expect(
2243 clippy::too_many_arguments,
2244 reason = "policies just have this many components"
2245)]
2246fn construct_template_policy(
2247 id: ast::PolicyID,
2248 annotations: ast::Annotations,
2249 effect: ast::Effect,
2250 principal: ast::PrincipalConstraint,
2251 action: ast::ActionConstraint,
2252 resource: ast::ResourceConstraint,
2253 conds: Vec<ast::Expr>,
2254 loc: Option<&Loc>,
2255) -> ast::Template {
2256 let construct_template = |non_scope_constraint| {
2257 ast::Template::new(
2258 id,
2259 loc.cloned(),
2260 annotations,
2261 effect,
2262 principal,
2263 action,
2264 resource,
2265 non_scope_constraint,
2266 )
2267 };
2268
2269 let mut conds_rev_iter = conds.into_iter().rev();
2272 if let Some(last_expr) = conds_rev_iter.next() {
2273 let builder = ast::ExprBuilder::new().with_maybe_source_loc(loc);
2274 construct_template(Some(
2275 conds_rev_iter.fold(last_expr, |acc, prev| builder.clone().and(prev, acc)),
2276 ))
2277 } else {
2278 construct_template(None)
2279 }
2280}
2281fn construct_string_from_var(v: ast::Var) -> SmolStr {
2282 match v {
2283 ast::Var::Principal => "principal".into(),
2284 ast::Var::Action => "action".into(),
2285 ast::Var::Resource => "resource".into(),
2286 ast::Var::Context => "context".into(),
2287 }
2288}
2289fn construct_name(path: Vec<ast::Id>, id: ast::Id, loc: Option<Loc>) -> ast::InternalName {
2290 ast::InternalName {
2291 id,
2292 path: Arc::new(path),
2293 loc,
2294 }
2295}
2296
2297fn construct_expr_rel<Build: ExprBuilderInfallibleBuild>(
2298 f: Build::Expr,
2299 rel: cst::RelOp,
2300 s: Build::Expr,
2301 loc: Option<Loc>,
2302) -> Result<Build::Expr> {
2303 let builder = Build::new().with_maybe_source_loc(loc.as_ref());
2304 match rel {
2305 cst::RelOp::Less => Ok(builder.less(f, s)),
2306 cst::RelOp::LessEq => Ok(builder.lesseq(f, s)),
2307 cst::RelOp::GreaterEq => Ok(builder.greatereq(f, s)),
2308 cst::RelOp::Greater => Ok(builder.greater(f, s)),
2309 cst::RelOp::NotEq => Ok(builder.noteq(f, s)),
2310 cst::RelOp::Eq => Ok(builder.is_eq(f, s)),
2311 cst::RelOp::In => Ok(builder.is_in(f, s)),
2312 cst::RelOp::InvalidSingleEq => {
2313 Err(ToASTError::new(ToASTErrorKind::InvalidSingleEq, loc).into())
2314 }
2315 }
2316}
2317
2318#[expect(
2319 clippy::panic,
2320 clippy::indexing_slicing,
2321 clippy::cognitive_complexity,
2322 reason = "Unit Test Code"
2323)]
2324#[cfg(test)]
2325mod tests {
2326 use super::*;
2327 use crate::{
2328 ast::{EntityUID, Expr},
2329 parser::{err::ParseErrors, test_utils::*, *},
2330 test_utils::*,
2331 };
2332 use ast::{InternalName, ReservedNameError};
2333 use cool_asserts::assert_matches;
2334
2335 #[track_caller]
2336 fn assert_parse_expr_succeeds(text: &str) -> Expr {
2337 text_to_cst::parse_expr(text)
2338 .expect("failed parser")
2339 .to_expr::<ast::ExprBuilder<()>>()
2340 .unwrap_or_else(|errs| {
2341 panic!("failed conversion to AST:\n{:?}", miette::Report::new(errs))
2342 })
2343 }
2344
2345 #[track_caller]
2346 fn assert_parse_expr_fails(text: &str) -> ParseErrors {
2347 let result = text_to_cst::parse_expr(text)
2348 .expect("failed parser")
2349 .to_expr::<ast::ExprBuilder<()>>();
2350 match result {
2351 Ok(expr) => {
2352 panic!("conversion to AST should have failed, but succeeded with:\n{expr}")
2353 }
2354 Err(errs) => errs,
2355 }
2356 }
2357
2358 #[track_caller]
2359 fn assert_parse_policy_succeeds(text: &str) -> ast::StaticPolicy {
2360 text_to_cst::parse_policy(text)
2361 .expect("failed parser")
2362 .to_policy(ast::PolicyID::from_string("id"))
2363 .unwrap_or_else(|errs| {
2364 panic!("failed conversion to AST:\n{:?}", miette::Report::new(errs))
2365 })
2366 }
2367
2368 #[track_caller]
2369 fn assert_parse_policy_fails(text: &str) -> ParseErrors {
2370 let result = text_to_cst::parse_policy(text)
2371 .expect("failed parser")
2372 .to_policy(ast::PolicyID::from_string("id"));
2373 match result {
2374 Ok(policy) => {
2375 panic!("conversion to AST should have failed, but succeeded with:\n{policy}")
2376 }
2377 Err(errs) => errs,
2378 }
2379 }
2380
2381 #[test]
2382 fn show_expr1() {
2383 assert_parse_expr_succeeds(
2384 r#"
2385 if 7 then 6 > 5 else !5 || "thursday" && ((8) >= "fish")
2386 "#,
2387 );
2388 }
2389
2390 #[test]
2391 fn show_expr2() {
2392 assert_parse_expr_succeeds(
2393 r#"
2394 [2,3,4].foo["hello"]
2395 "#,
2396 );
2397 }
2398
2399 #[test]
2400 fn show_expr3() {
2401 let expr = assert_parse_expr_succeeds(
2403 r#"
2404 "first".some_ident
2405 "#,
2406 );
2407 assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2408 assert_eq!(attr, "some_ident");
2409 });
2410 }
2411
2412 #[test]
2413 fn show_expr4() {
2414 let expr = assert_parse_expr_succeeds(
2415 r#"
2416 1.some_ident
2417 "#,
2418 );
2419 assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2420 assert_eq!(attr, "some_ident");
2421 });
2422 }
2423
2424 #[test]
2425 fn show_expr5() {
2426 let expr = assert_parse_expr_succeeds(
2427 r#"
2428 "first"["some string"]
2429 "#,
2430 );
2431 assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2432 assert_eq!(attr, "some string");
2433 });
2434 }
2435
2436 #[test]
2437 fn show_expr6() {
2438 let expr = assert_parse_expr_succeeds(
2439 r#"
2440 {"one":1,"two":2} has one
2441 "#,
2442 );
2443 assert_matches!(expr.expr_kind(), ast::ExprKind::HasAttr { attr, .. } => {
2444 assert_eq!(attr, "one");
2445 });
2446 }
2447
2448 #[test]
2449 fn show_expr7() {
2450 let expr = assert_parse_expr_succeeds(
2451 r#"
2452 {"one":1,"two":2}.one
2453 "#,
2454 );
2455 assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2456 assert_eq!(attr, "one");
2457 });
2458 }
2459
2460 #[test]
2461 fn show_expr8() {
2462 let expr = assert_parse_expr_succeeds(
2464 r#"
2465 {"one":1,"two":2}["one"]
2466 "#,
2467 );
2468 assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2469 assert_eq!(attr, "one");
2470 });
2471 }
2472
2473 #[test]
2474 fn show_expr9() {
2475 let expr = assert_parse_expr_succeeds(
2477 r#"
2478 {"this is a valid map key+.-_%()":1,"two":2}["this is a valid map key+.-_%()"]
2479 "#,
2480 );
2481 assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
2482 assert_eq!(attr, "this is a valid map key+.-_%()");
2483 });
2484 }
2485
2486 #[test]
2487 fn show_expr10() {
2488 let src = r#"
2489 {if true then a else b:"b"} ||
2490 {if false then a else b:"b"}
2491 "#;
2492 let errs = assert_parse_expr_fails(src);
2493 expect_n_errors(src, &errs, 4);
2494 expect_some_error_matches(
2495 src,
2496 &errs,
2497 &ExpectedErrorMessageBuilder::error("invalid variable: a")
2498 .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `a` in quotes to make a string?")
2499 .exactly_one_underline("a")
2500 .build(),
2501 );
2502 expect_some_error_matches(
2503 src,
2504 &errs,
2505 &ExpectedErrorMessageBuilder::error("invalid variable: b")
2506 .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `b` in quotes to make a string?")
2507 .exactly_one_underline("b")
2508 .build(),
2509 );
2510 }
2511
2512 #[test]
2513 fn show_expr11() {
2514 let expr = assert_parse_expr_succeeds(
2515 r#"
2516 {principal:"principal"}
2517 "#,
2518 );
2519 assert_matches!(expr.expr_kind(), ast::ExprKind::Record { .. });
2520 }
2521
2522 #[test]
2523 fn show_expr12() {
2524 let expr = assert_parse_expr_succeeds(
2525 r#"
2526 {"principal":"principal"}
2527 "#,
2528 );
2529 assert_matches!(expr.expr_kind(), ast::ExprKind::Record { .. });
2530 }
2531
2532 #[test]
2533 fn reserved_idents1() {
2534 let src = r#"
2535 The::true::path::to::"enlightenment".false
2536 "#;
2537 let errs = assert_parse_expr_fails(src);
2538 expect_n_errors(src, &errs, 2);
2539 expect_some_error_matches(
2540 src,
2541 &errs,
2542 &ExpectedErrorMessageBuilder::error(
2543 "this identifier is reserved and cannot be used: true",
2544 )
2545 .exactly_one_underline("true")
2546 .build(),
2547 );
2548 expect_some_error_matches(
2549 src,
2550 &errs,
2551 &ExpectedErrorMessageBuilder::error(
2552 "this identifier is reserved and cannot be used: false",
2553 )
2554 .exactly_one_underline("false")
2555 .build(),
2556 );
2557 }
2558
2559 #[test]
2560 fn reserved_idents2() {
2561 let src = r#"
2562 if {if: true}.if then {"if":false}["if"] else {when:true}.permit
2563 "#;
2564 let errs = assert_parse_expr_fails(src);
2565 expect_n_errors(src, &errs, 2);
2566 expect_some_error_matches(
2567 src,
2568 &errs,
2569 &ExpectedErrorMessageBuilder::error(
2570 "this identifier is reserved and cannot be used: if",
2571 )
2572 .exactly_one_underline("if: true")
2573 .build(),
2574 );
2575 expect_some_error_matches(
2576 src,
2577 &errs,
2578 &ExpectedErrorMessageBuilder::error(
2579 "this identifier is reserved and cannot be used: if",
2580 )
2581 .exactly_one_underline("if")
2582 .build(),
2583 );
2584 }
2585
2586 #[test]
2587 fn reserved_idents3() {
2588 let src = r#"
2589 if {where: true}.like || {has:false}.in then {"like":false}["in"] else {then:true}.else
2590 "#;
2591 let errs = assert_parse_expr_fails(src);
2592 expect_n_errors(src, &errs, 5);
2593 expect_some_error_matches(
2594 src,
2595 &errs,
2596 &ExpectedErrorMessageBuilder::error(
2597 "this identifier is reserved and cannot be used: has",
2598 )
2599 .exactly_one_underline("has")
2600 .build(),
2601 );
2602 expect_some_error_matches(
2603 src,
2604 &errs,
2605 &ExpectedErrorMessageBuilder::error(
2606 "this identifier is reserved and cannot be used: like",
2607 )
2608 .exactly_one_underline("like")
2609 .build(),
2610 );
2611 expect_some_error_matches(
2612 src,
2613 &errs,
2614 &ExpectedErrorMessageBuilder::error(
2615 "this identifier is reserved and cannot be used: in",
2616 )
2617 .exactly_one_underline("in")
2618 .build(),
2619 );
2620 expect_some_error_matches(
2621 src,
2622 &errs,
2623 &ExpectedErrorMessageBuilder::error(
2624 "this identifier is reserved and cannot be used: then",
2625 )
2626 .exactly_one_underline("then")
2627 .build(),
2628 );
2629 expect_some_error_matches(
2630 src,
2631 &errs,
2632 &ExpectedErrorMessageBuilder::error(
2633 "this identifier is reserved and cannot be used: else",
2634 )
2635 .exactly_one_underline("else")
2636 .build(),
2637 );
2638 }
2639
2640 #[test]
2641 fn show_policy1() {
2642 let src = r#"
2643 permit(principal:p,action:a,resource:r)when{w}unless{u}advice{"doit"};
2644 "#;
2645 let errs = assert_parse_policy_fails(src);
2646 expect_n_errors(src, &errs, 6);
2647 expect_some_error_matches(
2648 src,
2649 &errs,
2650 &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
2651 .help("try using `is` instead")
2652 .exactly_one_underline("p")
2653 .build(),
2654 );
2655 expect_some_error_matches(
2656 src,
2657 &errs,
2658 &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
2659 .help("try using `is` instead")
2660 .exactly_one_underline("a")
2661 .build(),
2662 );
2663 expect_some_error_matches(
2664 src,
2665 &errs,
2666 &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
2667 .help("try using `is` instead")
2668 .exactly_one_underline("r")
2669 .build(),
2670 );
2671 expect_some_error_matches(
2672 src,
2673 &errs,
2674 &ExpectedErrorMessageBuilder::error("invalid variable: w")
2675 .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `w` in quotes to make a string?")
2676 .exactly_one_underline("w")
2677 .build(),
2678 );
2679 expect_some_error_matches(
2680 src,
2681 &errs,
2682 &ExpectedErrorMessageBuilder::error("invalid variable: u")
2683 .help("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `u` in quotes to make a string?")
2684 .exactly_one_underline("u")
2685 .build(),
2686 );
2687 expect_some_error_matches(
2688 src,
2689 &errs,
2690 &ExpectedErrorMessageBuilder::error("invalid policy condition: advice")
2691 .help("condition must be either `when` or `unless`")
2692 .exactly_one_underline("advice")
2693 .build(),
2694 );
2695 }
2696
2697 #[test]
2698 fn show_policy2() {
2699 let src = r#"
2700 permit(principal,action,resource)when{true};
2701 "#;
2702 assert_parse_policy_succeeds(src);
2703 }
2704
2705 #[test]
2706 fn show_policy3() {
2707 let src = r#"
2708 permit(principal in User::"jane",action,resource);
2709 "#;
2710 assert_parse_policy_succeeds(src);
2711 }
2712
2713 #[test]
2714 fn show_policy4() {
2715 let src = r#"
2716 forbid(principal in User::"jane",action,resource)unless{
2717 context.group != "friends"
2718 };
2719 "#;
2720 assert_parse_policy_succeeds(src);
2721 }
2722
2723 #[test]
2724 fn single_annotation() {
2725 let policy = assert_parse_policy_succeeds(
2727 r#"
2728 @anno("good annotation")permit(principal,action,resource);
2729 "#,
2730 );
2731 assert_matches!(
2732 policy.annotation(&ast::AnyId::new_unchecked("anno")),
2733 Some(annotation) => assert_eq!(annotation.as_ref(), "good annotation")
2734 );
2735 }
2736
2737 #[test]
2738 fn duplicate_annotations_error() {
2739 let src = r#"
2741 @anno("good annotation")
2742 @anno2("good annotation")
2743 @anno("oops, duplicate")
2744 permit(principal,action,resource);
2745 "#;
2746 let errs = assert_parse_policy_fails(src);
2747 expect_n_errors(src, &errs, 1);
2749 expect_some_error_matches(
2750 src,
2751 &errs,
2752 &ExpectedErrorMessageBuilder::error("duplicate annotation: @anno")
2753 .exactly_one_underline("@anno(\"oops, duplicate\")")
2754 .build(),
2755 );
2756 }
2757
2758 #[test]
2759 fn multiple_policys_and_annotations_ok() {
2760 let policyset = text_to_cst::parse_policies(
2762 r#"
2763 @anno1("first")
2764 permit(principal,action,resource);
2765
2766 @anno2("second")
2767 permit(principal,action,resource);
2768
2769 @anno3a("third-a")
2770 @anno3b("third-b")
2771 permit(principal,action,resource);
2772 "#,
2773 )
2774 .expect("should parse")
2775 .to_policyset()
2776 .unwrap_or_else(|errs| panic!("failed convert to AST:\n{:?}", miette::Report::new(errs)));
2777 assert_matches!(
2778 policyset
2779 .get(&ast::PolicyID::from_string("policy0"))
2780 .expect("should be a policy")
2781 .annotation(&ast::AnyId::new_unchecked("anno0")),
2782 None
2783 );
2784 assert_matches!(
2785 policyset
2786 .get(&ast::PolicyID::from_string("policy0"))
2787 .expect("should be a policy")
2788 .annotation(&ast::AnyId::new_unchecked("anno1")),
2789 Some(annotation) => assert_eq!(annotation.as_ref(), "first")
2790 );
2791 assert_matches!(
2792 policyset
2793 .get(&ast::PolicyID::from_string("policy1"))
2794 .expect("should be a policy")
2795 .annotation(&ast::AnyId::new_unchecked("anno2")),
2796 Some(annotation) => assert_eq!(annotation.as_ref(), "second")
2797 );
2798 assert_matches!(
2799 policyset
2800 .get(&ast::PolicyID::from_string("policy2"))
2801 .expect("should be a policy")
2802 .annotation(&ast::AnyId::new_unchecked("anno3a")),
2803 Some(annotation) => assert_eq!(annotation.as_ref(), "third-a")
2804 );
2805 assert_matches!(
2806 policyset
2807 .get(&ast::PolicyID::from_string("policy2"))
2808 .expect("should be a policy")
2809 .annotation(&ast::AnyId::new_unchecked("anno3b")),
2810 Some(annotation) => assert_eq!(annotation.as_ref(), "third-b")
2811 );
2812 assert_matches!(
2813 policyset
2814 .get(&ast::PolicyID::from_string("policy2"))
2815 .expect("should be a policy")
2816 .annotation(&ast::AnyId::new_unchecked("anno3c")),
2817 None
2818 );
2819 assert_eq!(
2820 policyset
2821 .get(&ast::PolicyID::from_string("policy2"))
2822 .expect("should be a policy")
2823 .annotations()
2824 .count(),
2825 2
2826 );
2827 }
2828
2829 #[test]
2830 fn reserved_word_annotations_ok() {
2831 let policyset = text_to_cst::parse_policies(
2833 r#"
2834 @if("this is the annotation for `if`")
2835 @then("this is the annotation for `then`")
2836 @else("this is the annotation for `else`")
2837 @true("this is the annotation for `true`")
2838 @false("this is the annotation for `false`")
2839 @in("this is the annotation for `in`")
2840 @is("this is the annotation for `is`")
2841 @like("this is the annotation for `like`")
2842 @has("this is the annotation for `has`")
2843 @principal("this is the annotation for `principal`") // not reserved at time of this writing, but we test it anyway
2844 permit(principal, action, resource);
2845 "#,
2846 ).expect("should parse")
2847 .to_policyset()
2848 .unwrap_or_else(|errs| panic!("failed convert to AST:\n{:?}", miette::Report::new(errs)));
2849 let policy0 = policyset
2850 .get(&ast::PolicyID::from_string("policy0"))
2851 .expect("should be the right policy ID");
2852 assert_matches!(
2853 policy0.annotation(&ast::AnyId::new_unchecked("if")),
2854 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `if`")
2855 );
2856 assert_matches!(
2857 policy0.annotation(&ast::AnyId::new_unchecked("then")),
2858 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `then`")
2859 );
2860 assert_matches!(
2861 policy0.annotation(&ast::AnyId::new_unchecked("else")),
2862 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `else`")
2863 );
2864 assert_matches!(
2865 policy0.annotation(&ast::AnyId::new_unchecked("true")),
2866 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `true`")
2867 );
2868 assert_matches!(
2869 policy0.annotation(&ast::AnyId::new_unchecked("false")),
2870 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `false`")
2871 );
2872 assert_matches!(
2873 policy0.annotation(&ast::AnyId::new_unchecked("in")),
2874 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `in`")
2875 );
2876 assert_matches!(
2877 policy0.annotation(&ast::AnyId::new_unchecked("is")),
2878 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `is`")
2879 );
2880 assert_matches!(
2881 policy0.annotation(&ast::AnyId::new_unchecked("like")),
2882 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `like`")
2883 );
2884 assert_matches!(
2885 policy0.annotation(&ast::AnyId::new_unchecked("has")),
2886 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `has`")
2887 );
2888 assert_matches!(
2889 policy0.annotation(&ast::AnyId::new_unchecked("principal")),
2890 Some(annotation) => assert_eq!(annotation.as_ref(), "this is the annotation for `principal`")
2891 );
2892 }
2893
2894 #[test]
2895 fn single_annotation_without_value() {
2896 let policy = assert_parse_policy_succeeds(r#"@anno permit(principal,action,resource);"#);
2897 assert_matches!(
2898 policy.annotation(&ast::AnyId::new_unchecked("anno")),
2899 Some(annotation) => assert_eq!(annotation.as_ref(), ""),
2900 );
2901 }
2902
2903 #[test]
2904 fn duplicate_annotations_without_value() {
2905 let src = "@anno @anno permit(principal,action,resource);";
2906 let errs = assert_parse_policy_fails(src);
2907 expect_n_errors(src, &errs, 1);
2908 expect_some_error_matches(
2909 src,
2910 &errs,
2911 &ExpectedErrorMessageBuilder::error("duplicate annotation: @anno")
2912 .exactly_one_underline("@anno")
2913 .build(),
2914 );
2915 }
2916
2917 #[test]
2918 fn multiple_annotation_without_value() {
2919 let policy =
2920 assert_parse_policy_succeeds(r#"@foo @bar permit(principal,action,resource);"#);
2921 assert_matches!(
2922 policy.annotation(&ast::AnyId::new_unchecked("foo")),
2923 Some(annotation) => assert_eq!(annotation.as_ref(), ""),
2924 );
2925 assert_matches!(
2926 policy.annotation(&ast::AnyId::new_unchecked("bar")),
2927 Some(annotation) => assert_eq!(annotation.as_ref(), ""),
2928 );
2929 }
2930
2931 #[test]
2932 fn fail_scope1() {
2933 let src = r#"
2934 permit(
2935 principal in [User::"jane",Group::"friends"],
2936 action,
2937 resource
2938 );
2939 "#;
2940 let errs = assert_parse_policy_fails(src);
2941 expect_n_errors(src, &errs, 1);
2942 expect_some_error_matches(
2943 src,
2944 &errs,
2945 &ExpectedErrorMessageBuilder::error(
2946 "expected single entity uid or template slot, found set of entity uids",
2947 )
2948 .exactly_one_underline(r#"[User::"jane",Group::"friends"]"#)
2949 .build(),
2950 );
2951 }
2952
2953 #[test]
2954 fn fail_scope2() {
2955 let src = r#"
2956 permit(
2957 principal in User::"jane",
2958 action == if true then Photo::"view" else Photo::"edit",
2959 resource
2960 );
2961 "#;
2962 let errs = assert_parse_policy_fails(src);
2963 expect_n_errors(src, &errs, 1);
2964 expect_some_error_matches(
2965 src,
2966 &errs,
2967 &ExpectedErrorMessageBuilder::error("expected an entity uid, found an `if` expression")
2968 .exactly_one_underline(r#"if true then Photo::"view" else Photo::"edit""#)
2969 .build(),
2970 );
2971 }
2972
2973 #[test]
2974 fn fail_scope3() {
2975 let src = r#"
2976 permit(principal,action,resource,context);
2977 "#;
2978 let errs = assert_parse_policy_fails(src);
2979 expect_n_errors(src, &errs, 1);
2980 expect_some_error_matches(
2981 src,
2982 &errs,
2983 &ExpectedErrorMessageBuilder::error(
2984 "this policy has an extra element in the scope: context",
2985 )
2986 .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
2987 .exactly_one_underline("context")
2988 .build(),
2989 );
2990 }
2991
2992 #[test]
2993 fn method_call2() {
2994 assert_parse_expr_succeeds(
2995 r#"
2996 principal.contains(resource)
2997 "#,
2998 );
2999
3000 let src = r#"
3001 contains(principal,resource)
3002 "#;
3003 let errs = assert_parse_expr_fails(src);
3004 expect_n_errors(src, &errs, 1);
3005 expect_some_error_matches(
3006 src,
3007 &errs,
3008 &ExpectedErrorMessageBuilder::error("`contains` is a method, not a function")
3009 .help("use a method-style call `e.contains(..)`")
3010 .exactly_one_underline("contains(principal,resource)")
3011 .build(),
3012 );
3013 }
3014
3015 #[test]
3016 fn construct_record_1() {
3017 let e = assert_parse_expr_succeeds(
3018 r#"
3019 {one:"one"}
3020 "#,
3021 );
3022 assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3024 println!("{e}");
3025 }
3026
3027 #[test]
3028 fn construct_record_2() {
3029 let e = assert_parse_expr_succeeds(
3030 r#"
3031 {"one":"one"}
3032 "#,
3033 );
3034 assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3036 println!("{e}");
3037 }
3038
3039 #[test]
3040 fn construct_record_3() {
3041 let e = assert_parse_expr_succeeds(
3042 r#"
3043 {"one":"one",two:"two"}
3044 "#,
3045 );
3046 assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3048 println!("{e}");
3049 }
3050
3051 #[test]
3052 fn construct_record_4() {
3053 let e = assert_parse_expr_succeeds(
3054 r#"
3055 {one:"one","two":"two"}
3056 "#,
3057 );
3058 assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3060 println!("{e}");
3061 }
3062
3063 #[test]
3064 fn construct_record_5() {
3065 let e = assert_parse_expr_succeeds(
3066 r#"
3067 {one:"b\"","b\"":2}
3068 "#,
3069 );
3070 assert_matches!(e.expr_kind(), ast::ExprKind::Record { .. });
3072 println!("{e}");
3073 }
3074
3075 #[test]
3076 fn construct_invalid_get_1() {
3077 let src = r#"
3078 {"one":1, "two":"two"}[0]
3079 "#;
3080 let errs = assert_parse_expr_fails(src);
3081 expect_n_errors(src, &errs, 1);
3082 expect_some_error_matches(
3083 src,
3084 &errs,
3085 &ExpectedErrorMessageBuilder::error("invalid string literal: 0")
3086 .exactly_one_underline("0")
3087 .build(),
3088 );
3089 }
3090
3091 #[test]
3092 fn construct_invalid_get_2() {
3093 let src = r#"
3094 {"one":1, "two":"two"}[-1]
3095 "#;
3096 let errs = assert_parse_expr_fails(src);
3097 expect_n_errors(src, &errs, 1);
3098 expect_some_error_matches(
3099 src,
3100 &errs,
3101 &ExpectedErrorMessageBuilder::error("invalid string literal: (-1)")
3102 .exactly_one_underline("-1")
3103 .build(),
3104 );
3105 }
3106
3107 #[test]
3108 fn construct_invalid_get_3() {
3109 let src = r#"
3110 {"one":1, "two":"two"}[true]
3111 "#;
3112 let errs = assert_parse_expr_fails(src);
3113 expect_n_errors(src, &errs, 1);
3114 expect_some_error_matches(
3115 src,
3116 &errs,
3117 &ExpectedErrorMessageBuilder::error("invalid string literal: true")
3118 .exactly_one_underline("true")
3119 .build(),
3120 );
3121 }
3122
3123 #[test]
3124 fn construct_invalid_get_4() {
3125 let src = r#"
3126 {"one":1, "two":"two"}[one]
3127 "#;
3128 let errs = assert_parse_expr_fails(src);
3129 expect_n_errors(src, &errs, 1);
3130 expect_some_error_matches(
3131 src,
3132 &errs,
3133 &ExpectedErrorMessageBuilder::error("invalid string literal: one")
3134 .exactly_one_underline("one")
3135 .build(),
3136 );
3137 }
3138
3139 #[test]
3140 fn construct_invalid_get_var() {
3141 let src = r#"
3142 {"principal":1, "two":"two"}[principal]
3143 "#;
3144 let errs = assert_parse_expr_fails(src);
3145 expect_n_errors(src, &errs, 1);
3146 expect_some_error_matches(
3147 src,
3148 &errs,
3149 &ExpectedErrorMessageBuilder::error("invalid string literal: principal")
3150 .exactly_one_underline("principal")
3151 .build(),
3152 );
3153 }
3154
3155 #[test]
3156 fn construct_has_1() {
3157 let expr = assert_parse_expr_succeeds(
3158 r#"
3159 {"one":1,"two":2} has "arbitrary+ _string"
3160 "#,
3161 );
3162 assert_matches!(expr.expr_kind(), ast::ExprKind::HasAttr { attr, .. } => {
3163 assert_eq!(attr, "arbitrary+ _string");
3164 });
3165 }
3166
3167 #[test]
3168 fn construct_has_2() {
3169 let src = r#"
3170 {"one":1,"two":2} has 1
3171 "#;
3172 let errs = assert_parse_expr_fails(src);
3173 expect_n_errors(src, &errs, 1);
3174 expect_some_error_matches(
3175 src,
3176 &errs,
3177 &ExpectedErrorMessageBuilder::error("invalid RHS of a `has` operation: 1")
3178 .help("valid RHS of a `has` operation is either a sequence of identifiers separated by `.` or a string literal")
3179 .exactly_one_underline("1")
3180 .build(),
3181 );
3182 }
3183
3184 #[test]
3185 fn construct_like_1() {
3186 let expr = assert_parse_expr_succeeds(
3187 r#"
3188 "354 hams" like "*5*"
3189 "#,
3190 );
3191 assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3192 assert_eq!(pattern.to_string(), "*5*");
3193 });
3194 }
3195
3196 #[test]
3197 fn construct_like_2() {
3198 let src = r#"
3199 "354 hams" like 354
3200 "#;
3201 let errs = assert_parse_expr_fails(src);
3202 expect_n_errors(src, &errs, 1);
3203 expect_some_error_matches(
3204 src,
3205 &errs,
3206 &ExpectedErrorMessageBuilder::error(
3207 "right hand side of a `like` expression must be a pattern literal, but got `354`",
3208 )
3209 .exactly_one_underline("354")
3210 .build(),
3211 );
3212 }
3213
3214 #[test]
3215 fn construct_like_3() {
3216 let expr = assert_parse_expr_succeeds(
3217 r#"
3218 "string\\with\\backslashes" like "string\\with\\backslashes"
3219 "#,
3220 );
3221 assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3222 assert_eq!(pattern.to_string(), r"string\\with\\backslashes");
3223 });
3224 }
3225
3226 #[test]
3227 fn construct_like_4() {
3228 let expr = assert_parse_expr_succeeds(
3229 r#"
3230 "string\\with\\backslashes" like "string\*with\*backslashes"
3231 "#,
3232 );
3233 assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3234 assert_eq!(pattern.to_string(), r"string\*with\*backslashes");
3235 });
3236 }
3237
3238 #[test]
3239 fn construct_like_5() {
3240 let src = r#"
3241 "string\*with\*escaped\*stars" like "string\*with\*escaped\*stars"
3242 "#;
3243 let errs = assert_parse_expr_fails(src);
3244 expect_n_errors(src, &errs, 3);
3245 expect_some_error_matches(
3247 src,
3248 &errs,
3249 &ExpectedErrorMessageBuilder::error("the input `\\*` is not a valid escape")
3250 .exactly_one_underline(r#""string\*with\*escaped\*stars""#)
3251 .build(),
3252 );
3253 }
3254
3255 #[test]
3256 fn construct_like_6() {
3257 let expr = assert_parse_expr_succeeds(
3258 r#"
3259 "string*with*stars" like "string\*with\*stars"
3260 "#,
3261 );
3262 assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3263 assert_eq!(pattern.to_string(), "string\\*with\\*stars");
3264 });
3265 }
3266
3267 #[test]
3268 fn construct_like_7() {
3269 let expr = assert_parse_expr_succeeds(
3270 r#"
3271 "string\\*with\\*backslashes\\*and\\*stars" like "string\\\*with\\\*backslashes\\\*and\\\*stars"
3272 "#,
3273 );
3274 assert_matches!(expr.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3275 assert_eq!(
3276 pattern.to_string(),
3277 r"string\\\*with\\\*backslashes\\\*and\\\*stars"
3278 );
3279 });
3280 }
3281
3282 #[test]
3283 fn construct_like_var() {
3284 let src = r#"
3285 "principal" like principal
3286 "#;
3287 let errs = assert_parse_expr_fails(src);
3288 expect_n_errors(src, &errs, 1);
3289 expect_some_error_matches(
3290 src,
3291 &errs,
3292 &ExpectedErrorMessageBuilder::error(
3293 "right hand side of a `like` expression must be a pattern literal, but got `principal`",
3294 )
3295 .exactly_one_underline("principal")
3296 .build(),
3297 );
3298 }
3299
3300 #[test]
3301 fn construct_like_name() {
3302 let src = r#"
3303 "foo::bar::baz" like foo::bar
3304 "#;
3305 let errs = assert_parse_expr_fails(src);
3306 expect_n_errors(src, &errs, 1);
3307 expect_some_error_matches(
3308 src,
3309 &errs,
3310 &ExpectedErrorMessageBuilder::error(
3311 "right hand side of a `like` expression must be a pattern literal, but got `foo::bar`",
3312 )
3313 .exactly_one_underline("foo::bar")
3314 .build(),
3315 );
3316 }
3317
3318 #[test]
3319 fn pattern_roundtrip() {
3320 let test_pattern = ast::Pattern::from(vec![
3321 PatternElem::Char('h'),
3322 PatternElem::Char('e'),
3323 PatternElem::Char('l'),
3324 PatternElem::Char('l'),
3325 PatternElem::Char('o'),
3326 PatternElem::Char('\\'),
3327 PatternElem::Char('0'),
3328 PatternElem::Char('*'),
3329 PatternElem::Char('\\'),
3330 PatternElem::Char('*'),
3331 ]);
3332 let e1 = ast::Expr::like(ast::Expr::val("hello"), test_pattern.clone());
3333 let s1 = format!("{e1}");
3334 assert_eq!(s1, r#""hello" like "hello\\0\*\\\*""#);
3336 let e2 = assert_parse_expr_succeeds(&s1);
3337 assert_matches!(e2.expr_kind(), ast::ExprKind::Like { pattern, .. } => {
3338 assert_eq!(pattern.get_elems(), test_pattern.get_elems());
3339 });
3340 let s2 = format!("{e2}");
3341 assert_eq!(s1, s2);
3342 }
3343
3344 #[test]
3345 fn issue_wf_5046() {
3346 let policy = parse_policy(
3347 Some(ast::PolicyID::from_string("WF-5046")),
3348 r#"permit(
3349 principal,
3350 action in [Action::"action"],
3351 resource in G::""
3352 ) when {
3353 true && ("" like "/gisterNatives\\*D")
3354 };"#,
3355 );
3356 assert!(policy.is_ok());
3357 }
3358
3359 #[test]
3360 fn entity_access() {
3361 let expr = assert_parse_expr_succeeds(
3365 r#"
3366 User::"jane" has age
3367 "#,
3368 );
3369 assert_matches!(expr.expr_kind(), ast::ExprKind::HasAttr { attr, .. } => {
3370 assert_eq!(attr, "age");
3371 });
3372
3373 let expr = assert_parse_expr_succeeds(
3375 r#"
3376 User::"jane" has "arbitrary+ _string"
3377 "#,
3378 );
3379 assert_matches!(expr.expr_kind(), ast::ExprKind::HasAttr { attr, .. } => {
3380 assert_eq!(attr, "arbitrary+ _string");
3381 });
3382
3383 let src = r#"
3385 User::"jane" has 1
3386 "#;
3387 let errs = assert_parse_expr_fails(src);
3388 expect_n_errors(src, &errs, 1);
3389 expect_some_error_matches(
3390 src,
3391 &errs,
3392 &ExpectedErrorMessageBuilder::error("invalid RHS of a `has` operation: 1")
3393 .help("valid RHS of a `has` operation is either a sequence of identifiers separated by `.` or a string literal")
3394 .exactly_one_underline("1")
3395 .build(),
3396 );
3397
3398 let expr = assert_parse_expr_succeeds(
3400 r#"
3401 User::"jane".age
3402 "#,
3403 );
3404 assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
3405 assert_eq!(attr, "age");
3406 });
3407
3408 let expr: ast::Expr = assert_parse_expr_succeeds(
3410 r#"
3411 User::"jane"["arbitrary+ _string"]
3412 "#,
3413 );
3414 assert_matches!(expr.expr_kind(), ast::ExprKind::GetAttr { attr, .. } => {
3415 assert_eq!(attr, "arbitrary+ _string");
3416 });
3417
3418 let src = r#"
3420 User::"jane"[age]
3421 "#;
3422 let errs = assert_parse_expr_fails(src);
3423 expect_n_errors(src, &errs, 1);
3424 expect_some_error_matches(
3425 src,
3426 &errs,
3427 &ExpectedErrorMessageBuilder::error("invalid string literal: age")
3428 .exactly_one_underline("age")
3429 .build(),
3430 );
3431 }
3432
3433 #[test]
3434 fn relational_ops1() {
3435 let src = r#"
3436 3 >= 2 >= 1
3437 "#;
3438 let errs = assert_parse_expr_fails(src);
3439 expect_n_errors(src, &errs, 1);
3440 expect_some_error_matches(
3441 src,
3442 &errs,
3443 &ExpectedErrorMessageBuilder::error("multiple relational operators (>, ==, in, etc.) must be used with parentheses to make ordering explicit")
3444 .exactly_one_underline("3 >= 2 >= 1")
3445 .build(),
3446 );
3447 }
3448
3449 #[test]
3450 fn relational_ops2() {
3451 assert_parse_expr_succeeds(
3452 r#"
3453 3 >= ("dad" in "dad")
3454 "#,
3455 );
3456 }
3457
3458 #[test]
3459 fn relational_ops3() {
3460 assert_parse_expr_succeeds(
3461 r#"
3462 (3 >= 2) == true
3463 "#,
3464 );
3465 }
3466
3467 #[test]
3468 fn relational_ops4() {
3469 let src = r#"
3470 if 4 < 3 then 4 != 3 else 4 == 3 < 4
3471 "#;
3472 let errs = assert_parse_expr_fails(src);
3473 expect_n_errors(src, &errs, 1);
3474 expect_some_error_matches(
3475 src,
3476 &errs,
3477 &ExpectedErrorMessageBuilder::error("multiple relational operators (>, ==, in, etc.) must be used with parentheses to make ordering explicit")
3478 .exactly_one_underline("4 == 3 < 4")
3479 .build(),
3480 );
3481 }
3482
3483 #[test]
3484 fn arithmetic() {
3485 assert_parse_expr_succeeds(r#" 2 + 4 "#);
3486 assert_parse_expr_succeeds(r#" 2 + -5 "#);
3487 assert_parse_expr_succeeds(r#" 2 - 5 "#);
3488 assert_parse_expr_succeeds(r#" 2 * 5 "#);
3489 assert_parse_expr_succeeds(r#" 2 * -5 "#);
3490 assert_parse_expr_succeeds(r#" context.size * 4 "#);
3491 assert_parse_expr_succeeds(r#" 4 * context.size "#);
3492 assert_parse_expr_succeeds(r#" context.size * context.scale "#);
3493 assert_parse_expr_succeeds(r#" 5 + 10 + 90 "#);
3494 assert_parse_expr_succeeds(r#" 5 + 10 - 90 * -2 "#);
3495 assert_parse_expr_succeeds(r#" 5 + 10 * 90 - 2 "#);
3496 assert_parse_expr_succeeds(r#" 5 - 10 - 90 - 2 "#);
3497 assert_parse_expr_succeeds(r#" 5 * context.size * 10 "#);
3498 assert_parse_expr_succeeds(r#" context.size * 3 * context.scale "#);
3499 }
3500
3501 const CORRECT_TEMPLATES: [&str; 7] = [
3502 r#"permit(principal == ?principal, action == Action::"action", resource == ?resource);"#,
3503 r#"permit(principal in ?principal, action == Action::"action", resource in ?resource);"#,
3504 r#"permit(principal in ?principal, action == Action::"action", resource in ?resource);"#,
3505 r#"permit(principal in p::"principal", action == Action::"action", resource in ?resource);"#,
3506 r#"permit(principal == p::"principal", action == Action::"action", resource in ?resource);"#,
3507 r#"permit(principal in ?principal, action == Action::"action", resource in r::"resource");"#,
3508 r#"permit(principal in ?principal, action == Action::"action", resource == r::"resource");"#,
3509 ];
3510
3511 #[test]
3512 fn template_tests() {
3513 for src in CORRECT_TEMPLATES {
3514 text_to_cst::parse_policy(src)
3515 .expect("parse_error")
3516 .to_template(ast::PolicyID::from_string("i0"))
3517 .unwrap_or_else(|errs| {
3518 panic!(
3519 "Failed to create a policy template: {:?}",
3520 miette::Report::new(errs)
3521 );
3522 });
3523 }
3524 }
3525
3526 #[test]
3527 fn var_type() {
3528 assert_parse_policy_succeeds(
3529 r#"
3530 permit(principal,action,resource);
3531 "#,
3532 );
3533
3534 let src = r#"
3535 permit(principal:User,action,resource);
3536 "#;
3537 let errs = assert_parse_policy_fails(src);
3538 expect_n_errors(src, &errs, 1);
3539 expect_some_error_matches(
3540 src,
3541 &errs,
3542 &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
3543 .help("try using `is` instead")
3544 .exactly_one_underline("User")
3545 .build(),
3546 );
3547 }
3548
3549 #[test]
3550 fn unescape_err_positions() {
3551 let assert_invalid_escape = |p_src, underline| {
3552 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
3553 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("the input `\\q` is not a valid escape").exactly_one_underline(underline).build());
3554 });
3555 };
3556 assert_invalid_escape(
3557 r#"@foo("\q")permit(principal, action, resource);"#,
3558 r#"@foo("\q")"#,
3559 );
3560 assert_invalid_escape(
3561 r#"permit(principal, action, resource) when { "\q" };"#,
3562 r#""\q""#,
3563 );
3564 assert_invalid_escape(
3565 r#"permit(principal, action, resource) when { "\q".contains(0) };"#,
3566 r#""\q""#,
3567 );
3568 assert_invalid_escape(
3569 r#"permit(principal, action, resource) when { "\q".bar };"#,
3570 r#""\q""#,
3571 );
3572 assert_invalid_escape(
3573 r#"permit(principal, action, resource) when { "\q"["a"] };"#,
3574 r#""\q""#,
3575 );
3576 assert_invalid_escape(
3577 r#"permit(principal, action, resource) when { "" like "\q" };"#,
3578 r#""\q""#,
3579 );
3580 assert_invalid_escape(
3581 r#"permit(principal, action, resource) when { {}["\q"] };"#,
3582 r#""\q""#,
3583 );
3584 assert_invalid_escape(
3585 r#"permit(principal, action, resource) when { {"\q": 0} };"#,
3586 r#""\q""#,
3587 );
3588 assert_invalid_escape(
3589 r#"permit(principal, action, resource) when { User::"\q" };"#,
3590 r#"User::"\q""#,
3591 );
3592 }
3593
3594 #[track_caller] fn expect_action_error(test: &str, msg: &str, underline: &str) {
3596 assert_matches!(parse_policyset(test), Err(es) => {
3597 expect_some_error_matches(
3598 test,
3599 &es,
3600 &ExpectedErrorMessageBuilder::error(msg)
3601 .help("action entities must have type `Action`, optionally in a namespace")
3602 .exactly_one_underline(underline)
3603 .build(),
3604 );
3605 });
3606 }
3607
3608 #[test]
3609 fn action_must_be_action() {
3610 parse_policyset(r#"permit(principal, action == Action::"view", resource);"#)
3611 .expect("Valid policy failed to parse");
3612 parse_policyset(r#"permit(principal, action == Foo::Action::"view", resource);"#)
3613 .expect("Valid policy failed to parse");
3614 parse_policyset(r#"permit(principal, action in Action::"view", resource);"#)
3615 .expect("Valid policy failed to parse");
3616 parse_policyset(r#"permit(principal, action in Foo::Action::"view", resource);"#)
3617 .expect("Valid policy failed to parse");
3618 parse_policyset(r#"permit(principal, action in [Foo::Action::"view"], resource);"#)
3619 .expect("Valid policy failed to parse");
3620 parse_policyset(
3621 r#"permit(principal, action in [Foo::Action::"view", Action::"view"], resource);"#,
3622 )
3623 .expect("Valid policy failed to parse");
3624
3625 expect_action_error(
3626 r#"permit(principal, action == Foo::"view", resource);"#,
3627 "expected an entity uid with type `Action` but got `Foo::\"view\"`",
3628 "Foo::\"view\"",
3629 );
3630 expect_action_error(
3631 r#"permit(principal, action == Action::Foo::"view", resource);"#,
3632 "expected an entity uid with type `Action` but got `Action::Foo::\"view\"`",
3633 "Action::Foo::\"view\"",
3634 );
3635 expect_action_error(
3636 r#"permit(principal, action == Bar::Action::Foo::"view", resource);"#,
3637 "expected an entity uid with type `Action` but got `Bar::Action::Foo::\"view\"`",
3638 "Bar::Action::Foo::\"view\"",
3639 );
3640 expect_action_error(
3641 r#"permit(principal, action in Bar::Action::Foo::"view", resource);"#,
3642 "expected an entity uid with type `Action` but got `Bar::Action::Foo::\"view\"`",
3643 "Bar::Action::Foo::\"view\"",
3644 );
3645 expect_action_error(
3646 r#"permit(principal, action in [Bar::Action::Foo::"view"], resource);"#,
3647 "expected an entity uid with type `Action` but got `Bar::Action::Foo::\"view\"`",
3648 "[Bar::Action::Foo::\"view\"]",
3649 );
3650 expect_action_error(
3651 r#"permit(principal, action in [Bar::Action::Foo::"view", Action::"check"], resource);"#,
3652 "expected an entity uid with type `Action` but got `Bar::Action::Foo::\"view\"`",
3653 "[Bar::Action::Foo::\"view\", Action::\"check\"]",
3654 );
3655 expect_action_error(
3656 r#"permit(principal, action in [Bar::Action::Foo::"view", Foo::"delete", Action::"check"], resource);"#,
3657 "expected entity uids with type `Action` but got `Bar::Action::Foo::\"view\"` and `Foo::\"delete\"`",
3658 "[Bar::Action::Foo::\"view\", Foo::\"delete\", Action::\"check\"]",
3659 );
3660 }
3661
3662 #[test]
3663 fn method_style() {
3664 let src = r#"permit(principal, action, resource)
3665 when { contains(true) < 1 };"#;
3666 assert_matches!(parse_policyset(src), Err(e) => {
3667 expect_n_errors(src, &e, 1);
3668 expect_some_error_matches(src, &e, &ExpectedErrorMessageBuilder::error(
3669 "`contains` is a method, not a function",
3670 ).help(
3671 "use a method-style call `e.contains(..)`",
3672 ).exactly_one_underline("contains(true)").build());
3673 });
3674 }
3675
3676 #[test]
3677 fn test_mul() {
3678 for (str, expected) in [
3679 ("--2*3", Expr::mul(Expr::neg(Expr::val(-2)), Expr::val(3))),
3680 (
3681 "1 * 2 * false",
3682 Expr::mul(Expr::mul(Expr::val(1), Expr::val(2)), Expr::val(false)),
3683 ),
3684 (
3685 "0 * 1 * principal",
3686 Expr::mul(
3687 Expr::mul(Expr::val(0), Expr::val(1)),
3688 Expr::var(ast::Var::Principal),
3689 ),
3690 ),
3691 (
3692 "0 * (-1) * principal",
3693 Expr::mul(
3694 Expr::mul(Expr::val(0), Expr::val(-1)),
3695 Expr::var(ast::Var::Principal),
3696 ),
3697 ),
3698 (
3699 "0 * 6 * context.foo",
3700 Expr::mul(
3701 Expr::mul(Expr::val(0), Expr::val(6)),
3702 Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3703 ),
3704 ),
3705 (
3706 "(0 * 6) * context.foo",
3707 Expr::mul(
3708 Expr::mul(Expr::val(0), Expr::val(6)),
3709 Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3710 ),
3711 ),
3712 (
3713 "0 * (6 * context.foo)",
3714 Expr::mul(
3715 Expr::val(0),
3716 Expr::mul(
3717 Expr::val(6),
3718 Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3719 ),
3720 ),
3721 ),
3722 (
3723 "0 * (context.foo * 6)",
3724 Expr::mul(
3725 Expr::val(0),
3726 Expr::mul(
3727 Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3728 Expr::val(6),
3729 ),
3730 ),
3731 ),
3732 (
3733 "1 * 2 * 3 * context.foo * 4 * 5 * 6",
3734 Expr::mul(
3735 Expr::mul(
3736 Expr::mul(
3737 Expr::mul(
3738 Expr::mul(Expr::mul(Expr::val(1), Expr::val(2)), Expr::val(3)),
3739 Expr::get_attr(Expr::var(ast::Var::Context), "foo".into()),
3740 ),
3741 Expr::val(4),
3742 ),
3743 Expr::val(5),
3744 ),
3745 Expr::val(6),
3746 ),
3747 ),
3748 (
3749 "principal * (1 + 2)",
3750 Expr::mul(
3751 Expr::var(ast::Var::Principal),
3752 Expr::add(Expr::val(1), Expr::val(2)),
3753 ),
3754 ),
3755 (
3756 "principal * -(-1)",
3757 Expr::mul(Expr::var(ast::Var::Principal), Expr::neg(Expr::val(-1))),
3758 ),
3759 (
3760 "principal * --1",
3761 Expr::mul(Expr::var(ast::Var::Principal), Expr::neg(Expr::val(-1))),
3762 ),
3763 (
3764 r#"false * "bob""#,
3765 Expr::mul(Expr::val(false), Expr::val("bob")),
3766 ),
3767 ] {
3768 let e = assert_parse_expr_succeeds(str);
3769 assert!(
3770 e.eq_shape(&expected),
3771 "{e:?} and {expected:?} should have the same shape",
3772 );
3773 }
3774 }
3775
3776 #[test]
3777 fn test_not() {
3778 for (es, expr) in [
3779 (
3780 "!1 + 2 == 3",
3781 Expr::is_eq(
3782 Expr::add(Expr::not(Expr::val(1)), Expr::val(2)),
3783 Expr::val(3),
3784 ),
3785 ),
3786 (
3787 "!!1 + 2 == 3",
3788 Expr::is_eq(
3789 Expr::add(Expr::not(Expr::not(Expr::val(1))), Expr::val(2)),
3790 Expr::val(3),
3791 ),
3792 ),
3793 (
3794 "!!!1 + 2 == 3",
3795 Expr::is_eq(
3796 Expr::add(Expr::not(Expr::not(Expr::not(Expr::val(1)))), Expr::val(2)),
3797 Expr::val(3),
3798 ),
3799 ),
3800 (
3801 "!!!!1 + 2 == 3",
3802 Expr::is_eq(
3803 Expr::add(
3804 Expr::not(Expr::not(Expr::not(Expr::not(Expr::val(1))))),
3805 Expr::val(2),
3806 ),
3807 Expr::val(3),
3808 ),
3809 ),
3810 (
3811 "!!(-1) + 2 == 3",
3812 Expr::is_eq(
3813 Expr::add(Expr::not(Expr::not(Expr::val(-1))), Expr::val(2)),
3814 Expr::val(3),
3815 ),
3816 ),
3817 ] {
3818 let e = assert_parse_expr_succeeds(es);
3819 assert!(
3820 e.eq_shape(&expr),
3821 "{e:?} and {expr:?} should have the same shape."
3822 );
3823 }
3824 }
3825
3826 #[test]
3827 fn test_neg() {
3828 for (es, expr) in [
3829 ("-(1 + 2)", Expr::neg(Expr::add(Expr::val(1), Expr::val(2)))),
3830 ("1-(2)", Expr::sub(Expr::val(1), Expr::val(2))),
3831 ("1-2", Expr::sub(Expr::val(1), Expr::val(2))),
3832 ("(-1)", Expr::val(-1)),
3833 ("-(-1)", Expr::neg(Expr::val(-1))),
3834 ("--1", Expr::neg(Expr::val(-1))),
3835 ("--(--1)", Expr::neg(Expr::neg(Expr::neg(Expr::val(-1))))),
3836 ("2--1", Expr::sub(Expr::val(2), Expr::val(-1))),
3837 ("-9223372036854775808", Expr::val(-(9223372036854775808))),
3838 (
3841 "--9223372036854775808",
3842 Expr::neg(Expr::val(-9223372036854775808)),
3843 ),
3844 (
3845 "-(9223372036854775807)",
3846 Expr::neg(Expr::val(9223372036854775807)),
3847 ),
3848 ] {
3849 let e = assert_parse_expr_succeeds(es);
3850 assert!(
3851 e.eq_shape(&expr),
3852 "{e:?} and {expr:?} should have the same shape."
3853 );
3854 }
3855
3856 for (es, em) in [
3857 (
3858 "-9223372036854775809",
3859 ExpectedErrorMessageBuilder::error(
3860 "integer literal `9223372036854775809` is too large",
3861 )
3862 .help("maximum allowed integer literal is `9223372036854775807`")
3863 .exactly_one_underline("-9223372036854775809")
3864 .build(),
3865 ),
3866 (
3871 "-(9223372036854775808)",
3872 ExpectedErrorMessageBuilder::error(
3873 "integer literal `9223372036854775808` is too large",
3874 )
3875 .help("maximum allowed integer literal is `9223372036854775807`")
3876 .exactly_one_underline("9223372036854775808")
3877 .build(),
3878 ),
3879 ] {
3880 let errs = assert_parse_expr_fails(es);
3881 expect_err(es, &miette::Report::new(errs), &em);
3882 }
3883 }
3884
3885 #[test]
3886 fn test_is_condition_ok() {
3887 for (es, expr) in [
3888 (
3889 r#"User::"alice" is User"#,
3890 Expr::is_entity_type(
3891 Expr::val(r#"User::"alice""#.parse::<EntityUID>().unwrap()),
3892 "User".parse().unwrap(),
3893 ),
3894 ),
3895 (
3896 r#"principal is User"#,
3897 Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3898 ),
3899 (
3900 r#"principal.foo is User"#,
3901 Expr::is_entity_type(
3902 Expr::get_attr(Expr::var(ast::Var::Principal), "foo".into()),
3903 "User".parse().unwrap(),
3904 ),
3905 ),
3906 (
3907 r#"1 is User"#,
3908 Expr::is_entity_type(Expr::val(1), "User".parse().unwrap()),
3909 ),
3910 (
3911 r#"principal is User in Group::"friends""#,
3912 Expr::and(
3913 Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3914 Expr::is_in(
3915 Expr::var(ast::Var::Principal),
3916 Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
3917 ),
3918 ),
3919 ),
3920 (
3921 r#"principal is User && principal in Group::"friends""#,
3922 Expr::and(
3923 Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3924 Expr::is_in(
3925 Expr::var(ast::Var::Principal),
3926 Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
3927 ),
3928 ),
3929 ),
3930 (
3931 r#"principal is User || principal in Group::"friends""#,
3932 Expr::or(
3933 Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3934 Expr::is_in(
3935 Expr::var(ast::Var::Principal),
3936 Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
3937 ),
3938 ),
3939 ),
3940 (
3941 r#"true && principal is User in principal"#,
3942 Expr::and(
3943 Expr::val(true),
3944 Expr::and(
3945 Expr::is_entity_type(
3946 Expr::var(ast::Var::Principal),
3947 "User".parse().unwrap(),
3948 ),
3949 Expr::is_in(
3950 Expr::var(ast::Var::Principal),
3951 Expr::var(ast::Var::Principal),
3952 ),
3953 ),
3954 ),
3955 ),
3956 (
3957 r#"principal is User in principal && true"#,
3958 Expr::and(
3959 Expr::and(
3960 Expr::is_entity_type(
3961 Expr::var(ast::Var::Principal),
3962 "User".parse().unwrap(),
3963 ),
3964 Expr::is_in(
3965 Expr::var(ast::Var::Principal),
3966 Expr::var(ast::Var::Principal),
3967 ),
3968 ),
3969 Expr::val(true),
3970 ),
3971 ),
3972 (
3973 r#"principal is A::B::C::User"#,
3974 Expr::is_entity_type(
3975 Expr::var(ast::Var::Principal),
3976 "A::B::C::User".parse().unwrap(),
3977 ),
3978 ),
3979 (
3980 r#"principal is A::B::C::User in Group::"friends""#,
3981 Expr::and(
3982 Expr::is_entity_type(
3983 Expr::var(ast::Var::Principal),
3984 "A::B::C::User".parse().unwrap(),
3985 ),
3986 Expr::is_in(
3987 Expr::var(ast::Var::Principal),
3988 Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
3989 ),
3990 ),
3991 ),
3992 (
3993 r#"if principal is User then 1 else 2"#,
3994 Expr::ite(
3995 Expr::is_entity_type(Expr::var(ast::Var::Principal), "User".parse().unwrap()),
3996 Expr::val(1),
3997 Expr::val(2),
3998 ),
3999 ),
4000 (
4001 r#"if principal is User in Group::"friends" then 1 else 2"#,
4002 Expr::ite(
4003 Expr::and(
4004 Expr::is_entity_type(
4005 Expr::var(ast::Var::Principal),
4006 "User".parse().unwrap(),
4007 ),
4008 Expr::is_in(
4009 Expr::var(ast::Var::Principal),
4010 Expr::val(r#"Group::"friends""#.parse::<EntityUID>().unwrap()),
4011 ),
4012 ),
4013 Expr::val(1),
4014 Expr::val(2),
4015 ),
4016 ),
4017 (
4018 r#"principal::"alice" is principal"#,
4019 Expr::is_entity_type(
4020 Expr::val(r#"principal::"alice""#.parse::<EntityUID>().unwrap()),
4021 "principal".parse().unwrap(),
4022 ),
4023 ),
4024 (
4025 r#"foo::principal::"alice" is foo::principal"#,
4026 Expr::is_entity_type(
4027 Expr::val(r#"foo::principal::"alice""#.parse::<EntityUID>().unwrap()),
4028 "foo::principal".parse().unwrap(),
4029 ),
4030 ),
4031 (
4032 r#"principal::foo::"alice" is principal::foo"#,
4033 Expr::is_entity_type(
4034 Expr::val(r#"principal::foo::"alice""#.parse::<EntityUID>().unwrap()),
4035 "principal::foo".parse().unwrap(),
4036 ),
4037 ),
4038 (
4039 r#"resource::"thing" is resource"#,
4040 Expr::is_entity_type(
4041 Expr::val(r#"resource::"thing""#.parse::<EntityUID>().unwrap()),
4042 "resource".parse().unwrap(),
4043 ),
4044 ),
4045 (
4046 r#"action::"do" is action"#,
4047 Expr::is_entity_type(
4048 Expr::val(r#"action::"do""#.parse::<EntityUID>().unwrap()),
4049 "action".parse().unwrap(),
4050 ),
4051 ),
4052 (
4053 r#"context::"stuff" is context"#,
4054 Expr::is_entity_type(
4055 Expr::val(r#"context::"stuff""#.parse::<EntityUID>().unwrap()),
4056 "context".parse().unwrap(),
4057 ),
4058 ),
4059 ] {
4060 let e = parse_expr(es).unwrap();
4061 assert!(
4062 e.eq_shape(&expr),
4063 "{e:?} and {expr:?} should have the same shape."
4064 );
4065 }
4066 }
4067
4068 #[test]
4069 fn is_scope() {
4070 for (src, p, a, r) in [
4071 (
4072 r#"permit(principal is User, action, resource);"#,
4073 PrincipalConstraint::is_entity_type(Arc::new("User".parse().unwrap())),
4074 ActionConstraint::any(),
4075 ResourceConstraint::any(),
4076 ),
4077 (
4078 r#"permit(principal is principal, action, resource);"#,
4079 PrincipalConstraint::is_entity_type(Arc::new("principal".parse().unwrap())),
4080 ActionConstraint::any(),
4081 ResourceConstraint::any(),
4082 ),
4083 (
4084 r#"permit(principal is A::User, action, resource);"#,
4085 PrincipalConstraint::is_entity_type(Arc::new("A::User".parse().unwrap())),
4086 ActionConstraint::any(),
4087 ResourceConstraint::any(),
4088 ),
4089 (
4090 r#"permit(principal is User in Group::"thing", action, resource);"#,
4091 PrincipalConstraint::is_entity_type_in(
4092 Arc::new("User".parse().unwrap()),
4093 Arc::new(r#"Group::"thing""#.parse().unwrap()),
4094 ),
4095 ActionConstraint::any(),
4096 ResourceConstraint::any(),
4097 ),
4098 (
4099 r#"permit(principal is principal in Group::"thing", action, resource);"#,
4100 PrincipalConstraint::is_entity_type_in(
4101 Arc::new("principal".parse().unwrap()),
4102 Arc::new(r#"Group::"thing""#.parse().unwrap()),
4103 ),
4104 ActionConstraint::any(),
4105 ResourceConstraint::any(),
4106 ),
4107 (
4108 r#"permit(principal is A::User in Group::"thing", action, resource);"#,
4109 PrincipalConstraint::is_entity_type_in(
4110 Arc::new("A::User".parse().unwrap()),
4111 Arc::new(r#"Group::"thing""#.parse().unwrap()),
4112 ),
4113 ActionConstraint::any(),
4114 ResourceConstraint::any(),
4115 ),
4116 (
4117 r#"permit(principal is User in ?principal, action, resource);"#,
4118 PrincipalConstraint::is_entity_type_in_slot(Arc::new("User".parse().unwrap())),
4119 ActionConstraint::any(),
4120 ResourceConstraint::any(),
4121 ),
4122 (
4123 r#"permit(principal, action, resource is Folder);"#,
4124 PrincipalConstraint::any(),
4125 ActionConstraint::any(),
4126 ResourceConstraint::is_entity_type(Arc::new("Folder".parse().unwrap())),
4127 ),
4128 (
4129 r#"permit(principal, action, resource is Folder in Folder::"inner");"#,
4130 PrincipalConstraint::any(),
4131 ActionConstraint::any(),
4132 ResourceConstraint::is_entity_type_in(
4133 Arc::new("Folder".parse().unwrap()),
4134 Arc::new(r#"Folder::"inner""#.parse().unwrap()),
4135 ),
4136 ),
4137 (
4138 r#"permit(principal, action, resource is Folder in ?resource);"#,
4139 PrincipalConstraint::any(),
4140 ActionConstraint::any(),
4141 ResourceConstraint::is_entity_type_in_slot(Arc::new("Folder".parse().unwrap())),
4142 ),
4143 ] {
4144 let policy = parse_policy_or_template(None, src).unwrap();
4145 assert_eq!(policy.principal_constraint(), &p);
4146 assert_eq!(policy.action_constraint(), &a);
4147 assert_eq!(policy.resource_constraint(), &r);
4148 }
4149 }
4150
4151 #[test]
4152 fn is_err() {
4153 let invalid_is_policies = [
4154 (
4155 r#"permit(principal in Group::"friends" is User, action, resource);"#,
4156 ExpectedErrorMessageBuilder::error("when `is` and `in` are used together, `is` must come first")
4157 .help("try `_ is _ in _`")
4158 .exactly_one_underline(r#"principal in Group::"friends" is User"#)
4159 .build(),
4160 ),
4161 (
4162 r#"permit(principal, action in Group::"action_group" is Action, resource);"#,
4163 ExpectedErrorMessageBuilder::error("`is` cannot appear in the action scope")
4164 .help("try moving `action is ..` into a `when` condition")
4165 .exactly_one_underline(r#"action in Group::"action_group" is Action"#)
4166 .build(),
4167 ),
4168 (
4169 r#"permit(principal, action, resource in Folder::"folder" is File);"#,
4170 ExpectedErrorMessageBuilder::error("when `is` and `in` are used together, `is` must come first")
4171 .help("try `_ is _ in _`")
4172 .exactly_one_underline(r#"resource in Folder::"folder" is File"#)
4173 .build(),
4174 ),
4175 (
4176 r#"permit(principal is User == User::"Alice", action, resource);"#,
4177 ExpectedErrorMessageBuilder::error(
4178 "`is` cannot be used together with `==`",
4179 ).help(
4180 "try using `_ is _ in _`"
4181 ).exactly_one_underline("principal is User == User::\"Alice\"").build(),
4182 ),
4183 (
4184 r#"permit(principal, action, resource is Doc == Doc::"a");"#,
4185 ExpectedErrorMessageBuilder::error(
4186 "`is` cannot be used together with `==`",
4187 ).help(
4188 "try using `_ is _ in _`"
4189 ).exactly_one_underline("resource is Doc == Doc::\"a\"").build(),
4190 ),
4191 (
4192 r#"permit(principal is User::"alice", action, resource);"#,
4193 ExpectedErrorMessageBuilder::error(
4194 r#"right hand side of an `is` expression must be an entity type name, but got `User::"alice"`"#,
4195 ).help(r#"try using `==` to test for equality: `principal == User::"alice"`"#)
4196 .exactly_one_underline("User::\"alice\"").build(),
4197 ),
4198 (
4199 r#"permit(principal, action, resource is File::"f");"#,
4200 ExpectedErrorMessageBuilder::error(
4201 r#"right hand side of an `is` expression must be an entity type name, but got `File::"f"`"#,
4202 ).help(r#"try using `==` to test for equality: `resource == File::"f"`"#)
4203 .exactly_one_underline("File::\"f\"").build(),
4204 ),
4205 (
4206 r#"permit(principal is User in 1, action, resource);"#,
4207 ExpectedErrorMessageBuilder::error(
4208 "expected an entity uid or matching template slot, found literal `1`",
4209 ).exactly_one_underline("1").build(),
4210 ),
4211 (
4212 r#"permit(principal, action, resource is File in 1);"#,
4213 ExpectedErrorMessageBuilder::error(
4214 "expected an entity uid or matching template slot, found literal `1`",
4215 ).exactly_one_underline("1").build(),
4216 ),
4217 (
4218 r#"permit(principal is User in User, action, resource);"#,
4219 ExpectedErrorMessageBuilder::error(
4220 "expected an entity uid or matching template slot, found name `User`",
4221 )
4222 .help(
4223 "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4224 )
4225 .exactly_one_underline("User").build(),
4226 ),
4227 (
4228 r#"permit(principal is User::"Alice" in Group::"f", action, resource);"#,
4229 ExpectedErrorMessageBuilder::error(
4230 r#"right hand side of an `is` expression must be an entity type name, but got `User::"Alice"`"#,
4231 ).help(r#"try using `==` to test for equality: `principal == User::"Alice"`"#)
4232 .exactly_one_underline("User::\"Alice\"").build(),
4233 ),
4234 (
4235 r#"permit(principal, action, resource is File in File);"#,
4236 ExpectedErrorMessageBuilder::error(
4237 "expected an entity uid or matching template slot, found name `File`",
4238 )
4239 .help(
4240 "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4241 )
4242 .exactly_one_underline("File").build(),
4243 ),
4244 (
4245 r#"permit(principal, action, resource is File::"file" in Folder::"folder");"#,
4246 ExpectedErrorMessageBuilder::error(
4247 r#"right hand side of an `is` expression must be an entity type name, but got `File::"file"`"#,
4248 ).help(
4249 r#"try using `==` to test for equality: `resource == File::"file"`"#
4250 ).exactly_one_underline("File::\"file\"").build(),
4251 ),
4252 (
4253 r#"permit(principal is 1, action, resource);"#,
4254 ExpectedErrorMessageBuilder::error(
4255 r#"right hand side of an `is` expression must be an entity type name, but got `1`"#,
4256 ).help(
4257 "try using `==` to test for equality: `principal == 1`"
4258 ).exactly_one_underline("1").build(),
4259 ),
4260 (
4261 r#"permit(principal, action, resource is 1);"#,
4262 ExpectedErrorMessageBuilder::error(
4263 r#"right hand side of an `is` expression must be an entity type name, but got `1`"#,
4264 ).help(
4265 "try using `==` to test for equality: `resource == 1`"
4266 ).exactly_one_underline("1").build(),
4267 ),
4268 (
4269 r#"permit(principal, action is Action, resource);"#,
4270 ExpectedErrorMessageBuilder::error(
4271 "`is` cannot appear in the action scope",
4272 ).help(
4273 "try moving `action is ..` into a `when` condition"
4274 ).exactly_one_underline("action is Action").build(),
4275 ),
4276 (
4277 r#"permit(principal, action is Action::"a", resource);"#,
4278 ExpectedErrorMessageBuilder::error(
4279 "`is` cannot appear in the action scope",
4280 ).help(
4281 "try moving `action is ..` into a `when` condition"
4282 ).exactly_one_underline("action is Action::\"a\"").build(),
4283 ),
4284 (
4285 r#"permit(principal, action is Action in Action::"A", resource);"#,
4286 ExpectedErrorMessageBuilder::error(
4287 "`is` cannot appear in the action scope",
4288 ).help(
4289 "try moving `action is ..` into a `when` condition"
4290 ).exactly_one_underline("action is Action in Action::\"A\"").build(),
4291 ),
4292 (
4293 r#"permit(principal, action is Action in Action, resource);"#,
4294 ExpectedErrorMessageBuilder::error(
4295 "`is` cannot appear in the action scope",
4296 ).help(
4297 "try moving `action is ..` into a `when` condition"
4298 ).exactly_one_underline("action is Action in Action").build(),
4299 ),
4300 (
4301 r#"permit(principal, action is Action::"a" in Action::"b", resource);"#,
4302 ExpectedErrorMessageBuilder::error(
4303 "`is` cannot appear in the action scope",
4304 ).help(
4305 "try moving `action is ..` into a `when` condition"
4306 ).exactly_one_underline("action is Action::\"a\" in Action::\"b\"").build(),
4307 ),
4308 (
4309 r#"permit(principal, action is Action in ?action, resource);"#,
4310 ExpectedErrorMessageBuilder::error(
4311 "`is` cannot appear in the action scope",
4312 ).help(
4313 "try moving `action is ..` into a `when` condition"
4314 ).exactly_one_underline("action is Action in ?action").build(),
4315 ),
4316 (
4317 r#"permit(principal, action is ?action, resource);"#,
4318 ExpectedErrorMessageBuilder::error(
4319 "`is` cannot appear in the action scope",
4320 ).help(
4321 "try moving `action is ..` into a `when` condition"
4322 ).exactly_one_underline("action is ?action").build(),
4323 ),
4324 (
4325 r#"permit(principal is User in ?resource, action, resource);"#,
4326 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
4327 ),
4328 (
4329 r#"permit(principal, action, resource is Folder in ?principal);"#,
4330 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
4331 ),
4332 (
4333 r#"permit(principal is ?principal, action, resource);"#,
4334 ExpectedErrorMessageBuilder::error(
4335 "right hand side of an `is` expression must be an entity type name, but got `?principal`",
4336 ).help(
4337 "try using `==` to test for equality: `principal == ?principal`"
4338 ).exactly_one_underline("?principal").build(),
4339 ),
4340 (
4341 r#"permit(principal, action, resource is ?resource);"#,
4342 ExpectedErrorMessageBuilder::error(
4343 "right hand side of an `is` expression must be an entity type name, but got `?resource`",
4344 ).help(
4345 "try using `==` to test for equality: `resource == ?resource`"
4346 ).exactly_one_underline("?resource").build(),
4347 ),
4348 (
4349 r#"permit(principal, action, resource) when { principal is 1 };"#,
4350 ExpectedErrorMessageBuilder::error(
4351 r#"right hand side of an `is` expression must be an entity type name, but got `1`"#,
4352 ).help(
4353 "try using `==` to test for equality: `principal == 1`"
4354 ).exactly_one_underline("1").build(),
4355 ),
4356 (
4357 r#"permit(principal, action, resource) when { principal is User::"alice" in Group::"friends" };"#,
4358 ExpectedErrorMessageBuilder::error(
4359 r#"right hand side of an `is` expression must be an entity type name, but got `User::"alice"`"#,
4360 ).help(
4361 r#"try using `==` to test for equality: `principal == User::"alice"`"#
4362 ).exactly_one_underline("User::\"alice\"").build(),
4363 ),
4364 (
4365 r#"permit(principal, action, resource) when { principal is ! User::"alice" in Group::"friends" };"#,
4366 ExpectedErrorMessageBuilder::error(
4367 r#"right hand side of an `is` expression must be an entity type name, but got `! User::"alice"`"#,
4368 ).help(
4369 r#"try using `==` to test for equality: `principal == ! User::"alice"`"#
4370 ).exactly_one_underline("! User::\"alice\"").build(),
4371 ),
4372 (
4373 r#"permit(principal, action, resource) when { principal is User::"alice" + User::"alice" in Group::"friends" };"#,
4374 ExpectedErrorMessageBuilder::error(
4375 r#"right hand side of an `is` expression must be an entity type name, but got `User::"alice" + User::"alice"`"#,
4376 ).help(
4377 r#"try using `==` to test for equality: `principal == User::"alice" + User::"alice"`"#
4378 ).exactly_one_underline("User::\"alice\" + User::\"alice\"").build(),
4379 ),
4380 (
4381 r#"permit(principal, action, resource) when { principal is User in User::"alice" in Group::"friends" };"#,
4382 ExpectedErrorMessageBuilder::error("unexpected token `in`")
4383 .exactly_one_underline_with_label("in", "expected `&&`, `||`, or `}`")
4384 .build(),
4385 ),
4386 (
4387 r#"permit(principal, action, resource) when { principal is User == User::"alice" in Group::"friends" };"#,
4388 ExpectedErrorMessageBuilder::error("unexpected token `==`")
4389 .exactly_one_underline_with_label("==", "expected `&&`, `||`, `}`, or `in`")
4390 .build(),
4391 ),
4392 (
4393 r#"permit(principal, action, resource) when { principal in Group::"friends" is User };"#,
4395 ExpectedErrorMessageBuilder::error("unexpected token `is`")
4396 .exactly_one_underline_with_label(r#"is"#, "expected `!=`, `&&`, `<`, `<=`, `==`, `>`, `>=`, `||`, `}`, or `in`")
4397 .build(),
4398 ),
4399 (
4400 r#"permit(principal is "User", action, resource);"#,
4401 ExpectedErrorMessageBuilder::error(
4402 r#"right hand side of an `is` expression must be an entity type name, but got `"User"`"#,
4403 ).help(
4404 "try removing the quotes: `principal is User`"
4405 ).exactly_one_underline("\"User\"").build(),
4406 ),
4407 (
4408 r#"permit(principal, action, resource) when { principal is "User" };"#,
4409 ExpectedErrorMessageBuilder::error(
4410 r#"right hand side of an `is` expression must be an entity type name, but got `"User"`"#,
4411 ).help(
4412 "try removing the quotes: `principal is User`"
4413 ).exactly_one_underline("\"User\"").build(),
4414 ),
4415 ];
4416 for (p_src, expected) in invalid_is_policies {
4417 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4418 expect_err(p_src, &miette::Report::new(e), &expected);
4419 });
4420 }
4421 }
4422
4423 #[test]
4424 fn issue_255() {
4425 let policy = r#"
4426 permit (
4427 principal == name-with-dashes::"Alice",
4428 action,
4429 resource
4430 );
4431 "#;
4432 assert_matches!(
4433 parse_policy(None, policy),
4434 Err(e) => {
4435 expect_n_errors(policy, &e, 1);
4436 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
4437 "expected an entity uid or matching template slot, found a `+/-` expression",
4438 ).help(
4439 "entity types and namespaces cannot use `+` or `-` characters -- perhaps try `_` or `::` instead?",
4440 ).exactly_one_underline("name-with-dashes::\"Alice\"").build());
4441 }
4442 );
4443 }
4444
4445 #[test]
4446 fn invalid_methods_function_calls() {
4447 let invalid_exprs = [
4448 (
4449 r#"contains([], 1)"#,
4450 ExpectedErrorMessageBuilder::error("`contains` is a method, not a function")
4451 .help("use a method-style call `e.contains(..)`")
4452 .exactly_one_underline("contains([], 1)")
4453 .build(),
4454 ),
4455 (
4456 r#"[].contains()"#,
4457 ExpectedErrorMessageBuilder::error(
4458 "call to `contains` requires exactly 1 argument, but got 0 arguments",
4459 )
4460 .exactly_one_underline("[].contains()")
4461 .build(),
4462 ),
4463 (
4464 r#"[].contains(1, 2)"#,
4465 ExpectedErrorMessageBuilder::error(
4466 "call to `contains` requires exactly 1 argument, but got 2 arguments",
4467 )
4468 .exactly_one_underline("[].contains(1, 2)")
4469 .build(),
4470 ),
4471 (
4472 r#"[].containsAll()"#,
4473 ExpectedErrorMessageBuilder::error(
4474 "call to `containsAll` requires exactly 1 argument, but got 0 arguments",
4475 )
4476 .exactly_one_underline("[].containsAll()")
4477 .build(),
4478 ),
4479 (
4480 r#"[].containsAll(1, 2)"#,
4481 ExpectedErrorMessageBuilder::error(
4482 "call to `containsAll` requires exactly 1 argument, but got 2 arguments",
4483 )
4484 .exactly_one_underline("[].containsAll(1, 2)")
4485 .build(),
4486 ),
4487 (
4488 r#"[].containsAny()"#,
4489 ExpectedErrorMessageBuilder::error(
4490 "call to `containsAny` requires exactly 1 argument, but got 0 arguments",
4491 )
4492 .exactly_one_underline("[].containsAny()")
4493 .build(),
4494 ),
4495 (
4496 r#"[].containsAny(1, 2)"#,
4497 ExpectedErrorMessageBuilder::error(
4498 "call to `containsAny` requires exactly 1 argument, but got 2 arguments",
4499 )
4500 .exactly_one_underline("[].containsAny(1, 2)")
4501 .build(),
4502 ),
4503 (
4504 r#"[].isEmpty([])"#,
4505 ExpectedErrorMessageBuilder::error(
4506 "call to `isEmpty` requires exactly 0 arguments, but got 1 argument",
4507 )
4508 .exactly_one_underline("[].isEmpty([])")
4509 .build(),
4510 ),
4511 (
4512 r#""1.1.1.1".ip()"#,
4513 ExpectedErrorMessageBuilder::error("`ip` is a function, not a method")
4514 .help("use a function-style call `ip(..)`")
4515 .exactly_one_underline(r#""1.1.1.1".ip()"#)
4516 .build(),
4517 ),
4518 (
4519 r#"greaterThan(1, 2)"#,
4520 ExpectedErrorMessageBuilder::error("`greaterThan` is a method, not a function")
4521 .help("use a method-style call `e.greaterThan(..)`")
4522 .exactly_one_underline("greaterThan(1, 2)")
4523 .build(),
4524 ),
4525 (
4526 "[].bar()",
4527 ExpectedErrorMessageBuilder::error("`bar` is not a valid method")
4528 .exactly_one_underline("[].bar()")
4529 .build(),
4530 ),
4531 (
4532 "principal.addr.isipv4()",
4533 ExpectedErrorMessageBuilder::error("`isipv4` is not a valid method")
4534 .exactly_one_underline("principal.addr.isipv4()")
4535 .help("did you mean `isIpv4`?")
4536 .build(),
4537 ),
4538 (
4539 "bar([])",
4540 ExpectedErrorMessageBuilder::error("`bar` is not a valid function")
4541 .exactly_one_underline("bar([])")
4542 .help("did you mean `ip`?")
4543 .build(),
4544 ),
4545 (
4546 r#"Ip("1.1.1.1/24")"#,
4547 ExpectedErrorMessageBuilder::error("`Ip` is not a valid function")
4548 .exactly_one_underline(r#"Ip("1.1.1.1/24")"#)
4549 .help("did you mean `ip`?")
4550 .build(),
4551 ),
4552 (
4553 "principal()",
4554 ExpectedErrorMessageBuilder::error("`principal(...)` is not a valid function call")
4555 .help("variables cannot be called as functions")
4556 .exactly_one_underline("principal()")
4557 .build(),
4558 ),
4559 (
4560 "(1+1)()",
4561 ExpectedErrorMessageBuilder::error(
4562 "function calls must be of the form `<name>(arg1, arg2, ...)`",
4563 )
4564 .exactly_one_underline("(1+1)()")
4565 .build(),
4566 ),
4567 (
4568 "foo.bar()",
4569 ExpectedErrorMessageBuilder::error(
4570 "attempted to call `foo.bar(...)`, but `foo` does not have any methods",
4571 )
4572 .exactly_one_underline("foo.bar()")
4573 .build(),
4574 ),
4575 ];
4576 for (src, expected) in invalid_exprs {
4577 assert_matches!(parse_expr(src), Err(e) => {
4578 expect_err(src, &miette::Report::new(e), &expected);
4579 });
4580 }
4581 }
4582
4583 #[test]
4584 fn invalid_slot() {
4585 let invalid_policies = [
4586 (
4587 r#"permit(principal == ?resource, action, resource);"#,
4588 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
4589 ),
4590 (
4591 r#"permit(principal in ?resource, action, resource);"#,
4592 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
4593 ),
4594 (
4595 r#"permit(principal == ?foo, action, resource);"#,
4596 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?foo instead of ?principal").exactly_one_underline("?foo").build(),
4597 ),
4598 (
4599 r#"permit(principal in ?foo, action, resource);"#,
4600 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?foo instead of ?principal").exactly_one_underline("?foo").build(),
4601 ),
4602
4603 (
4604 r#"permit(principal, action, resource == ?principal);"#,
4605 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
4606 ),
4607 (
4608 r#"permit(principal, action, resource in ?principal);"#,
4609 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
4610 ),
4611 (
4612 r#"permit(principal, action, resource == ?baz);"#,
4613 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?baz instead of ?resource").exactly_one_underline("?baz").build(),
4614 ),
4615 (
4616 r#"permit(principal, action, resource in ?baz);"#,
4617 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?baz instead of ?resource").exactly_one_underline("?baz").build(),
4618 ),
4619 (
4620 r#"permit(principal, action, resource) when { principal == ?foo};"#,
4621 ExpectedErrorMessageBuilder::error(
4622 "`?foo` is not a valid template slot",
4623 ).help(
4624 "a template slot may only be `?principal` or `?resource`",
4625 ).exactly_one_underline("?foo").build(),
4626 ),
4627
4628 (
4629 r#"permit(principal, action == ?action, resource);"#,
4630 ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?action").build(),
4631 ),
4632 (
4633 r#"permit(principal, action in ?action, resource);"#,
4634 ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?action").build(),
4635 ),
4636 (
4637 r#"permit(principal, action == ?principal, resource);"#,
4638 ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?principal").build(),
4639 ),
4640 (
4641 r#"permit(principal, action in ?principal, resource);"#,
4642 ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?principal").build(),
4643 ),
4644 (
4645 r#"permit(principal, action == ?resource, resource);"#,
4646 ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?resource").build(),
4647 ),
4648 (
4649 r#"permit(principal, action in ?resource, resource);"#,
4650 ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?resource").build(),
4651 ),
4652 (
4653 r#"permit(principal, action in [?bar], resource);"#,
4654 ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?bar").build(),
4655 ),
4656 ];
4657
4658 for (p_src, expected) in invalid_policies {
4659 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4660 expect_err(p_src, &miette::Report::new(e), &expected);
4661 });
4662 let forbid_src = format!("forbid{}", &p_src[6..]);
4663 assert_matches!(parse_policy_or_template(None, &forbid_src), Err(e) => {
4664 expect_err(forbid_src.as_str(), &miette::Report::new(e), &expected);
4665 });
4666 }
4667 }
4668
4669 #[test]
4670 fn missing_scope_constraint() {
4671 let p_src = "permit();";
4672 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4673 expect_err(
4674 p_src,
4675 &miette::Report::new(e),
4676 &ExpectedErrorMessageBuilder::error("this policy is missing the `principal` variable in the scope")
4677 .exactly_one_underline("")
4678 .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
4679 .build()
4680 );
4681 });
4682 let p_src = "permit(principal);";
4683 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4684 expect_err(
4685 p_src,
4686 &miette::Report::new(e),
4687 &ExpectedErrorMessageBuilder::error("this policy is missing the `action` variable in the scope")
4688 .exactly_one_underline("")
4689 .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
4690 .build()
4691 );
4692 });
4693 let p_src = "permit(principal, action);";
4694 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4695 expect_err(
4696 p_src,
4697 &miette::Report::new(e),
4698 &ExpectedErrorMessageBuilder::error("this policy is missing the `resource` variable in the scope")
4699 .exactly_one_underline("")
4700 .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
4701 .build()
4702 );
4703 });
4704 }
4705
4706 #[test]
4707 fn invalid_scope_constraint() {
4708 let p_src = "permit(foo, action, resource);";
4709 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4710 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4711 "found an invalid variable in the policy scope: foo",
4712 ).help(
4713 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4714 ).exactly_one_underline("foo").build());
4715 });
4716 let p_src = "permit(foo::principal, action, resource);";
4717 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4718 expect_err(
4719 p_src,
4720 &miette::Report::new(e),
4721 &ExpectedErrorMessageBuilder::error("unexpected token `::`")
4722 .exactly_one_underline_with_label("::", "expected `!=`, `)`, `,`, `:`, `<`, `<=`, `==`, `>`, `>=`, `in`, or `is`")
4723 .build()
4724 );
4725 });
4726 let p_src = "permit(resource, action, resource);";
4727 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4728 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4729 "found the variable `resource` where the variable `principal` must be used",
4730 ).help(
4731 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4732 ).exactly_one_underline("resource").build());
4733 });
4734
4735 let p_src = "permit(principal, principal, resource);";
4736 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4737 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4738 "found the variable `principal` where the variable `action` must be used",
4739 ).help(
4740 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4741 ).exactly_one_underline("principal").build());
4742 });
4743 let p_src = "permit(principal, if, resource);";
4744 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4745 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4746 "found an invalid variable in the policy scope: if",
4747 ).help(
4748 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4749 ).exactly_one_underline("if").build());
4750 });
4751
4752 let p_src = "permit(principal, action, like);";
4753 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4754 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4755 "found an invalid variable in the policy scope: like",
4756 ).help(
4757 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4758 ).exactly_one_underline("like").build());
4759 });
4760 let p_src = "permit(principal, action, principal);";
4761 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4762 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4763 "found the variable `principal` where the variable `resource` must be used",
4764 ).help(
4765 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4766 ).exactly_one_underline("principal").build());
4767 });
4768 let p_src = "permit(principal, action, action);";
4769 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4770 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4771 "found the variable `action` where the variable `resource` must be used",
4772 ).help(
4773 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
4774 ).exactly_one_underline("action").build());
4775 });
4776 }
4777
4778 #[test]
4779 fn invalid_scope_operator() {
4780 let p_src = r#"permit(principal > User::"alice", action, resource);"#;
4781 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4782 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4783 "invalid operator in the policy scope: >",
4784 ).help(
4785 "policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"
4786 ).exactly_one_underline("principal > User::\"alice\"").build());
4787 });
4788 let p_src = r#"permit(principal, action != Action::"view", resource);"#;
4789 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4790 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4791 "invalid operator in the action scope: !=",
4792 ).help(
4793 "action scope clauses can only use `==` or `in`"
4794 ).exactly_one_underline("action != Action::\"view\"").build());
4795 });
4796 let p_src = r#"permit(principal, action, resource <= Folder::"things");"#;
4797 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4798 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4799 "invalid operator in the policy scope: <=",
4800 ).help(
4801 "policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"
4802 ).exactly_one_underline("resource <= Folder::\"things\"").build());
4803 });
4804 let p_src = r#"permit(principal = User::"alice", action, resource);"#;
4805 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4806 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4807 "'=' is not a valid operator in Cedar",
4808 ).help(
4809 "try using '==' instead",
4810 ).exactly_one_underline("principal = User::\"alice\"").build());
4811 });
4812 let p_src = r#"permit(principal, action = Action::"act", resource);"#;
4813 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4814 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4815 "'=' is not a valid operator in Cedar",
4816 ).help(
4817 "try using '==' instead",
4818 ).exactly_one_underline("action = Action::\"act\"").build());
4819 });
4820 let p_src = r#"permit(principal, action, resource = Photo::"photo");"#;
4821 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4822 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4823 "'=' is not a valid operator in Cedar",
4824 ).help(
4825 "try using '==' instead",
4826 ).exactly_one_underline("resource = Photo::\"photo\"").build());
4827 });
4828 }
4829
4830 #[test]
4831 fn scope_action_eq_set() {
4832 let p_src = r#"permit(principal, action == [Action::"view", Action::"edit"], resource);"#;
4833 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4834 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("expected single entity uid, found set of entity uids").exactly_one_underline(r#"[Action::"view", Action::"edit"]"#).build());
4835 });
4836 }
4837
4838 #[test]
4839 fn scope_compare_to_string() {
4840 let p_src = r#"permit(principal == "alice", action, resource);"#;
4841 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4842 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4843 r#"expected an entity uid or matching template slot, found literal `"alice"`"#
4844 ).help(
4845 "try including the entity type if you intended this string to be an entity uid"
4846 ).exactly_one_underline(r#""alice""#).build());
4847 });
4848 let p_src = r#"permit(principal in "bob_friends", action, resource);"#;
4849 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4850 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4851 r#"expected an entity uid or matching template slot, found literal `"bob_friends"`"#
4852 ).help(
4853 "try including the entity type if you intended this string to be an entity uid"
4854 ).exactly_one_underline(r#""bob_friends""#).build());
4855 });
4856 let p_src = r#"permit(principal, action, resource in "jane_photos");"#;
4857 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4858 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4859 r#"expected an entity uid or matching template slot, found literal `"jane_photos"`"#
4860 ).help(
4861 "try including the entity type if you intended this string to be an entity uid"
4862 ).exactly_one_underline(r#""jane_photos""#).build());
4863 });
4864 let p_src = r#"permit(principal, action in ["view_actions"], resource);"#;
4865 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4866 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4867 r#"expected an entity uid, found literal `"view_actions"`"#
4868 ).help(
4869 "try including the entity type if you intended this string to be an entity uid"
4870 ).exactly_one_underline(r#""view_actions""#).build());
4871 });
4872 }
4873
4874 #[test]
4875 fn scope_compare_to_name() {
4876 let p_src = r#"permit(principal == User, action, resource);"#;
4877 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4878 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4879 "expected an entity uid or matching template slot, found name `User`"
4880 ).help(
4881 "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4882 ).exactly_one_underline("User").build());
4883 });
4884 let p_src = r#"permit(principal in Group, action, resource);"#;
4885 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4886 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4887 "expected an entity uid or matching template slot, found name `Group`"
4888 ).help(
4889 "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4890 ).exactly_one_underline("Group").build());
4891 });
4892 let p_src = r#"permit(principal, action, resource in Album);"#;
4893 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4894 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4895 "expected an entity uid or matching template slot, found name `Album`"
4896 ).help(
4897 "try using `is` to test for an entity type or including an identifier string if you intended this name to be an entity uid"
4898 ).exactly_one_underline("Album").build());
4899 });
4900 let p_src = r#"permit(principal, action == Action, resource);"#;
4901 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4902 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4903 "expected an entity uid, found name `Action`"
4904 ).help(
4905 "try including an identifier string if you intended this name to be an entity uid"
4906 ).exactly_one_underline("Action").build());
4907 });
4908 }
4909
4910 #[test]
4911 fn scope_and() {
4912 let p_src = r#"permit(principal == User::"alice" && principal in Group::"jane_friends", action, resource);"#;
4913 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4914 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4915 "expected an entity uid or matching template slot, found a `&&` expression"
4916 ).help(
4917 "the policy scope can only contain one constraint per variable. Consider moving the second operand of this `&&` into a `when` condition",
4918 ).exactly_one_underline(r#"User::"alice" && principal in Group::"jane_friends""#).build());
4919 });
4920 }
4921
4922 #[test]
4923 fn scope_or() {
4924 let p_src =
4925 r#"permit(principal == User::"alice" || principal == User::"bob", action, resource);"#;
4926 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4927 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
4928 "expected an entity uid or matching template slot, found a `||` expression"
4929 ).help(
4930 "the policy scope can only contain one constraint per variable. Consider moving the second operand of this `||` into a new policy",
4931 ).exactly_one_underline(r#"User::"alice" || principal == User::"bob""#).build());
4932 });
4933 }
4934
4935 #[test]
4936 fn scope_action_in_set_set() {
4937 let p_src = r#"permit(principal, action in [[Action::"view"]], resource);"#;
4938 assert_matches!(parse_policy_or_template(None, p_src), Err(e) => {
4939 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("expected single entity uid, found set of entity uids").exactly_one_underline(r#"[Action::"view"]"#).build());
4940 });
4941 }
4942
4943 #[test]
4944 fn scope_unexpected_nested_sets() {
4945 let policy = r#"
4946 permit (
4947 principal == [[User::"alice"]],
4948 action,
4949 resource
4950 );
4951 "#;
4952 assert_matches!(
4953 parse_policy(None, policy),
4954 Err(e) => {
4955 expect_n_errors(policy, &e, 1);
4956 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
4957 "expected single entity uid or template slot, found set of entity uids",
4958 ).exactly_one_underline(r#"[[User::"alice"]]"#).build());
4959 }
4960 );
4961
4962 let policy = r#"
4963 permit (
4964 principal,
4965 action,
4966 resource == [[?resource]]
4967 );
4968 "#;
4969 assert_matches!(
4970 parse_policy(None, policy),
4971 Err(e) => {
4972 expect_n_errors(policy, &e, 1);
4973 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
4974 "expected single entity uid or template slot, found set of entity uids",
4975 ).exactly_one_underline("[[?resource]]").build());
4976 }
4977 );
4978
4979 let policy = r#"
4980 permit (
4981 principal,
4982 action in [[[Action::"act"]]],
4983 resource
4984 );
4985 "#;
4986 assert_matches!(
4987 parse_policy(None, policy),
4988 Err(e) => {
4989 expect_n_errors(policy, &e, 1);
4990 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
4991 "expected single entity uid, found set of entity uids",
4992 ).exactly_one_underline(r#"[[Action::"act"]]"#).build());
4993 }
4994 );
4995 }
4996
4997 #[test]
4998 fn unsupported_ops() {
4999 let src = "1/2";
5000 assert_matches!(parse_expr(src), Err(e) => {
5001 expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("division is not supported").exactly_one_underline("1/2").build());
5002 });
5003 let src = "7 % 3";
5004 assert_matches!(parse_expr(src), Err(e) => {
5005 expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("remainder/modulo is not supported").exactly_one_underline("7 % 3").build());
5006 });
5007 let src = "7 = 3";
5008 assert_matches!(parse_expr(src), Err(e) => {
5009 expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("'=' is not a valid operator in Cedar").exactly_one_underline("7 = 3").help("try using '==' instead").build());
5010 });
5011 }
5012
5013 #[test]
5014 fn over_unary() {
5015 let src = "!!!!!!false";
5016 assert_matches!(parse_expr(src), Err(e) => {
5017 expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5018 "too many occurrences of `!`",
5019 ).help(
5020 "cannot chain more the 4 applications of a unary operator"
5021 ).exactly_one_underline("!!!!!!false").build());
5022 });
5023 let src = "-------0";
5024 assert_matches!(parse_expr(src), Err(e) => {
5025 expect_err(src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5026 "too many occurrences of `-`",
5027 ).help(
5028 "cannot chain more the 4 applications of a unary operator"
5029 ).exactly_one_underline("-------0").build());
5030 });
5031 }
5032
5033 #[test]
5034 fn arbitrary_variables() {
5035 #[track_caller]
5036 fn expect_arbitrary_var(name: &str) {
5037 assert_matches!(parse_expr(name), Err(e) => {
5038 expect_err(name, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5039 &format!("invalid variable: {name}"),
5040 ).help(
5041 &format!("the valid Cedar variables are `principal`, `action`, `resource`, and `context`; did you mean to enclose `{name}` in quotes to make a string?"),
5042 ).exactly_one_underline(name).build());
5043 })
5044 }
5045 expect_arbitrary_var("foo::principal");
5046 expect_arbitrary_var("bar::action");
5047 expect_arbitrary_var("baz::resource");
5048 expect_arbitrary_var("buz::context");
5049 expect_arbitrary_var("foo::principal");
5050 expect_arbitrary_var("foo::bar::principal");
5051 expect_arbitrary_var("principal::foo");
5052 expect_arbitrary_var("principal::foo::bar");
5053 expect_arbitrary_var("foo::principal::bar");
5054 expect_arbitrary_var("foo");
5055 expect_arbitrary_var("foo::bar");
5056 expect_arbitrary_var("foo::bar::baz");
5057 }
5058
5059 #[test]
5060 fn empty_clause() {
5061 #[track_caller]
5062 fn expect_empty_clause(policy: &str, clause: &str) {
5063 assert_matches!(parse_policy_or_template(None, policy), Err(e) => {
5064 expect_err(policy, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5065 &format!("`{clause}` condition clause cannot be empty")
5066 ).exactly_one_underline(&format!("{clause} {{}}")).build());
5067 })
5068 }
5069
5070 expect_empty_clause("permit(principal, action, resource) when {};", "when");
5071 expect_empty_clause("permit(principal, action, resource) unless {};", "unless");
5072 expect_empty_clause(
5073 "permit(principal, action, resource) when { principal has foo } when {};",
5074 "when",
5075 );
5076 expect_empty_clause(
5077 "permit(principal, action, resource) when { principal has foo } unless {};",
5078 "unless",
5079 );
5080 expect_empty_clause(
5081 "permit(principal, action, resource) when {} unless { resource.bar };",
5082 "when",
5083 );
5084 expect_empty_clause(
5085 "permit(principal, action, resource) unless {} unless { resource.bar };",
5086 "unless",
5087 );
5088 }
5089
5090 #[test]
5091 fn namespaced_attr() {
5092 #[track_caller]
5093 fn expect_namespaced_attr(expr: &str, name: &str) {
5094 assert_matches!(parse_expr(expr), Err(e) => {
5095 expect_err(expr, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5096 &format!("`{name}` cannot be used as an attribute as it contains a namespace")
5097 ).exactly_one_underline(name).build());
5098 })
5099 }
5100
5101 expect_namespaced_attr("principal has foo::bar", "foo::bar");
5102 expect_namespaced_attr("principal has foo::bar::baz", "foo::bar::baz");
5103 expect_namespaced_attr("principal has foo::principal", "foo::principal");
5104 expect_namespaced_attr("{foo::bar: 1}", "foo::bar");
5105
5106 let expr = "principal has if::foo";
5107 assert_matches!(parse_expr(expr), Err(e) => {
5108 expect_err(expr, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5109 "this identifier is reserved and cannot be used: if"
5110 ).exactly_one_underline("if").build());
5111 })
5112 }
5113
5114 #[test]
5115 fn reserved_ident_var() {
5116 #[track_caller]
5117 fn expect_reserved_ident(name: &str, reserved: &str) {
5118 assert_matches!(parse_expr(name), Err(e) => {
5119 expect_err(name, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
5120 &format!("this identifier is reserved and cannot be used: {reserved}"),
5121 ).exactly_one_underline(reserved).build());
5122 })
5123 }
5124 expect_reserved_ident("if::principal", "if");
5125 expect_reserved_ident("then::action", "then");
5126 expect_reserved_ident("else::resource", "else");
5127 expect_reserved_ident("true::context", "true");
5128 expect_reserved_ident("false::bar::principal", "false");
5129 expect_reserved_ident("foo::in::principal", "in");
5130 expect_reserved_ident("foo::is::bar::principal", "is");
5131 }
5132
5133 #[test]
5134 fn reserved_namespace() {
5135 assert_matches!(parse_expr(r#"__cedar::"""#),
5136 Err(errs) if matches!(errs.as_ref().first(),
5137 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5138 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar".parse::<InternalName>().unwrap())));
5139 assert_matches!(parse_expr(r#"__cedar::A::"""#),
5140 Err(errs) if matches!(errs.as_ref().first(),
5141 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5142 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar::A".parse::<InternalName>().unwrap())));
5143 assert_matches!(parse_expr(r#"A::__cedar::B::"""#),
5144 Err(errs) if matches!(errs.as_ref().first(),
5145 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5146 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "A::__cedar::B".parse::<InternalName>().unwrap())));
5147 assert_matches!(parse_expr(r#"[A::"", __cedar::Action::"action"]"#),
5148 Err(errs) if matches!(errs.as_ref().first(),
5149 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5150 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar::Action".parse::<InternalName>().unwrap())));
5151 assert_matches!(parse_expr(r#"principal is __cedar::A"#),
5152 Err(errs) if matches!(errs.as_ref().first(),
5153 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5154 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar::A".parse::<InternalName>().unwrap())));
5155 assert_matches!(parse_expr(r#"__cedar::decimal("0.0")"#),
5156 Err(errs) if matches!(errs.as_ref().first(),
5157 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5158 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar::decimal".parse::<InternalName>().unwrap())));
5159 assert_matches!(parse_expr(r#"ip("").__cedar()"#),
5160 Err(errs) if matches!(errs.as_ref().first(),
5161 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5162 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar".parse::<InternalName>().unwrap())));
5163 assert_matches!(parse_expr(r#"{__cedar: 0}"#),
5164 Err(errs) if matches!(errs.as_ref().first(),
5165 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5166 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar".parse::<InternalName>().unwrap())));
5167 assert_matches!(parse_expr(r#"{a: 0}.__cedar"#),
5168 Err(errs) if matches!(errs.as_ref().first(),
5169 ParseError::ToAST(to_ast_err) if matches!(to_ast_err.kind(),
5170 ToASTErrorKind::ReservedNamespace(ReservedNameError(n)) if *n == "__cedar".parse::<InternalName>().unwrap())));
5171 assert_matches!(
5173 parse_policy(
5174 None,
5175 r#"@__cedar("foo") permit(principal, action, resource);"#
5176 ),
5177 Ok(_)
5178 );
5179 }
5180
5181 #[test]
5182 fn arbitrary_name_attr_access() {
5183 let src = "foo.attr";
5184 assert_matches!(parse_expr(src), Err(e) => {
5185 expect_err(src, &miette::Report::new(e),
5186 &ExpectedErrorMessageBuilder::error("invalid member access `foo.attr`, `foo` has no fields or methods")
5187 .exactly_one_underline("foo.attr")
5188 .build()
5189 );
5190 });
5191
5192 let src = r#"foo["attr"]"#;
5193 assert_matches!(parse_expr(src), Err(e) => {
5194 expect_err(src, &miette::Report::new(e),
5195 &ExpectedErrorMessageBuilder::error(r#"invalid indexing expression `foo["attr"]`, `foo` has no fields"#)
5196 .exactly_one_underline(r#"foo["attr"]"#)
5197 .build()
5198 );
5199 });
5200
5201 let src = r#"foo["\n"]"#;
5202 assert_matches!(parse_expr(src), Err(e) => {
5203 expect_err(src, &miette::Report::new(e),
5204 &ExpectedErrorMessageBuilder::error(r#"invalid indexing expression `foo["\n"]`, `foo` has no fields"#)
5205 .exactly_one_underline(r#"foo["\n"]"#)
5206 .build()
5207 );
5208 });
5209 }
5210
5211 #[test]
5212 fn extended_has() {
5213 assert_matches!(
5214 parse_policy(
5215 None,
5216 r#"
5217 permit(
5218 principal is User,
5219 action == Action::"preview",
5220 resource == Movie::"Blockbuster"
5221) when {
5222 principal has contactInfo.address.zip &&
5223 principal.contactInfo.address.zip == "90210"
5224};
5225 "#
5226 ),
5227 Ok(_)
5228 );
5229
5230 assert_matches!(parse_expr(r#"context has a.b"#), Ok(e) => {
5231 assert!(e.eq_shape(&parse_expr(r#"(context has a) && (context.a has b)"#).unwrap()));
5232 });
5233
5234 assert_matches!(parse_expr(r#"context has a.b.c"#), Ok(e) => {
5235 assert!(e.eq_shape(&parse_expr(r#"((context has a) && (context.a has b)) && (context.a.b has c)"#).unwrap()));
5236 });
5237
5238 let policy = r#"permit(principal, action, resource) when {
5239 principal has a.if
5240 };"#;
5241 assert_matches!(
5242 parse_policy(None, policy),
5243 Err(e) => {
5244 expect_n_errors(policy, &e, 1);
5245 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5246 "this identifier is reserved and cannot be used: if",
5247 ).exactly_one_underline(r#"if"#).build());
5248 }
5249 );
5250 let policy = r#"permit(principal, action, resource) when {
5251 principal has if.a
5252 };"#;
5253 assert_matches!(
5254 parse_policy(None, policy),
5255 Err(e) => {
5256 expect_n_errors(policy, &e, 1);
5257 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5258 "this identifier is reserved and cannot be used: if",
5259 ).exactly_one_underline(r#"if"#).build());
5260 }
5261 );
5262 let policy = r#"permit(principal, action, resource) when {
5263 principal has true.if
5264 };"#;
5265 assert_matches!(
5266 parse_policy(None, policy),
5267 Err(e) => {
5268 expect_n_errors(policy, &e, 1);
5269 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5270 "this identifier is reserved and cannot be used: true",
5271 ).exactly_one_underline(r#"true"#).build());
5272 }
5273 );
5274 let policy = r#"permit(principal, action, resource) when {
5275 principal has a.__cedar
5276 };"#;
5277 assert_matches!(
5278 parse_policy(None, policy),
5279 Err(e) => {
5280 expect_n_errors(policy, &e, 1);
5281 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5282 "The name `__cedar` contains `__cedar`, which is reserved",
5283 ).exactly_one_underline(r#"__cedar"#).build());
5284 }
5285 );
5286
5287 let help_msg = "valid RHS of a `has` operation is either a sequence of identifiers separated by `.` or a string literal";
5288
5289 let policy = r#"permit(principal, action, resource) when {
5290 principal has 1 + 1
5291 };"#;
5292 assert_matches!(
5293 parse_policy(None, policy),
5294 Err(e) => {
5295 expect_n_errors(policy, &e, 1);
5296 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5297 "invalid RHS of a `has` operation: 1 + 1",
5298 ).help(help_msg).
5299 exactly_one_underline(r#"1 + 1"#).build());
5300 }
5301 );
5302 let policy = r#"permit(principal, action, resource) when {
5303 principal has a - 1
5304 };"#;
5305 assert_matches!(
5306 parse_policy(None, policy),
5307 Err(e) => {
5308 expect_n_errors(policy, &e, 1);
5309 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5310 "invalid RHS of a `has` operation: a - 1",
5311 ).help(help_msg).exactly_one_underline(r#"a - 1"#).build());
5312 }
5313 );
5314 let policy = r#"permit(principal, action, resource) when {
5315 principal has a*3 + 1
5316 };"#;
5317 assert_matches!(
5318 parse_policy(None, policy),
5319 Err(e) => {
5320 expect_n_errors(policy, &e, 1);
5321 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5322 "invalid RHS of a `has` operation: a * 3 + 1",
5323 ).help(help_msg).exactly_one_underline(r#"a*3 + 1"#).build());
5324 }
5325 );
5326 let policy = r#"permit(principal, action, resource) when {
5327 principal has 3*a
5328 };"#;
5329 assert_matches!(
5330 parse_policy(None, policy),
5331 Err(e) => {
5332 expect_n_errors(policy, &e, 1);
5333 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5334 "invalid RHS of a `has` operation: 3 * a",
5335 ).help(help_msg).exactly_one_underline(r#"3*a"#).build());
5336 }
5337 );
5338 let policy = r#"permit(principal, action, resource) when {
5339 principal has -a.b
5340 };"#;
5341 assert_matches!(
5342 parse_policy(None, policy),
5343 Err(e) => {
5344 expect_n_errors(policy, &e, 1);
5345 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5346 "invalid RHS of a `has` operation: -a.b",
5347 ).help(help_msg).exactly_one_underline(r#"-a.b"#).build());
5348 }
5349 );
5350 let policy = r#"permit(principal, action, resource) when {
5351 principal has !a.b
5352 };"#;
5353 assert_matches!(
5354 parse_policy(None, policy),
5355 Err(e) => {
5356 expect_n_errors(policy, &e, 1);
5357 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5358 "invalid RHS of a `has` operation: !a.b",
5359 ).help(help_msg).exactly_one_underline(r#"!a.b"#).build());
5360 }
5361 );
5362 let policy = r#"permit(principal, action, resource) when {
5363 principal has a::b.c
5364 };"#;
5365 assert_matches!(
5366 parse_policy(None, policy),
5367 Err(e) => {
5368 expect_n_errors(policy, &e, 1);
5369 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5370 "`a::b.c` cannot be used as an attribute as it contains a namespace",
5371 ).exactly_one_underline(r#"a::b"#).build());
5372 }
5373 );
5374 let policy = r#"permit(principal, action, resource) when {
5375 principal has A::""
5376 };"#;
5377 assert_matches!(
5378 parse_policy(None, policy),
5379 Err(e) => {
5380 expect_n_errors(policy, &e, 1);
5381 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5382 "invalid RHS of a `has` operation: A::\"\"",
5383 ).help(help_msg).exactly_one_underline(r#"A::"""#).build());
5384 }
5385 );
5386 let policy = r#"permit(principal, action, resource) when {
5387 principal has A::"".a
5388 };"#;
5389 assert_matches!(
5390 parse_policy(None, policy),
5391 Err(e) => {
5392 expect_n_errors(policy, &e, 1);
5393 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5394 "invalid RHS of a `has` operation: A::\"\".a",
5395 ).help(help_msg).exactly_one_underline(r#"A::"""#).build());
5396 }
5397 );
5398 let policy = r#"permit(principal, action, resource) when {
5399 principal has ?principal
5400 };"#;
5401 assert_matches!(
5402 parse_policy(None, policy),
5403 Err(e) => {
5404 expect_n_errors(policy, &e, 1);
5405 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5406 "invalid RHS of a `has` operation: ?principal",
5407 ).help(help_msg).exactly_one_underline(r#"?principal"#).build());
5408 }
5409 );
5410 let policy = r#"permit(principal, action, resource) when {
5411 principal has ?principal.a
5412 };"#;
5413 assert_matches!(
5414 parse_policy(None, policy),
5415 Err(e) => {
5416 expect_n_errors(policy, &e, 1);
5417 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5418 "invalid RHS of a `has` operation: ?principal.a",
5419 ).help(help_msg).exactly_one_underline(r#"?principal"#).build());
5420 }
5421 );
5422 let policy = r#"permit(principal, action, resource) when {
5423 principal has (b).a
5424 };"#;
5425 assert_matches!(
5426 parse_policy(None, policy),
5427 Err(e) => {
5428 expect_n_errors(policy, &e, 1);
5429 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5430 "invalid RHS of a `has` operation: (b).a",
5431 ).help(help_msg).exactly_one_underline(r#"(b)"#).build());
5432 }
5433 );
5434 let policy = r#"permit(principal, action, resource) when {
5435 principal has [b].a
5436 };"#;
5437 assert_matches!(
5438 parse_policy(None, policy),
5439 Err(e) => {
5440 expect_n_errors(policy, &e, 1);
5441 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5442 "invalid RHS of a `has` operation: [b].a",
5443 ).help(help_msg).exactly_one_underline(r#"[b]"#).build());
5444 }
5445 );
5446 let policy = r#"permit(principal, action, resource) when {
5447 principal has {b:1}.a
5448 };"#;
5449 assert_matches!(
5450 parse_policy(None, policy),
5451 Err(e) => {
5452 expect_n_errors(policy, &e, 1);
5453 expect_some_error_matches(policy, &e, &ExpectedErrorMessageBuilder::error(
5454 "invalid RHS of a `has` operation: {b: 1}.a",
5455 ).help(help_msg).exactly_one_underline(r#"{b:1}"#).build());
5456 }
5457 );
5458 }
5459
5460 #[cfg(feature = "tolerant-ast")]
5461 #[track_caller]
5462 fn assert_parse_policy_allows_errors(text: &str) -> ast::StaticPolicy {
5463 text_to_cst::parse_policy_tolerant(text)
5464 .expect("failed parser")
5465 .to_policy_tolerant(ast::PolicyID::from_string("id"))
5466 .unwrap_or_else(|errs| {
5467 panic!("failed conversion to AST:\n{:?}", miette::Report::new(errs))
5468 })
5469 }
5470
5471 #[cfg(feature = "tolerant-ast")]
5472 #[track_caller]
5473 fn assert_parse_policy_allows_errors_fails(text: &str) -> ParseErrors {
5474 let result = text_to_cst::parse_policy_tolerant(text)
5475 .expect("failed parser")
5476 .to_policy_tolerant(ast::PolicyID::from_string("id"));
5477 match result {
5478 Ok(policy) => {
5479 panic!("conversion to AST should have failed, but succeeded with:\n{policy}")
5480 }
5481 Err(errs) => errs,
5482 }
5483 }
5484
5485 #[cfg(feature = "tolerant-ast")]
5487 #[test]
5488 fn parsing_with_errors_succeeds_with_empty_when() {
5489 let src = r#"
5490 permit(principal, action, resource) when {};
5491 "#;
5492 assert_parse_policy_allows_errors(src);
5493 }
5494
5495 #[cfg(feature = "tolerant-ast")]
5497 #[test]
5498 fn parsing_with_errors_succeeds_with_invalid_variable_in_when() {
5499 let src = r#"
5500 permit(principal, action, resource) when { pri };
5501 "#;
5502 assert_parse_policy_allows_errors(src);
5503 }
5504
5505 #[cfg(feature = "tolerant-ast")]
5506 #[test]
5507 fn parsing_with_errors_succeeds_with_invalid_method() {
5508 let src = r#"
5509 permit(principal, action, resource) when { ip(principal.ip).i() };
5510 "#;
5511 assert_parse_policy_allows_errors(src);
5512 }
5513
5514 #[cfg(feature = "tolerant-ast")]
5515 #[test]
5516 fn parsing_with_errors_succeeds_with_invalid_uid_resource_constraint() {
5517 let src = r#"
5518 permit (
5519 principal,
5520 action,
5521 resource in H
5522 )
5523 when { true };
5524 "#;
5525 assert_parse_policy_allows_errors(src);
5526 }
5527
5528 #[cfg(feature = "tolerant-ast")]
5529 #[test]
5530 fn parsing_with_errors_succeeds_with_invalid_uid_principal_constraint() {
5531 let src = r#"
5532 permit (
5533 principal in J,
5534 action,
5535 resource
5536 )
5537 when { true };
5538 "#;
5539 assert_parse_policy_allows_errors(src);
5540 }
5541
5542 #[cfg(feature = "tolerant-ast")]
5543 #[test]
5544 fn invalid_action_constraint_in_a_list() {
5545 let src = r#"
5546 permit (
5547 principal,
5548 action in [A],
5549 resource
5550 )
5551 when { true };
5552 "#;
5553 assert_parse_policy_allows_errors(src);
5554 }
5555
5556 #[cfg(feature = "tolerant-ast")]
5557 #[test]
5558 fn parsing_with_errors_succeeds_with_invalid_bracket_for_in() {
5559 let src = r#"
5560 permit (
5561 principal,
5562 action,
5563 resource in [
5564 )
5565 when { true };
5566 "#;
5567 assert_parse_policy_allows_errors(src);
5568 }
5569
5570 #[cfg(feature = "tolerant-ast")]
5571 #[test]
5572 fn parsing_with_errors_succeeds_with_missing_second_operand_eq_and_in() {
5573 let src_eq_cases = [
5575 r#"permit(principal ==, action, resource);"#,
5576 r#"permit(principal, action ==, resource);"#,
5577 r#"permit(principal, action, resource ==);"#,
5578 r#"permit(principal ==, action ==, resource);"#,
5579 r#"permit(principal, action ==, resource ==);"#,
5580 r#"permit(principal ==, action, resource ==);"#,
5581 r#"permit(principal ==, action ==, resource ==);"#,
5582 ];
5583
5584 for src in src_eq_cases.iter() {
5585 assert_parse_policy_allows_errors(src);
5586 }
5587
5588 let src_in_cases = [
5590 r#"permit(principal in, action, resource);"#,
5591 r#"permit(principal, action in, resource);"#,
5592 r#"permit(principal, action, resource in);"#,
5593 r#"permit(principal in, action in, resource);"#,
5594 r#"permit(principal, action in, resource in);"#,
5595 r#"permit(principal in, action, resource in);"#,
5596 r#"permit(principal in, action in, resource in);"#,
5597 ];
5598
5599 for src in src_in_cases.iter() {
5600 assert_parse_policy_allows_errors(src);
5601 }
5602
5603 let src_in_cases = [
5605 r#"permit(principal is something in, action, resource);"#,
5606 r#"permit(principal, action, resource is something in);"#,
5607 ];
5608 for src in src_in_cases.iter() {
5609 assert_parse_policy_allows_errors(src);
5610 }
5611 }
5612
5613 #[cfg(feature = "tolerant-ast")]
5614 #[test]
5615 fn parsing_with_errors_succeeds_with_invalid_variable_in_when_missing_operand() {
5616 let src = r#"
5617 permit(principal, action, resource) when { principal == };
5618 "#;
5619 assert_parse_policy_allows_errors(src);
5620
5621 let src = r#"
5622 permit(principal, action, resource) when { resource == };
5623 "#;
5624 assert_parse_policy_allows_errors(src);
5625
5626 let src = r#"
5627 permit(principal, action, resource) when { action == };
5628 "#;
5629 assert_parse_policy_allows_errors(src);
5630
5631 let src = r#"
5632 permit(principal, action, resource) when { principal == User::test && action == };
5633 "#;
5634 assert_parse_policy_allows_errors(src);
5635
5636 let src = r#"
5637 permit(principal, action, resource) when { action == && principal == User::test};
5638 "#;
5639 assert_parse_policy_allows_errors(src);
5640 }
5641
5642 #[cfg(feature = "tolerant-ast")]
5643 #[test]
5644 fn parsing_with_errors_succeeds_with_missing_second_operand_is() {
5645 let src = r#"
5646 permit(principal is something in, action, resource);
5647 "#;
5648 assert_parse_policy_allows_errors(src);
5649 }
5650
5651 #[cfg(feature = "tolerant-ast")]
5652 #[test]
5653 fn show_policy1_errors_enabled() {
5654 let src = r#"
5655 permit(principal:p,action:a,resource:r)when{w}unless{u}advice{"doit"};
5656 "#;
5657 let errs = assert_parse_policy_allows_errors_fails(src);
5658 expect_n_errors(src, &errs, 4);
5659 expect_some_error_matches(
5660 src,
5661 &errs,
5662 &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
5663 .help("try using `is` instead")
5664 .exactly_one_underline("p")
5665 .build(),
5666 );
5667 expect_some_error_matches(
5668 src,
5669 &errs,
5670 &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
5671 .help("try using `is` instead")
5672 .exactly_one_underline("a")
5673 .build(),
5674 );
5675 expect_some_error_matches(
5676 src,
5677 &errs,
5678 &ExpectedErrorMessageBuilder::error("type constraints using `:` are not supported")
5679 .help("try using `is` instead")
5680 .exactly_one_underline("r")
5681 .build(),
5682 );
5683 expect_some_error_matches(
5684 src,
5685 &errs,
5686 &ExpectedErrorMessageBuilder::error("invalid policy condition: advice")
5687 .help("condition must be either `when` or `unless`")
5688 .exactly_one_underline("advice")
5689 .build(),
5690 );
5691 }
5692
5693 #[cfg(feature = "tolerant-ast")]
5694 #[test]
5695 fn show_policy2_errors_enabled() {
5696 let src = r#"
5697 permit(principal,action,resource)when{true};
5698 "#;
5699 assert_parse_policy_allows_errors(src);
5700 }
5701
5702 #[cfg(feature = "tolerant-ast")]
5703 #[test]
5704 fn show_policy3_errors_enabled() {
5705 let src = r#"
5706 permit(principal in User::"jane",action,resource);
5707 "#;
5708 assert_parse_policy_allows_errors(src);
5709 }
5710
5711 #[cfg(feature = "tolerant-ast")]
5712 #[test]
5713 fn show_policy4_errors_enabled() {
5714 let src = r#"
5715 forbid(principal in User::"jane",action,resource)unless{
5716 context.group != "friends"
5717 };
5718 "#;
5719 assert_parse_policy_allows_errors(src);
5720 }
5721
5722 #[cfg(feature = "tolerant-ast")]
5723 #[test]
5724 fn invalid_policy_errors_enabled() {
5725 let src = r#"
5726 permit(principal,;
5727 "#;
5728 assert_parse_policy_allows_errors(src);
5729 }
5730
5731 #[cfg(feature = "tolerant-ast")]
5732 #[test]
5733 fn invalid_policy_with_trailing_dot_errors_enabled() {
5734 let src = r#"
5735 permit(principal, action, resource) { principal. };
5736 "#;
5737 assert_parse_policy_allows_errors(src);
5738 }
5739
5740 #[cfg(feature = "tolerant-ast")]
5741 #[test]
5742 fn missing_entity_identifier_errors_enabled() {
5743 let src = r#"
5744 permit(principal, action == Action::, resource);
5745 "#;
5746 assert_parse_policy_allows_errors(src);
5747 }
5748
5749 #[cfg(feature = "tolerant-ast")]
5750 #[test]
5751 fn single_annotation_errors_enabled() {
5752 let policy = assert_parse_policy_allows_errors(
5754 r#"
5755 @anno("good annotation")permit(principal,action,resource);
5756 "#,
5757 );
5758 assert_matches!(
5759 policy.annotation(&ast::AnyId::new_unchecked("anno")),
5760 Some(annotation) => assert_eq!(annotation.as_ref(), "good annotation")
5761 );
5762 }
5763
5764 #[cfg(feature = "tolerant-ast")]
5765 #[test]
5766 fn duplicate_annotations_error_errors_enabled() {
5767 let src = r#"
5769 @anno("good annotation")
5770 @anno2("good annotation")
5771 @anno("oops, duplicate")
5772 permit(principal,action,resource);
5773 "#;
5774 let errs = assert_parse_policy_allows_errors_fails(src);
5775 expect_n_errors(src, &errs, 1);
5777 expect_some_error_matches(
5778 src,
5779 &errs,
5780 &ExpectedErrorMessageBuilder::error("duplicate annotation: @anno")
5781 .exactly_one_underline("@anno(\"oops, duplicate\")")
5782 .build(),
5783 );
5784 }
5785
5786 #[cfg(feature = "tolerant-ast")]
5787 #[test]
5788 fn multiple_policys_with_unparsable_policy_ok() {
5789 let policyset = text_to_cst::parse_policies_tolerant(
5791 r#"
5792 // POLICY 1
5793 @id("Photo.owner")
5794 permit (
5795 principal,
5796 action in
5797 [PhotoApp::Action::"viewPhoto",
5798 PhotoApp::Action::"editPhoto",
5799 PhotoApp::Action::"deletePhoto"],
5800 resource in PhotoApp::Application::"PhotoApp"
5801 )
5802 when { resource.owner == principal };
5803
5804 // POLICY2 - unparsable
5805 @id("label_private")
5806 forbid (
5807 principal,
5808 acti
5809
5810 // POLICY3 - unparsable because previous policy is missing a ";"
5811 @id("Photo.subjects")
5812 permit (
5813 principal,
5814 action == PhotoApp::Action::"viewPhoto",
5815 resource in PhotoApp::Application::"PhotoApp"
5816 )
5817 when { resource has subjects && resource.subjects.contains(principal) };
5818
5819 // POLICY 4
5820 @id("PhotoJudge")
5821 permit (
5822 principal in PhotoApp::Role::"PhotoJudge",
5823 action == PhotoApp::Action::"viewPhoto",
5824 resource in PhotoApp::Application::"PhotoApp"
5825 )
5826 when { resource.labels.contains("contest") }
5827 when { context has judgingSession && context.judgingSession == true };
5828 "#,
5829 )
5830 .expect("should parse")
5831 .to_policyset_tolerant()
5832 .unwrap_or_else(|errs| panic!("failed convert to AST:\n{:?}", miette::Report::new(errs)));
5833 policyset
5834 .get(&ast::PolicyID::from_string("policy0"))
5835 .expect("should be a policy");
5836 policyset
5837 .get(&ast::PolicyID::from_string("policy1"))
5838 .expect("should be a policy");
5839 policyset
5840 .get(&ast::PolicyID::from_string("policy2"))
5841 .expect("should be a policy");
5842 assert!(policyset
5843 .get(&ast::PolicyID::from_string("policy3"))
5844 .is_none());
5845 }
5846
5847 #[cfg(feature = "tolerant-ast")]
5848 #[test]
5849 fn fail_scope1_tolerant_ast() {
5850 let src = r#"
5851 permit(
5852 principal in [User::"jane",Group::"friends"],
5853 action,
5854 resource
5855 );
5856 "#;
5857 let errs = assert_parse_policy_allows_errors_fails(src);
5858 expect_n_errors(src, &errs, 1);
5859 expect_some_error_matches(
5860 src,
5861 &errs,
5862 &ExpectedErrorMessageBuilder::error(
5863 "expected single entity uid or template slot, found set of entity uids",
5864 )
5865 .exactly_one_underline(r#"[User::"jane",Group::"friends"]"#)
5866 .build(),
5867 );
5868 }
5869
5870 #[cfg(feature = "tolerant-ast")]
5871 #[test]
5872 fn fail_scope2_tolerant_ast() {
5873 let src = r#"
5874 permit(
5875 principal in User::"jane",
5876 action == if true then Photo::"view" else Photo::"edit",
5877 resource
5878 );
5879 "#;
5880 let errs = assert_parse_policy_allows_errors_fails(src);
5881 expect_n_errors(src, &errs, 1);
5882 expect_some_error_matches(
5883 src,
5884 &errs,
5885 &ExpectedErrorMessageBuilder::error("expected an entity uid, found an `if` expression")
5886 .exactly_one_underline(r#"if true then Photo::"view" else Photo::"edit""#)
5887 .build(),
5888 );
5889 }
5890
5891 #[cfg(feature = "tolerant-ast")]
5892 #[test]
5893 fn invalid_slot_tolerant_ast() {
5894 let invalid_policies = [
5895 (
5896 r#"permit(principal == ?resource, action, resource);"#,
5897 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
5898 ),
5899 (
5900 r#"permit(principal in ?resource, action, resource);"#,
5901 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?resource instead of ?principal").exactly_one_underline("?resource").build(),
5902 ),
5903 (
5904 r#"permit(principal == ?foo, action, resource);"#,
5905 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?foo instead of ?principal").exactly_one_underline("?foo").build(),
5906 ),
5907 (
5908 r#"permit(principal in ?foo, action, resource);"#,
5909 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?foo instead of ?principal").exactly_one_underline("?foo").build(),
5910 ),
5911
5912 (
5913 r#"permit(principal, action, resource == ?principal);"#,
5914 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
5915 ),
5916 (
5917 r#"permit(principal, action, resource in ?principal);"#,
5918 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?principal instead of ?resource").exactly_one_underline("?principal").build(),
5919 ),
5920 (
5921 r#"permit(principal, action, resource == ?baz);"#,
5922 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?baz instead of ?resource").exactly_one_underline("?baz").build(),
5923 ),
5924 (
5925 r#"permit(principal, action, resource in ?baz);"#,
5926 ExpectedErrorMessageBuilder::error("expected an entity uid or matching template slot, found ?baz instead of ?resource").exactly_one_underline("?baz").build(),
5927 ),
5928 (
5929 r#"permit(principal, action, resource) when { principal == ?foo};"#,
5930 ExpectedErrorMessageBuilder::error(
5931 "`?foo` is not a valid template slot",
5932 ).help(
5933 "a template slot may only be `?principal` or `?resource`",
5934 ).exactly_one_underline("?foo").build(),
5935 ),
5936
5937 (
5938 r#"permit(principal, action == ?action, resource);"#,
5939 ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?action").build(),
5940 ),
5941 (
5942 r#"permit(principal, action in ?action, resource);"#,
5943 ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?action").build(),
5944 ),
5945 (
5946 r#"permit(principal, action == ?principal, resource);"#,
5947 ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?principal").build(),
5948 ),
5949 (
5950 r#"permit(principal, action in ?principal, resource);"#,
5951 ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?principal").build(),
5952 ),
5953 (
5954 r#"permit(principal, action == ?resource, resource);"#,
5955 ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?resource").build(),
5956 ),
5957 (
5958 r#"permit(principal, action in ?resource, resource);"#,
5959 ExpectedErrorMessageBuilder::error("expected single entity uid or set of entity uids, found template slot").exactly_one_underline("?resource").build(),
5960 ),
5961 (
5962 r#"permit(principal, action in [?bar], resource);"#,
5963 ExpectedErrorMessageBuilder::error("expected single entity uid, found template slot").exactly_one_underline("?bar").build(),
5964 ),
5965 ];
5966
5967 for (p_src, expected) in invalid_policies {
5968 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
5969 expect_err(p_src, &miette::Report::new(e), &expected);
5970 });
5971 let forbid_src = format!("forbid{}", &p_src[6..]);
5972 assert_matches!(parse_policy_or_template_tolerant(None, &forbid_src), Err(e) => {
5973 expect_err(forbid_src.as_str(), &miette::Report::new(e), &expected);
5974 });
5975 }
5976 }
5977
5978 #[cfg(feature = "tolerant-ast")]
5979 #[test]
5980 fn missing_scope_constraint_tolerant_ast() {
5981 let p_src = "permit();";
5982 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
5983 expect_err(
5984 p_src,
5985 &miette::Report::new(e),
5986 &ExpectedErrorMessageBuilder::error("this policy is missing the `principal` variable in the scope")
5987 .exactly_one_underline("")
5988 .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
5989 .build()
5990 );
5991 });
5992 let p_src = "permit(principal);";
5993 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
5994 expect_err(
5995 p_src,
5996 &miette::Report::new(e),
5997 &ExpectedErrorMessageBuilder::error("this policy is missing the `action` variable in the scope")
5998 .exactly_one_underline("")
5999 .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
6000 .build()
6001 );
6002 });
6003 let p_src = "permit(principal, action);";
6004 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6005 expect_err(
6006 p_src,
6007 &miette::Report::new(e),
6008 &ExpectedErrorMessageBuilder::error("this policy is missing the `resource` variable in the scope")
6009 .exactly_one_underline("")
6010 .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
6011 .build()
6012 );
6013 });
6014 }
6015
6016 #[cfg(feature = "tolerant-ast")]
6017 #[test]
6018 fn invalid_scope_constraint_tolerant() {
6019 let p_src = "permit(foo, action, resource);";
6020 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6021 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6022 "found an invalid variable in the policy scope: foo",
6023 ).help(
6024 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6025 ).exactly_one_underline("foo").build());
6026 });
6027
6028 let p_src = "permit(resource, action, resource);";
6029 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6030 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6031 "found the variable `resource` where the variable `principal` must be used",
6032 ).help(
6033 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6034 ).exactly_one_underline("resource").build());
6035 });
6036
6037 let p_src = "permit(principal, principal, resource);";
6038 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6039 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6040 "found the variable `principal` where the variable `action` must be used",
6041 ).help(
6042 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6043 ).exactly_one_underline("principal").build());
6044 });
6045 let p_src = "permit(principal, if, resource);";
6046 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6047 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6048 "found an invalid variable in the policy scope: if",
6049 ).help(
6050 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6051 ).exactly_one_underline("if").build());
6052 });
6053
6054 let p_src = "permit(principal, action, like);";
6055 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6056 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6057 "found an invalid variable in the policy scope: like",
6058 ).help(
6059 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6060 ).exactly_one_underline("like").build());
6061 });
6062 let p_src = "permit(principal, action, principal);";
6063 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6064 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6065 "found the variable `principal` where the variable `resource` must be used",
6066 ).help(
6067 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6068 ).exactly_one_underline("principal").build());
6069 });
6070 let p_src = "permit(principal, action, action);";
6071 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6072 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6073 "found the variable `action` where the variable `resource` must be used",
6074 ).help(
6075 "policy scopes must contain a `principal`, `action`, and `resource` element in that order",
6076 ).exactly_one_underline("action").build());
6077 });
6078 }
6079
6080 #[cfg(feature = "tolerant-ast")]
6081 #[test]
6082 fn invalid_scope_operator_tolerant() {
6083 let p_src = r#"permit(principal > User::"alice", action, resource);"#;
6084 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6085 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6086 "invalid operator in the policy scope: >",
6087 ).help(
6088 "policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"
6089 ).exactly_one_underline("principal > User::\"alice\"").build());
6090 });
6091 let p_src = r#"permit(principal, action != Action::"view", resource);"#;
6092 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6093 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6094 "invalid operator in the action scope: !=",
6095 ).help(
6096 "action scope clauses can only use `==` or `in`"
6097 ).exactly_one_underline("action != Action::\"view\"").build());
6098 });
6099 let p_src = r#"permit(principal, action, resource <= Folder::"things");"#;
6100 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6101 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6102 "invalid operator in the policy scope: <=",
6103 ).help(
6104 "policy scope clauses can only use `==`, `in`, `is`, or `_ is _ in _`"
6105 ).exactly_one_underline("resource <= Folder::\"things\"").build());
6106 });
6107 let p_src = r#"permit(principal = User::"alice", action, resource);"#;
6108 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6109 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6110 "'=' is not a valid operator in Cedar",
6111 ).help(
6112 "try using '==' instead",
6113 ).exactly_one_underline("principal = User::\"alice\"").build());
6114 });
6115 let p_src = r#"permit(principal, action = Action::"act", resource);"#;
6116 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6117 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6118 "'=' is not a valid operator in Cedar",
6119 ).help(
6120 "try using '==' instead",
6121 ).exactly_one_underline("action = Action::\"act\"").build());
6122 });
6123 let p_src = r#"permit(principal, action, resource = Photo::"photo");"#;
6124 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6125 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6126 "'=' is not a valid operator in Cedar",
6127 ).help(
6128 "try using '==' instead",
6129 ).exactly_one_underline("resource = Photo::\"photo\"").build());
6130 });
6131 }
6132
6133 #[cfg(feature = "tolerant-ast")]
6134 #[test]
6135 fn scope_action_eq_set_tolerant() {
6136 let p_src = r#"permit(principal, action == [Action::"view", Action::"edit"], resource);"#;
6137 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6138 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("expected single entity uid, found set of entity uids").exactly_one_underline(r#"[Action::"view", Action::"edit"]"#).build());
6139 });
6140 }
6141
6142 #[cfg(feature = "tolerant-ast")]
6143 #[test]
6144 fn scope_compare_to_string_tolerant() {
6145 let p_src = r#"permit(principal == "alice", action, resource);"#;
6146 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6147 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6148 r#"expected an entity uid or matching template slot, found literal `"alice"`"#
6149 ).help(
6150 "try including the entity type if you intended this string to be an entity uid"
6151 ).exactly_one_underline(r#""alice""#).build());
6152 });
6153 let p_src = r#"permit(principal in "bob_friends", action, resource);"#;
6154 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6155 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6156 r#"expected an entity uid or matching template slot, found literal `"bob_friends"`"#
6157 ).help(
6158 "try including the entity type if you intended this string to be an entity uid"
6159 ).exactly_one_underline(r#""bob_friends""#).build());
6160 });
6161 let p_src = r#"permit(principal, action, resource in "jane_photos");"#;
6162 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6163 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6164 r#"expected an entity uid or matching template slot, found literal `"jane_photos"`"#
6165 ).help(
6166 "try including the entity type if you intended this string to be an entity uid"
6167 ).exactly_one_underline(r#""jane_photos""#).build());
6168 });
6169 let p_src = r#"permit(principal, action in ["view_actions"], resource);"#;
6170 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6171 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6172 r#"expected an entity uid, found literal `"view_actions"`"#
6173 ).help(
6174 "try including the entity type if you intended this string to be an entity uid"
6175 ).exactly_one_underline(r#""view_actions""#).build());
6176 });
6177 }
6178
6179 #[cfg(feature = "tolerant-ast")]
6180 #[test]
6181 fn scope_and_tolerant() {
6182 let p_src = r#"permit(principal == User::"alice" && principal in Group::"jane_friends", action, resource);"#;
6183 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6184 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6185 "expected an entity uid or matching template slot, found a `&&` expression"
6186 ).help(
6187 "the policy scope can only contain one constraint per variable. Consider moving the second operand of this `&&` into a `when` condition",
6188 ).exactly_one_underline(r#"User::"alice" && principal in Group::"jane_friends""#).build());
6189 });
6190 }
6191
6192 #[cfg(feature = "tolerant-ast")]
6193 #[test]
6194 fn scope_or_tolerant() {
6195 let p_src =
6196 r#"permit(principal == User::"alice" || principal == User::"bob", action, resource);"#;
6197 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6198 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(
6199 "expected an entity uid or matching template slot, found a `||` expression"
6200 ).help(
6201 "the policy scope can only contain one constraint per variable. Consider moving the second operand of this `||` into a new policy",
6202 ).exactly_one_underline(r#"User::"alice" || principal == User::"bob""#).build());
6203 });
6204 }
6205
6206 #[cfg(feature = "tolerant-ast")]
6207 #[test]
6208 fn scope_action_in_set_set_tolerant() {
6209 let p_src = r#"permit(principal, action in [[Action::"view"]], resource);"#;
6210 assert_matches!(parse_policy_or_template_tolerant(None, p_src), Err(e) => {
6211 expect_err(p_src, &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("expected single entity uid, found set of entity uids").exactly_one_underline(r#"[Action::"view"]"#).build());
6212 });
6213 }
6214
6215 #[cfg(feature = "tolerant-ast")]
6216 fn parse_policy_or_template_tolerant(
6217 id: Option<ast::PolicyID>,
6218 text: &str,
6219 ) -> Result<ast::Template> {
6220 let id = id.unwrap_or_else(|| ast::PolicyID::from_string("policy0"));
6221 let cst = text_to_cst::parse_policy_tolerant(text)?;
6222 cst.to_template_tolerant(id)
6223 }
6224}