1#![allow(
18 clippy::needless_return,
19 reason = r#"
20 This module makes use of `return` to exit early with a particular exit code.
21 For consistency, it also uses `return` in some places where it could be
22 omitted.
23"#
24)]
25
26use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
27#[cfg(feature = "analyze")]
28use itertools::Itertools;
29use miette::{miette, IntoDiagnostic, NamedSource, Report, Result, WrapErr};
30use owo_colors::OwoColorize;
31use serde::de::{DeserializeSeed, IntoDeserializer};
32use serde::{Deserialize, Deserializer, Serialize};
33use std::collections::BTreeSet;
34use std::io::{BufReader, Write};
35use std::{
36 collections::HashMap,
37 fmt::{self, Display},
38 fs::OpenOptions,
39 path::{Path, PathBuf},
40 process::{ExitCode, Termination},
41 str::FromStr,
42 time::Instant,
43};
44
45use cedar_policy::*;
46use cedar_policy_formatter::{policies_str_to_pretty, Config};
47
48#[derive(Parser, Debug)]
50#[command(author, version, about, long_about = None)] pub struct Cli {
52 #[command(subcommand)]
53 pub command: Commands,
54 #[arg(
56 global = true,
57 short = 'f',
58 long = "error-format",
59 env = "CEDAR_ERROR_FORMAT",
60 default_value_t,
61 value_enum
62 )]
63 pub err_fmt: ErrorFormat,
64}
65
66#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
67pub enum ErrorFormat {
68 #[default]
71 Human,
72 Plain,
75 Json,
77}
78
79impl Display for ErrorFormat {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(
82 f,
83 "{}",
84 match self {
85 ErrorFormat::Human => "human",
86 ErrorFormat::Plain => "plain",
87 ErrorFormat::Json => "json",
88 }
89 )
90 }
91}
92
93#[derive(Subcommand, Debug)]
94pub enum Commands {
95 Authorize(AuthorizeArgs),
97 Evaluate(EvaluateArgs),
99 Validate(ValidateArgs),
101 CheckParse(CheckParseArgs),
106 Link(LinkArgs),
108 Format(FormatArgs),
110 TranslatePolicy(TranslatePolicyArgs),
112 TranslateSchema(TranslateSchemaArgs),
114 Visualize(VisualizeArgs),
117 New(NewArgs),
119 PartiallyAuthorize(PartiallyAuthorizeArgs),
121 Tpe(TpeArgs),
123 #[clap(verbatim_doc_comment)] RunTests(RunTestsArgs),
134 Symcc(SymccArgs),
136 LanguageVersion,
138}
139
140#[derive(Args, Debug)]
141pub struct TranslatePolicyArgs {
142 #[arg(long)]
144 pub direction: PolicyTranslationDirection,
145 #[arg(short = 'p', long = "policies", value_name = "FILE")]
148 pub input_file: Option<String>,
149}
150
151#[derive(Debug, Clone, Copy, ValueEnum)]
153pub enum PolicyTranslationDirection {
154 CedarToJson,
156 JsonToCedar,
158}
159
160#[derive(Args, Debug)]
161pub struct TranslateSchemaArgs {
162 #[arg(long)]
164 pub direction: SchemaTranslationDirection,
165 #[arg(short = 's', long = "schema", value_name = "FILE")]
168 pub input_file: Option<String>,
169}
170
171#[derive(Debug, Clone, Copy, ValueEnum)]
173pub enum SchemaTranslationDirection {
174 JsonToCedar,
176 CedarToJson,
178 CedarToJsonWithResolvedTypes,
183}
184
185#[derive(Debug, Default, Clone, Copy, ValueEnum)]
186pub enum SchemaFormat {
187 #[default]
189 Cedar,
190 Json,
192}
193
194#[derive(Debug, Clone, Copy, ValueEnum)]
195pub enum ValidationMode {
196 Strict,
198 Permissive,
200 Partial,
202}
203
204#[derive(Args, Debug)]
205pub struct ValidateArgs {
206 #[command(flatten)]
208 pub schema: SchemaArgs,
209 #[command(flatten)]
211 pub policies: PoliciesArgs,
212 #[arg(long)]
214 pub deny_warnings: bool,
215 #[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
220 pub validation_mode: ValidationMode,
221 #[arg(long)]
223 pub level: Option<u32>,
224}
225
226#[derive(Args, Debug)]
227pub struct CheckParseArgs {
228 #[command(flatten)]
230 pub policies: OptionalPoliciesArgs,
231 #[arg(long)]
233 pub expression: Option<String>,
234 #[command(flatten)]
236 pub schema: OptionalSchemaArgs,
237 #[arg(long = "entities", value_name = "FILE")]
239 pub entities_file: Option<PathBuf>,
240}
241
242#[derive(Args, Debug)]
244pub struct RequestArgs {
245 #[arg(short = 'l', long)]
247 pub principal: Option<String>,
248 #[arg(short, long)]
250 pub action: Option<String>,
251 #[arg(short, long)]
253 pub resource: Option<String>,
254 #[arg(short, long = "context", value_name = "FILE")]
257 pub context_json_file: Option<String>,
258 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
263 pub request_json_file: Option<String>,
264 #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
267 pub request_validation: bool,
268}
269
270#[cfg(feature = "tpe")]
271#[derive(Args, Debug)]
273pub struct TpeRequestArgs {
274 #[arg(long)]
276 pub principal_type: Option<String>,
277 #[arg(long)]
279 pub principal_eid: Option<String>,
280 #[arg(short, long)]
282 pub action: Option<String>,
283 #[arg(long)]
285 pub resource_type: Option<String>,
286 #[arg(long)]
288 pub resource_eid: Option<String>,
289 #[arg(short, long = "context", value_name = "FILE")]
292 pub context_json_file: Option<String>,
293 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal_type", "principal_eid", "action", "resource_type", "resource_eid", "context_json_file"])]
298 pub request_json_file: Option<String>,
299}
300
301#[cfg(feature = "partial-eval")]
302#[derive(Args, Debug)]
304pub struct PartialRequestArgs {
305 #[arg(short = 'l', long)]
307 pub principal: Option<String>,
308 #[arg(short, long)]
310 pub action: Option<String>,
311 #[arg(short, long)]
313 pub resource: Option<String>,
314 #[arg(short, long = "context", value_name = "FILE")]
317 pub context_json_file: Option<String>,
318 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
323 pub request_json_file: Option<String>,
324}
325
326impl RequestArgs {
327 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
334 match &self.request_json_file {
335 Some(jsonfile) => {
336 let jsonstring = std::fs::read_to_string(jsonfile)
337 .into_diagnostic()
338 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
339 let qjson: RequestJSON = serde_json::from_str(&jsonstring)
340 .into_diagnostic()
341 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
342 let principal = qjson.principal.parse().wrap_err_with(|| {
343 format!("failed to parse principal in {jsonfile} as entity Uid")
344 })?;
345 let action = qjson.action.parse().wrap_err_with(|| {
346 format!("failed to parse action in {jsonfile} as entity Uid")
347 })?;
348 let resource = qjson.resource.parse().wrap_err_with(|| {
349 format!("failed to parse resource in {jsonfile} as entity Uid")
350 })?;
351 let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
352 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
353 Request::new(
354 principal,
355 action,
356 resource,
357 context,
358 if self.request_validation {
359 schema
360 } else {
361 None
362 },
363 )
364 .map_err(|e| miette!("{e}"))
365 }
366 None => {
367 let principal = self
368 .principal
369 .as_ref()
370 .map(|s| {
371 s.parse().wrap_err_with(|| {
372 format!("failed to parse principal {s} as entity Uid")
373 })
374 })
375 .transpose()?;
376 let action = self
377 .action
378 .as_ref()
379 .map(|s| {
380 s.parse()
381 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
382 })
383 .transpose()?;
384 let resource = self
385 .resource
386 .as_ref()
387 .map(|s| {
388 s.parse()
389 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
390 })
391 .transpose()?;
392 let context: Context = match &self.context_json_file {
393 None => Context::empty(),
394 Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
395 Ok(f) => Context::from_json_file(
396 f,
397 schema.and_then(|s| Some((s, action.as_ref()?))),
398 )
399 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
400 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
401 format!("error while loading context from {jsonfile}")
402 })?,
403 },
404 };
405 match (principal, action, resource) {
406 (Some(principal), Some(action), Some(resource)) => Request::new(
407 principal,
408 action,
409 resource,
410 context,
411 if self.request_validation {
412 schema
413 } else {
414 None
415 },
416 )
417 .map_err(|e| miette!("{e}")),
418 _ => Err(miette!(
419 "All three (`principal`, `action`, `resource`) variables must be specified"
420 )),
421 }
422 }
423 }
424 }
425}
426
427#[cfg(feature = "tpe")]
428impl TpeRequestArgs {
429 fn get_request(&self, schema: &Schema) -> Result<PartialRequest> {
430 let qjson: TpeRequestJSON = match self.request_json_file.as_ref() {
431 Some(jsonfile) => {
432 let jsonstring = std::fs::read_to_string(jsonfile)
433 .into_diagnostic()
434 .wrap_err_with(|| format!("failed to open request json file {jsonfile}"))?;
435 serde_json::from_str(&jsonstring)
436 .into_diagnostic()
437 .wrap_err_with(|| format!("failed to parse context-json file {jsonfile}"))?
438 }
439 None => TpeRequestJSON {
440 principal_type: self
441 .principal_type
442 .clone()
443 .ok_or_else(|| miette!("principal type must be specified"))?,
444 principal_eid: self.principal_eid.clone(),
445 action: self
446 .action
447 .clone()
448 .ok_or_else(|| miette!("action must be specified"))?,
449 resource_type: self
450 .resource_type
451 .clone()
452 .ok_or_else(|| miette!("resource type must be specified"))?,
453 resource_eid: self.resource_eid.clone(),
454 context: self
455 .context_json_file
456 .as_ref()
457 .map(|jsonfile| {
458 let jsonstring = std::fs::read_to_string(jsonfile)
459 .into_diagnostic()
460 .wrap_err_with(|| {
461 format!("failed to open context-json file {jsonfile}")
462 })?;
463 serde_json::from_str(&jsonstring)
464 .into_diagnostic()
465 .wrap_err_with(|| {
466 format!("failed to parse context-json file {jsonfile}")
467 })
468 })
469 .transpose()?,
470 },
471 };
472 let action: EntityUid = qjson.action.parse()?;
473 Ok(PartialRequest::new(
474 PartialEntityUid::new(
475 qjson.principal_type.parse()?,
476 qjson.principal_eid.as_ref().map(EntityId::new),
477 ),
478 action.clone(),
479 PartialEntityUid::new(
480 qjson.resource_type.parse()?,
481 qjson.resource_eid.as_ref().map(EntityId::new),
482 ),
483 qjson
484 .context
485 .map(|val| Context::from_json_value(val, Some((schema, &action))))
486 .transpose()?,
487 schema,
488 )?)
489 }
490}
491
492#[cfg(feature = "partial-eval")]
493impl PartialRequestArgs {
494 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
495 let mut builder = RequestBuilder::default();
496 let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
497 Some(jsonfile) => {
498 let jsonstring = std::fs::read_to_string(jsonfile)
499 .into_diagnostic()
500 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
501 serde_json::from_str(&jsonstring)
502 .into_diagnostic()
503 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
504 }
505 None => PartialRequestJSON {
506 principal: self.principal.clone(),
507 action: self.action.clone(),
508 resource: self.resource.clone(),
509 context: self
510 .context_json_file
511 .as_ref()
512 .map(|jsonfile| {
513 let jsonstring = std::fs::read_to_string(jsonfile)
514 .into_diagnostic()
515 .wrap_err_with(|| {
516 format!("failed to open context-json file {jsonfile}")
517 })?;
518 serde_json::from_str(&jsonstring)
519 .into_diagnostic()
520 .wrap_err_with(|| {
521 format!("failed to parse context-json file {jsonfile}")
522 })
523 })
524 .transpose()?,
525 },
526 };
527
528 if let Some(principal) = qjson
529 .principal
530 .map(|s| {
531 s.parse()
532 .wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
533 })
534 .transpose()?
535 {
536 builder = builder.principal(principal);
537 }
538
539 let action = qjson
540 .action
541 .map(|s| {
542 s.parse::<EntityUid>()
543 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
544 })
545 .transpose()?;
546
547 if let Some(action_ref) = &action {
548 builder = builder.action(action_ref.clone());
549 }
550
551 if let Some(resource) = qjson
552 .resource
553 .map(|s| {
554 s.parse()
555 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
556 })
557 .transpose()?
558 {
559 builder = builder.resource(resource);
560 }
561
562 if let Some(context) = qjson
563 .context
564 .map(|json| {
565 Context::from_json_value(
566 json.clone(),
567 schema.and_then(|s| Some((s, action.as_ref()?))),
568 )
569 .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
570 })
571 .transpose()?
572 {
573 builder = builder.context(context);
574 }
575
576 if let Some(schema) = schema {
577 builder
578 .schema(schema)
579 .build()
580 .wrap_err_with(|| "failed to build request with validation".to_string())
581 } else {
582 Ok(builder.build())
583 }
584 }
585}
586
587#[derive(Args, Debug)]
589pub struct PoliciesArgs {
590 #[arg(short, long = "policies", value_name = "FILE")]
592 pub policies_file: Option<String>,
593 #[arg(long = "policy-format", default_value_t, value_enum)]
595 pub policy_format: PolicyFormat,
596 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
598 pub template_linked_file: Option<String>,
599}
600
601impl PoliciesArgs {
602 fn get_policy_set(&self) -> Result<PolicySet> {
604 let mut pset = match self.policy_format {
605 PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
606 PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
607 }?;
608 if let Some(links_filename) = self.template_linked_file.as_ref() {
609 add_template_links_to_set(links_filename, &mut pset)?;
610 }
611 Ok(pset)
612 }
613}
614
615#[derive(Args, Debug)]
618pub struct OptionalPoliciesArgs {
619 #[arg(short, long = "policies", value_name = "FILE")]
621 pub policies_file: Option<String>,
622 #[arg(long = "policy-format", default_value_t, value_enum)]
624 pub policy_format: PolicyFormat,
625 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
628 pub template_linked_file: Option<String>,
629}
630
631impl OptionalPoliciesArgs {
632 fn get_policy_set(&self) -> Result<Option<PolicySet>> {
635 match &self.policies_file {
636 None => Ok(None),
637 Some(policies_file) => {
638 let pargs = PoliciesArgs {
639 policies_file: Some(policies_file.clone()),
640 policy_format: self.policy_format,
641 template_linked_file: self.template_linked_file.clone(),
642 };
643 pargs.get_policy_set().map(Some)
644 }
645 }
646 }
647}
648
649#[derive(Args, Debug)]
651pub struct SchemaArgs {
652 #[arg(short, long = "schema", value_name = "FILE")]
654 pub schema_file: PathBuf,
655 #[arg(long, value_enum, default_value_t)]
657 pub schema_format: SchemaFormat,
658}
659
660impl SchemaArgs {
661 fn get_schema(&self) -> Result<Schema> {
663 read_schema_from_file(&self.schema_file, self.schema_format)
664 }
665}
666
667#[derive(Args, Debug)]
670pub struct OptionalSchemaArgs {
671 #[arg(short, long = "schema", value_name = "FILE")]
673 pub schema_file: Option<PathBuf>,
674 #[arg(long, value_enum, default_value_t)]
676 pub schema_format: SchemaFormat,
677}
678
679impl OptionalSchemaArgs {
680 fn get_schema(&self) -> Result<Option<Schema>> {
682 let Some(schema_file) = &self.schema_file else {
683 return Ok(None);
684 };
685 read_schema_from_file(schema_file, self.schema_format).map(Some)
686 }
687}
688
689fn read_schema_from_file(path: impl AsRef<Path>, format: SchemaFormat) -> Result<Schema> {
690 let path = path.as_ref();
691 let schema_src = read_from_file(path, "schema")?;
692 match format {
693 SchemaFormat::Json => Schema::from_json_str(&schema_src)
694 .wrap_err_with(|| format!("failed to parse schema from file {}", path.display())),
695 SchemaFormat::Cedar => {
696 let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)
697 .wrap_err_with(|| format!("failed to parse schema from file {}", path.display()))?;
698 for warning in warnings {
699 let report = miette::Report::new(warning);
700 eprintln!("{report:?}");
701 }
702 Ok(schema)
703 }
704 }
705}
706
707#[derive(Args, Debug)]
708pub struct AuthorizeArgs {
709 #[command(flatten)]
711 pub request: RequestArgs,
712 #[command(flatten)]
714 pub policies: PoliciesArgs,
715 #[command(flatten)]
720 pub schema: OptionalSchemaArgs,
721 #[arg(long = "entities", value_name = "FILE")]
723 pub entities_file: String,
724 #[arg(short, long)]
726 pub verbose: bool,
727 #[arg(short, long)]
729 pub timing: bool,
730}
731
732#[cfg(feature = "tpe")]
733#[derive(Args, Debug)]
734pub struct TpeArgs {
735 #[command(flatten)]
737 pub request: TpeRequestArgs,
738 #[command(flatten)]
740 pub policies: PoliciesArgs,
741 #[command(flatten)]
746 pub schema: SchemaArgs,
747 #[arg(long = "entities", value_name = "FILE")]
749 pub entities_file: String,
750 #[arg(short, long)]
752 pub timing: bool,
753}
754
755#[cfg(feature = "partial-eval")]
756#[derive(Args, Debug)]
757pub struct PartiallyAuthorizeArgs {
758 #[command(flatten)]
760 pub request: PartialRequestArgs,
761 #[command(flatten)]
763 pub policies: PoliciesArgs,
764 #[command(flatten)]
769 pub schema: OptionalSchemaArgs,
770 #[arg(long = "entities", value_name = "FILE")]
772 pub entities_file: String,
773 #[arg(short, long)]
775 pub timing: bool,
776}
777
778#[cfg(not(feature = "tpe"))]
779#[derive(Debug, Args)]
780pub struct TpeArgs;
781
782#[cfg(not(feature = "partial-eval"))]
783#[derive(Debug, Args)]
784pub struct PartiallyAuthorizeArgs;
785
786#[derive(Args, Debug)]
787pub struct RunTestsArgs {
788 #[command(flatten)]
790 pub policies: PoliciesArgs,
791 #[arg(long, value_name = "FILE")]
793 pub tests: String,
794 #[command(flatten)]
795 pub schema: OptionalSchemaArgs,
796}
797
798#[derive(Args, Debug)]
799pub struct VisualizeArgs {
800 #[arg(long = "entities", value_name = "FILE")]
801 pub entities_file: String,
802}
803
804#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
805pub enum PolicyFormat {
806 #[default]
808 Cedar,
809 Json,
811}
812
813#[derive(Args, Debug)]
814pub struct LinkArgs {
815 #[command(flatten)]
817 pub policies: PoliciesArgs,
818 #[arg(long)]
820 pub template_id: String,
821 #[arg(short, long)]
823 pub new_id: String,
824 #[arg(short, long)]
826 pub arguments: Arguments,
827}
828
829#[derive(Args, Debug)]
830pub struct FormatArgs {
831 #[arg(short, long = "policies", value_name = "FILE")]
833 pub policies_file: Option<String>,
834
835 #[arg(short, long, value_name = "UINT", default_value_t = 80)]
837 pub line_width: usize,
838
839 #[arg(short, long, value_name = "INT", default_value_t = 2)]
841 pub indent_width: isize,
842
843 #[arg(short, long, group = "action", requires = "policies_file")]
845 pub write: bool,
846
847 #[arg(short, long, group = "action")]
849 pub check: bool,
850}
851
852#[derive(Args, Debug)]
853pub struct NewArgs {
854 #[arg(short, long, value_name = "DIR")]
856 pub name: String,
857}
858
859#[derive(Args, Debug)]
860pub struct SymccArgs {
861 #[command(subcommand)]
862 pub command: SymccCommands,
863 #[arg(long, env = "CVC5")]
865 pub cvc5_path: Option<PathBuf>,
866 #[arg(long)]
868 pub principal_type: String,
869 #[arg(long)]
871 pub action: String,
872 #[arg(long)]
874 pub resource_type: String,
875 #[command(flatten)]
877 pub schema: SchemaArgs,
878 #[arg(long, default_value_t = true, conflicts_with = "no_counterexample")]
880 pub counterexample: bool,
881 #[arg(long, default_value_t = false, conflicts_with = "counterexample")]
883 pub no_counterexample: bool,
884 #[arg(short, long)]
886 pub verbose: bool,
887}
888
889#[derive(Subcommand, Debug)]
890pub enum SymccCommands {
891 NeverErrors(SymccPoliciesArgs),
894 AlwaysMatches(SymccPoliciesArgs),
896 NeverMatches(SymccPoliciesArgs),
898
899 MatchesEquivalent(TwoPolicyArgs),
902 MatchesImplies(TwoPolicyArgs),
904 MatchesDisjoint(TwoPolicyArgs),
906
907 AlwaysAllows(SymccPoliciesArgs),
910 AlwaysDenies(SymccPoliciesArgs),
912
913 Equivalent(SymccTwoPoliciesArgs),
916 Implies(SymccTwoPoliciesArgs),
918 Disjoint(SymccTwoPoliciesArgs),
920}
921
922#[derive(Args, Debug)]
924pub struct SymccPoliciesArgs {
925 #[arg(short, long = "policies", value_name = "FILE")]
927 pub policies_file: Option<String>,
928 #[arg(long = "policy-format", default_value_t, value_enum)]
930 pub policy_format: PolicyFormat,
931}
932
933#[cfg(feature = "analyze")]
934impl SymccPoliciesArgs {
935 fn get_policy_set(&self) -> Result<PolicySet> {
937 match self.policy_format {
938 PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
939 PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
940 }
941 }
942}
943
944#[derive(Args, Debug)]
946pub struct TwoPolicyArgs {
947 #[arg(long = "policy1", value_name = "FILE")]
949 pub policy1_file: Option<String>,
950 #[arg(long = "policy1-format", default_value_t, value_enum)]
952 pub policy1_format: PolicyFormat,
953 #[arg(long = "policy2", value_name = "FILE")]
955 pub policy2_file: Option<String>,
956 #[arg(long = "policy2-format", default_value_t, value_enum)]
958 pub policy2_format: PolicyFormat,
959}
960
961#[cfg(feature = "analyze")]
962impl TwoPolicyArgs {
963 fn get_policy_set_1(&self) -> Result<PolicySet> {
964 let pargs = PoliciesArgs {
965 policies_file: self.policy1_file.clone(),
966 policy_format: self.policy1_format,
967 template_linked_file: None,
968 };
969 pargs.get_policy_set()
970 }
971
972 fn get_policy_set_2(&self) -> Result<PolicySet> {
973 let pargs = PoliciesArgs {
974 policies_file: self.policy2_file.clone(),
975 policy_format: self.policy2_format,
976 template_linked_file: None,
977 };
978 pargs.get_policy_set()
979 }
980}
981
982#[derive(Args, Debug)]
984pub struct SymccTwoPoliciesArgs {
985 #[arg(long = "policies1", value_name = "FILE")]
987 pub policies1_file: Option<String>,
988 #[arg(long = "policies1-format", default_value_t, value_enum)]
990 pub policies1_format: PolicyFormat,
991 #[arg(long = "policies2", value_name = "FILE")]
993 pub policies2_file: Option<String>,
994 #[arg(long = "policies2-format", default_value_t, value_enum)]
996 pub policies2_format: PolicyFormat,
997}
998
999#[cfg(feature = "analyze")]
1000impl SymccTwoPoliciesArgs {
1001 fn get_policy_set_1(&self) -> Result<PolicySet> {
1002 let pargs = SymccPoliciesArgs {
1003 policies_file: self.policies1_file.clone(),
1004 policy_format: self.policies1_format,
1005 };
1006 pargs.get_policy_set()
1007 }
1008
1009 fn get_policy_set_2(&self) -> Result<PolicySet> {
1010 let pargs = SymccPoliciesArgs {
1011 policies_file: self.policies2_file.clone(),
1012 policy_format: self.policies2_format,
1013 };
1014 pargs.get_policy_set()
1015 }
1016}
1017
1018#[derive(Clone, Debug, Deserialize)]
1020#[serde(try_from = "HashMap<String,String>")]
1021pub struct Arguments {
1022 pub data: HashMap<SlotId, String>,
1023}
1024
1025impl TryFrom<HashMap<String, String>> for Arguments {
1026 type Error = String;
1027
1028 fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
1029 Ok(Self {
1030 data: value
1031 .into_iter()
1032 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1033 .collect::<Result<HashMap<SlotId, String>, String>>()?,
1034 })
1035 }
1036}
1037
1038impl FromStr for Arguments {
1039 type Err = serde_json::Error;
1040
1041 fn from_str(s: &str) -> Result<Self, Self::Err> {
1042 serde_json::from_str(s)
1043 }
1044}
1045
1046#[derive(Clone, Debug, Deserialize)]
1048struct RequestJSON {
1049 #[serde(default)]
1051 principal: String,
1052 #[serde(default)]
1054 action: String,
1055 #[serde(default)]
1057 resource: String,
1058 context: serde_json::Value,
1060}
1061
1062#[cfg(feature = "partial-eval")]
1063#[derive(Deserialize)]
1065struct PartialRequestJSON {
1066 pub(self) principal: Option<String>,
1068 pub(self) action: Option<String>,
1070 pub(self) resource: Option<String>,
1072 pub(self) context: Option<serde_json::Value>,
1074}
1075
1076#[cfg(feature = "tpe")]
1077#[derive(Deserialize)]
1079struct TpeRequestJSON {
1080 pub(self) principal_type: String,
1082 pub(self) principal_eid: Option<String>,
1084 pub(self) action: String,
1086 pub(self) resource_type: String,
1088 pub(self) resource_eid: Option<String>,
1090 pub(self) context: Option<serde_json::Value>,
1092}
1093
1094#[derive(Args, Debug)]
1095pub struct EvaluateArgs {
1096 #[command(flatten)]
1098 pub request: RequestArgs,
1099 #[command(flatten)]
1104 pub schema: OptionalSchemaArgs,
1105 #[arg(long = "entities", value_name = "FILE")]
1108 pub entities_file: Option<String>,
1109 #[arg(value_name = "EXPRESSION")]
1111 pub expression: String,
1112}
1113
1114#[derive(Eq, PartialEq, Debug, Copy, Clone)]
1115pub enum CedarExitCode {
1116 Success,
1119 Failure,
1121 AuthorizeDeny,
1124 ValidationFailure,
1127 #[cfg(any(feature = "partial-eval", feature = "tpe"))]
1128 Unknown,
1131}
1132
1133impl Termination for CedarExitCode {
1134 fn report(self) -> ExitCode {
1135 match self {
1136 CedarExitCode::Success => ExitCode::SUCCESS,
1137 CedarExitCode::Failure => ExitCode::FAILURE,
1138 CedarExitCode::AuthorizeDeny => ExitCode::from(2),
1139 CedarExitCode::ValidationFailure => ExitCode::from(3),
1140 #[cfg(any(feature = "partial-eval", feature = "tpe"))]
1141 CedarExitCode::Unknown => ExitCode::SUCCESS,
1142 }
1143 }
1144}
1145
1146pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
1147 if args.policies.policies_file.is_none()
1150 && args.schema.schema_file.is_none()
1151 && args.entities_file.is_none()
1152 && args.expression.is_none()
1153 {
1154 let pargs = PoliciesArgs {
1155 policies_file: None, policy_format: args.policies.policy_format,
1157 template_linked_file: args.policies.template_linked_file.clone(),
1158 };
1159 match pargs.get_policy_set() {
1160 Ok(_) => return CedarExitCode::Success,
1161 Err(e) => {
1162 println!("{e:?}");
1163 return CedarExitCode::Failure;
1164 }
1165 }
1166 }
1167
1168 #[expect(
1169 clippy::useless_let_if_seq,
1170 reason = "exit_code is mutated by later expressions"
1171 )]
1172 let mut exit_code = CedarExitCode::Success;
1173 if let Err(e) = args.policies.get_policy_set() {
1174 println!("{e:?}");
1175 exit_code = CedarExitCode::Failure;
1176 }
1177 if let Some(e) = args
1178 .expression
1179 .as_ref()
1180 .and_then(|expr| Expression::from_str(expr).err())
1181 {
1182 println!("{:?}", Report::new(e));
1183 exit_code = CedarExitCode::Failure;
1184 }
1185 let schema = match args.schema.get_schema() {
1186 Ok(schema) => schema,
1187 Err(e) => {
1188 println!("{e:?}");
1189 exit_code = CedarExitCode::Failure;
1190 None
1191 }
1192 };
1193 if let Some(e) = args
1194 .entities_file
1195 .as_ref()
1196 .and_then(|e| load_entities(e, schema.as_ref()).err())
1197 {
1198 println!("{e:?}");
1199 exit_code = CedarExitCode::Failure;
1200 }
1201 exit_code
1202}
1203
1204pub fn validate(args: &ValidateArgs) -> CedarExitCode {
1205 let mode = match args.validation_mode {
1206 ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
1207 ValidationMode::Permissive => {
1208 #[cfg(not(feature = "permissive-validate"))]
1209 {
1210 eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
1211 return CedarExitCode::Failure;
1212 }
1213 #[cfg(feature = "permissive-validate")]
1214 cedar_policy::ValidationMode::Permissive
1215 }
1216 ValidationMode::Partial => {
1217 #[cfg(not(feature = "partial-validate"))]
1218 {
1219 eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
1220 return CedarExitCode::Failure;
1221 }
1222 #[cfg(feature = "partial-validate")]
1223 cedar_policy::ValidationMode::Partial
1224 }
1225 };
1226
1227 let pset = match args.policies.get_policy_set() {
1228 Ok(pset) => pset,
1229 Err(e) => {
1230 println!("{e:?}");
1231 return CedarExitCode::Failure;
1232 }
1233 };
1234
1235 let schema = match args.schema.get_schema() {
1236 Ok(schema) => schema,
1237 Err(e) => {
1238 println!("{e:?}");
1239 return CedarExitCode::Failure;
1240 }
1241 };
1242
1243 let validator = Validator::new(schema);
1244
1245 let result = if let Some(level) = args.level {
1246 validator.validate_with_level(&pset, mode, level)
1247 } else {
1248 validator.validate(&pset, mode)
1249 };
1250
1251 if !result.validation_passed()
1252 || (args.deny_warnings && !result.validation_passed_without_warnings())
1253 {
1254 println!(
1255 "{:?}",
1256 Report::new(result).wrap_err("policy set validation failed")
1257 );
1258 CedarExitCode::ValidationFailure
1259 } else {
1260 println!(
1261 "{:?}",
1262 Report::new(result).wrap_err("policy set validation passed")
1263 );
1264 CedarExitCode::Success
1265 }
1266}
1267
1268pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
1269 println!();
1270 let schema = match args.schema.get_schema() {
1271 Ok(opt) => opt,
1272 Err(e) => {
1273 println!("{e:?}");
1274 return (CedarExitCode::Failure, EvalResult::Bool(false));
1275 }
1276 };
1277 let request = match args.request.get_request(schema.as_ref()) {
1278 Ok(q) => q,
1279 Err(e) => {
1280 println!("{e:?}");
1281 return (CedarExitCode::Failure, EvalResult::Bool(false));
1282 }
1283 };
1284 let expr =
1285 match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
1286 Ok(expr) => expr,
1287 Err(e) => {
1288 println!("{:?}", e.with_source_code(args.expression.clone()));
1289 return (CedarExitCode::Failure, EvalResult::Bool(false));
1290 }
1291 };
1292 let entities = match &args.entities_file {
1293 None => Entities::empty(),
1294 Some(file) => match load_entities(file, schema.as_ref()) {
1295 Ok(entities) => entities,
1296 Err(e) => {
1297 println!("{e:?}");
1298 return (CedarExitCode::Failure, EvalResult::Bool(false));
1299 }
1300 },
1301 };
1302 match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
1303 {
1304 Err(e) => {
1305 println!("{e:?}");
1306 return (CedarExitCode::Failure, EvalResult::Bool(false));
1307 }
1308 Ok(result) => {
1309 println!("{result}");
1310 return (CedarExitCode::Success, result);
1311 }
1312 }
1313}
1314
1315pub fn link(args: &LinkArgs) -> CedarExitCode {
1316 if let Err(err) = link_inner(args) {
1317 println!("{err:?}");
1318 CedarExitCode::Failure
1319 } else {
1320 CedarExitCode::Success
1321 }
1322}
1323
1324pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
1325 match load_entities(&args.entities_file, None) {
1326 Ok(entities) => {
1327 println!("{}", entities.to_dot_str());
1328 CedarExitCode::Success
1329 }
1330 Err(report) => {
1331 eprintln!("{report:?}");
1332 CedarExitCode::Failure
1333 }
1334 }
1335}
1336
1337fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
1342 let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
1343 let config = Config {
1344 line_width: args.line_width,
1345 indent_width: args.indent_width,
1346 };
1347 let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
1348 let are_policies_equivalent = policies_str == formatted_policy;
1349
1350 match &args.policies_file {
1351 Some(policies_file) if args.write => {
1352 let mut file = OpenOptions::new()
1353 .write(true)
1354 .truncate(true)
1355 .open(policies_file)
1356 .into_diagnostic()
1357 .wrap_err(format!("failed to open {policies_file} for writing"))?;
1358 file.write_all(formatted_policy.as_bytes())
1359 .into_diagnostic()
1360 .wrap_err(format!(
1361 "failed to write formatted policies to {policies_file}"
1362 ))?;
1363 }
1364 _ => print!("{formatted_policy}"),
1365 }
1366 Ok(are_policies_equivalent)
1367}
1368
1369pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
1370 match format_policies_inner(args) {
1371 Ok(false) if args.check => CedarExitCode::Failure,
1372 Err(err) => {
1373 println!("{err:?}");
1374 CedarExitCode::Failure
1375 }
1376 _ => CedarExitCode::Success,
1377 }
1378}
1379
1380fn translate_policy_to_cedar(
1381 json_src: Option<impl AsRef<Path> + std::marker::Copy>,
1382) -> Result<String> {
1383 let policy_set = read_json_policy_set(json_src)?;
1384 policy_set.to_cedar().ok_or_else(|| {
1385 miette!("Unable to translate policy set containing template linked policies.")
1386 })
1387}
1388
1389fn translate_policy_to_json(
1390 cedar_src: Option<impl AsRef<Path> + std::marker::Copy>,
1391) -> Result<String> {
1392 let policy_set = read_cedar_policy_set(cedar_src)?;
1393 let output = policy_set.to_json()?.to_string();
1394 Ok(output)
1395}
1396
1397fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
1398 let translate = match args.direction {
1399 PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
1400 PolicyTranslationDirection::JsonToCedar => translate_policy_to_cedar,
1401 };
1402 translate(args.input_file.as_ref())
1403}
1404
1405pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
1406 match translate_policy_inner(args) {
1407 Ok(sf) => {
1408 println!("{sf}");
1409 CedarExitCode::Success
1410 }
1411 Err(err) => {
1412 eprintln!("{err:?}");
1413 CedarExitCode::Failure
1414 }
1415 }
1416}
1417
1418fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
1419 let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
1420 let output = fragment.to_cedarschema()?;
1421 Ok(output)
1422}
1423
1424fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
1425 let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
1426 for warning in warnings {
1427 let report = miette::Report::new(warning);
1428 eprintln!("{report:?}");
1429 }
1430 let output = fragment.to_json_string()?;
1431 Ok(output)
1432}
1433
1434fn translate_schema_to_json_with_resolved_types(cedar_src: impl AsRef<str>) -> Result<String> {
1435 match cedar_policy::schema_str_to_json_with_resolved_types(cedar_src.as_ref()) {
1436 Ok((json_value, warnings)) => {
1437 for warning in &warnings {
1439 eprintln!("{warning}");
1440 }
1441
1442 serde_json::to_string_pretty(&json_value).into_diagnostic()
1444 }
1445 Err(error) => {
1446 Err(miette::Report::new(error))
1448 }
1449 }
1450}
1451
1452fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
1453 let translate = match args.direction {
1454 SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
1455 SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
1456 SchemaTranslationDirection::CedarToJsonWithResolvedTypes => {
1457 translate_schema_to_json_with_resolved_types
1458 }
1459 };
1460 read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate)
1461}
1462
1463pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
1464 match translate_schema_inner(args) {
1465 Ok(sf) => {
1466 println!("{sf}");
1467 CedarExitCode::Success
1468 }
1469 Err(err) => {
1470 eprintln!("{err:?}");
1471 CedarExitCode::Failure
1472 }
1473 }
1474}
1475
1476fn generate_schema(path: &Path) -> Result<()> {
1478 std::fs::write(
1479 path,
1480 serde_json::to_string_pretty(&serde_json::json!(
1481 {
1482 "": {
1483 "entityTypes": {
1484 "A": {
1485 "memberOfTypes": [
1486 "B"
1487 ]
1488 },
1489 "B": {
1490 "memberOfTypes": []
1491 },
1492 "C": {
1493 "memberOfTypes": []
1494 }
1495 },
1496 "actions": {
1497 "action": {
1498 "appliesTo": {
1499 "resourceTypes": [
1500 "C"
1501 ],
1502 "principalTypes": [
1503 "A",
1504 "B"
1505 ]
1506 }
1507 }
1508 }
1509 }
1510 }))
1511 .into_diagnostic()?,
1512 )
1513 .into_diagnostic()
1514}
1515
1516fn generate_policy(path: &Path) -> Result<()> {
1517 std::fs::write(
1518 path,
1519 r#"permit (
1520 principal in A::"a",
1521 action == Action::"action",
1522 resource == C::"c"
1523) when { true };
1524"#,
1525 )
1526 .into_diagnostic()
1527}
1528
1529fn generate_entities(path: &Path) -> Result<()> {
1530 std::fs::write(
1531 path,
1532 serde_json::to_string_pretty(&serde_json::json!(
1533 [
1534 {
1535 "uid": { "type": "A", "id": "a"} ,
1536 "attrs": {},
1537 "parents": [{"type": "B", "id": "b"}]
1538 },
1539 {
1540 "uid": { "type": "B", "id": "b"} ,
1541 "attrs": {},
1542 "parents": []
1543 },
1544 {
1545 "uid": { "type": "C", "id": "c"} ,
1546 "attrs": {},
1547 "parents": []
1548 }
1549 ]))
1550 .into_diagnostic()?,
1551 )
1552 .into_diagnostic()
1553}
1554
1555fn new_inner(args: &NewArgs) -> Result<()> {
1556 let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1557 std::fs::create_dir(dir).into_diagnostic()?;
1558 let schema_path = dir.join("schema.cedarschema.json");
1559 let policy_path = dir.join("policy.cedar");
1560 let entities_path = dir.join("entities.json");
1561 generate_schema(&schema_path)?;
1562 generate_policy(&policy_path)?;
1563 generate_entities(&entities_path)
1564}
1565
1566pub fn new(args: &NewArgs) -> CedarExitCode {
1567 if let Err(err) = new_inner(args) {
1568 println!("{err:?}");
1569 CedarExitCode::Failure
1570 } else {
1571 CedarExitCode::Success
1572 }
1573}
1574
1575pub fn language_version() -> CedarExitCode {
1576 let version = get_lang_version();
1577 println!(
1578 "Cedar language version: {}.{}",
1579 version.major, version.minor
1580 );
1581 CedarExitCode::Success
1582}
1583
1584#[cfg(not(feature = "analyze"))]
1585pub fn symcc(_: &SymccArgs) -> CedarExitCode {
1586 eprintln!("Cannot run `symcc`: this Cedar CLI was built without the 'analyze' feature enabled");
1587 CedarExitCode::Failure
1588}
1589
1590#[cfg(feature = "analyze")]
1591pub fn symcc(args: &SymccArgs) -> CedarExitCode {
1592 let rt = match tokio::runtime::Builder::new_multi_thread()
1593 .enable_all()
1594 .build()
1595 {
1596 Ok(rt) => rt,
1597 Err(e) => {
1598 eprintln!("Failed to initialize async runtime: {e}");
1599 return CedarExitCode::Failure;
1600 }
1601 };
1602
1603 rt.block_on(async {
1604 match symcc_async(args).await {
1605 Ok(()) => CedarExitCode::Success,
1606 Err(e) => {
1607 eprintln!("Analysis failed: {e:?}");
1608 CedarExitCode::Failure
1609 }
1610 }
1611 })
1612}
1613
1614#[cfg(feature = "analyze")]
1615fn initialize_solver(
1616 cvc5_path: &Option<PathBuf>,
1617) -> Result<cedar_policy_symcc::solver::LocalSolver> {
1618 match cvc5_path {
1619 Some(p) => cedar_policy_symcc::solver::LocalSolver::from_command(
1620 tokio::process::Command::new(p).args(["--lang", "smt", "--tlimit=60000"]),
1621 )
1622 .map_err(|e| {
1623 miette!(
1624 "CVC5 solver not found or failed to start at '{}': {e}",
1625 p.display()
1626 )
1627 }),
1628 None => cedar_policy_symcc::solver::LocalSolver::cvc5()
1629 .map_err(|e| miette!("CVC5 solver not found or failed to start: {e}")),
1630 }
1631}
1632
1633#[cfg(feature = "analyze")]
1634fn warn_if_contains_templates(pset: &PolicySet, name: &str) {
1635 let num_templates = pset.templates().count();
1636 if num_templates > 0 {
1637 let report = miette!(
1638 severity = miette::Severity::Warning,
1639 "{name} contains {num_templates} policy template(s), which will be ignored by analysis"
1640 );
1641 eprintln!("{report:?}");
1642 }
1643}
1644
1645#[cfg(feature = "analyze")]
1646fn load_single_policy(
1647 policies: &SymccPoliciesArgs,
1648 schema_args: &SchemaArgs,
1649) -> Result<(Policy, Schema)> {
1650 let pset = policies.get_policy_set()?;
1651 let schema = schema_args.get_schema()?;
1652 let policy = pset
1653 .policies()
1654 .exactly_one()
1655 .map_err(|e| miette!("Expected exactly one policy, found {}", e.count()))?
1656 .clone();
1657 Ok((policy, schema))
1658}
1659
1660#[cfg(feature = "analyze")]
1661fn load_two_policies(
1662 args: &TwoPolicyArgs,
1663 schema_args: &SchemaArgs,
1664) -> Result<(Policy, Policy, Schema)> {
1665 let pset1 = args.get_policy_set_1()?;
1666 let pset2 = args.get_policy_set_2()?;
1667 let schema = schema_args.get_schema()?;
1668 let p1 = pset1
1669 .policies()
1670 .exactly_one()
1671 .map_err(|e| {
1672 miette!(
1673 "Expected exactly one policy in --policy1, found {}",
1674 e.count()
1675 )
1676 })?
1677 .clone();
1678 let p2 = pset2
1679 .policies()
1680 .exactly_one()
1681 .map_err(|e| {
1682 miette!(
1683 "Expected exactly one policy in --policy2, found {}",
1684 e.count()
1685 )
1686 })?
1687 .clone();
1688 Ok((p1, p2, schema))
1689}
1690
1691#[cfg(feature = "analyze")]
1692fn load_policy_set(
1693 policies: &SymccPoliciesArgs,
1694 schema_args: &SchemaArgs,
1695) -> Result<(PolicySet, Schema)> {
1696 let pset = policies.get_policy_set()?;
1697 warn_if_contains_templates(&pset, "policy set");
1698 let schema = schema_args.get_schema()?;
1699 Ok((pset, schema))
1700}
1701
1702#[cfg(feature = "analyze")]
1703fn load_two_policy_sets(
1704 args: &SymccTwoPoliciesArgs,
1705 schema_args: &SchemaArgs,
1706) -> Result<(PolicySet, PolicySet, Schema)> {
1707 let pset1 = args.get_policy_set_1()?;
1708 let pset2 = args.get_policy_set_2()?;
1709 warn_if_contains_templates(&pset1, "first policy set");
1710 warn_if_contains_templates(&pset2, "second policy set");
1711 let schema = schema_args.get_schema()?;
1712 Ok((pset1, pset2, schema))
1713}
1714
1715#[cfg(feature = "analyze")]
1716fn format_bool_result(holds: bool, property: &str) {
1717 if holds {
1718 println!("✓ {property}: VERIFIED");
1719 } else {
1720 println!("✗ {property}: DOES NOT HOLD");
1721 }
1722}
1723
1724#[cfg(feature = "analyze")]
1725fn format_counterexample_result(
1726 cex: Option<cedar_policy_symcc::Env>,
1727 property: &str,
1728 verbose: bool,
1729) {
1730 match cex {
1731 None => {
1732 println!("✓ {property}: VERIFIED");
1733 if verbose {
1734 println!(" No counterexample found — property holds for all well-formed inputs.");
1735 }
1736 }
1737 Some(env) => {
1738 println!("✗ {property}: DOES NOT HOLD");
1739 println!(" Counterexample found:");
1740 println!("{env}");
1741 }
1742 }
1743}
1744
1745#[cfg(feature = "analyze")]
1746fn build_request_env(args: &SymccArgs) -> Result<RequestEnv> {
1747 let principal_type: EntityTypeName = args
1748 .principal_type
1749 .parse()
1750 .map_err(|e| miette!("Invalid --principal-type '{}': {e}", args.principal_type))?;
1751 let action: EntityUid = args
1752 .action
1753 .parse()
1754 .map_err(|e| miette!("Invalid --action '{}': {e}", args.action))?;
1755 let resource_type: EntityTypeName = args
1756 .resource_type
1757 .parse()
1758 .map_err(|e| miette!("Invalid --resource-type '{}': {e}", args.resource_type))?;
1759 Ok(RequestEnv::new(principal_type, action, resource_type))
1760}
1761
1762#[cfg(feature = "analyze")]
1763async fn symcc_async(args: &SymccArgs) -> Result<()> {
1764 use cedar_policy_symcc::{CedarSymCompiler, CompiledPolicy, CompiledPolicySet};
1765
1766 let solver = initialize_solver(&args.cvc5_path)?;
1767 let mut compiler = CedarSymCompiler::new(solver)
1768 .map_err(|e| miette!("Failed to initialize SymCC compiler: {e}"))?;
1769 let req_env = build_request_env(args)?;
1770
1771 match &args.command {
1772 SymccCommands::NeverErrors(cmd_args) => {
1774 let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
1775 let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
1776 .map_err(|e| miette!("Failed to compile policy: {e}"))?;
1777 if args.counterexample && !args.no_counterexample {
1778 let result = compiler
1779 .check_never_errors_with_counterexample_opt(&compiled)
1780 .await
1781 .map_err(|e| miette!("Verification failed: {e}"))?;
1782 format_counterexample_result(result, "Policy never errors", args.verbose);
1783 } else {
1784 let holds = compiler
1785 .check_never_errors_opt(&compiled)
1786 .await
1787 .map_err(|e| miette!("Verification failed: {e}"))?;
1788 format_bool_result(holds, "Policy never errors");
1789 }
1790 }
1791 SymccCommands::AlwaysMatches(cmd_args) => {
1792 let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
1793 let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
1794 .map_err(|e| miette!("Failed to compile policy: {e}"))?;
1795 if args.counterexample && !args.no_counterexample {
1796 let result = compiler
1797 .check_always_matches_with_counterexample_opt(&compiled)
1798 .await
1799 .map_err(|e| miette!("Verification failed: {e}"))?;
1800 format_counterexample_result(result, "Policy always matches", args.verbose);
1801 } else {
1802 let holds = compiler
1803 .check_always_matches_opt(&compiled)
1804 .await
1805 .map_err(|e| miette!("Verification failed: {e}"))?;
1806 format_bool_result(holds, "Policy always matches");
1807 }
1808 }
1809 SymccCommands::NeverMatches(cmd_args) => {
1810 let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
1811 let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
1812 .map_err(|e| miette!("Failed to compile policy: {e}"))?;
1813 if args.counterexample && !args.no_counterexample {
1814 let result = compiler
1815 .check_never_matches_with_counterexample_opt(&compiled)
1816 .await
1817 .map_err(|e| miette!("Verification failed: {e}"))?;
1818 format_counterexample_result(result, "Policy never matches", args.verbose);
1819 } else {
1820 let holds = compiler
1821 .check_never_matches_opt(&compiled)
1822 .await
1823 .map_err(|e| miette!("Verification failed: {e}"))?;
1824 format_bool_result(holds, "Policy never matches");
1825 }
1826 }
1827
1828 SymccCommands::MatchesEquivalent(cmd_args) => {
1830 let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
1831 let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
1832 .map_err(|e| miette!("Failed to compile policy1: {e}"))?;
1833 let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
1834 .map_err(|e| miette!("Failed to compile policy2: {e}"))?;
1835 if args.counterexample && !args.no_counterexample {
1836 let result = compiler
1837 .check_matches_equivalent_with_counterexample_opt(&compiled1, &compiled2)
1838 .await
1839 .map_err(|e| miette!("Verification failed: {e}"))?;
1840 format_counterexample_result(
1841 result,
1842 "Policies have equivalent match conditions",
1843 args.verbose,
1844 );
1845 } else {
1846 let holds = compiler
1847 .check_matches_equivalent_opt(&compiled1, &compiled2)
1848 .await
1849 .map_err(|e| miette!("Verification failed: {e}"))?;
1850 format_bool_result(holds, "Policies have equivalent match conditions");
1851 }
1852 }
1853 SymccCommands::MatchesImplies(cmd_args) => {
1854 let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
1855 let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
1856 .map_err(|e| miette!("Failed to compile policy1: {e}"))?;
1857 let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
1858 .map_err(|e| miette!("Failed to compile policy2: {e}"))?;
1859 if args.counterexample && !args.no_counterexample {
1860 let result = compiler
1861 .check_matches_implies_with_counterexample_opt(&compiled1, &compiled2)
1862 .await
1863 .map_err(|e| miette!("Verification failed: {e}"))?;
1864 format_counterexample_result(
1865 result,
1866 "Policy1 match implies Policy2 match",
1867 args.verbose,
1868 );
1869 } else {
1870 let holds = compiler
1871 .check_matches_implies_opt(&compiled1, &compiled2)
1872 .await
1873 .map_err(|e| miette!("Verification failed: {e}"))?;
1874 format_bool_result(holds, "Policy1 match implies Policy2 match");
1875 }
1876 }
1877 SymccCommands::MatchesDisjoint(cmd_args) => {
1878 let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
1879 let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
1880 .map_err(|e| miette!("Failed to compile policy1: {e}"))?;
1881 let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
1882 .map_err(|e| miette!("Failed to compile policy2: {e}"))?;
1883 if args.counterexample && !args.no_counterexample {
1884 let result = compiler
1885 .check_matches_disjoint_with_counterexample_opt(&compiled1, &compiled2)
1886 .await
1887 .map_err(|e| miette!("Verification failed: {e}"))?;
1888 format_counterexample_result(
1889 result,
1890 "Policies have disjoint match conditions",
1891 args.verbose,
1892 );
1893 } else {
1894 let holds = compiler
1895 .check_matches_disjoint_opt(&compiled1, &compiled2)
1896 .await
1897 .map_err(|e| miette!("Verification failed: {e}"))?;
1898 format_bool_result(holds, "Policies have disjoint match conditions");
1899 }
1900 }
1901
1902 SymccCommands::AlwaysAllows(cmd_args) => {
1904 let (pset, schema) = load_policy_set(cmd_args, &args.schema)?;
1905 let compiled = CompiledPolicySet::compile(&pset, &req_env, &schema)
1906 .map_err(|e| miette!("Failed to compile policy set: {e}"))?;
1907 if args.counterexample && !args.no_counterexample {
1908 let result = compiler
1909 .check_always_allows_with_counterexample_opt(&compiled)
1910 .await
1911 .map_err(|e| miette!("Verification failed: {e}"))?;
1912 format_counterexample_result(result, "Policy set always allows", args.verbose);
1913 } else {
1914 let holds = compiler
1915 .check_always_allows_opt(&compiled)
1916 .await
1917 .map_err(|e| miette!("Verification failed: {e}"))?;
1918 format_bool_result(holds, "Policy set always allows");
1919 }
1920 }
1921 SymccCommands::AlwaysDenies(cmd_args) => {
1922 let (pset, schema) = load_policy_set(cmd_args, &args.schema)?;
1923 let compiled = CompiledPolicySet::compile(&pset, &req_env, &schema)
1924 .map_err(|e| miette!("Failed to compile policy set: {e}"))?;
1925 if args.counterexample && !args.no_counterexample {
1926 let result = compiler
1927 .check_always_denies_with_counterexample_opt(&compiled)
1928 .await
1929 .map_err(|e| miette!("Verification failed: {e}"))?;
1930 format_counterexample_result(result, "Policy set always denies", args.verbose);
1931 } else {
1932 let holds = compiler
1933 .check_always_denies_opt(&compiled)
1934 .await
1935 .map_err(|e| miette!("Verification failed: {e}"))?;
1936 format_bool_result(holds, "Policy set always denies");
1937 }
1938 }
1939
1940 SymccCommands::Equivalent(cmd_args) => {
1942 let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
1943 let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
1944 .map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
1945 let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
1946 .map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
1947 if args.counterexample && !args.no_counterexample {
1948 let result = compiler
1949 .check_equivalent_with_counterexample_opt(&compiled1, &compiled2)
1950 .await
1951 .map_err(|e| miette!("Verification failed: {e}"))?;
1952 format_counterexample_result(result, "Policy sets are equivalent", args.verbose);
1953 } else {
1954 let holds = compiler
1955 .check_equivalent_opt(&compiled1, &compiled2)
1956 .await
1957 .map_err(|e| miette!("Verification failed: {e}"))?;
1958 format_bool_result(holds, "Policy sets are equivalent");
1959 }
1960 }
1961 SymccCommands::Implies(cmd_args) => {
1962 let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
1963 let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
1964 .map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
1965 let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
1966 .map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
1967 if args.counterexample && !args.no_counterexample {
1968 let result = compiler
1969 .check_implies_with_counterexample_opt(&compiled1, &compiled2)
1970 .await
1971 .map_err(|e| miette!("Verification failed: {e}"))?;
1972 format_counterexample_result(
1973 result,
1974 "Policy set 1 implies policy set 2",
1975 args.verbose,
1976 );
1977 } else {
1978 let holds = compiler
1979 .check_implies_opt(&compiled1, &compiled2)
1980 .await
1981 .map_err(|e| miette!("Verification failed: {e}"))?;
1982 format_bool_result(holds, "Policy set 1 implies policy set 2");
1983 }
1984 }
1985 SymccCommands::Disjoint(cmd_args) => {
1986 let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
1987 let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
1988 .map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
1989 let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
1990 .map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
1991 if args.counterexample && !args.no_counterexample {
1992 let result = compiler
1993 .check_disjoint_with_counterexample_opt(&compiled1, &compiled2)
1994 .await
1995 .map_err(|e| miette!("Verification failed: {e}"))?;
1996 format_counterexample_result(result, "Policy sets are disjoint", args.verbose);
1997 } else {
1998 let holds = compiler
1999 .check_disjoint_opt(&compiled1, &compiled2)
2000 .await
2001 .map_err(|e| miette!("Verification failed: {e}"))?;
2002 format_bool_result(holds, "Policy sets are disjoint");
2003 }
2004 }
2005 }
2006
2007 Ok(())
2008}
2009
2010fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
2011 data.iter()
2012 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
2013 .collect::<Result<HashMap<SlotId, EntityUid>>>()
2014}
2015
2016fn link_inner(args: &LinkArgs) -> Result<()> {
2017 let mut policies = args.policies.get_policy_set()?;
2018 let slotenv = create_slot_env(&args.arguments.data)?;
2019 policies.link(
2020 PolicyId::new(&args.template_id),
2021 PolicyId::new(&args.new_id),
2022 slotenv,
2023 )?;
2024 let linked = policies
2025 .policy(&PolicyId::new(&args.new_id))
2026 .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
2027 println!("Template-linked policy added: {linked}");
2028
2029 if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
2031 update_template_linked_file(
2032 links_filename,
2033 TemplateLinked {
2034 template_id: args.template_id.clone(),
2035 link_id: args.new_id.clone(),
2036 args: args.arguments.data.clone(),
2037 },
2038 )?;
2039 }
2040
2041 Ok(())
2042}
2043
2044#[derive(Clone, Serialize, Deserialize, Debug)]
2045#[serde(try_from = "LiteralTemplateLinked")]
2046#[serde(into = "LiteralTemplateLinked")]
2047struct TemplateLinked {
2048 template_id: String,
2049 link_id: String,
2050 args: HashMap<SlotId, String>,
2051}
2052
2053impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
2054 type Error = String;
2055
2056 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
2057 Ok(Self {
2058 template_id: value.template_id,
2059 link_id: value.link_id,
2060 args: value
2061 .args
2062 .into_iter()
2063 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
2064 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
2065 })
2066 }
2067}
2068
2069fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
2070 match s.as_ref() {
2071 "?principal" => Ok(SlotId::principal()),
2072 "?resource" => Ok(SlotId::resource()),
2073 _ => Err(format!(
2074 "Invalid SlotId! Expected ?principal|?resource, got: {}",
2075 s.as_ref()
2076 )),
2077 }
2078}
2079
2080#[derive(Serialize, Deserialize)]
2081struct LiteralTemplateLinked {
2082 template_id: String,
2083 link_id: String,
2084 args: HashMap<String, String>,
2085}
2086
2087impl From<TemplateLinked> for LiteralTemplateLinked {
2088 fn from(i: TemplateLinked) -> Self {
2089 Self {
2090 template_id: i.template_id,
2091 link_id: i.link_id,
2092 args: i
2093 .args
2094 .into_iter()
2095 .map(|(k, v)| (format!("{k}"), v))
2096 .collect(),
2097 }
2098 }
2099}
2100
2101fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
2103 for template_linked in load_links_from_file(path)? {
2104 let slot_env = create_slot_env(&template_linked.args)?;
2105 policy_set.link(
2106 PolicyId::new(&template_linked.template_id),
2107 PolicyId::new(&template_linked.link_id),
2108 slot_env,
2109 )?;
2110 }
2111 Ok(())
2112}
2113
2114fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
2116 let f = match std::fs::File::open(path) {
2117 Ok(f) => f,
2118 Err(_) => {
2119 return Ok(vec![]);
2121 }
2122 };
2123 if f.metadata()
2124 .into_diagnostic()
2125 .wrap_err("Failed to read metadata")?
2126 .len()
2127 == 0
2128 {
2129 Ok(vec![])
2131 } else {
2132 serde_json::from_reader(f)
2134 .into_diagnostic()
2135 .wrap_err("Deserialization error")
2136 }
2137}
2138
2139fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
2141 let mut template_linked = load_links_from_file(path.as_ref())?;
2142 template_linked.push(new_linked);
2143 write_template_linked_file(&template_linked, path.as_ref())
2144}
2145
2146fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
2148 let f = OpenOptions::new()
2149 .write(true)
2150 .truncate(true)
2151 .create(true)
2152 .open(path)
2153 .into_diagnostic()?;
2154 serde_json::to_writer(f, linked).into_diagnostic()
2155}
2156
2157pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
2158 println!();
2159 let ans = execute_request(
2160 &args.request,
2161 &args.policies,
2162 &args.entities_file,
2163 &args.schema,
2164 args.timing,
2165 );
2166 match ans {
2167 Ok(ans) => {
2168 let status = match ans.decision() {
2169 Decision::Allow => {
2170 println!("ALLOW");
2171 CedarExitCode::Success
2172 }
2173 Decision::Deny => {
2174 println!("DENY");
2175 CedarExitCode::AuthorizeDeny
2176 }
2177 };
2178 if ans.diagnostics().errors().peekable().peek().is_some() {
2179 println!();
2180 for err in ans.diagnostics().errors() {
2181 println!("{err}");
2182 }
2183 }
2184 if args.verbose {
2185 println!();
2186 if ans.diagnostics().reason().peekable().peek().is_none() {
2187 println!("note: no policies applied to this request");
2188 } else {
2189 println!("note: this decision was due to the following policies:");
2190 for reason in ans.diagnostics().reason() {
2191 println!(" {reason}");
2192 }
2193 println!();
2194 }
2195 }
2196 status
2197 }
2198 Err(errs) => {
2199 for err in errs {
2200 println!("{err:?}");
2201 }
2202 CedarExitCode::Failure
2203 }
2204 }
2205}
2206
2207#[cfg(not(feature = "partial-eval"))]
2208pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
2209 {
2210 eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
2211 return CedarExitCode::Failure;
2212 }
2213}
2214
2215#[cfg(not(feature = "tpe"))]
2216pub fn tpe(_: &TpeArgs) -> CedarExitCode {
2217 {
2218 eprintln!("Error: option `tpe` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
2219 return CedarExitCode::Failure;
2220 }
2221}
2222
2223#[cfg(feature = "tpe")]
2224pub fn tpe(args: &TpeArgs) -> CedarExitCode {
2225 println!();
2226 let ret = |errs| {
2227 for err in errs {
2228 println!("{err:?}");
2229 }
2230 CedarExitCode::Failure
2231 };
2232 let mut errs = vec![];
2233 let policies = match args.policies.get_policy_set() {
2234 Ok(pset) => pset,
2235 Err(e) => {
2236 errs.push(e);
2237 PolicySet::new()
2238 }
2239 };
2240 let schema: Schema = match args.schema.get_schema() {
2241 Ok(opt) => opt,
2242 Err(e) => {
2243 errs.push(e);
2244 return ret(errs);
2245 }
2246 };
2247
2248 let entities = match load_partial_entities(args.entities_file.clone(), &schema) {
2249 Ok(entities) => entities,
2250 Err(e) => {
2251 errs.push(e);
2252 PartialEntities::empty()
2253 }
2254 };
2255
2256 match args.request.get_request(&schema) {
2257 Ok(request) if errs.is_empty() => {
2258 let auth_start = Instant::now();
2259 let ans = policies.tpe(&request, &entities, &schema);
2260 let auth_dur = auth_start.elapsed();
2261 match ans {
2262 Ok(ans) => {
2263 if args.timing {
2264 println!(
2265 "Authorization Time (micro seconds) : {}",
2266 auth_dur.as_micros()
2267 );
2268 }
2269 match ans.decision() {
2270 Some(Decision::Allow) => {
2271 println!("ALLOW");
2272 CedarExitCode::Success
2273 }
2274 Some(Decision::Deny) => {
2275 println!("DENY");
2276 CedarExitCode::AuthorizeDeny
2277 }
2278 None => {
2279 println!("UNKNOWN");
2280 println!("All policy residuals:");
2281 for p in ans.residual_policies() {
2282 println!("{p}");
2283 }
2284 CedarExitCode::Unknown
2285 }
2286 }
2287 }
2288 Err(err) => {
2289 errs.push(miette!("{err}"));
2290 return ret(errs);
2291 }
2292 }
2293 }
2294 Ok(_) => {
2295 return ret(errs);
2296 }
2297 Err(e) => {
2298 errs.push(e.wrap_err("failed to parse request"));
2299 return ret(errs);
2300 }
2301 }
2302}
2303
2304#[cfg(feature = "partial-eval")]
2305pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
2306 println!();
2307 let ans = execute_partial_request(
2308 &args.request,
2309 &args.policies,
2310 &args.entities_file,
2311 &args.schema,
2312 args.timing,
2313 );
2314 match ans {
2315 Ok(ans) => match ans.decision() {
2316 Some(Decision::Allow) => {
2317 println!("ALLOW");
2318 CedarExitCode::Success
2319 }
2320 Some(Decision::Deny) => {
2321 println!("DENY");
2322 CedarExitCode::AuthorizeDeny
2323 }
2324 None => {
2325 println!("UNKNOWN");
2326 println!("All policy residuals:");
2327 for p in ans.nontrivial_residuals() {
2328 println!("{p}");
2329 }
2330 CedarExitCode::Unknown
2331 }
2332 },
2333 Err(errs) => {
2334 for err in errs {
2335 println!("{err:?}");
2336 }
2337 CedarExitCode::Failure
2338 }
2339 }
2340}
2341
2342#[derive(Clone, Debug)]
2343enum TestResult {
2344 Pass,
2345 Fail(String),
2346}
2347
2348fn compare_test_decisions(test: &TestCase, ans: &Response) -> TestResult {
2350 if ans.decision() == test.decision.into() {
2351 let mut errors = Vec::new();
2352 let reason = ans.diagnostics().reason().collect::<BTreeSet<_>>();
2353
2354 let missing_reason = test
2356 .reason
2357 .iter()
2358 .filter(|r| !reason.contains(&PolicyId::new(r)))
2359 .collect::<Vec<_>>();
2360
2361 if !missing_reason.is_empty() {
2362 errors.push(format!(
2363 "missing reason(s): {}",
2364 missing_reason
2365 .into_iter()
2366 .map(|r| format!("`{r}`"))
2367 .collect::<Vec<_>>()
2368 .join(", ")
2369 ));
2370 }
2371
2372 let num_errors = ans.diagnostics().errors().count();
2374 if num_errors != test.num_errors {
2375 errors.push(format!(
2376 "expected {} error(s), but got {} runtime error(s){}",
2377 test.num_errors,
2378 num_errors,
2379 if num_errors == 0 {
2380 "".to_string()
2381 } else {
2382 format!(
2383 ": {}",
2384 ans.diagnostics()
2385 .errors()
2386 .map(|e| e.to_string())
2387 .collect::<Vec<_>>()
2388 .join(", ")
2389 )
2390 },
2391 ));
2392 }
2393
2394 if errors.is_empty() {
2395 TestResult::Pass
2396 } else {
2397 TestResult::Fail(errors.join("; "))
2398 }
2399 } else {
2400 TestResult::Fail(format!(
2401 "expected {:?}, got {:?}",
2402 test.decision,
2403 ans.decision()
2404 ))
2405 }
2406}
2407
2408fn run_one_test(
2411 policies: &PolicySet,
2412 test: &serde_json::Value,
2413 validator: Option<&Validator>,
2414) -> Result<TestResult> {
2415 let test = CheckedTestCaseSeed(validator.map(Validator::schema))
2416 .deserialize(test.into_deserializer())
2417 .into_diagnostic()?;
2418 if let Some(validator) = validator {
2419 let val_res = validator.validate(policies, cedar_policy::ValidationMode::Strict);
2420 if !val_res.validation_passed_without_warnings() {
2421 return Err(Report::new(val_res).wrap_err("policy set validation failed"));
2422 }
2423 }
2424 let ans = Authorizer::new().is_authorized(&test.request, policies, &test.entities);
2425 Ok(compare_test_decisions(&test, &ans))
2426}
2427
2428fn run_tests_inner(args: &RunTestsArgs) -> Result<CedarExitCode> {
2429 let policies = args.policies.get_policy_set()?;
2430 let tests = load_partial_tests(&args.tests)?;
2431 let validator = args.schema.get_schema()?.map(Validator::new);
2432
2433 let mut total_fails: usize = 0;
2434
2435 println!("running {} test(s)", tests.len());
2436 for test in tests.iter() {
2437 if let Some(name) = test["name"].as_str() {
2438 print!(" test {name} ... ");
2439 } else {
2440 print!(" test (unnamed) ... ");
2441 }
2442 std::io::stdout().flush().into_diagnostic()?;
2443 match run_one_test(&policies, test, validator.as_ref()) {
2444 Ok(TestResult::Pass) => {
2445 println!(
2446 "{}",
2447 "ok".if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
2448 );
2449 }
2450 Ok(TestResult::Fail(reason)) => {
2451 total_fails += 1;
2452 println!(
2453 "{}: {}",
2454 "fail".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
2455 reason
2456 );
2457 }
2458 Err(e) => {
2459 total_fails += 1;
2460 println!(
2461 "{}:\n {:?}",
2462 "error".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
2463 e
2464 );
2465 }
2466 }
2467 }
2468
2469 println!(
2470 "results: {} {}, {} {}",
2471 tests.len() - total_fails,
2472 if total_fails == 0 {
2473 "passed"
2474 .if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
2475 .to_string()
2476 } else {
2477 "passed".to_string()
2478 },
2479 total_fails,
2480 if total_fails != 0 {
2481 "failed"
2482 .if_supports_color(owo_colors::Stream::Stdout, |s| s.red())
2483 .to_string()
2484 } else {
2485 "failed".to_string()
2486 },
2487 );
2488
2489 Ok(if total_fails != 0 {
2490 CedarExitCode::Failure
2491 } else {
2492 CedarExitCode::Success
2493 })
2494}
2495
2496pub fn run_tests(args: &RunTestsArgs) -> CedarExitCode {
2497 run_tests_inner(args).unwrap_or_else(|e| {
2498 println!("{e:?}");
2499 CedarExitCode::Failure
2500 })
2501}
2502
2503#[derive(Copy, Clone, Debug, Deserialize)]
2504enum ExpectedDecision {
2505 #[serde(rename = "allow")]
2506 Allow,
2507 #[serde(rename = "deny")]
2508 Deny,
2509}
2510
2511impl From<ExpectedDecision> for Decision {
2512 fn from(value: ExpectedDecision) -> Self {
2513 match value {
2514 ExpectedDecision::Allow => Decision::Allow,
2515 ExpectedDecision::Deny => Decision::Deny,
2516 }
2517 }
2518}
2519
2520#[derive(Clone, Debug, Deserialize)]
2521struct UncheckedTestCase {
2522 request: RequestJSON,
2523 entities: serde_json::Value,
2524 decision: ExpectedDecision,
2525 reason: Vec<String>,
2526 num_errors: usize,
2527}
2528
2529#[derive(Clone, Debug)]
2530struct TestCase {
2531 request: Request,
2532 entities: Entities,
2533 decision: ExpectedDecision,
2534 reason: Vec<String>,
2535 num_errors: usize,
2536}
2537
2538struct CheckedTestCaseSeed<'a>(Option<&'a Schema>);
2539
2540impl<'de, 'a> DeserializeSeed<'de> for CheckedTestCaseSeed<'a> {
2541 type Value = TestCase;
2542
2543 fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
2544 where
2545 D: Deserializer<'de>,
2546 {
2547 let UncheckedTestCase {
2548 request,
2549 entities,
2550 decision,
2551 reason,
2552 num_errors,
2553 } = UncheckedTestCase::deserialize(deserializer)?;
2554
2555 let principal = request.principal.parse().map_err(|e| {
2556 serde::de::Error::custom(format!(
2557 "failed to parse principal `{}`: {}",
2558 request.principal, e
2559 ))
2560 })?;
2561
2562 let action = request.action.parse().map_err(|e| {
2563 serde::de::Error::custom(format!(
2564 "failed to parse action `{}`: {}",
2565 request.action, e
2566 ))
2567 })?;
2568
2569 let resource = request.resource.parse().map_err(|e| {
2570 serde::de::Error::custom(format!(
2571 "failed to parse resource `{}`: {}",
2572 request.resource, e
2573 ))
2574 })?;
2575
2576 let context = Context::from_json_value(request.context.clone(), None).map_err(|e| {
2577 serde::de::Error::custom(format!(
2578 "failed to parse context `{}`: {}",
2579 request.context, e
2580 ))
2581 })?;
2582
2583 let request = Request::new(principal, action, resource, context, self.0)
2584 .map_err(|e| serde::de::Error::custom(format!("failed to create request: {e}")))?;
2585
2586 let entities = Entities::from_json_value(entities, self.0)
2587 .map_err(|e| serde::de::Error::custom(format!("failed to parse entities: {e}")))?;
2588
2589 Ok(TestCase {
2590 request,
2591 entities,
2592 decision,
2593 reason,
2594 num_errors,
2595 })
2596 }
2597}
2598
2599fn load_partial_tests(tests_filename: impl AsRef<Path>) -> Result<Vec<serde_json::Value>> {
2602 match std::fs::OpenOptions::new()
2603 .read(true)
2604 .open(tests_filename.as_ref())
2605 {
2606 Ok(f) => {
2607 let reader = BufReader::new(f);
2608 serde_json::from_reader(reader).map_err(|e| {
2609 miette!(
2610 "failed to parse tests from file {}: {e}",
2611 tests_filename.as_ref().display()
2612 )
2613 })
2614 }
2615 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2616 format!(
2617 "failed to open test file {}",
2618 tests_filename.as_ref().display()
2619 )
2620 }),
2621 }
2622}
2623
2624#[cfg(feature = "tpe")]
2625fn load_partial_entities(
2627 entities_filename: impl AsRef<Path>,
2628 schema: &Schema,
2629) -> Result<PartialEntities> {
2630 match std::fs::OpenOptions::new()
2631 .read(true)
2632 .open(entities_filename.as_ref())
2633 {
2634 Ok(f) => {
2635 PartialEntities::from_json_value(serde_json::from_reader(f).into_diagnostic()?, schema)
2636 .map_err(|e| miette!("{e}"))
2637 .wrap_err_with(|| {
2638 format!(
2639 "failed to parse entities from file {}",
2640 entities_filename.as_ref().display()
2641 )
2642 })
2643 }
2644 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2645 format!(
2646 "failed to open entities file {}",
2647 entities_filename.as_ref().display()
2648 )
2649 }),
2650 }
2651}
2652
2653fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
2655 match std::fs::OpenOptions::new()
2656 .read(true)
2657 .open(entities_filename.as_ref())
2658 {
2659 Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
2660 format!(
2661 "failed to parse entities from file {}",
2662 entities_filename.as_ref().display()
2663 )
2664 }),
2665 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2666 format!(
2667 "failed to open entities file {}",
2668 entities_filename.as_ref().display()
2669 )
2670 }),
2671 }
2672}
2673
2674fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
2681 let mut new_ps = PolicySet::new();
2682 let t_iter = ps.templates().map(|t| match t.annotation("id") {
2683 None => Ok(t.clone()),
2684 Some(anno) => anno.parse().map(|a| t.new_id(a)),
2685 });
2686 for t in t_iter {
2687 let template = t.unwrap_or_else(|never| match never {});
2688 new_ps
2689 .add_template(template)
2690 .wrap_err("failed to add template to policy set")?;
2691 }
2692 let p_iter = ps.policies().map(|p| match p.annotation("id") {
2693 None => Ok(p.clone()),
2694 Some(anno) => anno.parse().map(|a| p.new_id(a)),
2695 });
2696 for p in p_iter {
2697 let policy = p.unwrap_or_else(|never| match never {});
2698 new_ps
2699 .add(policy)
2700 .wrap_err("failed to add template to policy set")?;
2701 }
2702 Ok(new_ps)
2703}
2704
2705fn read_from_file_or_stdin(filename: Option<&impl AsRef<Path>>, context: &str) -> Result<String> {
2707 let mut src_str = String::new();
2708 match filename {
2709 Some(path) => {
2710 src_str = std::fs::read_to_string(path)
2711 .into_diagnostic()
2712 .wrap_err_with(|| {
2713 format!("failed to open {context} file {}", path.as_ref().display())
2714 })?;
2715 }
2716 None => {
2717 std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
2718 .into_diagnostic()
2719 .wrap_err_with(|| format!("failed to read {context} from stdin"))?;
2720 }
2721 };
2722 Ok(src_str)
2723}
2724
2725fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
2727 read_from_file_or_stdin(Some(&filename), context)
2728}
2729
2730fn read_cedar_policy_set(
2733 filename: Option<impl AsRef<Path> + std::marker::Copy>,
2734) -> Result<PolicySet> {
2735 let context = "policy set";
2736 let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
2737 let ps = PolicySet::from_str(&ps_str)
2738 .map_err(|err| {
2739 let name = filename.map_or_else(
2740 || "<stdin>".to_owned(),
2741 |n| n.as_ref().display().to_string(),
2742 );
2743 Report::new(err).with_source_code(NamedSource::new(name, ps_str))
2744 })
2745 .wrap_err_with(|| format!("failed to parse {context}"))?;
2746 rename_from_id_annotation(&ps)
2747}
2748
2749fn read_json_policy_set(
2752 filename: Option<impl AsRef<Path> + std::marker::Copy>,
2753) -> Result<PolicySet> {
2754 let context = "JSON policy";
2755 let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
2756 let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
2757 let policy_type = get_json_policy_type(&json)?;
2758
2759 let add_json_source = |report: Report| {
2760 let name = filename.map_or_else(
2761 || "<stdin>".to_owned(),
2762 |n| n.as_ref().display().to_string(),
2763 );
2764 report.with_source_code(NamedSource::new(name, json_source.clone()))
2765 };
2766
2767 match policy_type {
2768 JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
2769 Ok(policy) => PolicySet::from_policies([policy])
2770 .wrap_err_with(|| format!("failed to create policy set from {context}")),
2771 Err(_) => match Template::from_json(None, json)
2772 .map_err(|err| add_json_source(Report::new(err)))
2773 {
2774 Ok(template) => {
2775 let mut ps = PolicySet::new();
2776 ps.add_template(template)?;
2777 Ok(ps)
2778 }
2779 Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
2780 },
2781 },
2782 JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
2783 .map_err(|err| add_json_source(Report::new(err)))
2784 .wrap_err_with(|| format!("failed to create policy set from {context}")),
2785 }
2786}
2787
2788fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
2789 let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
2790 let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
2791
2792 let json_has_property = |p| json.get(p).is_some();
2793 let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
2794 let has_any_policy_property = policy_properties.iter().any(json_has_property);
2795
2796 match (has_any_policy_set_property, has_any_policy_property) {
2797 (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
2798 (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
2799 (true, _) => Ok(JsonPolicyType::PolicySet),
2800 (_, true) => Ok(JsonPolicyType::SinglePolicy),
2801 }
2802}
2803
2804enum JsonPolicyType {
2805 SinglePolicy,
2806 PolicySet,
2807}
2808
2809fn execute_request(
2811 request: &RequestArgs,
2812 policies: &PoliciesArgs,
2813 entities_filename: impl AsRef<Path>,
2814 schema: &OptionalSchemaArgs,
2815 compute_duration: bool,
2816) -> Result<Response, Vec<Report>> {
2817 let mut errs = vec![];
2818 let policies = match policies.get_policy_set() {
2819 Ok(pset) => pset,
2820 Err(e) => {
2821 errs.push(e);
2822 PolicySet::new()
2823 }
2824 };
2825 let schema = match schema.get_schema() {
2826 Ok(opt) => opt,
2827 Err(e) => {
2828 errs.push(e);
2829 None
2830 }
2831 };
2832 let entities = match load_entities(entities_filename, schema.as_ref()) {
2833 Ok(entities) => entities,
2834 Err(e) => {
2835 errs.push(e);
2836 Entities::empty()
2837 }
2838 };
2839 match request.get_request(schema.as_ref()) {
2840 Ok(request) if errs.is_empty() => {
2841 let authorizer = Authorizer::new();
2842 let auth_start = Instant::now();
2843 let ans = authorizer.is_authorized(&request, &policies, &entities);
2844 let auth_dur = auth_start.elapsed();
2845 if compute_duration {
2846 println!(
2847 "Authorization Time (micro seconds) : {}",
2848 auth_dur.as_micros()
2849 );
2850 }
2851 Ok(ans)
2852 }
2853 Ok(_) => Err(errs),
2854 Err(e) => {
2855 errs.push(e.wrap_err("failed to parse request"));
2856 Err(errs)
2857 }
2858 }
2859}
2860
2861#[cfg(feature = "partial-eval")]
2862fn execute_partial_request(
2863 request: &PartialRequestArgs,
2864 policies: &PoliciesArgs,
2865 entities_filename: impl AsRef<Path>,
2866 schema: &OptionalSchemaArgs,
2867 compute_duration: bool,
2868) -> Result<PartialResponse, Vec<Report>> {
2869 let mut errs = vec![];
2870 let policies = match policies.get_policy_set() {
2871 Ok(pset) => pset,
2872 Err(e) => {
2873 errs.push(e);
2874 PolicySet::new()
2875 }
2876 };
2877 let schema = match schema.get_schema() {
2878 Ok(opt) => opt,
2879 Err(e) => {
2880 errs.push(e);
2881 None
2882 }
2883 };
2884 let entities = match load_entities(entities_filename, schema.as_ref()) {
2885 Ok(entities) => entities,
2886 Err(e) => {
2887 errs.push(e);
2888 Entities::empty()
2889 }
2890 };
2891 match request.get_request(schema.as_ref()) {
2892 Ok(request) if errs.is_empty() => {
2893 let authorizer = Authorizer::new();
2894 let auth_start = Instant::now();
2895 let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
2896 let auth_dur = auth_start.elapsed();
2897 if compute_duration {
2898 println!(
2899 "Authorization Time (micro seconds) : {}",
2900 auth_dur.as_micros()
2901 );
2902 }
2903 Ok(ans)
2904 }
2905 Ok(_) => Err(errs),
2906 Err(e) => {
2907 errs.push(e.wrap_err("failed to parse request"));
2908 Err(errs)
2909 }
2910 }
2911}