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