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
22mod err;
23
24use clap::{Args, Parser, Subcommand, ValueEnum};
25use miette::{miette, NamedSource, Report, Result, WrapErr};
26use serde::{Deserialize, Serialize};
27use std::{
28    collections::HashMap,
29    fmt::{self, Display},
30    fs::OpenOptions,
31    path::Path,
32    process::{ExitCode, Termination},
33    str::FromStr,
34    time::Instant,
35};
36
37use cedar_policy::*;
38use cedar_policy_formatter::{policies_str_to_pretty, Config};
39
40use crate::err::IntoDiagnostic;
41
42/// Basic Cedar CLI for evaluating authorization queries
43#[derive(Parser)]
44#[command(author, version, about, long_about = None)] // Pull from `Cargo.toml`
45pub struct Cli {
46    #[command(subcommand)]
47    pub command: Commands,
48    /// The output format to use for error reporting.
49    #[arg(
50        global = true,
51        short = 'f',
52        long = "error-format",
53        env = "CEDAR_ERROR_FORMAT",
54        default_value_t,
55        value_enum
56    )]
57    pub err_fmt: ErrorFormat,
58}
59
60#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
61pub enum ErrorFormat {
62    /// Human-readable error messages with terminal graphics and inline code
63    /// snippets.
64    #[default]
65    Human,
66    /// Plain-text error messages without fancy graphics or colors, suitable for
67    /// screen readers.
68    Plain,
69    /// Machine-readable JSON output.
70    Json,
71}
72
73impl Display for ErrorFormat {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(
76            f,
77            "{}",
78            match self {
79                ErrorFormat::Human => "human",
80                ErrorFormat::Plain => "plain",
81                ErrorFormat::Json => "json",
82            }
83        )
84    }
85}
86
87#[derive(Subcommand, Debug)]
88pub enum Commands {
89    /// Evaluate an authorization request
90    Authorize(AuthorizeArgs),
91    /// Evaluate a Cedar expression
92    Evaluate(EvaluateArgs),
93    /// Validate a policy set against a schema
94    Validate(ValidateArgs),
95    /// Check that policies successfully parse
96    CheckParse(CheckParseArgs),
97    /// Link a template
98    Link(LinkArgs),
99    /// Format a policy set
100    Format(FormatArgs),
101}
102
103#[derive(Args, Debug)]
104pub struct ValidateArgs {
105    /// File containing the schema
106    #[arg(short, long = "schema", value_name = "FILE")]
107    pub schema_file: String,
108    /// File containing the policy set
109    #[arg(short, long = "policies", value_name = "FILE")]
110    pub policies_file: String,
111}
112
113#[derive(Args, Debug)]
114pub struct CheckParseArgs {
115    /// File containing the policy set
116    #[clap(short, long = "policies", value_name = "FILE")]
117    pub policies_file: Option<String>,
118}
119
120/// This struct contains the arguments that together specify a request.
121#[derive(Args, Debug)]
122pub struct RequestArgs {
123    /// Principal for the request, e.g., User::"alice"
124    #[arg(short, long)]
125    pub principal: Option<String>,
126    /// Action for the request, e.g., Action::"view"
127    #[arg(short, long)]
128    pub action: Option<String>,
129    /// Resource for the request, e.g., File::"myfile.txt"
130    #[arg(short, long)]
131    pub resource: Option<String>,
132    /// File containing a JSON object representing the context for the request.
133    /// Should be a (possibly empty) map from keys to values.
134    #[arg(short, long = "context", value_name = "FILE")]
135    pub context_json_file: Option<String>,
136    /// File containing a JSON object representing the entire request. Must have
137    /// fields "principal", "action", "resource", and "context", where "context"
138    /// is a (possibly empty) map from keys to values. This option replaces
139    /// --principal, --action, etc.
140    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
141    pub request_json_file: Option<String>,
142}
143
144impl RequestArgs {
145    /// Turn this `RequestArgs` into the appropriate `Request` object
146    fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
147        match &self.request_json_file {
148            Some(jsonfile) => {
149                let jsonstring = std::fs::read_to_string(jsonfile)
150                    .into_diagnostic()
151                    .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
152                let qjson: RequestJSON = serde_json::from_str(&jsonstring)
153                    .into_diagnostic()
154                    .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
155                let principal = qjson
156                    .principal
157                    .map(|s| {
158                        s.parse().wrap_err_with(|| {
159                            format!("failed to parse principal in {jsonfile} as entity Uid")
160                        })
161                    })
162                    .transpose()?;
163                let action = qjson
164                    .action
165                    .map(|s| {
166                        s.parse().wrap_err_with(|| {
167                            format!("failed to parse action in {jsonfile} as entity Uid")
168                        })
169                    })
170                    .transpose()?;
171                let resource = qjson
172                    .resource
173                    .map(|s| {
174                        s.parse().wrap_err_with(|| {
175                            format!("failed to parse resource in {jsonfile} as entity Uid")
176                        })
177                    })
178                    .transpose()?;
179                let context = Context::from_json_value(
180                    qjson.context,
181                    schema.and_then(|s| Some((s, action.as_ref()?))),
182                )
183                .into_diagnostic()
184                .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
185                Ok(Request::new(principal, action, resource, context))
186            }
187            None => {
188                let principal = self
189                    .principal
190                    .as_ref()
191                    .map(|s| {
192                        s.parse().wrap_err_with(|| {
193                            format!("failed to parse principal {s} as entity Uid")
194                        })
195                    })
196                    .transpose()?;
197                let action = self
198                    .action
199                    .as_ref()
200                    .map(|s| {
201                        s.parse()
202                            .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
203                    })
204                    .transpose()?;
205                let resource = self
206                    .resource
207                    .as_ref()
208                    .map(|s| {
209                        s.parse()
210                            .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
211                    })
212                    .transpose()?;
213                let context: Context = match &self.context_json_file {
214                    None => Context::empty(),
215                    Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
216                        Ok(f) => Context::from_json_file(
217                            f,
218                            schema.and_then(|s| Some((s, action.as_ref()?))),
219                        )
220                        .into_diagnostic()
221                        .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
222                        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
223                            format!("error while loading context from {jsonfile}")
224                        })?,
225                    },
226                };
227                Ok(Request::new(principal, action, resource, context))
228            }
229        }
230    }
231}
232
233#[derive(Args, Debug)]
234pub struct AuthorizeArgs {
235    /// Request args (incorporated by reference)
236    #[command(flatten)]
237    pub request: RequestArgs,
238    /// File containing the static Cedar policies and templates to evaluate against
239    #[arg(long = "policies", value_name = "FILE")]
240    pub policies_file: String,
241    /// File containing template linked policies
242    #[arg(long = "template-linked", value_name = "FILE")]
243    pub template_linked_file: Option<String>,
244    /// File containing schema information
245    /// Used to populate the store with action entities and for schema-based
246    /// parsing of entity hierarchy, if present
247    #[arg(long = "schema", value_name = "FILE")]
248    pub schema_file: Option<String>,
249    /// File containing JSON representation of the Cedar entity hierarchy
250    #[arg(long = "entities", value_name = "FILE")]
251    pub entities_file: String,
252    /// More verbose output. (For instance, indicate which policies applied to the request, if any.)
253    #[arg(short, long)]
254    pub verbose: bool,
255    /// Time authorization and report timing information
256    #[arg(short, long)]
257    pub timing: bool,
258}
259
260#[derive(Args, Debug)]
261pub struct LinkArgs {
262    /// File containing static policies and templates.
263    #[arg(short, long)]
264    pub policies_file: String,
265    /// File containing template-linked policies
266    #[arg(short, long)]
267    pub template_linked_file: String,
268    /// Id of the template to instantiate
269    #[arg(long)]
270    pub template_id: String,
271    /// Id for the new template linked policy
272    #[arg(short, long)]
273    pub new_id: String,
274    /// Arguments to fill slots
275    #[arg(short, long)]
276    pub arguments: Arguments,
277}
278
279#[derive(Args, Debug)]
280pub struct FormatArgs {
281    /// Optional policy file name. If none is provided, read input from stdin.
282    #[arg(value_name = "FILE")]
283    pub file_name: Option<String>,
284
285    /// Custom line width (default: 80).
286    #[arg(short, long, value_name = "UINT", default_value_t = 80)]
287    pub line_width: usize,
288
289    /// Custom indentation width (default: 2).
290    #[arg(short, long, value_name = "INT", default_value_t = 2)]
291    pub indent_width: isize,
292}
293
294/// Wrapper struct
295#[derive(Clone, Debug, Deserialize)]
296#[serde(try_from = "HashMap<String,String>")]
297pub struct Arguments {
298    pub data: HashMap<SlotId, String>,
299}
300
301impl TryFrom<HashMap<String, String>> for Arguments {
302    type Error = String;
303
304    fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
305        Ok(Self {
306            data: value
307                .into_iter()
308                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
309                .collect::<Result<HashMap<SlotId, String>, String>>()?,
310        })
311    }
312}
313
314impl FromStr for Arguments {
315    type Err = serde_json::Error;
316
317    fn from_str(s: &str) -> Result<Self, Self::Err> {
318        serde_json::from_str(s)
319    }
320}
321
322/// This struct is the serde structure expected for --request-json
323#[derive(Deserialize)]
324struct RequestJSON {
325    /// Principal for the request
326    #[serde(default)]
327    principal: Option<String>,
328    /// Action for the request
329    #[serde(default)]
330    action: Option<String>,
331    /// Resource for the request
332    #[serde(default)]
333    resource: Option<String>,
334    /// Context for the request
335    context: serde_json::Value,
336}
337
338#[derive(Args, Debug)]
339pub struct EvaluateArgs {
340    /// Request args (incorporated by reference)
341    #[command(flatten)]
342    pub request: RequestArgs,
343    /// File containing schema information
344    /// Used to populate the store with action entities and for schema-based
345    /// parsing of entity hierarchy, if present
346    #[arg(long = "schema", value_name = "FILE")]
347    pub schema_file: Option<String>,
348    /// File containing JSON representation of the Cedar entity hierarchy.
349    /// This is optional; if not present, we'll just use an empty hierarchy.
350    #[arg(long = "entities", value_name = "FILE")]
351    pub entities_file: Option<String>,
352    /// Expression to evaluate
353    #[arg(value_name = "EXPRESSION")]
354    pub expression: String,
355}
356
357#[derive(Eq, PartialEq, Debug)]
358pub enum CedarExitCode {
359    // The command completed successfully with a result other than a
360    // authorization deny or validation failure.
361    Success,
362    // The command failed to complete successfully.
363    Failure,
364    // The command completed successfully, but the result of the authorization
365    // request was DENY.
366    AuthorizeDeny,
367    // The command completed successfully, but it detected a validation failure
368    // in the given schema and policies.
369    ValidationFailure,
370}
371
372impl Termination for CedarExitCode {
373    fn report(self) -> ExitCode {
374        match self {
375            CedarExitCode::Success => ExitCode::SUCCESS,
376            CedarExitCode::Failure => ExitCode::FAILURE,
377            CedarExitCode::AuthorizeDeny => ExitCode::from(2),
378            CedarExitCode::ValidationFailure => ExitCode::from(3),
379        }
380    }
381}
382
383pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
384    match read_policy_set(args.policies_file.as_ref()) {
385        Ok(_) => CedarExitCode::Success,
386        Err(e) => {
387            println!("Error: {e:?}");
388            CedarExitCode::Failure
389        }
390    }
391}
392
393pub fn validate(args: &ValidateArgs) -> CedarExitCode {
394    let pset = match read_policy_set(Some(&args.policies_file)) {
395        Ok(pset) => pset,
396        Err(e) => {
397            println!("Error: {e:?}");
398            return CedarExitCode::Failure;
399        }
400    };
401
402    let schema = match read_schema_file(&args.schema_file) {
403        Ok(schema) => schema,
404        Err(e) => {
405            println!("Error: {e:?}");
406            return CedarExitCode::Failure;
407        }
408    };
409
410    let validator = Validator::new(schema);
411    let result = validator.validate(&pset, ValidationMode::default());
412    if result.validation_passed() {
413        println!("Validation Passed");
414        return CedarExitCode::Success;
415    } else {
416        println!("Validation Results:");
417        for note in result.validation_errors() {
418            println!("{}", note);
419        }
420        return CedarExitCode::ValidationFailure;
421    }
422}
423
424pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
425    println!();
426    let schema = match args.schema_file.as_ref().map(read_schema_file) {
427        None => None,
428        Some(Ok(schema)) => Some(schema),
429        Some(Err(e)) => {
430            println!("Error: {e:?}");
431            return (CedarExitCode::Failure, EvalResult::Bool(false));
432        }
433    };
434    let request = match args.request.get_request(schema.as_ref()) {
435        Ok(q) => q,
436        Err(e) => {
437            println!("Error: {e:?}");
438            return (CedarExitCode::Failure, EvalResult::Bool(false));
439        }
440    };
441    let expr =
442        match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
443            Ok(expr) => expr,
444            Err(e) => {
445                println!("Error: {e:?}");
446                return (CedarExitCode::Failure, EvalResult::Bool(false));
447            }
448        };
449    let entities = match &args.entities_file {
450        None => Entities::empty(),
451        Some(file) => match load_entities(file, schema.as_ref()) {
452            Ok(entities) => entities,
453            Err(e) => {
454                println!("Error: {e:?}");
455                return (CedarExitCode::Failure, EvalResult::Bool(false));
456            }
457        },
458    };
459    let entities = match load_actions_from_schema(entities, &schema) {
460        Ok(entities) => entities,
461        Err(e) => {
462            println!("Error: {e:?}");
463            return (CedarExitCode::Failure, EvalResult::Bool(false));
464        }
465    };
466    match eval_expression(&request, &entities, &expr)
467        .into_diagnostic()
468        .wrap_err("failed to evaluate the expression")
469    {
470        Err(e) => {
471            println!("Error: {e:?}");
472            return (CedarExitCode::Failure, EvalResult::Bool(false));
473        }
474        Ok(result) => {
475            println!("{result}");
476            return (CedarExitCode::Success, result);
477        }
478    }
479}
480
481pub fn link(args: &LinkArgs) -> CedarExitCode {
482    if let Err(err) = link_inner(args) {
483        println!("Error: {err:?}");
484        CedarExitCode::Failure
485    } else {
486        CedarExitCode::Success
487    }
488}
489
490fn format_policies_inner(args: &FormatArgs) -> Result<()> {
491    let policies_str = read_from_file_or_stdin(args.file_name.as_ref(), "policy set")?;
492    let config = Config {
493        line_width: args.line_width,
494        indent_width: args.indent_width,
495    };
496    println!("{}", policies_str_to_pretty(&policies_str, &config)?);
497    Ok(())
498}
499
500pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
501    if let Err(err) = format_policies_inner(args) {
502        println!("Error: {err:?}");
503        CedarExitCode::Failure
504    } else {
505        CedarExitCode::Success
506    }
507}
508
509fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
510    data.iter()
511        .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
512        .collect::<Result<HashMap<SlotId, EntityUid>>>()
513}
514
515fn link_inner(args: &LinkArgs) -> Result<()> {
516    let mut policies = read_policy_set(Some(&args.policies_file))?;
517    let slotenv = create_slot_env(&args.arguments.data)?;
518    policies
519        .link(
520            PolicyId::from_str(&args.template_id)?,
521            PolicyId::from_str(&args.new_id)?,
522            slotenv,
523        )
524        .into_diagnostic()?;
525    let linked = policies
526        .policy(&PolicyId::from_str(&args.new_id)?)
527        .ok_or_else(|| miette!("Failed to add template-linked policy"))?;
528    println!("Template Linked Policy Added: {linked}");
529    let linked = TemplateLinked {
530        template_id: args.template_id.clone(),
531        link_id: args.new_id.clone(),
532        args: args.arguments.data.clone(),
533    };
534
535    update_template_linked_file(&args.template_linked_file, linked)
536}
537
538#[derive(Clone, Serialize, Deserialize, Debug)]
539#[serde(try_from = "LiteralTemplateLinked")]
540#[serde(into = "LiteralTemplateLinked")]
541struct TemplateLinked {
542    template_id: String,
543    link_id: String,
544    args: HashMap<SlotId, String>,
545}
546
547impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
548    type Error = String;
549
550    fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
551        Ok(Self {
552            template_id: value.template_id,
553            link_id: value.link_id,
554            args: value
555                .args
556                .into_iter()
557                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
558                .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
559        })
560    }
561}
562
563fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
564    match s.as_ref() {
565        "?principal" => Ok(SlotId::principal()),
566        "?resource" => Ok(SlotId::resource()),
567        _ => Err(format!(
568            "Invalid SlotId! Expected ?principal|?resource, got: {}",
569            s.as_ref()
570        )),
571    }
572}
573
574#[derive(Serialize, Deserialize)]
575struct LiteralTemplateLinked {
576    template_id: String,
577    link_id: String,
578    args: HashMap<String, String>,
579}
580
581impl From<TemplateLinked> for LiteralTemplateLinked {
582    fn from(i: TemplateLinked) -> Self {
583        Self {
584            template_id: i.template_id,
585            link_id: i.link_id,
586            args: i
587                .args
588                .into_iter()
589                .map(|(k, v)| (format!("{k}"), v))
590                .collect(),
591        }
592    }
593}
594
595/// Iterate over links in the template-linked file and add them to the set
596fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
597    for template_linked in load_liked_file(path)? {
598        let slot_env = create_slot_env(&template_linked.args)?;
599        policy_set
600            .link(
601                PolicyId::from_str(&template_linked.template_id)?,
602                PolicyId::from_str(&template_linked.link_id)?,
603                slot_env,
604            )
605            .into_diagnostic()?;
606    }
607    Ok(())
608}
609
610/// Read template linked set to a Vec
611fn load_liked_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
612    let f = match std::fs::File::open(path) {
613        Ok(f) => f,
614        Err(_) => {
615            // If the file doesn't exist, then give back the empty entity set
616            return Ok(vec![]);
617        }
618    };
619    if f.metadata()
620        .into_diagnostic()
621        .wrap_err("Failed to read metadata")?
622        .len()
623        == 0
624    {
625        // File is empty, return empty set
626        Ok(vec![])
627    } else {
628        // File has contents, deserialize
629        serde_json::from_reader(f)
630            .into_diagnostic()
631            .wrap_err("Deserialization error")
632    }
633}
634
635/// Add a single template-linked policy to the linked file
636fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
637    let mut template_linked = load_liked_file(path.as_ref())?;
638    template_linked.push(new_linked);
639    write_template_linked_file(&template_linked, path.as_ref())
640}
641
642/// Write a slice of template-linked policies to the linked file
643fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
644    let f = OpenOptions::new()
645        .write(true)
646        .truncate(true)
647        .create(true)
648        .open(path)
649        .into_diagnostic()?;
650    serde_json::to_writer(f, linked).into_diagnostic()
651}
652
653pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
654    println!();
655    let ans = execute_request(
656        &args.request,
657        &args.policies_file,
658        args.template_linked_file.as_ref(),
659        &args.entities_file,
660        args.schema_file.as_ref(),
661        args.timing,
662    );
663    match ans {
664        Ok(ans) => {
665            let status = match ans.decision() {
666                Decision::Allow => {
667                    println!("ALLOW");
668                    CedarExitCode::Success
669                }
670                Decision::Deny => {
671                    println!("DENY");
672                    CedarExitCode::AuthorizeDeny
673                }
674            };
675            if ans.diagnostics().errors().peekable().peek().is_some() {
676                println!();
677                for err in ans.diagnostics().errors() {
678                    println!("{err}");
679                }
680            }
681            if args.verbose {
682                println!();
683                if ans.diagnostics().reason().peekable().peek().is_none() {
684                    println!("note: no policies applied to this request");
685                } else {
686                    println!("note: this decision was due to the following policies:");
687                    for reason in ans.diagnostics().reason() {
688                        println!("  {}", reason);
689                    }
690                    println!();
691                }
692            }
693            status
694        }
695        Err(errs) => {
696            for err in errs {
697                println!("{err:?}");
698            }
699            CedarExitCode::Failure
700        }
701    }
702}
703
704/// Load an `Entities` object from the given JSON filename and optional schema.
705fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
706    match std::fs::OpenOptions::new()
707        .read(true)
708        .open(entities_filename.as_ref())
709    {
710        Ok(f) => Entities::from_json_file(f, schema)
711            .into_diagnostic()
712            .wrap_err_with(|| {
713                format!(
714                    "failed to parse entities from file {}",
715                    entities_filename.as_ref().display()
716                )
717            }),
718        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
719            format!(
720                "failed to open entities file {}",
721                entities_filename.as_ref().display()
722            )
723        }),
724    }
725}
726
727/// Renames policies and templates based on (@id("new_id") annotation.
728/// If no such annotation exists, it keeps the current id.
729///
730/// This will rename template-linked policies to the id of their template, which may
731/// cause id conflicts, so only call this function before instancing
732/// templates into the policy set.
733fn rename_from_id_annotation(ps: PolicySet) -> Result<PolicySet> {
734    let mut new_ps = PolicySet::new();
735    let t_iter = ps.templates().map(|t| match t.annotation("id") {
736        None => Ok(t.clone()),
737        Some(anno) => anno.parse().map(|a| t.new_id(a)),
738    });
739    for t in t_iter {
740        let template = t
741            .into_diagnostic()
742            .wrap_err("failed to parse policy id annotation")?;
743        new_ps
744            .add_template(template)
745            .into_diagnostic()
746            .wrap_err("failed to add template to policy set")?;
747    }
748    let p_iter = ps.policies().map(|p| match p.annotation("id") {
749        None => Ok(p.clone()),
750        Some(anno) => anno.parse().map(|a| p.new_id(a)),
751    });
752    for p in p_iter {
753        let policy = p
754            .into_diagnostic()
755            .wrap_err("failed to parse policy id annotation")?;
756        new_ps
757            .add(policy)
758            .into_diagnostic()
759            .wrap_err("failed to add template to policy set")?;
760    }
761    Ok(new_ps)
762}
763
764fn read_policy_and_links(
765    policies_filename: impl AsRef<Path>,
766    links_filename: Option<impl AsRef<Path>>,
767) -> Result<PolicySet> {
768    let mut pset = read_policy_set(Some(policies_filename.as_ref()))?;
769    if let Some(links_filename) = links_filename {
770        add_template_links_to_set(links_filename.as_ref(), &mut pset)?;
771    }
772    Ok(pset)
773}
774
775// Read from a file (when `filename` is a `Some`) or stdin (when `filename` is `None`)
776fn read_from_file_or_stdin(filename: Option<impl AsRef<Path>>, context: &str) -> Result<String> {
777    let mut src_str = String::new();
778    match filename.as_ref() {
779        Some(path) => {
780            src_str = std::fs::read_to_string(path)
781                .into_diagnostic()
782                .wrap_err_with(|| {
783                    format!(
784                        "failed to open {} file {}",
785                        context,
786                        path.as_ref().display()
787                    )
788                })?;
789        }
790        None => {
791            std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
792                .into_diagnostic()
793                .wrap_err_with(|| format!("failed to read {} from stdin", context))?;
794        }
795    };
796    Ok(src_str)
797}
798
799// Convenient wrapper around `read_from_file_or_stdin` to just read from a file
800fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
801    read_from_file_or_stdin(Some(filename), context)
802}
803
804fn read_policy_set(
805    filename: Option<impl AsRef<Path> + std::marker::Copy>,
806) -> miette::Result<PolicySet> {
807    let context = "policy set";
808    let ps_str = read_from_file_or_stdin(filename, context)?;
809    let ps = PolicySet::from_str(&ps_str)
810        .map_err(|err| {
811            let name = filename.map_or_else(
812                || "<stdin>".to_owned(),
813                |n| n.as_ref().display().to_string(),
814            );
815            Report::new(err).with_source_code(NamedSource::new(name, ps_str))
816        })
817        .wrap_err_with(|| format!("failed to parse {context}"))?;
818    rename_from_id_annotation(ps)
819}
820
821fn read_schema_file(filename: impl AsRef<Path> + std::marker::Copy) -> Result<Schema> {
822    let schema_src = read_from_file(filename, "schema")?;
823    Schema::from_str(&schema_src)
824        .into_diagnostic()
825        .wrap_err_with(|| {
826            format!(
827                "failed to parse schema from file {}",
828                filename.as_ref().display()
829            )
830        })
831}
832
833fn load_actions_from_schema(entities: Entities, schema: &Option<Schema>) -> Result<Entities> {
834    match schema {
835        Some(schema) => match schema.action_entities() {
836            Ok(action_entities) => Entities::from_entities(
837                entities
838                    .iter()
839                    .cloned()
840                    .chain(action_entities.iter().cloned()),
841            )
842            .into_diagnostic()
843            .wrap_err("failed to merge action entities with entity file"),
844            Err(e) => Err(e)
845                .into_diagnostic()
846                .wrap_err("failed to construct action entities"),
847        },
848        None => Ok(entities),
849    }
850}
851
852/// This uses the Cedar API to call the authorization engine.
853fn execute_request(
854    request: &RequestArgs,
855    policies_filename: impl AsRef<Path> + std::marker::Copy,
856    links_filename: Option<impl AsRef<Path>>,
857    entities_filename: impl AsRef<Path>,
858    schema_filename: Option<impl AsRef<Path> + std::marker::Copy>,
859    compute_duration: bool,
860) -> Result<Response, Vec<Report>> {
861    let mut errs = vec![];
862    let policies = match read_policy_and_links(policies_filename.as_ref(), links_filename) {
863        Ok(pset) => pset,
864        Err(e) => {
865            errs.push(e);
866            PolicySet::new()
867        }
868    };
869    let schema = match schema_filename.map(read_schema_file) {
870        None => None,
871        Some(Ok(schema)) => Some(schema),
872        Some(Err(e)) => {
873            errs.push(e);
874            None
875        }
876    };
877    let entities = match load_entities(entities_filename, schema.as_ref()) {
878        Ok(entities) => entities,
879        Err(e) => {
880            errs.push(e);
881            Entities::empty()
882        }
883    };
884    let entities = match load_actions_from_schema(entities, &schema) {
885        Ok(entities) => entities,
886        Err(e) => {
887            errs.push(e);
888            Entities::empty()
889        }
890    };
891    match request.get_request(schema.as_ref()) {
892        Ok(request) if errs.is_empty() => {
893            let authorizer = Authorizer::new();
894            let auth_start = Instant::now();
895            let ans = authorizer.is_authorized(&request, &policies, &entities);
896            let auth_dur = auth_start.elapsed();
897            if compute_duration {
898                println!(
899                    "Authorization Time (micro seconds) : {}",
900                    auth_dur.as_micros()
901                );
902            }
903            Ok(ans)
904        }
905        Ok(_) => Err(errs),
906        Err(e) => {
907            errs.push(e.wrap_err("failed to parse request"));
908            Err(errs)
909        }
910    }
911}