cedar_policy_cli/
lib.rs

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