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 natural policy syntax to JSON (except comments)
99    TranslatePolicy(TranslatePolicyArgs),
100    /// Translate JSON schema to natural schema syntax and vice versa (except comments)
101    TranslateSchema(TranslateSchemaArgs),
102    /// Visualize a set of JSON entities to the graphviz format.
103    /// Warning: Entity visualization is best-effort and not well tested.
104    Visualize(VisualizeArgs),
105    /// Create a Cedar project
106    New(NewArgs),
107    /// Partially evaluate an authorization request
108    PartiallyAuthorize(PartiallyAuthorizeArgs),
109}
110
111#[derive(Args, Debug)]
112pub struct TranslatePolicyArgs {
113    /// The direction of translation,
114    #[arg(long)]
115    pub direction: PolicyTranslationDirection,
116    /// Filename to read the policies from.
117    /// If not provided, will default to reading stdin.
118    #[arg(short = 'p', long = "policies", value_name = "FILE")]
119    pub input_file: Option<String>,
120}
121
122/// The direction of translation
123#[derive(Debug, Clone, Copy, ValueEnum)]
124pub enum PolicyTranslationDirection {
125    /// Human policy syntax -> JSON
126    HumanToJson,
127}
128
129#[derive(Args, Debug)]
130pub struct TranslateSchemaArgs {
131    /// The direction of translation,
132    #[arg(long)]
133    pub direction: SchemaTranslationDirection,
134    /// Filename to read the schema from.
135    /// If not provided, will default to reading stdin.
136    #[arg(short = 's', long = "schema", value_name = "FILE")]
137    pub input_file: Option<String>,
138}
139
140/// The direction of translation
141#[derive(Debug, Clone, Copy, ValueEnum)]
142pub enum SchemaTranslationDirection {
143    /// JSON -> Human schema syntax
144    JsonToHuman,
145    /// Human schema syntax -> JSON
146    HumanToJson,
147}
148
149#[derive(Debug, Clone, Copy, ValueEnum)]
150pub enum SchemaFormat {
151    /// Human-readable format
152    Human,
153    /// JSON format
154    Json,
155}
156
157impl Default for SchemaFormat {
158    fn default() -> Self {
159        Self::Json
160    }
161}
162
163#[derive(Args, Debug)]
164pub struct ValidateArgs {
165    /// File containing the schema
166    #[arg(short, long = "schema", value_name = "FILE")]
167    pub schema_file: String,
168    /// Policies args (incorporated by reference)
169    #[command(flatten)]
170    pub policies: PoliciesArgs,
171    /// Report a validation failure for non-fatal warnings
172    #[arg(long)]
173    pub deny_warnings: bool,
174    /// Validate the policy using partial schema validation. This option is
175    /// experimental and will cause the CLI to exit if it was not built with the
176    /// experimental `partial-validate` feature enabled.
177    #[arg(long = "partial-validate")]
178    pub partial_validate: bool,
179    /// Schema format (Human-readable or json)
180    #[arg(long, value_enum, default_value_t = SchemaFormat::Json)]
181    pub schema_format: SchemaFormat,
182}
183
184#[derive(Args, Debug)]
185pub struct CheckParseArgs {
186    /// Policies args (incorporated by reference)
187    #[command(flatten)]
188    pub policies: PoliciesArgs,
189}
190
191/// This struct contains the arguments that together specify a request.
192#[derive(Args, Debug)]
193pub struct RequestArgs {
194    /// Principal for the request, e.g., User::"alice"
195    #[arg(short = 'l', long)]
196    pub principal: Option<String>,
197    /// Action for the request, e.g., Action::"view"
198    #[arg(short, long)]
199    pub action: Option<String>,
200    /// Resource for the request, e.g., File::"myfile.txt"
201    #[arg(short, long)]
202    pub resource: Option<String>,
203    /// File containing a JSON object representing the context for the request.
204    /// Should be a (possibly empty) map from keys to values.
205    #[arg(short, long = "context", value_name = "FILE")]
206    pub context_json_file: Option<String>,
207    /// File containing a JSON object representing the entire request. Must have
208    /// fields "principal", "action", "resource", and "context", where "context"
209    /// is a (possibly empty) map from keys to values. This option replaces
210    /// --principal, --action, etc.
211    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
212    pub request_json_file: Option<String>,
213    /// Whether to enable request validation. This has no effect if a schema is
214    /// not provided.
215    #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
216    pub request_validation: bool,
217}
218
219#[cfg(feature = "partial-eval")]
220/// This struct contains the arguments that together specify a request.
221#[derive(Args, Debug)]
222pub struct PartialRequestArgs {
223    /// Principal for the request, e.g., User::"alice"
224    #[arg(short = 'l', long)]
225    pub principal: Option<String>,
226    /// Action for the request, e.g., Action::"view"
227    #[arg(short, long)]
228    pub action: Option<String>,
229    /// Resource for the request, e.g., File::"myfile.txt"
230    #[arg(short, long)]
231    pub resource: Option<String>,
232    /// File containing a JSON object representing the context for the request.
233    /// Should be a (possibly empty) map from keys to values.
234    #[arg(short, long = "context", value_name = "FILE")]
235    pub context_json_file: Option<String>,
236    /// File containing a JSON object representing the entire request. Must have
237    /// fields "principal", "action", "resource", and "context", where "context"
238    /// is a (possibly empty) map from keys to values. This option replaces
239    /// --principal, --action, etc.
240    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
241    pub request_json_file: Option<String>,
242}
243
244impl RequestArgs {
245    /// Turn this `RequestArgs` into the appropriate `Request` object
246    ///
247    /// `schema` will be used for schema-based parsing of the context, and also
248    /// (if `self.request_validation` is `true`) for request validation.
249    ///
250    /// `self.request_validation` has no effect if `schema` is `None`.
251    fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
252        match &self.request_json_file {
253            Some(jsonfile) => {
254                let jsonstring = std::fs::read_to_string(jsonfile)
255                    .into_diagnostic()
256                    .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
257                let qjson: RequestJSON = serde_json::from_str(&jsonstring)
258                    .into_diagnostic()
259                    .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
260                let principal = qjson
261                    .principal
262                    .map(|s| {
263                        s.parse().wrap_err_with(|| {
264                            format!("failed to parse principal in {jsonfile} as entity Uid")
265                        })
266                    })
267                    .transpose()?;
268                let action = qjson
269                    .action
270                    .map(|s| {
271                        s.parse().wrap_err_with(|| {
272                            format!("failed to parse action in {jsonfile} as entity Uid")
273                        })
274                    })
275                    .transpose()?;
276                let resource = qjson
277                    .resource
278                    .map(|s| {
279                        s.parse().wrap_err_with(|| {
280                            format!("failed to parse resource in {jsonfile} as entity Uid")
281                        })
282                    })
283                    .transpose()?;
284                let context = Context::from_json_value(
285                    qjson.context,
286                    schema.and_then(|s| Some((s, action.as_ref()?))),
287                )
288                .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
289                Request::new(
290                    principal,
291                    action,
292                    resource,
293                    context,
294                    if self.request_validation {
295                        schema
296                    } else {
297                        None
298                    },
299                )
300                .map_err(|e| miette!("{e}"))
301            }
302            None => {
303                let principal = self
304                    .principal
305                    .as_ref()
306                    .map(|s| {
307                        s.parse().wrap_err_with(|| {
308                            format!("failed to parse principal {s} as entity Uid")
309                        })
310                    })
311                    .transpose()?;
312                let action = self
313                    .action
314                    .as_ref()
315                    .map(|s| {
316                        s.parse()
317                            .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
318                    })
319                    .transpose()?;
320                let resource = self
321                    .resource
322                    .as_ref()
323                    .map(|s| {
324                        s.parse()
325                            .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
326                    })
327                    .transpose()?;
328                let context: Context = match &self.context_json_file {
329                    None => Context::empty(),
330                    Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
331                        Ok(f) => Context::from_json_file(
332                            f,
333                            schema.and_then(|s| Some((s, action.as_ref()?))),
334                        )
335                        .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
336                        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
337                            format!("error while loading context from {jsonfile}")
338                        })?,
339                    },
340                };
341                Request::new(
342                    principal,
343                    action,
344                    resource,
345                    context,
346                    if self.request_validation {
347                        schema
348                    } else {
349                        None
350                    },
351                )
352                .map_err(|e| miette!("{e}"))
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(Some(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(Some(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(Some(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::Human => read_human_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 (Human-readable or JSON)
485    #[arg(long, value_enum, default_value_t = SchemaFormat::Json)]
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 human-readable Cedar policy format, documented at <https://docs.cedarpolicy.com/policies/syntax-policy.html>
528    #[default]
529    Human,
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 instantiate
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: Option<String>,
614    /// Action for the request
615    #[serde(default)]
616    action: Option<String>,
617    /// Resource for the request
618    #[serde(default)]
619    resource: Option<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)]
627pub(self) struct 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 (Human-readable or JSON)
649    #[arg(long, value_enum, default_value_t = SchemaFormat::Json)]
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 = if args.partial_validate {
704        #[cfg(not(feature = "partial-validate"))]
705        {
706            eprintln!("Error: arguments include the experimental option `--partial-validate`, but this executable was not built with `partial-validate` experimental feature enabled");
707            return CedarExitCode::Failure;
708        }
709        #[cfg(feature = "partial-validate")]
710        ValidationMode::Partial
711    } else {
712        ValidationMode::default()
713    };
714
715    let pset = match args.policies.get_policy_set() {
716        Ok(pset) => pset,
717        Err(e) => {
718            println!("{e:?}");
719            return CedarExitCode::Failure;
720        }
721    };
722
723    let schema = match read_schema_file(&args.schema_file, args.schema_format) {
724        Ok(schema) => schema,
725        Err(e) => {
726            println!("{e:?}");
727            return CedarExitCode::Failure;
728        }
729    };
730
731    let validator = Validator::new(schema);
732    let result = validator.validate(&pset, mode);
733
734    if !result.validation_passed()
735        || (args.deny_warnings && !result.validation_passed_without_warnings())
736    {
737        println!(
738            "{:?}",
739            Report::new(result).wrap_err("policy set validation failed")
740        );
741        CedarExitCode::ValidationFailure
742    } else {
743        println!(
744            "{:?}",
745            Report::new(result).wrap_err("policy set validation passed")
746        );
747        CedarExitCode::Success
748    }
749}
750
751pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
752    println!();
753    let schema = match args
754        .schema_file
755        .as_ref()
756        .map(|f| read_schema_file(f, args.schema_format))
757    {
758        None => None,
759        Some(Ok(schema)) => Some(schema),
760        Some(Err(e)) => {
761            println!("{e:?}");
762            return (CedarExitCode::Failure, EvalResult::Bool(false));
763        }
764    };
765    let request = match args.request.get_request(schema.as_ref()) {
766        Ok(q) => q,
767        Err(e) => {
768            println!("{e:?}");
769            return (CedarExitCode::Failure, EvalResult::Bool(false));
770        }
771    };
772    let expr =
773        match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
774            Ok(expr) => expr,
775            Err(e) => {
776                println!("{:?}", e.with_source_code(args.expression.clone()));
777                return (CedarExitCode::Failure, EvalResult::Bool(false));
778            }
779        };
780    let entities = match &args.entities_file {
781        None => Entities::empty(),
782        Some(file) => match load_entities(file, schema.as_ref()) {
783            Ok(entities) => entities,
784            Err(e) => {
785                println!("{e:?}");
786                return (CedarExitCode::Failure, EvalResult::Bool(false));
787            }
788        },
789    };
790    match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
791    {
792        Err(e) => {
793            println!("{e:?}");
794            return (CedarExitCode::Failure, EvalResult::Bool(false));
795        }
796        Ok(result) => {
797            println!("{result}");
798            return (CedarExitCode::Success, result);
799        }
800    }
801}
802
803pub fn link(args: &LinkArgs) -> CedarExitCode {
804    if let Err(err) = link_inner(args) {
805        println!("{err:?}");
806        CedarExitCode::Failure
807    } else {
808        CedarExitCode::Success
809    }
810}
811
812pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
813    match load_entities(&args.entities_file, None) {
814        Ok(entities) => {
815            println!("{}", entities.to_dot_str());
816            CedarExitCode::Success
817        }
818        Err(report) => {
819            eprintln!("{report:?}");
820            CedarExitCode::Failure
821        }
822    }
823}
824
825/// Format the policies in the given file or stdin.
826///
827/// Returns a boolean indicating whether the formatted policies are the same as the original
828/// policies.
829fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
830    let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
831    let config = Config {
832        line_width: args.line_width,
833        indent_width: args.indent_width,
834    };
835    let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
836    let are_policies_equivalent = policies_str == formatted_policy;
837
838    match &args.policies_file {
839        Some(policies_file) if args.write => {
840            let mut file = OpenOptions::new()
841                .write(true)
842                .truncate(true)
843                .open(policies_file)
844                .into_diagnostic()
845                .wrap_err(format!("failed to open {policies_file} for writing"))?;
846            file.write_all(formatted_policy.as_bytes())
847                .into_diagnostic()
848                .wrap_err(format!(
849                    "failed to write formatted policies to {policies_file}"
850                ))?;
851        }
852        _ => println!("{}", formatted_policy),
853    }
854    Ok(are_policies_equivalent)
855}
856
857pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
858    match format_policies_inner(args) {
859        Ok(false) if args.check => CedarExitCode::Failure,
860        Err(err) => {
861            println!("{err:?}");
862            CedarExitCode::Failure
863        }
864        _ => CedarExitCode::Success,
865    }
866}
867
868fn translate_policy_to_json(natural_src: impl AsRef<str>) -> Result<String> {
869    let policy_set = PolicySet::from_str(natural_src.as_ref())?;
870    let output = policy_set.to_json()?.to_string();
871    Ok(output)
872}
873
874fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
875    let translate = match args.direction {
876        PolicyTranslationDirection::HumanToJson => translate_policy_to_json,
877    };
878    read_from_file_or_stdin(args.input_file.clone(), "policy").and_then(translate)
879}
880
881pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
882    match translate_policy_inner(args) {
883        Ok(sf) => {
884            println!("{sf}");
885            CedarExitCode::Success
886        }
887        Err(err) => {
888            eprintln!("{err:?}");
889            CedarExitCode::Failure
890        }
891    }
892}
893
894fn translate_schema_to_human(json_src: impl AsRef<str>) -> Result<String> {
895    let fragment = SchemaFragment::from_str(json_src.as_ref())?;
896    let output = fragment.to_cedarschema()?;
897    Ok(output)
898}
899
900fn translate_schema_to_json(natural_src: impl AsRef<str>) -> Result<String> {
901    let (fragment, warnings) = SchemaFragment::from_cedarschema_str(natural_src.as_ref())?;
902    for warning in warnings {
903        let report = miette::Report::new(warning);
904        eprintln!("{:?}", report);
905    }
906    let output = fragment.to_json_string()?;
907    Ok(output)
908}
909
910fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
911    let translate = match args.direction {
912        SchemaTranslationDirection::JsonToHuman => translate_schema_to_human,
913        SchemaTranslationDirection::HumanToJson => translate_schema_to_json,
914    };
915    read_from_file_or_stdin(args.input_file.clone(), "schema").and_then(translate)
916}
917
918pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
919    match translate_schema_inner(args) {
920        Ok(sf) => {
921            println!("{sf}");
922            CedarExitCode::Success
923        }
924        Err(err) => {
925            eprintln!("{err:?}");
926            CedarExitCode::Failure
927        }
928    }
929}
930
931fn generate_schema(path: &Path) -> Result<()> {
932    std::fs::write(
933        path,
934        serde_json::to_string_pretty(&serde_json::json!(
935        {
936            "": {
937                "entityTypes": {
938                    "A": {
939                        "memberOfTypes": [
940                            "B"
941                        ]
942                    },
943                    "B": {
944                        "memberOfTypes": []
945                    },
946                    "C": {
947                        "memberOfTypes": []
948                    }
949                },
950                "actions": {
951                    "action": {
952                        "appliesTo": {
953                            "resourceTypes": [
954                                "C"
955                            ],
956                            "principalTypes": [
957                                "A",
958                                "B"
959                            ]
960                        }
961                    }
962                }
963            }
964        }))
965        .into_diagnostic()?,
966    )
967    .into_diagnostic()
968}
969
970fn generate_policy(path: &Path) -> Result<()> {
971    std::fs::write(
972        path,
973        r#"permit (
974  principal in A::"a",
975  action == Action::"action",
976  resource == C::"c"
977) when { true };
978"#,
979    )
980    .into_diagnostic()
981}
982
983fn generate_entities(path: &Path) -> Result<()> {
984    std::fs::write(
985        path,
986        serde_json::to_string_pretty(&serde_json::json!(
987        [
988            {
989                "uid": { "type": "A", "id": "a"} ,
990                "attrs": {},
991                "parents": [{"type": "B", "id": "b"}]
992            },
993            {
994                "uid": { "type": "B", "id": "b"} ,
995                "attrs": {},
996                "parents": []
997            },
998            {
999                "uid": { "type": "C", "id": "c"} ,
1000                "attrs": {},
1001                "parents": []
1002            }
1003        ]))
1004        .into_diagnostic()?,
1005    )
1006    .into_diagnostic()
1007}
1008
1009fn new_inner(args: &NewArgs) -> Result<()> {
1010    let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1011    std::fs::create_dir(dir).into_diagnostic()?;
1012    let schema_path = dir.join("schema.cedarschema.json");
1013    let policy_path = dir.join("policy.cedar");
1014    let entities_path = dir.join("entities.jon");
1015    generate_schema(&schema_path)?;
1016    generate_policy(&policy_path)?;
1017    generate_entities(&entities_path)
1018}
1019
1020pub fn new(args: &NewArgs) -> CedarExitCode {
1021    if let Err(err) = new_inner(args) {
1022        println!("{err:?}");
1023        CedarExitCode::Failure
1024    } else {
1025        CedarExitCode::Success
1026    }
1027}
1028
1029fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
1030    data.iter()
1031        .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
1032        .collect::<Result<HashMap<SlotId, EntityUid>>>()
1033}
1034
1035fn link_inner(args: &LinkArgs) -> Result<()> {
1036    let mut policies = args.policies.get_policy_set()?;
1037    let slotenv = create_slot_env(&args.arguments.data)?;
1038    policies.link(
1039        PolicyId::new(&args.template_id),
1040        PolicyId::new(&args.new_id),
1041        slotenv,
1042    )?;
1043    let linked = policies
1044        .policy(&PolicyId::new(&args.new_id))
1045        .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
1046    println!("Template-linked policy added: {linked}");
1047
1048    // If a `--template-linked` / `-k` option was provided, update that file with the new link
1049    if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
1050        update_template_linked_file(
1051            links_filename,
1052            TemplateLinked {
1053                template_id: args.template_id.clone(),
1054                link_id: args.new_id.clone(),
1055                args: args.arguments.data.clone(),
1056            },
1057        )?;
1058    }
1059
1060    Ok(())
1061}
1062
1063#[derive(Clone, Serialize, Deserialize, Debug)]
1064#[serde(try_from = "LiteralTemplateLinked")]
1065#[serde(into = "LiteralTemplateLinked")]
1066struct TemplateLinked {
1067    template_id: String,
1068    link_id: String,
1069    args: HashMap<SlotId, String>,
1070}
1071
1072impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
1073    type Error = String;
1074
1075    fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
1076        Ok(Self {
1077            template_id: value.template_id,
1078            link_id: value.link_id,
1079            args: value
1080                .args
1081                .into_iter()
1082                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1083                .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
1084        })
1085    }
1086}
1087
1088fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
1089    match s.as_ref() {
1090        "?principal" => Ok(SlotId::principal()),
1091        "?resource" => Ok(SlotId::resource()),
1092        _ => Err(format!(
1093            "Invalid SlotId! Expected ?principal|?resource, got: {}",
1094            s.as_ref()
1095        )),
1096    }
1097}
1098
1099#[derive(Serialize, Deserialize)]
1100struct LiteralTemplateLinked {
1101    template_id: String,
1102    link_id: String,
1103    args: HashMap<String, String>,
1104}
1105
1106impl From<TemplateLinked> for LiteralTemplateLinked {
1107    fn from(i: TemplateLinked) -> Self {
1108        Self {
1109            template_id: i.template_id,
1110            link_id: i.link_id,
1111            args: i
1112                .args
1113                .into_iter()
1114                .map(|(k, v)| (format!("{k}"), v))
1115                .collect(),
1116        }
1117    }
1118}
1119
1120/// Iterate over links in the template-linked file and add them to the set
1121fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
1122    for template_linked in load_links_from_file(path)? {
1123        let slot_env = create_slot_env(&template_linked.args)?;
1124        policy_set.link(
1125            PolicyId::new(&template_linked.template_id),
1126            PolicyId::new(&template_linked.link_id),
1127            slot_env,
1128        )?;
1129    }
1130    Ok(())
1131}
1132
1133/// Given a file containing template links, return a `Vec` of those links
1134fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
1135    let f = match std::fs::File::open(path) {
1136        Ok(f) => f,
1137        Err(_) => {
1138            // If the file doesn't exist, then give back the empty entity set
1139            return Ok(vec![]);
1140        }
1141    };
1142    if f.metadata()
1143        .into_diagnostic()
1144        .wrap_err("Failed to read metadata")?
1145        .len()
1146        == 0
1147    {
1148        // File is empty, return empty set
1149        Ok(vec![])
1150    } else {
1151        // File has contents, deserialize
1152        serde_json::from_reader(f)
1153            .into_diagnostic()
1154            .wrap_err("Deserialization error")
1155    }
1156}
1157
1158/// Add a single template-linked policy to the linked file
1159fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
1160    let mut template_linked = load_links_from_file(path.as_ref())?;
1161    template_linked.push(new_linked);
1162    write_template_linked_file(&template_linked, path.as_ref())
1163}
1164
1165/// Write a slice of template-linked policies to the linked file
1166fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
1167    let f = OpenOptions::new()
1168        .write(true)
1169        .truncate(true)
1170        .create(true)
1171        .open(path)
1172        .into_diagnostic()?;
1173    serde_json::to_writer(f, linked).into_diagnostic()
1174}
1175
1176pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
1177    println!();
1178    let ans = execute_request(
1179        &args.request,
1180        &args.policies,
1181        &args.entities_file,
1182        args.schema_file.as_ref(),
1183        args.schema_format,
1184        args.timing,
1185    );
1186    match ans {
1187        Ok(ans) => {
1188            let status = match ans.decision() {
1189                Decision::Allow => {
1190                    println!("ALLOW");
1191                    CedarExitCode::Success
1192                }
1193                Decision::Deny => {
1194                    println!("DENY");
1195                    CedarExitCode::AuthorizeDeny
1196                }
1197            };
1198            if ans.diagnostics().errors().peekable().peek().is_some() {
1199                println!();
1200                for err in ans.diagnostics().errors() {
1201                    println!("{err}");
1202                }
1203            }
1204            if args.verbose {
1205                println!();
1206                if ans.diagnostics().reason().peekable().peek().is_none() {
1207                    println!("note: no policies applied to this request");
1208                } else {
1209                    println!("note: this decision was due to the following policies:");
1210                    for reason in ans.diagnostics().reason() {
1211                        println!("  {}", reason);
1212                    }
1213                    println!();
1214                }
1215            }
1216            status
1217        }
1218        Err(errs) => {
1219            for err in errs {
1220                println!("{err:?}");
1221            }
1222            CedarExitCode::Failure
1223        }
1224    }
1225}
1226
1227#[cfg(not(feature = "partial-eval"))]
1228pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
1229    {
1230        eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
1231        return CedarExitCode::Failure;
1232    }
1233}
1234
1235#[cfg(feature = "partial-eval")]
1236pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
1237    println!();
1238    let ans = execute_partial_request(
1239        &args.request,
1240        &args.policies,
1241        &args.entities_file,
1242        args.timing,
1243    );
1244    match ans {
1245        Ok(ans) => {
1246            let status = match ans.decision() {
1247                Some(Decision::Allow) => {
1248                    println!("ALLOW");
1249                    CedarExitCode::Success
1250                }
1251                Some(Decision::Deny) => {
1252                    println!("DENY");
1253                    CedarExitCode::AuthorizeDeny
1254                }
1255                None => {
1256                    println!("UNKNOWN");
1257                    println!("All policy residuals:");
1258                    for p in ans.nontrivial_residuals() {
1259                        println!("{p}");
1260                    }
1261                    CedarExitCode::Unknown
1262                }
1263            };
1264            status
1265        }
1266        Err(errs) => {
1267            for err in errs {
1268                println!("{err:?}");
1269            }
1270            CedarExitCode::Failure
1271        }
1272    }
1273}
1274
1275/// Load an `Entities` object from the given JSON filename and optional schema.
1276fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
1277    match std::fs::OpenOptions::new()
1278        .read(true)
1279        .open(entities_filename.as_ref())
1280    {
1281        Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
1282            format!(
1283                "failed to parse entities from file {}",
1284                entities_filename.as_ref().display()
1285            )
1286        }),
1287        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
1288            format!(
1289                "failed to open entities file {}",
1290                entities_filename.as_ref().display()
1291            )
1292        }),
1293    }
1294}
1295
1296/// Renames policies and templates based on (@id("new_id") annotation.
1297/// If no such annotation exists, it keeps the current id.
1298///
1299/// This will rename template-linked policies to the id of their template, which may
1300/// cause id conflicts, so only call this function before instancing
1301/// templates into the policy set.
1302fn rename_from_id_annotation(ps: PolicySet) -> Result<PolicySet> {
1303    let mut new_ps = PolicySet::new();
1304    let t_iter = ps.templates().map(|t| match t.annotation("id") {
1305        None => Ok(t.clone()),
1306        Some(anno) => anno.parse().map(|a| t.new_id(a)),
1307    });
1308    for t in t_iter {
1309        let template = t.wrap_err("failed to parse policy id annotation")?;
1310        new_ps
1311            .add_template(template)
1312            .wrap_err("failed to add template to policy set")?;
1313    }
1314    let p_iter = ps.policies().map(|p| match p.annotation("id") {
1315        None => Ok(p.clone()),
1316        Some(anno) => anno.parse().map(|a| p.new_id(a)),
1317    });
1318    for p in p_iter {
1319        let policy = p.wrap_err("failed to parse policy id annotation")?;
1320        new_ps
1321            .add(policy)
1322            .wrap_err("failed to add template to policy set")?;
1323    }
1324    Ok(new_ps)
1325}
1326
1327// Read from a file (when `filename` is a `Some`) or stdin (when `filename` is `None`) to a `String`
1328fn read_from_file_or_stdin(filename: Option<impl AsRef<Path>>, context: &str) -> Result<String> {
1329    let mut src_str = String::new();
1330    match filename.as_ref() {
1331        Some(path) => {
1332            src_str = std::fs::read_to_string(path)
1333                .into_diagnostic()
1334                .wrap_err_with(|| {
1335                    format!("failed to open {context} file {}", path.as_ref().display())
1336                })?;
1337        }
1338        None => {
1339            std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
1340                .into_diagnostic()
1341                .wrap_err_with(|| format!("failed to read {} from stdin", context))?;
1342        }
1343    };
1344    Ok(src_str)
1345}
1346
1347// Convenient wrapper around `read_from_file_or_stdin` to just read from a file
1348fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
1349    read_from_file_or_stdin(Some(filename), context)
1350}
1351
1352/// Read a policy set, in Cedar human syntax, from the file given in `filename`,
1353/// or from stdin if `filename` is `None`.
1354fn read_human_policy_set(
1355    filename: Option<impl AsRef<Path> + std::marker::Copy>,
1356) -> Result<PolicySet> {
1357    let context = "policy set";
1358    let ps_str = read_from_file_or_stdin(filename, context)?;
1359    let ps = PolicySet::from_str(&ps_str)
1360        .map_err(|err| {
1361            let name = filename.map_or_else(
1362                || "<stdin>".to_owned(),
1363                |n| n.as_ref().display().to_string(),
1364            );
1365            Report::new(err).with_source_code(NamedSource::new(name, ps_str))
1366        })
1367        .wrap_err_with(|| format!("failed to parse {context}"))?;
1368    rename_from_id_annotation(ps)
1369}
1370
1371/// Read a policy set, static policy or policy template, in Cedar JSON (EST) syntax, from the file given
1372/// in `filename`, or from stdin if `filename` is `None`.
1373fn read_json_policy_set(
1374    filename: Option<impl AsRef<Path> + std::marker::Copy>,
1375) -> Result<PolicySet> {
1376    let context = "JSON policy";
1377    let json_source = read_from_file_or_stdin(filename, context)?;
1378    let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
1379    let policy_type = get_json_policy_type(&json)?;
1380
1381    let add_json_source = |report: Report| {
1382        let name = filename.map_or_else(
1383            || "<stdin>".to_owned(),
1384            |n| n.as_ref().display().to_string(),
1385        );
1386        report.with_source_code(NamedSource::new(name, json_source.clone()))
1387    };
1388
1389    match policy_type {
1390        JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
1391            Ok(policy) => PolicySet::from_policies([policy])
1392                .wrap_err_with(|| format!("failed to create policy set from {context}")),
1393            Err(_) => match Template::from_json(None, json)
1394                .map_err(|err| add_json_source(Report::new(err)))
1395            {
1396                Ok(template) => {
1397                    let mut ps = PolicySet::new();
1398                    ps.add_template(template)?;
1399                    Ok(ps)
1400                }
1401                Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
1402            },
1403        },
1404        JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
1405            .map_err(|err| add_json_source(Report::new(err)))
1406            .wrap_err_with(|| format!("failed to create policy set from {context}")),
1407    }
1408}
1409
1410fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
1411    let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
1412    let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
1413
1414    let json_has_property = |p| json.get(p).is_some();
1415    let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
1416    let has_any_policy_property = policy_properties.iter().any(json_has_property);
1417
1418    match (has_any_policy_set_property, has_any_policy_property) {
1419        (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
1420        (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
1421        (true, _) => Ok(JsonPolicyType::PolicySet),
1422        (_, true) => Ok(JsonPolicyType::SinglePolicy),
1423    }
1424}
1425
1426enum JsonPolicyType {
1427    SinglePolicy,
1428    PolicySet,
1429}
1430
1431fn read_schema_file(
1432    filename: impl AsRef<Path> + std::marker::Copy,
1433    format: SchemaFormat,
1434) -> Result<Schema> {
1435    let schema_src = read_from_file(filename, "schema")?;
1436    match format {
1437        SchemaFormat::Json => Schema::from_str(&schema_src).wrap_err_with(|| {
1438            format!(
1439                "failed to parse schema from file {}",
1440                filename.as_ref().display()
1441            )
1442        }),
1443        SchemaFormat::Human => {
1444            let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)?;
1445            for warning in warnings {
1446                let report = miette::Report::new(warning);
1447                eprintln!("{:?}", report);
1448            }
1449            Ok(schema)
1450        }
1451    }
1452}
1453
1454/// This uses the Cedar API to call the authorization engine.
1455fn execute_request(
1456    request: &RequestArgs,
1457    policies: &PoliciesArgs,
1458    entities_filename: impl AsRef<Path>,
1459    schema_filename: Option<impl AsRef<Path> + std::marker::Copy>,
1460    schema_format: SchemaFormat,
1461    compute_duration: bool,
1462) -> Result<Response, Vec<Report>> {
1463    let mut errs = vec![];
1464    let policies = match policies.get_policy_set() {
1465        Ok(pset) => pset,
1466        Err(e) => {
1467            errs.push(e);
1468            PolicySet::new()
1469        }
1470    };
1471    let schema = match schema_filename.map(|f| read_schema_file(f, schema_format)) {
1472        None => None,
1473        Some(Ok(schema)) => Some(schema),
1474        Some(Err(e)) => {
1475            errs.push(e);
1476            None
1477        }
1478    };
1479    let entities = match load_entities(entities_filename, schema.as_ref()) {
1480        Ok(entities) => entities,
1481        Err(e) => {
1482            errs.push(e);
1483            Entities::empty()
1484        }
1485    };
1486    match request.get_request(schema.as_ref()) {
1487        Ok(request) if errs.is_empty() => {
1488            let authorizer = Authorizer::new();
1489            let auth_start = Instant::now();
1490            let ans = authorizer.is_authorized(&request, &policies, &entities);
1491            let auth_dur = auth_start.elapsed();
1492            if compute_duration {
1493                println!(
1494                    "Authorization Time (micro seconds) : {}",
1495                    auth_dur.as_micros()
1496                );
1497            }
1498            Ok(ans)
1499        }
1500        Ok(_) => Err(errs),
1501        Err(e) => {
1502            errs.push(e.wrap_err("failed to parse request"));
1503            Err(errs)
1504        }
1505    }
1506}
1507
1508#[cfg(feature = "partial-eval")]
1509fn execute_partial_request(
1510    request: &PartialRequestArgs,
1511    policies: &PoliciesArgs,
1512    entities_filename: impl AsRef<Path>,
1513    compute_duration: bool,
1514) -> Result<PartialResponse, Vec<Report>> {
1515    let mut errs = vec![];
1516    let policies = match policies.get_policy_set() {
1517        Ok(pset) => pset,
1518        Err(e) => {
1519            errs.push(e);
1520            PolicySet::new()
1521        }
1522    };
1523    let entities = match load_entities(entities_filename, None) {
1524        Ok(entities) => entities,
1525        Err(e) => {
1526            errs.push(e);
1527            Entities::empty()
1528        }
1529    };
1530    match request.get_request() {
1531        Ok(request) if errs.is_empty() => {
1532            let authorizer = Authorizer::new();
1533            let auth_start = Instant::now();
1534            let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
1535            let auth_dur = auth_start.elapsed();
1536            if compute_duration {
1537                println!(
1538                    "Authorization Time (micro seconds) : {}",
1539                    auth_dur.as_micros()
1540                );
1541            }
1542            Ok(ans)
1543        }
1544        Ok(_) => Err(errs),
1545        Err(e) => {
1546            errs.push(e.wrap_err("failed to parse request"));
1547            Err(errs)
1548        }
1549    }
1550}