cedar_policy_cli/
lib.rs

1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// This modules makes use of `return` to exit early with a particular exit code.
18// For consistency, it also uses `return` in some places where it could be
19// omitted.
20#![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/// Basic Cedar CLI for evaluating authorization queries
37#[derive(Parser)]
38#[command(author, version, about, long_about = None)] // Pull from `Cargo.toml`
39pub struct Cli {
40    #[command(subcommand)]
41    pub command: Commands,
42}
43
44#[derive(Subcommand, Debug)]
45pub enum Commands {
46    /// Evaluate an authorization request
47    Authorize(AuthorizeArgs),
48    /// Evaluate a Cedar expression
49    Evaluate(EvaluateArgs),
50    /// Validate a policy set against a schema
51    Validate(ValidateArgs),
52    /// Check that policies successfully parse
53    CheckParse(CheckParseArgs),
54    /// Link a template
55    Link(LinkArgs),
56    /// Format a policy set
57    Format(FormatArgs),
58}
59
60#[derive(Args, Debug)]
61pub struct ValidateArgs {
62    /// File containing the schema
63    #[arg(short, long = "schema", value_name = "FILE")]
64    pub schema_file: String,
65    /// File containing the policy set
66    #[arg(short, long = "policies", value_name = "FILE")]
67    pub policies_file: String,
68}
69
70#[derive(Args, Debug)]
71pub struct CheckParseArgs {
72    /// File containing the policy set
73    #[clap(short, long = "policies", value_name = "FILE")]
74    pub policies_file: String,
75}
76
77/// This struct contains the arguments that together specify a request.
78#[derive(Args, Debug)]
79pub struct RequestArgs {
80    /// Principal for the request, e.g., User::"alice"
81    #[arg(short, long)]
82    pub principal: Option<String>,
83    /// Action for the request, e.g., Action::"view"
84    #[arg(short, long)]
85    pub action: Option<String>,
86    /// Resource for the request, e.g., File::"myfile.txt"
87    #[arg(short, long)]
88    pub resource: Option<String>,
89    /// File containing a JSON object representing the context for the request.
90    /// Should be a (possibly empty) map from keys to values.
91    #[arg(short, long = "context", value_name = "FILE")]
92    pub context_json_file: Option<String>,
93    /// File containing a JSON object representing the entire request. Must have
94    /// fields "principal", "action", "resource", and "context", where "context"
95    /// is a (possibly empty) map from keys to values. This option replaces
96    /// --principal, --action, etc.
97    #[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    /// Turn this `RequestArgs` into the appropriate `Request` object
103    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    /// Request args (incorporated by reference)
185    #[command(flatten)]
186    pub request: RequestArgs,
187    /// File containing the static Cedar policies and templates to evaluate against
188    #[arg(long = "policies", value_name = "FILE")]
189    pub policies_file: String,
190    /// File containing template linked policies
191    #[arg(long = "template-linked", value_name = "FILE")]
192    pub template_linked_file: Option<String>,
193    /// File containing schema information
194    /// Used to populate the store with action entities and for schema-based
195    /// parsing of entity hierarchy, if present
196    #[arg(long = "schema", value_name = "FILE")]
197    pub schema_file: Option<String>,
198    /// File containing JSON representation of the Cedar entity hierarchy
199    #[arg(long = "entities", value_name = "FILE")]
200    pub entities_file: String,
201    /// More verbose output. (For instance, indicate which policies applied to the request, if any.)
202    #[arg(short, long)]
203    pub verbose: bool,
204    /// Time authorization and report timing information
205    #[arg(short, long)]
206    pub timing: bool,
207}
208
209#[derive(Args, Debug)]
210pub struct LinkArgs {
211    /// File containing static policies and templates.
212    #[arg(short, long)]
213    pub policies_file: String,
214    /// File containing template-linked policies
215    #[arg(short, long)]
216    pub template_linked_file: String,
217    /// Id of the template to instantiate
218    #[arg(long)]
219    pub template_id: String,
220    /// Id for the new template linked policy
221    #[arg(short, long)]
222    pub new_id: String,
223    /// Arguments to fill slots
224    #[arg(short, long)]
225    pub arguments: Arguments,
226}
227
228#[derive(Args, Debug)]
229pub struct FormatArgs {
230    /// Optional policy file name. If none is provided, read input from stdin.
231    #[arg(value_name = "FILE")]
232    pub file_name: Option<String>,
233
234    /// Custom line width (default: 80).
235    #[arg(short, long, value_name = "UINT", default_value_t = 80)]
236    pub line_width: usize,
237
238    /// Custom indentation width (default: 2).
239    #[arg(short, long, value_name = "INT", default_value_t = 2)]
240    pub indent_width: isize,
241}
242
243/// Wrapper struct
244#[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/// This struct is the serde structure expected for --request-json
272#[derive(Deserialize)]
273struct RequestJSON {
274    /// Principal for the request
275    #[serde(default)]
276    principal: Option<String>,
277    /// Action for the request
278    #[serde(default)]
279    action: Option<String>,
280    /// Resource for the request
281    #[serde(default)]
282    resource: Option<String>,
283    /// Context for the request
284    context: serde_json::Value,
285}
286
287#[derive(Args, Debug)]
288pub struct EvaluateArgs {
289    /// Request args (incorporated by reference)
290    #[command(flatten)]
291    pub request: RequestArgs,
292    /// File containing schema information
293    /// Used to populate the store with action entities and for schema-based
294    /// parsing of entity hierarchy, if present
295    #[arg(long = "schema", value_name = "FILE")]
296    pub schema_file: Option<String>,
297    /// File containing JSON representation of the Cedar entity hierarchy.
298    /// This is optional; if not present, we'll just use an empty hierarchy.
299    #[arg(long = "entities", value_name = "FILE")]
300    pub entities_file: Option<String>,
301    /// Expression to evaluate
302    #[arg(value_name = "EXPRESSION")]
303    pub expression: String,
304}
305
306#[derive(Eq, PartialEq, Debug)]
307pub enum CedarExitCode {
308    // The command completed successfully with a result other than a
309    // authorization deny or validation failure.
310    Success,
311    // The command failed to complete successfully.
312    Failure,
313    // The command completed successfully, but the result of the authorization
314    // request was DENY.
315    AuthorizeDeny,
316    // The command completed successfully, but it detected a validation failure
317    // in the given schema and policies.
318    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
546/// Iterate over links in the template-linked file and add them to the set
547fn 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
559/// Read template linked set to a Vec
560fn 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            // If the file doesn't exist, then give back the empty entity set
565            return Ok(vec![]);
566        }
567    };
568    if f.metadata().context("Failed to read metadata")?.len() == 0 {
569        // File is empty, return empty set
570        Ok(vec![])
571    } else {
572        // File has contents, deserialize
573        serde_json::from_reader(f).context("Deserialization error")
574    }
575}
576
577/// Add a single template-linked policy to the linked file
578fn 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
584/// Write a slice of template-linked policies to the linked file
585fn 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
645/// Load an `Entities` object from the given JSON filename and optional schema.
646fn 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
662/// Renames policies and templates based on (@id("new_id") annotation.
663/// If no such annotation exists, it keeps the current id.
664///
665/// This will rename template-linked policies to the id of their template, which may
666/// cause id conflicts, so only call this function before instancing
667/// templates into the policy set.
668fn 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
737/// This uses the Cedar API to call the authorization engine.
738fn 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}