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 Cedar policy syntax to JSON policy syntax (except comments)
99    TranslatePolicy(TranslatePolicyArgs),
100    /// Translate Cedar schema syntax to JSON schema syntax and vice versa (except comments)
101    TranslateSchema(TranslateSchemaArgs),
102    /// Visualize a set of JSON entities to the graphviz format.
103    /// Warning: Entity visualization is best-effort and not well tested.
104    Visualize(VisualizeArgs),
105    /// Create a Cedar project
106    New(NewArgs),
107    /// Partially evaluate an authorization request
108    PartiallyAuthorize(PartiallyAuthorizeArgs),
109}
110
111#[derive(Args, Debug)]
112pub struct TranslatePolicyArgs {
113    /// The direction of translation,
114    #[arg(long)]
115    pub direction: PolicyTranslationDirection,
116    /// Filename to read the policies from.
117    /// If not provided, will default to reading stdin.
118    #[arg(short = 'p', long = "policies", value_name = "FILE")]
119    pub input_file: Option<String>,
120}
121
122/// The direction of translation
123#[derive(Debug, Clone, Copy, ValueEnum)]
124pub enum PolicyTranslationDirection {
125    /// Cedar policy syntax -> JSON
126    CedarToJson,
127}
128
129#[derive(Args, Debug)]
130pub struct TranslateSchemaArgs {
131    /// The direction of translation,
132    #[arg(long)]
133    pub direction: SchemaTranslationDirection,
134    /// Filename to read the schema from.
135    /// If not provided, will default to reading stdin.
136    #[arg(short = 's', long = "schema", value_name = "FILE")]
137    pub input_file: Option<String>,
138}
139
140/// The direction of translation
141#[derive(Debug, Clone, Copy, ValueEnum)]
142pub enum SchemaTranslationDirection {
143    /// JSON -> Cedar schema syntax
144    JsonToCedar,
145    /// Cedar schema syntax -> JSON
146    CedarToJson,
147}
148
149#[derive(Debug, Clone, Copy, ValueEnum)]
150pub enum SchemaFormat {
151    /// the Cedar format
152    Cedar,
153    /// JSON format
154    Json,
155}
156
157impl Default for SchemaFormat {
158    fn default() -> Self {
159        Self::Cedar
160    }
161}
162
163#[derive(Debug, Clone, Copy, ValueEnum)]
164pub enum ValidationMode {
165    /// Strict validation
166    Strict,
167    /// Permissive validation
168    Permissive,
169    /// Partial validation
170    Partial,
171}
172
173#[derive(Args, Debug)]
174pub struct ValidateArgs {
175    /// File containing the schema
176    #[arg(short, long = "schema", value_name = "FILE")]
177    pub schema_file: String,
178    /// Policies args (incorporated by reference)
179    #[command(flatten)]
180    pub policies: PoliciesArgs,
181    /// Report a validation failure for non-fatal warnings
182    #[arg(long)]
183    pub deny_warnings: bool,
184    /// Schema format (Cedar or JSON)
185    #[arg(long, value_enum, default_value_t = SchemaFormat::Cedar)]
186    pub schema_format: SchemaFormat,
187    /// Validate the policy using this mode.
188    /// The options `permissive` and `partial` are experimental
189    /// and will cause the CLI to exit if it was not built with the
190    /// experimental feature `permissive-validate` and `partial-validate`, respectively, enabled.
191    #[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
192    pub validation_mode: ValidationMode,
193}
194
195#[derive(Args, Debug)]
196pub struct CheckParseArgs {
197    /// Policies args (incorporated by reference)
198    #[command(flatten)]
199    pub policies: PoliciesArgs,
200}
201
202/// This struct contains the arguments that together specify a request.
203#[derive(Args, Debug)]
204pub struct RequestArgs {
205    /// Principal for the request, e.g., User::"alice"
206    #[arg(short = 'l', long)]
207    pub principal: Option<String>,
208    /// Action for the request, e.g., Action::"view"
209    #[arg(short, long)]
210    pub action: Option<String>,
211    /// Resource for the request, e.g., File::"myfile.txt"
212    #[arg(short, long)]
213    pub resource: Option<String>,
214    /// File containing a JSON object representing the context for the request.
215    /// Should be a (possibly empty) map from keys to values.
216    #[arg(short, long = "context", value_name = "FILE")]
217    pub context_json_file: Option<String>,
218    /// File containing a JSON object representing the entire request. Must have
219    /// fields "principal", "action", "resource", and "context", where "context"
220    /// is a (possibly empty) map from keys to values. This option replaces
221    /// --principal, --action, etc.
222    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
223    pub request_json_file: Option<String>,
224    /// Whether to enable request validation. This has no effect if a schema is
225    /// not provided.
226    #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
227    pub request_validation: bool,
228}
229
230#[cfg(feature = "partial-eval")]
231/// This struct contains the arguments that together specify a request.
232#[derive(Args, Debug)]
233pub struct PartialRequestArgs {
234    /// Principal for the request, e.g., User::"alice"
235    #[arg(short = 'l', long)]
236    pub principal: Option<String>,
237    /// Action for the request, e.g., Action::"view"
238    #[arg(short, long)]
239    pub action: Option<String>,
240    /// Resource for the request, e.g., File::"myfile.txt"
241    #[arg(short, long)]
242    pub resource: Option<String>,
243    /// File containing a JSON object representing the context for the request.
244    /// Should be a (possibly empty) map from keys to values.
245    #[arg(short, long = "context", value_name = "FILE")]
246    pub context_json_file: Option<String>,
247    /// File containing a JSON object representing the entire request. Must have
248    /// fields "principal", "action", "resource", and "context", where "context"
249    /// is a (possibly empty) map from keys to values. This option replaces
250    /// --principal, --action, etc.
251    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
252    pub request_json_file: Option<String>,
253}
254
255impl RequestArgs {
256    /// Turn this `RequestArgs` into the appropriate `Request` object
257    ///
258    /// `schema` will be used for schema-based parsing of the context, and also
259    /// (if `self.request_validation` is `true`) for request validation.
260    ///
261    /// `self.request_validation` has no effect if `schema` is `None`.
262    fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
263        match &self.request_json_file {
264            Some(jsonfile) => {
265                let jsonstring = std::fs::read_to_string(jsonfile)
266                    .into_diagnostic()
267                    .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
268                let qjson: RequestJSON = serde_json::from_str(&jsonstring)
269                    .into_diagnostic()
270                    .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
271                let principal = qjson.principal.parse().wrap_err_with(|| {
272                    format!("failed to parse principal in {jsonfile} as entity Uid")
273                })?;
274                let action = qjson.action.parse().wrap_err_with(|| {
275                    format!("failed to parse action in {jsonfile} as entity Uid")
276                })?;
277                let resource = qjson.resource.parse().wrap_err_with(|| {
278                    format!("failed to parse resource in {jsonfile} as entity Uid")
279                })?;
280                let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
281                    .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
282                Request::new(
283                    principal,
284                    action,
285                    resource,
286                    context,
287                    if self.request_validation {
288                        schema
289                    } else {
290                        None
291                    },
292                )
293                .map_err(|e| miette!("{e}"))
294            }
295            None => {
296                let principal = self
297                    .principal
298                    .as_ref()
299                    .map(|s| {
300                        s.parse().wrap_err_with(|| {
301                            format!("failed to parse principal {s} as entity Uid")
302                        })
303                    })
304                    .transpose()?;
305                let action = self
306                    .action
307                    .as_ref()
308                    .map(|s| {
309                        s.parse()
310                            .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
311                    })
312                    .transpose()?;
313                let resource = self
314                    .resource
315                    .as_ref()
316                    .map(|s| {
317                        s.parse()
318                            .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
319                    })
320                    .transpose()?;
321                let context: Context = match &self.context_json_file {
322                    None => Context::empty(),
323                    Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
324                        Ok(f) => Context::from_json_file(
325                            f,
326                            schema.and_then(|s| Some((s, action.as_ref()?))),
327                        )
328                        .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
329                        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
330                            format!("error while loading context from {jsonfile}")
331                        })?,
332                    },
333                };
334                match (principal, action, resource) {
335                    (Some(principal), Some(action), Some(resource)) => Request::new(
336                        principal,
337                        action,
338                        resource,
339                        context,
340                        if self.request_validation {
341                            schema
342                        } else {
343                            None
344                        },
345                    )
346                    .map_err(|e| miette!("{e}")),
347                    _ => Err(miette!(
348                        "All three (`principal`, `action`, `resource`) variables must be specified"
349                    )),
350                }
351            }
352        }
353    }
354}
355
356#[cfg(feature = "partial-eval")]
357impl PartialRequestArgs {
358    fn get_request(&self) -> Result<Request> {
359        let mut builder = RequestBuilder::default();
360        let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
361            Some(jsonfile) => {
362                let jsonstring = std::fs::read_to_string(jsonfile)
363                    .into_diagnostic()
364                    .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
365                serde_json::from_str(&jsonstring)
366                    .into_diagnostic()
367                    .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
368            }
369            None => PartialRequestJSON {
370                principal: self.principal.clone(),
371                action: self.action.clone(),
372                resource: self.resource.clone(),
373                context: self
374                    .context_json_file
375                    .as_ref()
376                    .map(|jsonfile| {
377                        let jsonstring = std::fs::read_to_string(jsonfile)
378                            .into_diagnostic()
379                            .wrap_err_with(|| {
380                                format!("failed to open context-json file {jsonfile}")
381                            })?;
382                        serde_json::from_str(&jsonstring)
383                            .into_diagnostic()
384                            .wrap_err_with(|| {
385                                format!("failed to parse context-json file {jsonfile}")
386                            })
387                    })
388                    .transpose()?,
389            },
390        };
391
392        if let Some(principal) = qjson
393            .principal
394            .map(|s| {
395                s.parse()
396                    .wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
397            })
398            .transpose()?
399        {
400            builder = builder.principal(principal);
401        }
402
403        if let Some(action) = qjson
404            .action
405            .map(|s| {
406                s.parse()
407                    .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
408            })
409            .transpose()?
410        {
411            builder = builder.action(action);
412        }
413
414        if let Some(resource) = qjson
415            .resource
416            .map(|s| {
417                s.parse()
418                    .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
419            })
420            .transpose()?
421        {
422            builder = builder.resource(resource);
423        }
424
425        if let Some(context) = qjson
426            .context
427            .map(|json| {
428                Context::from_json_value(json.clone(), None)
429                    .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
430            })
431            .transpose()?
432        {
433            builder = builder.context(context);
434        }
435
436        Ok(builder.build())
437    }
438}
439
440/// This struct contains the arguments that together specify an input policy or policy set.
441#[derive(Args, Debug)]
442pub struct PoliciesArgs {
443    /// File containing the static Cedar policies and/or templates. If not provided, read policies from stdin.
444    #[arg(short, long = "policies", value_name = "FILE")]
445    pub policies_file: Option<String>,
446    /// Format of policies in the `--policies` file
447    #[arg(long = "policy-format", default_value_t, value_enum)]
448    pub policy_format: PolicyFormat,
449    /// File containing template-linked policies
450    #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
451    pub template_linked_file: Option<String>,
452}
453
454impl PoliciesArgs {
455    /// Turn this `PoliciesArgs` into the appropriate `PolicySet` object
456    fn get_policy_set(&self) -> Result<PolicySet> {
457        let mut pset = match self.policy_format {
458            PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
459            PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
460        }?;
461        if let Some(links_filename) = self.template_linked_file.as_ref() {
462            add_template_links_to_set(links_filename, &mut pset)?;
463        }
464        Ok(pset)
465    }
466}
467
468#[derive(Args, Debug)]
469pub struct AuthorizeArgs {
470    /// Request args (incorporated by reference)
471    #[command(flatten)]
472    pub request: RequestArgs,
473    /// Policies args (incorporated by reference)
474    #[command(flatten)]
475    pub policies: PoliciesArgs,
476    /// File containing schema information
477    ///
478    /// Used to populate the store with action entities and for schema-based
479    /// parsing of entity hierarchy, if present
480    #[arg(short, long = "schema", value_name = "FILE")]
481    pub schema_file: Option<String>,
482    /// Schema format (Cedar or JSON)
483    #[arg(long, value_enum, default_value_t = SchemaFormat::Cedar)]
484    pub schema_format: SchemaFormat,
485    /// File containing JSON representation of the Cedar entity hierarchy
486    #[arg(long = "entities", value_name = "FILE")]
487    pub entities_file: String,
488    /// More verbose output. (For instance, indicate which policies applied to the request, if any.)
489    #[arg(short, long)]
490    pub verbose: bool,
491    /// Time authorization and report timing information
492    #[arg(short, long)]
493    pub timing: bool,
494}
495
496#[cfg(feature = "partial-eval")]
497#[derive(Args, Debug)]
498pub struct PartiallyAuthorizeArgs {
499    /// Request args (incorporated by reference)
500    #[command(flatten)]
501    pub request: PartialRequestArgs,
502    /// Policies args (incorporated by reference)
503    #[command(flatten)]
504    pub policies: PoliciesArgs,
505    /// File containing JSON representation of the Cedar entity hierarchy
506    #[arg(long = "entities", value_name = "FILE")]
507    pub entities_file: String,
508    /// Time authorization and report timing information
509    #[arg(short, long)]
510    pub timing: bool,
511}
512
513#[cfg(not(feature = "partial-eval"))]
514#[derive(Debug, Args)]
515pub struct PartiallyAuthorizeArgs;
516
517#[derive(Args, Debug)]
518pub struct VisualizeArgs {
519    #[arg(long = "entities", value_name = "FILE")]
520    pub entities_file: String,
521}
522
523#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
524pub enum PolicyFormat {
525    /// The standard Cedar policy format, documented at <https://docs.cedarpolicy.com/policies/syntax-policy.html>
526    #[default]
527    Cedar,
528    /// Cedar's JSON policy format, documented at <https://docs.cedarpolicy.com/policies/json-format.html>
529    Json,
530}
531
532#[derive(Args, Debug)]
533pub struct LinkArgs {
534    /// Policies args (incorporated by reference)
535    #[command(flatten)]
536    pub policies: PoliciesArgs,
537    /// Id of the template to link
538    #[arg(long)]
539    pub template_id: String,
540    /// Id for the new template linked policy
541    #[arg(short, long)]
542    pub new_id: String,
543    /// Arguments to fill slots
544    #[arg(short, long)]
545    pub arguments: Arguments,
546}
547
548#[derive(Args, Debug)]
549pub struct FormatArgs {
550    /// File containing the static Cedar policies and/or templates. If not provided, read policies from stdin.
551    #[arg(short, long = "policies", value_name = "FILE")]
552    pub policies_file: Option<String>,
553
554    /// Custom line width (default: 80).
555    #[arg(short, long, value_name = "UINT", default_value_t = 80)]
556    pub line_width: usize,
557
558    /// Custom indentation width (default: 2).
559    #[arg(short, long, value_name = "INT", default_value_t = 2)]
560    pub indent_width: isize,
561
562    /// Automatically write back the formatted policies to the input file.
563    #[arg(short, long, group = "action", requires = "policies_file")]
564    pub write: bool,
565
566    /// Check that the policies formats without any changes. Mutually exclusive with `write`.
567    #[arg(short, long, group = "action")]
568    pub check: bool,
569}
570
571#[derive(Args, Debug)]
572pub struct NewArgs {
573    /// Name of the Cedar project
574    #[arg(short, long, value_name = "DIR")]
575    pub name: String,
576}
577
578/// Wrapper struct
579#[derive(Clone, Debug, Deserialize)]
580#[serde(try_from = "HashMap<String,String>")]
581pub struct Arguments {
582    pub data: HashMap<SlotId, String>,
583}
584
585impl TryFrom<HashMap<String, String>> for Arguments {
586    type Error = String;
587
588    fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
589        Ok(Self {
590            data: value
591                .into_iter()
592                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
593                .collect::<Result<HashMap<SlotId, String>, String>>()?,
594        })
595    }
596}
597
598impl FromStr for Arguments {
599    type Err = serde_json::Error;
600
601    fn from_str(s: &str) -> Result<Self, Self::Err> {
602        serde_json::from_str(s)
603    }
604}
605
606/// This struct is the serde structure expected for --request-json
607#[derive(Deserialize)]
608struct RequestJSON {
609    /// Principal for the request
610    #[serde(default)]
611    principal: String,
612    /// Action for the request
613    #[serde(default)]
614    action: String,
615    /// Resource for the request
616    #[serde(default)]
617    resource: String,
618    /// Context for the request
619    context: serde_json::Value,
620}
621
622#[cfg(feature = "partial-eval")]
623/// This struct is the serde structure expected for --request-json
624#[derive(Deserialize)]
625pub(self) struct PartialRequestJSON {
626    /// Principal for the request
627    pub(self) principal: Option<String>,
628    /// Action for the request
629    pub(self) action: Option<String>,
630    /// Resource for the request
631    pub(self) resource: Option<String>,
632    /// Context for the request
633    pub(self) context: Option<serde_json::Value>,
634}
635
636#[derive(Args, Debug)]
637pub struct EvaluateArgs {
638    /// Request args (incorporated by reference)
639    #[command(flatten)]
640    pub request: RequestArgs,
641    /// File containing schema information
642    /// Used to populate the store with action entities and for schema-based
643    /// parsing of entity hierarchy, if present
644    #[arg(short, long = "schema", value_name = "FILE")]
645    pub schema_file: Option<String>,
646    /// Schema format (Cedar or JSON)
647    #[arg(long, value_enum, default_value_t = SchemaFormat::Cedar)]
648    pub schema_format: SchemaFormat,
649    /// File containing JSON representation of the Cedar entity hierarchy.
650    /// This is optional; if not present, we'll just use an empty hierarchy.
651    #[arg(long = "entities", value_name = "FILE")]
652    pub entities_file: Option<String>,
653    /// Expression to evaluate
654    #[arg(value_name = "EXPRESSION")]
655    pub expression: String,
656}
657
658#[derive(Eq, PartialEq, Debug)]
659pub enum CedarExitCode {
660    // The command completed successfully with a result other than a
661    // authorization deny or validation failure.
662    Success,
663    // The command failed to complete successfully.
664    Failure,
665    // The command completed successfully, but the result of the authorization
666    // request was DENY.
667    AuthorizeDeny,
668    // The command completed successfully, but it detected a validation failure
669    // in the given schema and policies.
670    ValidationFailure,
671    #[cfg(feature = "partial-eval")]
672    // The command completed successfully with an incomplete result, e.g.,
673    // partial authorization result is not determining.
674    Unknown,
675}
676
677impl Termination for CedarExitCode {
678    fn report(self) -> ExitCode {
679        match self {
680            CedarExitCode::Success => ExitCode::SUCCESS,
681            CedarExitCode::Failure => ExitCode::FAILURE,
682            CedarExitCode::AuthorizeDeny => ExitCode::from(2),
683            CedarExitCode::ValidationFailure => ExitCode::from(3),
684            #[cfg(feature = "partial-eval")]
685            CedarExitCode::Unknown => ExitCode::SUCCESS,
686        }
687    }
688}
689
690pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
691    match args.policies.get_policy_set() {
692        Ok(_) => CedarExitCode::Success,
693        Err(e) => {
694            println!("{e:?}");
695            CedarExitCode::Failure
696        }
697    }
698}
699
700pub fn validate(args: &ValidateArgs) -> CedarExitCode {
701    let mode = match args.validation_mode {
702        ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
703        ValidationMode::Permissive => {
704            #[cfg(not(feature = "permissive-validate"))]
705            {
706                eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
707                return CedarExitCode::Failure;
708            }
709            #[cfg(feature = "permissive-validate")]
710            cedar_policy::ValidationMode::Permissive
711        }
712        ValidationMode::Partial => {
713            #[cfg(not(feature = "partial-validate"))]
714            {
715                eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
716                return CedarExitCode::Failure;
717            }
718            #[cfg(feature = "partial-validate")]
719            cedar_policy::ValidationMode::Partial
720        }
721    };
722
723    let pset = match args.policies.get_policy_set() {
724        Ok(pset) => pset,
725        Err(e) => {
726            println!("{e:?}");
727            return CedarExitCode::Failure;
728        }
729    };
730
731    let schema = match read_schema_file(&args.schema_file, args.schema_format) {
732        Ok(schema) => schema,
733        Err(e) => {
734            println!("{e:?}");
735            return CedarExitCode::Failure;
736        }
737    };
738
739    let validator = Validator::new(schema);
740    let result = validator.validate(&pset, mode);
741
742    if !result.validation_passed()
743        || (args.deny_warnings && !result.validation_passed_without_warnings())
744    {
745        println!(
746            "{:?}",
747            Report::new(result).wrap_err("policy set validation failed")
748        );
749        CedarExitCode::ValidationFailure
750    } else {
751        println!(
752            "{:?}",
753            Report::new(result).wrap_err("policy set validation passed")
754        );
755        CedarExitCode::Success
756    }
757}
758
759pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
760    println!();
761    let schema = match args
762        .schema_file
763        .as_ref()
764        .map(|f| read_schema_file(f, args.schema_format))
765    {
766        None => None,
767        Some(Ok(schema)) => Some(schema),
768        Some(Err(e)) => {
769            println!("{e:?}");
770            return (CedarExitCode::Failure, EvalResult::Bool(false));
771        }
772    };
773    let request = match args.request.get_request(schema.as_ref()) {
774        Ok(q) => q,
775        Err(e) => {
776            println!("{e:?}");
777            return (CedarExitCode::Failure, EvalResult::Bool(false));
778        }
779    };
780    let expr =
781        match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
782            Ok(expr) => expr,
783            Err(e) => {
784                println!("{:?}", e.with_source_code(args.expression.clone()));
785                return (CedarExitCode::Failure, EvalResult::Bool(false));
786            }
787        };
788    let entities = match &args.entities_file {
789        None => Entities::empty(),
790        Some(file) => match load_entities(file, schema.as_ref()) {
791            Ok(entities) => entities,
792            Err(e) => {
793                println!("{e:?}");
794                return (CedarExitCode::Failure, EvalResult::Bool(false));
795            }
796        },
797    };
798    match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
799    {
800        Err(e) => {
801            println!("{e:?}");
802            return (CedarExitCode::Failure, EvalResult::Bool(false));
803        }
804        Ok(result) => {
805            println!("{result}");
806            return (CedarExitCode::Success, result);
807        }
808    }
809}
810
811pub fn link(args: &LinkArgs) -> CedarExitCode {
812    if let Err(err) = link_inner(args) {
813        println!("{err:?}");
814        CedarExitCode::Failure
815    } else {
816        CedarExitCode::Success
817    }
818}
819
820pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
821    match load_entities(&args.entities_file, None) {
822        Ok(entities) => {
823            println!("{}", entities.to_dot_str());
824            CedarExitCode::Success
825        }
826        Err(report) => {
827            eprintln!("{report:?}");
828            CedarExitCode::Failure
829        }
830    }
831}
832
833/// Format the policies in the given file or stdin.
834///
835/// Returns a boolean indicating whether the formatted policies are the same as the original
836/// policies.
837fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
838    let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
839    let config = Config {
840        line_width: args.line_width,
841        indent_width: args.indent_width,
842    };
843    let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
844    let are_policies_equivalent = policies_str == formatted_policy;
845
846    match &args.policies_file {
847        Some(policies_file) if args.write => {
848            let mut file = OpenOptions::new()
849                .write(true)
850                .truncate(true)
851                .open(policies_file)
852                .into_diagnostic()
853                .wrap_err(format!("failed to open {policies_file} for writing"))?;
854            file.write_all(formatted_policy.as_bytes())
855                .into_diagnostic()
856                .wrap_err(format!(
857                    "failed to write formatted policies to {policies_file}"
858                ))?;
859        }
860        _ => println!("{}", formatted_policy),
861    }
862    Ok(are_policies_equivalent)
863}
864
865pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
866    match format_policies_inner(args) {
867        Ok(false) if args.check => CedarExitCode::Failure,
868        Err(err) => {
869            println!("{err:?}");
870            CedarExitCode::Failure
871        }
872        _ => CedarExitCode::Success,
873    }
874}
875
876fn translate_policy_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
877    let policy_set = PolicySet::from_str(cedar_src.as_ref())?;
878    let output = policy_set.to_json()?.to_string();
879    Ok(output)
880}
881
882fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
883    let translate = match args.direction {
884        PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
885    };
886    read_from_file_or_stdin(args.input_file.clone(), "policy").and_then(translate)
887}
888
889pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
890    match translate_policy_inner(args) {
891        Ok(sf) => {
892            println!("{sf}");
893            CedarExitCode::Success
894        }
895        Err(err) => {
896            eprintln!("{err:?}");
897            CedarExitCode::Failure
898        }
899    }
900}
901
902fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
903    let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
904    let output = fragment.to_cedarschema()?;
905    Ok(output)
906}
907
908fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
909    let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
910    for warning in warnings {
911        let report = miette::Report::new(warning);
912        eprintln!("{:?}", report);
913    }
914    let output = fragment.to_json_string()?;
915    Ok(output)
916}
917
918fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
919    let translate = match args.direction {
920        SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
921        SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
922    };
923    read_from_file_or_stdin(args.input_file.clone(), "schema").and_then(translate)
924}
925
926pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
927    match translate_schema_inner(args) {
928        Ok(sf) => {
929            println!("{sf}");
930            CedarExitCode::Success
931        }
932        Err(err) => {
933            eprintln!("{err:?}");
934            CedarExitCode::Failure
935        }
936    }
937}
938
939/// Write a schema (in JSON format) to `path`
940fn generate_schema(path: &Path) -> Result<()> {
941    std::fs::write(
942        path,
943        serde_json::to_string_pretty(&serde_json::json!(
944        {
945            "": {
946                "entityTypes": {
947                    "A": {
948                        "memberOfTypes": [
949                            "B"
950                        ]
951                    },
952                    "B": {
953                        "memberOfTypes": []
954                    },
955                    "C": {
956                        "memberOfTypes": []
957                    }
958                },
959                "actions": {
960                    "action": {
961                        "appliesTo": {
962                            "resourceTypes": [
963                                "C"
964                            ],
965                            "principalTypes": [
966                                "A",
967                                "B"
968                            ]
969                        }
970                    }
971                }
972            }
973        }))
974        .into_diagnostic()?,
975    )
976    .into_diagnostic()
977}
978
979fn generate_policy(path: &Path) -> Result<()> {
980    std::fs::write(
981        path,
982        r#"permit (
983  principal in A::"a",
984  action == Action::"action",
985  resource == C::"c"
986) when { true };
987"#,
988    )
989    .into_diagnostic()
990}
991
992fn generate_entities(path: &Path) -> Result<()> {
993    std::fs::write(
994        path,
995        serde_json::to_string_pretty(&serde_json::json!(
996        [
997            {
998                "uid": { "type": "A", "id": "a"} ,
999                "attrs": {},
1000                "parents": [{"type": "B", "id": "b"}]
1001            },
1002            {
1003                "uid": { "type": "B", "id": "b"} ,
1004                "attrs": {},
1005                "parents": []
1006            },
1007            {
1008                "uid": { "type": "C", "id": "c"} ,
1009                "attrs": {},
1010                "parents": []
1011            }
1012        ]))
1013        .into_diagnostic()?,
1014    )
1015    .into_diagnostic()
1016}
1017
1018fn new_inner(args: &NewArgs) -> Result<()> {
1019    let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1020    std::fs::create_dir(dir).into_diagnostic()?;
1021    let schema_path = dir.join("schema.cedarschema.json");
1022    let policy_path = dir.join("policy.cedar");
1023    let entities_path = dir.join("entities.json");
1024    generate_schema(&schema_path)?;
1025    generate_policy(&policy_path)?;
1026    generate_entities(&entities_path)
1027}
1028
1029pub fn new(args: &NewArgs) -> CedarExitCode {
1030    if let Err(err) = new_inner(args) {
1031        println!("{err:?}");
1032        CedarExitCode::Failure
1033    } else {
1034        CedarExitCode::Success
1035    }
1036}
1037
1038fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
1039    data.iter()
1040        .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
1041        .collect::<Result<HashMap<SlotId, EntityUid>>>()
1042}
1043
1044fn link_inner(args: &LinkArgs) -> Result<()> {
1045    let mut policies = args.policies.get_policy_set()?;
1046    let slotenv = create_slot_env(&args.arguments.data)?;
1047    policies.link(
1048        PolicyId::new(&args.template_id),
1049        PolicyId::new(&args.new_id),
1050        slotenv,
1051    )?;
1052    let linked = policies
1053        .policy(&PolicyId::new(&args.new_id))
1054        .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
1055    println!("Template-linked policy added: {linked}");
1056
1057    // If a `--template-linked` / `-k` option was provided, update that file with the new link
1058    if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
1059        update_template_linked_file(
1060            links_filename,
1061            TemplateLinked {
1062                template_id: args.template_id.clone(),
1063                link_id: args.new_id.clone(),
1064                args: args.arguments.data.clone(),
1065            },
1066        )?;
1067    }
1068
1069    Ok(())
1070}
1071
1072#[derive(Clone, Serialize, Deserialize, Debug)]
1073#[serde(try_from = "LiteralTemplateLinked")]
1074#[serde(into = "LiteralTemplateLinked")]
1075struct TemplateLinked {
1076    template_id: String,
1077    link_id: String,
1078    args: HashMap<SlotId, String>,
1079}
1080
1081impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
1082    type Error = String;
1083
1084    fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
1085        Ok(Self {
1086            template_id: value.template_id,
1087            link_id: value.link_id,
1088            args: value
1089                .args
1090                .into_iter()
1091                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1092                .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
1093        })
1094    }
1095}
1096
1097fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
1098    match s.as_ref() {
1099        "?principal" => Ok(SlotId::principal()),
1100        "?resource" => Ok(SlotId::resource()),
1101        _ => Err(format!(
1102            "Invalid SlotId! Expected ?principal|?resource, got: {}",
1103            s.as_ref()
1104        )),
1105    }
1106}
1107
1108#[derive(Serialize, Deserialize)]
1109struct LiteralTemplateLinked {
1110    template_id: String,
1111    link_id: String,
1112    args: HashMap<String, String>,
1113}
1114
1115impl From<TemplateLinked> for LiteralTemplateLinked {
1116    fn from(i: TemplateLinked) -> Self {
1117        Self {
1118            template_id: i.template_id,
1119            link_id: i.link_id,
1120            args: i
1121                .args
1122                .into_iter()
1123                .map(|(k, v)| (format!("{k}"), v))
1124                .collect(),
1125        }
1126    }
1127}
1128
1129/// Iterate over links in the template-linked file and add them to the set
1130fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
1131    for template_linked in load_links_from_file(path)? {
1132        let slot_env = create_slot_env(&template_linked.args)?;
1133        policy_set.link(
1134            PolicyId::new(&template_linked.template_id),
1135            PolicyId::new(&template_linked.link_id),
1136            slot_env,
1137        )?;
1138    }
1139    Ok(())
1140}
1141
1142/// Given a file containing template links, return a `Vec` of those links
1143fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
1144    let f = match std::fs::File::open(path) {
1145        Ok(f) => f,
1146        Err(_) => {
1147            // If the file doesn't exist, then give back the empty entity set
1148            return Ok(vec![]);
1149        }
1150    };
1151    if f.metadata()
1152        .into_diagnostic()
1153        .wrap_err("Failed to read metadata")?
1154        .len()
1155        == 0
1156    {
1157        // File is empty, return empty set
1158        Ok(vec![])
1159    } else {
1160        // File has contents, deserialize
1161        serde_json::from_reader(f)
1162            .into_diagnostic()
1163            .wrap_err("Deserialization error")
1164    }
1165}
1166
1167/// Add a single template-linked policy to the linked file
1168fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
1169    let mut template_linked = load_links_from_file(path.as_ref())?;
1170    template_linked.push(new_linked);
1171    write_template_linked_file(&template_linked, path.as_ref())
1172}
1173
1174/// Write a slice of template-linked policies to the linked file
1175fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
1176    let f = OpenOptions::new()
1177        .write(true)
1178        .truncate(true)
1179        .create(true)
1180        .open(path)
1181        .into_diagnostic()?;
1182    serde_json::to_writer(f, linked).into_diagnostic()
1183}
1184
1185pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
1186    println!();
1187    let ans = execute_request(
1188        &args.request,
1189        &args.policies,
1190        &args.entities_file,
1191        args.schema_file.as_ref(),
1192        args.schema_format,
1193        args.timing,
1194    );
1195    match ans {
1196        Ok(ans) => {
1197            let status = match ans.decision() {
1198                Decision::Allow => {
1199                    println!("ALLOW");
1200                    CedarExitCode::Success
1201                }
1202                Decision::Deny => {
1203                    println!("DENY");
1204                    CedarExitCode::AuthorizeDeny
1205                }
1206            };
1207            if ans.diagnostics().errors().peekable().peek().is_some() {
1208                println!();
1209                for err in ans.diagnostics().errors() {
1210                    println!("{err}");
1211                }
1212            }
1213            if args.verbose {
1214                println!();
1215                if ans.diagnostics().reason().peekable().peek().is_none() {
1216                    println!("note: no policies applied to this request");
1217                } else {
1218                    println!("note: this decision was due to the following policies:");
1219                    for reason in ans.diagnostics().reason() {
1220                        println!("  {}", reason);
1221                    }
1222                    println!();
1223                }
1224            }
1225            status
1226        }
1227        Err(errs) => {
1228            for err in errs {
1229                println!("{err:?}");
1230            }
1231            CedarExitCode::Failure
1232        }
1233    }
1234}
1235
1236#[cfg(not(feature = "partial-eval"))]
1237pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
1238    {
1239        eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1240        return CedarExitCode::Failure;
1241    }
1242}
1243
1244#[cfg(feature = "partial-eval")]
1245pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
1246    println!();
1247    let ans = execute_partial_request(
1248        &args.request,
1249        &args.policies,
1250        &args.entities_file,
1251        args.timing,
1252    );
1253    match ans {
1254        Ok(ans) => {
1255            let status = match ans.decision() {
1256                Some(Decision::Allow) => {
1257                    println!("ALLOW");
1258                    CedarExitCode::Success
1259                }
1260                Some(Decision::Deny) => {
1261                    println!("DENY");
1262                    CedarExitCode::AuthorizeDeny
1263                }
1264                None => {
1265                    println!("UNKNOWN");
1266                    println!("All policy residuals:");
1267                    for p in ans.nontrivial_residuals() {
1268                        println!("{p}");
1269                    }
1270                    CedarExitCode::Unknown
1271                }
1272            };
1273            status
1274        }
1275        Err(errs) => {
1276            for err in errs {
1277                println!("{err:?}");
1278            }
1279            CedarExitCode::Failure
1280        }
1281    }
1282}
1283
1284/// Load an `Entities` object from the given JSON filename and optional schema.
1285fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
1286    match std::fs::OpenOptions::new()
1287        .read(true)
1288        .open(entities_filename.as_ref())
1289    {
1290        Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
1291            format!(
1292                "failed to parse entities from file {}",
1293                entities_filename.as_ref().display()
1294            )
1295        }),
1296        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1297            format!(
1298                "failed to open entities file {}",
1299                entities_filename.as_ref().display()
1300            )
1301        }),
1302    }
1303}
1304
1305/// Renames policies and templates based on (@id("new_id") annotation.
1306/// If no such annotation exists, it keeps the current id.
1307///
1308/// This will rename template-linked policies to the id of their template, which may
1309/// cause id conflicts, so only call this function before instancing
1310/// templates into the policy set.
1311fn rename_from_id_annotation(ps: PolicySet) -> Result<PolicySet> {
1312    let mut new_ps = PolicySet::new();
1313    let t_iter = ps.templates().map(|t| match t.annotation("id") {
1314        None => Ok(t.clone()),
1315        Some(anno) => anno.parse().map(|a| t.new_id(a)),
1316    });
1317    for t in t_iter {
1318        let template = t.unwrap_or_else(|never| match never {});
1319        new_ps
1320            .add_template(template)
1321            .wrap_err("failed to add template to policy set")?;
1322    }
1323    let p_iter = ps.policies().map(|p| match p.annotation("id") {
1324        None => Ok(p.clone()),
1325        Some(anno) => anno.parse().map(|a| p.new_id(a)),
1326    });
1327    for p in p_iter {
1328        let policy = p.unwrap_or_else(|never| match never {});
1329        new_ps
1330            .add(policy)
1331            .wrap_err("failed to add template to policy set")?;
1332    }
1333    Ok(new_ps)
1334}
1335
1336// Read from a file (when `filename` is a `Some`) or stdin (when `filename` is `None`) to a `String`
1337fn read_from_file_or_stdin(filename: Option<impl AsRef<Path>>, context: &str) -> Result<String> {
1338    let mut src_str = String::new();
1339    match filename.as_ref() {
1340        Some(path) => {
1341            src_str = std::fs::read_to_string(path)
1342                .into_diagnostic()
1343                .wrap_err_with(|| {
1344                    format!("failed to open {context} file {}", path.as_ref().display())
1345                })?;
1346        }
1347        None => {
1348            std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
1349                .into_diagnostic()
1350                .wrap_err_with(|| format!("failed to read {} from stdin", context))?;
1351        }
1352    };
1353    Ok(src_str)
1354}
1355
1356// Convenient wrapper around `read_from_file_or_stdin` to just read from a file
1357fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
1358    read_from_file_or_stdin(Some(filename), context)
1359}
1360
1361/// Read a policy set, in Cedar syntax, from the file given in `filename`,
1362/// or from stdin if `filename` is `None`.
1363fn read_cedar_policy_set(
1364    filename: Option<impl AsRef<Path> + std::marker::Copy>,
1365) -> Result<PolicySet> {
1366    let context = "policy set";
1367    let ps_str = read_from_file_or_stdin(filename, context)?;
1368    let ps = PolicySet::from_str(&ps_str)
1369        .map_err(|err| {
1370            let name = filename.map_or_else(
1371                || "<stdin>".to_owned(),
1372                |n| n.as_ref().display().to_string(),
1373            );
1374            Report::new(err).with_source_code(NamedSource::new(name, ps_str))
1375        })
1376        .wrap_err_with(|| format!("failed to parse {context}"))?;
1377    rename_from_id_annotation(ps)
1378}
1379
1380/// Read a policy set, static policy or policy template, in Cedar JSON (EST) syntax, from the file given
1381/// in `filename`, or from stdin if `filename` is `None`.
1382fn read_json_policy_set(
1383    filename: Option<impl AsRef<Path> + std::marker::Copy>,
1384) -> Result<PolicySet> {
1385    let context = "JSON policy";
1386    let json_source = read_from_file_or_stdin(filename, context)?;
1387    let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
1388    let policy_type = get_json_policy_type(&json)?;
1389
1390    let add_json_source = |report: Report| {
1391        let name = filename.map_or_else(
1392            || "<stdin>".to_owned(),
1393            |n| n.as_ref().display().to_string(),
1394        );
1395        report.with_source_code(NamedSource::new(name, json_source.clone()))
1396    };
1397
1398    match policy_type {
1399        JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
1400            Ok(policy) => PolicySet::from_policies([policy])
1401                .wrap_err_with(|| format!("failed to create policy set from {context}")),
1402            Err(_) => match Template::from_json(None, json)
1403                .map_err(|err| add_json_source(Report::new(err)))
1404            {
1405                Ok(template) => {
1406                    let mut ps = PolicySet::new();
1407                    ps.add_template(template)?;
1408                    Ok(ps)
1409                }
1410                Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
1411            },
1412        },
1413        JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
1414            .map_err(|err| add_json_source(Report::new(err)))
1415            .wrap_err_with(|| format!("failed to create policy set from {context}")),
1416    }
1417}
1418
1419fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
1420    let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
1421    let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
1422
1423    let json_has_property = |p| json.get(p).is_some();
1424    let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
1425    let has_any_policy_property = policy_properties.iter().any(json_has_property);
1426
1427    match (has_any_policy_set_property, has_any_policy_property) {
1428        (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
1429        (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
1430        (true, _) => Ok(JsonPolicyType::PolicySet),
1431        (_, true) => Ok(JsonPolicyType::SinglePolicy),
1432    }
1433}
1434
1435enum JsonPolicyType {
1436    SinglePolicy,
1437    PolicySet,
1438}
1439
1440fn read_schema_file(
1441    filename: impl AsRef<Path> + std::marker::Copy,
1442    format: SchemaFormat,
1443) -> Result<Schema> {
1444    let schema_src = read_from_file(filename, "schema")?;
1445    match format {
1446        SchemaFormat::Json => Schema::from_json_str(&schema_src).wrap_err_with(|| {
1447            format!(
1448                "failed to parse schema from file {}",
1449                filename.as_ref().display()
1450            )
1451        }),
1452        SchemaFormat::Cedar => {
1453            let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)?;
1454            for warning in warnings {
1455                let report = miette::Report::new(warning);
1456                eprintln!("{:?}", report);
1457            }
1458            Ok(schema)
1459        }
1460    }
1461}
1462
1463/// This uses the Cedar API to call the authorization engine.
1464fn execute_request(
1465    request: &RequestArgs,
1466    policies: &PoliciesArgs,
1467    entities_filename: impl AsRef<Path>,
1468    schema_filename: Option<impl AsRef<Path> + std::marker::Copy>,
1469    schema_format: SchemaFormat,
1470    compute_duration: bool,
1471) -> Result<Response, Vec<Report>> {
1472    let mut errs = vec![];
1473    let policies = match policies.get_policy_set() {
1474        Ok(pset) => pset,
1475        Err(e) => {
1476            errs.push(e);
1477            PolicySet::new()
1478        }
1479    };
1480    let schema = match schema_filename.map(|f| read_schema_file(f, schema_format)) {
1481        None => None,
1482        Some(Ok(schema)) => Some(schema),
1483        Some(Err(e)) => {
1484            errs.push(e);
1485            None
1486        }
1487    };
1488    let entities = match load_entities(entities_filename, schema.as_ref()) {
1489        Ok(entities) => entities,
1490        Err(e) => {
1491            errs.push(e);
1492            Entities::empty()
1493        }
1494    };
1495    match request.get_request(schema.as_ref()) {
1496        Ok(request) if errs.is_empty() => {
1497            let authorizer = Authorizer::new();
1498            let auth_start = Instant::now();
1499            let ans = authorizer.is_authorized(&request, &policies, &entities);
1500            let auth_dur = auth_start.elapsed();
1501            if compute_duration {
1502                println!(
1503                    "Authorization Time (micro seconds) : {}",
1504                    auth_dur.as_micros()
1505                );
1506            }
1507            Ok(ans)
1508        }
1509        Ok(_) => Err(errs),
1510        Err(e) => {
1511            errs.push(e.wrap_err("failed to parse request"));
1512            Err(errs)
1513        }
1514    }
1515}
1516
1517#[cfg(feature = "partial-eval")]
1518fn execute_partial_request(
1519    request: &PartialRequestArgs,
1520    policies: &PoliciesArgs,
1521    entities_filename: impl AsRef<Path>,
1522    compute_duration: bool,
1523) -> Result<PartialResponse, Vec<Report>> {
1524    let mut errs = vec![];
1525    let policies = match policies.get_policy_set() {
1526        Ok(pset) => pset,
1527        Err(e) => {
1528            errs.push(e);
1529            PolicySet::new()
1530        }
1531    };
1532    let entities = match load_entities(entities_filename, None) {
1533        Ok(entities) => entities,
1534        Err(e) => {
1535            errs.push(e);
1536            Entities::empty()
1537        }
1538    };
1539    match request.get_request() {
1540        Ok(request) if errs.is_empty() => {
1541            let authorizer = Authorizer::new();
1542            let auth_start = Instant::now();
1543            let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
1544            let auth_dur = auth_start.elapsed();
1545            if compute_duration {
1546                println!(
1547                    "Authorization Time (micro seconds) : {}",
1548                    auth_dur.as_micros()
1549                );
1550            }
1551            Ok(ans)
1552        }
1553        Ok(_) => Err(errs),
1554        Err(e) => {
1555            errs.push(e.wrap_err("failed to parse request"));
1556            Err(errs)
1557        }
1558    }
1559}