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