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 owo_colors::OwoColorize;
25use serde::{Deserialize, Deserializer, Serialize};
26use std::collections::BTreeSet;
27use std::io::{BufReader, Write};
28use std::{
29    collections::HashMap,
30    fmt::{self, Display},
31    fs::OpenOptions,
32    path::{Path, PathBuf},
33    process::{ExitCode, Termination},
34    str::FromStr,
35    time::Instant,
36};
37
38use cedar_policy::*;
39use cedar_policy_formatter::{policies_str_to_pretty, Config};
40
41/// Basic Cedar CLI for evaluating authorization queries
42#[derive(Parser, Debug)]
43#[command(author, version, about, long_about = None)] // Pull from `Cargo.toml`
44pub struct Cli {
45    #[command(subcommand)]
46    pub command: Commands,
47    /// The output format to use for error reporting.
48    #[arg(
49        global = true,
50        short = 'f',
51        long = "error-format",
52        env = "CEDAR_ERROR_FORMAT",
53        default_value_t,
54        value_enum
55    )]
56    pub err_fmt: ErrorFormat,
57}
58
59#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
60pub enum ErrorFormat {
61    /// Human-readable error messages with terminal graphics and inline code
62    /// snippets.
63    #[default]
64    Human,
65    /// Plain-text error messages without fancy graphics or colors, suitable for
66    /// screen readers.
67    Plain,
68    /// Machine-readable JSON output.
69    Json,
70}
71
72impl Display for ErrorFormat {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(
75            f,
76            "{}",
77            match self {
78                ErrorFormat::Human => "human",
79                ErrorFormat::Plain => "plain",
80                ErrorFormat::Json => "json",
81            }
82        )
83    }
84}
85
86#[derive(Subcommand, Debug)]
87pub enum Commands {
88    /// Evaluate an authorization request
89    Authorize(AuthorizeArgs),
90    /// Evaluate a Cedar expression
91    Evaluate(EvaluateArgs),
92    /// Validate a policy set against a schema
93    Validate(ValidateArgs),
94    /// Check that policies, schema, and/or entities successfully parse.
95    /// (All arguments are optional; this checks that whatever is provided parses)
96    ///
97    /// If no arguments are provided, reads policies from stdin and checks that they parse.
98    CheckParse(CheckParseArgs),
99    /// Link a template
100    Link(LinkArgs),
101    /// Format a policy set
102    Format(FormatArgs),
103    /// Translate Cedar policy syntax to JSON policy syntax (except comments)
104    TranslatePolicy(TranslatePolicyArgs),
105    /// Translate Cedar schema syntax to JSON schema syntax and vice versa (except comments)
106    TranslateSchema(TranslateSchemaArgs),
107    /// Visualize a set of JSON entities to the graphviz format.
108    /// Warning: Entity visualization is best-effort and not well tested.
109    Visualize(VisualizeArgs),
110    /// Create a Cedar project
111    New(NewArgs),
112    /// Partially evaluate an authorization request
113    PartiallyAuthorize(PartiallyAuthorizeArgs),
114    /// Partially evaluate an authorization request in a type-aware manner
115    Tpe(TpeArgs),
116    /// Run test cases on a policy set
117    RunTests(RunTestsArgs),
118    /// Print Cedar language version
119    LanguageVersion,
120}
121
122#[derive(Args, Debug)]
123pub struct TranslatePolicyArgs {
124    /// The direction of translation,
125    #[arg(long)]
126    pub direction: PolicyTranslationDirection,
127    /// Filename to read the policies from.
128    /// If not provided, will default to reading stdin.
129    #[arg(short = 'p', long = "policies", value_name = "FILE")]
130    pub input_file: Option<String>,
131}
132
133/// The direction of translation
134#[derive(Debug, Clone, Copy, ValueEnum)]
135pub enum PolicyTranslationDirection {
136    /// Cedar policy syntax -> JSON
137    CedarToJson,
138    /// JSON -> Cedar policy syntax
139    JsonToCedar,
140}
141
142#[derive(Args, Debug)]
143pub struct TranslateSchemaArgs {
144    /// The direction of translation,
145    #[arg(long)]
146    pub direction: SchemaTranslationDirection,
147    /// Filename to read the schema from.
148    /// If not provided, will default to reading stdin.
149    #[arg(short = 's', long = "schema", value_name = "FILE")]
150    pub input_file: Option<String>,
151}
152
153/// The direction of translation
154#[derive(Debug, Clone, Copy, ValueEnum)]
155pub enum SchemaTranslationDirection {
156    /// JSON -> Cedar schema syntax
157    JsonToCedar,
158    /// Cedar schema syntax -> JSON
159    CedarToJson,
160}
161
162#[derive(Debug, Default, Clone, Copy, ValueEnum)]
163pub enum SchemaFormat {
164    /// the Cedar format
165    #[default]
166    Cedar,
167    /// JSON format
168    Json,
169}
170
171#[derive(Debug, Clone, Copy, ValueEnum)]
172pub enum ValidationMode {
173    /// Strict validation
174    Strict,
175    /// Permissive validation
176    Permissive,
177    /// Partial validation
178    Partial,
179}
180
181#[derive(Args, Debug)]
182pub struct ValidateArgs {
183    /// Schema args (incorporated by reference)
184    #[command(flatten)]
185    pub schema: SchemaArgs,
186    /// Policies args (incorporated by reference)
187    #[command(flatten)]
188    pub policies: PoliciesArgs,
189    /// Report a validation failure for non-fatal warnings
190    #[arg(long)]
191    pub deny_warnings: bool,
192    /// Validate the policy using this mode.
193    /// The options `permissive` and `partial` are experimental
194    /// and will cause the CLI to exit if it was not built with the
195    /// experimental feature `permissive-validate` and `partial-validate`, respectively, enabled.
196    #[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
197    pub validation_mode: ValidationMode,
198    /// Validate the policy at this level.
199    #[arg(long)]
200    pub level: Option<u32>,
201}
202
203#[derive(Args, Debug)]
204pub struct CheckParseArgs {
205    /// Policies args (incorporated by reference)
206    #[command(flatten)]
207    pub policies: OptionalPoliciesArgs,
208    /// Schema args (incorporated by reference)
209    #[command(flatten)]
210    pub schema: OptionalSchemaArgs,
211    /// File containing JSON representation of a Cedar entity hierarchy
212    #[arg(long = "entities", value_name = "FILE")]
213    pub entities_file: Option<PathBuf>,
214}
215
216/// This struct contains the arguments that together specify a request.
217#[derive(Args, Debug)]
218pub struct RequestArgs {
219    /// Principal for the request, e.g., User::"alice"
220    #[arg(short = 'l', long)]
221    pub principal: Option<String>,
222    /// Action for the request, e.g., Action::"view"
223    #[arg(short, long)]
224    pub action: Option<String>,
225    /// Resource for the request, e.g., File::"myfile.txt"
226    #[arg(short, long)]
227    pub resource: Option<String>,
228    /// File containing a JSON object representing the context for the request.
229    /// Should be a (possibly empty) map from keys to values.
230    #[arg(short, long = "context", value_name = "FILE")]
231    pub context_json_file: Option<String>,
232    /// File containing a JSON object representing the entire request. Must have
233    /// fields "principal", "action", "resource", and "context", where "context"
234    /// is a (possibly empty) map from keys to values. This option replaces
235    /// --principal, --action, etc.
236    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
237    pub request_json_file: Option<String>,
238    /// Whether to enable request validation. This has no effect if a schema is
239    /// not provided.
240    #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
241    pub request_validation: bool,
242}
243
244#[cfg(feature = "tpe")]
245/// This struct contains the arguments that together specify a request.
246#[derive(Args, Debug)]
247pub struct TpeRequestArgs {
248    /// Principal type of the request, e.g., User
249    #[arg(long)]
250    pub principal_type: Option<String>,
251    /// Optional principal eid
252    #[arg(long)]
253    pub principal_eid: Option<String>,
254    /// Action for the request, e.g., Action::"view"
255    #[arg(short, long)]
256    pub action: Option<String>,
257    /// Resource type of the request, e.g., File
258    #[arg(long)]
259    pub resource_type: Option<String>,
260    /// Optional resource eid
261    #[arg(long)]
262    pub resource_eid: Option<String>,
263    /// File containing a JSON object representing the context for the request.
264    /// Should be a (possibly empty) map from keys to values.
265    #[arg(short, long = "context", value_name = "FILE")]
266    pub context_json_file: Option<String>,
267    /// File containing a JSON object representing the entire request. Must have
268    /// fields "principal", "action", "resource", and "context", where "context"
269    /// is a (possibly empty) map from keys to values. This option replaces
270    /// --principal*, --action, etc.
271    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal_type", "principal_eid", "action", "resource_type", "resource_eid", "context_json_file"])]
272    pub request_json_file: Option<String>,
273}
274
275#[cfg(feature = "partial-eval")]
276/// This struct contains the arguments that together specify a request.
277#[derive(Args, Debug)]
278pub struct PartialRequestArgs {
279    /// Principal for the request, e.g., User::"alice"
280    #[arg(short = 'l', long)]
281    pub principal: Option<String>,
282    /// Action for the request, e.g., Action::"view"
283    #[arg(short, long)]
284    pub action: Option<String>,
285    /// Resource for the request, e.g., File::"myfile.txt"
286    #[arg(short, long)]
287    pub resource: Option<String>,
288    /// File containing a JSON object representing the context for the request.
289    /// Should be a (possibly empty) map from keys to values.
290    #[arg(short, long = "context", value_name = "FILE")]
291    pub context_json_file: Option<String>,
292    /// File containing a JSON object representing the entire request. Must have
293    /// fields "principal", "action", "resource", and "context", where "context"
294    /// is a (possibly empty) map from keys to values. This option replaces
295    /// --principal, --action, etc.
296    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
297    pub request_json_file: Option<String>,
298}
299
300impl RequestArgs {
301    /// Turn this `RequestArgs` into the appropriate `Request` object
302    ///
303    /// `schema` will be used for schema-based parsing of the context, and also
304    /// (if `self.request_validation` is `true`) for request validation.
305    ///
306    /// `self.request_validation` has no effect if `schema` is `None`.
307    fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
308        match &self.request_json_file {
309            Some(jsonfile) => {
310                let jsonstring = std::fs::read_to_string(jsonfile)
311                    .into_diagnostic()
312                    .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
313                let qjson: RequestJSON = serde_json::from_str(&jsonstring)
314                    .into_diagnostic()
315                    .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
316                let principal = qjson.principal.parse().wrap_err_with(|| {
317                    format!("failed to parse principal in {jsonfile} as entity Uid")
318                })?;
319                let action = qjson.action.parse().wrap_err_with(|| {
320                    format!("failed to parse action in {jsonfile} as entity Uid")
321                })?;
322                let resource = qjson.resource.parse().wrap_err_with(|| {
323                    format!("failed to parse resource in {jsonfile} as entity Uid")
324                })?;
325                let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
326                    .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
327                Request::new(
328                    principal,
329                    action,
330                    resource,
331                    context,
332                    if self.request_validation {
333                        schema
334                    } else {
335                        None
336                    },
337                )
338                .map_err(|e| miette!("{e}"))
339            }
340            None => {
341                let principal = self
342                    .principal
343                    .as_ref()
344                    .map(|s| {
345                        s.parse().wrap_err_with(|| {
346                            format!("failed to parse principal {s} as entity Uid")
347                        })
348                    })
349                    .transpose()?;
350                let action = self
351                    .action
352                    .as_ref()
353                    .map(|s| {
354                        s.parse()
355                            .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
356                    })
357                    .transpose()?;
358                let resource = self
359                    .resource
360                    .as_ref()
361                    .map(|s| {
362                        s.parse()
363                            .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
364                    })
365                    .transpose()?;
366                let context: Context = match &self.context_json_file {
367                    None => Context::empty(),
368                    Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
369                        Ok(f) => Context::from_json_file(
370                            f,
371                            schema.and_then(|s| Some((s, action.as_ref()?))),
372                        )
373                        .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
374                        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
375                            format!("error while loading context from {jsonfile}")
376                        })?,
377                    },
378                };
379                match (principal, action, resource) {
380                    (Some(principal), Some(action), Some(resource)) => Request::new(
381                        principal,
382                        action,
383                        resource,
384                        context,
385                        if self.request_validation {
386                            schema
387                        } else {
388                            None
389                        },
390                    )
391                    .map_err(|e| miette!("{e}")),
392                    _ => Err(miette!(
393                        "All three (`principal`, `action`, `resource`) variables must be specified"
394                    )),
395                }
396            }
397        }
398    }
399}
400
401#[cfg(feature = "tpe")]
402impl TpeRequestArgs {
403    fn get_request(&self, schema: &Schema) -> Result<PartialRequest> {
404        let qjson: TpeRequestJSON = match self.request_json_file.as_ref() {
405            Some(jsonfile) => {
406                let jsonstring = std::fs::read_to_string(jsonfile)
407                    .into_diagnostic()
408                    .wrap_err_with(|| format!("failed to open request json file {jsonfile}"))?;
409                serde_json::from_str(&jsonstring)
410                    .into_diagnostic()
411                    .wrap_err_with(|| format!("failed to parse context-json file {jsonfile}"))?
412            }
413            None => TpeRequestJSON {
414                principal_type: self
415                    .principal_type
416                    .clone()
417                    .ok_or_else(|| miette!("principal type must be specified"))?,
418                principal_eid: self.principal_eid.clone(),
419                action: self
420                    .action
421                    .clone()
422                    .ok_or_else(|| miette!("action must be specified"))?,
423                resource_type: self
424                    .resource_type
425                    .clone()
426                    .ok_or_else(|| miette!("resource type must be specified"))?,
427                resource_eid: self.resource_eid.clone(),
428                context: self
429                    .context_json_file
430                    .as_ref()
431                    .map(|jsonfile| {
432                        let jsonstring = std::fs::read_to_string(jsonfile)
433                            .into_diagnostic()
434                            .wrap_err_with(|| {
435                                format!("failed to open context-json file {jsonfile}")
436                            })?;
437                        serde_json::from_str(&jsonstring)
438                            .into_diagnostic()
439                            .wrap_err_with(|| {
440                                format!("failed to parse context-json file {jsonfile}")
441                            })
442                    })
443                    .transpose()?,
444            },
445        };
446        let action: EntityUid = qjson.action.parse()?;
447        Ok(PartialRequest::new(
448            PartialEntityUid::new(
449                qjson.principal_type.parse()?,
450                qjson.principal_eid.as_ref().map(EntityId::new),
451            ),
452            action.clone(),
453            PartialEntityUid::new(
454                qjson.resource_type.parse()?,
455                qjson.resource_eid.as_ref().map(EntityId::new),
456            ),
457            qjson
458                .context
459                .map(|val| Context::from_json_value(val, Some((schema, &action))))
460                .transpose()?,
461            schema,
462        )?)
463    }
464}
465
466#[cfg(feature = "partial-eval")]
467impl PartialRequestArgs {
468    fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
469        let mut builder = RequestBuilder::default();
470        let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
471            Some(jsonfile) => {
472                let jsonstring = std::fs::read_to_string(jsonfile)
473                    .into_diagnostic()
474                    .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
475                serde_json::from_str(&jsonstring)
476                    .into_diagnostic()
477                    .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
478            }
479            None => PartialRequestJSON {
480                principal: self.principal.clone(),
481                action: self.action.clone(),
482                resource: self.resource.clone(),
483                context: self
484                    .context_json_file
485                    .as_ref()
486                    .map(|jsonfile| {
487                        let jsonstring = std::fs::read_to_string(jsonfile)
488                            .into_diagnostic()
489                            .wrap_err_with(|| {
490                                format!("failed to open context-json file {jsonfile}")
491                            })?;
492                        serde_json::from_str(&jsonstring)
493                            .into_diagnostic()
494                            .wrap_err_with(|| {
495                                format!("failed to parse context-json file {jsonfile}")
496                            })
497                    })
498                    .transpose()?,
499            },
500        };
501
502        if let Some(principal) = qjson
503            .principal
504            .map(|s| {
505                s.parse()
506                    .wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
507            })
508            .transpose()?
509        {
510            builder = builder.principal(principal);
511        }
512
513        let action = qjson
514            .action
515            .map(|s| {
516                s.parse::<EntityUid>()
517                    .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
518            })
519            .transpose()?;
520
521        if let Some(action_ref) = &action {
522            builder = builder.action(action_ref.clone());
523        }
524
525        if let Some(resource) = qjson
526            .resource
527            .map(|s| {
528                s.parse()
529                    .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
530            })
531            .transpose()?
532        {
533            builder = builder.resource(resource);
534        }
535
536        if let Some(context) = qjson
537            .context
538            .map(|json| {
539                Context::from_json_value(
540                    json.clone(),
541                    schema.and_then(|s| Some((s, action.as_ref()?))),
542                )
543                .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
544            })
545            .transpose()?
546        {
547            builder = builder.context(context);
548        }
549
550        if let Some(schema) = schema {
551            builder
552                .schema(schema)
553                .build()
554                .wrap_err_with(|| "failed to build request with validation".to_string())
555        } else {
556            Ok(builder.build())
557        }
558    }
559}
560
561/// This struct contains the arguments that together specify an input policy or policy set.
562#[derive(Args, Debug)]
563pub struct PoliciesArgs {
564    /// File containing the static Cedar policies and/or templates. If not provided, read policies from stdin.
565    #[arg(short, long = "policies", value_name = "FILE")]
566    pub policies_file: Option<String>,
567    /// Format of policies in the `--policies` file
568    #[arg(long = "policy-format", default_value_t, value_enum)]
569    pub policy_format: PolicyFormat,
570    /// File containing template-linked policies
571    #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
572    pub template_linked_file: Option<String>,
573}
574
575impl PoliciesArgs {
576    /// Turn this `PoliciesArgs` into the appropriate `PolicySet` object
577    fn get_policy_set(&self) -> Result<PolicySet> {
578        let mut pset = match self.policy_format {
579            PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
580            PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
581        }?;
582        if let Some(links_filename) = self.template_linked_file.as_ref() {
583            add_template_links_to_set(links_filename, &mut pset)?;
584        }
585        Ok(pset)
586    }
587}
588
589/// This struct contains the arguments that together specify an input policy or policy set,
590/// for commands where policies are optional.
591#[derive(Args, Debug)]
592pub struct OptionalPoliciesArgs {
593    /// File containing static Cedar policies and/or templates
594    #[arg(short, long = "policies", value_name = "FILE")]
595    pub policies_file: Option<String>,
596    /// Format of policies in the `--policies` file
597    #[arg(long = "policy-format", default_value_t, value_enum)]
598    pub policy_format: PolicyFormat,
599    /// File containing template-linked policies. Ignored if `--policies` is not
600    /// present (because in that case there are no templates to link against)
601    #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
602    pub template_linked_file: Option<String>,
603}
604
605impl OptionalPoliciesArgs {
606    /// Turn this `OptionalPoliciesArgs` into the appropriate `PolicySet`
607    /// object, or `None` if no policies were provided
608    fn get_policy_set(&self) -> Result<Option<PolicySet>> {
609        match &self.policies_file {
610            None => Ok(None),
611            Some(policies_file) => {
612                let pargs = PoliciesArgs {
613                    policies_file: Some(policies_file.clone()),
614                    policy_format: self.policy_format,
615                    template_linked_file: self.template_linked_file.clone(),
616                };
617                pargs.get_policy_set().map(Some)
618            }
619        }
620    }
621}
622
623/// This struct contains the arguments that together specify an input schema.
624#[derive(Args, Debug)]
625pub struct SchemaArgs {
626    /// File containing the schema
627    #[arg(short, long = "schema", value_name = "FILE")]
628    pub schema_file: PathBuf,
629    /// Schema format
630    #[arg(long, value_enum, default_value_t)]
631    pub schema_format: SchemaFormat,
632}
633
634impl SchemaArgs {
635    /// Turn this `SchemaArgs` into the appropriate `Schema` object
636    fn get_schema(&self) -> Result<Schema> {
637        read_schema_from_file(&self.schema_file, self.schema_format)
638    }
639}
640
641/// This struct contains the arguments that together specify an input schema,
642/// for commands where the schema is optional.
643#[derive(Args, Debug)]
644pub struct OptionalSchemaArgs {
645    /// File containing the schema
646    #[arg(short, long = "schema", value_name = "FILE")]
647    pub schema_file: Option<PathBuf>,
648    /// Schema format
649    #[arg(long, value_enum, default_value_t)]
650    pub schema_format: SchemaFormat,
651}
652
653impl OptionalSchemaArgs {
654    /// Turn this `OptionalSchemaArgs` into the appropriate `Schema` object, or `None`
655    fn get_schema(&self) -> Result<Option<Schema>> {
656        let Some(schema_file) = &self.schema_file else {
657            return Ok(None);
658        };
659        read_schema_from_file(schema_file, self.schema_format).map(Some)
660    }
661}
662
663fn read_schema_from_file(path: impl AsRef<Path>, format: SchemaFormat) -> Result<Schema> {
664    let path = path.as_ref();
665    let schema_src = read_from_file(path, "schema")?;
666    match format {
667        SchemaFormat::Json => Schema::from_json_str(&schema_src)
668            .wrap_err_with(|| format!("failed to parse schema from file {}", path.display())),
669        SchemaFormat::Cedar => {
670            let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)
671                .wrap_err_with(|| format!("failed to parse schema from file {}", path.display()))?;
672            for warning in warnings {
673                let report = miette::Report::new(warning);
674                eprintln!("{report:?}");
675            }
676            Ok(schema)
677        }
678    }
679}
680
681#[derive(Args, Debug)]
682pub struct AuthorizeArgs {
683    /// Request args (incorporated by reference)
684    #[command(flatten)]
685    pub request: RequestArgs,
686    /// Policies args (incorporated by reference)
687    #[command(flatten)]
688    pub policies: PoliciesArgs,
689    /// Schema args (incorporated by reference)
690    ///
691    /// Used to populate the store with action entities and for schema-based
692    /// parsing of entity hierarchy, if present
693    #[command(flatten)]
694    pub schema: OptionalSchemaArgs,
695    /// File containing JSON representation of the Cedar entity hierarchy
696    #[arg(long = "entities", value_name = "FILE")]
697    pub entities_file: String,
698    /// More verbose output. (For instance, indicate which policies applied to the request, if any.)
699    #[arg(short, long)]
700    pub verbose: bool,
701    /// Time authorization and report timing information
702    #[arg(short, long)]
703    pub timing: bool,
704}
705
706#[cfg(feature = "tpe")]
707#[derive(Args, Debug)]
708pub struct TpeArgs {
709    /// Request args (incorporated by reference)
710    #[command(flatten)]
711    pub request: TpeRequestArgs,
712    /// Policies args (incorporated by reference)
713    #[command(flatten)]
714    pub policies: PoliciesArgs,
715    /// Schema args (incorporated by reference)
716    ///
717    /// Used to populate the store with action entities and for schema-based
718    /// parsing of entity hierarchy, if present
719    #[command(flatten)]
720    pub schema: SchemaArgs,
721    /// File containing JSON representation of the Cedar entity hierarchy
722    #[arg(long = "entities", value_name = "FILE")]
723    pub entities_file: String,
724    /// Time authorization and report timing information
725    #[arg(short, long)]
726    pub timing: bool,
727}
728
729#[cfg(feature = "partial-eval")]
730#[derive(Args, Debug)]
731pub struct PartiallyAuthorizeArgs {
732    /// Request args (incorporated by reference)
733    #[command(flatten)]
734    pub request: PartialRequestArgs,
735    /// Policies args (incorporated by reference)
736    #[command(flatten)]
737    pub policies: PoliciesArgs,
738    /// Schema args (incorporated by reference)
739    ///
740    /// Used to populate the store with action entities and for schema-based
741    /// parsing of entity hierarchy, if present
742    #[command(flatten)]
743    pub schema: OptionalSchemaArgs,
744    /// File containing JSON representation of the Cedar entity hierarchy
745    #[arg(long = "entities", value_name = "FILE")]
746    pub entities_file: String,
747    /// Time authorization and report timing information
748    #[arg(short, long)]
749    pub timing: bool,
750}
751
752#[cfg(not(feature = "tpe"))]
753#[derive(Debug, Args)]
754pub struct TpeArgs;
755
756#[cfg(not(feature = "partial-eval"))]
757#[derive(Debug, Args)]
758pub struct PartiallyAuthorizeArgs;
759
760#[derive(Args, Debug)]
761pub struct RunTestsArgs {
762    /// Policies args (incorporated by reference)
763    #[command(flatten)]
764    pub policies: PoliciesArgs,
765    /// Tests in JSON format
766    #[arg(long, value_name = "FILE")]
767    pub tests: String,
768}
769
770#[derive(Args, Debug)]
771pub struct VisualizeArgs {
772    #[arg(long = "entities", value_name = "FILE")]
773    pub entities_file: String,
774}
775
776#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
777pub enum PolicyFormat {
778    /// The standard Cedar policy format, documented at <https://docs.cedarpolicy.com/policies/syntax-policy.html>
779    #[default]
780    Cedar,
781    /// Cedar's JSON policy format, documented at <https://docs.cedarpolicy.com/policies/json-format.html>
782    Json,
783}
784
785#[derive(Args, Debug)]
786pub struct LinkArgs {
787    /// Policies args (incorporated by reference)
788    #[command(flatten)]
789    pub policies: PoliciesArgs,
790    /// Id of the template to link
791    #[arg(long)]
792    pub template_id: String,
793    /// Id for the new template linked policy
794    #[arg(short, long)]
795    pub new_id: String,
796    /// Arguments to fill slots
797    #[arg(short, long)]
798    pub arguments: Arguments,
799}
800
801#[derive(Args, Debug)]
802pub struct FormatArgs {
803    /// File containing the static Cedar policies and/or templates. If not provided, read policies from stdin.
804    #[arg(short, long = "policies", value_name = "FILE")]
805    pub policies_file: Option<String>,
806
807    /// Custom line width (default: 80).
808    #[arg(short, long, value_name = "UINT", default_value_t = 80)]
809    pub line_width: usize,
810
811    /// Custom indentation width (default: 2).
812    #[arg(short, long, value_name = "INT", default_value_t = 2)]
813    pub indent_width: isize,
814
815    /// Automatically write back the formatted policies to the input file.
816    #[arg(short, long, group = "action", requires = "policies_file")]
817    pub write: bool,
818
819    /// Check that the policies formats without any changes. Mutually exclusive with `write`.
820    #[arg(short, long, group = "action")]
821    pub check: bool,
822}
823
824#[derive(Args, Debug)]
825pub struct NewArgs {
826    /// Name of the Cedar project
827    #[arg(short, long, value_name = "DIR")]
828    pub name: String,
829}
830
831/// Wrapper struct
832#[derive(Clone, Debug, Deserialize)]
833#[serde(try_from = "HashMap<String,String>")]
834pub struct Arguments {
835    pub data: HashMap<SlotId, String>,
836}
837
838impl TryFrom<HashMap<String, String>> for Arguments {
839    type Error = String;
840
841    fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
842        Ok(Self {
843            data: value
844                .into_iter()
845                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
846                .collect::<Result<HashMap<SlotId, String>, String>>()?,
847        })
848    }
849}
850
851impl FromStr for Arguments {
852    type Err = serde_json::Error;
853
854    fn from_str(s: &str) -> Result<Self, Self::Err> {
855        serde_json::from_str(s)
856    }
857}
858
859/// This struct is the serde structure expected for --request-json
860#[derive(Clone, Debug, Deserialize)]
861struct RequestJSON {
862    /// Principal for the request
863    #[serde(default)]
864    principal: String,
865    /// Action for the request
866    #[serde(default)]
867    action: String,
868    /// Resource for the request
869    #[serde(default)]
870    resource: String,
871    /// Context for the request
872    context: serde_json::Value,
873}
874
875#[cfg(feature = "partial-eval")]
876/// This struct is the serde structure expected for --request-json
877#[derive(Deserialize)]
878struct PartialRequestJSON {
879    /// Principal for the request
880    pub(self) principal: Option<String>,
881    /// Action for the request
882    pub(self) action: Option<String>,
883    /// Resource for the request
884    pub(self) resource: Option<String>,
885    /// Context for the request
886    pub(self) context: Option<serde_json::Value>,
887}
888
889#[cfg(feature = "tpe")]
890// This struct is the serde structure expected for --request-json
891#[derive(Deserialize)]
892struct TpeRequestJSON {
893    // Principal for the request
894    pub(self) principal_type: String,
895    // Optional principal eid
896    pub(self) principal_eid: Option<String>,
897    // Action for the request
898    pub(self) action: String,
899    // Resource for the request
900    pub(self) resource_type: String,
901    // Optional resource eid
902    pub(self) resource_eid: Option<String>,
903    // Context for the request
904    pub(self) context: Option<serde_json::Value>,
905}
906
907#[derive(Args, Debug)]
908pub struct EvaluateArgs {
909    /// Request args (incorporated by reference)
910    #[command(flatten)]
911    pub request: RequestArgs,
912    /// Schema args (incorporated by reference)
913    ///
914    /// Used to populate the store with action entities and for schema-based
915    /// parsing of entity hierarchy, if present
916    #[command(flatten)]
917    pub schema: OptionalSchemaArgs,
918    /// File containing JSON representation of the Cedar entity hierarchy.
919    /// This is optional; if not present, we'll just use an empty hierarchy.
920    #[arg(long = "entities", value_name = "FILE")]
921    pub entities_file: Option<String>,
922    /// Expression to evaluate
923    #[arg(value_name = "EXPRESSION")]
924    pub expression: String,
925}
926
927#[derive(Eq, PartialEq, Debug, Copy, Clone)]
928pub enum CedarExitCode {
929    // The command completed successfully with a result other than a
930    // authorization deny or validation failure.
931    Success,
932    // The command failed to complete successfully.
933    Failure,
934    // The command completed successfully, but the result of the authorization
935    // request was DENY.
936    AuthorizeDeny,
937    // The command completed successfully, but it detected a validation failure
938    // in the given schema and policies.
939    ValidationFailure,
940    #[cfg(any(feature = "partial-eval", feature = "tpe"))]
941    // The command completed successfully with an incomplete result, e.g.,
942    // partial authorization result is not determining.
943    Unknown,
944}
945
946impl Termination for CedarExitCode {
947    fn report(self) -> ExitCode {
948        match self {
949            CedarExitCode::Success => ExitCode::SUCCESS,
950            CedarExitCode::Failure => ExitCode::FAILURE,
951            CedarExitCode::AuthorizeDeny => ExitCode::from(2),
952            CedarExitCode::ValidationFailure => ExitCode::from(3),
953            #[cfg(any(feature = "partial-eval", feature = "tpe"))]
954            CedarExitCode::Unknown => ExitCode::SUCCESS,
955        }
956    }
957}
958
959pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
960    // for backwards compatibility: if no policies/schema/entities are provided,
961    // read policies from stdin and check that they parse
962    if (
963        &args.policies.policies_file,
964        &args.schema.schema_file,
965        &args.entities_file,
966    ) == (&None, &None, &None)
967    {
968        let pargs = PoliciesArgs {
969            policies_file: None, // read from stdin
970            policy_format: args.policies.policy_format,
971            template_linked_file: args.policies.template_linked_file.clone(),
972        };
973        match pargs.get_policy_set() {
974            Ok(_) => return CedarExitCode::Success,
975            Err(e) => {
976                println!("{e:?}");
977                return CedarExitCode::Failure;
978            }
979        }
980    }
981
982    let mut exit_code = CedarExitCode::Success;
983    match args.policies.get_policy_set() {
984        Ok(_) => (),
985        Err(e) => {
986            println!("{e:?}");
987            exit_code = CedarExitCode::Failure;
988        }
989    }
990    let schema = match args.schema.get_schema() {
991        Ok(schema) => schema,
992        Err(e) => {
993            println!("{e:?}");
994            exit_code = CedarExitCode::Failure;
995            None
996        }
997    };
998    match &args.entities_file {
999        None => (),
1000        Some(efile) => match load_entities(efile, schema.as_ref()) {
1001            Ok(_) => (),
1002            Err(e) => {
1003                println!("{e:?}");
1004                exit_code = CedarExitCode::Failure;
1005            }
1006        },
1007    }
1008    exit_code
1009}
1010
1011pub fn validate(args: &ValidateArgs) -> CedarExitCode {
1012    let mode = match args.validation_mode {
1013        ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
1014        ValidationMode::Permissive => {
1015            #[cfg(not(feature = "permissive-validate"))]
1016            {
1017                eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
1018                return CedarExitCode::Failure;
1019            }
1020            #[cfg(feature = "permissive-validate")]
1021            cedar_policy::ValidationMode::Permissive
1022        }
1023        ValidationMode::Partial => {
1024            #[cfg(not(feature = "partial-validate"))]
1025            {
1026                eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
1027                return CedarExitCode::Failure;
1028            }
1029            #[cfg(feature = "partial-validate")]
1030            cedar_policy::ValidationMode::Partial
1031        }
1032    };
1033
1034    let pset = match args.policies.get_policy_set() {
1035        Ok(pset) => pset,
1036        Err(e) => {
1037            println!("{e:?}");
1038            return CedarExitCode::Failure;
1039        }
1040    };
1041
1042    let schema = match args.schema.get_schema() {
1043        Ok(schema) => schema,
1044        Err(e) => {
1045            println!("{e:?}");
1046            return CedarExitCode::Failure;
1047        }
1048    };
1049
1050    let validator = Validator::new(schema);
1051
1052    let result = if let Some(level) = args.level {
1053        validator.validate_with_level(&pset, mode, level)
1054    } else {
1055        validator.validate(&pset, mode)
1056    };
1057
1058    if !result.validation_passed()
1059        || (args.deny_warnings && !result.validation_passed_without_warnings())
1060    {
1061        println!(
1062            "{:?}",
1063            Report::new(result).wrap_err("policy set validation failed")
1064        );
1065        CedarExitCode::ValidationFailure
1066    } else {
1067        println!(
1068            "{:?}",
1069            Report::new(result).wrap_err("policy set validation passed")
1070        );
1071        CedarExitCode::Success
1072    }
1073}
1074
1075pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
1076    println!();
1077    let schema = match args.schema.get_schema() {
1078        Ok(opt) => opt,
1079        Err(e) => {
1080            println!("{e:?}");
1081            return (CedarExitCode::Failure, EvalResult::Bool(false));
1082        }
1083    };
1084    let request = match args.request.get_request(schema.as_ref()) {
1085        Ok(q) => q,
1086        Err(e) => {
1087            println!("{e:?}");
1088            return (CedarExitCode::Failure, EvalResult::Bool(false));
1089        }
1090    };
1091    let expr =
1092        match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
1093            Ok(expr) => expr,
1094            Err(e) => {
1095                println!("{:?}", e.with_source_code(args.expression.clone()));
1096                return (CedarExitCode::Failure, EvalResult::Bool(false));
1097            }
1098        };
1099    let entities = match &args.entities_file {
1100        None => Entities::empty(),
1101        Some(file) => match load_entities(file, schema.as_ref()) {
1102            Ok(entities) => entities,
1103            Err(e) => {
1104                println!("{e:?}");
1105                return (CedarExitCode::Failure, EvalResult::Bool(false));
1106            }
1107        },
1108    };
1109    match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
1110    {
1111        Err(e) => {
1112            println!("{e:?}");
1113            return (CedarExitCode::Failure, EvalResult::Bool(false));
1114        }
1115        Ok(result) => {
1116            println!("{result}");
1117            return (CedarExitCode::Success, result);
1118        }
1119    }
1120}
1121
1122pub fn link(args: &LinkArgs) -> CedarExitCode {
1123    if let Err(err) = link_inner(args) {
1124        println!("{err:?}");
1125        CedarExitCode::Failure
1126    } else {
1127        CedarExitCode::Success
1128    }
1129}
1130
1131pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
1132    match load_entities(&args.entities_file, None) {
1133        Ok(entities) => {
1134            println!("{}", entities.to_dot_str());
1135            CedarExitCode::Success
1136        }
1137        Err(report) => {
1138            eprintln!("{report:?}");
1139            CedarExitCode::Failure
1140        }
1141    }
1142}
1143
1144/// Format the policies in the given file or stdin.
1145///
1146/// Returns a boolean indicating whether the formatted policies are the same as the original
1147/// policies.
1148fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
1149    let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
1150    let config = Config {
1151        line_width: args.line_width,
1152        indent_width: args.indent_width,
1153    };
1154    let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
1155    let are_policies_equivalent = policies_str == formatted_policy;
1156
1157    match &args.policies_file {
1158        Some(policies_file) if args.write => {
1159            let mut file = OpenOptions::new()
1160                .write(true)
1161                .truncate(true)
1162                .open(policies_file)
1163                .into_diagnostic()
1164                .wrap_err(format!("failed to open {policies_file} for writing"))?;
1165            file.write_all(formatted_policy.as_bytes())
1166                .into_diagnostic()
1167                .wrap_err(format!(
1168                    "failed to write formatted policies to {policies_file}"
1169                ))?;
1170        }
1171        _ => print!("{formatted_policy}"),
1172    }
1173    Ok(are_policies_equivalent)
1174}
1175
1176pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
1177    match format_policies_inner(args) {
1178        Ok(false) if args.check => CedarExitCode::Failure,
1179        Err(err) => {
1180            println!("{err:?}");
1181            CedarExitCode::Failure
1182        }
1183        _ => CedarExitCode::Success,
1184    }
1185}
1186
1187fn translate_policy_to_cedar(
1188    json_src: Option<impl AsRef<Path> + std::marker::Copy>,
1189) -> Result<String> {
1190    let policy_set = read_json_policy_set(json_src)?;
1191    policy_set.to_cedar().ok_or_else(|| {
1192        miette!("Unable to translate policy set containing template linked policies.")
1193    })
1194}
1195
1196fn translate_policy_to_json(
1197    cedar_src: Option<impl AsRef<Path> + std::marker::Copy>,
1198) -> Result<String> {
1199    let policy_set = read_cedar_policy_set(cedar_src)?;
1200    let output = policy_set.to_json()?.to_string();
1201    Ok(output)
1202}
1203
1204fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
1205    let translate = match args.direction {
1206        PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
1207        PolicyTranslationDirection::JsonToCedar => translate_policy_to_cedar,
1208    };
1209    translate(args.input_file.as_ref())
1210}
1211
1212pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
1213    match translate_policy_inner(args) {
1214        Ok(sf) => {
1215            println!("{sf}");
1216            CedarExitCode::Success
1217        }
1218        Err(err) => {
1219            eprintln!("{err:?}");
1220            CedarExitCode::Failure
1221        }
1222    }
1223}
1224
1225fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
1226    let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
1227    let output = fragment.to_cedarschema()?;
1228    Ok(output)
1229}
1230
1231fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
1232    let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
1233    for warning in warnings {
1234        let report = miette::Report::new(warning);
1235        eprintln!("{report:?}");
1236    }
1237    let output = fragment.to_json_string()?;
1238    Ok(output)
1239}
1240
1241fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
1242    let translate = match args.direction {
1243        SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
1244        SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
1245    };
1246    read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate)
1247}
1248
1249pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
1250    match translate_schema_inner(args) {
1251        Ok(sf) => {
1252            println!("{sf}");
1253            CedarExitCode::Success
1254        }
1255        Err(err) => {
1256            eprintln!("{err:?}");
1257            CedarExitCode::Failure
1258        }
1259    }
1260}
1261
1262/// Write a schema (in JSON format) to `path`
1263fn generate_schema(path: &Path) -> Result<()> {
1264    std::fs::write(
1265        path,
1266        serde_json::to_string_pretty(&serde_json::json!(
1267        {
1268            "": {
1269                "entityTypes": {
1270                    "A": {
1271                        "memberOfTypes": [
1272                            "B"
1273                        ]
1274                    },
1275                    "B": {
1276                        "memberOfTypes": []
1277                    },
1278                    "C": {
1279                        "memberOfTypes": []
1280                    }
1281                },
1282                "actions": {
1283                    "action": {
1284                        "appliesTo": {
1285                            "resourceTypes": [
1286                                "C"
1287                            ],
1288                            "principalTypes": [
1289                                "A",
1290                                "B"
1291                            ]
1292                        }
1293                    }
1294                }
1295            }
1296        }))
1297        .into_diagnostic()?,
1298    )
1299    .into_diagnostic()
1300}
1301
1302fn generate_policy(path: &Path) -> Result<()> {
1303    std::fs::write(
1304        path,
1305        r#"permit (
1306  principal in A::"a",
1307  action == Action::"action",
1308  resource == C::"c"
1309) when { true };
1310"#,
1311    )
1312    .into_diagnostic()
1313}
1314
1315fn generate_entities(path: &Path) -> Result<()> {
1316    std::fs::write(
1317        path,
1318        serde_json::to_string_pretty(&serde_json::json!(
1319        [
1320            {
1321                "uid": { "type": "A", "id": "a"} ,
1322                "attrs": {},
1323                "parents": [{"type": "B", "id": "b"}]
1324            },
1325            {
1326                "uid": { "type": "B", "id": "b"} ,
1327                "attrs": {},
1328                "parents": []
1329            },
1330            {
1331                "uid": { "type": "C", "id": "c"} ,
1332                "attrs": {},
1333                "parents": []
1334            }
1335        ]))
1336        .into_diagnostic()?,
1337    )
1338    .into_diagnostic()
1339}
1340
1341fn new_inner(args: &NewArgs) -> Result<()> {
1342    let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1343    std::fs::create_dir(dir).into_diagnostic()?;
1344    let schema_path = dir.join("schema.cedarschema.json");
1345    let policy_path = dir.join("policy.cedar");
1346    let entities_path = dir.join("entities.json");
1347    generate_schema(&schema_path)?;
1348    generate_policy(&policy_path)?;
1349    generate_entities(&entities_path)
1350}
1351
1352pub fn new(args: &NewArgs) -> CedarExitCode {
1353    if let Err(err) = new_inner(args) {
1354        println!("{err:?}");
1355        CedarExitCode::Failure
1356    } else {
1357        CedarExitCode::Success
1358    }
1359}
1360
1361pub fn language_version() -> CedarExitCode {
1362    let version = get_lang_version();
1363    println!(
1364        "Cedar language version: {}.{}",
1365        version.major, version.minor
1366    );
1367    CedarExitCode::Success
1368}
1369
1370fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
1371    data.iter()
1372        .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
1373        .collect::<Result<HashMap<SlotId, EntityUid>>>()
1374}
1375
1376fn link_inner(args: &LinkArgs) -> Result<()> {
1377    let mut policies = args.policies.get_policy_set()?;
1378    let slotenv = create_slot_env(&args.arguments.data)?;
1379    policies.link(
1380        PolicyId::new(&args.template_id),
1381        PolicyId::new(&args.new_id),
1382        slotenv,
1383    )?;
1384    let linked = policies
1385        .policy(&PolicyId::new(&args.new_id))
1386        .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
1387    println!("Template-linked policy added: {linked}");
1388
1389    // If a `--template-linked` / `-k` option was provided, update that file with the new link
1390    if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
1391        update_template_linked_file(
1392            links_filename,
1393            TemplateLinked {
1394                template_id: args.template_id.clone(),
1395                link_id: args.new_id.clone(),
1396                args: args.arguments.data.clone(),
1397            },
1398        )?;
1399    }
1400
1401    Ok(())
1402}
1403
1404#[derive(Clone, Serialize, Deserialize, Debug)]
1405#[serde(try_from = "LiteralTemplateLinked")]
1406#[serde(into = "LiteralTemplateLinked")]
1407struct TemplateLinked {
1408    template_id: String,
1409    link_id: String,
1410    args: HashMap<SlotId, String>,
1411}
1412
1413impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
1414    type Error = String;
1415
1416    fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
1417        Ok(Self {
1418            template_id: value.template_id,
1419            link_id: value.link_id,
1420            args: value
1421                .args
1422                .into_iter()
1423                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1424                .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
1425        })
1426    }
1427}
1428
1429fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
1430    match s.as_ref() {
1431        "?principal" => Ok(SlotId::principal()),
1432        "?resource" => Ok(SlotId::resource()),
1433        _ => Err(format!(
1434            "Invalid SlotId! Expected ?principal|?resource, got: {}",
1435            s.as_ref()
1436        )),
1437    }
1438}
1439
1440#[derive(Serialize, Deserialize)]
1441struct LiteralTemplateLinked {
1442    template_id: String,
1443    link_id: String,
1444    args: HashMap<String, String>,
1445}
1446
1447impl From<TemplateLinked> for LiteralTemplateLinked {
1448    fn from(i: TemplateLinked) -> Self {
1449        Self {
1450            template_id: i.template_id,
1451            link_id: i.link_id,
1452            args: i
1453                .args
1454                .into_iter()
1455                .map(|(k, v)| (format!("{k}"), v))
1456                .collect(),
1457        }
1458    }
1459}
1460
1461/// Iterate over links in the template-linked file and add them to the set
1462fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
1463    for template_linked in load_links_from_file(path)? {
1464        let slot_env = create_slot_env(&template_linked.args)?;
1465        policy_set.link(
1466            PolicyId::new(&template_linked.template_id),
1467            PolicyId::new(&template_linked.link_id),
1468            slot_env,
1469        )?;
1470    }
1471    Ok(())
1472}
1473
1474/// Given a file containing template links, return a `Vec` of those links
1475fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
1476    let f = match std::fs::File::open(path) {
1477        Ok(f) => f,
1478        Err(_) => {
1479            // If the file doesn't exist, then give back the empty entity set
1480            return Ok(vec![]);
1481        }
1482    };
1483    if f.metadata()
1484        .into_diagnostic()
1485        .wrap_err("Failed to read metadata")?
1486        .len()
1487        == 0
1488    {
1489        // File is empty, return empty set
1490        Ok(vec![])
1491    } else {
1492        // File has contents, deserialize
1493        serde_json::from_reader(f)
1494            .into_diagnostic()
1495            .wrap_err("Deserialization error")
1496    }
1497}
1498
1499/// Add a single template-linked policy to the linked file
1500fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
1501    let mut template_linked = load_links_from_file(path.as_ref())?;
1502    template_linked.push(new_linked);
1503    write_template_linked_file(&template_linked, path.as_ref())
1504}
1505
1506/// Write a slice of template-linked policies to the linked file
1507fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
1508    let f = OpenOptions::new()
1509        .write(true)
1510        .truncate(true)
1511        .create(true)
1512        .open(path)
1513        .into_diagnostic()?;
1514    serde_json::to_writer(f, linked).into_diagnostic()
1515}
1516
1517pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
1518    println!();
1519    let ans = execute_request(
1520        &args.request,
1521        &args.policies,
1522        &args.entities_file,
1523        &args.schema,
1524        args.timing,
1525    );
1526    match ans {
1527        Ok(ans) => {
1528            let status = match ans.decision() {
1529                Decision::Allow => {
1530                    println!("ALLOW");
1531                    CedarExitCode::Success
1532                }
1533                Decision::Deny => {
1534                    println!("DENY");
1535                    CedarExitCode::AuthorizeDeny
1536                }
1537            };
1538            if ans.diagnostics().errors().peekable().peek().is_some() {
1539                println!();
1540                for err in ans.diagnostics().errors() {
1541                    println!("{err}");
1542                }
1543            }
1544            if args.verbose {
1545                println!();
1546                if ans.diagnostics().reason().peekable().peek().is_none() {
1547                    println!("note: no policies applied to this request");
1548                } else {
1549                    println!("note: this decision was due to the following policies:");
1550                    for reason in ans.diagnostics().reason() {
1551                        println!("  {reason}");
1552                    }
1553                    println!();
1554                }
1555            }
1556            status
1557        }
1558        Err(errs) => {
1559            for err in errs {
1560                println!("{err:?}");
1561            }
1562            CedarExitCode::Failure
1563        }
1564    }
1565}
1566
1567#[cfg(not(feature = "partial-eval"))]
1568pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
1569    {
1570        eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1571        return CedarExitCode::Failure;
1572    }
1573}
1574
1575#[cfg(not(feature = "tpe"))]
1576pub fn tpe(_: &TpeArgs) -> CedarExitCode {
1577    {
1578        eprintln!("Error: option `tpe` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1579        return CedarExitCode::Failure;
1580    }
1581}
1582
1583#[cfg(feature = "tpe")]
1584pub fn tpe(args: &TpeArgs) -> CedarExitCode {
1585    println!();
1586    let ret = |errs| {
1587        for err in errs {
1588            println!("{err:?}");
1589        }
1590        CedarExitCode::Failure
1591    };
1592    let mut errs = vec![];
1593    let policies = match args.policies.get_policy_set() {
1594        Ok(pset) => pset,
1595        Err(e) => {
1596            errs.push(e);
1597            PolicySet::new()
1598        }
1599    };
1600    let schema: Schema = match args.schema.get_schema() {
1601        Ok(opt) => opt,
1602        Err(e) => {
1603            errs.push(e);
1604            return ret(errs);
1605        }
1606    };
1607
1608    let entities = match load_partial_entities(args.entities_file.clone(), &schema) {
1609        Ok(entities) => entities,
1610        Err(e) => {
1611            errs.push(e);
1612            PartialEntities::empty()
1613        }
1614    };
1615
1616    match args.request.get_request(&schema) {
1617        Ok(request) if errs.is_empty() => {
1618            let auth_start = Instant::now();
1619            let ans = policies.tpe(&request, &entities, &schema);
1620            let auth_dur = auth_start.elapsed();
1621            match ans {
1622                Ok(ans) => {
1623                    if args.timing {
1624                        println!(
1625                            "Authorization Time (micro seconds) : {}",
1626                            auth_dur.as_micros()
1627                        );
1628                    }
1629                    match ans.decision() {
1630                        Some(Decision::Allow) => {
1631                            println!("ALLOW");
1632                            CedarExitCode::Success
1633                        }
1634                        Some(Decision::Deny) => {
1635                            println!("DENY");
1636                            CedarExitCode::AuthorizeDeny
1637                        }
1638                        None => {
1639                            println!("UNKNOWN");
1640                            println!("All policy residuals:");
1641                            for p in ans.residual_policies() {
1642                                println!("{p}");
1643                            }
1644                            CedarExitCode::Unknown
1645                        }
1646                    }
1647                }
1648                Err(err) => {
1649                    errs.push(miette!("{err}"));
1650                    return ret(errs);
1651                }
1652            }
1653        }
1654        Ok(_) => {
1655            return ret(errs);
1656        }
1657        Err(e) => {
1658            errs.push(e.wrap_err("failed to parse request"));
1659            return ret(errs);
1660        }
1661    }
1662}
1663
1664#[cfg(feature = "partial-eval")]
1665pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
1666    println!();
1667    let ans = execute_partial_request(
1668        &args.request,
1669        &args.policies,
1670        &args.entities_file,
1671        &args.schema,
1672        args.timing,
1673    );
1674    match ans {
1675        Ok(ans) => match ans.decision() {
1676            Some(Decision::Allow) => {
1677                println!("ALLOW");
1678                CedarExitCode::Success
1679            }
1680            Some(Decision::Deny) => {
1681                println!("DENY");
1682                CedarExitCode::AuthorizeDeny
1683            }
1684            None => {
1685                println!("UNKNOWN");
1686                println!("All policy residuals:");
1687                for p in ans.nontrivial_residuals() {
1688                    println!("{p}");
1689                }
1690                CedarExitCode::Unknown
1691            }
1692        },
1693        Err(errs) => {
1694            for err in errs {
1695                println!("{err:?}");
1696            }
1697            CedarExitCode::Failure
1698        }
1699    }
1700}
1701
1702#[derive(Clone, Debug)]
1703enum TestResult {
1704    Pass,
1705    Fail(String),
1706}
1707
1708/// Compare the test's expected decision against the actual decision
1709fn compare_test_decisions(test: &TestCase, ans: &Response) -> TestResult {
1710    if ans.decision() == test.decision.into() {
1711        let mut errors = Vec::new();
1712        let reason = ans.diagnostics().reason().collect::<BTreeSet<_>>();
1713
1714        // Check that the declared reason is a subset of the actual reason
1715        let missing_reason = test
1716            .reason
1717            .iter()
1718            .filter(|r| !reason.contains(&PolicyId::new(r)))
1719            .collect::<Vec<_>>();
1720
1721        if !missing_reason.is_empty() {
1722            errors.push(format!(
1723                "missing reason(s): {}",
1724                missing_reason
1725                    .into_iter()
1726                    .map(|r| format!("`{r}`"))
1727                    .collect::<Vec<_>>()
1728                    .join(", ")
1729            ));
1730        }
1731
1732        // Check that evaluation errors are expected
1733        let num_errors = ans.diagnostics().errors().count();
1734        if num_errors != test.num_errors {
1735            errors.push(format!(
1736                "expected {} error(s), but got {} runtime error(s){}",
1737                test.num_errors,
1738                num_errors,
1739                if num_errors == 0 {
1740                    "".to_string()
1741                } else {
1742                    format!(
1743                        ": {}",
1744                        ans.diagnostics()
1745                            .errors()
1746                            .map(|e| e.to_string())
1747                            .collect::<Vec<_>>()
1748                            .join(", ")
1749                    )
1750                },
1751            ));
1752        }
1753
1754        if errors.is_empty() {
1755            TestResult::Pass
1756        } else {
1757            TestResult::Fail(errors.join("; "))
1758        }
1759    } else {
1760        TestResult::Fail(format!(
1761            "expected {:?}, got {:?}",
1762            test.decision,
1763            ans.decision()
1764        ))
1765    }
1766}
1767
1768/// Parse the test, validate against schema,
1769/// and then check the authorization decision
1770fn run_one_test(policies: &PolicySet, test: &serde_json::Value) -> Result<TestResult> {
1771    let test = TestCase::deserialize(test.clone()).into_diagnostic()?;
1772    let ans = Authorizer::new().is_authorized(&test.request, policies, &test.entities);
1773    Ok(compare_test_decisions(&test, &ans))
1774}
1775
1776fn run_tests_inner(args: &RunTestsArgs) -> Result<CedarExitCode> {
1777    let policies = args.policies.get_policy_set()?;
1778    let tests = load_partial_tests(&args.tests)?;
1779
1780    let mut total_fails: usize = 0;
1781
1782    println!("running {} test(s)", tests.len());
1783    for test in tests.iter() {
1784        if let Some(name) = test["name"].as_str() {
1785            print!("  test {name} ... ");
1786        } else {
1787            print!("  test (unamed) ... ");
1788        }
1789        std::io::stdout().flush().into_diagnostic()?;
1790
1791        match run_one_test(&policies, test) {
1792            Ok(TestResult::Pass) => {
1793                println!(
1794                    "{}",
1795                    "ok".if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
1796                );
1797            }
1798            Ok(TestResult::Fail(reason)) => {
1799                total_fails += 1;
1800                println!(
1801                    "{}: {}",
1802                    "fail".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
1803                    reason
1804                );
1805            }
1806            Err(e) => {
1807                total_fails += 1;
1808                println!(
1809                    "{}:\n  {:?}",
1810                    "error".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
1811                    e
1812                );
1813            }
1814        }
1815    }
1816
1817    println!(
1818        "results: {} {}, {} {}",
1819        tests.len() - total_fails,
1820        if total_fails == 0 {
1821            "passed"
1822                .if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
1823                .to_string()
1824        } else {
1825            "passed".to_string()
1826        },
1827        total_fails,
1828        if total_fails != 0 {
1829            "failed"
1830                .if_supports_color(owo_colors::Stream::Stdout, |s| s.red())
1831                .to_string()
1832        } else {
1833            "failed".to_string()
1834        },
1835    );
1836
1837    Ok(if total_fails != 0 {
1838        CedarExitCode::Failure
1839    } else {
1840        CedarExitCode::Success
1841    })
1842}
1843
1844pub fn run_tests(args: &RunTestsArgs) -> CedarExitCode {
1845    match run_tests_inner(args) {
1846        Ok(status) => status,
1847        Err(e) => {
1848            println!("{e:?}");
1849            CedarExitCode::Failure
1850        }
1851    }
1852}
1853
1854#[derive(Copy, Clone, Debug, Deserialize)]
1855enum ExpectedDecision {
1856    #[serde(rename = "allow")]
1857    Allow,
1858    #[serde(rename = "deny")]
1859    Deny,
1860}
1861
1862impl From<ExpectedDecision> for Decision {
1863    fn from(value: ExpectedDecision) -> Self {
1864        match value {
1865            ExpectedDecision::Allow => Decision::Allow,
1866            ExpectedDecision::Deny => Decision::Deny,
1867        }
1868    }
1869}
1870
1871#[derive(Clone, Debug, Deserialize)]
1872struct TestCase {
1873    #[serde(deserialize_with = "deserialize_request")]
1874    request: Request,
1875    #[serde(deserialize_with = "deserialize_entities")]
1876    entities: Entities,
1877    decision: ExpectedDecision,
1878    reason: Vec<String>,
1879    num_errors: usize,
1880}
1881
1882/// Helper function to deserialize a `Request` from JSON (without schema)
1883fn deserialize_request<'de, D>(data: D) -> Result<Request, D::Error>
1884where
1885    D: Deserializer<'de>,
1886{
1887    let qjson = RequestJSON::deserialize(data)?;
1888
1889    let principal = qjson.principal.parse().map_err(|e| {
1890        serde::de::Error::custom(format!(
1891            "failed to parse principal `{}`: {}",
1892            qjson.principal, e
1893        ))
1894    })?;
1895
1896    let action = qjson.action.parse().map_err(|e| {
1897        serde::de::Error::custom(format!("failed to parse action `{}`: {}", qjson.action, e))
1898    })?;
1899
1900    let resource = qjson.resource.parse().map_err(|e| {
1901        serde::de::Error::custom(format!(
1902            "failed to parse resource `{}`: {}",
1903            qjson.resource, e
1904        ))
1905    })?;
1906
1907    let context = Context::from_json_value(qjson.context.clone(), None).map_err(|e| {
1908        serde::de::Error::custom(format!(
1909            "failed to parse context `{}`: {}",
1910            qjson.context, e
1911        ))
1912    })?;
1913
1914    Request::new(principal, action, resource, context, None)
1915        .map_err(|e| serde::de::Error::custom(format!("failed to create request: {e}")))
1916}
1917
1918/// Helper function to deserialize an `Entities` from JSON (without schema)
1919fn deserialize_entities<'de, D>(data: D) -> Result<Entities, D::Error>
1920where
1921    D: Deserializer<'de>,
1922{
1923    let value = serde_json::Value::deserialize(data)?;
1924    Entities::from_json_value(value, None)
1925        .map_err(|e| serde::de::Error::custom(format!("failed to parse entities: {e}")))
1926}
1927
1928/// Load partially parsed tests from a JSON file
1929/// (as JSON values first without parsing to TestCase)
1930fn load_partial_tests(tests_filename: impl AsRef<Path>) -> Result<Vec<serde_json::Value>> {
1931    match std::fs::OpenOptions::new()
1932        .read(true)
1933        .open(tests_filename.as_ref())
1934    {
1935        Ok(f) => {
1936            let reader = BufReader::new(f);
1937            serde_json::from_reader(reader).map_err(|e| {
1938                miette!(
1939                    "failed to parse tests from file {}: {e}",
1940                    tests_filename.as_ref().display()
1941                )
1942            })
1943        }
1944        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1945            format!(
1946                "failed to open test file {}",
1947                tests_filename.as_ref().display()
1948            )
1949        }),
1950    }
1951}
1952
1953#[cfg(feature = "tpe")]
1954/// Load an `PartialEntities` object from the given JSON filename and optional schema.
1955fn load_partial_entities(
1956    entities_filename: impl AsRef<Path>,
1957    schema: &Schema,
1958) -> Result<PartialEntities> {
1959    match std::fs::OpenOptions::new()
1960        .read(true)
1961        .open(entities_filename.as_ref())
1962    {
1963        Ok(f) => {
1964            PartialEntities::from_json_value(serde_json::from_reader(f).into_diagnostic()?, schema)
1965                .map_err(|e| miette!("{e}"))
1966                .wrap_err_with(|| {
1967                    format!(
1968                        "failed to parse entities from file {}",
1969                        entities_filename.as_ref().display()
1970                    )
1971                })
1972        }
1973        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1974            format!(
1975                "failed to open entities file {}",
1976                entities_filename.as_ref().display()
1977            )
1978        }),
1979    }
1980}
1981
1982/// Load an `Entities` object from the given JSON filename and optional schema.
1983fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
1984    match std::fs::OpenOptions::new()
1985        .read(true)
1986        .open(entities_filename.as_ref())
1987    {
1988        Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
1989            format!(
1990                "failed to parse entities from file {}",
1991                entities_filename.as_ref().display()
1992            )
1993        }),
1994        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1995            format!(
1996                "failed to open entities file {}",
1997                entities_filename.as_ref().display()
1998            )
1999        }),
2000    }
2001}
2002
2003/// Renames policies and templates based on (@id("new_id") annotation.
2004/// If no such annotation exists, it keeps the current id.
2005///
2006/// This will rename template-linked policies to the id of their template, which may
2007/// cause id conflicts, so only call this function before instancing
2008/// templates into the policy set.
2009fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
2010    let mut new_ps = PolicySet::new();
2011    let t_iter = ps.templates().map(|t| match t.annotation("id") {
2012        None => Ok(t.clone()),
2013        Some(anno) => anno.parse().map(|a| t.new_id(a)),
2014    });
2015    for t in t_iter {
2016        let template = t.unwrap_or_else(|never| match never {});
2017        new_ps
2018            .add_template(template)
2019            .wrap_err("failed to add template to policy set")?;
2020    }
2021    let p_iter = ps.policies().map(|p| match p.annotation("id") {
2022        None => Ok(p.clone()),
2023        Some(anno) => anno.parse().map(|a| p.new_id(a)),
2024    });
2025    for p in p_iter {
2026        let policy = p.unwrap_or_else(|never| match never {});
2027        new_ps
2028            .add(policy)
2029            .wrap_err("failed to add template to policy set")?;
2030    }
2031    Ok(new_ps)
2032}
2033
2034// Read from a file (when `filename` is a `Some`) or stdin (when `filename` is `None`) to a `String`
2035fn read_from_file_or_stdin(filename: Option<&impl AsRef<Path>>, context: &str) -> Result<String> {
2036    let mut src_str = String::new();
2037    match filename {
2038        Some(path) => {
2039            src_str = std::fs::read_to_string(path)
2040                .into_diagnostic()
2041                .wrap_err_with(|| {
2042                    format!("failed to open {context} file {}", path.as_ref().display())
2043                })?;
2044        }
2045        None => {
2046            std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
2047                .into_diagnostic()
2048                .wrap_err_with(|| format!("failed to read {context} from stdin"))?;
2049        }
2050    };
2051    Ok(src_str)
2052}
2053
2054// Convenient wrapper around `read_from_file_or_stdin` to just read from a file
2055fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
2056    read_from_file_or_stdin(Some(&filename), context)
2057}
2058
2059/// Read a policy set, in Cedar syntax, from the file given in `filename`,
2060/// or from stdin if `filename` is `None`.
2061fn read_cedar_policy_set(
2062    filename: Option<impl AsRef<Path> + std::marker::Copy>,
2063) -> Result<PolicySet> {
2064    let context = "policy set";
2065    let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
2066    let ps = PolicySet::from_str(&ps_str)
2067        .map_err(|err| {
2068            let name = filename.map_or_else(
2069                || "<stdin>".to_owned(),
2070                |n| n.as_ref().display().to_string(),
2071            );
2072            Report::new(err).with_source_code(NamedSource::new(name, ps_str))
2073        })
2074        .wrap_err_with(|| format!("failed to parse {context}"))?;
2075    rename_from_id_annotation(&ps)
2076}
2077
2078/// Read a policy set, static policy or policy template, in Cedar JSON (EST) syntax, from the file given
2079/// in `filename`, or from stdin if `filename` is `None`.
2080fn read_json_policy_set(
2081    filename: Option<impl AsRef<Path> + std::marker::Copy>,
2082) -> Result<PolicySet> {
2083    let context = "JSON policy";
2084    let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
2085    let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
2086    let policy_type = get_json_policy_type(&json)?;
2087
2088    let add_json_source = |report: Report| {
2089        let name = filename.map_or_else(
2090            || "<stdin>".to_owned(),
2091            |n| n.as_ref().display().to_string(),
2092        );
2093        report.with_source_code(NamedSource::new(name, json_source.clone()))
2094    };
2095
2096    match policy_type {
2097        JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
2098            Ok(policy) => PolicySet::from_policies([policy])
2099                .wrap_err_with(|| format!("failed to create policy set from {context}")),
2100            Err(_) => match Template::from_json(None, json)
2101                .map_err(|err| add_json_source(Report::new(err)))
2102            {
2103                Ok(template) => {
2104                    let mut ps = PolicySet::new();
2105                    ps.add_template(template)?;
2106                    Ok(ps)
2107                }
2108                Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
2109            },
2110        },
2111        JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
2112            .map_err(|err| add_json_source(Report::new(err)))
2113            .wrap_err_with(|| format!("failed to create policy set from {context}")),
2114    }
2115}
2116
2117fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
2118    let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
2119    let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
2120
2121    let json_has_property = |p| json.get(p).is_some();
2122    let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
2123    let has_any_policy_property = policy_properties.iter().any(json_has_property);
2124
2125    match (has_any_policy_set_property, has_any_policy_property) {
2126        (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
2127        (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
2128        (true, _) => Ok(JsonPolicyType::PolicySet),
2129        (_, true) => Ok(JsonPolicyType::SinglePolicy),
2130    }
2131}
2132
2133enum JsonPolicyType {
2134    SinglePolicy,
2135    PolicySet,
2136}
2137
2138/// This uses the Cedar API to call the authorization engine.
2139fn execute_request(
2140    request: &RequestArgs,
2141    policies: &PoliciesArgs,
2142    entities_filename: impl AsRef<Path>,
2143    schema: &OptionalSchemaArgs,
2144    compute_duration: bool,
2145) -> Result<Response, Vec<Report>> {
2146    let mut errs = vec![];
2147    let policies = match policies.get_policy_set() {
2148        Ok(pset) => pset,
2149        Err(e) => {
2150            errs.push(e);
2151            PolicySet::new()
2152        }
2153    };
2154    let schema = match schema.get_schema() {
2155        Ok(opt) => opt,
2156        Err(e) => {
2157            errs.push(e);
2158            None
2159        }
2160    };
2161    let entities = match load_entities(entities_filename, schema.as_ref()) {
2162        Ok(entities) => entities,
2163        Err(e) => {
2164            errs.push(e);
2165            Entities::empty()
2166        }
2167    };
2168    match request.get_request(schema.as_ref()) {
2169        Ok(request) if errs.is_empty() => {
2170            let authorizer = Authorizer::new();
2171            let auth_start = Instant::now();
2172            let ans = authorizer.is_authorized(&request, &policies, &entities);
2173            let auth_dur = auth_start.elapsed();
2174            if compute_duration {
2175                println!(
2176                    "Authorization Time (micro seconds) : {}",
2177                    auth_dur.as_micros()
2178                );
2179            }
2180            Ok(ans)
2181        }
2182        Ok(_) => Err(errs),
2183        Err(e) => {
2184            errs.push(e.wrap_err("failed to parse request"));
2185            Err(errs)
2186        }
2187    }
2188}
2189
2190#[cfg(feature = "partial-eval")]
2191fn execute_partial_request(
2192    request: &PartialRequestArgs,
2193    policies: &PoliciesArgs,
2194    entities_filename: impl AsRef<Path>,
2195    schema: &OptionalSchemaArgs,
2196    compute_duration: bool,
2197) -> Result<PartialResponse, Vec<Report>> {
2198    let mut errs = vec![];
2199    let policies = match policies.get_policy_set() {
2200        Ok(pset) => pset,
2201        Err(e) => {
2202            errs.push(e);
2203            PolicySet::new()
2204        }
2205    };
2206    let schema = match schema.get_schema() {
2207        Ok(opt) => opt,
2208        Err(e) => {
2209            errs.push(e);
2210            None
2211        }
2212    };
2213    let entities = match load_entities(entities_filename, schema.as_ref()) {
2214        Ok(entities) => entities,
2215        Err(e) => {
2216            errs.push(e);
2217            Entities::empty()
2218        }
2219    };
2220    match request.get_request(schema.as_ref()) {
2221        Ok(request) if errs.is_empty() => {
2222            let authorizer = Authorizer::new();
2223            let auth_start = Instant::now();
2224            let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
2225            let auth_dur = auth_start.elapsed();
2226            if compute_duration {
2227                println!(
2228                    "Authorization Time (micro seconds) : {}",
2229                    auth_dur.as_micros()
2230                );
2231            }
2232            Ok(ans)
2233        }
2234        Ok(_) => Err(errs),
2235        Err(e) => {
2236            errs.push(e.wrap_err("failed to parse request"));
2237            Err(errs)
2238        }
2239    }
2240}