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