1#![allow(clippy::needless_return)]
21
22use anyhow::{Context as _, Error, Result};
23use cedar_policy::*;
24use cedar_policy_formatter::{policies_str_to_pretty, Config};
25use clap::{Args, Parser, Subcommand};
26use serde::{Deserialize, Serialize};
27use std::{
28 collections::HashMap,
29 fs::OpenOptions,
30 path::Path,
31 process::{ExitCode, Termination},
32 str::FromStr,
33 time::Instant,
34};
35
36#[derive(Parser)]
38#[command(author, version, about, long_about = None)] pub struct Cli {
40 #[command(subcommand)]
41 pub command: Commands,
42}
43
44#[derive(Subcommand, Debug)]
45pub enum Commands {
46 Authorize(AuthorizeArgs),
48 Evaluate(EvaluateArgs),
50 Validate(ValidateArgs),
52 CheckParse(CheckParseArgs),
54 Link(LinkArgs),
56 Format(FormatArgs),
58}
59
60#[derive(Args, Debug)]
61pub struct ValidateArgs {
62 #[arg(short, long = "schema", value_name = "FILE")]
64 pub schema_file: String,
65 #[arg(short, long = "policies", value_name = "FILE")]
67 pub policies_file: String,
68}
69
70#[derive(Args, Debug)]
71pub struct CheckParseArgs {
72 #[clap(short, long = "policies", value_name = "FILE")]
74 pub policies_file: String,
75}
76
77#[derive(Args, Debug)]
79pub struct RequestArgs {
80 #[arg(short, long)]
82 pub principal: Option<String>,
83 #[arg(short, long)]
85 pub action: Option<String>,
86 #[arg(short, long)]
88 pub resource: Option<String>,
89 #[arg(short, long = "context", value_name = "FILE")]
92 pub context_json_file: Option<String>,
93 #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
98 pub request_json_file: Option<String>,
99}
100
101impl RequestArgs {
102 fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
104 match &self.request_json_file {
105 Some(jsonfile) => {
106 let jsonstring = std::fs::read_to_string(jsonfile)
107 .context(format!("failed to open request-json file {jsonfile}"))?;
108 let qjson: RequestJSON = serde_json::from_str(&jsonstring)
109 .context(format!("failed to parse request-json file {jsonfile}"))?;
110 let principal = qjson
111 .principal
112 .map(|s| {
113 s.parse().context(format!(
114 "failed to parse principal in {jsonfile} as entity Uid"
115 ))
116 })
117 .transpose()?;
118 let action = qjson
119 .action
120 .map(|s| {
121 s.parse().context(format!(
122 "failed to parse action in {jsonfile} as entity Uid"
123 ))
124 })
125 .transpose()?;
126 let resource = qjson
127 .resource
128 .map(|s| {
129 s.parse().context(format!(
130 "failed to parse resource in {jsonfile} as entity Uid"
131 ))
132 })
133 .transpose()?;
134 let context = Context::from_json_value(
135 qjson.context,
136 schema.and_then(|s| Some((s, action.as_ref()?))),
137 )?;
138 Ok(Request::new(principal, action, resource, context))
139 }
140 None => {
141 let principal = self
142 .principal
143 .as_ref()
144 .map(|s| {
145 s.parse()
146 .context(format!("failed to parse principal {s} as entity Uid"))
147 })
148 .transpose()?;
149 let action = self
150 .action
151 .as_ref()
152 .map(|s| {
153 s.parse()
154 .context(format!("failed to parse action {s} as entity Uid"))
155 })
156 .transpose()?;
157 let resource = self
158 .resource
159 .as_ref()
160 .map(|s| {
161 s.parse()
162 .context(format!("failed to parse resource {s} as entity Uid"))
163 })
164 .transpose()?;
165 let context: Context = match &self.context_json_file {
166 None => Context::empty(),
167 Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
168 Ok(f) => Context::from_json_file(
169 f,
170 schema.and_then(|s| Some((s, action.as_ref()?))),
171 )?,
172 Err(e) => Err(Error::from(e)
173 .context(format!("error while loading context from {jsonfile}")))?,
174 },
175 };
176 Ok(Request::new(principal, action, resource, context))
177 }
178 }
179 }
180}
181
182#[derive(Args, Debug)]
183pub struct AuthorizeArgs {
184 #[command(flatten)]
186 pub request: RequestArgs,
187 #[arg(long = "policies", value_name = "FILE")]
189 pub policies_file: String,
190 #[arg(long = "template-linked", value_name = "FILE")]
192 pub template_linked_file: Option<String>,
193 #[arg(long = "schema", value_name = "FILE")]
197 pub schema_file: Option<String>,
198 #[arg(long = "entities", value_name = "FILE")]
200 pub entities_file: String,
201 #[arg(short, long)]
203 pub verbose: bool,
204 #[arg(short, long)]
206 pub timing: bool,
207}
208
209#[derive(Args, Debug)]
210pub struct LinkArgs {
211 #[arg(short, long)]
213 pub policies_file: String,
214 #[arg(short, long)]
216 pub template_linked_file: String,
217 #[arg(long)]
219 pub template_id: String,
220 #[arg(short, long)]
222 pub new_id: String,
223 #[arg(short, long)]
225 pub arguments: Arguments,
226}
227
228#[derive(Args, Debug)]
229pub struct FormatArgs {
230 #[arg(value_name = "FILE")]
232 pub file_name: Option<String>,
233
234 #[arg(short, long, value_name = "UINT", default_value_t = 80)]
236 pub line_width: usize,
237
238 #[arg(short, long, value_name = "INT", default_value_t = 2)]
240 pub indent_width: isize,
241}
242
243#[derive(Clone, Debug, Deserialize)]
245#[serde(try_from = "HashMap<String,String>")]
246pub struct Arguments {
247 pub data: HashMap<SlotId, String>,
248}
249
250impl TryFrom<HashMap<String, String>> for Arguments {
251 type Error = String;
252
253 fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
254 Ok(Self {
255 data: value
256 .into_iter()
257 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
258 .collect::<Result<HashMap<SlotId, String>, String>>()?,
259 })
260 }
261}
262
263impl FromStr for Arguments {
264 type Err = serde_json::Error;
265
266 fn from_str(s: &str) -> Result<Self, Self::Err> {
267 serde_json::from_str(s)
268 }
269}
270
271#[derive(Deserialize)]
273struct RequestJSON {
274 #[serde(default)]
276 principal: Option<String>,
277 #[serde(default)]
279 action: Option<String>,
280 #[serde(default)]
282 resource: Option<String>,
283 context: serde_json::Value,
285}
286
287#[derive(Args, Debug)]
288pub struct EvaluateArgs {
289 #[command(flatten)]
291 pub request: RequestArgs,
292 #[arg(long = "schema", value_name = "FILE")]
296 pub schema_file: Option<String>,
297 #[arg(long = "entities", value_name = "FILE")]
300 pub entities_file: Option<String>,
301 #[arg(value_name = "EXPRESSION")]
303 pub expression: String,
304}
305
306#[derive(Eq, PartialEq, Debug)]
307pub enum CedarExitCode {
308 Success,
311 Failure,
313 AuthorizeDeny,
316 ValidationFailure,
319}
320
321impl Termination for CedarExitCode {
322 fn report(self) -> ExitCode {
323 match self {
324 CedarExitCode::Success => ExitCode::SUCCESS,
325 CedarExitCode::Failure => ExitCode::FAILURE,
326 CedarExitCode::AuthorizeDeny => ExitCode::from(2),
327 CedarExitCode::ValidationFailure => ExitCode::from(3),
328 }
329 }
330}
331
332pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
333 match read_policy_file(&args.policies_file) {
334 Ok(_) => CedarExitCode::Success,
335 Err(e) => {
336 println!("{:#}", e);
337 CedarExitCode::Failure
338 }
339 }
340}
341
342pub fn validate(args: &ValidateArgs) -> CedarExitCode {
343 let pset = match read_policy_file(&args.policies_file) {
344 Ok(pset) => pset,
345 Err(e) => {
346 println!("{:#}", e);
347 return CedarExitCode::Failure;
348 }
349 };
350
351 let schema = match read_schema_file(&args.schema_file) {
352 Ok(schema) => schema,
353 Err(e) => {
354 println!("{:#}", e);
355 return CedarExitCode::Failure;
356 }
357 };
358
359 let validator = Validator::new(schema);
360 let result = validator.validate(&pset, ValidationMode::default());
361 if result.validation_passed() {
362 println!("Validation Passed");
363 return CedarExitCode::Success;
364 } else {
365 println!("Validation Results:");
366 for note in result.validation_errors() {
367 println!("{}", note);
368 }
369 return CedarExitCode::ValidationFailure;
370 }
371}
372
373pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
374 println!();
375 let schema = match args.schema_file.as_ref().map(read_schema_file) {
376 None => None,
377 Some(Ok(schema)) => Some(schema),
378 Some(Err(e)) => {
379 println!("{:#}", e);
380 return (CedarExitCode::Failure, EvalResult::Bool(false));
381 }
382 };
383 let request = match args.request.get_request(schema.as_ref()) {
384 Ok(q) => q,
385 Err(e) => {
386 println!("error: {:#}", e);
387 return (CedarExitCode::Failure, EvalResult::Bool(false));
388 }
389 };
390 let expr = match Expression::from_str(&args.expression) {
391 Ok(expr) => expr,
392 Err(e) => {
393 println!("error while parsing the expression: {e}");
394 return (CedarExitCode::Failure, EvalResult::Bool(false));
395 }
396 };
397 let entities = match &args.entities_file {
398 None => Entities::empty(),
399 Some(file) => match load_entities(file, schema.as_ref()) {
400 Ok(entities) => entities,
401 Err(e) => {
402 println!("error: {:#}", e);
403 return (CedarExitCode::Failure, EvalResult::Bool(false));
404 }
405 },
406 };
407 let entities = match load_actions_from_schema(entities, &schema) {
408 Ok(entities) => entities,
409 Err(e) => {
410 println!("error: {:#}", e);
411 return (CedarExitCode::Failure, EvalResult::Bool(false));
412 }
413 };
414 match eval_expression(&request, &entities, &expr) {
415 Err(e) => {
416 println!("error while evaluating the expression: {e}");
417 return (CedarExitCode::Failure, EvalResult::Bool(false));
418 }
419 Ok(result) => {
420 println!("{result}");
421 return (CedarExitCode::Success, result);
422 }
423 }
424}
425
426pub fn link(args: &LinkArgs) -> CedarExitCode {
427 if let Err(msg) = link_inner(args) {
428 eprintln!("{:#}", msg);
429 CedarExitCode::Failure
430 } else {
431 CedarExitCode::Success
432 }
433}
434
435fn format_policies_inner(args: &FormatArgs) -> Result<()> {
436 let mut policies_str = String::new();
437 match &args.file_name {
438 Some(path) => {
439 policies_str = std::fs::read_to_string(path)?;
440 }
441 None => {
442 std::io::Read::read_to_string(&mut std::io::stdin(), &mut policies_str)?;
443 }
444 };
445 let config = Config {
446 line_width: args.line_width,
447 indent_width: args.indent_width,
448 };
449 println!("{}", policies_str_to_pretty(&policies_str, &config)?);
450 Ok(())
451}
452
453pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
454 if let Err(msg) = format_policies_inner(args) {
455 eprintln!("{:#}", msg);
456 CedarExitCode::Failure
457 } else {
458 CedarExitCode::Success
459 }
460}
461
462fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
463 data.iter()
464 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
465 .collect::<Result<HashMap<SlotId, EntityUid>>>()
466}
467
468fn link_inner(args: &LinkArgs) -> Result<()> {
469 let mut policies = read_policy_file(&args.policies_file)?;
470 let slotenv = create_slot_env(&args.arguments.data)?;
471 policies.link(
472 PolicyId::from_str(&args.template_id)?,
473 PolicyId::from_str(&args.new_id)?,
474 slotenv,
475 )?;
476 let linked = policies
477 .policy(&PolicyId::from_str(&args.new_id)?)
478 .context("Failed to add template-linked policy")?;
479 println!("Template Linked Policy Added: {linked}");
480 let linked = TemplateLinked {
481 template_id: args.template_id.clone(),
482 link_id: args.new_id.clone(),
483 args: args.arguments.data.clone(),
484 };
485
486 update_template_linked_file(&args.template_linked_file, linked)
487}
488
489#[derive(Clone, Serialize, Deserialize, Debug)]
490#[serde(try_from = "LiteralTemplateLinked")]
491#[serde(into = "LiteralTemplateLinked")]
492struct TemplateLinked {
493 template_id: String,
494 link_id: String,
495 args: HashMap<SlotId, String>,
496}
497
498impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
499 type Error = String;
500
501 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
502 Ok(Self {
503 template_id: value.template_id,
504 link_id: value.link_id,
505 args: value
506 .args
507 .into_iter()
508 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
509 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
510 })
511 }
512}
513
514fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
515 match s.as_ref() {
516 "?principal" => Ok(SlotId::principal()),
517 "?resource" => Ok(SlotId::resource()),
518 _ => Err(format!(
519 "Invalid SlotId! Expected ?principal|?resource, got: {}",
520 s.as_ref()
521 )),
522 }
523}
524
525#[derive(Serialize, Deserialize)]
526struct LiteralTemplateLinked {
527 template_id: String,
528 link_id: String,
529 args: HashMap<String, String>,
530}
531
532impl From<TemplateLinked> for LiteralTemplateLinked {
533 fn from(i: TemplateLinked) -> Self {
534 Self {
535 template_id: i.template_id,
536 link_id: i.link_id,
537 args: i
538 .args
539 .into_iter()
540 .map(|(k, v)| (format!("{k}"), v))
541 .collect(),
542 }
543 }
544}
545
546fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
548 for template_linked in load_liked_file(path)? {
549 let slot_env = create_slot_env(&template_linked.args)?;
550 policy_set.link(
551 PolicyId::from_str(&template_linked.template_id)?,
552 PolicyId::from_str(&template_linked.link_id)?,
553 slot_env,
554 )?;
555 }
556 Ok(())
557}
558
559fn load_liked_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
561 let f = match std::fs::File::open(path) {
562 Ok(f) => f,
563 Err(_) => {
564 return Ok(vec![]);
566 }
567 };
568 if f.metadata().context("Failed to read metadata")?.len() == 0 {
569 Ok(vec![])
571 } else {
572 serde_json::from_reader(f).context("Deserialization error")
574 }
575}
576
577fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
579 let mut template_linked = load_liked_file(path.as_ref())?;
580 template_linked.push(new_linked);
581 write_template_linked_file(&template_linked, path.as_ref())
582}
583
584fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
586 let f = OpenOptions::new()
587 .write(true)
588 .truncate(true)
589 .create(true)
590 .open(path)?;
591 Ok(serde_json::to_writer(f, linked)?)
592}
593
594pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
595 println!();
596 let ans = execute_request(
597 &args.request,
598 &args.policies_file,
599 args.template_linked_file.as_ref(),
600 &args.entities_file,
601 args.schema_file.as_ref(),
602 args.timing,
603 );
604 match ans {
605 Ok(ans) => {
606 let status = match ans.decision() {
607 Decision::Allow => {
608 println!("ALLOW");
609 CedarExitCode::Success
610 }
611 Decision::Deny => {
612 println!("DENY");
613 CedarExitCode::AuthorizeDeny
614 }
615 };
616 if ans.diagnostics().errors().peekable().peek().is_some() {
617 println!();
618 for err in ans.diagnostics().errors() {
619 println!("{}", err);
620 }
621 }
622 if args.verbose {
623 println!();
624 if ans.diagnostics().reason().peekable().peek().is_none() {
625 println!("note: no policies applied to this request");
626 } else {
627 println!("note: this decision was due to the following policies:");
628 for reason in ans.diagnostics().reason() {
629 println!(" {}", reason);
630 }
631 println!();
632 }
633 }
634 status
635 }
636 Err(errs) => {
637 for err in errs {
638 println!("{:#}", err);
639 }
640 CedarExitCode::Failure
641 }
642 }
643}
644
645fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
647 match std::fs::OpenOptions::new()
648 .read(true)
649 .open(entities_filename.as_ref())
650 {
651 Ok(f) => Entities::from_json_file(f, schema).context(format!(
652 "failed to parse entities from file {}",
653 entities_filename.as_ref().display()
654 )),
655 Err(e) => Err(e).context(format!(
656 "failed to open entities file {}",
657 entities_filename.as_ref().display()
658 )),
659 }
660}
661
662fn rename_from_id_annotation(ps: PolicySet) -> PolicySet {
669 let mut new_ps = PolicySet::new();
670 let t_iter = ps.templates().map(|t| match t.annotation("id") {
671 None => t.clone(),
672 Some(anno) => t.new_id(anno.parse().expect("id annotation should be valid id")),
673 });
674 for t in t_iter {
675 new_ps.add_template(t).expect("should still be a template");
676 }
677 let p_iter = ps.policies().map(|p| match p.annotation("id") {
678 None => p.clone(),
679 Some(anno) => p.new_id(anno.parse().expect("id annotation should be valid id")),
680 });
681 for p in p_iter {
682 new_ps.add(p).expect("should still be a policy");
683 }
684 new_ps
685}
686
687fn read_policy_and_links(
688 policies_filename: impl AsRef<Path>,
689 links_filename: Option<impl AsRef<Path>>,
690) -> Result<PolicySet> {
691 let mut pset = read_policy_file(policies_filename.as_ref())?;
692 if let Some(links_filename) = links_filename {
693 add_template_links_to_set(links_filename.as_ref(), &mut pset)?;
694 }
695 Ok(pset)
696}
697
698fn read_policy_file(filename: impl AsRef<Path>) -> Result<PolicySet> {
699 let src = std::fs::read_to_string(filename.as_ref()).context(format!(
700 "failed to open policy file {}",
701 filename.as_ref().display()
702 ))?;
703 let ps = PolicySet::from_str(&src).context(format!(
704 "failed to parse policies from file {}",
705 filename.as_ref().display()
706 ))?;
707 Ok(rename_from_id_annotation(ps))
708}
709
710fn read_schema_file(filename: impl AsRef<Path>) -> Result<Schema> {
711 let schema_src = std::fs::read_to_string(filename.as_ref()).context(format!(
712 "failed to open schema file {}",
713 filename.as_ref().display()
714 ))?;
715 Schema::from_str(&schema_src).context(format!(
716 "failed to parse schema from file {}",
717 filename.as_ref().display()
718 ))
719}
720
721fn load_actions_from_schema(entities: Entities, schema: &Option<Schema>) -> Result<Entities> {
722 match schema {
723 Some(schema) => match schema.action_entities() {
724 Ok(action_entities) => Entities::from_entities(
725 entities
726 .iter()
727 .cloned()
728 .chain(action_entities.iter().cloned()),
729 )
730 .context("failed to merge action entities with entity file"),
731 Err(e) => Err(e).context("failed to construct action entities"),
732 },
733 None => Ok(entities),
734 }
735}
736
737fn execute_request(
739 request: &RequestArgs,
740 policies_filename: impl AsRef<Path>,
741 links_filename: Option<impl AsRef<Path>>,
742 entities_filename: impl AsRef<Path>,
743 schema_filename: Option<impl AsRef<Path>>,
744 compute_duration: bool,
745) -> Result<Response, Vec<Error>> {
746 let mut errs = vec![];
747 let policies = match read_policy_and_links(policies_filename.as_ref(), links_filename) {
748 Ok(pset) => pset,
749 Err(e) => {
750 errs.push(e);
751 PolicySet::new()
752 }
753 };
754 let schema = match schema_filename.map(read_schema_file) {
755 None => None,
756 Some(Ok(schema)) => Some(schema),
757 Some(Err(e)) => {
758 errs.push(e);
759 None
760 }
761 };
762 let entities = match load_entities(entities_filename, schema.as_ref()) {
763 Ok(entities) => entities,
764 Err(e) => {
765 errs.push(e);
766 Entities::empty()
767 }
768 };
769 let entities = match load_actions_from_schema(entities, &schema) {
770 Ok(entities) => entities,
771 Err(e) => {
772 errs.push(e);
773 Entities::empty()
774 }
775 };
776 let request = match request.get_request(schema.as_ref()) {
777 Ok(q) => Some(q),
778 Err(e) => {
779 errs.push(e.context("failed to parse request"));
780 None
781 }
782 };
783 if errs.is_empty() {
784 let request = request.expect("if errs is empty, we should have a request");
785 let authorizer = Authorizer::new();
786 let auth_start = Instant::now();
787 let ans = authorizer.is_authorized(&request, &policies, &entities);
788 let auth_dur = auth_start.elapsed();
789 if compute_duration {
790 println!(
791 "Authorization Time (micro seconds) : {}",
792 auth_dur.as_micros()
793 );
794 }
795 Ok(ans)
796 } else {
797 Err(errs)
798 }
799}