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