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