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};
27use miette::{miette, IntoDiagnostic, NamedSource, Report, Result, WrapErr};
28use owo_colors::OwoColorize;
29use serde::de::{DeserializeSeed, IntoDeserializer};
30use serde::{Deserialize, Deserializer, Serialize};
31use std::collections::BTreeSet;
32use std::io::{BufReader, Write};
33use std::{
34 collections::HashMap,
35 fmt::{self, Display},
36 fs::OpenOptions,
37 path::{Path, PathBuf},
38 process::{ExitCode, Termination},
39 str::FromStr,
40 time::Instant,
41};
42
43use cedar_policy::*;
44use cedar_policy_formatter::{policies_str_to_pretty, Config};
45
46#[derive(Parser, Debug)]
48#[command(author, version, about, long_about = None)] pub struct Cli {
50 #[command(subcommand)]
51 pub command: Commands,
52 #[arg(
54 global = true,
55 short = 'f',
56 long = "error-format",
57 env = "CEDAR_ERROR_FORMAT",
58 default_value_t,
59 value_enum
60 )]
61 pub err_fmt: ErrorFormat,
62}
63
64#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
65pub enum ErrorFormat {
66 #[default]
69 Human,
70 Plain,
73 Json,
75}
76
77impl Display for ErrorFormat {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(
80 f,
81 "{}",
82 match self {
83 ErrorFormat::Human => "human",
84 ErrorFormat::Plain => "plain",
85 ErrorFormat::Json => "json",
86 }
87 )
88 }
89}
90
91#[derive(Subcommand, Debug)]
92pub enum Commands {
93 Authorize(AuthorizeArgs),
95 Evaluate(EvaluateArgs),
97 Validate(ValidateArgs),
99 CheckParse(CheckParseArgs),
104 Link(LinkArgs),
106 Format(FormatArgs),
108 TranslatePolicy(TranslatePolicyArgs),
110 TranslateSchema(TranslateSchemaArgs),
112 Visualize(VisualizeArgs),
115 New(NewArgs),
117 PartiallyAuthorize(PartiallyAuthorizeArgs),
119 Tpe(TpeArgs),
121 #[clap(verbatim_doc_comment)] RunTests(RunTestsArgs),
132 LanguageVersion,
134}
135
136#[derive(Args, Debug)]
137pub struct TranslatePolicyArgs {
138 #[arg(long)]
140 pub direction: PolicyTranslationDirection,
141 #[arg(short = 'p', long = "policies", value_name = "FILE")]
144 pub input_file: Option<String>,
145}
146
147#[derive(Debug, Clone, Copy, ValueEnum)]
149pub enum PolicyTranslationDirection {
150 CedarToJson,
152 JsonToCedar,
154}
155
156#[derive(Args, Debug)]
157pub struct TranslateSchemaArgs {
158 #[arg(long)]
160 pub direction: SchemaTranslationDirection,
161 #[arg(short = 's', long = "schema", value_name = "FILE")]
164 pub input_file: Option<String>,
165}
166
167#[derive(Debug, Clone, Copy, ValueEnum)]
169pub enum SchemaTranslationDirection {
170 JsonToCedar,
172 CedarToJson,
174 CedarToJsonWithResolvedTypes,
179}
180
181#[derive(Debug, Default, Clone, Copy, ValueEnum)]
182pub enum SchemaFormat {
183 #[default]
185 Cedar,
186 Json,
188}
189
190#[derive(Debug, Clone, Copy, ValueEnum)]
191pub enum ValidationMode {
192 Strict,
194 Permissive,
196 Partial,
198}
199
200#[derive(Args, Debug)]
201pub struct ValidateArgs {
202 #[command(flatten)]
204 pub schema: SchemaArgs,
205 #[command(flatten)]
207 pub policies: PoliciesArgs,
208 #[arg(long)]
210 pub deny_warnings: bool,
211 #[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
216 pub validation_mode: ValidationMode,
217 #[arg(long)]
219 pub level: Option<u32>,
220}
221
222#[derive(Args, Debug)]
223pub struct CheckParseArgs {
224 #[command(flatten)]
226 pub policies: OptionalPoliciesArgs,
227 #[command(flatten)]
229 pub schema: OptionalSchemaArgs,
230 #[arg(long = "entities", value_name = "FILE")]
232 pub entities_file: Option<PathBuf>,
233}
234
235#[derive(Args, Debug)]
237pub struct RequestArgs {
238 #[arg(short = 'l', long)]
240 pub principal: Option<String>,
241 #[arg(short, long)]
243 pub action: Option<String>,
244 #[arg(short, long)]
246 pub resource: Option<String>,
247 #[arg(short, long = "context", value_name = "FILE")]
250 pub context_json_file: Option<String>,
251 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
256 pub request_json_file: Option<String>,
257 #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
260 pub request_validation: bool,
261}
262
263#[cfg(feature = "tpe")]
264#[derive(Args, Debug)]
266pub struct TpeRequestArgs {
267 #[arg(long)]
269 pub principal_type: Option<String>,
270 #[arg(long)]
272 pub principal_eid: Option<String>,
273 #[arg(short, long)]
275 pub action: Option<String>,
276 #[arg(long)]
278 pub resource_type: Option<String>,
279 #[arg(long)]
281 pub resource_eid: Option<String>,
282 #[arg(short, long = "context", value_name = "FILE")]
285 pub context_json_file: Option<String>,
286 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal_type", "principal_eid", "action", "resource_type", "resource_eid", "context_json_file"])]
291 pub request_json_file: Option<String>,
292}
293
294#[cfg(feature = "partial-eval")]
295#[derive(Args, Debug)]
297pub struct PartialRequestArgs {
298 #[arg(short = 'l', long)]
300 pub principal: Option<String>,
301 #[arg(short, long)]
303 pub action: Option<String>,
304 #[arg(short, long)]
306 pub resource: Option<String>,
307 #[arg(short, long = "context", value_name = "FILE")]
310 pub context_json_file: Option<String>,
311 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
316 pub request_json_file: Option<String>,
317}
318
319impl RequestArgs {
320 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
327 match &self.request_json_file {
328 Some(jsonfile) => {
329 let jsonstring = std::fs::read_to_string(jsonfile)
330 .into_diagnostic()
331 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
332 let qjson: RequestJSON = serde_json::from_str(&jsonstring)
333 .into_diagnostic()
334 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
335 let principal = qjson.principal.parse().wrap_err_with(|| {
336 format!("failed to parse principal in {jsonfile} as entity Uid")
337 })?;
338 let action = qjson.action.parse().wrap_err_with(|| {
339 format!("failed to parse action in {jsonfile} as entity Uid")
340 })?;
341 let resource = qjson.resource.parse().wrap_err_with(|| {
342 format!("failed to parse resource in {jsonfile} as entity Uid")
343 })?;
344 let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
345 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
346 Request::new(
347 principal,
348 action,
349 resource,
350 context,
351 if self.request_validation {
352 schema
353 } else {
354 None
355 },
356 )
357 .map_err(|e| miette!("{e}"))
358 }
359 None => {
360 let principal = self
361 .principal
362 .as_ref()
363 .map(|s| {
364 s.parse().wrap_err_with(|| {
365 format!("failed to parse principal {s} as entity Uid")
366 })
367 })
368 .transpose()?;
369 let action = self
370 .action
371 .as_ref()
372 .map(|s| {
373 s.parse()
374 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
375 })
376 .transpose()?;
377 let resource = self
378 .resource
379 .as_ref()
380 .map(|s| {
381 s.parse()
382 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
383 })
384 .transpose()?;
385 let context: Context = match &self.context_json_file {
386 None => Context::empty(),
387 Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
388 Ok(f) => Context::from_json_file(
389 f,
390 schema.and_then(|s| Some((s, action.as_ref()?))),
391 )
392 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
393 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
394 format!("error while loading context from {jsonfile}")
395 })?,
396 },
397 };
398 match (principal, action, resource) {
399 (Some(principal), Some(action), Some(resource)) => Request::new(
400 principal,
401 action,
402 resource,
403 context,
404 if self.request_validation {
405 schema
406 } else {
407 None
408 },
409 )
410 .map_err(|e| miette!("{e}")),
411 _ => Err(miette!(
412 "All three (`principal`, `action`, `resource`) variables must be specified"
413 )),
414 }
415 }
416 }
417 }
418}
419
420#[cfg(feature = "tpe")]
421impl TpeRequestArgs {
422 fn get_request(&self, schema: &Schema) -> Result<PartialRequest> {
423 let qjson: TpeRequestJSON = match self.request_json_file.as_ref() {
424 Some(jsonfile) => {
425 let jsonstring = std::fs::read_to_string(jsonfile)
426 .into_diagnostic()
427 .wrap_err_with(|| format!("failed to open request json file {jsonfile}"))?;
428 serde_json::from_str(&jsonstring)
429 .into_diagnostic()
430 .wrap_err_with(|| format!("failed to parse context-json file {jsonfile}"))?
431 }
432 None => TpeRequestJSON {
433 principal_type: self
434 .principal_type
435 .clone()
436 .ok_or_else(|| miette!("principal type must be specified"))?,
437 principal_eid: self.principal_eid.clone(),
438 action: self
439 .action
440 .clone()
441 .ok_or_else(|| miette!("action must be specified"))?,
442 resource_type: self
443 .resource_type
444 .clone()
445 .ok_or_else(|| miette!("resource type must be specified"))?,
446 resource_eid: self.resource_eid.clone(),
447 context: self
448 .context_json_file
449 .as_ref()
450 .map(|jsonfile| {
451 let jsonstring = std::fs::read_to_string(jsonfile)
452 .into_diagnostic()
453 .wrap_err_with(|| {
454 format!("failed to open context-json file {jsonfile}")
455 })?;
456 serde_json::from_str(&jsonstring)
457 .into_diagnostic()
458 .wrap_err_with(|| {
459 format!("failed to parse context-json file {jsonfile}")
460 })
461 })
462 .transpose()?,
463 },
464 };
465 let action: EntityUid = qjson.action.parse()?;
466 Ok(PartialRequest::new(
467 PartialEntityUid::new(
468 qjson.principal_type.parse()?,
469 qjson.principal_eid.as_ref().map(EntityId::new),
470 ),
471 action.clone(),
472 PartialEntityUid::new(
473 qjson.resource_type.parse()?,
474 qjson.resource_eid.as_ref().map(EntityId::new),
475 ),
476 qjson
477 .context
478 .map(|val| Context::from_json_value(val, Some((schema, &action))))
479 .transpose()?,
480 schema,
481 )?)
482 }
483}
484
485#[cfg(feature = "partial-eval")]
486impl PartialRequestArgs {
487 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
488 let mut builder = RequestBuilder::default();
489 let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
490 Some(jsonfile) => {
491 let jsonstring = std::fs::read_to_string(jsonfile)
492 .into_diagnostic()
493 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
494 serde_json::from_str(&jsonstring)
495 .into_diagnostic()
496 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
497 }
498 None => PartialRequestJSON {
499 principal: self.principal.clone(),
500 action: self.action.clone(),
501 resource: self.resource.clone(),
502 context: self
503 .context_json_file
504 .as_ref()
505 .map(|jsonfile| {
506 let jsonstring = std::fs::read_to_string(jsonfile)
507 .into_diagnostic()
508 .wrap_err_with(|| {
509 format!("failed to open context-json file {jsonfile}")
510 })?;
511 serde_json::from_str(&jsonstring)
512 .into_diagnostic()
513 .wrap_err_with(|| {
514 format!("failed to parse context-json file {jsonfile}")
515 })
516 })
517 .transpose()?,
518 },
519 };
520
521 if let Some(principal) = qjson
522 .principal
523 .map(|s| {
524 s.parse()
525 .wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
526 })
527 .transpose()?
528 {
529 builder = builder.principal(principal);
530 }
531
532 let action = qjson
533 .action
534 .map(|s| {
535 s.parse::<EntityUid>()
536 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
537 })
538 .transpose()?;
539
540 if let Some(action_ref) = &action {
541 builder = builder.action(action_ref.clone());
542 }
543
544 if let Some(resource) = qjson
545 .resource
546 .map(|s| {
547 s.parse()
548 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
549 })
550 .transpose()?
551 {
552 builder = builder.resource(resource);
553 }
554
555 if let Some(context) = qjson
556 .context
557 .map(|json| {
558 Context::from_json_value(
559 json.clone(),
560 schema.and_then(|s| Some((s, action.as_ref()?))),
561 )
562 .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
563 })
564 .transpose()?
565 {
566 builder = builder.context(context);
567 }
568
569 if let Some(schema) = schema {
570 builder
571 .schema(schema)
572 .build()
573 .wrap_err_with(|| "failed to build request with validation".to_string())
574 } else {
575 Ok(builder.build())
576 }
577 }
578}
579
580#[derive(Args, Debug)]
582pub struct PoliciesArgs {
583 #[arg(short, long = "policies", value_name = "FILE")]
585 pub policies_file: Option<String>,
586 #[arg(long = "policy-format", default_value_t, value_enum)]
588 pub policy_format: PolicyFormat,
589 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
591 pub template_linked_file: Option<String>,
592}
593
594impl PoliciesArgs {
595 fn get_policy_set(&self) -> Result<PolicySet> {
597 let mut pset = match self.policy_format {
598 PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
599 PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
600 }?;
601 if let Some(links_filename) = self.template_linked_file.as_ref() {
602 add_template_links_to_set(links_filename, &mut pset)?;
603 }
604 Ok(pset)
605 }
606}
607
608#[derive(Args, Debug)]
611pub struct OptionalPoliciesArgs {
612 #[arg(short, long = "policies", value_name = "FILE")]
614 pub policies_file: Option<String>,
615 #[arg(long = "policy-format", default_value_t, value_enum)]
617 pub policy_format: PolicyFormat,
618 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
621 pub template_linked_file: Option<String>,
622}
623
624impl OptionalPoliciesArgs {
625 fn get_policy_set(&self) -> Result<Option<PolicySet>> {
628 match &self.policies_file {
629 None => Ok(None),
630 Some(policies_file) => {
631 let pargs = PoliciesArgs {
632 policies_file: Some(policies_file.clone()),
633 policy_format: self.policy_format,
634 template_linked_file: self.template_linked_file.clone(),
635 };
636 pargs.get_policy_set().map(Some)
637 }
638 }
639 }
640}
641
642#[derive(Args, Debug)]
644pub struct SchemaArgs {
645 #[arg(short, long = "schema", value_name = "FILE")]
647 pub schema_file: PathBuf,
648 #[arg(long, value_enum, default_value_t)]
650 pub schema_format: SchemaFormat,
651}
652
653impl SchemaArgs {
654 fn get_schema(&self) -> Result<Schema> {
656 read_schema_from_file(&self.schema_file, self.schema_format)
657 }
658}
659
660#[derive(Args, Debug)]
663pub struct OptionalSchemaArgs {
664 #[arg(short, long = "schema", value_name = "FILE")]
666 pub schema_file: Option<PathBuf>,
667 #[arg(long, value_enum, default_value_t)]
669 pub schema_format: SchemaFormat,
670}
671
672impl OptionalSchemaArgs {
673 fn get_schema(&self) -> Result<Option<Schema>> {
675 let Some(schema_file) = &self.schema_file else {
676 return Ok(None);
677 };
678 read_schema_from_file(schema_file, self.schema_format).map(Some)
679 }
680}
681
682fn read_schema_from_file(path: impl AsRef<Path>, format: SchemaFormat) -> Result<Schema> {
683 let path = path.as_ref();
684 let schema_src = read_from_file(path, "schema")?;
685 match format {
686 SchemaFormat::Json => Schema::from_json_str(&schema_src)
687 .wrap_err_with(|| format!("failed to parse schema from file {}", path.display())),
688 SchemaFormat::Cedar => {
689 let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)
690 .wrap_err_with(|| format!("failed to parse schema from file {}", path.display()))?;
691 for warning in warnings {
692 let report = miette::Report::new(warning);
693 eprintln!("{report:?}");
694 }
695 Ok(schema)
696 }
697 }
698}
699
700#[derive(Args, Debug)]
701pub struct AuthorizeArgs {
702 #[command(flatten)]
704 pub request: RequestArgs,
705 #[command(flatten)]
707 pub policies: PoliciesArgs,
708 #[command(flatten)]
713 pub schema: OptionalSchemaArgs,
714 #[arg(long = "entities", value_name = "FILE")]
716 pub entities_file: String,
717 #[arg(short, long)]
719 pub verbose: bool,
720 #[arg(short, long)]
722 pub timing: bool,
723}
724
725#[cfg(feature = "tpe")]
726#[derive(Args, Debug)]
727pub struct TpeArgs {
728 #[command(flatten)]
730 pub request: TpeRequestArgs,
731 #[command(flatten)]
733 pub policies: PoliciesArgs,
734 #[command(flatten)]
739 pub schema: SchemaArgs,
740 #[arg(long = "entities", value_name = "FILE")]
742 pub entities_file: String,
743 #[arg(short, long)]
745 pub timing: bool,
746}
747
748#[cfg(feature = "partial-eval")]
749#[derive(Args, Debug)]
750pub struct PartiallyAuthorizeArgs {
751 #[command(flatten)]
753 pub request: PartialRequestArgs,
754 #[command(flatten)]
756 pub policies: PoliciesArgs,
757 #[command(flatten)]
762 pub schema: OptionalSchemaArgs,
763 #[arg(long = "entities", value_name = "FILE")]
765 pub entities_file: String,
766 #[arg(short, long)]
768 pub timing: bool,
769}
770
771#[cfg(not(feature = "tpe"))]
772#[derive(Debug, Args)]
773pub struct TpeArgs;
774
775#[cfg(not(feature = "partial-eval"))]
776#[derive(Debug, Args)]
777pub struct PartiallyAuthorizeArgs;
778
779#[derive(Args, Debug)]
780pub struct RunTestsArgs {
781 #[command(flatten)]
783 pub policies: PoliciesArgs,
784 #[arg(long, value_name = "FILE")]
786 pub tests: String,
787 #[command(flatten)]
788 pub schema: OptionalSchemaArgs,
789}
790
791#[derive(Args, Debug)]
792pub struct VisualizeArgs {
793 #[arg(long = "entities", value_name = "FILE")]
794 pub entities_file: String,
795}
796
797#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
798pub enum PolicyFormat {
799 #[default]
801 Cedar,
802 Json,
804}
805
806#[derive(Args, Debug)]
807pub struct LinkArgs {
808 #[command(flatten)]
810 pub policies: PoliciesArgs,
811 #[arg(long)]
813 pub template_id: String,
814 #[arg(short, long)]
816 pub new_id: String,
817 #[arg(short, long)]
819 pub arguments: Arguments,
820}
821
822#[derive(Args, Debug)]
823pub struct FormatArgs {
824 #[arg(short, long = "policies", value_name = "FILE")]
826 pub policies_file: Option<String>,
827
828 #[arg(short, long, value_name = "UINT", default_value_t = 80)]
830 pub line_width: usize,
831
832 #[arg(short, long, value_name = "INT", default_value_t = 2)]
834 pub indent_width: isize,
835
836 #[arg(short, long, group = "action", requires = "policies_file")]
838 pub write: bool,
839
840 #[arg(short, long, group = "action")]
842 pub check: bool,
843}
844
845#[derive(Args, Debug)]
846pub struct NewArgs {
847 #[arg(short, long, value_name = "DIR")]
849 pub name: String,
850}
851
852#[derive(Clone, Debug, Deserialize)]
854#[serde(try_from = "HashMap<String,String>")]
855pub struct Arguments {
856 pub data: HashMap<SlotId, String>,
857}
858
859impl TryFrom<HashMap<String, String>> for Arguments {
860 type Error = String;
861
862 fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
863 Ok(Self {
864 data: value
865 .into_iter()
866 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
867 .collect::<Result<HashMap<SlotId, String>, String>>()?,
868 })
869 }
870}
871
872impl FromStr for Arguments {
873 type Err = serde_json::Error;
874
875 fn from_str(s: &str) -> Result<Self, Self::Err> {
876 serde_json::from_str(s)
877 }
878}
879
880#[derive(Clone, Debug, Deserialize)]
882struct RequestJSON {
883 #[serde(default)]
885 principal: String,
886 #[serde(default)]
888 action: String,
889 #[serde(default)]
891 resource: String,
892 context: serde_json::Value,
894}
895
896#[cfg(feature = "partial-eval")]
897#[derive(Deserialize)]
899struct PartialRequestJSON {
900 pub(self) principal: Option<String>,
902 pub(self) action: Option<String>,
904 pub(self) resource: Option<String>,
906 pub(self) context: Option<serde_json::Value>,
908}
909
910#[cfg(feature = "tpe")]
911#[derive(Deserialize)]
913struct TpeRequestJSON {
914 pub(self) principal_type: String,
916 pub(self) principal_eid: Option<String>,
918 pub(self) action: String,
920 pub(self) resource_type: String,
922 pub(self) resource_eid: Option<String>,
924 pub(self) context: Option<serde_json::Value>,
926}
927
928#[derive(Args, Debug)]
929pub struct EvaluateArgs {
930 #[command(flatten)]
932 pub request: RequestArgs,
933 #[command(flatten)]
938 pub schema: OptionalSchemaArgs,
939 #[arg(long = "entities", value_name = "FILE")]
942 pub entities_file: Option<String>,
943 #[arg(value_name = "EXPRESSION")]
945 pub expression: String,
946}
947
948#[derive(Eq, PartialEq, Debug, Copy, Clone)]
949pub enum CedarExitCode {
950 Success,
953 Failure,
955 AuthorizeDeny,
958 ValidationFailure,
961 #[cfg(any(feature = "partial-eval", feature = "tpe"))]
962 Unknown,
965}
966
967impl Termination for CedarExitCode {
968 fn report(self) -> ExitCode {
969 match self {
970 CedarExitCode::Success => ExitCode::SUCCESS,
971 CedarExitCode::Failure => ExitCode::FAILURE,
972 CedarExitCode::AuthorizeDeny => ExitCode::from(2),
973 CedarExitCode::ValidationFailure => ExitCode::from(3),
974 #[cfg(any(feature = "partial-eval", feature = "tpe"))]
975 CedarExitCode::Unknown => ExitCode::SUCCESS,
976 }
977 }
978}
979
980pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
981 if (
984 &args.policies.policies_file,
985 &args.schema.schema_file,
986 &args.entities_file,
987 ) == (&None, &None, &None)
988 {
989 let pargs = PoliciesArgs {
990 policies_file: None, policy_format: args.policies.policy_format,
992 template_linked_file: args.policies.template_linked_file.clone(),
993 };
994 match pargs.get_policy_set() {
995 Ok(_) => return CedarExitCode::Success,
996 Err(e) => {
997 println!("{e:?}");
998 return CedarExitCode::Failure;
999 }
1000 }
1001 }
1002
1003 let mut exit_code = CedarExitCode::Success;
1004 match args.policies.get_policy_set() {
1005 Ok(_) => (),
1006 Err(e) => {
1007 println!("{e:?}");
1008 exit_code = CedarExitCode::Failure;
1009 }
1010 }
1011 let schema = match args.schema.get_schema() {
1012 Ok(schema) => schema,
1013 Err(e) => {
1014 println!("{e:?}");
1015 exit_code = CedarExitCode::Failure;
1016 None
1017 }
1018 };
1019 match &args.entities_file {
1020 None => (),
1021 Some(efile) => match load_entities(efile, schema.as_ref()) {
1022 Ok(_) => (),
1023 Err(e) => {
1024 println!("{e:?}");
1025 exit_code = CedarExitCode::Failure;
1026 }
1027 },
1028 }
1029 exit_code
1030}
1031
1032pub fn validate(args: &ValidateArgs) -> CedarExitCode {
1033 let mode = match args.validation_mode {
1034 ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
1035 ValidationMode::Permissive => {
1036 #[cfg(not(feature = "permissive-validate"))]
1037 {
1038 eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
1039 return CedarExitCode::Failure;
1040 }
1041 #[cfg(feature = "permissive-validate")]
1042 cedar_policy::ValidationMode::Permissive
1043 }
1044 ValidationMode::Partial => {
1045 #[cfg(not(feature = "partial-validate"))]
1046 {
1047 eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
1048 return CedarExitCode::Failure;
1049 }
1050 #[cfg(feature = "partial-validate")]
1051 cedar_policy::ValidationMode::Partial
1052 }
1053 };
1054
1055 let pset = match args.policies.get_policy_set() {
1056 Ok(pset) => pset,
1057 Err(e) => {
1058 println!("{e:?}");
1059 return CedarExitCode::Failure;
1060 }
1061 };
1062
1063 let schema = match args.schema.get_schema() {
1064 Ok(schema) => schema,
1065 Err(e) => {
1066 println!("{e:?}");
1067 return CedarExitCode::Failure;
1068 }
1069 };
1070
1071 let validator = Validator::new(schema);
1072
1073 let result = if let Some(level) = args.level {
1074 validator.validate_with_level(&pset, mode, level)
1075 } else {
1076 validator.validate(&pset, mode)
1077 };
1078
1079 if !result.validation_passed()
1080 || (args.deny_warnings && !result.validation_passed_without_warnings())
1081 {
1082 println!(
1083 "{:?}",
1084 Report::new(result).wrap_err("policy set validation failed")
1085 );
1086 CedarExitCode::ValidationFailure
1087 } else {
1088 println!(
1089 "{:?}",
1090 Report::new(result).wrap_err("policy set validation passed")
1091 );
1092 CedarExitCode::Success
1093 }
1094}
1095
1096pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
1097 println!();
1098 let schema = match args.schema.get_schema() {
1099 Ok(opt) => opt,
1100 Err(e) => {
1101 println!("{e:?}");
1102 return (CedarExitCode::Failure, EvalResult::Bool(false));
1103 }
1104 };
1105 let request = match args.request.get_request(schema.as_ref()) {
1106 Ok(q) => q,
1107 Err(e) => {
1108 println!("{e:?}");
1109 return (CedarExitCode::Failure, EvalResult::Bool(false));
1110 }
1111 };
1112 let expr =
1113 match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
1114 Ok(expr) => expr,
1115 Err(e) => {
1116 println!("{:?}", e.with_source_code(args.expression.clone()));
1117 return (CedarExitCode::Failure, EvalResult::Bool(false));
1118 }
1119 };
1120 let entities = match &args.entities_file {
1121 None => Entities::empty(),
1122 Some(file) => match load_entities(file, schema.as_ref()) {
1123 Ok(entities) => entities,
1124 Err(e) => {
1125 println!("{e:?}");
1126 return (CedarExitCode::Failure, EvalResult::Bool(false));
1127 }
1128 },
1129 };
1130 match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
1131 {
1132 Err(e) => {
1133 println!("{e:?}");
1134 return (CedarExitCode::Failure, EvalResult::Bool(false));
1135 }
1136 Ok(result) => {
1137 println!("{result}");
1138 return (CedarExitCode::Success, result);
1139 }
1140 }
1141}
1142
1143pub fn link(args: &LinkArgs) -> CedarExitCode {
1144 if let Err(err) = link_inner(args) {
1145 println!("{err:?}");
1146 CedarExitCode::Failure
1147 } else {
1148 CedarExitCode::Success
1149 }
1150}
1151
1152pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
1153 match load_entities(&args.entities_file, None) {
1154 Ok(entities) => {
1155 println!("{}", entities.to_dot_str());
1156 CedarExitCode::Success
1157 }
1158 Err(report) => {
1159 eprintln!("{report:?}");
1160 CedarExitCode::Failure
1161 }
1162 }
1163}
1164
1165fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
1170 let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
1171 let config = Config {
1172 line_width: args.line_width,
1173 indent_width: args.indent_width,
1174 };
1175 let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
1176 let are_policies_equivalent = policies_str == formatted_policy;
1177
1178 match &args.policies_file {
1179 Some(policies_file) if args.write => {
1180 let mut file = OpenOptions::new()
1181 .write(true)
1182 .truncate(true)
1183 .open(policies_file)
1184 .into_diagnostic()
1185 .wrap_err(format!("failed to open {policies_file} for writing"))?;
1186 file.write_all(formatted_policy.as_bytes())
1187 .into_diagnostic()
1188 .wrap_err(format!(
1189 "failed to write formatted policies to {policies_file}"
1190 ))?;
1191 }
1192 _ => print!("{formatted_policy}"),
1193 }
1194 Ok(are_policies_equivalent)
1195}
1196
1197pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
1198 match format_policies_inner(args) {
1199 Ok(false) if args.check => CedarExitCode::Failure,
1200 Err(err) => {
1201 println!("{err:?}");
1202 CedarExitCode::Failure
1203 }
1204 _ => CedarExitCode::Success,
1205 }
1206}
1207
1208fn translate_policy_to_cedar(
1209 json_src: Option<impl AsRef<Path> + std::marker::Copy>,
1210) -> Result<String> {
1211 let policy_set = read_json_policy_set(json_src)?;
1212 policy_set.to_cedar().ok_or_else(|| {
1213 miette!("Unable to translate policy set containing template linked policies.")
1214 })
1215}
1216
1217fn translate_policy_to_json(
1218 cedar_src: Option<impl AsRef<Path> + std::marker::Copy>,
1219) -> Result<String> {
1220 let policy_set = read_cedar_policy_set(cedar_src)?;
1221 let output = policy_set.to_json()?.to_string();
1222 Ok(output)
1223}
1224
1225fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
1226 let translate = match args.direction {
1227 PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
1228 PolicyTranslationDirection::JsonToCedar => translate_policy_to_cedar,
1229 };
1230 translate(args.input_file.as_ref())
1231}
1232
1233pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
1234 match translate_policy_inner(args) {
1235 Ok(sf) => {
1236 println!("{sf}");
1237 CedarExitCode::Success
1238 }
1239 Err(err) => {
1240 eprintln!("{err:?}");
1241 CedarExitCode::Failure
1242 }
1243 }
1244}
1245
1246fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
1247 let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
1248 let output = fragment.to_cedarschema()?;
1249 Ok(output)
1250}
1251
1252fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
1253 let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
1254 for warning in warnings {
1255 let report = miette::Report::new(warning);
1256 eprintln!("{report:?}");
1257 }
1258 let output = fragment.to_json_string()?;
1259 Ok(output)
1260}
1261
1262fn translate_schema_to_json_with_resolved_types(cedar_src: impl AsRef<str>) -> Result<String> {
1263 match cedar_policy::schema_str_to_json_with_resolved_types(cedar_src.as_ref()) {
1264 Ok((json_value, warnings)) => {
1265 for warning in &warnings {
1267 eprintln!("{warning}");
1268 }
1269
1270 serde_json::to_string_pretty(&json_value).into_diagnostic()
1272 }
1273 Err(error) => {
1274 Err(miette::Report::new(error))
1276 }
1277 }
1278}
1279
1280fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
1281 let translate = match args.direction {
1282 SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
1283 SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
1284 SchemaTranslationDirection::CedarToJsonWithResolvedTypes => {
1285 translate_schema_to_json_with_resolved_types
1286 }
1287 };
1288 read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate)
1289}
1290
1291pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
1292 match translate_schema_inner(args) {
1293 Ok(sf) => {
1294 println!("{sf}");
1295 CedarExitCode::Success
1296 }
1297 Err(err) => {
1298 eprintln!("{err:?}");
1299 CedarExitCode::Failure
1300 }
1301 }
1302}
1303
1304fn generate_schema(path: &Path) -> Result<()> {
1306 std::fs::write(
1307 path,
1308 serde_json::to_string_pretty(&serde_json::json!(
1309 {
1310 "": {
1311 "entityTypes": {
1312 "A": {
1313 "memberOfTypes": [
1314 "B"
1315 ]
1316 },
1317 "B": {
1318 "memberOfTypes": []
1319 },
1320 "C": {
1321 "memberOfTypes": []
1322 }
1323 },
1324 "actions": {
1325 "action": {
1326 "appliesTo": {
1327 "resourceTypes": [
1328 "C"
1329 ],
1330 "principalTypes": [
1331 "A",
1332 "B"
1333 ]
1334 }
1335 }
1336 }
1337 }
1338 }))
1339 .into_diagnostic()?,
1340 )
1341 .into_diagnostic()
1342}
1343
1344fn generate_policy(path: &Path) -> Result<()> {
1345 std::fs::write(
1346 path,
1347 r#"permit (
1348 principal in A::"a",
1349 action == Action::"action",
1350 resource == C::"c"
1351) when { true };
1352"#,
1353 )
1354 .into_diagnostic()
1355}
1356
1357fn generate_entities(path: &Path) -> Result<()> {
1358 std::fs::write(
1359 path,
1360 serde_json::to_string_pretty(&serde_json::json!(
1361 [
1362 {
1363 "uid": { "type": "A", "id": "a"} ,
1364 "attrs": {},
1365 "parents": [{"type": "B", "id": "b"}]
1366 },
1367 {
1368 "uid": { "type": "B", "id": "b"} ,
1369 "attrs": {},
1370 "parents": []
1371 },
1372 {
1373 "uid": { "type": "C", "id": "c"} ,
1374 "attrs": {},
1375 "parents": []
1376 }
1377 ]))
1378 .into_diagnostic()?,
1379 )
1380 .into_diagnostic()
1381}
1382
1383fn new_inner(args: &NewArgs) -> Result<()> {
1384 let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1385 std::fs::create_dir(dir).into_diagnostic()?;
1386 let schema_path = dir.join("schema.cedarschema.json");
1387 let policy_path = dir.join("policy.cedar");
1388 let entities_path = dir.join("entities.json");
1389 generate_schema(&schema_path)?;
1390 generate_policy(&policy_path)?;
1391 generate_entities(&entities_path)
1392}
1393
1394pub fn new(args: &NewArgs) -> CedarExitCode {
1395 if let Err(err) = new_inner(args) {
1396 println!("{err:?}");
1397 CedarExitCode::Failure
1398 } else {
1399 CedarExitCode::Success
1400 }
1401}
1402
1403pub fn language_version() -> CedarExitCode {
1404 let version = get_lang_version();
1405 println!(
1406 "Cedar language version: {}.{}",
1407 version.major, version.minor
1408 );
1409 CedarExitCode::Success
1410}
1411
1412fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
1413 data.iter()
1414 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
1415 .collect::<Result<HashMap<SlotId, EntityUid>>>()
1416}
1417
1418fn link_inner(args: &LinkArgs) -> Result<()> {
1419 let mut policies = args.policies.get_policy_set()?;
1420 let slotenv = create_slot_env(&args.arguments.data)?;
1421 policies.link(
1422 PolicyId::new(&args.template_id),
1423 PolicyId::new(&args.new_id),
1424 slotenv,
1425 )?;
1426 let linked = policies
1427 .policy(&PolicyId::new(&args.new_id))
1428 .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
1429 println!("Template-linked policy added: {linked}");
1430
1431 if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
1433 update_template_linked_file(
1434 links_filename,
1435 TemplateLinked {
1436 template_id: args.template_id.clone(),
1437 link_id: args.new_id.clone(),
1438 args: args.arguments.data.clone(),
1439 },
1440 )?;
1441 }
1442
1443 Ok(())
1444}
1445
1446#[derive(Clone, Serialize, Deserialize, Debug)]
1447#[serde(try_from = "LiteralTemplateLinked")]
1448#[serde(into = "LiteralTemplateLinked")]
1449struct TemplateLinked {
1450 template_id: String,
1451 link_id: String,
1452 args: HashMap<SlotId, String>,
1453}
1454
1455impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
1456 type Error = String;
1457
1458 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
1459 Ok(Self {
1460 template_id: value.template_id,
1461 link_id: value.link_id,
1462 args: value
1463 .args
1464 .into_iter()
1465 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1466 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
1467 })
1468 }
1469}
1470
1471fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
1472 match s.as_ref() {
1473 "?principal" => Ok(SlotId::principal()),
1474 "?resource" => Ok(SlotId::resource()),
1475 _ => Err(format!(
1476 "Invalid SlotId! Expected ?principal|?resource, got: {}",
1477 s.as_ref()
1478 )),
1479 }
1480}
1481
1482#[derive(Serialize, Deserialize)]
1483struct LiteralTemplateLinked {
1484 template_id: String,
1485 link_id: String,
1486 args: HashMap<String, String>,
1487}
1488
1489impl From<TemplateLinked> for LiteralTemplateLinked {
1490 fn from(i: TemplateLinked) -> Self {
1491 Self {
1492 template_id: i.template_id,
1493 link_id: i.link_id,
1494 args: i
1495 .args
1496 .into_iter()
1497 .map(|(k, v)| (format!("{k}"), v))
1498 .collect(),
1499 }
1500 }
1501}
1502
1503fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
1505 for template_linked in load_links_from_file(path)? {
1506 let slot_env = create_slot_env(&template_linked.args)?;
1507 policy_set.link(
1508 PolicyId::new(&template_linked.template_id),
1509 PolicyId::new(&template_linked.link_id),
1510 slot_env,
1511 )?;
1512 }
1513 Ok(())
1514}
1515
1516fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
1518 let f = match std::fs::File::open(path) {
1519 Ok(f) => f,
1520 Err(_) => {
1521 return Ok(vec![]);
1523 }
1524 };
1525 if f.metadata()
1526 .into_diagnostic()
1527 .wrap_err("Failed to read metadata")?
1528 .len()
1529 == 0
1530 {
1531 Ok(vec![])
1533 } else {
1534 serde_json::from_reader(f)
1536 .into_diagnostic()
1537 .wrap_err("Deserialization error")
1538 }
1539}
1540
1541fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
1543 let mut template_linked = load_links_from_file(path.as_ref())?;
1544 template_linked.push(new_linked);
1545 write_template_linked_file(&template_linked, path.as_ref())
1546}
1547
1548fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
1550 let f = OpenOptions::new()
1551 .write(true)
1552 .truncate(true)
1553 .create(true)
1554 .open(path)
1555 .into_diagnostic()?;
1556 serde_json::to_writer(f, linked).into_diagnostic()
1557}
1558
1559pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
1560 println!();
1561 let ans = execute_request(
1562 &args.request,
1563 &args.policies,
1564 &args.entities_file,
1565 &args.schema,
1566 args.timing,
1567 );
1568 match ans {
1569 Ok(ans) => {
1570 let status = match ans.decision() {
1571 Decision::Allow => {
1572 println!("ALLOW");
1573 CedarExitCode::Success
1574 }
1575 Decision::Deny => {
1576 println!("DENY");
1577 CedarExitCode::AuthorizeDeny
1578 }
1579 };
1580 if ans.diagnostics().errors().peekable().peek().is_some() {
1581 println!();
1582 for err in ans.diagnostics().errors() {
1583 println!("{err}");
1584 }
1585 }
1586 if args.verbose {
1587 println!();
1588 if ans.diagnostics().reason().peekable().peek().is_none() {
1589 println!("note: no policies applied to this request");
1590 } else {
1591 println!("note: this decision was due to the following policies:");
1592 for reason in ans.diagnostics().reason() {
1593 println!(" {reason}");
1594 }
1595 println!();
1596 }
1597 }
1598 status
1599 }
1600 Err(errs) => {
1601 for err in errs {
1602 println!("{err:?}");
1603 }
1604 CedarExitCode::Failure
1605 }
1606 }
1607}
1608
1609#[cfg(not(feature = "partial-eval"))]
1610pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
1611 {
1612 eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1613 return CedarExitCode::Failure;
1614 }
1615}
1616
1617#[cfg(not(feature = "tpe"))]
1618pub fn tpe(_: &TpeArgs) -> CedarExitCode {
1619 {
1620 eprintln!("Error: option `tpe` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1621 return CedarExitCode::Failure;
1622 }
1623}
1624
1625#[cfg(feature = "tpe")]
1626pub fn tpe(args: &TpeArgs) -> CedarExitCode {
1627 println!();
1628 let ret = |errs| {
1629 for err in errs {
1630 println!("{err:?}");
1631 }
1632 CedarExitCode::Failure
1633 };
1634 let mut errs = vec![];
1635 let policies = match args.policies.get_policy_set() {
1636 Ok(pset) => pset,
1637 Err(e) => {
1638 errs.push(e);
1639 PolicySet::new()
1640 }
1641 };
1642 let schema: Schema = match args.schema.get_schema() {
1643 Ok(opt) => opt,
1644 Err(e) => {
1645 errs.push(e);
1646 return ret(errs);
1647 }
1648 };
1649
1650 let entities = match load_partial_entities(args.entities_file.clone(), &schema) {
1651 Ok(entities) => entities,
1652 Err(e) => {
1653 errs.push(e);
1654 PartialEntities::empty()
1655 }
1656 };
1657
1658 match args.request.get_request(&schema) {
1659 Ok(request) if errs.is_empty() => {
1660 let auth_start = Instant::now();
1661 let ans = policies.tpe(&request, &entities, &schema);
1662 let auth_dur = auth_start.elapsed();
1663 match ans {
1664 Ok(ans) => {
1665 if args.timing {
1666 println!(
1667 "Authorization Time (micro seconds) : {}",
1668 auth_dur.as_micros()
1669 );
1670 }
1671 match ans.decision() {
1672 Some(Decision::Allow) => {
1673 println!("ALLOW");
1674 CedarExitCode::Success
1675 }
1676 Some(Decision::Deny) => {
1677 println!("DENY");
1678 CedarExitCode::AuthorizeDeny
1679 }
1680 None => {
1681 println!("UNKNOWN");
1682 println!("All policy residuals:");
1683 for p in ans.residual_policies() {
1684 println!("{p}");
1685 }
1686 CedarExitCode::Unknown
1687 }
1688 }
1689 }
1690 Err(err) => {
1691 errs.push(miette!("{err}"));
1692 return ret(errs);
1693 }
1694 }
1695 }
1696 Ok(_) => {
1697 return ret(errs);
1698 }
1699 Err(e) => {
1700 errs.push(e.wrap_err("failed to parse request"));
1701 return ret(errs);
1702 }
1703 }
1704}
1705
1706#[cfg(feature = "partial-eval")]
1707pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
1708 println!();
1709 let ans = execute_partial_request(
1710 &args.request,
1711 &args.policies,
1712 &args.entities_file,
1713 &args.schema,
1714 args.timing,
1715 );
1716 match ans {
1717 Ok(ans) => match ans.decision() {
1718 Some(Decision::Allow) => {
1719 println!("ALLOW");
1720 CedarExitCode::Success
1721 }
1722 Some(Decision::Deny) => {
1723 println!("DENY");
1724 CedarExitCode::AuthorizeDeny
1725 }
1726 None => {
1727 println!("UNKNOWN");
1728 println!("All policy residuals:");
1729 for p in ans.nontrivial_residuals() {
1730 println!("{p}");
1731 }
1732 CedarExitCode::Unknown
1733 }
1734 },
1735 Err(errs) => {
1736 for err in errs {
1737 println!("{err:?}");
1738 }
1739 CedarExitCode::Failure
1740 }
1741 }
1742}
1743
1744#[derive(Clone, Debug)]
1745enum TestResult {
1746 Pass,
1747 Fail(String),
1748}
1749
1750fn compare_test_decisions(test: &TestCase, ans: &Response) -> TestResult {
1752 if ans.decision() == test.decision.into() {
1753 let mut errors = Vec::new();
1754 let reason = ans.diagnostics().reason().collect::<BTreeSet<_>>();
1755
1756 let missing_reason = test
1758 .reason
1759 .iter()
1760 .filter(|r| !reason.contains(&PolicyId::new(r)))
1761 .collect::<Vec<_>>();
1762
1763 if !missing_reason.is_empty() {
1764 errors.push(format!(
1765 "missing reason(s): {}",
1766 missing_reason
1767 .into_iter()
1768 .map(|r| format!("`{r}`"))
1769 .collect::<Vec<_>>()
1770 .join(", ")
1771 ));
1772 }
1773
1774 let num_errors = ans.diagnostics().errors().count();
1776 if num_errors != test.num_errors {
1777 errors.push(format!(
1778 "expected {} error(s), but got {} runtime error(s){}",
1779 test.num_errors,
1780 num_errors,
1781 if num_errors == 0 {
1782 "".to_string()
1783 } else {
1784 format!(
1785 ": {}",
1786 ans.diagnostics()
1787 .errors()
1788 .map(|e| e.to_string())
1789 .collect::<Vec<_>>()
1790 .join(", ")
1791 )
1792 },
1793 ));
1794 }
1795
1796 if errors.is_empty() {
1797 TestResult::Pass
1798 } else {
1799 TestResult::Fail(errors.join("; "))
1800 }
1801 } else {
1802 TestResult::Fail(format!(
1803 "expected {:?}, got {:?}",
1804 test.decision,
1805 ans.decision()
1806 ))
1807 }
1808}
1809
1810fn run_one_test(
1813 policies: &PolicySet,
1814 test: &serde_json::Value,
1815 validator: Option<&Validator>,
1816) -> Result<TestResult> {
1817 let test = CheckedTestCaseSeed(validator.map(Validator::schema))
1818 .deserialize(test.into_deserializer())
1819 .into_diagnostic()?;
1820 if let Some(validator) = validator {
1821 let val_res = validator.validate(policies, cedar_policy::ValidationMode::Strict);
1822 if !val_res.validation_passed_without_warnings() {
1823 return Err(Report::new(val_res).wrap_err("policy set validation failed"));
1824 }
1825 }
1826 let ans = Authorizer::new().is_authorized(&test.request, policies, &test.entities);
1827 Ok(compare_test_decisions(&test, &ans))
1828}
1829
1830fn run_tests_inner(args: &RunTestsArgs) -> Result<CedarExitCode> {
1831 let policies = args.policies.get_policy_set()?;
1832 let tests = load_partial_tests(&args.tests)?;
1833 let validator = args.schema.get_schema()?.map(Validator::new);
1834
1835 let mut total_fails: usize = 0;
1836
1837 println!("running {} test(s)", tests.len());
1838 for test in tests.iter() {
1839 if let Some(name) = test["name"].as_str() {
1840 print!(" test {name} ... ");
1841 } else {
1842 print!(" test (unnamed) ... ");
1843 }
1844 std::io::stdout().flush().into_diagnostic()?;
1845 match run_one_test(&policies, test, validator.as_ref()) {
1846 Ok(TestResult::Pass) => {
1847 println!(
1848 "{}",
1849 "ok".if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
1850 );
1851 }
1852 Ok(TestResult::Fail(reason)) => {
1853 total_fails += 1;
1854 println!(
1855 "{}: {}",
1856 "fail".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
1857 reason
1858 );
1859 }
1860 Err(e) => {
1861 total_fails += 1;
1862 println!(
1863 "{}:\n {:?}",
1864 "error".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
1865 e
1866 );
1867 }
1868 }
1869 }
1870
1871 println!(
1872 "results: {} {}, {} {}",
1873 tests.len() - total_fails,
1874 if total_fails == 0 {
1875 "passed"
1876 .if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
1877 .to_string()
1878 } else {
1879 "passed".to_string()
1880 },
1881 total_fails,
1882 if total_fails != 0 {
1883 "failed"
1884 .if_supports_color(owo_colors::Stream::Stdout, |s| s.red())
1885 .to_string()
1886 } else {
1887 "failed".to_string()
1888 },
1889 );
1890
1891 Ok(if total_fails != 0 {
1892 CedarExitCode::Failure
1893 } else {
1894 CedarExitCode::Success
1895 })
1896}
1897
1898pub fn run_tests(args: &RunTestsArgs) -> CedarExitCode {
1899 run_tests_inner(args).unwrap_or_else(|e| {
1900 println!("{e:?}");
1901 CedarExitCode::Failure
1902 })
1903}
1904
1905#[derive(Copy, Clone, Debug, Deserialize)]
1906enum ExpectedDecision {
1907 #[serde(rename = "allow")]
1908 Allow,
1909 #[serde(rename = "deny")]
1910 Deny,
1911}
1912
1913impl From<ExpectedDecision> for Decision {
1914 fn from(value: ExpectedDecision) -> Self {
1915 match value {
1916 ExpectedDecision::Allow => Decision::Allow,
1917 ExpectedDecision::Deny => Decision::Deny,
1918 }
1919 }
1920}
1921
1922#[derive(Clone, Debug, Deserialize)]
1923struct UncheckedTestCase {
1924 request: RequestJSON,
1925 entities: serde_json::Value,
1926 decision: ExpectedDecision,
1927 reason: Vec<String>,
1928 num_errors: usize,
1929}
1930
1931#[derive(Clone, Debug)]
1932struct TestCase {
1933 request: Request,
1934 entities: Entities,
1935 decision: ExpectedDecision,
1936 reason: Vec<String>,
1937 num_errors: usize,
1938}
1939
1940struct CheckedTestCaseSeed<'a>(Option<&'a Schema>);
1941
1942impl<'de, 'a> DeserializeSeed<'de> for CheckedTestCaseSeed<'a> {
1943 type Value = TestCase;
1944
1945 fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
1946 where
1947 D: Deserializer<'de>,
1948 {
1949 let UncheckedTestCase {
1950 request,
1951 entities,
1952 decision,
1953 reason,
1954 num_errors,
1955 } = UncheckedTestCase::deserialize(deserializer)?;
1956
1957 let principal = request.principal.parse().map_err(|e| {
1958 serde::de::Error::custom(format!(
1959 "failed to parse principal `{}`: {}",
1960 request.principal, e
1961 ))
1962 })?;
1963
1964 let action = request.action.parse().map_err(|e| {
1965 serde::de::Error::custom(format!(
1966 "failed to parse action `{}`: {}",
1967 request.action, e
1968 ))
1969 })?;
1970
1971 let resource = request.resource.parse().map_err(|e| {
1972 serde::de::Error::custom(format!(
1973 "failed to parse resource `{}`: {}",
1974 request.resource, e
1975 ))
1976 })?;
1977
1978 let context = Context::from_json_value(request.context.clone(), None).map_err(|e| {
1979 serde::de::Error::custom(format!(
1980 "failed to parse context `{}`: {}",
1981 request.context, e
1982 ))
1983 })?;
1984
1985 let request = Request::new(principal, action, resource, context, self.0)
1986 .map_err(|e| serde::de::Error::custom(format!("failed to create request: {e}")))?;
1987
1988 let entities = Entities::from_json_value(entities, self.0)
1989 .map_err(|e| serde::de::Error::custom(format!("failed to parse entities: {e}")))?;
1990
1991 Ok(TestCase {
1992 request,
1993 entities,
1994 decision,
1995 reason,
1996 num_errors,
1997 })
1998 }
1999}
2000
2001fn load_partial_tests(tests_filename: impl AsRef<Path>) -> Result<Vec<serde_json::Value>> {
2004 match std::fs::OpenOptions::new()
2005 .read(true)
2006 .open(tests_filename.as_ref())
2007 {
2008 Ok(f) => {
2009 let reader = BufReader::new(f);
2010 serde_json::from_reader(reader).map_err(|e| {
2011 miette!(
2012 "failed to parse tests from file {}: {e}",
2013 tests_filename.as_ref().display()
2014 )
2015 })
2016 }
2017 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2018 format!(
2019 "failed to open test file {}",
2020 tests_filename.as_ref().display()
2021 )
2022 }),
2023 }
2024}
2025
2026#[cfg(feature = "tpe")]
2027fn load_partial_entities(
2029 entities_filename: impl AsRef<Path>,
2030 schema: &Schema,
2031) -> Result<PartialEntities> {
2032 match std::fs::OpenOptions::new()
2033 .read(true)
2034 .open(entities_filename.as_ref())
2035 {
2036 Ok(f) => {
2037 PartialEntities::from_json_value(serde_json::from_reader(f).into_diagnostic()?, schema)
2038 .map_err(|e| miette!("{e}"))
2039 .wrap_err_with(|| {
2040 format!(
2041 "failed to parse entities from file {}",
2042 entities_filename.as_ref().display()
2043 )
2044 })
2045 }
2046 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2047 format!(
2048 "failed to open entities file {}",
2049 entities_filename.as_ref().display()
2050 )
2051 }),
2052 }
2053}
2054
2055fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
2057 match std::fs::OpenOptions::new()
2058 .read(true)
2059 .open(entities_filename.as_ref())
2060 {
2061 Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
2062 format!(
2063 "failed to parse entities from file {}",
2064 entities_filename.as_ref().display()
2065 )
2066 }),
2067 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2068 format!(
2069 "failed to open entities file {}",
2070 entities_filename.as_ref().display()
2071 )
2072 }),
2073 }
2074}
2075
2076fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
2083 let mut new_ps = PolicySet::new();
2084 let t_iter = ps.templates().map(|t| match t.annotation("id") {
2085 None => Ok(t.clone()),
2086 Some(anno) => anno.parse().map(|a| t.new_id(a)),
2087 });
2088 for t in t_iter {
2089 let template = t.unwrap_or_else(|never| match never {});
2090 new_ps
2091 .add_template(template)
2092 .wrap_err("failed to add template to policy set")?;
2093 }
2094 let p_iter = ps.policies().map(|p| match p.annotation("id") {
2095 None => Ok(p.clone()),
2096 Some(anno) => anno.parse().map(|a| p.new_id(a)),
2097 });
2098 for p in p_iter {
2099 let policy = p.unwrap_or_else(|never| match never {});
2100 new_ps
2101 .add(policy)
2102 .wrap_err("failed to add template to policy set")?;
2103 }
2104 Ok(new_ps)
2105}
2106
2107fn read_from_file_or_stdin(filename: Option<&impl AsRef<Path>>, context: &str) -> Result<String> {
2109 let mut src_str = String::new();
2110 match filename {
2111 Some(path) => {
2112 src_str = std::fs::read_to_string(path)
2113 .into_diagnostic()
2114 .wrap_err_with(|| {
2115 format!("failed to open {context} file {}", path.as_ref().display())
2116 })?;
2117 }
2118 None => {
2119 std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
2120 .into_diagnostic()
2121 .wrap_err_with(|| format!("failed to read {context} from stdin"))?;
2122 }
2123 };
2124 Ok(src_str)
2125}
2126
2127fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
2129 read_from_file_or_stdin(Some(&filename), context)
2130}
2131
2132fn read_cedar_policy_set(
2135 filename: Option<impl AsRef<Path> + std::marker::Copy>,
2136) -> Result<PolicySet> {
2137 let context = "policy set";
2138 let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
2139 let ps = PolicySet::from_str(&ps_str)
2140 .map_err(|err| {
2141 let name = filename.map_or_else(
2142 || "<stdin>".to_owned(),
2143 |n| n.as_ref().display().to_string(),
2144 );
2145 Report::new(err).with_source_code(NamedSource::new(name, ps_str))
2146 })
2147 .wrap_err_with(|| format!("failed to parse {context}"))?;
2148 rename_from_id_annotation(&ps)
2149}
2150
2151fn read_json_policy_set(
2154 filename: Option<impl AsRef<Path> + std::marker::Copy>,
2155) -> Result<PolicySet> {
2156 let context = "JSON policy";
2157 let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
2158 let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
2159 let policy_type = get_json_policy_type(&json)?;
2160
2161 let add_json_source = |report: Report| {
2162 let name = filename.map_or_else(
2163 || "<stdin>".to_owned(),
2164 |n| n.as_ref().display().to_string(),
2165 );
2166 report.with_source_code(NamedSource::new(name, json_source.clone()))
2167 };
2168
2169 match policy_type {
2170 JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
2171 Ok(policy) => PolicySet::from_policies([policy])
2172 .wrap_err_with(|| format!("failed to create policy set from {context}")),
2173 Err(_) => match Template::from_json(None, json)
2174 .map_err(|err| add_json_source(Report::new(err)))
2175 {
2176 Ok(template) => {
2177 let mut ps = PolicySet::new();
2178 ps.add_template(template)?;
2179 Ok(ps)
2180 }
2181 Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
2182 },
2183 },
2184 JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
2185 .map_err(|err| add_json_source(Report::new(err)))
2186 .wrap_err_with(|| format!("failed to create policy set from {context}")),
2187 }
2188}
2189
2190fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
2191 let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
2192 let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
2193
2194 let json_has_property = |p| json.get(p).is_some();
2195 let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
2196 let has_any_policy_property = policy_properties.iter().any(json_has_property);
2197
2198 match (has_any_policy_set_property, has_any_policy_property) {
2199 (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
2200 (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
2201 (true, _) => Ok(JsonPolicyType::PolicySet),
2202 (_, true) => Ok(JsonPolicyType::SinglePolicy),
2203 }
2204}
2205
2206enum JsonPolicyType {
2207 SinglePolicy,
2208 PolicySet,
2209}
2210
2211fn execute_request(
2213 request: &RequestArgs,
2214 policies: &PoliciesArgs,
2215 entities_filename: impl AsRef<Path>,
2216 schema: &OptionalSchemaArgs,
2217 compute_duration: bool,
2218) -> Result<Response, Vec<Report>> {
2219 let mut errs = vec![];
2220 let policies = match policies.get_policy_set() {
2221 Ok(pset) => pset,
2222 Err(e) => {
2223 errs.push(e);
2224 PolicySet::new()
2225 }
2226 };
2227 let schema = match schema.get_schema() {
2228 Ok(opt) => opt,
2229 Err(e) => {
2230 errs.push(e);
2231 None
2232 }
2233 };
2234 let entities = match load_entities(entities_filename, schema.as_ref()) {
2235 Ok(entities) => entities,
2236 Err(e) => {
2237 errs.push(e);
2238 Entities::empty()
2239 }
2240 };
2241 match request.get_request(schema.as_ref()) {
2242 Ok(request) if errs.is_empty() => {
2243 let authorizer = Authorizer::new();
2244 let auth_start = Instant::now();
2245 let ans = authorizer.is_authorized(&request, &policies, &entities);
2246 let auth_dur = auth_start.elapsed();
2247 if compute_duration {
2248 println!(
2249 "Authorization Time (micro seconds) : {}",
2250 auth_dur.as_micros()
2251 );
2252 }
2253 Ok(ans)
2254 }
2255 Ok(_) => Err(errs),
2256 Err(e) => {
2257 errs.push(e.wrap_err("failed to parse request"));
2258 Err(errs)
2259 }
2260 }
2261}
2262
2263#[cfg(feature = "partial-eval")]
2264fn execute_partial_request(
2265 request: &PartialRequestArgs,
2266 policies: &PoliciesArgs,
2267 entities_filename: impl AsRef<Path>,
2268 schema: &OptionalSchemaArgs,
2269 compute_duration: bool,
2270) -> Result<PartialResponse, Vec<Report>> {
2271 let mut errs = vec![];
2272 let policies = match policies.get_policy_set() {
2273 Ok(pset) => pset,
2274 Err(e) => {
2275 errs.push(e);
2276 PolicySet::new()
2277 }
2278 };
2279 let schema = match schema.get_schema() {
2280 Ok(opt) => opt,
2281 Err(e) => {
2282 errs.push(e);
2283 None
2284 }
2285 };
2286 let entities = match load_entities(entities_filename, schema.as_ref()) {
2287 Ok(entities) => entities,
2288 Err(e) => {
2289 errs.push(e);
2290 Entities::empty()
2291 }
2292 };
2293 match request.get_request(schema.as_ref()) {
2294 Ok(request) if errs.is_empty() => {
2295 let authorizer = Authorizer::new();
2296 let auth_start = Instant::now();
2297 let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
2298 let auth_dur = auth_start.elapsed();
2299 if compute_duration {
2300 println!(
2301 "Authorization Time (micro seconds) : {}",
2302 auth_dur.as_micros()
2303 );
2304 }
2305 Ok(ans)
2306 }
2307 Ok(_) => Err(errs),
2308 Err(e) => {
2309 errs.push(e.wrap_err("failed to parse request"));
2310 Err(errs)
2311 }
2312 }
2313}