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::{
26 collections::HashMap,
27 fmt::{self, Display},
28 fs::OpenOptions,
29 path::Path,
30 process::{ExitCode, Termination},
31 str::FromStr,
32 time::Instant,
33};
34
35use cedar_policy::*;
36use cedar_policy_formatter::{policies_str_to_pretty, Config};
37
38#[derive(Parser)]
40#[command(author, version, about, long_about = None)] pub struct Cli {
42 #[command(subcommand)]
43 pub command: Commands,
44 #[arg(
46 global = true,
47 short = 'f',
48 long = "error-format",
49 env = "CEDAR_ERROR_FORMAT",
50 default_value_t,
51 value_enum
52 )]
53 pub err_fmt: ErrorFormat,
54}
55
56#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
57pub enum ErrorFormat {
58 #[default]
61 Human,
62 Plain,
65 Json,
67}
68
69impl Display for ErrorFormat {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(
72 f,
73 "{}",
74 match self {
75 ErrorFormat::Human => "human",
76 ErrorFormat::Plain => "plain",
77 ErrorFormat::Json => "json",
78 }
79 )
80 }
81}
82
83#[derive(Subcommand, Debug)]
84pub enum Commands {
85 Authorize(AuthorizeArgs),
87 Evaluate(EvaluateArgs),
89 Validate(ValidateArgs),
91 CheckParse(CheckParseArgs),
93 Link(LinkArgs),
95 Format(FormatArgs),
97 TranslateSchema(TranslateSchemaArgs),
99 New(NewArgs),
101}
102
103#[derive(Args, Debug)]
104pub struct TranslateSchemaArgs {
105 #[arg(long)]
107 pub direction: TranslationDirection,
108 #[arg(short = 's', long = "schema", value_name = "FILE")]
111 pub input_file: Option<String>,
112}
113
114#[derive(Debug, Clone, Copy, ValueEnum)]
116pub enum TranslationDirection {
117 JsonToHuman,
119 HumanToJson,
121}
122
123#[derive(Debug, Clone, Copy, ValueEnum)]
124pub enum SchemaFormat {
125 Human,
127 Json,
129}
130
131impl Default for SchemaFormat {
132 fn default() -> Self {
133 Self::Json
134 }
135}
136
137#[derive(Args, Debug)]
138pub struct ValidateArgs {
139 #[arg(short, long = "schema", value_name = "FILE")]
141 pub schema_file: String,
142 #[command(flatten)]
144 pub policies: PoliciesArgs,
145 #[arg(long)]
147 pub deny_warnings: bool,
148 #[arg(long = "partial-validate")]
152 pub partial_validate: bool,
153 #[arg(long, value_enum, default_value_t = SchemaFormat::Json)]
155 pub schema_format: SchemaFormat,
156}
157
158#[derive(Args, Debug)]
159pub struct CheckParseArgs {
160 #[command(flatten)]
162 pub policies: PoliciesArgs,
163}
164
165#[derive(Args, Debug)]
167pub struct RequestArgs {
168 #[arg(short = 'l', long)]
170 pub principal: Option<String>,
171 #[arg(short, long)]
173 pub action: Option<String>,
174 #[arg(short, long)]
176 pub resource: Option<String>,
177 #[arg(short, long = "context", value_name = "FILE")]
180 pub context_json_file: Option<String>,
181 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
186 pub request_json_file: Option<String>,
187 #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
190 pub request_validation: bool,
191}
192
193impl RequestArgs {
194 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
201 match &self.request_json_file {
202 Some(jsonfile) => {
203 let jsonstring = std::fs::read_to_string(jsonfile)
204 .into_diagnostic()
205 .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
206 let qjson: RequestJSON = serde_json::from_str(&jsonstring)
207 .into_diagnostic()
208 .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
209 let principal = qjson
210 .principal
211 .map(|s| {
212 s.parse().wrap_err_with(|| {
213 format!("failed to parse principal in {jsonfile} as entity Uid")
214 })
215 })
216 .transpose()?;
217 let action = qjson
218 .action
219 .map(|s| {
220 s.parse().wrap_err_with(|| {
221 format!("failed to parse action in {jsonfile} as entity Uid")
222 })
223 })
224 .transpose()?;
225 let resource = qjson
226 .resource
227 .map(|s| {
228 s.parse().wrap_err_with(|| {
229 format!("failed to parse resource in {jsonfile} as entity Uid")
230 })
231 })
232 .transpose()?;
233 let context = Context::from_json_value(
234 qjson.context,
235 schema.and_then(|s| Some((s, action.as_ref()?))),
236 )
237 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
238 Request::new(
239 principal,
240 action,
241 resource,
242 context,
243 if self.request_validation {
244 schema
245 } else {
246 None
247 },
248 )
249 .map_err(|e| miette!("{e}"))
250 }
251 None => {
252 let principal = self
253 .principal
254 .as_ref()
255 .map(|s| {
256 s.parse().wrap_err_with(|| {
257 format!("failed to parse principal {s} as entity Uid")
258 })
259 })
260 .transpose()?;
261 let action = self
262 .action
263 .as_ref()
264 .map(|s| {
265 s.parse()
266 .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
267 })
268 .transpose()?;
269 let resource = self
270 .resource
271 .as_ref()
272 .map(|s| {
273 s.parse()
274 .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
275 })
276 .transpose()?;
277 let context: Context = match &self.context_json_file {
278 None => Context::empty(),
279 Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
280 Ok(f) => Context::from_json_file(
281 f,
282 schema.and_then(|s| Some((s, action.as_ref()?))),
283 )
284 .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
285 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
286 format!("error while loading context from {jsonfile}")
287 })?,
288 },
289 };
290 Request::new(
291 principal,
292 action,
293 resource,
294 context,
295 if self.request_validation {
296 schema
297 } else {
298 None
299 },
300 )
301 .map_err(|e| miette!("{e}"))
302 }
303 }
304 }
305}
306
307#[derive(Args, Debug)]
309pub struct PoliciesArgs {
310 #[arg(short, long = "policies", value_name = "FILE")]
312 pub policies_file: Option<String>,
313 #[arg(long = "policy-format", default_value_t, value_enum)]
315 pub policy_format: PolicyFormat,
316 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
318 pub template_linked_file: Option<String>,
319}
320
321impl PoliciesArgs {
322 fn get_policy_set(&self) -> Result<PolicySet> {
324 let mut pset = match self.policy_format {
325 PolicyFormat::Human => read_policy_set(self.policies_file.as_ref()),
326 PolicyFormat::Json => read_json_policy(self.policies_file.as_ref()),
327 }?;
328 if let Some(links_filename) = self.template_linked_file.as_ref() {
329 add_template_links_to_set(links_filename, &mut pset)?;
330 }
331 Ok(pset)
332 }
333}
334
335#[derive(Args, Debug)]
336pub struct AuthorizeArgs {
337 #[command(flatten)]
339 pub request: RequestArgs,
340 #[command(flatten)]
342 pub policies: PoliciesArgs,
343 #[arg(short, long = "schema", value_name = "FILE")]
348 pub schema_file: Option<String>,
349 #[arg(long, value_enum, default_value_t = SchemaFormat::Json)]
351 pub schema_format: SchemaFormat,
352 #[arg(long = "entities", value_name = "FILE")]
354 pub entities_file: String,
355 #[arg(short, long)]
357 pub verbose: bool,
358 #[arg(short, long)]
360 pub timing: bool,
361}
362
363#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
364pub enum PolicyFormat {
365 #[default]
367 Human,
368 Json,
370}
371
372#[derive(Args, Debug)]
373pub struct LinkArgs {
374 #[command(flatten)]
376 pub policies: PoliciesArgs,
377 #[arg(long)]
379 pub template_id: String,
380 #[arg(short, long)]
382 pub new_id: String,
383 #[arg(short, long)]
385 pub arguments: Arguments,
386}
387
388#[derive(Args, Debug)]
389pub struct FormatArgs {
390 #[arg(short, long = "policies", value_name = "FILE")]
392 pub policies_file: Option<String>,
393
394 #[arg(short, long, value_name = "UINT", default_value_t = 80)]
396 pub line_width: usize,
397
398 #[arg(short, long, value_name = "INT", default_value_t = 2)]
400 pub indent_width: isize,
401}
402
403#[derive(Args, Debug)]
404pub struct NewArgs {
405 #[arg(short, long, value_name = "DIR")]
407 pub name: String,
408}
409
410#[derive(Clone, Debug, Deserialize)]
412#[serde(try_from = "HashMap<String,String>")]
413pub struct Arguments {
414 pub data: HashMap<SlotId, String>,
415}
416
417impl TryFrom<HashMap<String, String>> for Arguments {
418 type Error = String;
419
420 fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
421 Ok(Self {
422 data: value
423 .into_iter()
424 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
425 .collect::<Result<HashMap<SlotId, String>, String>>()?,
426 })
427 }
428}
429
430impl FromStr for Arguments {
431 type Err = serde_json::Error;
432
433 fn from_str(s: &str) -> Result<Self, Self::Err> {
434 serde_json::from_str(s)
435 }
436}
437
438#[derive(Deserialize)]
440struct RequestJSON {
441 #[serde(default)]
443 principal: Option<String>,
444 #[serde(default)]
446 action: Option<String>,
447 #[serde(default)]
449 resource: Option<String>,
450 context: serde_json::Value,
452}
453
454#[derive(Args, Debug)]
455pub struct EvaluateArgs {
456 #[command(flatten)]
458 pub request: RequestArgs,
459 #[arg(short, long = "schema", value_name = "FILE")]
463 pub schema_file: Option<String>,
464 #[arg(long, value_enum, default_value_t = SchemaFormat::Json)]
466 pub schema_format: SchemaFormat,
467 #[arg(long = "entities", value_name = "FILE")]
470 pub entities_file: Option<String>,
471 #[arg(value_name = "EXPRESSION")]
473 pub expression: String,
474}
475
476#[derive(Eq, PartialEq, Debug)]
477pub enum CedarExitCode {
478 Success,
481 Failure,
483 AuthorizeDeny,
486 ValidationFailure,
489}
490
491impl Termination for CedarExitCode {
492 fn report(self) -> ExitCode {
493 match self {
494 CedarExitCode::Success => ExitCode::SUCCESS,
495 CedarExitCode::Failure => ExitCode::FAILURE,
496 CedarExitCode::AuthorizeDeny => ExitCode::from(2),
497 CedarExitCode::ValidationFailure => ExitCode::from(3),
498 }
499 }
500}
501
502pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
503 match args.policies.get_policy_set() {
504 Ok(_) => CedarExitCode::Success,
505 Err(e) => {
506 println!("{e:?}");
507 CedarExitCode::Failure
508 }
509 }
510}
511
512pub fn validate(args: &ValidateArgs) -> CedarExitCode {
513 let mode = if args.partial_validate {
514 #[cfg(not(feature = "partial-validate"))]
515 {
516 eprintln!("Error: arguments include the experimental option `--partial-validate`, but this executable was not built with `partial-validate` experimental feature enabled");
517 return CedarExitCode::Failure;
518 }
519 #[cfg(feature = "partial-validate")]
520 ValidationMode::Partial
521 } else {
522 ValidationMode::default()
523 };
524
525 let pset = match args.policies.get_policy_set() {
526 Ok(pset) => pset,
527 Err(e) => {
528 println!("{e:?}");
529 return CedarExitCode::Failure;
530 }
531 };
532
533 let schema = match read_schema_file(&args.schema_file, args.schema_format) {
534 Ok(schema) => schema,
535 Err(e) => {
536 println!("{e:?}");
537 return CedarExitCode::Failure;
538 }
539 };
540
541 let validator = Validator::new(schema);
542 let result = validator.validate(&pset, mode);
543
544 if !result.validation_passed()
545 || (args.deny_warnings && !result.validation_passed_without_warnings())
546 {
547 println!(
548 "{:?}",
549 Report::new(result).wrap_err("policy set validation failed")
550 );
551 CedarExitCode::ValidationFailure
552 } else {
553 println!(
554 "{:?}",
555 Report::new(result).wrap_err("policy set validation passed")
556 );
557 CedarExitCode::Success
558 }
559}
560
561pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
562 println!();
563 let schema = match args
564 .schema_file
565 .as_ref()
566 .map(|f| read_schema_file(f, args.schema_format))
567 {
568 None => None,
569 Some(Ok(schema)) => Some(schema),
570 Some(Err(e)) => {
571 println!("{e:?}");
572 return (CedarExitCode::Failure, EvalResult::Bool(false));
573 }
574 };
575 let request = match args.request.get_request(schema.as_ref()) {
576 Ok(q) => q,
577 Err(e) => {
578 println!("{e:?}");
579 return (CedarExitCode::Failure, EvalResult::Bool(false));
580 }
581 };
582 let expr =
583 match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
584 Ok(expr) => expr,
585 Err(e) => {
586 println!("{:?}", e.with_source_code(args.expression.clone()));
587 return (CedarExitCode::Failure, EvalResult::Bool(false));
588 }
589 };
590 let entities = match &args.entities_file {
591 None => Entities::empty(),
592 Some(file) => match load_entities(file, schema.as_ref()) {
593 Ok(entities) => entities,
594 Err(e) => {
595 println!("{e:?}");
596 return (CedarExitCode::Failure, EvalResult::Bool(false));
597 }
598 },
599 };
600 match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
601 {
602 Err(e) => {
603 println!("{e:?}");
604 return (CedarExitCode::Failure, EvalResult::Bool(false));
605 }
606 Ok(result) => {
607 println!("{result}");
608 return (CedarExitCode::Success, result);
609 }
610 }
611}
612
613pub fn link(args: &LinkArgs) -> CedarExitCode {
614 if let Err(err) = link_inner(args) {
615 println!("{err:?}");
616 CedarExitCode::Failure
617 } else {
618 CedarExitCode::Success
619 }
620}
621
622fn format_policies_inner(args: &FormatArgs) -> Result<()> {
623 let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
624 let config = Config {
625 line_width: args.line_width,
626 indent_width: args.indent_width,
627 };
628 println!("{}", policies_str_to_pretty(&policies_str, &config)?);
629 Ok(())
630}
631
632pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
633 if let Err(err) = format_policies_inner(args) {
634 println!("{err:?}");
635 CedarExitCode::Failure
636 } else {
637 CedarExitCode::Success
638 }
639}
640
641fn translate_to_human(json_src: impl AsRef<str>) -> Result<String> {
642 let fragment = SchemaFragment::from_str(json_src.as_ref())?;
643 let output = fragment.as_natural()?;
644 Ok(output)
645}
646
647fn translate_to_json(natural_src: impl AsRef<str>) -> Result<String> {
648 let (fragment, warnings) = SchemaFragment::from_str_natural(natural_src.as_ref())?;
649 for warning in warnings {
650 let report = miette::Report::new(warning);
651 eprintln!("{:?}", report);
652 }
653 let output = fragment.as_json_string()?;
654 Ok(output)
655}
656
657fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
658 let translate = match args.direction {
659 TranslationDirection::JsonToHuman => translate_to_human,
660 TranslationDirection::HumanToJson => translate_to_json,
661 };
662 read_from_file_or_stdin(args.input_file.clone(), "schema").and_then(translate)
663}
664pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
665 match translate_schema_inner(args) {
666 Ok(sf) => {
667 println!("{sf}");
668 CedarExitCode::Success
669 }
670 Err(err) => {
671 eprintln!("{err:?}");
672 CedarExitCode::Failure
673 }
674 }
675}
676
677fn generate_schema(path: &Path) -> Result<()> {
678 std::fs::write(
679 path,
680 serde_json::to_string_pretty(&serde_json::json!(
681 {
682 "": {
683 "entityTypes": {
684 "A": {
685 "memberOfTypes": [
686 "B"
687 ]
688 },
689 "B": {
690 "memberOfTypes": []
691 },
692 "C": {
693 "memberOfTypes": []
694 }
695 },
696 "actions": {
697 "action": {
698 "appliesTo": {
699 "resourceTypes": [
700 "C"
701 ],
702 "principalTypes": [
703 "A",
704 "B"
705 ]
706 }
707 }
708 }
709 }
710 }))
711 .into_diagnostic()?,
712 )
713 .into_diagnostic()
714}
715
716fn generate_policy(path: &Path) -> Result<()> {
717 std::fs::write(
718 path,
719 r#"permit (
720 principal in A::"a",
721 action == Action::"action",
722 resource == C::"c"
723) when { true };
724"#,
725 )
726 .into_diagnostic()
727}
728
729fn generate_entities(path: &Path) -> Result<()> {
730 std::fs::write(
731 path,
732 serde_json::to_string_pretty(&serde_json::json!(
733 [
734 {
735 "uid": { "type": "A", "id": "a"} ,
736 "attrs": {},
737 "parents": [{"type": "B", "id": "b"}]
738 },
739 {
740 "uid": { "type": "B", "id": "b"} ,
741 "attrs": {},
742 "parents": []
743 },
744 {
745 "uid": { "type": "C", "id": "c"} ,
746 "attrs": {},
747 "parents": []
748 }
749 ]))
750 .into_diagnostic()?,
751 )
752 .into_diagnostic()
753}
754
755fn new_inner(args: &NewArgs) -> Result<()> {
756 let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
757 std::fs::create_dir(dir).into_diagnostic()?;
758 let schema_path = dir.join("schema.cedarschema.json");
759 let policy_path = dir.join("policy.cedar");
760 let entities_path = dir.join("entities.jon");
761 generate_schema(&schema_path)?;
762 generate_policy(&policy_path)?;
763 generate_entities(&entities_path)
764}
765
766pub fn new(args: &NewArgs) -> CedarExitCode {
767 if let Err(err) = new_inner(args) {
768 println!("{err:?}");
769 CedarExitCode::Failure
770 } else {
771 CedarExitCode::Success
772 }
773}
774
775fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
776 data.iter()
777 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
778 .collect::<Result<HashMap<SlotId, EntityUid>>>()
779}
780
781fn link_inner(args: &LinkArgs) -> Result<()> {
782 let mut policies = args.policies.get_policy_set()?;
783 let slotenv = create_slot_env(&args.arguments.data)?;
784 policies.link(
785 PolicyId::new(&args.template_id),
786 PolicyId::new(&args.new_id),
787 slotenv,
788 )?;
789 let linked = policies
790 .policy(&PolicyId::new(&args.new_id))
791 .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
792 println!("Template-linked policy added: {linked}");
793
794 if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
796 update_template_linked_file(
797 links_filename,
798 TemplateLinked {
799 template_id: args.template_id.clone(),
800 link_id: args.new_id.clone(),
801 args: args.arguments.data.clone(),
802 },
803 )?;
804 }
805
806 Ok(())
807}
808
809#[derive(Clone, Serialize, Deserialize, Debug)]
810#[serde(try_from = "LiteralTemplateLinked")]
811#[serde(into = "LiteralTemplateLinked")]
812struct TemplateLinked {
813 template_id: String,
814 link_id: String,
815 args: HashMap<SlotId, String>,
816}
817
818impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
819 type Error = String;
820
821 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
822 Ok(Self {
823 template_id: value.template_id,
824 link_id: value.link_id,
825 args: value
826 .args
827 .into_iter()
828 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
829 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
830 })
831 }
832}
833
834fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
835 match s.as_ref() {
836 "?principal" => Ok(SlotId::principal()),
837 "?resource" => Ok(SlotId::resource()),
838 _ => Err(format!(
839 "Invalid SlotId! Expected ?principal|?resource, got: {}",
840 s.as_ref()
841 )),
842 }
843}
844
845#[derive(Serialize, Deserialize)]
846struct LiteralTemplateLinked {
847 template_id: String,
848 link_id: String,
849 args: HashMap<String, String>,
850}
851
852impl From<TemplateLinked> for LiteralTemplateLinked {
853 fn from(i: TemplateLinked) -> Self {
854 Self {
855 template_id: i.template_id,
856 link_id: i.link_id,
857 args: i
858 .args
859 .into_iter()
860 .map(|(k, v)| (format!("{k}"), v))
861 .collect(),
862 }
863 }
864}
865
866fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
868 for template_linked in load_links_from_file(path)? {
869 let slot_env = create_slot_env(&template_linked.args)?;
870 policy_set.link(
871 PolicyId::new(&template_linked.template_id),
872 PolicyId::new(&template_linked.link_id),
873 slot_env,
874 )?;
875 }
876 Ok(())
877}
878
879fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
881 let f = match std::fs::File::open(path) {
882 Ok(f) => f,
883 Err(_) => {
884 return Ok(vec![]);
886 }
887 };
888 if f.metadata()
889 .into_diagnostic()
890 .wrap_err("Failed to read metadata")?
891 .len()
892 == 0
893 {
894 Ok(vec![])
896 } else {
897 serde_json::from_reader(f)
899 .into_diagnostic()
900 .wrap_err("Deserialization error")
901 }
902}
903
904fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
906 let mut template_linked = load_links_from_file(path.as_ref())?;
907 template_linked.push(new_linked);
908 write_template_linked_file(&template_linked, path.as_ref())
909}
910
911fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
913 let f = OpenOptions::new()
914 .write(true)
915 .truncate(true)
916 .create(true)
917 .open(path)
918 .into_diagnostic()?;
919 serde_json::to_writer(f, linked).into_diagnostic()
920}
921
922pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
923 println!();
924 let ans = execute_request(
925 &args.request,
926 &args.policies,
927 &args.entities_file,
928 args.schema_file.as_ref(),
929 args.schema_format,
930 args.timing,
931 );
932 match ans {
933 Ok(ans) => {
934 let status = match ans.decision() {
935 Decision::Allow => {
936 println!("ALLOW");
937 CedarExitCode::Success
938 }
939 Decision::Deny => {
940 println!("DENY");
941 CedarExitCode::AuthorizeDeny
942 }
943 };
944 if ans.diagnostics().errors().peekable().peek().is_some() {
945 println!();
946 for err in ans.diagnostics().errors() {
947 println!("{err}");
948 }
949 }
950 if args.verbose {
951 println!();
952 if ans.diagnostics().reason().peekable().peek().is_none() {
953 println!("note: no policies applied to this request");
954 } else {
955 println!("note: this decision was due to the following policies:");
956 for reason in ans.diagnostics().reason() {
957 println!(" {}", reason);
958 }
959 println!();
960 }
961 }
962 status
963 }
964 Err(errs) => {
965 for err in errs {
966 println!("{err:?}");
967 }
968 CedarExitCode::Failure
969 }
970 }
971}
972
973fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
975 match std::fs::OpenOptions::new()
976 .read(true)
977 .open(entities_filename.as_ref())
978 {
979 Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
980 format!(
981 "failed to parse entities from file {}",
982 entities_filename.as_ref().display()
983 )
984 }),
985 Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
986 format!(
987 "failed to open entities file {}",
988 entities_filename.as_ref().display()
989 )
990 }),
991 }
992}
993
994fn rename_from_id_annotation(ps: PolicySet) -> Result<PolicySet> {
1001 let mut new_ps = PolicySet::new();
1002 let t_iter = ps.templates().map(|t| match t.annotation("id") {
1003 None => Ok(t.clone()),
1004 Some(anno) => anno.parse().map(|a| t.new_id(a)),
1005 });
1006 for t in t_iter {
1007 let template = t.wrap_err("failed to parse policy id annotation")?;
1008 new_ps
1009 .add_template(template)
1010 .wrap_err("failed to add template to policy set")?;
1011 }
1012 let p_iter = ps.policies().map(|p| match p.annotation("id") {
1013 None => Ok(p.clone()),
1014 Some(anno) => anno.parse().map(|a| p.new_id(a)),
1015 });
1016 for p in p_iter {
1017 let policy = p.wrap_err("failed to parse policy id annotation")?;
1018 new_ps
1019 .add(policy)
1020 .wrap_err("failed to add template to policy set")?;
1021 }
1022 Ok(new_ps)
1023}
1024
1025fn read_from_file_or_stdin(filename: Option<impl AsRef<Path>>, context: &str) -> Result<String> {
1027 let mut src_str = String::new();
1028 match filename.as_ref() {
1029 Some(path) => {
1030 src_str = std::fs::read_to_string(path)
1031 .into_diagnostic()
1032 .wrap_err_with(|| {
1033 format!("failed to open {context} file {}", path.as_ref().display())
1034 })?;
1035 }
1036 None => {
1037 std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
1038 .into_diagnostic()
1039 .wrap_err_with(|| format!("failed to read {} from stdin", context))?;
1040 }
1041 };
1042 Ok(src_str)
1043}
1044
1045fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
1047 read_from_file_or_stdin(Some(filename), context)
1048}
1049
1050fn read_policy_set(filename: Option<impl AsRef<Path> + std::marker::Copy>) -> Result<PolicySet> {
1053 let context = "policy set";
1054 let ps_str = read_from_file_or_stdin(filename, context)?;
1055 let ps = PolicySet::from_str(&ps_str)
1056 .map_err(|err| {
1057 let name = filename.map_or_else(
1058 || "<stdin>".to_owned(),
1059 |n| n.as_ref().display().to_string(),
1060 );
1061 Report::new(err).with_source_code(NamedSource::new(name, ps_str))
1062 })
1063 .wrap_err_with(|| format!("failed to parse {context}"))?;
1064 rename_from_id_annotation(ps)
1065}
1066
1067fn read_json_policy(filename: Option<impl AsRef<Path> + std::marker::Copy>) -> Result<PolicySet> {
1070 let context = "JSON policy";
1071 let json_source = read_from_file_or_stdin(filename, context)?;
1072 let json: serde_json::Value = serde_json::from_str(&json_source).into_diagnostic()?;
1073 let err_to_report = |err| {
1074 let name = filename.map_or_else(
1075 || "<stdin>".to_owned(),
1076 |n| n.as_ref().display().to_string(),
1077 );
1078 Report::new(err).with_source_code(NamedSource::new(name, json_source.clone()))
1079 };
1080
1081 match Policy::from_json(None, json.clone()).map_err(err_to_report) {
1082 Ok(policy) => PolicySet::from_policies([policy])
1083 .wrap_err_with(|| format!("failed to create policy set from {context}")),
1084 Err(_) => match Template::from_json(None, json).map_err(err_to_report) {
1085 Ok(template) => {
1086 let mut ps = PolicySet::new();
1087 ps.add_template(template)?;
1088 Ok(ps)
1089 }
1090 Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
1091 },
1092 }
1093}
1094
1095fn read_schema_file(
1096 filename: impl AsRef<Path> + std::marker::Copy,
1097 format: SchemaFormat,
1098) -> Result<Schema> {
1099 let schema_src = read_from_file(filename, "schema")?;
1100 match format {
1101 SchemaFormat::Json => Schema::from_str(&schema_src).wrap_err_with(|| {
1102 format!(
1103 "failed to parse schema from file {}",
1104 filename.as_ref().display()
1105 )
1106 }),
1107 SchemaFormat::Human => {
1108 let (schema, warnings) = Schema::from_str_natural(&schema_src)?;
1109 for warning in warnings {
1110 let report = miette::Report::new(warning);
1111 eprintln!("{:?}", report);
1112 }
1113 Ok(schema)
1114 }
1115 }
1116}
1117
1118fn execute_request(
1120 request: &RequestArgs,
1121 policies: &PoliciesArgs,
1122 entities_filename: impl AsRef<Path>,
1123 schema_filename: Option<impl AsRef<Path> + std::marker::Copy>,
1124 schema_format: SchemaFormat,
1125 compute_duration: bool,
1126) -> Result<Response, Vec<Report>> {
1127 let mut errs = vec![];
1128 let policies = match policies.get_policy_set() {
1129 Ok(pset) => pset,
1130 Err(e) => {
1131 errs.push(e);
1132 PolicySet::new()
1133 }
1134 };
1135 let schema = match schema_filename.map(|f| read_schema_file(f, schema_format)) {
1136 None => None,
1137 Some(Ok(schema)) => Some(schema),
1138 Some(Err(e)) => {
1139 errs.push(e);
1140 None
1141 }
1142 };
1143 let entities = match load_entities(entities_filename, schema.as_ref()) {
1144 Ok(entities) => entities,
1145 Err(e) => {
1146 errs.push(e);
1147 Entities::empty()
1148 }
1149 };
1150 match request.get_request(schema.as_ref()) {
1151 Ok(request) if errs.is_empty() => {
1152 let authorizer = Authorizer::new();
1153 let auth_start = Instant::now();
1154 let ans = authorizer.is_authorized(&request, &policies, &entities);
1155 let auth_dur = auth_start.elapsed();
1156 if compute_duration {
1157 println!(
1158 "Authorization Time (micro seconds) : {}",
1159 auth_dur.as_micros()
1160 );
1161 }
1162 Ok(ans)
1163 }
1164 Ok(_) => Err(errs),
1165 Err(e) => {
1166 errs.push(e.wrap_err("failed to parse request"));
1167 Err(errs)
1168 }
1169 }
1170}