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