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