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