cedar_policy_cli/
lib.rs

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