cedar_policy_cli/
lib.rs

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