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