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