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")]
196 pub schema_file: Option<String>,
197 #[arg(long = "entities", value_name = "FILE")]
199 pub entities_file: String,
200 #[arg(short, long)]
202 pub verbose: bool,
203 #[arg(short, long)]
205 pub timing: bool,
206}
207
208#[derive(Args, Debug)]
209pub struct LinkArgs {
210 #[arg(short, long)]
212 pub policies_file: String,
213 #[arg(short, long)]
215 pub template_linked_file: String,
216 #[arg(long)]
218 pub template_id: String,
219 #[arg(short, long)]
221 pub new_id: String,
222 #[arg(short, long)]
224 pub arguments: Arguments,
225}
226
227#[derive(Args, Debug)]
228pub struct FormatArgs {
229 #[arg(value_name = "FILE")]
231 pub file_name: Option<String>,
232
233 #[arg(short, long, value_name = "UINT", default_value_t = 80)]
235 pub line_width: usize,
236
237 #[arg(short, long, value_name = "INT", default_value_t = 2)]
239 pub indent_width: isize,
240}
241
242#[derive(Clone, Debug, Deserialize)]
244#[serde(try_from = "HashMap<String,String>")]
245pub struct Arguments {
246 pub data: HashMap<SlotId, String>,
247}
248
249impl TryFrom<HashMap<String, String>> for Arguments {
250 type Error = String;
251
252 fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
253 Ok(Self {
254 data: value
255 .into_iter()
256 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
257 .collect::<Result<HashMap<SlotId, String>, String>>()?,
258 })
259 }
260}
261
262impl FromStr for Arguments {
263 type Err = serde_json::Error;
264
265 fn from_str(s: &str) -> Result<Self, Self::Err> {
266 serde_json::from_str(s)
267 }
268}
269
270#[derive(Deserialize)]
272struct RequestJSON {
273 #[serde(default)]
275 principal: Option<String>,
276 #[serde(default)]
278 action: Option<String>,
279 #[serde(default)]
281 resource: Option<String>,
282 context: serde_json::Value,
284}
285
286#[derive(Args, Debug)]
287pub struct EvaluateArgs {
288 #[command(flatten)]
290 pub request: RequestArgs,
291 #[arg(long = "schema", value_name = "FILE")]
294 pub schema_file: Option<String>,
295 #[arg(long = "entities", value_name = "FILE")]
298 pub entities_file: Option<String>,
299 #[arg(value_name = "EXPRESSION")]
301 pub expression: String,
302}
303
304#[derive(Eq, PartialEq, Debug)]
305pub enum CedarExitCode {
306 Success,
309 Failure,
311 AuthorizeDeny,
314 ValidationFailure,
317}
318
319impl Termination for CedarExitCode {
320 fn report(self) -> ExitCode {
321 match self {
322 CedarExitCode::Success => ExitCode::SUCCESS,
323 CedarExitCode::Failure => ExitCode::FAILURE,
324 CedarExitCode::AuthorizeDeny => ExitCode::from(2),
325 CedarExitCode::ValidationFailure => ExitCode::from(3),
326 }
327 }
328}
329
330pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
331 match read_policy_file(&args.policies_file) {
332 Ok(_) => CedarExitCode::Success,
333 Err(e) => {
334 println!("{:#}", e);
335 CedarExitCode::Failure
336 }
337 }
338}
339
340pub fn validate(args: &ValidateArgs) -> CedarExitCode {
341 let pset = match read_policy_file(&args.policies_file) {
342 Ok(pset) => pset,
343 Err(e) => {
344 println!("{:#}", e);
345 return CedarExitCode::Failure;
346 }
347 };
348
349 let schema = match read_schema_file(&args.schema_file) {
350 Ok(schema) => schema,
351 Err(e) => {
352 println!("{:#}", e);
353 return CedarExitCode::Failure;
354 }
355 };
356
357 let validator = Validator::new(schema);
358 let result = validator.validate(&pset, ValidationMode::default());
359 if result.validation_passed() {
360 println!("Validation Passed");
361 return CedarExitCode::Success;
362 } else {
363 println!("Validation Results:");
364 for note in result.validation_errors() {
365 println!("{}", note);
366 }
367 return CedarExitCode::ValidationFailure;
368 }
369}
370
371pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
372 println!();
373 let schema = match args.schema_file.as_ref().map(read_schema_file) {
374 None => None,
375 Some(Ok(schema)) => Some(schema),
376 Some(Err(e)) => {
377 println!("{:#}", e);
378 return (CedarExitCode::Failure, EvalResult::Bool(false));
379 }
380 };
381 let request = match args.request.get_request(schema.as_ref()) {
382 Ok(q) => q,
383 Err(e) => {
384 println!("error: {:#}", e);
385 return (CedarExitCode::Failure, EvalResult::Bool(false));
386 }
387 };
388 let expr = match Expression::from_str(&args.expression) {
389 Ok(expr) => expr,
390 Err(e) => {
391 println!("error while parsing the expression: {e}");
392 return (CedarExitCode::Failure, EvalResult::Bool(false));
393 }
394 };
395 let entities = match &args.entities_file {
396 None => Entities::empty(),
397 Some(file) => match load_entities(file, schema.as_ref()) {
398 Ok(entities) => entities,
399 Err(e) => {
400 println!("error: {:#}", e);
401 return (CedarExitCode::Failure, EvalResult::Bool(false));
402 }
403 },
404 };
405 match eval_expression(&request, &entities, &expr) {
406 Err(e) => {
407 println!("error while evaluating the expression: {e}");
408 return (CedarExitCode::Failure, EvalResult::Bool(false));
409 }
410 Ok(result) => {
411 println!("{result}");
412 return (CedarExitCode::Success, result);
413 }
414 }
415}
416
417pub fn link(args: &LinkArgs) -> CedarExitCode {
418 if let Err(msg) = link_inner(args) {
419 eprintln!("{:#}", msg);
420 CedarExitCode::Failure
421 } else {
422 CedarExitCode::Success
423 }
424}
425
426fn format_policies_inner(args: &FormatArgs) -> Result<()> {
427 let mut policies_str = String::new();
428 match &args.file_name {
429 Some(path) => {
430 policies_str = std::fs::read_to_string(path)?;
431 }
432 None => {
433 std::io::Read::read_to_string(&mut std::io::stdin(), &mut policies_str)?;
434 }
435 };
436 let config = Config {
437 line_width: args.line_width,
438 indent_width: args.indent_width,
439 };
440 println!("{}", policies_str_to_pretty(&policies_str, &config)?);
441 Ok(())
442}
443
444pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
445 if let Err(msg) = format_policies_inner(args) {
446 eprintln!("{:#}", msg);
447 CedarExitCode::Failure
448 } else {
449 CedarExitCode::Success
450 }
451}
452
453fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
454 data.iter()
455 .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
456 .collect::<Result<HashMap<SlotId, EntityUid>>>()
457}
458
459fn link_inner(args: &LinkArgs) -> Result<()> {
460 let mut policies = read_policy_file(&args.policies_file)?;
461 let slotenv = create_slot_env(&args.arguments.data)?;
462 policies.link(
463 PolicyId::from_str(&args.template_id)?,
464 PolicyId::from_str(&args.new_id)?,
465 slotenv,
466 )?;
467 let linked = policies
468 .policy(&PolicyId::from_str(&args.new_id)?)
469 .context("Failed to add template-linked policy")?;
470 println!("Template Linked Policy Added: {linked}");
471 let linked = TemplateLinked {
472 template_id: args.template_id.clone(),
473 link_id: args.new_id.clone(),
474 args: args.arguments.data.clone(),
475 };
476
477 update_template_linked_file(&args.template_linked_file, linked)
478}
479
480#[derive(Clone, Serialize, Deserialize, Debug)]
481#[serde(try_from = "LiteralTemplateLinked")]
482#[serde(into = "LiteralTemplateLinked")]
483struct TemplateLinked {
484 template_id: String,
485 link_id: String,
486 args: HashMap<SlotId, String>,
487}
488
489impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
490 type Error = String;
491
492 fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
493 Ok(Self {
494 template_id: value.template_id,
495 link_id: value.link_id,
496 args: value
497 .args
498 .into_iter()
499 .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
500 .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
501 })
502 }
503}
504
505fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
506 match s.as_ref() {
507 "?principal" => Ok(SlotId::principal()),
508 "?resource" => Ok(SlotId::resource()),
509 _ => Err(format!(
510 "Invalid SlotId! Expected ?principal|?resource, got: {}",
511 s.as_ref()
512 )),
513 }
514}
515
516#[derive(Serialize, Deserialize)]
517struct LiteralTemplateLinked {
518 template_id: String,
519 link_id: String,
520 args: HashMap<String, String>,
521}
522
523impl From<TemplateLinked> for LiteralTemplateLinked {
524 fn from(i: TemplateLinked) -> Self {
525 Self {
526 template_id: i.template_id,
527 link_id: i.link_id,
528 args: i
529 .args
530 .into_iter()
531 .map(|(k, v)| (format!("{k}"), v))
532 .collect(),
533 }
534 }
535}
536
537fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
539 for template_linked in load_liked_file(path)? {
540 let slot_env = create_slot_env(&template_linked.args)?;
541 policy_set.link(
542 PolicyId::from_str(&template_linked.template_id)?,
543 PolicyId::from_str(&template_linked.link_id)?,
544 slot_env,
545 )?;
546 }
547 Ok(())
548}
549
550fn load_liked_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
552 let f = match std::fs::File::open(path) {
553 Ok(f) => f,
554 Err(_) => {
555 return Ok(vec![]);
557 }
558 };
559 if f.metadata().context("Failed to read metadata")?.len() == 0 {
560 Ok(vec![])
562 } else {
563 serde_json::from_reader(f).context("Deserialization error")
565 }
566}
567
568fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
570 let mut template_linked = load_liked_file(path.as_ref())?;
571 template_linked.push(new_linked);
572 write_template_linked_file(&template_linked, path.as_ref())
573}
574
575fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
577 let f = OpenOptions::new()
578 .write(true)
579 .truncate(true)
580 .create(true)
581 .open(path)?;
582 Ok(serde_json::to_writer(f, linked)?)
583}
584
585pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
586 println!();
587 let ans = execute_request(
588 &args.request,
589 &args.policies_file,
590 args.template_linked_file.as_ref(),
591 &args.entities_file,
592 args.schema_file.as_ref(),
593 args.timing,
594 );
595 match ans {
596 Ok(ans) => {
597 let status = match ans.decision() {
598 Decision::Allow => {
599 println!("ALLOW");
600 CedarExitCode::Success
601 }
602 Decision::Deny => {
603 println!("DENY");
604 CedarExitCode::AuthorizeDeny
605 }
606 };
607 if ans.diagnostics().errors().peekable().peek().is_some() {
608 println!();
609 for err in ans.diagnostics().errors() {
610 println!("{}", err);
611 }
612 }
613 if args.verbose {
614 println!();
615 if ans.diagnostics().reason().peekable().peek().is_none() {
616 println!("note: no policies applied to this request");
617 } else {
618 println!("note: this decision was due to the following policies:");
619 for reason in ans.diagnostics().reason() {
620 println!(" {}", reason);
621 }
622 println!();
623 }
624 }
625 status
626 }
627 Err(errs) => {
628 for err in errs {
629 println!("{:#}", err);
630 }
631 CedarExitCode::Failure
632 }
633 }
634}
635
636fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
638 match std::fs::OpenOptions::new()
639 .read(true)
640 .open(entities_filename.as_ref())
641 {
642 Ok(f) => Entities::from_json_file(f, schema).context(format!(
643 "failed to parse entities from file {}",
644 entities_filename.as_ref().display()
645 )),
646 Err(e) => Err(e).context(format!(
647 "failed to open entities file {}",
648 entities_filename.as_ref().display()
649 )),
650 }
651}
652
653fn rename_from_id_annotation(ps: PolicySet) -> PolicySet {
660 let mut new_ps = PolicySet::new();
661 let t_iter = ps.templates().map(|t| match t.annotation("id") {
662 None => t.clone(),
663 Some(anno) => t.new_id(anno.parse().expect("id annotation should be valid id")),
664 });
665 for t in t_iter {
666 new_ps.add_template(t).expect("should still be a template");
667 }
668 let p_iter = ps.policies().map(|p| match p.annotation("id") {
669 None => p.clone(),
670 Some(anno) => p.new_id(anno.parse().expect("id annotation should be valid id")),
671 });
672 for p in p_iter {
673 new_ps.add(p).expect("should still be a policy");
674 }
675 new_ps
676}
677
678fn read_policy_and_links(
679 policies_filename: impl AsRef<Path>,
680 links_filename: Option<impl AsRef<Path>>,
681) -> Result<PolicySet> {
682 let mut pset = read_policy_file(policies_filename.as_ref())?;
683 if let Some(links_filename) = links_filename {
684 add_template_links_to_set(links_filename.as_ref(), &mut pset)?;
685 }
686 Ok(pset)
687}
688
689fn read_policy_file(filename: impl AsRef<Path>) -> Result<PolicySet> {
690 let src = std::fs::read_to_string(filename.as_ref()).context(format!(
691 "failed to open policy file {}",
692 filename.as_ref().display()
693 ))?;
694 let ps = PolicySet::from_str(&src).context(format!(
695 "failed to parse policies from file {}",
696 filename.as_ref().display()
697 ))?;
698 Ok(rename_from_id_annotation(ps))
699}
700
701fn read_schema_file(filename: impl AsRef<Path>) -> Result<Schema> {
702 let schema_src = std::fs::read_to_string(filename.as_ref()).context(format!(
703 "failed to open schema file {}",
704 filename.as_ref().display()
705 ))?;
706 Schema::from_str(&schema_src).context(format!(
707 "failed to parse schema from file {}",
708 filename.as_ref().display()
709 ))
710}
711
712fn execute_request(
714 request: &RequestArgs,
715 policies_filename: impl AsRef<Path>,
716 links_filename: Option<impl AsRef<Path>>,
717 entities_filename: impl AsRef<Path>,
718 schema_filename: Option<impl AsRef<Path>>,
719 compute_duration: bool,
720) -> Result<Response, Vec<Error>> {
721 let mut errs = vec![];
722 let policies = match read_policy_and_links(policies_filename.as_ref(), links_filename) {
723 Ok(pset) => pset,
724 Err(e) => {
725 errs.push(e);
726 PolicySet::new()
727 }
728 };
729 let schema = match schema_filename.map(read_schema_file) {
730 None => None,
731 Some(Ok(schema)) => Some(schema),
732 Some(Err(e)) => {
733 errs.push(e);
734 None
735 }
736 };
737 let entities = match load_entities(entities_filename, schema.as_ref()) {
738 Ok(entities) => entities,
739 Err(e) => {
740 errs.push(e);
741 Entities::empty()
742 }
743 };
744 let request = match request.get_request(schema.as_ref()) {
745 Ok(q) => Some(q),
746 Err(e) => {
747 errs.push(e.context("failed to parse request"));
748 None
749 }
750 };
751 if errs.is_empty() {
752 let request = request.expect("if errs is empty, we should have a request");
753 let authorizer = Authorizer::new();
754 let auth_start = Instant::now();
755 let ans = authorizer.is_authorized(&request, &policies, &entities);
756 let auth_dur = auth_start.elapsed();
757 if compute_duration {
758 println!(
759 "Authorization Time (micro seconds) : {}",
760 auth_dur.as_micros()
761 );
762 }
763 Ok(ans)
764 } else {
765 Err(errs)
766 }
767}