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