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