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 RunTests(RunTestsArgs),
116 LanguageVersion,
118}
119
120#[derive(Args, Debug)]
121pub struct TranslatePolicyArgs {
122 #[arg(long)]
124 pub direction: PolicyTranslationDirection,
125 #[arg(short = 'p', long = "policies", value_name = "FILE")]
128 pub input_file: Option<String>,
129}
130
131#[derive(Debug, Clone, Copy, ValueEnum)]
133pub enum PolicyTranslationDirection {
134 CedarToJson,
136 JsonToCedar,
138}
139
140#[derive(Args, Debug)]
141pub struct TranslateSchemaArgs {
142 #[arg(long)]
144 pub direction: SchemaTranslationDirection,
145 #[arg(short = 's', long = "schema", value_name = "FILE")]
148 pub input_file: Option<String>,
149}
150
151#[derive(Debug, Clone, Copy, ValueEnum)]
153pub enum SchemaTranslationDirection {
154 JsonToCedar,
156 CedarToJson,
158}
159
160#[derive(Debug, Default, Clone, Copy, ValueEnum)]
161pub enum SchemaFormat {
162 #[default]
164 Cedar,
165 Json,
167}
168
169#[derive(Debug, Clone, Copy, ValueEnum)]
170pub enum ValidationMode {
171 Strict,
173 Permissive,
175 Partial,
177}
178
179#[derive(Args, Debug)]
180pub struct ValidateArgs {
181 #[command(flatten)]
183 pub schema: SchemaArgs,
184 #[command(flatten)]
186 pub policies: PoliciesArgs,
187 #[arg(long)]
189 pub deny_warnings: bool,
190 #[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
195 pub validation_mode: ValidationMode,
196 #[arg(long)]
198 pub level: Option<u32>,
199}
200
201#[derive(Args, Debug)]
202pub struct CheckParseArgs {
203 #[command(flatten)]
205 pub policies: OptionalPoliciesArgs,
206 #[command(flatten)]
208 pub schema: OptionalSchemaArgs,
209 #[arg(long = "entities", value_name = "FILE")]
211 pub entities_file: Option<PathBuf>,
212}
213
214#[derive(Args, Debug)]
216pub struct RequestArgs {
217 #[arg(short = 'l', long)]
219 pub principal: Option<String>,
220 #[arg(short, long)]
222 pub action: Option<String>,
223 #[arg(short, long)]
225 pub resource: Option<String>,
226 #[arg(short, long = "context", value_name = "FILE")]
229 pub context_json_file: Option<String>,
230 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
235 pub request_json_file: Option<String>,
236 #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
239 pub request_validation: bool,
240}
241
242#[cfg(feature = "partial-eval")]
243#[derive(Args, Debug)]
245pub struct PartialRequestArgs {
246 #[arg(short = 'l', long)]
248 pub principal: Option<String>,
249 #[arg(short, long)]
251 pub action: Option<String>,
252 #[arg(short, long)]
254 pub resource: Option<String>,
255 #[arg(short, long = "context", value_name = "FILE")]
258 pub context_json_file: Option<String>,
259 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
264 pub request_json_file: Option<String>,
265}
266
267impl RequestArgs {
268 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
275 match &self.request_json_file {
276 Some(jsonfile) => {
277 let jsonstring = std::fs::read_to_string(jsonfile)
278 .into_diagnostic()
279 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
280 let qjson: RequestJSON = serde_json::from_str(&jsonstring)
281 .into_diagnostic()
282 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
283 let principal = qjson.principal.parse().wrap_err_with(|| {
284 format!("failed to parse principal in {jsonfile} as entity Uid")
285 })?;
286 let action = qjson.action.parse().wrap_err_with(|| {
287 format!("failed to parse action in {jsonfile} as entity Uid")
288 })?;
289 let resource = qjson.resource.parse().wrap_err_with(|| {
290 format!("failed to parse resource in {jsonfile} as entity Uid")
291 })?;
292 let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
293 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
294 Request::new(
295 principal,
296 action,
297 resource,
298 context,
299 if self.request_validation {
300 schema
301 } else {
302 None
303 },
304 )
305 .map_err(|e| miette!("{e}"))
306 }
307 None => {
308 let principal = self
309 .principal
310 .as_ref()
311 .map(|s| {
312 s.parse().wrap_err_with(|| {
313 format!("failed to parse principal {s} as entity Uid")
314 })
315 })
316 .transpose()?;
317 let action = self
318 .action
319 .as_ref()
320 .map(|s| {
321 s.parse()
322 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
323 })
324 .transpose()?;
325 let resource = self
326 .resource
327 .as_ref()
328 .map(|s| {
329 s.parse()
330 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
331 })
332 .transpose()?;
333 let context: Context = match &self.context_json_file {
334 None => Context::empty(),
335 Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
336 Ok(f) => Context::from_json_file(
337 f,
338 schema.and_then(|s| Some((s, action.as_ref()?))),
339 )
340 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
341 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
342 format!("error while loading context from {jsonfile}")
343 })?,
344 },
345 };
346 match (principal, action, resource) {
347 (Some(principal), Some(action), Some(resource)) => Request::new(
348 principal,
349 action,
350 resource,
351 context,
352 if self.request_validation {
353 schema
354 } else {
355 None
356 },
357 )
358 .map_err(|e| miette!("{e}")),
359 _ => Err(miette!(
360 "All three (`principal`, `action`, `resource`) variables must be specified"
361 )),
362 }
363 }
364 }
365 }
366}
367
368#[cfg(feature = "partial-eval")]
369impl PartialRequestArgs {
370 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
371 let mut builder = RequestBuilder::default();
372 let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
373 Some(jsonfile) => {
374 let jsonstring = std::fs::read_to_string(jsonfile)
375 .into_diagnostic()
376 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
377 serde_json::from_str(&jsonstring)
378 .into_diagnostic()
379 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
380 }
381 None => PartialRequestJSON {
382 principal: self.principal.clone(),
383 action: self.action.clone(),
384 resource: self.resource.clone(),
385 context: self
386 .context_json_file
387 .as_ref()
388 .map(|jsonfile| {
389 let jsonstring = std::fs::read_to_string(jsonfile)
390 .into_diagnostic()
391 .wrap_err_with(|| {
392 format!("failed to open context-json file {jsonfile}")
393 })?;
394 serde_json::from_str(&jsonstring)
395 .into_diagnostic()
396 .wrap_err_with(|| {
397 format!("failed to parse context-json file {jsonfile}")
398 })
399 })
400 .transpose()?,
401 },
402 };
403
404 if let Some(principal) = qjson
405 .principal
406 .map(|s| {
407 s.parse()
408 .wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
409 })
410 .transpose()?
411 {
412 builder = builder.principal(principal);
413 }
414
415 let action = qjson
416 .action
417 .map(|s| {
418 s.parse::<EntityUid>()
419 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
420 })
421 .transpose()?;
422
423 if let Some(action_ref) = &action {
424 builder = builder.action(action_ref.clone());
425 }
426
427 if let Some(resource) = qjson
428 .resource
429 .map(|s| {
430 s.parse()
431 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
432 })
433 .transpose()?
434 {
435 builder = builder.resource(resource);
436 }
437
438 if let Some(context) = qjson
439 .context
440 .map(|json| {
441 Context::from_json_value(
442 json.clone(),
443 schema.and_then(|s| Some((s, action.as_ref()?))),
444 )
445 .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
446 })
447 .transpose()?
448 {
449 builder = builder.context(context);
450 }
451
452 if let Some(schema) = schema {
453 builder
454 .schema(schema)
455 .build()
456 .wrap_err_with(|| "failed to build request with validation".to_string())
457 } else {
458 Ok(builder.build())
459 }
460 }
461}
462
463#[derive(Args, Debug)]
465pub struct PoliciesArgs {
466 #[arg(short, long = "policies", value_name = "FILE")]
468 pub policies_file: Option<String>,
469 #[arg(long = "policy-format", default_value_t, value_enum)]
471 pub policy_format: PolicyFormat,
472 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
474 pub template_linked_file: Option<String>,
475}
476
477impl PoliciesArgs {
478 fn get_policy_set(&self) -> Result<PolicySet> {
480 let mut pset = match self.policy_format {
481 PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
482 PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
483 }?;
484 if let Some(links_filename) = self.template_linked_file.as_ref() {
485 add_template_links_to_set(links_filename, &mut pset)?;
486 }
487 Ok(pset)
488 }
489}
490
491#[derive(Args, Debug)]
494pub struct OptionalPoliciesArgs {
495 #[arg(short, long = "policies", value_name = "FILE")]
497 pub policies_file: Option<String>,
498 #[arg(long = "policy-format", default_value_t, value_enum)]
500 pub policy_format: PolicyFormat,
501 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
504 pub template_linked_file: Option<String>,
505}
506
507impl OptionalPoliciesArgs {
508 fn get_policy_set(&self) -> Result<Option<PolicySet>> {
511 match &self.policies_file {
512 None => Ok(None),
513 Some(policies_file) => {
514 let pargs = PoliciesArgs {
515 policies_file: Some(policies_file.clone()),
516 policy_format: self.policy_format,
517 template_linked_file: self.template_linked_file.clone(),
518 };
519 pargs.get_policy_set().map(Some)
520 }
521 }
522 }
523}
524
525#[derive(Args, Debug)]
527pub struct SchemaArgs {
528 #[arg(short, long = "schema", value_name = "FILE")]
530 pub schema_file: PathBuf,
531 #[arg(long, value_enum, default_value_t)]
533 pub schema_format: SchemaFormat,
534}
535
536impl SchemaArgs {
537 fn get_schema(&self) -> Result<Schema> {
539 read_schema_from_file(&self.schema_file, self.schema_format)
540 }
541}
542
543#[derive(Args, Debug)]
546pub struct OptionalSchemaArgs {
547 #[arg(short, long = "schema", value_name = "FILE")]
549 pub schema_file: Option<PathBuf>,
550 #[arg(long, value_enum, default_value_t)]
552 pub schema_format: SchemaFormat,
553}
554
555impl OptionalSchemaArgs {
556 fn get_schema(&self) -> Result<Option<Schema>> {
558 let Some(schema_file) = &self.schema_file else {
559 return Ok(None);
560 };
561 read_schema_from_file(schema_file, self.schema_format).map(Some)
562 }
563}
564
565fn read_schema_from_file(path: impl AsRef<Path>, format: SchemaFormat) -> Result<Schema> {
566 let path = path.as_ref();
567 let schema_src = read_from_file(path, "schema")?;
568 match format {
569 SchemaFormat::Json => Schema::from_json_str(&schema_src)
570 .wrap_err_with(|| format!("failed to parse schema from file {}", path.display())),
571 SchemaFormat::Cedar => {
572 let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)
573 .wrap_err_with(|| format!("failed to parse schema from file {}", path.display()))?;
574 for warning in warnings {
575 let report = miette::Report::new(warning);
576 eprintln!("{report:?}");
577 }
578 Ok(schema)
579 }
580 }
581}
582
583#[derive(Args, Debug)]
584pub struct AuthorizeArgs {
585 #[command(flatten)]
587 pub request: RequestArgs,
588 #[command(flatten)]
590 pub policies: PoliciesArgs,
591 #[command(flatten)]
596 pub schema: OptionalSchemaArgs,
597 #[arg(long = "entities", value_name = "FILE")]
599 pub entities_file: String,
600 #[arg(short, long)]
602 pub verbose: bool,
603 #[arg(short, long)]
605 pub timing: bool,
606}
607
608#[cfg(feature = "partial-eval")]
609#[derive(Args, Debug)]
610pub struct PartiallyAuthorizeArgs {
611 #[command(flatten)]
613 pub request: PartialRequestArgs,
614 #[command(flatten)]
616 pub policies: PoliciesArgs,
617 #[command(flatten)]
622 pub schema: OptionalSchemaArgs,
623 #[arg(long = "entities", value_name = "FILE")]
625 pub entities_file: String,
626 #[arg(short, long)]
628 pub timing: bool,
629}
630
631#[cfg(not(feature = "partial-eval"))]
632#[derive(Debug, Args)]
633pub struct PartiallyAuthorizeArgs;
634
635#[derive(Args, Debug)]
636pub struct RunTestsArgs {
637 #[command(flatten)]
639 pub policies: PoliciesArgs,
640 #[arg(long, value_name = "FILE")]
642 pub tests: String,
643}
644
645#[derive(Args, Debug)]
646pub struct VisualizeArgs {
647 #[arg(long = "entities", value_name = "FILE")]
648 pub entities_file: String,
649}
650
651#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
652pub enum PolicyFormat {
653 #[default]
655 Cedar,
656 Json,
658}
659
660#[derive(Args, Debug)]
661pub struct LinkArgs {
662 #[command(flatten)]
664 pub policies: PoliciesArgs,
665 #[arg(long)]
667 pub template_id: String,
668 #[arg(short, long)]
670 pub new_id: String,
671 #[arg(short, long)]
673 pub arguments: Arguments,
674}
675
676#[derive(Args, Debug)]
677pub struct FormatArgs {
678 #[arg(short, long = "policies", value_name = "FILE")]
680 pub policies_file: Option<String>,
681
682 #[arg(short, long, value_name = "UINT", default_value_t = 80)]
684 pub line_width: usize,
685
686 #[arg(short, long, value_name = "INT", default_value_t = 2)]
688 pub indent_width: isize,
689
690 #[arg(short, long, group = "action", requires = "policies_file")]
692 pub write: bool,
693
694 #[arg(short, long, group = "action")]
696 pub check: bool,
697}
698
699#[derive(Args, Debug)]
700pub struct NewArgs {
701 #[arg(short, long, value_name = "DIR")]
703 pub name: String,
704}
705
706#[derive(Clone, Debug, Deserialize)]
708#[serde(try_from = "HashMap<String,String>")]
709pub struct Arguments {
710 pub data: HashMap<SlotId, String>,
711}
712
713impl TryFrom<HashMap<String, String>> for Arguments {
714 type Error = String;
715
716 fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
717 Ok(Self {
718 data: value
719 .into_iter()
720 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
721 .collect::<Result<HashMap<SlotId, String>, String>>()?,
722 })
723 }
724}
725
726impl FromStr for Arguments {
727 type Err = serde_json::Error;
728
729 fn from_str(s: &str) -> Result<Self, Self::Err> {
730 serde_json::from_str(s)
731 }
732}
733
734#[derive(Clone, Debug, Deserialize)]
736struct RequestJSON {
737 #[serde(default)]
739 principal: String,
740 #[serde(default)]
742 action: String,
743 #[serde(default)]
745 resource: String,
746 context: serde_json::Value,
748}
749
750#[cfg(feature = "partial-eval")]
751#[derive(Deserialize)]
753struct PartialRequestJSON {
754 pub(self) principal: Option<String>,
756 pub(self) action: Option<String>,
758 pub(self) resource: Option<String>,
760 pub(self) context: Option<serde_json::Value>,
762}
763
764#[derive(Args, Debug)]
765pub struct EvaluateArgs {
766 #[command(flatten)]
768 pub request: RequestArgs,
769 #[command(flatten)]
774 pub schema: OptionalSchemaArgs,
775 #[arg(long = "entities", value_name = "FILE")]
778 pub entities_file: Option<String>,
779 #[arg(value_name = "EXPRESSION")]
781 pub expression: String,
782}
783
784#[derive(Eq, PartialEq, Debug, Copy, Clone)]
785pub enum CedarExitCode {
786 Success,
789 Failure,
791 AuthorizeDeny,
794 ValidationFailure,
797 #[cfg(feature = "partial-eval")]
798 Unknown,
801}
802
803impl Termination for CedarExitCode {
804 fn report(self) -> ExitCode {
805 match self {
806 CedarExitCode::Success => ExitCode::SUCCESS,
807 CedarExitCode::Failure => ExitCode::FAILURE,
808 CedarExitCode::AuthorizeDeny => ExitCode::from(2),
809 CedarExitCode::ValidationFailure => ExitCode::from(3),
810 #[cfg(feature = "partial-eval")]
811 CedarExitCode::Unknown => ExitCode::SUCCESS,
812 }
813 }
814}
815
816pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
817 if (
820 &args.policies.policies_file,
821 &args.schema.schema_file,
822 &args.entities_file,
823 ) == (&None, &None, &None)
824 {
825 let pargs = PoliciesArgs {
826 policies_file: None, policy_format: args.policies.policy_format,
828 template_linked_file: args.policies.template_linked_file.clone(),
829 };
830 match pargs.get_policy_set() {
831 Ok(_) => return CedarExitCode::Success,
832 Err(e) => {
833 println!("{e:?}");
834 return CedarExitCode::Failure;
835 }
836 }
837 }
838
839 let mut exit_code = CedarExitCode::Success;
840 match args.policies.get_policy_set() {
841 Ok(_) => (),
842 Err(e) => {
843 println!("{e:?}");
844 exit_code = CedarExitCode::Failure;
845 }
846 }
847 let schema = match args.schema.get_schema() {
848 Ok(schema) => schema,
849 Err(e) => {
850 println!("{e:?}");
851 exit_code = CedarExitCode::Failure;
852 None
853 }
854 };
855 match &args.entities_file {
856 None => (),
857 Some(efile) => match load_entities(efile, schema.as_ref()) {
858 Ok(_) => (),
859 Err(e) => {
860 println!("{e:?}");
861 exit_code = CedarExitCode::Failure;
862 }
863 },
864 }
865 exit_code
866}
867
868pub fn validate(args: &ValidateArgs) -> CedarExitCode {
869 let mode = match args.validation_mode {
870 ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
871 ValidationMode::Permissive => {
872 #[cfg(not(feature = "permissive-validate"))]
873 {
874 eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
875 return CedarExitCode::Failure;
876 }
877 #[cfg(feature = "permissive-validate")]
878 cedar_policy::ValidationMode::Permissive
879 }
880 ValidationMode::Partial => {
881 #[cfg(not(feature = "partial-validate"))]
882 {
883 eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
884 return CedarExitCode::Failure;
885 }
886 #[cfg(feature = "partial-validate")]
887 cedar_policy::ValidationMode::Partial
888 }
889 };
890
891 let pset = match args.policies.get_policy_set() {
892 Ok(pset) => pset,
893 Err(e) => {
894 println!("{e:?}");
895 return CedarExitCode::Failure;
896 }
897 };
898
899 let schema = match args.schema.get_schema() {
900 Ok(schema) => schema,
901 Err(e) => {
902 println!("{e:?}");
903 return CedarExitCode::Failure;
904 }
905 };
906
907 let validator = Validator::new(schema);
908
909 let result = if let Some(level) = args.level {
910 validator.validate_with_level(&pset, mode, level)
911 } else {
912 validator.validate(&pset, mode)
913 };
914
915 if !result.validation_passed()
916 || (args.deny_warnings && !result.validation_passed_without_warnings())
917 {
918 println!(
919 "{:?}",
920 Report::new(result).wrap_err("policy set validation failed")
921 );
922 CedarExitCode::ValidationFailure
923 } else {
924 println!(
925 "{:?}",
926 Report::new(result).wrap_err("policy set validation passed")
927 );
928 CedarExitCode::Success
929 }
930}
931
932pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
933 println!();
934 let schema = match args.schema.get_schema() {
935 Ok(opt) => opt,
936 Err(e) => {
937 println!("{e:?}");
938 return (CedarExitCode::Failure, EvalResult::Bool(false));
939 }
940 };
941 let request = match args.request.get_request(schema.as_ref()) {
942 Ok(q) => q,
943 Err(e) => {
944 println!("{e:?}");
945 return (CedarExitCode::Failure, EvalResult::Bool(false));
946 }
947 };
948 let expr =
949 match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
950 Ok(expr) => expr,
951 Err(e) => {
952 println!("{:?}", e.with_source_code(args.expression.clone()));
953 return (CedarExitCode::Failure, EvalResult::Bool(false));
954 }
955 };
956 let entities = match &args.entities_file {
957 None => Entities::empty(),
958 Some(file) => match load_entities(file, schema.as_ref()) {
959 Ok(entities) => entities,
960 Err(e) => {
961 println!("{e:?}");
962 return (CedarExitCode::Failure, EvalResult::Bool(false));
963 }
964 },
965 };
966 match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
967 {
968 Err(e) => {
969 println!("{e:?}");
970 return (CedarExitCode::Failure, EvalResult::Bool(false));
971 }
972 Ok(result) => {
973 println!("{result}");
974 return (CedarExitCode::Success, result);
975 }
976 }
977}
978
979pub fn link(args: &LinkArgs) -> CedarExitCode {
980 if let Err(err) = link_inner(args) {
981 println!("{err:?}");
982 CedarExitCode::Failure
983 } else {
984 CedarExitCode::Success
985 }
986}
987
988pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
989 match load_entities(&args.entities_file, None) {
990 Ok(entities) => {
991 println!("{}", entities.to_dot_str());
992 CedarExitCode::Success
993 }
994 Err(report) => {
995 eprintln!("{report:?}");
996 CedarExitCode::Failure
997 }
998 }
999}
1000
1001fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
1006 let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
1007 let config = Config {
1008 line_width: args.line_width,
1009 indent_width: args.indent_width,
1010 };
1011 let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
1012 let are_policies_equivalent = policies_str == formatted_policy;
1013
1014 match &args.policies_file {
1015 Some(policies_file) if args.write => {
1016 let mut file = OpenOptions::new()
1017 .write(true)
1018 .truncate(true)
1019 .open(policies_file)
1020 .into_diagnostic()
1021 .wrap_err(format!("failed to open {policies_file} for writing"))?;
1022 file.write_all(formatted_policy.as_bytes())
1023 .into_diagnostic()
1024 .wrap_err(format!(
1025 "failed to write formatted policies to {policies_file}"
1026 ))?;
1027 }
1028 _ => print!("{formatted_policy}"),
1029 }
1030 Ok(are_policies_equivalent)
1031}
1032
1033pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
1034 match format_policies_inner(args) {
1035 Ok(false) if args.check => CedarExitCode::Failure,
1036 Err(err) => {
1037 println!("{err:?}");
1038 CedarExitCode::Failure
1039 }
1040 _ => CedarExitCode::Success,
1041 }
1042}
1043
1044fn translate_policy_to_cedar(
1045 json_src: Option<impl AsRef<Path> + std::marker::Copy>,
1046) -> Result<String> {
1047 let policy_set = read_json_policy_set(json_src)?;
1048 policy_set.to_cedar().ok_or_else(|| {
1049 miette!("Unable to translate policy set containing template linked policies.")
1050 })
1051}
1052
1053fn translate_policy_to_json(
1054 cedar_src: Option<impl AsRef<Path> + std::marker::Copy>,
1055) -> Result<String> {
1056 let policy_set = read_cedar_policy_set(cedar_src)?;
1057 let output = policy_set.to_json()?.to_string();
1058 Ok(output)
1059}
1060
1061fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
1062 let translate = match args.direction {
1063 PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
1064 PolicyTranslationDirection::JsonToCedar => translate_policy_to_cedar,
1065 };
1066 translate(args.input_file.as_ref())
1067}
1068
1069pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
1070 match translate_policy_inner(args) {
1071 Ok(sf) => {
1072 println!("{sf}");
1073 CedarExitCode::Success
1074 }
1075 Err(err) => {
1076 eprintln!("{err:?}");
1077 CedarExitCode::Failure
1078 }
1079 }
1080}
1081
1082fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
1083 let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
1084 let output = fragment.to_cedarschema()?;
1085 Ok(output)
1086}
1087
1088fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
1089 let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
1090 for warning in warnings {
1091 let report = miette::Report::new(warning);
1092 eprintln!("{report:?}");
1093 }
1094 let output = fragment.to_json_string()?;
1095 Ok(output)
1096}
1097
1098fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
1099 let translate = match args.direction {
1100 SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
1101 SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
1102 };
1103 read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate)
1104}
1105
1106pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
1107 match translate_schema_inner(args) {
1108 Ok(sf) => {
1109 println!("{sf}");
1110 CedarExitCode::Success
1111 }
1112 Err(err) => {
1113 eprintln!("{err:?}");
1114 CedarExitCode::Failure
1115 }
1116 }
1117}
1118
1119fn generate_schema(path: &Path) -> Result<()> {
1121 std::fs::write(
1122 path,
1123 serde_json::to_string_pretty(&serde_json::json!(
1124 {
1125 "": {
1126 "entityTypes": {
1127 "A": {
1128 "memberOfTypes": [
1129 "B"
1130 ]
1131 },
1132 "B": {
1133 "memberOfTypes": []
1134 },
1135 "C": {
1136 "memberOfTypes": []
1137 }
1138 },
1139 "actions": {
1140 "action": {
1141 "appliesTo": {
1142 "resourceTypes": [
1143 "C"
1144 ],
1145 "principalTypes": [
1146 "A",
1147 "B"
1148 ]
1149 }
1150 }
1151 }
1152 }
1153 }))
1154 .into_diagnostic()?,
1155 )
1156 .into_diagnostic()
1157}
1158
1159fn generate_policy(path: &Path) -> Result<()> {
1160 std::fs::write(
1161 path,
1162 r#"permit (
1163 principal in A::"a",
1164 action == Action::"action",
1165 resource == C::"c"
1166) when { true };
1167"#,
1168 )
1169 .into_diagnostic()
1170}
1171
1172fn generate_entities(path: &Path) -> Result<()> {
1173 std::fs::write(
1174 path,
1175 serde_json::to_string_pretty(&serde_json::json!(
1176 [
1177 {
1178 "uid": { "type": "A", "id": "a"} ,
1179 "attrs": {},
1180 "parents": [{"type": "B", "id": "b"}]
1181 },
1182 {
1183 "uid": { "type": "B", "id": "b"} ,
1184 "attrs": {},
1185 "parents": []
1186 },
1187 {
1188 "uid": { "type": "C", "id": "c"} ,
1189 "attrs": {},
1190 "parents": []
1191 }
1192 ]))
1193 .into_diagnostic()?,
1194 )
1195 .into_diagnostic()
1196}
1197
1198fn new_inner(args: &NewArgs) -> Result<()> {
1199 let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1200 std::fs::create_dir(dir).into_diagnostic()?;
1201 let schema_path = dir.join("schema.cedarschema.json");
1202 let policy_path = dir.join("policy.cedar");
1203 let entities_path = dir.join("entities.json");
1204 generate_schema(&schema_path)?;
1205 generate_policy(&policy_path)?;
1206 generate_entities(&entities_path)
1207}
1208
1209pub fn new(args: &NewArgs) -> CedarExitCode {
1210 if let Err(err) = new_inner(args) {
1211 println!("{err:?}");
1212 CedarExitCode::Failure
1213 } else {
1214 CedarExitCode::Success
1215 }
1216}
1217
1218pub fn language_version() -> CedarExitCode {
1219 let version = get_lang_version();
1220 println!(
1221 "Cedar language version: {}.{}",
1222 version.major, version.minor
1223 );
1224 CedarExitCode::Success
1225}
1226
1227fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
1228 data.iter()
1229 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
1230 .collect::<Result<HashMap<SlotId, EntityUid>>>()
1231}
1232
1233fn link_inner(args: &LinkArgs) -> Result<()> {
1234 let mut policies = args.policies.get_policy_set()?;
1235 let slotenv = create_slot_env(&args.arguments.data)?;
1236 policies.link(
1237 PolicyId::new(&args.template_id),
1238 PolicyId::new(&args.new_id),
1239 slotenv,
1240 )?;
1241 let linked = policies
1242 .policy(&PolicyId::new(&args.new_id))
1243 .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
1244 println!("Template-linked policy added: {linked}");
1245
1246 if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
1248 update_template_linked_file(
1249 links_filename,
1250 TemplateLinked {
1251 template_id: args.template_id.clone(),
1252 link_id: args.new_id.clone(),
1253 args: args.arguments.data.clone(),
1254 },
1255 )?;
1256 }
1257
1258 Ok(())
1259}
1260
1261#[derive(Clone, Serialize, Deserialize, Debug)]
1262#[serde(try_from = "LiteralTemplateLinked")]
1263#[serde(into = "LiteralTemplateLinked")]
1264struct TemplateLinked {
1265 template_id: String,
1266 link_id: String,
1267 args: HashMap<SlotId, String>,
1268}
1269
1270impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
1271 type Error = String;
1272
1273 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
1274 Ok(Self {
1275 template_id: value.template_id,
1276 link_id: value.link_id,
1277 args: value
1278 .args
1279 .into_iter()
1280 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1281 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
1282 })
1283 }
1284}
1285
1286fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
1287 match s.as_ref() {
1288 "?principal" => Ok(SlotId::principal()),
1289 "?resource" => Ok(SlotId::resource()),
1290 _ => Err(format!(
1291 "Invalid SlotId! Expected ?principal|?resource, got: {}",
1292 s.as_ref()
1293 )),
1294 }
1295}
1296
1297#[derive(Serialize, Deserialize)]
1298struct LiteralTemplateLinked {
1299 template_id: String,
1300 link_id: String,
1301 args: HashMap<String, String>,
1302}
1303
1304impl From<TemplateLinked> for LiteralTemplateLinked {
1305 fn from(i: TemplateLinked) -> Self {
1306 Self {
1307 template_id: i.template_id,
1308 link_id: i.link_id,
1309 args: i
1310 .args
1311 .into_iter()
1312 .map(|(k, v)| (format!("{k}"), v))
1313 .collect(),
1314 }
1315 }
1316}
1317
1318fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
1320 for template_linked in load_links_from_file(path)? {
1321 let slot_env = create_slot_env(&template_linked.args)?;
1322 policy_set.link(
1323 PolicyId::new(&template_linked.template_id),
1324 PolicyId::new(&template_linked.link_id),
1325 slot_env,
1326 )?;
1327 }
1328 Ok(())
1329}
1330
1331fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
1333 let f = match std::fs::File::open(path) {
1334 Ok(f) => f,
1335 Err(_) => {
1336 return Ok(vec![]);
1338 }
1339 };
1340 if f.metadata()
1341 .into_diagnostic()
1342 .wrap_err("Failed to read metadata")?
1343 .len()
1344 == 0
1345 {
1346 Ok(vec![])
1348 } else {
1349 serde_json::from_reader(f)
1351 .into_diagnostic()
1352 .wrap_err("Deserialization error")
1353 }
1354}
1355
1356fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
1358 let mut template_linked = load_links_from_file(path.as_ref())?;
1359 template_linked.push(new_linked);
1360 write_template_linked_file(&template_linked, path.as_ref())
1361}
1362
1363fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
1365 let f = OpenOptions::new()
1366 .write(true)
1367 .truncate(true)
1368 .create(true)
1369 .open(path)
1370 .into_diagnostic()?;
1371 serde_json::to_writer(f, linked).into_diagnostic()
1372}
1373
1374pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
1375 println!();
1376 let ans = execute_request(
1377 &args.request,
1378 &args.policies,
1379 &args.entities_file,
1380 &args.schema,
1381 args.timing,
1382 );
1383 match ans {
1384 Ok(ans) => {
1385 let status = match ans.decision() {
1386 Decision::Allow => {
1387 println!("ALLOW");
1388 CedarExitCode::Success
1389 }
1390 Decision::Deny => {
1391 println!("DENY");
1392 CedarExitCode::AuthorizeDeny
1393 }
1394 };
1395 if ans.diagnostics().errors().peekable().peek().is_some() {
1396 println!();
1397 for err in ans.diagnostics().errors() {
1398 println!("{err}");
1399 }
1400 }
1401 if args.verbose {
1402 println!();
1403 if ans.diagnostics().reason().peekable().peek().is_none() {
1404 println!("note: no policies applied to this request");
1405 } else {
1406 println!("note: this decision was due to the following policies:");
1407 for reason in ans.diagnostics().reason() {
1408 println!(" {reason}");
1409 }
1410 println!();
1411 }
1412 }
1413 status
1414 }
1415 Err(errs) => {
1416 for err in errs {
1417 println!("{err:?}");
1418 }
1419 CedarExitCode::Failure
1420 }
1421 }
1422}
1423
1424#[cfg(not(feature = "partial-eval"))]
1425pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
1426 {
1427 eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1428 return CedarExitCode::Failure;
1429 }
1430}
1431
1432#[cfg(feature = "partial-eval")]
1433pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
1434 println!();
1435 let ans = execute_partial_request(
1436 &args.request,
1437 &args.policies,
1438 &args.entities_file,
1439 &args.schema,
1440 args.timing,
1441 );
1442 match ans {
1443 Ok(ans) => match ans.decision() {
1444 Some(Decision::Allow) => {
1445 println!("ALLOW");
1446 CedarExitCode::Success
1447 }
1448 Some(Decision::Deny) => {
1449 println!("DENY");
1450 CedarExitCode::AuthorizeDeny
1451 }
1452 None => {
1453 println!("UNKNOWN");
1454 println!("All policy residuals:");
1455 for p in ans.nontrivial_residuals() {
1456 println!("{p}");
1457 }
1458 CedarExitCode::Unknown
1459 }
1460 },
1461 Err(errs) => {
1462 for err in errs {
1463 println!("{err:?}");
1464 }
1465 CedarExitCode::Failure
1466 }
1467 }
1468}
1469
1470#[derive(Clone, Debug)]
1471enum TestResult {
1472 Pass,
1473 Fail(String),
1474}
1475
1476fn compare_test_decisions(test: &TestCase, ans: &Response) -> TestResult {
1478 if ans.decision() == test.decision.into() {
1479 let mut errors = Vec::new();
1480 let reason = ans.diagnostics().reason().collect::<BTreeSet<_>>();
1481
1482 let missing_reason = test
1484 .reason
1485 .iter()
1486 .filter(|r| !reason.contains(&PolicyId::new(r)))
1487 .collect::<Vec<_>>();
1488
1489 if !missing_reason.is_empty() {
1490 errors.push(format!(
1491 "missing reason(s): {}",
1492 missing_reason
1493 .into_iter()
1494 .map(|r| format!("`{r}`"))
1495 .collect::<Vec<_>>()
1496 .join(", ")
1497 ));
1498 }
1499
1500 let num_errors = ans.diagnostics().errors().count();
1502 if num_errors != test.num_errors {
1503 errors.push(format!(
1504 "expected {} error(s), but got {} runtime error(s){}",
1505 test.num_errors,
1506 num_errors,
1507 if num_errors == 0 {
1508 "".to_string()
1509 } else {
1510 format!(
1511 ": {}",
1512 ans.diagnostics()
1513 .errors()
1514 .map(|e| e.to_string())
1515 .collect::<Vec<_>>()
1516 .join(", ")
1517 )
1518 },
1519 ));
1520 }
1521
1522 if errors.is_empty() {
1523 TestResult::Pass
1524 } else {
1525 TestResult::Fail(errors.join("; "))
1526 }
1527 } else {
1528 TestResult::Fail(format!(
1529 "expected {:?}, got {:?}",
1530 test.decision,
1531 ans.decision()
1532 ))
1533 }
1534}
1535
1536fn run_one_test(policies: &PolicySet, test: &serde_json::Value) -> Result<TestResult> {
1539 let test = TestCase::deserialize(test.clone()).into_diagnostic()?;
1540 let ans = Authorizer::new().is_authorized(&test.request, policies, &test.entities);
1541 Ok(compare_test_decisions(&test, &ans))
1542}
1543
1544fn run_tests_inner(args: &RunTestsArgs) -> Result<CedarExitCode> {
1545 let policies = args.policies.get_policy_set()?;
1546 let tests = load_partial_tests(&args.tests)?;
1547
1548 let mut total_fails: usize = 0;
1549
1550 println!("running {} test(s)", tests.len());
1551 for test in tests.iter() {
1552 if let Some(name) = test["name"].as_str() {
1553 print!(" test {name} ... ");
1554 } else {
1555 print!(" test (unamed) ... ");
1556 }
1557 std::io::stdout().flush().into_diagnostic()?;
1558
1559 match run_one_test(&policies, test) {
1560 Ok(TestResult::Pass) => {
1561 println!(
1562 "{}",
1563 "ok".if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
1564 );
1565 }
1566 Ok(TestResult::Fail(reason)) => {
1567 total_fails += 1;
1568 println!(
1569 "{}: {}",
1570 "fail".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
1571 reason
1572 );
1573 }
1574 Err(e) => {
1575 total_fails += 1;
1576 println!(
1577 "{}:\n {:?}",
1578 "error".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
1579 e
1580 );
1581 }
1582 }
1583 }
1584
1585 println!(
1586 "results: {} {}, {} {}",
1587 tests.len() - total_fails,
1588 if total_fails == 0 {
1589 "passed"
1590 .if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
1591 .to_string()
1592 } else {
1593 "passed".to_string()
1594 },
1595 total_fails,
1596 if total_fails != 0 {
1597 "failed"
1598 .if_supports_color(owo_colors::Stream::Stdout, |s| s.red())
1599 .to_string()
1600 } else {
1601 "failed".to_string()
1602 },
1603 );
1604
1605 Ok(if total_fails != 0 {
1606 CedarExitCode::Failure
1607 } else {
1608 CedarExitCode::Success
1609 })
1610}
1611
1612pub fn run_tests(args: &RunTestsArgs) -> CedarExitCode {
1613 match run_tests_inner(args) {
1614 Ok(status) => status,
1615 Err(e) => {
1616 println!("{e:?}");
1617 CedarExitCode::Failure
1618 }
1619 }
1620}
1621
1622#[derive(Copy, Clone, Debug, Deserialize)]
1623enum ExpectedDecision {
1624 #[serde(rename = "allow")]
1625 Allow,
1626 #[serde(rename = "deny")]
1627 Deny,
1628}
1629
1630impl From<ExpectedDecision> for Decision {
1631 fn from(value: ExpectedDecision) -> Self {
1632 match value {
1633 ExpectedDecision::Allow => Decision::Allow,
1634 ExpectedDecision::Deny => Decision::Deny,
1635 }
1636 }
1637}
1638
1639#[derive(Clone, Debug, Deserialize)]
1640struct TestCase {
1641 #[serde(deserialize_with = "deserialize_request")]
1642 request: Request,
1643 #[serde(deserialize_with = "deserialize_entities")]
1644 entities: Entities,
1645 decision: ExpectedDecision,
1646 reason: Vec<String>,
1647 num_errors: usize,
1648}
1649
1650fn deserialize_request<'de, D>(data: D) -> Result<Request, D::Error>
1652where
1653 D: Deserializer<'de>,
1654{
1655 let qjson = RequestJSON::deserialize(data)?;
1656
1657 let principal = qjson.principal.parse().map_err(|e| {
1658 serde::de::Error::custom(format!(
1659 "failed to parse principal `{}`: {}",
1660 qjson.principal, e
1661 ))
1662 })?;
1663
1664 let action = qjson.action.parse().map_err(|e| {
1665 serde::de::Error::custom(format!("failed to parse action `{}`: {}", qjson.action, e))
1666 })?;
1667
1668 let resource = qjson.resource.parse().map_err(|e| {
1669 serde::de::Error::custom(format!(
1670 "failed to parse resource `{}`: {}",
1671 qjson.resource, e
1672 ))
1673 })?;
1674
1675 let context = Context::from_json_value(qjson.context.clone(), None).map_err(|e| {
1676 serde::de::Error::custom(format!(
1677 "failed to parse context `{}`: {}",
1678 qjson.context, e
1679 ))
1680 })?;
1681
1682 Request::new(principal, action, resource, context, None)
1683 .map_err(|e| serde::de::Error::custom(format!("failed to create request: {e}")))
1684}
1685
1686fn deserialize_entities<'de, D>(data: D) -> Result<Entities, D::Error>
1688where
1689 D: Deserializer<'de>,
1690{
1691 let value = serde_json::Value::deserialize(data)?;
1692 Entities::from_json_value(value, None)
1693 .map_err(|e| serde::de::Error::custom(format!("failed to parse entities: {e}")))
1694}
1695
1696fn load_partial_tests(tests_filename: impl AsRef<Path>) -> Result<Vec<serde_json::Value>> {
1699 match std::fs::OpenOptions::new()
1700 .read(true)
1701 .open(tests_filename.as_ref())
1702 {
1703 Ok(f) => {
1704 let reader = BufReader::new(f);
1705 serde_json::from_reader(reader).map_err(|e| {
1706 miette!(
1707 "failed to parse tests from file {}: {e}",
1708 tests_filename.as_ref().display()
1709 )
1710 })
1711 }
1712 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1713 format!(
1714 "failed to open test file {}",
1715 tests_filename.as_ref().display()
1716 )
1717 }),
1718 }
1719}
1720
1721fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
1723 match std::fs::OpenOptions::new()
1724 .read(true)
1725 .open(entities_filename.as_ref())
1726 {
1727 Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
1728 format!(
1729 "failed to parse entities from file {}",
1730 entities_filename.as_ref().display()
1731 )
1732 }),
1733 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1734 format!(
1735 "failed to open entities file {}",
1736 entities_filename.as_ref().display()
1737 )
1738 }),
1739 }
1740}
1741
1742fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
1749 let mut new_ps = PolicySet::new();
1750 let t_iter = ps.templates().map(|t| match t.annotation("id") {
1751 None => Ok(t.clone()),
1752 Some(anno) => anno.parse().map(|a| t.new_id(a)),
1753 });
1754 for t in t_iter {
1755 let template = t.unwrap_or_else(|never| match never {});
1756 new_ps
1757 .add_template(template)
1758 .wrap_err("failed to add template to policy set")?;
1759 }
1760 let p_iter = ps.policies().map(|p| match p.annotation("id") {
1761 None => Ok(p.clone()),
1762 Some(anno) => anno.parse().map(|a| p.new_id(a)),
1763 });
1764 for p in p_iter {
1765 let policy = p.unwrap_or_else(|never| match never {});
1766 new_ps
1767 .add(policy)
1768 .wrap_err("failed to add template to policy set")?;
1769 }
1770 Ok(new_ps)
1771}
1772
1773fn read_from_file_or_stdin(filename: Option<&impl AsRef<Path>>, context: &str) -> Result<String> {
1775 let mut src_str = String::new();
1776 match filename {
1777 Some(path) => {
1778 src_str = std::fs::read_to_string(path)
1779 .into_diagnostic()
1780 .wrap_err_with(|| {
1781 format!("failed to open {context} file {}", path.as_ref().display())
1782 })?;
1783 }
1784 None => {
1785 std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
1786 .into_diagnostic()
1787 .wrap_err_with(|| format!("failed to read {context} from stdin"))?;
1788 }
1789 };
1790 Ok(src_str)
1791}
1792
1793fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
1795 read_from_file_or_stdin(Some(&filename), context)
1796}
1797
1798fn read_cedar_policy_set(
1801 filename: Option<impl AsRef<Path> + std::marker::Copy>,
1802) -> Result<PolicySet> {
1803 let context = "policy set";
1804 let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
1805 let ps = PolicySet::from_str(&ps_str)
1806 .map_err(|err| {
1807 let name = filename.map_or_else(
1808 || "<stdin>".to_owned(),
1809 |n| n.as_ref().display().to_string(),
1810 );
1811 Report::new(err).with_source_code(NamedSource::new(name, ps_str))
1812 })
1813 .wrap_err_with(|| format!("failed to parse {context}"))?;
1814 rename_from_id_annotation(&ps)
1815}
1816
1817fn read_json_policy_set(
1820 filename: Option<impl AsRef<Path> + std::marker::Copy>,
1821) -> Result<PolicySet> {
1822 let context = "JSON policy";
1823 let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
1824 let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
1825 let policy_type = get_json_policy_type(&json)?;
1826
1827 let add_json_source = |report: Report| {
1828 let name = filename.map_or_else(
1829 || "<stdin>".to_owned(),
1830 |n| n.as_ref().display().to_string(),
1831 );
1832 report.with_source_code(NamedSource::new(name, json_source.clone()))
1833 };
1834
1835 match policy_type {
1836 JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
1837 Ok(policy) => PolicySet::from_policies([policy])
1838 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1839 Err(_) => match Template::from_json(None, json)
1840 .map_err(|err| add_json_source(Report::new(err)))
1841 {
1842 Ok(template) => {
1843 let mut ps = PolicySet::new();
1844 ps.add_template(template)?;
1845 Ok(ps)
1846 }
1847 Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
1848 },
1849 },
1850 JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
1851 .map_err(|err| add_json_source(Report::new(err)))
1852 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1853 }
1854}
1855
1856fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
1857 let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
1858 let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
1859
1860 let json_has_property = |p| json.get(p).is_some();
1861 let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
1862 let has_any_policy_property = policy_properties.iter().any(json_has_property);
1863
1864 match (has_any_policy_set_property, has_any_policy_property) {
1865 (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
1866 (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
1867 (true, _) => Ok(JsonPolicyType::PolicySet),
1868 (_, true) => Ok(JsonPolicyType::SinglePolicy),
1869 }
1870}
1871
1872enum JsonPolicyType {
1873 SinglePolicy,
1874 PolicySet,
1875}
1876
1877fn execute_request(
1879 request: &RequestArgs,
1880 policies: &PoliciesArgs,
1881 entities_filename: impl AsRef<Path>,
1882 schema: &OptionalSchemaArgs,
1883 compute_duration: bool,
1884) -> Result<Response, Vec<Report>> {
1885 let mut errs = vec![];
1886 let policies = match policies.get_policy_set() {
1887 Ok(pset) => pset,
1888 Err(e) => {
1889 errs.push(e);
1890 PolicySet::new()
1891 }
1892 };
1893 let schema = match schema.get_schema() {
1894 Ok(opt) => opt,
1895 Err(e) => {
1896 errs.push(e);
1897 None
1898 }
1899 };
1900 let entities = match load_entities(entities_filename, schema.as_ref()) {
1901 Ok(entities) => entities,
1902 Err(e) => {
1903 errs.push(e);
1904 Entities::empty()
1905 }
1906 };
1907 match request.get_request(schema.as_ref()) {
1908 Ok(request) if errs.is_empty() => {
1909 let authorizer = Authorizer::new();
1910 let auth_start = Instant::now();
1911 let ans = authorizer.is_authorized(&request, &policies, &entities);
1912 let auth_dur = auth_start.elapsed();
1913 if compute_duration {
1914 println!(
1915 "Authorization Time (micro seconds) : {}",
1916 auth_dur.as_micros()
1917 );
1918 }
1919 Ok(ans)
1920 }
1921 Ok(_) => Err(errs),
1922 Err(e) => {
1923 errs.push(e.wrap_err("failed to parse request"));
1924 Err(errs)
1925 }
1926 }
1927}
1928
1929#[cfg(feature = "partial-eval")]
1930fn execute_partial_request(
1931 request: &PartialRequestArgs,
1932 policies: &PoliciesArgs,
1933 entities_filename: impl AsRef<Path>,
1934 schema: &OptionalSchemaArgs,
1935 compute_duration: bool,
1936) -> Result<PartialResponse, Vec<Report>> {
1937 let mut errs = vec![];
1938 let policies = match policies.get_policy_set() {
1939 Ok(pset) => pset,
1940 Err(e) => {
1941 errs.push(e);
1942 PolicySet::new()
1943 }
1944 };
1945 let schema = match schema.get_schema() {
1946 Ok(opt) => opt,
1947 Err(e) => {
1948 errs.push(e);
1949 None
1950 }
1951 };
1952 let entities = match load_entities(entities_filename, schema.as_ref()) {
1953 Ok(entities) => entities,
1954 Err(e) => {
1955 errs.push(e);
1956 Entities::empty()
1957 }
1958 };
1959 match request.get_request(schema.as_ref()) {
1960 Ok(request) if errs.is_empty() => {
1961 let authorizer = Authorizer::new();
1962 let auth_start = Instant::now();
1963 let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
1964 let auth_dur = auth_start.elapsed();
1965 if compute_duration {
1966 println!(
1967 "Authorization Time (micro seconds) : {}",
1968 auth_dur.as_micros()
1969 );
1970 }
1971 Ok(ans)
1972 }
1973 Ok(_) => Err(errs),
1974 Err(e) => {
1975 errs.push(e.wrap_err("failed to parse request"));
1976 Err(errs)
1977 }
1978 }
1979}