cedar_policy_cli/
lib.rs

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