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