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