1#![allow(clippy::needless_return)]
21
22use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
23use miette::{miette, IntoDiagnostic, NamedSource, Report, Result, WrapErr};
24use serde::{Deserialize, Serialize};
25use std::io::Write;
26use std::{
27 collections::HashMap,
28 fmt::{self, Display},
29 fs::OpenOptions,
30 path::Path,
31 process::{ExitCode, Termination},
32 str::FromStr,
33 time::Instant,
34};
35
36use cedar_policy::*;
37use cedar_policy_formatter::{policies_str_to_pretty, Config};
38
39#[derive(Parser)]
41#[command(author, version, about, long_about = None)] pub struct Cli {
43 #[command(subcommand)]
44 pub command: Commands,
45 #[arg(
47 global = true,
48 short = 'f',
49 long = "error-format",
50 env = "CEDAR_ERROR_FORMAT",
51 default_value_t,
52 value_enum
53 )]
54 pub err_fmt: ErrorFormat,
55}
56
57#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
58pub enum ErrorFormat {
59 #[default]
62 Human,
63 Plain,
66 Json,
68}
69
70impl Display for ErrorFormat {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(
73 f,
74 "{}",
75 match self {
76 ErrorFormat::Human => "human",
77 ErrorFormat::Plain => "plain",
78 ErrorFormat::Json => "json",
79 }
80 )
81 }
82}
83
84#[derive(Subcommand, Debug)]
85pub enum Commands {
86 Authorize(AuthorizeArgs),
88 Evaluate(EvaluateArgs),
90 Validate(ValidateArgs),
92 CheckParse(CheckParseArgs),
94 Link(LinkArgs),
96 Format(FormatArgs),
98 TranslatePolicy(TranslatePolicyArgs),
100 TranslateSchema(TranslateSchemaArgs),
102 Visualize(VisualizeArgs),
105 New(NewArgs),
107 PartiallyAuthorize(PartiallyAuthorizeArgs),
109}
110
111#[derive(Args, Debug)]
112pub struct TranslatePolicyArgs {
113 #[arg(long)]
115 pub direction: PolicyTranslationDirection,
116 #[arg(short = 'p', long = "policies", value_name = "FILE")]
119 pub input_file: Option<String>,
120}
121
122#[derive(Debug, Clone, Copy, ValueEnum)]
124pub enum PolicyTranslationDirection {
125 CedarToJson,
127}
128
129#[derive(Args, Debug)]
130pub struct TranslateSchemaArgs {
131 #[arg(long)]
133 pub direction: SchemaTranslationDirection,
134 #[arg(short = 's', long = "schema", value_name = "FILE")]
137 pub input_file: Option<String>,
138}
139
140#[derive(Debug, Clone, Copy, ValueEnum)]
142pub enum SchemaTranslationDirection {
143 JsonToCedar,
145 CedarToJson,
147}
148
149#[derive(Debug, Clone, Copy, ValueEnum)]
150pub enum SchemaFormat {
151 Cedar,
153 Json,
155}
156
157impl Default for SchemaFormat {
158 fn default() -> Self {
159 Self::Cedar
160 }
161}
162
163#[derive(Debug, Clone, Copy, ValueEnum)]
164pub enum ValidationMode {
165 Strict,
167 Permissive,
169 Partial,
171}
172
173#[derive(Args, Debug)]
174pub struct ValidateArgs {
175 #[arg(short, long = "schema", value_name = "FILE")]
177 pub schema_file: String,
178 #[command(flatten)]
180 pub policies: PoliciesArgs,
181 #[arg(long)]
183 pub deny_warnings: bool,
184 #[arg(long, value_enum, default_value_t = SchemaFormat::Cedar)]
186 pub schema_format: SchemaFormat,
187 #[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
192 pub validation_mode: ValidationMode,
193}
194
195#[derive(Args, Debug)]
196pub struct CheckParseArgs {
197 #[command(flatten)]
199 pub policies: PoliciesArgs,
200}
201
202#[derive(Args, Debug)]
204pub struct RequestArgs {
205 #[arg(short = 'l', long)]
207 pub principal: Option<String>,
208 #[arg(short, long)]
210 pub action: Option<String>,
211 #[arg(short, long)]
213 pub resource: Option<String>,
214 #[arg(short, long = "context", value_name = "FILE")]
217 pub context_json_file: Option<String>,
218 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
223 pub request_json_file: Option<String>,
224 #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
227 pub request_validation: bool,
228}
229
230#[cfg(feature = "partial-eval")]
231#[derive(Args, Debug)]
233pub struct PartialRequestArgs {
234 #[arg(short = 'l', long)]
236 pub principal: Option<String>,
237 #[arg(short, long)]
239 pub action: Option<String>,
240 #[arg(short, long)]
242 pub resource: Option<String>,
243 #[arg(short, long = "context", value_name = "FILE")]
246 pub context_json_file: Option<String>,
247 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
252 pub request_json_file: Option<String>,
253}
254
255impl RequestArgs {
256 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
263 match &self.request_json_file {
264 Some(jsonfile) => {
265 let jsonstring = std::fs::read_to_string(jsonfile)
266 .into_diagnostic()
267 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
268 let qjson: RequestJSON = serde_json::from_str(&jsonstring)
269 .into_diagnostic()
270 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
271 let principal = qjson.principal.parse().wrap_err_with(|| {
272 format!("failed to parse principal in {jsonfile} as entity Uid")
273 })?;
274 let action = qjson.action.parse().wrap_err_with(|| {
275 format!("failed to parse action in {jsonfile} as entity Uid")
276 })?;
277 let resource = qjson.resource.parse().wrap_err_with(|| {
278 format!("failed to parse resource in {jsonfile} as entity Uid")
279 })?;
280 let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
281 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
282 Request::new(
283 principal,
284 action,
285 resource,
286 context,
287 if self.request_validation {
288 schema
289 } else {
290 None
291 },
292 )
293 .map_err(|e| miette!("{e}"))
294 }
295 None => {
296 let principal = self
297 .principal
298 .as_ref()
299 .map(|s| {
300 s.parse().wrap_err_with(|| {
301 format!("failed to parse principal {s} as entity Uid")
302 })
303 })
304 .transpose()?;
305 let action = self
306 .action
307 .as_ref()
308 .map(|s| {
309 s.parse()
310 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
311 })
312 .transpose()?;
313 let resource = self
314 .resource
315 .as_ref()
316 .map(|s| {
317 s.parse()
318 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
319 })
320 .transpose()?;
321 let context: Context = match &self.context_json_file {
322 None => Context::empty(),
323 Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
324 Ok(f) => Context::from_json_file(
325 f,
326 schema.and_then(|s| Some((s, action.as_ref()?))),
327 )
328 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
329 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
330 format!("error while loading context from {jsonfile}")
331 })?,
332 },
333 };
334 match (principal, action, resource) {
335 (Some(principal), Some(action), Some(resource)) => Request::new(
336 principal,
337 action,
338 resource,
339 context,
340 if self.request_validation {
341 schema
342 } else {
343 None
344 },
345 )
346 .map_err(|e| miette!("{e}")),
347 _ => Err(miette!(
348 "All three (`principal`, `action`, `resource`) variables must be specified"
349 )),
350 }
351 }
352 }
353 }
354}
355
356#[cfg(feature = "partial-eval")]
357impl PartialRequestArgs {
358 fn get_request(&self) -> Result<Request> {
359 let mut builder = RequestBuilder::default();
360 let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
361 Some(jsonfile) => {
362 let jsonstring = std::fs::read_to_string(jsonfile)
363 .into_diagnostic()
364 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
365 serde_json::from_str(&jsonstring)
366 .into_diagnostic()
367 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
368 }
369 None => PartialRequestJSON {
370 principal: self.principal.clone(),
371 action: self.action.clone(),
372 resource: self.resource.clone(),
373 context: self
374 .context_json_file
375 .as_ref()
376 .map(|jsonfile| {
377 let jsonstring = std::fs::read_to_string(jsonfile)
378 .into_diagnostic()
379 .wrap_err_with(|| {
380 format!("failed to open context-json file {jsonfile}")
381 })?;
382 serde_json::from_str(&jsonstring)
383 .into_diagnostic()
384 .wrap_err_with(|| {
385 format!("failed to parse context-json file {jsonfile}")
386 })
387 })
388 .transpose()?,
389 },
390 };
391
392 if let Some(principal) = qjson
393 .principal
394 .map(|s| {
395 s.parse()
396 .wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
397 })
398 .transpose()?
399 {
400 builder = builder.principal(principal);
401 }
402
403 if let Some(action) = qjson
404 .action
405 .map(|s| {
406 s.parse()
407 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
408 })
409 .transpose()?
410 {
411 builder = builder.action(action);
412 }
413
414 if let Some(resource) = qjson
415 .resource
416 .map(|s| {
417 s.parse()
418 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
419 })
420 .transpose()?
421 {
422 builder = builder.resource(resource);
423 }
424
425 if let Some(context) = qjson
426 .context
427 .map(|json| {
428 Context::from_json_value(json.clone(), None)
429 .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
430 })
431 .transpose()?
432 {
433 builder = builder.context(context);
434 }
435
436 Ok(builder.build())
437 }
438}
439
440#[derive(Args, Debug)]
442pub struct PoliciesArgs {
443 #[arg(short, long = "policies", value_name = "FILE")]
445 pub policies_file: Option<String>,
446 #[arg(long = "policy-format", default_value_t, value_enum)]
448 pub policy_format: PolicyFormat,
449 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
451 pub template_linked_file: Option<String>,
452}
453
454impl PoliciesArgs {
455 fn get_policy_set(&self) -> Result<PolicySet> {
457 let mut pset = match self.policy_format {
458 PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
459 PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
460 }?;
461 if let Some(links_filename) = self.template_linked_file.as_ref() {
462 add_template_links_to_set(links_filename, &mut pset)?;
463 }
464 Ok(pset)
465 }
466}
467
468#[derive(Args, Debug)]
469pub struct AuthorizeArgs {
470 #[command(flatten)]
472 pub request: RequestArgs,
473 #[command(flatten)]
475 pub policies: PoliciesArgs,
476 #[arg(short, long = "schema", value_name = "FILE")]
481 pub schema_file: Option<String>,
482 #[arg(long, value_enum, default_value_t = SchemaFormat::Cedar)]
484 pub schema_format: SchemaFormat,
485 #[arg(long = "entities", value_name = "FILE")]
487 pub entities_file: String,
488 #[arg(short, long)]
490 pub verbose: bool,
491 #[arg(short, long)]
493 pub timing: bool,
494}
495
496#[cfg(feature = "partial-eval")]
497#[derive(Args, Debug)]
498pub struct PartiallyAuthorizeArgs {
499 #[command(flatten)]
501 pub request: PartialRequestArgs,
502 #[command(flatten)]
504 pub policies: PoliciesArgs,
505 #[arg(long = "entities", value_name = "FILE")]
507 pub entities_file: String,
508 #[arg(short, long)]
510 pub timing: bool,
511}
512
513#[cfg(not(feature = "partial-eval"))]
514#[derive(Debug, Args)]
515pub struct PartiallyAuthorizeArgs;
516
517#[derive(Args, Debug)]
518pub struct VisualizeArgs {
519 #[arg(long = "entities", value_name = "FILE")]
520 pub entities_file: String,
521}
522
523#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
524pub enum PolicyFormat {
525 #[default]
527 Cedar,
528 Json,
530}
531
532#[derive(Args, Debug)]
533pub struct LinkArgs {
534 #[command(flatten)]
536 pub policies: PoliciesArgs,
537 #[arg(long)]
539 pub template_id: String,
540 #[arg(short, long)]
542 pub new_id: String,
543 #[arg(short, long)]
545 pub arguments: Arguments,
546}
547
548#[derive(Args, Debug)]
549pub struct FormatArgs {
550 #[arg(short, long = "policies", value_name = "FILE")]
552 pub policies_file: Option<String>,
553
554 #[arg(short, long, value_name = "UINT", default_value_t = 80)]
556 pub line_width: usize,
557
558 #[arg(short, long, value_name = "INT", default_value_t = 2)]
560 pub indent_width: isize,
561
562 #[arg(short, long, group = "action", requires = "policies_file")]
564 pub write: bool,
565
566 #[arg(short, long, group = "action")]
568 pub check: bool,
569}
570
571#[derive(Args, Debug)]
572pub struct NewArgs {
573 #[arg(short, long, value_name = "DIR")]
575 pub name: String,
576}
577
578#[derive(Clone, Debug, Deserialize)]
580#[serde(try_from = "HashMap<String,String>")]
581pub struct Arguments {
582 pub data: HashMap<SlotId, String>,
583}
584
585impl TryFrom<HashMap<String, String>> for Arguments {
586 type Error = String;
587
588 fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
589 Ok(Self {
590 data: value
591 .into_iter()
592 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
593 .collect::<Result<HashMap<SlotId, String>, String>>()?,
594 })
595 }
596}
597
598impl FromStr for Arguments {
599 type Err = serde_json::Error;
600
601 fn from_str(s: &str) -> Result<Self, Self::Err> {
602 serde_json::from_str(s)
603 }
604}
605
606#[derive(Deserialize)]
608struct RequestJSON {
609 #[serde(default)]
611 principal: String,
612 #[serde(default)]
614 action: String,
615 #[serde(default)]
617 resource: String,
618 context: serde_json::Value,
620}
621
622#[cfg(feature = "partial-eval")]
623#[derive(Deserialize)]
625pub(self) struct PartialRequestJSON {
626 pub(self) principal: Option<String>,
628 pub(self) action: Option<String>,
630 pub(self) resource: Option<String>,
632 pub(self) context: Option<serde_json::Value>,
634}
635
636#[derive(Args, Debug)]
637pub struct EvaluateArgs {
638 #[command(flatten)]
640 pub request: RequestArgs,
641 #[arg(short, long = "schema", value_name = "FILE")]
645 pub schema_file: Option<String>,
646 #[arg(long, value_enum, default_value_t = SchemaFormat::Cedar)]
648 pub schema_format: SchemaFormat,
649 #[arg(long = "entities", value_name = "FILE")]
652 pub entities_file: Option<String>,
653 #[arg(value_name = "EXPRESSION")]
655 pub expression: String,
656}
657
658#[derive(Eq, PartialEq, Debug)]
659pub enum CedarExitCode {
660 Success,
663 Failure,
665 AuthorizeDeny,
668 ValidationFailure,
671 #[cfg(feature = "partial-eval")]
672 Unknown,
675}
676
677impl Termination for CedarExitCode {
678 fn report(self) -> ExitCode {
679 match self {
680 CedarExitCode::Success => ExitCode::SUCCESS,
681 CedarExitCode::Failure => ExitCode::FAILURE,
682 CedarExitCode::AuthorizeDeny => ExitCode::from(2),
683 CedarExitCode::ValidationFailure => ExitCode::from(3),
684 #[cfg(feature = "partial-eval")]
685 CedarExitCode::Unknown => ExitCode::SUCCESS,
686 }
687 }
688}
689
690pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
691 match args.policies.get_policy_set() {
692 Ok(_) => CedarExitCode::Success,
693 Err(e) => {
694 println!("{e:?}");
695 CedarExitCode::Failure
696 }
697 }
698}
699
700pub fn validate(args: &ValidateArgs) -> CedarExitCode {
701 let mode = match args.validation_mode {
702 ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
703 ValidationMode::Permissive => {
704 #[cfg(not(feature = "permissive-validate"))]
705 {
706 eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
707 return CedarExitCode::Failure;
708 }
709 #[cfg(feature = "permissive-validate")]
710 cedar_policy::ValidationMode::Permissive
711 }
712 ValidationMode::Partial => {
713 #[cfg(not(feature = "partial-validate"))]
714 {
715 eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
716 return CedarExitCode::Failure;
717 }
718 #[cfg(feature = "partial-validate")]
719 cedar_policy::ValidationMode::Partial
720 }
721 };
722
723 let pset = match args.policies.get_policy_set() {
724 Ok(pset) => pset,
725 Err(e) => {
726 println!("{e:?}");
727 return CedarExitCode::Failure;
728 }
729 };
730
731 let schema = match read_schema_file(&args.schema_file, args.schema_format) {
732 Ok(schema) => schema,
733 Err(e) => {
734 println!("{e:?}");
735 return CedarExitCode::Failure;
736 }
737 };
738
739 let validator = Validator::new(schema);
740 let result = validator.validate(&pset, mode);
741
742 if !result.validation_passed()
743 || (args.deny_warnings && !result.validation_passed_without_warnings())
744 {
745 println!(
746 "{:?}",
747 Report::new(result).wrap_err("policy set validation failed")
748 );
749 CedarExitCode::ValidationFailure
750 } else {
751 println!(
752 "{:?}",
753 Report::new(result).wrap_err("policy set validation passed")
754 );
755 CedarExitCode::Success
756 }
757}
758
759pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
760 println!();
761 let schema = match args
762 .schema_file
763 .as_ref()
764 .map(|f| read_schema_file(f, args.schema_format))
765 {
766 None => None,
767 Some(Ok(schema)) => Some(schema),
768 Some(Err(e)) => {
769 println!("{e:?}");
770 return (CedarExitCode::Failure, EvalResult::Bool(false));
771 }
772 };
773 let request = match args.request.get_request(schema.as_ref()) {
774 Ok(q) => q,
775 Err(e) => {
776 println!("{e:?}");
777 return (CedarExitCode::Failure, EvalResult::Bool(false));
778 }
779 };
780 let expr =
781 match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
782 Ok(expr) => expr,
783 Err(e) => {
784 println!("{:?}", e.with_source_code(args.expression.clone()));
785 return (CedarExitCode::Failure, EvalResult::Bool(false));
786 }
787 };
788 let entities = match &args.entities_file {
789 None => Entities::empty(),
790 Some(file) => match load_entities(file, schema.as_ref()) {
791 Ok(entities) => entities,
792 Err(e) => {
793 println!("{e:?}");
794 return (CedarExitCode::Failure, EvalResult::Bool(false));
795 }
796 },
797 };
798 match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
799 {
800 Err(e) => {
801 println!("{e:?}");
802 return (CedarExitCode::Failure, EvalResult::Bool(false));
803 }
804 Ok(result) => {
805 println!("{result}");
806 return (CedarExitCode::Success, result);
807 }
808 }
809}
810
811pub fn link(args: &LinkArgs) -> CedarExitCode {
812 if let Err(err) = link_inner(args) {
813 println!("{err:?}");
814 CedarExitCode::Failure
815 } else {
816 CedarExitCode::Success
817 }
818}
819
820pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
821 match load_entities(&args.entities_file, None) {
822 Ok(entities) => {
823 println!("{}", entities.to_dot_str());
824 CedarExitCode::Success
825 }
826 Err(report) => {
827 eprintln!("{report:?}");
828 CedarExitCode::Failure
829 }
830 }
831}
832
833fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
838 let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
839 let config = Config {
840 line_width: args.line_width,
841 indent_width: args.indent_width,
842 };
843 let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
844 let are_policies_equivalent = policies_str == formatted_policy;
845
846 match &args.policies_file {
847 Some(policies_file) if args.write => {
848 let mut file = OpenOptions::new()
849 .write(true)
850 .truncate(true)
851 .open(policies_file)
852 .into_diagnostic()
853 .wrap_err(format!("failed to open {policies_file} for writing"))?;
854 file.write_all(formatted_policy.as_bytes())
855 .into_diagnostic()
856 .wrap_err(format!(
857 "failed to write formatted policies to {policies_file}"
858 ))?;
859 }
860 _ => println!("{}", formatted_policy),
861 }
862 Ok(are_policies_equivalent)
863}
864
865pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
866 match format_policies_inner(args) {
867 Ok(false) if args.check => CedarExitCode::Failure,
868 Err(err) => {
869 println!("{err:?}");
870 CedarExitCode::Failure
871 }
872 _ => CedarExitCode::Success,
873 }
874}
875
876fn translate_policy_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
877 let policy_set = PolicySet::from_str(cedar_src.as_ref())?;
878 let output = policy_set.to_json()?.to_string();
879 Ok(output)
880}
881
882fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
883 let translate = match args.direction {
884 PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
885 };
886 read_from_file_or_stdin(args.input_file.clone(), "policy").and_then(translate)
887}
888
889pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
890 match translate_policy_inner(args) {
891 Ok(sf) => {
892 println!("{sf}");
893 CedarExitCode::Success
894 }
895 Err(err) => {
896 eprintln!("{err:?}");
897 CedarExitCode::Failure
898 }
899 }
900}
901
902fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
903 let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
904 let output = fragment.to_cedarschema()?;
905 Ok(output)
906}
907
908fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
909 let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
910 for warning in warnings {
911 let report = miette::Report::new(warning);
912 eprintln!("{:?}", report);
913 }
914 let output = fragment.to_json_string()?;
915 Ok(output)
916}
917
918fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
919 let translate = match args.direction {
920 SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
921 SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
922 };
923 read_from_file_or_stdin(args.input_file.clone(), "schema").and_then(translate)
924}
925
926pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
927 match translate_schema_inner(args) {
928 Ok(sf) => {
929 println!("{sf}");
930 CedarExitCode::Success
931 }
932 Err(err) => {
933 eprintln!("{err:?}");
934 CedarExitCode::Failure
935 }
936 }
937}
938
939fn generate_schema(path: &Path) -> Result<()> {
941 std::fs::write(
942 path,
943 serde_json::to_string_pretty(&serde_json::json!(
944 {
945 "": {
946 "entityTypes": {
947 "A": {
948 "memberOfTypes": [
949 "B"
950 ]
951 },
952 "B": {
953 "memberOfTypes": []
954 },
955 "C": {
956 "memberOfTypes": []
957 }
958 },
959 "actions": {
960 "action": {
961 "appliesTo": {
962 "resourceTypes": [
963 "C"
964 ],
965 "principalTypes": [
966 "A",
967 "B"
968 ]
969 }
970 }
971 }
972 }
973 }))
974 .into_diagnostic()?,
975 )
976 .into_diagnostic()
977}
978
979fn generate_policy(path: &Path) -> Result<()> {
980 std::fs::write(
981 path,
982 r#"permit (
983 principal in A::"a",
984 action == Action::"action",
985 resource == C::"c"
986) when { true };
987"#,
988 )
989 .into_diagnostic()
990}
991
992fn generate_entities(path: &Path) -> Result<()> {
993 std::fs::write(
994 path,
995 serde_json::to_string_pretty(&serde_json::json!(
996 [
997 {
998 "uid": { "type": "A", "id": "a"} ,
999 "attrs": {},
1000 "parents": [{"type": "B", "id": "b"}]
1001 },
1002 {
1003 "uid": { "type": "B", "id": "b"} ,
1004 "attrs": {},
1005 "parents": []
1006 },
1007 {
1008 "uid": { "type": "C", "id": "c"} ,
1009 "attrs": {},
1010 "parents": []
1011 }
1012 ]))
1013 .into_diagnostic()?,
1014 )
1015 .into_diagnostic()
1016}
1017
1018fn new_inner(args: &NewArgs) -> Result<()> {
1019 let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1020 std::fs::create_dir(dir).into_diagnostic()?;
1021 let schema_path = dir.join("schema.cedarschema.json");
1022 let policy_path = dir.join("policy.cedar");
1023 let entities_path = dir.join("entities.json");
1024 generate_schema(&schema_path)?;
1025 generate_policy(&policy_path)?;
1026 generate_entities(&entities_path)
1027}
1028
1029pub fn new(args: &NewArgs) -> CedarExitCode {
1030 if let Err(err) = new_inner(args) {
1031 println!("{err:?}");
1032 CedarExitCode::Failure
1033 } else {
1034 CedarExitCode::Success
1035 }
1036}
1037
1038fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
1039 data.iter()
1040 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
1041 .collect::<Result<HashMap<SlotId, EntityUid>>>()
1042}
1043
1044fn link_inner(args: &LinkArgs) -> Result<()> {
1045 let mut policies = args.policies.get_policy_set()?;
1046 let slotenv = create_slot_env(&args.arguments.data)?;
1047 policies.link(
1048 PolicyId::new(&args.template_id),
1049 PolicyId::new(&args.new_id),
1050 slotenv,
1051 )?;
1052 let linked = policies
1053 .policy(&PolicyId::new(&args.new_id))
1054 .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
1055 println!("Template-linked policy added: {linked}");
1056
1057 if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
1059 update_template_linked_file(
1060 links_filename,
1061 TemplateLinked {
1062 template_id: args.template_id.clone(),
1063 link_id: args.new_id.clone(),
1064 args: args.arguments.data.clone(),
1065 },
1066 )?;
1067 }
1068
1069 Ok(())
1070}
1071
1072#[derive(Clone, Serialize, Deserialize, Debug)]
1073#[serde(try_from = "LiteralTemplateLinked")]
1074#[serde(into = "LiteralTemplateLinked")]
1075struct TemplateLinked {
1076 template_id: String,
1077 link_id: String,
1078 args: HashMap<SlotId, String>,
1079}
1080
1081impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
1082 type Error = String;
1083
1084 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
1085 Ok(Self {
1086 template_id: value.template_id,
1087 link_id: value.link_id,
1088 args: value
1089 .args
1090 .into_iter()
1091 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1092 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
1093 })
1094 }
1095}
1096
1097fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
1098 match s.as_ref() {
1099 "?principal" => Ok(SlotId::principal()),
1100 "?resource" => Ok(SlotId::resource()),
1101 _ => Err(format!(
1102 "Invalid SlotId! Expected ?principal|?resource, got: {}",
1103 s.as_ref()
1104 )),
1105 }
1106}
1107
1108#[derive(Serialize, Deserialize)]
1109struct LiteralTemplateLinked {
1110 template_id: String,
1111 link_id: String,
1112 args: HashMap<String, String>,
1113}
1114
1115impl From<TemplateLinked> for LiteralTemplateLinked {
1116 fn from(i: TemplateLinked) -> Self {
1117 Self {
1118 template_id: i.template_id,
1119 link_id: i.link_id,
1120 args: i
1121 .args
1122 .into_iter()
1123 .map(|(k, v)| (format!("{k}"), v))
1124 .collect(),
1125 }
1126 }
1127}
1128
1129fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
1131 for template_linked in load_links_from_file(path)? {
1132 let slot_env = create_slot_env(&template_linked.args)?;
1133 policy_set.link(
1134 PolicyId::new(&template_linked.template_id),
1135 PolicyId::new(&template_linked.link_id),
1136 slot_env,
1137 )?;
1138 }
1139 Ok(())
1140}
1141
1142fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
1144 let f = match std::fs::File::open(path) {
1145 Ok(f) => f,
1146 Err(_) => {
1147 return Ok(vec![]);
1149 }
1150 };
1151 if f.metadata()
1152 .into_diagnostic()
1153 .wrap_err("Failed to read metadata")?
1154 .len()
1155 == 0
1156 {
1157 Ok(vec![])
1159 } else {
1160 serde_json::from_reader(f)
1162 .into_diagnostic()
1163 .wrap_err("Deserialization error")
1164 }
1165}
1166
1167fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
1169 let mut template_linked = load_links_from_file(path.as_ref())?;
1170 template_linked.push(new_linked);
1171 write_template_linked_file(&template_linked, path.as_ref())
1172}
1173
1174fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
1176 let f = OpenOptions::new()
1177 .write(true)
1178 .truncate(true)
1179 .create(true)
1180 .open(path)
1181 .into_diagnostic()?;
1182 serde_json::to_writer(f, linked).into_diagnostic()
1183}
1184
1185pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
1186 println!();
1187 let ans = execute_request(
1188 &args.request,
1189 &args.policies,
1190 &args.entities_file,
1191 args.schema_file.as_ref(),
1192 args.schema_format,
1193 args.timing,
1194 );
1195 match ans {
1196 Ok(ans) => {
1197 let status = match ans.decision() {
1198 Decision::Allow => {
1199 println!("ALLOW");
1200 CedarExitCode::Success
1201 }
1202 Decision::Deny => {
1203 println!("DENY");
1204 CedarExitCode::AuthorizeDeny
1205 }
1206 };
1207 if ans.diagnostics().errors().peekable().peek().is_some() {
1208 println!();
1209 for err in ans.diagnostics().errors() {
1210 println!("{err}");
1211 }
1212 }
1213 if args.verbose {
1214 println!();
1215 if ans.diagnostics().reason().peekable().peek().is_none() {
1216 println!("note: no policies applied to this request");
1217 } else {
1218 println!("note: this decision was due to the following policies:");
1219 for reason in ans.diagnostics().reason() {
1220 println!(" {}", reason);
1221 }
1222 println!();
1223 }
1224 }
1225 status
1226 }
1227 Err(errs) => {
1228 for err in errs {
1229 println!("{err:?}");
1230 }
1231 CedarExitCode::Failure
1232 }
1233 }
1234}
1235
1236#[cfg(not(feature = "partial-eval"))]
1237pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
1238 {
1239 eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1240 return CedarExitCode::Failure;
1241 }
1242}
1243
1244#[cfg(feature = "partial-eval")]
1245pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
1246 println!();
1247 let ans = execute_partial_request(
1248 &args.request,
1249 &args.policies,
1250 &args.entities_file,
1251 args.timing,
1252 );
1253 match ans {
1254 Ok(ans) => {
1255 let status = match ans.decision() {
1256 Some(Decision::Allow) => {
1257 println!("ALLOW");
1258 CedarExitCode::Success
1259 }
1260 Some(Decision::Deny) => {
1261 println!("DENY");
1262 CedarExitCode::AuthorizeDeny
1263 }
1264 None => {
1265 println!("UNKNOWN");
1266 println!("All policy residuals:");
1267 for p in ans.nontrivial_residuals() {
1268 println!("{p}");
1269 }
1270 CedarExitCode::Unknown
1271 }
1272 };
1273 status
1274 }
1275 Err(errs) => {
1276 for err in errs {
1277 println!("{err:?}");
1278 }
1279 CedarExitCode::Failure
1280 }
1281 }
1282}
1283
1284fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
1286 match std::fs::OpenOptions::new()
1287 .read(true)
1288 .open(entities_filename.as_ref())
1289 {
1290 Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
1291 format!(
1292 "failed to parse entities from file {}",
1293 entities_filename.as_ref().display()
1294 )
1295 }),
1296 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1297 format!(
1298 "failed to open entities file {}",
1299 entities_filename.as_ref().display()
1300 )
1301 }),
1302 }
1303}
1304
1305fn rename_from_id_annotation(ps: PolicySet) -> Result<PolicySet> {
1312 let mut new_ps = PolicySet::new();
1313 let t_iter = ps.templates().map(|t| match t.annotation("id") {
1314 None => Ok(t.clone()),
1315 Some(anno) => anno.parse().map(|a| t.new_id(a)),
1316 });
1317 for t in t_iter {
1318 let template = t.unwrap_or_else(|never| match never {});
1319 new_ps
1320 .add_template(template)
1321 .wrap_err("failed to add template to policy set")?;
1322 }
1323 let p_iter = ps.policies().map(|p| match p.annotation("id") {
1324 None => Ok(p.clone()),
1325 Some(anno) => anno.parse().map(|a| p.new_id(a)),
1326 });
1327 for p in p_iter {
1328 let policy = p.unwrap_or_else(|never| match never {});
1329 new_ps
1330 .add(policy)
1331 .wrap_err("failed to add template to policy set")?;
1332 }
1333 Ok(new_ps)
1334}
1335
1336fn read_from_file_or_stdin(filename: Option<impl AsRef<Path>>, context: &str) -> Result<String> {
1338 let mut src_str = String::new();
1339 match filename.as_ref() {
1340 Some(path) => {
1341 src_str = std::fs::read_to_string(path)
1342 .into_diagnostic()
1343 .wrap_err_with(|| {
1344 format!("failed to open {context} file {}", path.as_ref().display())
1345 })?;
1346 }
1347 None => {
1348 std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
1349 .into_diagnostic()
1350 .wrap_err_with(|| format!("failed to read {} from stdin", context))?;
1351 }
1352 };
1353 Ok(src_str)
1354}
1355
1356fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
1358 read_from_file_or_stdin(Some(filename), context)
1359}
1360
1361fn read_cedar_policy_set(
1364 filename: Option<impl AsRef<Path> + std::marker::Copy>,
1365) -> Result<PolicySet> {
1366 let context = "policy set";
1367 let ps_str = read_from_file_or_stdin(filename, context)?;
1368 let ps = PolicySet::from_str(&ps_str)
1369 .map_err(|err| {
1370 let name = filename.map_or_else(
1371 || "<stdin>".to_owned(),
1372 |n| n.as_ref().display().to_string(),
1373 );
1374 Report::new(err).with_source_code(NamedSource::new(name, ps_str))
1375 })
1376 .wrap_err_with(|| format!("failed to parse {context}"))?;
1377 rename_from_id_annotation(ps)
1378}
1379
1380fn read_json_policy_set(
1383 filename: Option<impl AsRef<Path> + std::marker::Copy>,
1384) -> Result<PolicySet> {
1385 let context = "JSON policy";
1386 let json_source = read_from_file_or_stdin(filename, context)?;
1387 let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
1388 let policy_type = get_json_policy_type(&json)?;
1389
1390 let add_json_source = |report: Report| {
1391 let name = filename.map_or_else(
1392 || "<stdin>".to_owned(),
1393 |n| n.as_ref().display().to_string(),
1394 );
1395 report.with_source_code(NamedSource::new(name, json_source.clone()))
1396 };
1397
1398 match policy_type {
1399 JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
1400 Ok(policy) => PolicySet::from_policies([policy])
1401 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1402 Err(_) => match Template::from_json(None, json)
1403 .map_err(|err| add_json_source(Report::new(err)))
1404 {
1405 Ok(template) => {
1406 let mut ps = PolicySet::new();
1407 ps.add_template(template)?;
1408 Ok(ps)
1409 }
1410 Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
1411 },
1412 },
1413 JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
1414 .map_err(|err| add_json_source(Report::new(err)))
1415 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1416 }
1417}
1418
1419fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
1420 let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
1421 let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
1422
1423 let json_has_property = |p| json.get(p).is_some();
1424 let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
1425 let has_any_policy_property = policy_properties.iter().any(json_has_property);
1426
1427 match (has_any_policy_set_property, has_any_policy_property) {
1428 (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
1429 (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
1430 (true, _) => Ok(JsonPolicyType::PolicySet),
1431 (_, true) => Ok(JsonPolicyType::SinglePolicy),
1432 }
1433}
1434
1435enum JsonPolicyType {
1436 SinglePolicy,
1437 PolicySet,
1438}
1439
1440fn read_schema_file(
1441 filename: impl AsRef<Path> + std::marker::Copy,
1442 format: SchemaFormat,
1443) -> Result<Schema> {
1444 let schema_src = read_from_file(filename, "schema")?;
1445 match format {
1446 SchemaFormat::Json => Schema::from_json_str(&schema_src).wrap_err_with(|| {
1447 format!(
1448 "failed to parse schema from file {}",
1449 filename.as_ref().display()
1450 )
1451 }),
1452 SchemaFormat::Cedar => {
1453 let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)?;
1454 for warning in warnings {
1455 let report = miette::Report::new(warning);
1456 eprintln!("{:?}", report);
1457 }
1458 Ok(schema)
1459 }
1460 }
1461}
1462
1463fn execute_request(
1465 request: &RequestArgs,
1466 policies: &PoliciesArgs,
1467 entities_filename: impl AsRef<Path>,
1468 schema_filename: Option<impl AsRef<Path> + std::marker::Copy>,
1469 schema_format: SchemaFormat,
1470 compute_duration: bool,
1471) -> Result<Response, Vec<Report>> {
1472 let mut errs = vec![];
1473 let policies = match policies.get_policy_set() {
1474 Ok(pset) => pset,
1475 Err(e) => {
1476 errs.push(e);
1477 PolicySet::new()
1478 }
1479 };
1480 let schema = match schema_filename.map(|f| read_schema_file(f, schema_format)) {
1481 None => None,
1482 Some(Ok(schema)) => Some(schema),
1483 Some(Err(e)) => {
1484 errs.push(e);
1485 None
1486 }
1487 };
1488 let entities = match load_entities(entities_filename, schema.as_ref()) {
1489 Ok(entities) => entities,
1490 Err(e) => {
1491 errs.push(e);
1492 Entities::empty()
1493 }
1494 };
1495 match request.get_request(schema.as_ref()) {
1496 Ok(request) if errs.is_empty() => {
1497 let authorizer = Authorizer::new();
1498 let auth_start = Instant::now();
1499 let ans = authorizer.is_authorized(&request, &policies, &entities);
1500 let auth_dur = auth_start.elapsed();
1501 if compute_duration {
1502 println!(
1503 "Authorization Time (micro seconds) : {}",
1504 auth_dur.as_micros()
1505 );
1506 }
1507 Ok(ans)
1508 }
1509 Ok(_) => Err(errs),
1510 Err(e) => {
1511 errs.push(e.wrap_err("failed to parse request"));
1512 Err(errs)
1513 }
1514 }
1515}
1516
1517#[cfg(feature = "partial-eval")]
1518fn execute_partial_request(
1519 request: &PartialRequestArgs,
1520 policies: &PoliciesArgs,
1521 entities_filename: impl AsRef<Path>,
1522 compute_duration: bool,
1523) -> Result<PartialResponse, Vec<Report>> {
1524 let mut errs = vec![];
1525 let policies = match policies.get_policy_set() {
1526 Ok(pset) => pset,
1527 Err(e) => {
1528 errs.push(e);
1529 PolicySet::new()
1530 }
1531 };
1532 let entities = match load_entities(entities_filename, None) {
1533 Ok(entities) => entities,
1534 Err(e) => {
1535 errs.push(e);
1536 Entities::empty()
1537 }
1538 };
1539 match request.get_request() {
1540 Ok(request) if errs.is_empty() => {
1541 let authorizer = Authorizer::new();
1542 let auth_start = Instant::now();
1543 let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
1544 let auth_dur = auth_start.elapsed();
1545 if compute_duration {
1546 println!(
1547 "Authorization Time (micro seconds) : {}",
1548 auth_dur.as_micros()
1549 );
1550 }
1551 Ok(ans)
1552 }
1553 Ok(_) => Err(errs),
1554 Err(e) => {
1555 errs.push(e.wrap_err("failed to parse request"));
1556 Err(errs)
1557 }
1558 }
1559}