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 HumanToJson,
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 JsonToHuman,
145 HumanToJson,
147}
148
149#[derive(Debug, Clone, Copy, ValueEnum)]
150pub enum SchemaFormat {
151 Human,
153 Json,
155}
156
157impl Default for SchemaFormat {
158 fn default() -> Self {
159 Self::Json
160 }
161}
162
163#[derive(Args, Debug)]
164pub struct ValidateArgs {
165 #[arg(short, long = "schema", value_name = "FILE")]
167 pub schema_file: String,
168 #[command(flatten)]
170 pub policies: PoliciesArgs,
171 #[arg(long)]
173 pub deny_warnings: bool,
174 #[arg(long = "partial-validate")]
178 pub partial_validate: bool,
179 #[arg(long, value_enum, default_value_t = SchemaFormat::Json)]
181 pub schema_format: SchemaFormat,
182}
183
184#[derive(Args, Debug)]
185pub struct CheckParseArgs {
186 #[command(flatten)]
188 pub policies: PoliciesArgs,
189}
190
191#[derive(Args, Debug)]
193pub struct RequestArgs {
194 #[arg(short = 'l', long)]
196 pub principal: Option<String>,
197 #[arg(short, long)]
199 pub action: Option<String>,
200 #[arg(short, long)]
202 pub resource: Option<String>,
203 #[arg(short, long = "context", value_name = "FILE")]
206 pub context_json_file: Option<String>,
207 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
212 pub request_json_file: Option<String>,
213 #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
216 pub request_validation: bool,
217}
218
219#[cfg(feature = "partial-eval")]
220#[derive(Args, Debug)]
222pub struct PartialRequestArgs {
223 #[arg(short = 'l', long)]
225 pub principal: Option<String>,
226 #[arg(short, long)]
228 pub action: Option<String>,
229 #[arg(short, long)]
231 pub resource: Option<String>,
232 #[arg(short, long = "context", value_name = "FILE")]
235 pub context_json_file: Option<String>,
236 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
241 pub request_json_file: Option<String>,
242}
243
244impl RequestArgs {
245 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
252 match &self.request_json_file {
253 Some(jsonfile) => {
254 let jsonstring = std::fs::read_to_string(jsonfile)
255 .into_diagnostic()
256 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
257 let qjson: RequestJSON = serde_json::from_str(&jsonstring)
258 .into_diagnostic()
259 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
260 let principal = qjson
261 .principal
262 .map(|s| {
263 s.parse().wrap_err_with(|| {
264 format!("failed to parse principal in {jsonfile} as entity Uid")
265 })
266 })
267 .transpose()?;
268 let action = qjson
269 .action
270 .map(|s| {
271 s.parse().wrap_err_with(|| {
272 format!("failed to parse action in {jsonfile} as entity Uid")
273 })
274 })
275 .transpose()?;
276 let resource = qjson
277 .resource
278 .map(|s| {
279 s.parse().wrap_err_with(|| {
280 format!("failed to parse resource in {jsonfile} as entity Uid")
281 })
282 })
283 .transpose()?;
284 let context = Context::from_json_value(
285 qjson.context,
286 schema.and_then(|s| Some((s, action.as_ref()?))),
287 )
288 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
289 Request::new(
290 principal,
291 action,
292 resource,
293 context,
294 if self.request_validation {
295 schema
296 } else {
297 None
298 },
299 )
300 .map_err(|e| miette!("{e}"))
301 }
302 None => {
303 let principal = self
304 .principal
305 .as_ref()
306 .map(|s| {
307 s.parse().wrap_err_with(|| {
308 format!("failed to parse principal {s} as entity Uid")
309 })
310 })
311 .transpose()?;
312 let action = self
313 .action
314 .as_ref()
315 .map(|s| {
316 s.parse()
317 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
318 })
319 .transpose()?;
320 let resource = self
321 .resource
322 .as_ref()
323 .map(|s| {
324 s.parse()
325 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
326 })
327 .transpose()?;
328 let context: Context = match &self.context_json_file {
329 None => Context::empty(),
330 Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
331 Ok(f) => Context::from_json_file(
332 f,
333 schema.and_then(|s| Some((s, action.as_ref()?))),
334 )
335 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
336 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
337 format!("error while loading context from {jsonfile}")
338 })?,
339 },
340 };
341 Request::new(
342 principal,
343 action,
344 resource,
345 context,
346 if self.request_validation {
347 schema
348 } else {
349 None
350 },
351 )
352 .map_err(|e| miette!("{e}"))
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(Some(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(Some(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(Some(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::Human => read_human_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::Json)]
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 Human,
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: Option<String>,
614 #[serde(default)]
616 action: Option<String>,
617 #[serde(default)]
619 resource: Option<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::Json)]
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 = if args.partial_validate {
704 #[cfg(not(feature = "partial-validate"))]
705 {
706 eprintln!("Error: arguments include the experimental option `--partial-validate`, but this executable was not built with `partial-validate` experimental feature enabled");
707 return CedarExitCode::Failure;
708 }
709 #[cfg(feature = "partial-validate")]
710 ValidationMode::Partial
711 } else {
712 ValidationMode::default()
713 };
714
715 let pset = match args.policies.get_policy_set() {
716 Ok(pset) => pset,
717 Err(e) => {
718 println!("{e:?}");
719 return CedarExitCode::Failure;
720 }
721 };
722
723 let schema = match read_schema_file(&args.schema_file, args.schema_format) {
724 Ok(schema) => schema,
725 Err(e) => {
726 println!("{e:?}");
727 return CedarExitCode::Failure;
728 }
729 };
730
731 let validator = Validator::new(schema);
732 let result = validator.validate(&pset, mode);
733
734 if !result.validation_passed()
735 || (args.deny_warnings && !result.validation_passed_without_warnings())
736 {
737 println!(
738 "{:?}",
739 Report::new(result).wrap_err("policy set validation failed")
740 );
741 CedarExitCode::ValidationFailure
742 } else {
743 println!(
744 "{:?}",
745 Report::new(result).wrap_err("policy set validation passed")
746 );
747 CedarExitCode::Success
748 }
749}
750
751pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
752 println!();
753 let schema = match args
754 .schema_file
755 .as_ref()
756 .map(|f| read_schema_file(f, args.schema_format))
757 {
758 None => None,
759 Some(Ok(schema)) => Some(schema),
760 Some(Err(e)) => {
761 println!("{e:?}");
762 return (CedarExitCode::Failure, EvalResult::Bool(false));
763 }
764 };
765 let request = match args.request.get_request(schema.as_ref()) {
766 Ok(q) => q,
767 Err(e) => {
768 println!("{e:?}");
769 return (CedarExitCode::Failure, EvalResult::Bool(false));
770 }
771 };
772 let expr =
773 match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
774 Ok(expr) => expr,
775 Err(e) => {
776 println!("{:?}", e.with_source_code(args.expression.clone()));
777 return (CedarExitCode::Failure, EvalResult::Bool(false));
778 }
779 };
780 let entities = match &args.entities_file {
781 None => Entities::empty(),
782 Some(file) => match load_entities(file, schema.as_ref()) {
783 Ok(entities) => entities,
784 Err(e) => {
785 println!("{e:?}");
786 return (CedarExitCode::Failure, EvalResult::Bool(false));
787 }
788 },
789 };
790 match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
791 {
792 Err(e) => {
793 println!("{e:?}");
794 return (CedarExitCode::Failure, EvalResult::Bool(false));
795 }
796 Ok(result) => {
797 println!("{result}");
798 return (CedarExitCode::Success, result);
799 }
800 }
801}
802
803pub fn link(args: &LinkArgs) -> CedarExitCode {
804 if let Err(err) = link_inner(args) {
805 println!("{err:?}");
806 CedarExitCode::Failure
807 } else {
808 CedarExitCode::Success
809 }
810}
811
812pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
813 match load_entities(&args.entities_file, None) {
814 Ok(entities) => {
815 println!("{}", entities.to_dot_str());
816 CedarExitCode::Success
817 }
818 Err(report) => {
819 eprintln!("{report:?}");
820 CedarExitCode::Failure
821 }
822 }
823}
824
825fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
830 let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
831 let config = Config {
832 line_width: args.line_width,
833 indent_width: args.indent_width,
834 };
835 let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
836 let are_policies_equivalent = policies_str == formatted_policy;
837
838 match &args.policies_file {
839 Some(policies_file) if args.write => {
840 let mut file = OpenOptions::new()
841 .write(true)
842 .truncate(true)
843 .open(policies_file)
844 .into_diagnostic()
845 .wrap_err(format!("failed to open {policies_file} for writing"))?;
846 file.write_all(formatted_policy.as_bytes())
847 .into_diagnostic()
848 .wrap_err(format!(
849 "failed to write formatted policies to {policies_file}"
850 ))?;
851 }
852 _ => println!("{}", formatted_policy),
853 }
854 Ok(are_policies_equivalent)
855}
856
857pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
858 match format_policies_inner(args) {
859 Ok(false) if args.check => CedarExitCode::Failure,
860 Err(err) => {
861 println!("{err:?}");
862 CedarExitCode::Failure
863 }
864 _ => CedarExitCode::Success,
865 }
866}
867
868fn translate_policy_to_json(natural_src: impl AsRef<str>) -> Result<String> {
869 let policy_set = PolicySet::from_str(natural_src.as_ref())?;
870 let output = policy_set.to_json()?.to_string();
871 Ok(output)
872}
873
874fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
875 let translate = match args.direction {
876 PolicyTranslationDirection::HumanToJson => translate_policy_to_json,
877 };
878 read_from_file_or_stdin(args.input_file.clone(), "policy").and_then(translate)
879}
880
881pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
882 match translate_policy_inner(args) {
883 Ok(sf) => {
884 println!("{sf}");
885 CedarExitCode::Success
886 }
887 Err(err) => {
888 eprintln!("{err:?}");
889 CedarExitCode::Failure
890 }
891 }
892}
893
894fn translate_schema_to_human(json_src: impl AsRef<str>) -> Result<String> {
895 let fragment = SchemaFragment::from_str(json_src.as_ref())?;
896 let output = fragment.to_cedarschema()?;
897 Ok(output)
898}
899
900fn translate_schema_to_json(natural_src: impl AsRef<str>) -> Result<String> {
901 let (fragment, warnings) = SchemaFragment::from_cedarschema_str(natural_src.as_ref())?;
902 for warning in warnings {
903 let report = miette::Report::new(warning);
904 eprintln!("{:?}", report);
905 }
906 let output = fragment.to_json_string()?;
907 Ok(output)
908}
909
910fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
911 let translate = match args.direction {
912 SchemaTranslationDirection::JsonToHuman => translate_schema_to_human,
913 SchemaTranslationDirection::HumanToJson => translate_schema_to_json,
914 };
915 read_from_file_or_stdin(args.input_file.clone(), "schema").and_then(translate)
916}
917
918pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
919 match translate_schema_inner(args) {
920 Ok(sf) => {
921 println!("{sf}");
922 CedarExitCode::Success
923 }
924 Err(err) => {
925 eprintln!("{err:?}");
926 CedarExitCode::Failure
927 }
928 }
929}
930
931fn generate_schema(path: &Path) -> Result<()> {
932 std::fs::write(
933 path,
934 serde_json::to_string_pretty(&serde_json::json!(
935 {
936 "": {
937 "entityTypes": {
938 "A": {
939 "memberOfTypes": [
940 "B"
941 ]
942 },
943 "B": {
944 "memberOfTypes": []
945 },
946 "C": {
947 "memberOfTypes": []
948 }
949 },
950 "actions": {
951 "action": {
952 "appliesTo": {
953 "resourceTypes": [
954 "C"
955 ],
956 "principalTypes": [
957 "A",
958 "B"
959 ]
960 }
961 }
962 }
963 }
964 }))
965 .into_diagnostic()?,
966 )
967 .into_diagnostic()
968}
969
970fn generate_policy(path: &Path) -> Result<()> {
971 std::fs::write(
972 path,
973 r#"permit (
974 principal in A::"a",
975 action == Action::"action",
976 resource == C::"c"
977) when { true };
978"#,
979 )
980 .into_diagnostic()
981}
982
983fn generate_entities(path: &Path) -> Result<()> {
984 std::fs::write(
985 path,
986 serde_json::to_string_pretty(&serde_json::json!(
987 [
988 {
989 "uid": { "type": "A", "id": "a"} ,
990 "attrs": {},
991 "parents": [{"type": "B", "id": "b"}]
992 },
993 {
994 "uid": { "type": "B", "id": "b"} ,
995 "attrs": {},
996 "parents": []
997 },
998 {
999 "uid": { "type": "C", "id": "c"} ,
1000 "attrs": {},
1001 "parents": []
1002 }
1003 ]))
1004 .into_diagnostic()?,
1005 )
1006 .into_diagnostic()
1007}
1008
1009fn new_inner(args: &NewArgs) -> Result<()> {
1010 let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1011 std::fs::create_dir(dir).into_diagnostic()?;
1012 let schema_path = dir.join("schema.cedarschema.json");
1013 let policy_path = dir.join("policy.cedar");
1014 let entities_path = dir.join("entities.jon");
1015 generate_schema(&schema_path)?;
1016 generate_policy(&policy_path)?;
1017 generate_entities(&entities_path)
1018}
1019
1020pub fn new(args: &NewArgs) -> CedarExitCode {
1021 if let Err(err) = new_inner(args) {
1022 println!("{err:?}");
1023 CedarExitCode::Failure
1024 } else {
1025 CedarExitCode::Success
1026 }
1027}
1028
1029fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
1030 data.iter()
1031 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
1032 .collect::<Result<HashMap<SlotId, EntityUid>>>()
1033}
1034
1035fn link_inner(args: &LinkArgs) -> Result<()> {
1036 let mut policies = args.policies.get_policy_set()?;
1037 let slotenv = create_slot_env(&args.arguments.data)?;
1038 policies.link(
1039 PolicyId::new(&args.template_id),
1040 PolicyId::new(&args.new_id),
1041 slotenv,
1042 )?;
1043 let linked = policies
1044 .policy(&PolicyId::new(&args.new_id))
1045 .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
1046 println!("Template-linked policy added: {linked}");
1047
1048 if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
1050 update_template_linked_file(
1051 links_filename,
1052 TemplateLinked {
1053 template_id: args.template_id.clone(),
1054 link_id: args.new_id.clone(),
1055 args: args.arguments.data.clone(),
1056 },
1057 )?;
1058 }
1059
1060 Ok(())
1061}
1062
1063#[derive(Clone, Serialize, Deserialize, Debug)]
1064#[serde(try_from = "LiteralTemplateLinked")]
1065#[serde(into = "LiteralTemplateLinked")]
1066struct TemplateLinked {
1067 template_id: String,
1068 link_id: String,
1069 args: HashMap<SlotId, String>,
1070}
1071
1072impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
1073 type Error = String;
1074
1075 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
1076 Ok(Self {
1077 template_id: value.template_id,
1078 link_id: value.link_id,
1079 args: value
1080 .args
1081 .into_iter()
1082 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1083 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
1084 })
1085 }
1086}
1087
1088fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
1089 match s.as_ref() {
1090 "?principal" => Ok(SlotId::principal()),
1091 "?resource" => Ok(SlotId::resource()),
1092 _ => Err(format!(
1093 "Invalid SlotId! Expected ?principal|?resource, got: {}",
1094 s.as_ref()
1095 )),
1096 }
1097}
1098
1099#[derive(Serialize, Deserialize)]
1100struct LiteralTemplateLinked {
1101 template_id: String,
1102 link_id: String,
1103 args: HashMap<String, String>,
1104}
1105
1106impl From<TemplateLinked> for LiteralTemplateLinked {
1107 fn from(i: TemplateLinked) -> Self {
1108 Self {
1109 template_id: i.template_id,
1110 link_id: i.link_id,
1111 args: i
1112 .args
1113 .into_iter()
1114 .map(|(k, v)| (format!("{k}"), v))
1115 .collect(),
1116 }
1117 }
1118}
1119
1120fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
1122 for template_linked in load_links_from_file(path)? {
1123 let slot_env = create_slot_env(&template_linked.args)?;
1124 policy_set.link(
1125 PolicyId::new(&template_linked.template_id),
1126 PolicyId::new(&template_linked.link_id),
1127 slot_env,
1128 )?;
1129 }
1130 Ok(())
1131}
1132
1133fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
1135 let f = match std::fs::File::open(path) {
1136 Ok(f) => f,
1137 Err(_) => {
1138 return Ok(vec![]);
1140 }
1141 };
1142 if f.metadata()
1143 .into_diagnostic()
1144 .wrap_err("Failed to read metadata")?
1145 .len()
1146 == 0
1147 {
1148 Ok(vec![])
1150 } else {
1151 serde_json::from_reader(f)
1153 .into_diagnostic()
1154 .wrap_err("Deserialization error")
1155 }
1156}
1157
1158fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
1160 let mut template_linked = load_links_from_file(path.as_ref())?;
1161 template_linked.push(new_linked);
1162 write_template_linked_file(&template_linked, path.as_ref())
1163}
1164
1165fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
1167 let f = OpenOptions::new()
1168 .write(true)
1169 .truncate(true)
1170 .create(true)
1171 .open(path)
1172 .into_diagnostic()?;
1173 serde_json::to_writer(f, linked).into_diagnostic()
1174}
1175
1176pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
1177 println!();
1178 let ans = execute_request(
1179 &args.request,
1180 &args.policies,
1181 &args.entities_file,
1182 args.schema_file.as_ref(),
1183 args.schema_format,
1184 args.timing,
1185 );
1186 match ans {
1187 Ok(ans) => {
1188 let status = match ans.decision() {
1189 Decision::Allow => {
1190 println!("ALLOW");
1191 CedarExitCode::Success
1192 }
1193 Decision::Deny => {
1194 println!("DENY");
1195 CedarExitCode::AuthorizeDeny
1196 }
1197 };
1198 if ans.diagnostics().errors().peekable().peek().is_some() {
1199 println!();
1200 for err in ans.diagnostics().errors() {
1201 println!("{err}");
1202 }
1203 }
1204 if args.verbose {
1205 println!();
1206 if ans.diagnostics().reason().peekable().peek().is_none() {
1207 println!("note: no policies applied to this request");
1208 } else {
1209 println!("note: this decision was due to the following policies:");
1210 for reason in ans.diagnostics().reason() {
1211 println!(" {}", reason);
1212 }
1213 println!();
1214 }
1215 }
1216 status
1217 }
1218 Err(errs) => {
1219 for err in errs {
1220 println!("{err:?}");
1221 }
1222 CedarExitCode::Failure
1223 }
1224 }
1225}
1226
1227#[cfg(not(feature = "partial-eval"))]
1228pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
1229 {
1230 eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1231 return CedarExitCode::Failure;
1232 }
1233}
1234
1235#[cfg(feature = "partial-eval")]
1236pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
1237 println!();
1238 let ans = execute_partial_request(
1239 &args.request,
1240 &args.policies,
1241 &args.entities_file,
1242 args.timing,
1243 );
1244 match ans {
1245 Ok(ans) => {
1246 let status = match ans.decision() {
1247 Some(Decision::Allow) => {
1248 println!("ALLOW");
1249 CedarExitCode::Success
1250 }
1251 Some(Decision::Deny) => {
1252 println!("DENY");
1253 CedarExitCode::AuthorizeDeny
1254 }
1255 None => {
1256 println!("UNKNOWN");
1257 println!("All policy residuals:");
1258 for p in ans.nontrivial_residuals() {
1259 println!("{p}");
1260 }
1261 CedarExitCode::Unknown
1262 }
1263 };
1264 status
1265 }
1266 Err(errs) => {
1267 for err in errs {
1268 println!("{err:?}");
1269 }
1270 CedarExitCode::Failure
1271 }
1272 }
1273}
1274
1275fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
1277 match std::fs::OpenOptions::new()
1278 .read(true)
1279 .open(entities_filename.as_ref())
1280 {
1281 Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
1282 format!(
1283 "failed to parse entities from file {}",
1284 entities_filename.as_ref().display()
1285 )
1286 }),
1287 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1288 format!(
1289 "failed to open entities file {}",
1290 entities_filename.as_ref().display()
1291 )
1292 }),
1293 }
1294}
1295
1296fn rename_from_id_annotation(ps: PolicySet) -> Result<PolicySet> {
1303 let mut new_ps = PolicySet::new();
1304 let t_iter = ps.templates().map(|t| match t.annotation("id") {
1305 None => Ok(t.clone()),
1306 Some(anno) => anno.parse().map(|a| t.new_id(a)),
1307 });
1308 for t in t_iter {
1309 let template = t.wrap_err("failed to parse policy id annotation")?;
1310 new_ps
1311 .add_template(template)
1312 .wrap_err("failed to add template to policy set")?;
1313 }
1314 let p_iter = ps.policies().map(|p| match p.annotation("id") {
1315 None => Ok(p.clone()),
1316 Some(anno) => anno.parse().map(|a| p.new_id(a)),
1317 });
1318 for p in p_iter {
1319 let policy = p.wrap_err("failed to parse policy id annotation")?;
1320 new_ps
1321 .add(policy)
1322 .wrap_err("failed to add template to policy set")?;
1323 }
1324 Ok(new_ps)
1325}
1326
1327fn read_from_file_or_stdin(filename: Option<impl AsRef<Path>>, context: &str) -> Result<String> {
1329 let mut src_str = String::new();
1330 match filename.as_ref() {
1331 Some(path) => {
1332 src_str = std::fs::read_to_string(path)
1333 .into_diagnostic()
1334 .wrap_err_with(|| {
1335 format!("failed to open {context} file {}", path.as_ref().display())
1336 })?;
1337 }
1338 None => {
1339 std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
1340 .into_diagnostic()
1341 .wrap_err_with(|| format!("failed to read {} from stdin", context))?;
1342 }
1343 };
1344 Ok(src_str)
1345}
1346
1347fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
1349 read_from_file_or_stdin(Some(filename), context)
1350}
1351
1352fn read_human_policy_set(
1355 filename: Option<impl AsRef<Path> + std::marker::Copy>,
1356) -> Result<PolicySet> {
1357 let context = "policy set";
1358 let ps_str = read_from_file_or_stdin(filename, context)?;
1359 let ps = PolicySet::from_str(&ps_str)
1360 .map_err(|err| {
1361 let name = filename.map_or_else(
1362 || "<stdin>".to_owned(),
1363 |n| n.as_ref().display().to_string(),
1364 );
1365 Report::new(err).with_source_code(NamedSource::new(name, ps_str))
1366 })
1367 .wrap_err_with(|| format!("failed to parse {context}"))?;
1368 rename_from_id_annotation(ps)
1369}
1370
1371fn read_json_policy_set(
1374 filename: Option<impl AsRef<Path> + std::marker::Copy>,
1375) -> Result<PolicySet> {
1376 let context = "JSON policy";
1377 let json_source = read_from_file_or_stdin(filename, context)?;
1378 let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
1379 let policy_type = get_json_policy_type(&json)?;
1380
1381 let add_json_source = |report: Report| {
1382 let name = filename.map_or_else(
1383 || "<stdin>".to_owned(),
1384 |n| n.as_ref().display().to_string(),
1385 );
1386 report.with_source_code(NamedSource::new(name, json_source.clone()))
1387 };
1388
1389 match policy_type {
1390 JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
1391 Ok(policy) => PolicySet::from_policies([policy])
1392 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1393 Err(_) => match Template::from_json(None, json)
1394 .map_err(|err| add_json_source(Report::new(err)))
1395 {
1396 Ok(template) => {
1397 let mut ps = PolicySet::new();
1398 ps.add_template(template)?;
1399 Ok(ps)
1400 }
1401 Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
1402 },
1403 },
1404 JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
1405 .map_err(|err| add_json_source(Report::new(err)))
1406 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1407 }
1408}
1409
1410fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
1411 let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
1412 let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
1413
1414 let json_has_property = |p| json.get(p).is_some();
1415 let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
1416 let has_any_policy_property = policy_properties.iter().any(json_has_property);
1417
1418 match (has_any_policy_set_property, has_any_policy_property) {
1419 (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
1420 (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
1421 (true, _) => Ok(JsonPolicyType::PolicySet),
1422 (_, true) => Ok(JsonPolicyType::SinglePolicy),
1423 }
1424}
1425
1426enum JsonPolicyType {
1427 SinglePolicy,
1428 PolicySet,
1429}
1430
1431fn read_schema_file(
1432 filename: impl AsRef<Path> + std::marker::Copy,
1433 format: SchemaFormat,
1434) -> Result<Schema> {
1435 let schema_src = read_from_file(filename, "schema")?;
1436 match format {
1437 SchemaFormat::Json => Schema::from_str(&schema_src).wrap_err_with(|| {
1438 format!(
1439 "failed to parse schema from file {}",
1440 filename.as_ref().display()
1441 )
1442 }),
1443 SchemaFormat::Human => {
1444 let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)?;
1445 for warning in warnings {
1446 let report = miette::Report::new(warning);
1447 eprintln!("{:?}", report);
1448 }
1449 Ok(schema)
1450 }
1451 }
1452}
1453
1454fn execute_request(
1456 request: &RequestArgs,
1457 policies: &PoliciesArgs,
1458 entities_filename: impl AsRef<Path>,
1459 schema_filename: Option<impl AsRef<Path> + std::marker::Copy>,
1460 schema_format: SchemaFormat,
1461 compute_duration: bool,
1462) -> Result<Response, Vec<Report>> {
1463 let mut errs = vec![];
1464 let policies = match policies.get_policy_set() {
1465 Ok(pset) => pset,
1466 Err(e) => {
1467 errs.push(e);
1468 PolicySet::new()
1469 }
1470 };
1471 let schema = match schema_filename.map(|f| read_schema_file(f, schema_format)) {
1472 None => None,
1473 Some(Ok(schema)) => Some(schema),
1474 Some(Err(e)) => {
1475 errs.push(e);
1476 None
1477 }
1478 };
1479 let entities = match load_entities(entities_filename, schema.as_ref()) {
1480 Ok(entities) => entities,
1481 Err(e) => {
1482 errs.push(e);
1483 Entities::empty()
1484 }
1485 };
1486 match request.get_request(schema.as_ref()) {
1487 Ok(request) if errs.is_empty() => {
1488 let authorizer = Authorizer::new();
1489 let auth_start = Instant::now();
1490 let ans = authorizer.is_authorized(&request, &policies, &entities);
1491 let auth_dur = auth_start.elapsed();
1492 if compute_duration {
1493 println!(
1494 "Authorization Time (micro seconds) : {}",
1495 auth_dur.as_micros()
1496 );
1497 }
1498 Ok(ans)
1499 }
1500 Ok(_) => Err(errs),
1501 Err(e) => {
1502 errs.push(e.wrap_err("failed to parse request"));
1503 Err(errs)
1504 }
1505 }
1506}
1507
1508#[cfg(feature = "partial-eval")]
1509fn execute_partial_request(
1510 request: &PartialRequestArgs,
1511 policies: &PoliciesArgs,
1512 entities_filename: impl AsRef<Path>,
1513 compute_duration: bool,
1514) -> Result<PartialResponse, Vec<Report>> {
1515 let mut errs = vec![];
1516 let policies = match policies.get_policy_set() {
1517 Ok(pset) => pset,
1518 Err(e) => {
1519 errs.push(e);
1520 PolicySet::new()
1521 }
1522 };
1523 let entities = match load_entities(entities_filename, None) {
1524 Ok(entities) => entities,
1525 Err(e) => {
1526 errs.push(e);
1527 Entities::empty()
1528 }
1529 };
1530 match request.get_request() {
1531 Ok(request) if errs.is_empty() => {
1532 let authorizer = Authorizer::new();
1533 let auth_start = Instant::now();
1534 let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
1535 let auth_dur = auth_start.elapsed();
1536 if compute_duration {
1537 println!(
1538 "Authorization Time (micro seconds) : {}",
1539 auth_dur.as_micros()
1540 );
1541 }
1542 Ok(ans)
1543 }
1544 Ok(_) => Err(errs),
1545 Err(e) => {
1546 errs.push(e.wrap_err("failed to parse request"));
1547 Err(errs)
1548 }
1549 }
1550}