Skip to main content

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#![allow(
18    clippy::needless_return,
19    reason = r#"
20    This module makes use of `return` to exit early with a particular exit code.
21    For consistency, it also uses `return` in some places where it could be
22    omitted.
23"#
24)]
25
26use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
27#[cfg(feature = "analyze")]
28use itertools::Itertools;
29use miette::{miette, IntoDiagnostic, NamedSource, Report, Result, WrapErr};
30use owo_colors::OwoColorize;
31use serde::de::{DeserializeSeed, IntoDeserializer};
32use serde::{Deserialize, Deserializer, Serialize};
33use std::collections::BTreeSet;
34use std::io::{BufReader, Write};
35use std::{
36    collections::HashMap,
37    fmt::{self, Display},
38    fs::OpenOptions,
39    path::{Path, PathBuf},
40    process::{ExitCode, Termination},
41    str::FromStr,
42    time::Instant,
43};
44
45use cedar_policy::*;
46use cedar_policy_formatter::{policies_str_to_pretty, Config};
47
48/// Basic Cedar CLI for evaluating authorization queries
49#[derive(Parser, Debug)]
50#[command(author, version, about, long_about = None)] // Pull from `Cargo.toml`
51pub struct Cli {
52    #[command(subcommand)]
53    pub command: Commands,
54    /// The output format to use for error reporting.
55    #[arg(
56        global = true,
57        short = 'f',
58        long = "error-format",
59        env = "CEDAR_ERROR_FORMAT",
60        default_value_t,
61        value_enum
62    )]
63    pub err_fmt: ErrorFormat,
64}
65
66#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
67pub enum ErrorFormat {
68    /// Human-readable error messages with terminal graphics and inline code
69    /// snippets.
70    #[default]
71    Human,
72    /// Plain-text error messages without fancy graphics or colors, suitable for
73    /// screen readers.
74    Plain,
75    /// Machine-readable JSON output.
76    Json,
77}
78
79impl Display for ErrorFormat {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(
82            f,
83            "{}",
84            match self {
85                ErrorFormat::Human => "human",
86                ErrorFormat::Plain => "plain",
87                ErrorFormat::Json => "json",
88            }
89        )
90    }
91}
92
93#[derive(Subcommand, Debug)]
94pub enum Commands {
95    /// Evaluate an authorization request
96    Authorize(AuthorizeArgs),
97    /// Evaluate a Cedar expression
98    Evaluate(EvaluateArgs),
99    /// Validate a policy set against a schema
100    Validate(ValidateArgs),
101    /// Check that policies, expressions, schema, and/or entities successfully parse.
102    /// (All arguments are optional; this checks that whatever is provided parses)
103    ///
104    /// If no arguments are provided, reads policies from stdin and checks that they parse.
105    CheckParse(CheckParseArgs),
106    /// Link a template
107    Link(LinkArgs),
108    /// Format a policy set
109    Format(FormatArgs),
110    /// Translate Cedar policy syntax to JSON policy syntax (except comments)
111    TranslatePolicy(TranslatePolicyArgs),
112    /// Translate Cedar schema syntax to JSON schema syntax and vice versa (except comments)
113    TranslateSchema(TranslateSchemaArgs),
114    /// Visualize a set of JSON entities to the graphviz format.
115    /// Warning: Entity visualization is best-effort and not well tested.
116    Visualize(VisualizeArgs),
117    /// Create a Cedar project
118    New(NewArgs),
119    /// Partially evaluate an authorization request
120    PartiallyAuthorize(PartiallyAuthorizeArgs),
121    /// Partially evaluate an authorization request in a type-aware manner
122    Tpe(TpeArgs),
123    /// Run test cases on a policy set
124    ///
125    /// Tests are defined in a JSON array of objects with the following fields:
126    ///   - name: optional test name string
127    ///   - request: object using the same format as the `--request-json` argument for authorization
128    ///   - entities: array of entity JSON objects in the same format expected by `--entities` argument for authorization
129    ///   - decision: the string "allow" or "deny"
130    ///   - reason: array of policy ID strings expected to contribute to the authorization decision
131    ///   - num_errors: expected number of erroring policies
132    #[clap(verbatim_doc_comment)] // stops clap from dropping newlines in bulleted list
133    RunTests(RunTestsArgs),
134    /// Symbolic analysis of Cedar policies using SymCC
135    Symcc(SymccArgs),
136    /// Print Cedar language version
137    LanguageVersion,
138}
139
140#[derive(Args, Debug)]
141pub struct TranslatePolicyArgs {
142    /// The direction of translation,
143    #[arg(long)]
144    pub direction: PolicyTranslationDirection,
145    /// Filename to read the policies from.
146    /// If not provided, will default to reading stdin.
147    #[arg(short = 'p', long = "policies", value_name = "FILE")]
148    pub input_file: Option<String>,
149}
150
151/// The direction of translation
152#[derive(Debug, Clone, Copy, ValueEnum)]
153pub enum PolicyTranslationDirection {
154    /// Cedar policy syntax -> JSON
155    CedarToJson,
156    /// JSON -> Cedar policy syntax
157    JsonToCedar,
158}
159
160#[derive(Args, Debug)]
161pub struct TranslateSchemaArgs {
162    /// The direction of translation,
163    #[arg(long)]
164    pub direction: SchemaTranslationDirection,
165    /// Filename to read the schema from.
166    /// If not provided, will default to reading stdin.
167    #[arg(short = 's', long = "schema", value_name = "FILE")]
168    pub input_file: Option<String>,
169}
170
171/// The direction of translation
172#[derive(Debug, Clone, Copy, ValueEnum)]
173pub enum SchemaTranslationDirection {
174    /// JSON -> Cedar schema syntax
175    JsonToCedar,
176    /// Cedar schema syntax -> JSON
177    CedarToJson,
178    /// Cedar schema syntax -> JSON with all types resolved to entity or common.
179    ///
180    /// In contrast to `cedar-to-json`, this option requires that every type
181    /// referenced in the schema is also defined.
182    CedarToJsonWithResolvedTypes,
183}
184
185#[derive(Debug, Default, Clone, Copy, ValueEnum)]
186pub enum SchemaFormat {
187    /// the Cedar format
188    #[default]
189    Cedar,
190    /// JSON format
191    Json,
192}
193
194#[derive(Debug, Clone, Copy, ValueEnum)]
195pub enum ValidationMode {
196    /// Strict validation
197    Strict,
198    /// Permissive validation
199    Permissive,
200    /// Partial validation
201    Partial,
202}
203
204#[derive(Args, Debug)]
205pub struct ValidateArgs {
206    /// Schema args (incorporated by reference)
207    #[command(flatten)]
208    pub schema: SchemaArgs,
209    /// Policies args (incorporated by reference)
210    #[command(flatten)]
211    pub policies: PoliciesArgs,
212    /// Report a validation failure for non-fatal warnings
213    #[arg(long)]
214    pub deny_warnings: bool,
215    /// Validate the policy using this mode.
216    /// The options `permissive` and `partial` are experimental
217    /// and will cause the CLI to exit if it was not built with the
218    /// experimental feature `permissive-validate` and `partial-validate`, respectively, enabled.
219    #[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
220    pub validation_mode: ValidationMode,
221    /// Validate the policy at this level.
222    #[arg(long)]
223    pub level: Option<u32>,
224}
225
226#[derive(Args, Debug)]
227pub struct CheckParseArgs {
228    /// Policies args (incorporated by reference)
229    #[command(flatten)]
230    pub policies: OptionalPoliciesArgs,
231    /// Expression to parse
232    #[arg(long)]
233    pub expression: Option<String>,
234    /// Schema args (incorporated by reference)
235    #[command(flatten)]
236    pub schema: OptionalSchemaArgs,
237    /// File containing JSON representation of a Cedar entity hierarchy
238    #[arg(long = "entities", value_name = "FILE")]
239    pub entities_file: Option<PathBuf>,
240}
241
242/// This struct contains the arguments that together specify a request.
243#[derive(Args, Debug)]
244pub struct RequestArgs {
245    /// Principal for the request, e.g., User::"alice"
246    #[arg(short = 'l', long)]
247    pub principal: Option<String>,
248    /// Action for the request, e.g., Action::"view"
249    #[arg(short, long)]
250    pub action: Option<String>,
251    /// Resource for the request, e.g., File::"myfile.txt"
252    #[arg(short, long)]
253    pub resource: Option<String>,
254    /// File containing a JSON object representing the context for the request.
255    /// Should be a (possibly empty) map from keys to values.
256    #[arg(short, long = "context", value_name = "FILE")]
257    pub context_json_file: Option<String>,
258    /// File containing a JSON object representing the entire request. Must have
259    /// fields "principal", "action", "resource", and "context", where "context"
260    /// is a (possibly empty) map from keys to values. This option replaces
261    /// --principal, --action, etc.
262    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
263    pub request_json_file: Option<String>,
264    /// Whether to enable request validation. This has no effect if a schema is
265    /// not provided.
266    #[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
267    pub request_validation: bool,
268}
269
270#[cfg(feature = "tpe")]
271/// This struct contains the arguments that together specify a request.
272#[derive(Args, Debug)]
273pub struct TpeRequestArgs {
274    /// Principal type of the request, e.g., User
275    #[arg(long)]
276    pub principal_type: Option<String>,
277    /// Optional principal eid
278    #[arg(long)]
279    pub principal_eid: Option<String>,
280    /// Action for the request, e.g., Action::"view"
281    #[arg(short, long)]
282    pub action: Option<String>,
283    /// Resource type of the request, e.g., File
284    #[arg(long)]
285    pub resource_type: Option<String>,
286    /// Optional resource eid
287    #[arg(long)]
288    pub resource_eid: Option<String>,
289    /// File containing a JSON object representing the context for the request.
290    /// Should be a (possibly empty) map from keys to values.
291    #[arg(short, long = "context", value_name = "FILE")]
292    pub context_json_file: Option<String>,
293    /// File containing a JSON object representing the entire request. Must have
294    /// fields "principal", "action", "resource", and "context", where "context"
295    /// is a (possibly empty) map from keys to values. This option replaces
296    /// --principal*, --action, etc.
297    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal_type", "principal_eid", "action", "resource_type", "resource_eid", "context_json_file"])]
298    pub request_json_file: Option<String>,
299}
300
301#[cfg(feature = "partial-eval")]
302/// This struct contains the arguments that together specify a request.
303#[derive(Args, Debug)]
304pub struct PartialRequestArgs {
305    /// Principal for the request, e.g., User::"alice"
306    #[arg(short = 'l', long)]
307    pub principal: Option<String>,
308    /// Action for the request, e.g., Action::"view"
309    #[arg(short, long)]
310    pub action: Option<String>,
311    /// Resource for the request, e.g., File::"myfile.txt"
312    #[arg(short, long)]
313    pub resource: Option<String>,
314    /// File containing a JSON object representing the context for the request.
315    /// Should be a (possibly empty) map from keys to values.
316    #[arg(short, long = "context", value_name = "FILE")]
317    pub context_json_file: Option<String>,
318    /// File containing a JSON object representing the entire request. Must have
319    /// fields "principal", "action", "resource", and "context", where "context"
320    /// is a (possibly empty) map from keys to values. This option replaces
321    /// --principal, --action, etc.
322    #[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
323    pub request_json_file: Option<String>,
324}
325
326impl RequestArgs {
327    /// Turn this `RequestArgs` into the appropriate `Request` object
328    ///
329    /// `schema` will be used for schema-based parsing of the context, and also
330    /// (if `self.request_validation` is `true`) for request validation.
331    ///
332    /// `self.request_validation` has no effect if `schema` is `None`.
333    fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
334        match &self.request_json_file {
335            Some(jsonfile) => {
336                let jsonstring = std::fs::read_to_string(jsonfile)
337                    .into_diagnostic()
338                    .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
339                let qjson: RequestJSON = serde_json::from_str(&jsonstring)
340                    .into_diagnostic()
341                    .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
342                let principal = qjson.principal.parse().wrap_err_with(|| {
343                    format!("failed to parse principal in {jsonfile} as entity Uid")
344                })?;
345                let action = qjson.action.parse().wrap_err_with(|| {
346                    format!("failed to parse action in {jsonfile} as entity Uid")
347                })?;
348                let resource = qjson.resource.parse().wrap_err_with(|| {
349                    format!("failed to parse resource in {jsonfile} as entity Uid")
350                })?;
351                let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
352                    .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
353                Request::new(
354                    principal,
355                    action,
356                    resource,
357                    context,
358                    if self.request_validation {
359                        schema
360                    } else {
361                        None
362                    },
363                )
364                .map_err(|e| miette!("{e}"))
365            }
366            None => {
367                let principal = self
368                    .principal
369                    .as_ref()
370                    .map(|s| {
371                        s.parse().wrap_err_with(|| {
372                            format!("failed to parse principal {s} as entity Uid")
373                        })
374                    })
375                    .transpose()?;
376                let action = self
377                    .action
378                    .as_ref()
379                    .map(|s| {
380                        s.parse()
381                            .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
382                    })
383                    .transpose()?;
384                let resource = self
385                    .resource
386                    .as_ref()
387                    .map(|s| {
388                        s.parse()
389                            .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
390                    })
391                    .transpose()?;
392                let context: Context = match &self.context_json_file {
393                    None => Context::empty(),
394                    Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
395                        Ok(f) => Context::from_json_file(
396                            f,
397                            schema.and_then(|s| Some((s, action.as_ref()?))),
398                        )
399                        .wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
400                        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
401                            format!("error while loading context from {jsonfile}")
402                        })?,
403                    },
404                };
405                match (principal, action, resource) {
406                    (Some(principal), Some(action), Some(resource)) => Request::new(
407                        principal,
408                        action,
409                        resource,
410                        context,
411                        if self.request_validation {
412                            schema
413                        } else {
414                            None
415                        },
416                    )
417                    .map_err(|e| miette!("{e}")),
418                    _ => Err(miette!(
419                        "All three (`principal`, `action`, `resource`) variables must be specified"
420                    )),
421                }
422            }
423        }
424    }
425}
426
427#[cfg(feature = "tpe")]
428impl TpeRequestArgs {
429    fn get_request(&self, schema: &Schema) -> Result<PartialRequest> {
430        let qjson: TpeRequestJSON = match self.request_json_file.as_ref() {
431            Some(jsonfile) => {
432                let jsonstring = std::fs::read_to_string(jsonfile)
433                    .into_diagnostic()
434                    .wrap_err_with(|| format!("failed to open request json file {jsonfile}"))?;
435                serde_json::from_str(&jsonstring)
436                    .into_diagnostic()
437                    .wrap_err_with(|| format!("failed to parse context-json file {jsonfile}"))?
438            }
439            None => TpeRequestJSON {
440                principal_type: self
441                    .principal_type
442                    .clone()
443                    .ok_or_else(|| miette!("principal type must be specified"))?,
444                principal_eid: self.principal_eid.clone(),
445                action: self
446                    .action
447                    .clone()
448                    .ok_or_else(|| miette!("action must be specified"))?,
449                resource_type: self
450                    .resource_type
451                    .clone()
452                    .ok_or_else(|| miette!("resource type must be specified"))?,
453                resource_eid: self.resource_eid.clone(),
454                context: self
455                    .context_json_file
456                    .as_ref()
457                    .map(|jsonfile| {
458                        let jsonstring = std::fs::read_to_string(jsonfile)
459                            .into_diagnostic()
460                            .wrap_err_with(|| {
461                                format!("failed to open context-json file {jsonfile}")
462                            })?;
463                        serde_json::from_str(&jsonstring)
464                            .into_diagnostic()
465                            .wrap_err_with(|| {
466                                format!("failed to parse context-json file {jsonfile}")
467                            })
468                    })
469                    .transpose()?,
470            },
471        };
472        let action: EntityUid = qjson.action.parse()?;
473        Ok(PartialRequest::new(
474            PartialEntityUid::new(
475                qjson.principal_type.parse()?,
476                qjson.principal_eid.as_ref().map(EntityId::new),
477            ),
478            action.clone(),
479            PartialEntityUid::new(
480                qjson.resource_type.parse()?,
481                qjson.resource_eid.as_ref().map(EntityId::new),
482            ),
483            qjson
484                .context
485                .map(|val| Context::from_json_value(val, Some((schema, &action))))
486                .transpose()?,
487            schema,
488        )?)
489    }
490}
491
492#[cfg(feature = "partial-eval")]
493impl PartialRequestArgs {
494    fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
495        let mut builder = RequestBuilder::default();
496        let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
497            Some(jsonfile) => {
498                let jsonstring = std::fs::read_to_string(jsonfile)
499                    .into_diagnostic()
500                    .wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
501                serde_json::from_str(&jsonstring)
502                    .into_diagnostic()
503                    .wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
504            }
505            None => PartialRequestJSON {
506                principal: self.principal.clone(),
507                action: self.action.clone(),
508                resource: self.resource.clone(),
509                context: self
510                    .context_json_file
511                    .as_ref()
512                    .map(|jsonfile| {
513                        let jsonstring = std::fs::read_to_string(jsonfile)
514                            .into_diagnostic()
515                            .wrap_err_with(|| {
516                                format!("failed to open context-json file {jsonfile}")
517                            })?;
518                        serde_json::from_str(&jsonstring)
519                            .into_diagnostic()
520                            .wrap_err_with(|| {
521                                format!("failed to parse context-json file {jsonfile}")
522                            })
523                    })
524                    .transpose()?,
525            },
526        };
527
528        if let Some(principal) = qjson
529            .principal
530            .map(|s| {
531                s.parse()
532                    .wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
533            })
534            .transpose()?
535        {
536            builder = builder.principal(principal);
537        }
538
539        let action = qjson
540            .action
541            .map(|s| {
542                s.parse::<EntityUid>()
543                    .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
544            })
545            .transpose()?;
546
547        if let Some(action_ref) = &action {
548            builder = builder.action(action_ref.clone());
549        }
550
551        if let Some(resource) = qjson
552            .resource
553            .map(|s| {
554                s.parse()
555                    .wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
556            })
557            .transpose()?
558        {
559            builder = builder.resource(resource);
560        }
561
562        if let Some(context) = qjson
563            .context
564            .map(|json| {
565                Context::from_json_value(
566                    json.clone(),
567                    schema.and_then(|s| Some((s, action.as_ref()?))),
568                )
569                .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
570            })
571            .transpose()?
572        {
573            builder = builder.context(context);
574        }
575
576        if let Some(schema) = schema {
577            builder
578                .schema(schema)
579                .build()
580                .wrap_err_with(|| "failed to build request with validation".to_string())
581        } else {
582            Ok(builder.build())
583        }
584    }
585}
586
587/// This struct contains the arguments that together specify an input policy or policy set.
588#[derive(Args, Debug)]
589pub struct PoliciesArgs {
590    /// File containing the static Cedar policies and/or templates. If not provided, read policies from stdin.
591    #[arg(short, long = "policies", value_name = "FILE")]
592    pub policies_file: Option<String>,
593    /// Format of policies in the `--policies` file
594    #[arg(long = "policy-format", default_value_t, value_enum)]
595    pub policy_format: PolicyFormat,
596    /// File containing template-linked policies
597    #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
598    pub template_linked_file: Option<String>,
599}
600
601impl PoliciesArgs {
602    /// Turn this `PoliciesArgs` into the appropriate `PolicySet` object
603    fn get_policy_set(&self) -> Result<PolicySet> {
604        let mut pset = match self.policy_format {
605            PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
606            PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
607        }?;
608        if let Some(links_filename) = self.template_linked_file.as_ref() {
609            add_template_links_to_set(links_filename, &mut pset)?;
610        }
611        Ok(pset)
612    }
613}
614
615/// This struct contains the arguments that together specify an input policy or policy set,
616/// for commands where policies are optional.
617#[derive(Args, Debug)]
618pub struct OptionalPoliciesArgs {
619    /// File containing static Cedar policies and/or templates
620    #[arg(short, long = "policies", value_name = "FILE")]
621    pub policies_file: Option<String>,
622    /// Format of policies in the `--policies` file
623    #[arg(long = "policy-format", default_value_t, value_enum)]
624    pub policy_format: PolicyFormat,
625    /// File containing template-linked policies. Ignored if `--policies` is not
626    /// present (because in that case there are no templates to link against)
627    #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
628    pub template_linked_file: Option<String>,
629}
630
631impl OptionalPoliciesArgs {
632    /// Turn this `OptionalPoliciesArgs` into the appropriate `PolicySet`
633    /// object, or `None` if no policies were provided
634    fn get_policy_set(&self) -> Result<Option<PolicySet>> {
635        match &self.policies_file {
636            None => Ok(None),
637            Some(policies_file) => {
638                let pargs = PoliciesArgs {
639                    policies_file: Some(policies_file.clone()),
640                    policy_format: self.policy_format,
641                    template_linked_file: self.template_linked_file.clone(),
642                };
643                pargs.get_policy_set().map(Some)
644            }
645        }
646    }
647}
648
649/// This struct contains the arguments that together specify an input schema.
650#[derive(Args, Debug)]
651pub struct SchemaArgs {
652    /// File containing the schema
653    #[arg(short, long = "schema", value_name = "FILE")]
654    pub schema_file: PathBuf,
655    /// Schema format
656    #[arg(long, value_enum, default_value_t)]
657    pub schema_format: SchemaFormat,
658}
659
660impl SchemaArgs {
661    /// Turn this `SchemaArgs` into the appropriate `Schema` object
662    fn get_schema(&self) -> Result<Schema> {
663        read_schema_from_file(&self.schema_file, self.schema_format)
664    }
665}
666
667/// This struct contains the arguments that together specify an input schema,
668/// for commands where the schema is optional.
669#[derive(Args, Debug)]
670pub struct OptionalSchemaArgs {
671    /// File containing the schema
672    #[arg(short, long = "schema", value_name = "FILE")]
673    pub schema_file: Option<PathBuf>,
674    /// Schema format
675    #[arg(long, value_enum, default_value_t)]
676    pub schema_format: SchemaFormat,
677}
678
679impl OptionalSchemaArgs {
680    /// Turn this `OptionalSchemaArgs` into the appropriate `Schema` object, or `None`
681    fn get_schema(&self) -> Result<Option<Schema>> {
682        let Some(schema_file) = &self.schema_file else {
683            return Ok(None);
684        };
685        read_schema_from_file(schema_file, self.schema_format).map(Some)
686    }
687}
688
689fn read_schema_from_file(path: impl AsRef<Path>, format: SchemaFormat) -> Result<Schema> {
690    let path = path.as_ref();
691    let schema_src = read_from_file(path, "schema")?;
692    match format {
693        SchemaFormat::Json => Schema::from_json_str(&schema_src)
694            .wrap_err_with(|| format!("failed to parse schema from file {}", path.display())),
695        SchemaFormat::Cedar => {
696            let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)
697                .wrap_err_with(|| format!("failed to parse schema from file {}", path.display()))?;
698            for warning in warnings {
699                let report = miette::Report::new(warning);
700                eprintln!("{report:?}");
701            }
702            Ok(schema)
703        }
704    }
705}
706
707#[derive(Args, Debug)]
708pub struct AuthorizeArgs {
709    /// Request args (incorporated by reference)
710    #[command(flatten)]
711    pub request: RequestArgs,
712    /// Policies args (incorporated by reference)
713    #[command(flatten)]
714    pub policies: PoliciesArgs,
715    /// Schema args (incorporated by reference)
716    ///
717    /// Used to populate the store with action entities and for schema-based
718    /// parsing of entity hierarchy, if present
719    #[command(flatten)]
720    pub schema: OptionalSchemaArgs,
721    /// File containing JSON representation of the Cedar entity hierarchy
722    #[arg(long = "entities", value_name = "FILE")]
723    pub entities_file: String,
724    /// More verbose output. (For instance, indicate which policies applied to the request, if any.)
725    #[arg(short, long)]
726    pub verbose: bool,
727    /// Time authorization and report timing information
728    #[arg(short, long)]
729    pub timing: bool,
730}
731
732#[cfg(feature = "tpe")]
733#[derive(Args, Debug)]
734pub struct TpeArgs {
735    /// Request args (incorporated by reference)
736    #[command(flatten)]
737    pub request: TpeRequestArgs,
738    /// Policies args (incorporated by reference)
739    #[command(flatten)]
740    pub policies: PoliciesArgs,
741    /// Schema args (incorporated by reference)
742    ///
743    /// Used to populate the store with action entities and for schema-based
744    /// parsing of entity hierarchy, if present
745    #[command(flatten)]
746    pub schema: SchemaArgs,
747    /// File containing JSON representation of the Cedar entity hierarchy
748    #[arg(long = "entities", value_name = "FILE")]
749    pub entities_file: String,
750    /// Time authorization and report timing information
751    #[arg(short, long)]
752    pub timing: bool,
753}
754
755#[cfg(feature = "partial-eval")]
756#[derive(Args, Debug)]
757pub struct PartiallyAuthorizeArgs {
758    /// Request args (incorporated by reference)
759    #[command(flatten)]
760    pub request: PartialRequestArgs,
761    /// Policies args (incorporated by reference)
762    #[command(flatten)]
763    pub policies: PoliciesArgs,
764    /// Schema args (incorporated by reference)
765    ///
766    /// Used to populate the store with action entities and for schema-based
767    /// parsing of entity hierarchy, if present
768    #[command(flatten)]
769    pub schema: OptionalSchemaArgs,
770    /// File containing JSON representation of the Cedar entity hierarchy
771    #[arg(long = "entities", value_name = "FILE")]
772    pub entities_file: String,
773    /// Time authorization and report timing information
774    #[arg(short, long)]
775    pub timing: bool,
776}
777
778#[cfg(not(feature = "tpe"))]
779#[derive(Debug, Args)]
780pub struct TpeArgs;
781
782#[cfg(not(feature = "partial-eval"))]
783#[derive(Debug, Args)]
784pub struct PartiallyAuthorizeArgs;
785
786#[derive(Args, Debug)]
787pub struct RunTestsArgs {
788    /// Policies args (incorporated by reference)
789    #[command(flatten)]
790    pub policies: PoliciesArgs,
791    /// Tests in JSON format
792    #[arg(long, value_name = "FILE")]
793    pub tests: String,
794    #[command(flatten)]
795    pub schema: OptionalSchemaArgs,
796}
797
798#[derive(Args, Debug)]
799pub struct VisualizeArgs {
800    #[arg(long = "entities", value_name = "FILE")]
801    pub entities_file: String,
802}
803
804#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
805pub enum PolicyFormat {
806    /// The standard Cedar policy format, documented at <https://docs.cedarpolicy.com/policies/syntax-policy.html>
807    #[default]
808    Cedar,
809    /// Cedar's JSON policy format, documented at <https://docs.cedarpolicy.com/policies/json-format.html>
810    Json,
811}
812
813#[derive(Args, Debug)]
814pub struct LinkArgs {
815    /// Policies args (incorporated by reference)
816    #[command(flatten)]
817    pub policies: PoliciesArgs,
818    /// Id of the template to link
819    #[arg(long)]
820    pub template_id: String,
821    /// Id for the new template linked policy
822    #[arg(short, long)]
823    pub new_id: String,
824    /// Arguments to fill slots
825    #[arg(short, long)]
826    pub arguments: Arguments,
827}
828
829#[derive(Args, Debug)]
830pub struct FormatArgs {
831    /// File containing the static Cedar policies and/or templates. If not provided, read policies from stdin.
832    #[arg(short, long = "policies", value_name = "FILE")]
833    pub policies_file: Option<String>,
834
835    /// Custom line width (default: 80).
836    #[arg(short, long, value_name = "UINT", default_value_t = 80)]
837    pub line_width: usize,
838
839    /// Custom indentation width (default: 2).
840    #[arg(short, long, value_name = "INT", default_value_t = 2)]
841    pub indent_width: isize,
842
843    /// Automatically write back the formatted policies to the input file.
844    #[arg(short, long, group = "action", requires = "policies_file")]
845    pub write: bool,
846
847    /// Check that the policies formats without any changes. Mutually exclusive with `write`.
848    #[arg(short, long, group = "action")]
849    pub check: bool,
850}
851
852#[derive(Args, Debug)]
853pub struct NewArgs {
854    /// Name of the Cedar project
855    #[arg(short, long, value_name = "DIR")]
856    pub name: String,
857}
858
859#[derive(Args, Debug)]
860pub struct SymccArgs {
861    #[command(subcommand)]
862    pub command: SymccCommands,
863    /// Path to CVC5 solver executable
864    #[arg(long, env = "CVC5")]
865    pub cvc5_path: Option<PathBuf>,
866    /// Principal entity type (e.g., 'User')
867    #[arg(long)]
868    pub principal_type: String,
869    /// Action entity UID (e.g., 'Action::"view"')
870    #[arg(long)]
871    pub action: String,
872    /// Resource entity type (e.g., 'Photo')
873    #[arg(long)]
874    pub resource_type: String,
875    /// Schema args (shared across all subcommands)
876    #[command(flatten)]
877    pub schema: SchemaArgs,
878    /// Generate counterexamples when verification fails
879    #[arg(long, default_value_t = true, conflicts_with = "no_counterexample")]
880    pub counterexample: bool,
881    /// Don't generate counterexamples when verification fails
882    #[arg(long, default_value_t = false, conflicts_with = "counterexample")]
883    pub no_counterexample: bool,
884    /// Verbose output showing verification details
885    #[arg(short, long)]
886    pub verbose: bool,
887}
888
889#[derive(Subcommand, Debug)]
890pub enum SymccCommands {
891    // --- Single-policy primitives ---
892    /// Verify that a policy never produces runtime errors
893    NeverErrors(SymccPoliciesArgs),
894    /// Verify that a policy always matches (is always true)
895    AlwaysMatches(SymccPoliciesArgs),
896    /// Verify that a policy never matches (is always false)
897    NeverMatches(SymccPoliciesArgs),
898
899    // --- Two-policy comparison primitives ---
900    /// Check if two individual policies have equivalent match conditions
901    MatchesEquivalent(TwoPolicyArgs),
902    /// Check if one policy's match condition implies another's
903    MatchesImplies(TwoPolicyArgs),
904    /// Check if two policies' match conditions are disjoint
905    MatchesDisjoint(TwoPolicyArgs),
906
907    // --- Single-policy-set primitives ---
908    /// Verify that policy set always allows all well-formed requests
909    AlwaysAllows(SymccPoliciesArgs),
910    /// Verify that policy set always denies all well-formed requests
911    AlwaysDenies(SymccPoliciesArgs),
912
913    // --- Two-policy-set comparison primitives ---
914    /// Verify that two policy sets are logically equivalent
915    Equivalent(SymccTwoPoliciesArgs),
916    /// Verify that one policy set implies another (subsumption)
917    Implies(SymccTwoPoliciesArgs),
918    /// Verify that two policy sets are disjoint (no overlapping permissions)
919    Disjoint(SymccTwoPoliciesArgs),
920}
921
922/// This struct contains the arguments that together specify an input policy or policy set without linked policies.
923#[derive(Args, Debug)]
924pub struct SymccPoliciesArgs {
925    /// File containing the Cedar policies. If not provided, read policies from stdin.
926    #[arg(short, long = "policies", value_name = "FILE")]
927    pub policies_file: Option<String>,
928    /// Format of policies in the `--policies` file
929    #[arg(long = "policy-format", default_value_t, value_enum)]
930    pub policy_format: PolicyFormat,
931}
932
933#[cfg(feature = "analyze")]
934impl SymccPoliciesArgs {
935    /// Turn this `SymccPoliciesArgs` into the appropriate `PolicySet` object
936    fn get_policy_set(&self) -> Result<PolicySet> {
937        match self.policy_format {
938            PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
939            PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
940        }
941    }
942}
943
944/// Two-policy comparison: policy inputs
945#[derive(Args, Debug)]
946pub struct TwoPolicyArgs {
947    /// File containing the first Cedar policy
948    #[arg(long = "policy1", value_name = "FILE")]
949    pub policy1_file: Option<String>,
950    /// Format of the first policy file
951    #[arg(long = "policy1-format", default_value_t, value_enum)]
952    pub policy1_format: PolicyFormat,
953    /// File containing the second Cedar policy
954    #[arg(long = "policy2", value_name = "FILE")]
955    pub policy2_file: Option<String>,
956    /// Format of the second policy file
957    #[arg(long = "policy2-format", default_value_t, value_enum)]
958    pub policy2_format: PolicyFormat,
959}
960
961#[cfg(feature = "analyze")]
962impl TwoPolicyArgs {
963    fn get_policy_set_1(&self) -> Result<PolicySet> {
964        let pargs = PoliciesArgs {
965            policies_file: self.policy1_file.clone(),
966            policy_format: self.policy1_format,
967            template_linked_file: None,
968        };
969        pargs.get_policy_set()
970    }
971
972    fn get_policy_set_2(&self) -> Result<PolicySet> {
973        let pargs = PoliciesArgs {
974            policies_file: self.policy2_file.clone(),
975            policy_format: self.policy2_format,
976            template_linked_file: None,
977        };
978        pargs.get_policy_set()
979    }
980}
981
982/// Two policy-set comparison: policy set inputs without linked policies.
983#[derive(Args, Debug)]
984pub struct SymccTwoPoliciesArgs {
985    /// File containing the first policy set
986    #[arg(long = "policies1", value_name = "FILE")]
987    pub policies1_file: Option<String>,
988    /// Format of the first policy set file
989    #[arg(long = "policies1-format", default_value_t, value_enum)]
990    pub policies1_format: PolicyFormat,
991    /// File containing the second policy set
992    #[arg(long = "policies2", value_name = "FILE")]
993    pub policies2_file: Option<String>,
994    /// Format of the second policy set file
995    #[arg(long = "policies2-format", default_value_t, value_enum)]
996    pub policies2_format: PolicyFormat,
997}
998
999#[cfg(feature = "analyze")]
1000impl SymccTwoPoliciesArgs {
1001    fn get_policy_set_1(&self) -> Result<PolicySet> {
1002        let pargs = SymccPoliciesArgs {
1003            policies_file: self.policies1_file.clone(),
1004            policy_format: self.policies1_format,
1005        };
1006        pargs.get_policy_set()
1007    }
1008
1009    fn get_policy_set_2(&self) -> Result<PolicySet> {
1010        let pargs = SymccPoliciesArgs {
1011            policies_file: self.policies2_file.clone(),
1012            policy_format: self.policies2_format,
1013        };
1014        pargs.get_policy_set()
1015    }
1016}
1017
1018/// Wrapper struct
1019#[derive(Clone, Debug, Deserialize)]
1020#[serde(try_from = "HashMap<String,String>")]
1021pub struct Arguments {
1022    pub data: HashMap<SlotId, String>,
1023}
1024
1025impl TryFrom<HashMap<String, String>> for Arguments {
1026    type Error = String;
1027
1028    fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
1029        Ok(Self {
1030            data: value
1031                .into_iter()
1032                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
1033                .collect::<Result<HashMap<SlotId, String>, String>>()?,
1034        })
1035    }
1036}
1037
1038impl FromStr for Arguments {
1039    type Err = serde_json::Error;
1040
1041    fn from_str(s: &str) -> Result<Self, Self::Err> {
1042        serde_json::from_str(s)
1043    }
1044}
1045
1046/// This struct is the serde structure expected for --request-json
1047#[derive(Clone, Debug, Deserialize)]
1048struct RequestJSON {
1049    /// Principal for the request
1050    #[serde(default)]
1051    principal: String,
1052    /// Action for the request
1053    #[serde(default)]
1054    action: String,
1055    /// Resource for the request
1056    #[serde(default)]
1057    resource: String,
1058    /// Context for the request
1059    context: serde_json::Value,
1060}
1061
1062#[cfg(feature = "partial-eval")]
1063/// This struct is the serde structure expected for --request-json
1064#[derive(Deserialize)]
1065struct PartialRequestJSON {
1066    /// Principal for the request
1067    pub(self) principal: Option<String>,
1068    /// Action for the request
1069    pub(self) action: Option<String>,
1070    /// Resource for the request
1071    pub(self) resource: Option<String>,
1072    /// Context for the request
1073    pub(self) context: Option<serde_json::Value>,
1074}
1075
1076#[cfg(feature = "tpe")]
1077// This struct is the serde structure expected for --request-json
1078#[derive(Deserialize)]
1079struct TpeRequestJSON {
1080    // Principal for the request
1081    pub(self) principal_type: String,
1082    // Optional principal eid
1083    pub(self) principal_eid: Option<String>,
1084    // Action for the request
1085    pub(self) action: String,
1086    // Resource for the request
1087    pub(self) resource_type: String,
1088    // Optional resource eid
1089    pub(self) resource_eid: Option<String>,
1090    // Context for the request
1091    pub(self) context: Option<serde_json::Value>,
1092}
1093
1094#[derive(Args, Debug)]
1095pub struct EvaluateArgs {
1096    /// Request args (incorporated by reference)
1097    #[command(flatten)]
1098    pub request: RequestArgs,
1099    /// Schema args (incorporated by reference)
1100    ///
1101    /// Used to populate the store with action entities and for schema-based
1102    /// parsing of entity hierarchy, if present
1103    #[command(flatten)]
1104    pub schema: OptionalSchemaArgs,
1105    /// File containing JSON representation of the Cedar entity hierarchy.
1106    /// This is optional; if not present, we'll just use an empty hierarchy.
1107    #[arg(long = "entities", value_name = "FILE")]
1108    pub entities_file: Option<String>,
1109    /// Expression to evaluate
1110    #[arg(value_name = "EXPRESSION")]
1111    pub expression: String,
1112}
1113
1114#[derive(Eq, PartialEq, Debug, Copy, Clone)]
1115pub enum CedarExitCode {
1116    // The command completed successfully with a result other than a
1117    // authorization deny or validation failure.
1118    Success,
1119    // The command failed to complete successfully.
1120    Failure,
1121    // The command completed successfully, but the result of the authorization
1122    // request was DENY.
1123    AuthorizeDeny,
1124    // The command completed successfully, but it detected a validation failure
1125    // in the given schema and policies.
1126    ValidationFailure,
1127    #[cfg(any(feature = "partial-eval", feature = "tpe"))]
1128    // The command completed successfully with an incomplete result, e.g.,
1129    // partial authorization result is not determining.
1130    Unknown,
1131}
1132
1133impl Termination for CedarExitCode {
1134    fn report(self) -> ExitCode {
1135        match self {
1136            CedarExitCode::Success => ExitCode::SUCCESS,
1137            CedarExitCode::Failure => ExitCode::FAILURE,
1138            CedarExitCode::AuthorizeDeny => ExitCode::from(2),
1139            CedarExitCode::ValidationFailure => ExitCode::from(3),
1140            #[cfg(any(feature = "partial-eval", feature = "tpe"))]
1141            CedarExitCode::Unknown => ExitCode::SUCCESS,
1142        }
1143    }
1144}
1145
1146pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
1147    // for backwards compatibility: if no policies/schema/entities/expression
1148    // are provided, read policies from stdin and check that they parse
1149    if args.policies.policies_file.is_none()
1150        && args.schema.schema_file.is_none()
1151        && args.entities_file.is_none()
1152        && args.expression.is_none()
1153    {
1154        let pargs = PoliciesArgs {
1155            policies_file: None, // read from stdin
1156            policy_format: args.policies.policy_format,
1157            template_linked_file: args.policies.template_linked_file.clone(),
1158        };
1159        match pargs.get_policy_set() {
1160            Ok(_) => return CedarExitCode::Success,
1161            Err(e) => {
1162                println!("{e:?}");
1163                return CedarExitCode::Failure;
1164            }
1165        }
1166    }
1167
1168    #[expect(
1169        clippy::useless_let_if_seq,
1170        reason = "exit_code is mutated by later expressions"
1171    )]
1172    let mut exit_code = CedarExitCode::Success;
1173    if let Err(e) = args.policies.get_policy_set() {
1174        println!("{e:?}");
1175        exit_code = CedarExitCode::Failure;
1176    }
1177    if let Some(e) = args
1178        .expression
1179        .as_ref()
1180        .and_then(|expr| Expression::from_str(expr).err())
1181    {
1182        println!("{:?}", Report::new(e));
1183        exit_code = CedarExitCode::Failure;
1184    }
1185    let schema = match args.schema.get_schema() {
1186        Ok(schema) => schema,
1187        Err(e) => {
1188            println!("{e:?}");
1189            exit_code = CedarExitCode::Failure;
1190            None
1191        }
1192    };
1193    if let Some(e) = args
1194        .entities_file
1195        .as_ref()
1196        .and_then(|e| load_entities(e, schema.as_ref()).err())
1197    {
1198        println!("{e:?}");
1199        exit_code = CedarExitCode::Failure;
1200    }
1201    exit_code
1202}
1203
1204pub fn validate(args: &ValidateArgs) -> CedarExitCode {
1205    let mode = match args.validation_mode {
1206        ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
1207        ValidationMode::Permissive => {
1208            #[cfg(not(feature = "permissive-validate"))]
1209            {
1210                eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
1211                return CedarExitCode::Failure;
1212            }
1213            #[cfg(feature = "permissive-validate")]
1214            cedar_policy::ValidationMode::Permissive
1215        }
1216        ValidationMode::Partial => {
1217            #[cfg(not(feature = "partial-validate"))]
1218            {
1219                eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
1220                return CedarExitCode::Failure;
1221            }
1222            #[cfg(feature = "partial-validate")]
1223            cedar_policy::ValidationMode::Partial
1224        }
1225    };
1226
1227    let pset = match args.policies.get_policy_set() {
1228        Ok(pset) => pset,
1229        Err(e) => {
1230            println!("{e:?}");
1231            return CedarExitCode::Failure;
1232        }
1233    };
1234
1235    let schema = match args.schema.get_schema() {
1236        Ok(schema) => schema,
1237        Err(e) => {
1238            println!("{e:?}");
1239            return CedarExitCode::Failure;
1240        }
1241    };
1242
1243    let validator = Validator::new(schema);
1244
1245    let result = if let Some(level) = args.level {
1246        validator.validate_with_level(&pset, mode, level)
1247    } else {
1248        validator.validate(&pset, mode)
1249    };
1250
1251    if !result.validation_passed()
1252        || (args.deny_warnings && !result.validation_passed_without_warnings())
1253    {
1254        println!(
1255            "{:?}",
1256            Report::new(result).wrap_err("policy set validation failed")
1257        );
1258        CedarExitCode::ValidationFailure
1259    } else {
1260        println!(
1261            "{:?}",
1262            Report::new(result).wrap_err("policy set validation passed")
1263        );
1264        CedarExitCode::Success
1265    }
1266}
1267
1268pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
1269    println!();
1270    let schema = match args.schema.get_schema() {
1271        Ok(opt) => opt,
1272        Err(e) => {
1273            println!("{e:?}");
1274            return (CedarExitCode::Failure, EvalResult::Bool(false));
1275        }
1276    };
1277    let request = match args.request.get_request(schema.as_ref()) {
1278        Ok(q) => q,
1279        Err(e) => {
1280            println!("{e:?}");
1281            return (CedarExitCode::Failure, EvalResult::Bool(false));
1282        }
1283    };
1284    let expr =
1285        match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
1286            Ok(expr) => expr,
1287            Err(e) => {
1288                println!("{:?}", e.with_source_code(args.expression.clone()));
1289                return (CedarExitCode::Failure, EvalResult::Bool(false));
1290            }
1291        };
1292    let entities = match &args.entities_file {
1293        None => Entities::empty(),
1294        Some(file) => match load_entities(file, schema.as_ref()) {
1295            Ok(entities) => entities,
1296            Err(e) => {
1297                println!("{e:?}");
1298                return (CedarExitCode::Failure, EvalResult::Bool(false));
1299            }
1300        },
1301    };
1302    match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
1303    {
1304        Err(e) => {
1305            println!("{e:?}");
1306            return (CedarExitCode::Failure, EvalResult::Bool(false));
1307        }
1308        Ok(result) => {
1309            println!("{result}");
1310            return (CedarExitCode::Success, result);
1311        }
1312    }
1313}
1314
1315pub fn link(args: &LinkArgs) -> CedarExitCode {
1316    if let Err(err) = link_inner(args) {
1317        println!("{err:?}");
1318        CedarExitCode::Failure
1319    } else {
1320        CedarExitCode::Success
1321    }
1322}
1323
1324pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
1325    match load_entities(&args.entities_file, None) {
1326        Ok(entities) => {
1327            println!("{}", entities.to_dot_str());
1328            CedarExitCode::Success
1329        }
1330        Err(report) => {
1331            eprintln!("{report:?}");
1332            CedarExitCode::Failure
1333        }
1334    }
1335}
1336
1337/// Format the policies in the given file or stdin.
1338///
1339/// Returns a boolean indicating whether the formatted policies are the same as the original
1340/// policies.
1341fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
1342    let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
1343    let config = Config {
1344        line_width: args.line_width,
1345        indent_width: args.indent_width,
1346    };
1347    let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
1348    let are_policies_equivalent = policies_str == formatted_policy;
1349
1350    match &args.policies_file {
1351        Some(policies_file) if args.write => {
1352            let mut file = OpenOptions::new()
1353                .write(true)
1354                .truncate(true)
1355                .open(policies_file)
1356                .into_diagnostic()
1357                .wrap_err(format!("failed to open {policies_file} for writing"))?;
1358            file.write_all(formatted_policy.as_bytes())
1359                .into_diagnostic()
1360                .wrap_err(format!(
1361                    "failed to write formatted policies to {policies_file}"
1362                ))?;
1363        }
1364        _ => print!("{formatted_policy}"),
1365    }
1366    Ok(are_policies_equivalent)
1367}
1368
1369pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
1370    match format_policies_inner(args) {
1371        Ok(false) if args.check => CedarExitCode::Failure,
1372        Err(err) => {
1373            println!("{err:?}");
1374            CedarExitCode::Failure
1375        }
1376        _ => CedarExitCode::Success,
1377    }
1378}
1379
1380fn translate_policy_to_cedar(
1381    json_src: Option<impl AsRef<Path> + std::marker::Copy>,
1382) -> Result<String> {
1383    let policy_set = read_json_policy_set(json_src)?;
1384    policy_set.to_cedar().ok_or_else(|| {
1385        miette!("Unable to translate policy set containing template linked policies.")
1386    })
1387}
1388
1389fn translate_policy_to_json(
1390    cedar_src: Option<impl AsRef<Path> + std::marker::Copy>,
1391) -> Result<String> {
1392    let policy_set = read_cedar_policy_set(cedar_src)?;
1393    let output = policy_set.to_json()?.to_string();
1394    Ok(output)
1395}
1396
1397fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
1398    let translate = match args.direction {
1399        PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
1400        PolicyTranslationDirection::JsonToCedar => translate_policy_to_cedar,
1401    };
1402    translate(args.input_file.as_ref())
1403}
1404
1405pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
1406    match translate_policy_inner(args) {
1407        Ok(sf) => {
1408            println!("{sf}");
1409            CedarExitCode::Success
1410        }
1411        Err(err) => {
1412            eprintln!("{err:?}");
1413            CedarExitCode::Failure
1414        }
1415    }
1416}
1417
1418fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
1419    let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
1420    let output = fragment.to_cedarschema()?;
1421    Ok(output)
1422}
1423
1424fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
1425    let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
1426    for warning in warnings {
1427        let report = miette::Report::new(warning);
1428        eprintln!("{report:?}");
1429    }
1430    let output = fragment.to_json_string()?;
1431    Ok(output)
1432}
1433
1434fn translate_schema_to_json_with_resolved_types(cedar_src: impl AsRef<str>) -> Result<String> {
1435    match cedar_policy::schema_str_to_json_with_resolved_types(cedar_src.as_ref()) {
1436        Ok((json_value, warnings)) => {
1437            // Output warnings to stderr
1438            for warning in &warnings {
1439                eprintln!("{warning}");
1440            }
1441
1442            // Serialize to JSON with pretty formatting
1443            serde_json::to_string_pretty(&json_value).into_diagnostic()
1444        }
1445        Err(error) => {
1446            // Convert CedarSchemaError to miette::Report to preserve all diagnostic information
1447            Err(miette::Report::new(error))
1448        }
1449    }
1450}
1451
1452fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
1453    let translate = match args.direction {
1454        SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
1455        SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
1456        SchemaTranslationDirection::CedarToJsonWithResolvedTypes => {
1457            translate_schema_to_json_with_resolved_types
1458        }
1459    };
1460    read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate)
1461}
1462
1463pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
1464    match translate_schema_inner(args) {
1465        Ok(sf) => {
1466            println!("{sf}");
1467            CedarExitCode::Success
1468        }
1469        Err(err) => {
1470            eprintln!("{err:?}");
1471            CedarExitCode::Failure
1472        }
1473    }
1474}
1475
1476/// Write a schema (in JSON format) to `path`
1477fn generate_schema(path: &Path) -> Result<()> {
1478    std::fs::write(
1479        path,
1480        serde_json::to_string_pretty(&serde_json::json!(
1481        {
1482            "": {
1483                "entityTypes": {
1484                    "A": {
1485                        "memberOfTypes": [
1486                            "B"
1487                        ]
1488                    },
1489                    "B": {
1490                        "memberOfTypes": []
1491                    },
1492                    "C": {
1493                        "memberOfTypes": []
1494                    }
1495                },
1496                "actions": {
1497                    "action": {
1498                        "appliesTo": {
1499                            "resourceTypes": [
1500                                "C"
1501                            ],
1502                            "principalTypes": [
1503                                "A",
1504                                "B"
1505                            ]
1506                        }
1507                    }
1508                }
1509            }
1510        }))
1511        .into_diagnostic()?,
1512    )
1513    .into_diagnostic()
1514}
1515
1516fn generate_policy(path: &Path) -> Result<()> {
1517    std::fs::write(
1518        path,
1519        r#"permit (
1520  principal in A::"a",
1521  action == Action::"action",
1522  resource == C::"c"
1523) when { true };
1524"#,
1525    )
1526    .into_diagnostic()
1527}
1528
1529fn generate_entities(path: &Path) -> Result<()> {
1530    std::fs::write(
1531        path,
1532        serde_json::to_string_pretty(&serde_json::json!(
1533        [
1534            {
1535                "uid": { "type": "A", "id": "a"} ,
1536                "attrs": {},
1537                "parents": [{"type": "B", "id": "b"}]
1538            },
1539            {
1540                "uid": { "type": "B", "id": "b"} ,
1541                "attrs": {},
1542                "parents": []
1543            },
1544            {
1545                "uid": { "type": "C", "id": "c"} ,
1546                "attrs": {},
1547                "parents": []
1548            }
1549        ]))
1550        .into_diagnostic()?,
1551    )
1552    .into_diagnostic()
1553}
1554
1555fn new_inner(args: &NewArgs) -> Result<()> {
1556    let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
1557    std::fs::create_dir(dir).into_diagnostic()?;
1558    let schema_path = dir.join("schema.cedarschema.json");
1559    let policy_path = dir.join("policy.cedar");
1560    let entities_path = dir.join("entities.json");
1561    generate_schema(&schema_path)?;
1562    generate_policy(&policy_path)?;
1563    generate_entities(&entities_path)
1564}
1565
1566pub fn new(args: &NewArgs) -> CedarExitCode {
1567    if let Err(err) = new_inner(args) {
1568        println!("{err:?}");
1569        CedarExitCode::Failure
1570    } else {
1571        CedarExitCode::Success
1572    }
1573}
1574
1575pub fn language_version() -> CedarExitCode {
1576    let version = get_lang_version();
1577    println!(
1578        "Cedar language version: {}.{}",
1579        version.major, version.minor
1580    );
1581    CedarExitCode::Success
1582}
1583
1584#[cfg(not(feature = "analyze"))]
1585pub fn symcc(_: &SymccArgs) -> CedarExitCode {
1586    eprintln!("Cannot run `symcc`: this Cedar CLI was built without the 'analyze' feature enabled");
1587    CedarExitCode::Failure
1588}
1589
1590#[cfg(feature = "analyze")]
1591pub fn symcc(args: &SymccArgs) -> CedarExitCode {
1592    let rt = match tokio::runtime::Builder::new_multi_thread()
1593        .enable_all()
1594        .build()
1595    {
1596        Ok(rt) => rt,
1597        Err(e) => {
1598            eprintln!("Failed to initialize async runtime: {e}");
1599            return CedarExitCode::Failure;
1600        }
1601    };
1602
1603    rt.block_on(async {
1604        match symcc_async(args).await {
1605            Ok(()) => CedarExitCode::Success,
1606            Err(e) => {
1607                eprintln!("Analysis failed: {e:?}");
1608                CedarExitCode::Failure
1609            }
1610        }
1611    })
1612}
1613
1614#[cfg(feature = "analyze")]
1615fn initialize_solver(
1616    cvc5_path: &Option<PathBuf>,
1617) -> Result<cedar_policy_symcc::solver::LocalSolver> {
1618    match cvc5_path {
1619        Some(p) => cedar_policy_symcc::solver::LocalSolver::from_command(
1620            tokio::process::Command::new(p).args(["--lang", "smt", "--tlimit=60000"]),
1621        )
1622        .map_err(|e| {
1623            miette!(
1624                "CVC5 solver not found or failed to start at '{}': {e}",
1625                p.display()
1626            )
1627        }),
1628        None => cedar_policy_symcc::solver::LocalSolver::cvc5()
1629            .map_err(|e| miette!("CVC5 solver not found or failed to start: {e}")),
1630    }
1631}
1632
1633#[cfg(feature = "analyze")]
1634fn warn_if_contains_templates(pset: &PolicySet, name: &str) {
1635    let num_templates = pset.templates().count();
1636    if num_templates > 0 {
1637        let report = miette!(
1638            severity = miette::Severity::Warning,
1639            "{name} contains {num_templates} policy template(s), which will be ignored by analysis"
1640        );
1641        eprintln!("{report:?}");
1642    }
1643}
1644
1645#[cfg(feature = "analyze")]
1646fn load_single_policy(
1647    policies: &SymccPoliciesArgs,
1648    schema_args: &SchemaArgs,
1649) -> Result<(Policy, Schema)> {
1650    let pset = policies.get_policy_set()?;
1651    let schema = schema_args.get_schema()?;
1652    let policy = pset
1653        .policies()
1654        .exactly_one()
1655        .map_err(|e| miette!("Expected exactly one policy, found {}", e.count()))?
1656        .clone();
1657    Ok((policy, schema))
1658}
1659
1660#[cfg(feature = "analyze")]
1661fn load_two_policies(
1662    args: &TwoPolicyArgs,
1663    schema_args: &SchemaArgs,
1664) -> Result<(Policy, Policy, Schema)> {
1665    let pset1 = args.get_policy_set_1()?;
1666    let pset2 = args.get_policy_set_2()?;
1667    let schema = schema_args.get_schema()?;
1668    let p1 = pset1
1669        .policies()
1670        .exactly_one()
1671        .map_err(|e| {
1672            miette!(
1673                "Expected exactly one policy in --policy1, found {}",
1674                e.count()
1675            )
1676        })?
1677        .clone();
1678    let p2 = pset2
1679        .policies()
1680        .exactly_one()
1681        .map_err(|e| {
1682            miette!(
1683                "Expected exactly one policy in --policy2, found {}",
1684                e.count()
1685            )
1686        })?
1687        .clone();
1688    Ok((p1, p2, schema))
1689}
1690
1691#[cfg(feature = "analyze")]
1692fn load_policy_set(
1693    policies: &SymccPoliciesArgs,
1694    schema_args: &SchemaArgs,
1695) -> Result<(PolicySet, Schema)> {
1696    let pset = policies.get_policy_set()?;
1697    warn_if_contains_templates(&pset, "policy set");
1698    let schema = schema_args.get_schema()?;
1699    Ok((pset, schema))
1700}
1701
1702#[cfg(feature = "analyze")]
1703fn load_two_policy_sets(
1704    args: &SymccTwoPoliciesArgs,
1705    schema_args: &SchemaArgs,
1706) -> Result<(PolicySet, PolicySet, Schema)> {
1707    let pset1 = args.get_policy_set_1()?;
1708    let pset2 = args.get_policy_set_2()?;
1709    warn_if_contains_templates(&pset1, "first policy set");
1710    warn_if_contains_templates(&pset2, "second policy set");
1711    let schema = schema_args.get_schema()?;
1712    Ok((pset1, pset2, schema))
1713}
1714
1715#[cfg(feature = "analyze")]
1716fn format_bool_result(holds: bool, property: &str) {
1717    if holds {
1718        println!("✓ {property}: VERIFIED");
1719    } else {
1720        println!("✗ {property}: DOES NOT HOLD");
1721    }
1722}
1723
1724#[cfg(feature = "analyze")]
1725fn format_counterexample_result(
1726    cex: Option<cedar_policy_symcc::Env>,
1727    property: &str,
1728    verbose: bool,
1729) {
1730    match cex {
1731        None => {
1732            println!("✓ {property}: VERIFIED");
1733            if verbose {
1734                println!("  No counterexample found — property holds for all well-formed inputs.");
1735            }
1736        }
1737        Some(env) => {
1738            println!("✗ {property}: DOES NOT HOLD");
1739            println!("  Counterexample found:");
1740            println!("{env}");
1741        }
1742    }
1743}
1744
1745#[cfg(feature = "analyze")]
1746fn build_request_env(args: &SymccArgs) -> Result<RequestEnv> {
1747    let principal_type: EntityTypeName = args
1748        .principal_type
1749        .parse()
1750        .map_err(|e| miette!("Invalid --principal-type '{}': {e}", args.principal_type))?;
1751    let action: EntityUid = args
1752        .action
1753        .parse()
1754        .map_err(|e| miette!("Invalid --action '{}': {e}", args.action))?;
1755    let resource_type: EntityTypeName = args
1756        .resource_type
1757        .parse()
1758        .map_err(|e| miette!("Invalid --resource-type '{}': {e}", args.resource_type))?;
1759    Ok(RequestEnv::new(principal_type, action, resource_type))
1760}
1761
1762#[cfg(feature = "analyze")]
1763async fn symcc_async(args: &SymccArgs) -> Result<()> {
1764    use cedar_policy_symcc::{CedarSymCompiler, CompiledPolicy, CompiledPolicySet};
1765
1766    let solver = initialize_solver(&args.cvc5_path)?;
1767    let mut compiler = CedarSymCompiler::new(solver)
1768        .map_err(|e| miette!("Failed to initialize SymCC compiler: {e}"))?;
1769    let req_env = build_request_env(args)?;
1770
1771    match &args.command {
1772        // --- Single-policy primitives ---
1773        SymccCommands::NeverErrors(cmd_args) => {
1774            let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
1775            let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
1776                .map_err(|e| miette!("Failed to compile policy: {e}"))?;
1777            if args.counterexample && !args.no_counterexample {
1778                let result = compiler
1779                    .check_never_errors_with_counterexample_opt(&compiled)
1780                    .await
1781                    .map_err(|e| miette!("Verification failed: {e}"))?;
1782                format_counterexample_result(result, "Policy never errors", args.verbose);
1783            } else {
1784                let holds = compiler
1785                    .check_never_errors_opt(&compiled)
1786                    .await
1787                    .map_err(|e| miette!("Verification failed: {e}"))?;
1788                format_bool_result(holds, "Policy never errors");
1789            }
1790        }
1791        SymccCommands::AlwaysMatches(cmd_args) => {
1792            let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
1793            let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
1794                .map_err(|e| miette!("Failed to compile policy: {e}"))?;
1795            if args.counterexample && !args.no_counterexample {
1796                let result = compiler
1797                    .check_always_matches_with_counterexample_opt(&compiled)
1798                    .await
1799                    .map_err(|e| miette!("Verification failed: {e}"))?;
1800                format_counterexample_result(result, "Policy always matches", args.verbose);
1801            } else {
1802                let holds = compiler
1803                    .check_always_matches_opt(&compiled)
1804                    .await
1805                    .map_err(|e| miette!("Verification failed: {e}"))?;
1806                format_bool_result(holds, "Policy always matches");
1807            }
1808        }
1809        SymccCommands::NeverMatches(cmd_args) => {
1810            let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
1811            let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
1812                .map_err(|e| miette!("Failed to compile policy: {e}"))?;
1813            if args.counterexample && !args.no_counterexample {
1814                let result = compiler
1815                    .check_never_matches_with_counterexample_opt(&compiled)
1816                    .await
1817                    .map_err(|e| miette!("Verification failed: {e}"))?;
1818                format_counterexample_result(result, "Policy never matches", args.verbose);
1819            } else {
1820                let holds = compiler
1821                    .check_never_matches_opt(&compiled)
1822                    .await
1823                    .map_err(|e| miette!("Verification failed: {e}"))?;
1824                format_bool_result(holds, "Policy never matches");
1825            }
1826        }
1827
1828        // --- Two-policy comparison primitives ---
1829        SymccCommands::MatchesEquivalent(cmd_args) => {
1830            let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
1831            let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
1832                .map_err(|e| miette!("Failed to compile policy1: {e}"))?;
1833            let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
1834                .map_err(|e| miette!("Failed to compile policy2: {e}"))?;
1835            if args.counterexample && !args.no_counterexample {
1836                let result = compiler
1837                    .check_matches_equivalent_with_counterexample_opt(&compiled1, &compiled2)
1838                    .await
1839                    .map_err(|e| miette!("Verification failed: {e}"))?;
1840                format_counterexample_result(
1841                    result,
1842                    "Policies have equivalent match conditions",
1843                    args.verbose,
1844                );
1845            } else {
1846                let holds = compiler
1847                    .check_matches_equivalent_opt(&compiled1, &compiled2)
1848                    .await
1849                    .map_err(|e| miette!("Verification failed: {e}"))?;
1850                format_bool_result(holds, "Policies have equivalent match conditions");
1851            }
1852        }
1853        SymccCommands::MatchesImplies(cmd_args) => {
1854            let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
1855            let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
1856                .map_err(|e| miette!("Failed to compile policy1: {e}"))?;
1857            let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
1858                .map_err(|e| miette!("Failed to compile policy2: {e}"))?;
1859            if args.counterexample && !args.no_counterexample {
1860                let result = compiler
1861                    .check_matches_implies_with_counterexample_opt(&compiled1, &compiled2)
1862                    .await
1863                    .map_err(|e| miette!("Verification failed: {e}"))?;
1864                format_counterexample_result(
1865                    result,
1866                    "Policy1 match implies Policy2 match",
1867                    args.verbose,
1868                );
1869            } else {
1870                let holds = compiler
1871                    .check_matches_implies_opt(&compiled1, &compiled2)
1872                    .await
1873                    .map_err(|e| miette!("Verification failed: {e}"))?;
1874                format_bool_result(holds, "Policy1 match implies Policy2 match");
1875            }
1876        }
1877        SymccCommands::MatchesDisjoint(cmd_args) => {
1878            let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
1879            let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
1880                .map_err(|e| miette!("Failed to compile policy1: {e}"))?;
1881            let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
1882                .map_err(|e| miette!("Failed to compile policy2: {e}"))?;
1883            if args.counterexample && !args.no_counterexample {
1884                let result = compiler
1885                    .check_matches_disjoint_with_counterexample_opt(&compiled1, &compiled2)
1886                    .await
1887                    .map_err(|e| miette!("Verification failed: {e}"))?;
1888                format_counterexample_result(
1889                    result,
1890                    "Policies have disjoint match conditions",
1891                    args.verbose,
1892                );
1893            } else {
1894                let holds = compiler
1895                    .check_matches_disjoint_opt(&compiled1, &compiled2)
1896                    .await
1897                    .map_err(|e| miette!("Verification failed: {e}"))?;
1898                format_bool_result(holds, "Policies have disjoint match conditions");
1899            }
1900        }
1901
1902        // --- Single-policy-set primitives ---
1903        SymccCommands::AlwaysAllows(cmd_args) => {
1904            let (pset, schema) = load_policy_set(cmd_args, &args.schema)?;
1905            let compiled = CompiledPolicySet::compile(&pset, &req_env, &schema)
1906                .map_err(|e| miette!("Failed to compile policy set: {e}"))?;
1907            if args.counterexample && !args.no_counterexample {
1908                let result = compiler
1909                    .check_always_allows_with_counterexample_opt(&compiled)
1910                    .await
1911                    .map_err(|e| miette!("Verification failed: {e}"))?;
1912                format_counterexample_result(result, "Policy set always allows", args.verbose);
1913            } else {
1914                let holds = compiler
1915                    .check_always_allows_opt(&compiled)
1916                    .await
1917                    .map_err(|e| miette!("Verification failed: {e}"))?;
1918                format_bool_result(holds, "Policy set always allows");
1919            }
1920        }
1921        SymccCommands::AlwaysDenies(cmd_args) => {
1922            let (pset, schema) = load_policy_set(cmd_args, &args.schema)?;
1923            let compiled = CompiledPolicySet::compile(&pset, &req_env, &schema)
1924                .map_err(|e| miette!("Failed to compile policy set: {e}"))?;
1925            if args.counterexample && !args.no_counterexample {
1926                let result = compiler
1927                    .check_always_denies_with_counterexample_opt(&compiled)
1928                    .await
1929                    .map_err(|e| miette!("Verification failed: {e}"))?;
1930                format_counterexample_result(result, "Policy set always denies", args.verbose);
1931            } else {
1932                let holds = compiler
1933                    .check_always_denies_opt(&compiled)
1934                    .await
1935                    .map_err(|e| miette!("Verification failed: {e}"))?;
1936                format_bool_result(holds, "Policy set always denies");
1937            }
1938        }
1939
1940        // --- Two-policy-set primitives ---
1941        SymccCommands::Equivalent(cmd_args) => {
1942            let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
1943            let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
1944                .map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
1945            let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
1946                .map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
1947            if args.counterexample && !args.no_counterexample {
1948                let result = compiler
1949                    .check_equivalent_with_counterexample_opt(&compiled1, &compiled2)
1950                    .await
1951                    .map_err(|e| miette!("Verification failed: {e}"))?;
1952                format_counterexample_result(result, "Policy sets are equivalent", args.verbose);
1953            } else {
1954                let holds = compiler
1955                    .check_equivalent_opt(&compiled1, &compiled2)
1956                    .await
1957                    .map_err(|e| miette!("Verification failed: {e}"))?;
1958                format_bool_result(holds, "Policy sets are equivalent");
1959            }
1960        }
1961        SymccCommands::Implies(cmd_args) => {
1962            let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
1963            let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
1964                .map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
1965            let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
1966                .map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
1967            if args.counterexample && !args.no_counterexample {
1968                let result = compiler
1969                    .check_implies_with_counterexample_opt(&compiled1, &compiled2)
1970                    .await
1971                    .map_err(|e| miette!("Verification failed: {e}"))?;
1972                format_counterexample_result(
1973                    result,
1974                    "Policy set 1 implies policy set 2",
1975                    args.verbose,
1976                );
1977            } else {
1978                let holds = compiler
1979                    .check_implies_opt(&compiled1, &compiled2)
1980                    .await
1981                    .map_err(|e| miette!("Verification failed: {e}"))?;
1982                format_bool_result(holds, "Policy set 1 implies policy set 2");
1983            }
1984        }
1985        SymccCommands::Disjoint(cmd_args) => {
1986            let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
1987            let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
1988                .map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
1989            let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
1990                .map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
1991            if args.counterexample && !args.no_counterexample {
1992                let result = compiler
1993                    .check_disjoint_with_counterexample_opt(&compiled1, &compiled2)
1994                    .await
1995                    .map_err(|e| miette!("Verification failed: {e}"))?;
1996                format_counterexample_result(result, "Policy sets are disjoint", args.verbose);
1997            } else {
1998                let holds = compiler
1999                    .check_disjoint_opt(&compiled1, &compiled2)
2000                    .await
2001                    .map_err(|e| miette!("Verification failed: {e}"))?;
2002                format_bool_result(holds, "Policy sets are disjoint");
2003            }
2004        }
2005    }
2006
2007    Ok(())
2008}
2009
2010fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
2011    data.iter()
2012        .map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
2013        .collect::<Result<HashMap<SlotId, EntityUid>>>()
2014}
2015
2016fn link_inner(args: &LinkArgs) -> Result<()> {
2017    let mut policies = args.policies.get_policy_set()?;
2018    let slotenv = create_slot_env(&args.arguments.data)?;
2019    policies.link(
2020        PolicyId::new(&args.template_id),
2021        PolicyId::new(&args.new_id),
2022        slotenv,
2023    )?;
2024    let linked = policies
2025        .policy(&PolicyId::new(&args.new_id))
2026        .ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
2027    println!("Template-linked policy added: {linked}");
2028
2029    // If a `--template-linked` / `-k` option was provided, update that file with the new link
2030    if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
2031        update_template_linked_file(
2032            links_filename,
2033            TemplateLinked {
2034                template_id: args.template_id.clone(),
2035                link_id: args.new_id.clone(),
2036                args: args.arguments.data.clone(),
2037            },
2038        )?;
2039    }
2040
2041    Ok(())
2042}
2043
2044#[derive(Clone, Serialize, Deserialize, Debug)]
2045#[serde(try_from = "LiteralTemplateLinked")]
2046#[serde(into = "LiteralTemplateLinked")]
2047struct TemplateLinked {
2048    template_id: String,
2049    link_id: String,
2050    args: HashMap<SlotId, String>,
2051}
2052
2053impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
2054    type Error = String;
2055
2056    fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
2057        Ok(Self {
2058            template_id: value.template_id,
2059            link_id: value.link_id,
2060            args: value
2061                .args
2062                .into_iter()
2063                .map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
2064                .collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
2065        })
2066    }
2067}
2068
2069fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
2070    match s.as_ref() {
2071        "?principal" => Ok(SlotId::principal()),
2072        "?resource" => Ok(SlotId::resource()),
2073        _ => Err(format!(
2074            "Invalid SlotId! Expected ?principal|?resource, got: {}",
2075            s.as_ref()
2076        )),
2077    }
2078}
2079
2080#[derive(Serialize, Deserialize)]
2081struct LiteralTemplateLinked {
2082    template_id: String,
2083    link_id: String,
2084    args: HashMap<String, String>,
2085}
2086
2087impl From<TemplateLinked> for LiteralTemplateLinked {
2088    fn from(i: TemplateLinked) -> Self {
2089        Self {
2090            template_id: i.template_id,
2091            link_id: i.link_id,
2092            args: i
2093                .args
2094                .into_iter()
2095                .map(|(k, v)| (format!("{k}"), v))
2096                .collect(),
2097        }
2098    }
2099}
2100
2101/// Iterate over links in the template-linked file and add them to the set
2102fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
2103    for template_linked in load_links_from_file(path)? {
2104        let slot_env = create_slot_env(&template_linked.args)?;
2105        policy_set.link(
2106            PolicyId::new(&template_linked.template_id),
2107            PolicyId::new(&template_linked.link_id),
2108            slot_env,
2109        )?;
2110    }
2111    Ok(())
2112}
2113
2114/// Given a file containing template links, return a `Vec` of those links
2115fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
2116    let f = match std::fs::File::open(path) {
2117        Ok(f) => f,
2118        Err(_) => {
2119            // If the file doesn't exist, then give back the empty entity set
2120            return Ok(vec![]);
2121        }
2122    };
2123    if f.metadata()
2124        .into_diagnostic()
2125        .wrap_err("Failed to read metadata")?
2126        .len()
2127        == 0
2128    {
2129        // File is empty, return empty set
2130        Ok(vec![])
2131    } else {
2132        // File has contents, deserialize
2133        serde_json::from_reader(f)
2134            .into_diagnostic()
2135            .wrap_err("Deserialization error")
2136    }
2137}
2138
2139/// Add a single template-linked policy to the linked file
2140fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
2141    let mut template_linked = load_links_from_file(path.as_ref())?;
2142    template_linked.push(new_linked);
2143    write_template_linked_file(&template_linked, path.as_ref())
2144}
2145
2146/// Write a slice of template-linked policies to the linked file
2147fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
2148    let f = OpenOptions::new()
2149        .write(true)
2150        .truncate(true)
2151        .create(true)
2152        .open(path)
2153        .into_diagnostic()?;
2154    serde_json::to_writer(f, linked).into_diagnostic()
2155}
2156
2157pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
2158    println!();
2159    let ans = execute_request(
2160        &args.request,
2161        &args.policies,
2162        &args.entities_file,
2163        &args.schema,
2164        args.timing,
2165    );
2166    match ans {
2167        Ok(ans) => {
2168            let status = match ans.decision() {
2169                Decision::Allow => {
2170                    println!("ALLOW");
2171                    CedarExitCode::Success
2172                }
2173                Decision::Deny => {
2174                    println!("DENY");
2175                    CedarExitCode::AuthorizeDeny
2176                }
2177            };
2178            if ans.diagnostics().errors().peekable().peek().is_some() {
2179                println!();
2180                for err in ans.diagnostics().errors() {
2181                    println!("{err}");
2182                }
2183            }
2184            if args.verbose {
2185                println!();
2186                if ans.diagnostics().reason().peekable().peek().is_none() {
2187                    println!("note: no policies applied to this request");
2188                } else {
2189                    println!("note: this decision was due to the following policies:");
2190                    for reason in ans.diagnostics().reason() {
2191                        println!("  {reason}");
2192                    }
2193                    println!();
2194                }
2195            }
2196            status
2197        }
2198        Err(errs) => {
2199            for err in errs {
2200                println!("{err:?}");
2201            }
2202            CedarExitCode::Failure
2203        }
2204    }
2205}
2206
2207#[cfg(not(feature = "partial-eval"))]
2208pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
2209    {
2210        eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
2211        return CedarExitCode::Failure;
2212    }
2213}
2214
2215#[cfg(not(feature = "tpe"))]
2216pub fn tpe(_: &TpeArgs) -> CedarExitCode {
2217    {
2218        eprintln!("Error: option `tpe` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
2219        return CedarExitCode::Failure;
2220    }
2221}
2222
2223#[cfg(feature = "tpe")]
2224pub fn tpe(args: &TpeArgs) -> CedarExitCode {
2225    println!();
2226    let ret = |errs| {
2227        for err in errs {
2228            println!("{err:?}");
2229        }
2230        CedarExitCode::Failure
2231    };
2232    let mut errs = vec![];
2233    let policies = match args.policies.get_policy_set() {
2234        Ok(pset) => pset,
2235        Err(e) => {
2236            errs.push(e);
2237            PolicySet::new()
2238        }
2239    };
2240    let schema: Schema = match args.schema.get_schema() {
2241        Ok(opt) => opt,
2242        Err(e) => {
2243            errs.push(e);
2244            return ret(errs);
2245        }
2246    };
2247
2248    let entities = match load_partial_entities(args.entities_file.clone(), &schema) {
2249        Ok(entities) => entities,
2250        Err(e) => {
2251            errs.push(e);
2252            PartialEntities::empty()
2253        }
2254    };
2255
2256    match args.request.get_request(&schema) {
2257        Ok(request) if errs.is_empty() => {
2258            let auth_start = Instant::now();
2259            let ans = policies.tpe(&request, &entities, &schema);
2260            let auth_dur = auth_start.elapsed();
2261            match ans {
2262                Ok(ans) => {
2263                    if args.timing {
2264                        println!(
2265                            "Authorization Time (micro seconds) : {}",
2266                            auth_dur.as_micros()
2267                        );
2268                    }
2269                    match ans.decision() {
2270                        Some(Decision::Allow) => {
2271                            println!("ALLOW");
2272                            CedarExitCode::Success
2273                        }
2274                        Some(Decision::Deny) => {
2275                            println!("DENY");
2276                            CedarExitCode::AuthorizeDeny
2277                        }
2278                        None => {
2279                            println!("UNKNOWN");
2280                            println!("All policy residuals:");
2281                            for p in ans.residual_policies() {
2282                                println!("{p}");
2283                            }
2284                            CedarExitCode::Unknown
2285                        }
2286                    }
2287                }
2288                Err(err) => {
2289                    errs.push(miette!("{err}"));
2290                    return ret(errs);
2291                }
2292            }
2293        }
2294        Ok(_) => {
2295            return ret(errs);
2296        }
2297        Err(e) => {
2298            errs.push(e.wrap_err("failed to parse request"));
2299            return ret(errs);
2300        }
2301    }
2302}
2303
2304#[cfg(feature = "partial-eval")]
2305pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
2306    println!();
2307    let ans = execute_partial_request(
2308        &args.request,
2309        &args.policies,
2310        &args.entities_file,
2311        &args.schema,
2312        args.timing,
2313    );
2314    match ans {
2315        Ok(ans) => match ans.decision() {
2316            Some(Decision::Allow) => {
2317                println!("ALLOW");
2318                CedarExitCode::Success
2319            }
2320            Some(Decision::Deny) => {
2321                println!("DENY");
2322                CedarExitCode::AuthorizeDeny
2323            }
2324            None => {
2325                println!("UNKNOWN");
2326                println!("All policy residuals:");
2327                for p in ans.nontrivial_residuals() {
2328                    println!("{p}");
2329                }
2330                CedarExitCode::Unknown
2331            }
2332        },
2333        Err(errs) => {
2334            for err in errs {
2335                println!("{err:?}");
2336            }
2337            CedarExitCode::Failure
2338        }
2339    }
2340}
2341
2342#[derive(Clone, Debug)]
2343enum TestResult {
2344    Pass,
2345    Fail(String),
2346}
2347
2348/// Compare the test's expected decision against the actual decision
2349fn compare_test_decisions(test: &TestCase, ans: &Response) -> TestResult {
2350    if ans.decision() == test.decision.into() {
2351        let mut errors = Vec::new();
2352        let reason = ans.diagnostics().reason().collect::<BTreeSet<_>>();
2353
2354        // Check that the declared reason is a subset of the actual reason
2355        let missing_reason = test
2356            .reason
2357            .iter()
2358            .filter(|r| !reason.contains(&PolicyId::new(r)))
2359            .collect::<Vec<_>>();
2360
2361        if !missing_reason.is_empty() {
2362            errors.push(format!(
2363                "missing reason(s): {}",
2364                missing_reason
2365                    .into_iter()
2366                    .map(|r| format!("`{r}`"))
2367                    .collect::<Vec<_>>()
2368                    .join(", ")
2369            ));
2370        }
2371
2372        // Check that evaluation errors are expected
2373        let num_errors = ans.diagnostics().errors().count();
2374        if num_errors != test.num_errors {
2375            errors.push(format!(
2376                "expected {} error(s), but got {} runtime error(s){}",
2377                test.num_errors,
2378                num_errors,
2379                if num_errors == 0 {
2380                    "".to_string()
2381                } else {
2382                    format!(
2383                        ": {}",
2384                        ans.diagnostics()
2385                            .errors()
2386                            .map(|e| e.to_string())
2387                            .collect::<Vec<_>>()
2388                            .join(", ")
2389                    )
2390                },
2391            ));
2392        }
2393
2394        if errors.is_empty() {
2395            TestResult::Pass
2396        } else {
2397            TestResult::Fail(errors.join("; "))
2398        }
2399    } else {
2400        TestResult::Fail(format!(
2401            "expected {:?}, got {:?}",
2402            test.decision,
2403            ans.decision()
2404        ))
2405    }
2406}
2407
2408/// Parse the test, validate against schema,
2409/// and then check the authorization decision
2410fn run_one_test(
2411    policies: &PolicySet,
2412    test: &serde_json::Value,
2413    validator: Option<&Validator>,
2414) -> Result<TestResult> {
2415    let test = CheckedTestCaseSeed(validator.map(Validator::schema))
2416        .deserialize(test.into_deserializer())
2417        .into_diagnostic()?;
2418    if let Some(validator) = validator {
2419        let val_res = validator.validate(policies, cedar_policy::ValidationMode::Strict);
2420        if !val_res.validation_passed_without_warnings() {
2421            return Err(Report::new(val_res).wrap_err("policy set validation failed"));
2422        }
2423    }
2424    let ans = Authorizer::new().is_authorized(&test.request, policies, &test.entities);
2425    Ok(compare_test_decisions(&test, &ans))
2426}
2427
2428fn run_tests_inner(args: &RunTestsArgs) -> Result<CedarExitCode> {
2429    let policies = args.policies.get_policy_set()?;
2430    let tests = load_partial_tests(&args.tests)?;
2431    let validator = args.schema.get_schema()?.map(Validator::new);
2432
2433    let mut total_fails: usize = 0;
2434
2435    println!("running {} test(s)", tests.len());
2436    for test in tests.iter() {
2437        if let Some(name) = test["name"].as_str() {
2438            print!("  test {name} ... ");
2439        } else {
2440            print!("  test (unnamed) ... ");
2441        }
2442        std::io::stdout().flush().into_diagnostic()?;
2443        match run_one_test(&policies, test, validator.as_ref()) {
2444            Ok(TestResult::Pass) => {
2445                println!(
2446                    "{}",
2447                    "ok".if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
2448                );
2449            }
2450            Ok(TestResult::Fail(reason)) => {
2451                total_fails += 1;
2452                println!(
2453                    "{}: {}",
2454                    "fail".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
2455                    reason
2456                );
2457            }
2458            Err(e) => {
2459                total_fails += 1;
2460                println!(
2461                    "{}:\n  {:?}",
2462                    "error".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
2463                    e
2464                );
2465            }
2466        }
2467    }
2468
2469    println!(
2470        "results: {} {}, {} {}",
2471        tests.len() - total_fails,
2472        if total_fails == 0 {
2473            "passed"
2474                .if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
2475                .to_string()
2476        } else {
2477            "passed".to_string()
2478        },
2479        total_fails,
2480        if total_fails != 0 {
2481            "failed"
2482                .if_supports_color(owo_colors::Stream::Stdout, |s| s.red())
2483                .to_string()
2484        } else {
2485            "failed".to_string()
2486        },
2487    );
2488
2489    Ok(if total_fails != 0 {
2490        CedarExitCode::Failure
2491    } else {
2492        CedarExitCode::Success
2493    })
2494}
2495
2496pub fn run_tests(args: &RunTestsArgs) -> CedarExitCode {
2497    run_tests_inner(args).unwrap_or_else(|e| {
2498        println!("{e:?}");
2499        CedarExitCode::Failure
2500    })
2501}
2502
2503#[derive(Copy, Clone, Debug, Deserialize)]
2504enum ExpectedDecision {
2505    #[serde(rename = "allow")]
2506    Allow,
2507    #[serde(rename = "deny")]
2508    Deny,
2509}
2510
2511impl From<ExpectedDecision> for Decision {
2512    fn from(value: ExpectedDecision) -> Self {
2513        match value {
2514            ExpectedDecision::Allow => Decision::Allow,
2515            ExpectedDecision::Deny => Decision::Deny,
2516        }
2517    }
2518}
2519
2520#[derive(Clone, Debug, Deserialize)]
2521struct UncheckedTestCase {
2522    request: RequestJSON,
2523    entities: serde_json::Value,
2524    decision: ExpectedDecision,
2525    reason: Vec<String>,
2526    num_errors: usize,
2527}
2528
2529#[derive(Clone, Debug)]
2530struct TestCase {
2531    request: Request,
2532    entities: Entities,
2533    decision: ExpectedDecision,
2534    reason: Vec<String>,
2535    num_errors: usize,
2536}
2537
2538struct CheckedTestCaseSeed<'a>(Option<&'a Schema>);
2539
2540impl<'de, 'a> DeserializeSeed<'de> for CheckedTestCaseSeed<'a> {
2541    type Value = TestCase;
2542
2543    fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
2544    where
2545        D: Deserializer<'de>,
2546    {
2547        let UncheckedTestCase {
2548            request,
2549            entities,
2550            decision,
2551            reason,
2552            num_errors,
2553        } = UncheckedTestCase::deserialize(deserializer)?;
2554
2555        let principal = request.principal.parse().map_err(|e| {
2556            serde::de::Error::custom(format!(
2557                "failed to parse principal `{}`: {}",
2558                request.principal, e
2559            ))
2560        })?;
2561
2562        let action = request.action.parse().map_err(|e| {
2563            serde::de::Error::custom(format!(
2564                "failed to parse action `{}`: {}",
2565                request.action, e
2566            ))
2567        })?;
2568
2569        let resource = request.resource.parse().map_err(|e| {
2570            serde::de::Error::custom(format!(
2571                "failed to parse resource `{}`: {}",
2572                request.resource, e
2573            ))
2574        })?;
2575
2576        let context = Context::from_json_value(request.context.clone(), None).map_err(|e| {
2577            serde::de::Error::custom(format!(
2578                "failed to parse context `{}`: {}",
2579                request.context, e
2580            ))
2581        })?;
2582
2583        let request = Request::new(principal, action, resource, context, self.0)
2584            .map_err(|e| serde::de::Error::custom(format!("failed to create request: {e}")))?;
2585
2586        let entities = Entities::from_json_value(entities, self.0)
2587            .map_err(|e| serde::de::Error::custom(format!("failed to parse entities: {e}")))?;
2588
2589        Ok(TestCase {
2590            request,
2591            entities,
2592            decision,
2593            reason,
2594            num_errors,
2595        })
2596    }
2597}
2598
2599/// Load partially parsed tests from a JSON file
2600/// (as JSON values first without parsing to TestCase)
2601fn load_partial_tests(tests_filename: impl AsRef<Path>) -> Result<Vec<serde_json::Value>> {
2602    match std::fs::OpenOptions::new()
2603        .read(true)
2604        .open(tests_filename.as_ref())
2605    {
2606        Ok(f) => {
2607            let reader = BufReader::new(f);
2608            serde_json::from_reader(reader).map_err(|e| {
2609                miette!(
2610                    "failed to parse tests from file {}: {e}",
2611                    tests_filename.as_ref().display()
2612                )
2613            })
2614        }
2615        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2616            format!(
2617                "failed to open test file {}",
2618                tests_filename.as_ref().display()
2619            )
2620        }),
2621    }
2622}
2623
2624#[cfg(feature = "tpe")]
2625/// Load an `PartialEntities` object from the given JSON filename and optional schema.
2626fn load_partial_entities(
2627    entities_filename: impl AsRef<Path>,
2628    schema: &Schema,
2629) -> Result<PartialEntities> {
2630    match std::fs::OpenOptions::new()
2631        .read(true)
2632        .open(entities_filename.as_ref())
2633    {
2634        Ok(f) => {
2635            PartialEntities::from_json_value(serde_json::from_reader(f).into_diagnostic()?, schema)
2636                .map_err(|e| miette!("{e}"))
2637                .wrap_err_with(|| {
2638                    format!(
2639                        "failed to parse entities from file {}",
2640                        entities_filename.as_ref().display()
2641                    )
2642                })
2643        }
2644        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2645            format!(
2646                "failed to open entities file {}",
2647                entities_filename.as_ref().display()
2648            )
2649        }),
2650    }
2651}
2652
2653/// Load an `Entities` object from the given JSON filename and optional schema.
2654fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
2655    match std::fs::OpenOptions::new()
2656        .read(true)
2657        .open(entities_filename.as_ref())
2658    {
2659        Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
2660            format!(
2661                "failed to parse entities from file {}",
2662                entities_filename.as_ref().display()
2663            )
2664        }),
2665        Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
2666            format!(
2667                "failed to open entities file {}",
2668                entities_filename.as_ref().display()
2669            )
2670        }),
2671    }
2672}
2673
2674/// Renames policies and templates based on (@id("new_id") annotation.
2675/// If no such annotation exists, it keeps the current id.
2676///
2677/// This will rename template-linked policies to the id of their template, which may
2678/// cause id conflicts, so only call this function before instancing
2679/// templates into the policy set.
2680fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
2681    let mut new_ps = PolicySet::new();
2682    let t_iter = ps.templates().map(|t| match t.annotation("id") {
2683        None => Ok(t.clone()),
2684        Some(anno) => anno.parse().map(|a| t.new_id(a)),
2685    });
2686    for t in t_iter {
2687        let template = t.unwrap_or_else(|never| match never {});
2688        new_ps
2689            .add_template(template)
2690            .wrap_err("failed to add template to policy set")?;
2691    }
2692    let p_iter = ps.policies().map(|p| match p.annotation("id") {
2693        None => Ok(p.clone()),
2694        Some(anno) => anno.parse().map(|a| p.new_id(a)),
2695    });
2696    for p in p_iter {
2697        let policy = p.unwrap_or_else(|never| match never {});
2698        new_ps
2699            .add(policy)
2700            .wrap_err("failed to add template to policy set")?;
2701    }
2702    Ok(new_ps)
2703}
2704
2705// Read from a file (when `filename` is a `Some`) or stdin (when `filename` is `None`) to a `String`
2706fn read_from_file_or_stdin(filename: Option<&impl AsRef<Path>>, context: &str) -> Result<String> {
2707    let mut src_str = String::new();
2708    match filename {
2709        Some(path) => {
2710            src_str = std::fs::read_to_string(path)
2711                .into_diagnostic()
2712                .wrap_err_with(|| {
2713                    format!("failed to open {context} file {}", path.as_ref().display())
2714                })?;
2715        }
2716        None => {
2717            std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
2718                .into_diagnostic()
2719                .wrap_err_with(|| format!("failed to read {context} from stdin"))?;
2720        }
2721    };
2722    Ok(src_str)
2723}
2724
2725// Convenient wrapper around `read_from_file_or_stdin` to just read from a file
2726fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
2727    read_from_file_or_stdin(Some(&filename), context)
2728}
2729
2730/// Read a policy set, in Cedar syntax, from the file given in `filename`,
2731/// or from stdin if `filename` is `None`.
2732fn read_cedar_policy_set(
2733    filename: Option<impl AsRef<Path> + std::marker::Copy>,
2734) -> Result<PolicySet> {
2735    let context = "policy set";
2736    let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
2737    let ps = PolicySet::from_str(&ps_str)
2738        .map_err(|err| {
2739            let name = filename.map_or_else(
2740                || "<stdin>".to_owned(),
2741                |n| n.as_ref().display().to_string(),
2742            );
2743            Report::new(err).with_source_code(NamedSource::new(name, ps_str))
2744        })
2745        .wrap_err_with(|| format!("failed to parse {context}"))?;
2746    rename_from_id_annotation(&ps)
2747}
2748
2749/// Read a policy set, static policy or policy template, in Cedar JSON (EST) syntax, from the file given
2750/// in `filename`, or from stdin if `filename` is `None`.
2751fn read_json_policy_set(
2752    filename: Option<impl AsRef<Path> + std::marker::Copy>,
2753) -> Result<PolicySet> {
2754    let context = "JSON policy";
2755    let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
2756    let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
2757    let policy_type = get_json_policy_type(&json)?;
2758
2759    let add_json_source = |report: Report| {
2760        let name = filename.map_or_else(
2761            || "<stdin>".to_owned(),
2762            |n| n.as_ref().display().to_string(),
2763        );
2764        report.with_source_code(NamedSource::new(name, json_source.clone()))
2765    };
2766
2767    match policy_type {
2768        JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
2769            Ok(policy) => PolicySet::from_policies([policy])
2770                .wrap_err_with(|| format!("failed to create policy set from {context}")),
2771            Err(_) => match Template::from_json(None, json)
2772                .map_err(|err| add_json_source(Report::new(err)))
2773            {
2774                Ok(template) => {
2775                    let mut ps = PolicySet::new();
2776                    ps.add_template(template)?;
2777                    Ok(ps)
2778                }
2779                Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
2780            },
2781        },
2782        JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
2783            .map_err(|err| add_json_source(Report::new(err)))
2784            .wrap_err_with(|| format!("failed to create policy set from {context}")),
2785    }
2786}
2787
2788fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
2789    let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
2790    let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
2791
2792    let json_has_property = |p| json.get(p).is_some();
2793    let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
2794    let has_any_policy_property = policy_properties.iter().any(json_has_property);
2795
2796    match (has_any_policy_set_property, has_any_policy_property) {
2797        (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
2798        (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
2799        (true, _) => Ok(JsonPolicyType::PolicySet),
2800        (_, true) => Ok(JsonPolicyType::SinglePolicy),
2801    }
2802}
2803
2804enum JsonPolicyType {
2805    SinglePolicy,
2806    PolicySet,
2807}
2808
2809/// This uses the Cedar API to call the authorization engine.
2810fn execute_request(
2811    request: &RequestArgs,
2812    policies: &PoliciesArgs,
2813    entities_filename: impl AsRef<Path>,
2814    schema: &OptionalSchemaArgs,
2815    compute_duration: bool,
2816) -> Result<Response, Vec<Report>> {
2817    let mut errs = vec![];
2818    let policies = match policies.get_policy_set() {
2819        Ok(pset) => pset,
2820        Err(e) => {
2821            errs.push(e);
2822            PolicySet::new()
2823        }
2824    };
2825    let schema = match schema.get_schema() {
2826        Ok(opt) => opt,
2827        Err(e) => {
2828            errs.push(e);
2829            None
2830        }
2831    };
2832    let entities = match load_entities(entities_filename, schema.as_ref()) {
2833        Ok(entities) => entities,
2834        Err(e) => {
2835            errs.push(e);
2836            Entities::empty()
2837        }
2838    };
2839    match request.get_request(schema.as_ref()) {
2840        Ok(request) if errs.is_empty() => {
2841            let authorizer = Authorizer::new();
2842            let auth_start = Instant::now();
2843            let ans = authorizer.is_authorized(&request, &policies, &entities);
2844            let auth_dur = auth_start.elapsed();
2845            if compute_duration {
2846                println!(
2847                    "Authorization Time (micro seconds) : {}",
2848                    auth_dur.as_micros()
2849                );
2850            }
2851            Ok(ans)
2852        }
2853        Ok(_) => Err(errs),
2854        Err(e) => {
2855            errs.push(e.wrap_err("failed to parse request"));
2856            Err(errs)
2857        }
2858    }
2859}
2860
2861#[cfg(feature = "partial-eval")]
2862fn execute_partial_request(
2863    request: &PartialRequestArgs,
2864    policies: &PoliciesArgs,
2865    entities_filename: impl AsRef<Path>,
2866    schema: &OptionalSchemaArgs,
2867    compute_duration: bool,
2868) -> Result<PartialResponse, Vec<Report>> {
2869    let mut errs = vec![];
2870    let policies = match policies.get_policy_set() {
2871        Ok(pset) => pset,
2872        Err(e) => {
2873            errs.push(e);
2874            PolicySet::new()
2875        }
2876    };
2877    let schema = match schema.get_schema() {
2878        Ok(opt) => opt,
2879        Err(e) => {
2880            errs.push(e);
2881            None
2882        }
2883    };
2884    let entities = match load_entities(entities_filename, schema.as_ref()) {
2885        Ok(entities) => entities,
2886        Err(e) => {
2887            errs.push(e);
2888            Entities::empty()
2889        }
2890    };
2891    match request.get_request(schema.as_ref()) {
2892        Ok(request) if errs.is_empty() => {
2893            let authorizer = Authorizer::new();
2894            let auth_start = Instant::now();
2895            let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
2896            let auth_dur = auth_start.elapsed();
2897            if compute_duration {
2898                println!(
2899                    "Authorization Time (micro seconds) : {}",
2900                    auth_dur.as_micros()
2901                );
2902            }
2903            Ok(ans)
2904        }
2905        Ok(_) => Err(errs),
2906        Err(e) => {
2907            errs.push(e.wrap_err("failed to parse request"));
2908            Err(errs)
2909        }
2910    }
2911}