#![allow(
clippy::needless_return,
reason = r#"
This module makes use of `return` to exit early with a particular exit code.
For consistency, it also uses `return` in some places where it could be
omitted.
"#
)]
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
#[cfg(feature = "analyze")]
use itertools::Itertools;
use miette::{miette, IntoDiagnostic, NamedSource, Report, Result, WrapErr};
use owo_colors::OwoColorize;
use serde::de::{DeserializeSeed, IntoDeserializer};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::BTreeSet;
use std::io::{BufReader, Write};
use std::{
collections::HashMap,
fmt::{self, Display},
fs::OpenOptions,
path::{Path, PathBuf},
process::{ExitCode, Termination},
str::FromStr,
time::Instant,
};
use cedar_policy::*;
use cedar_policy_formatter::{policies_str_to_pretty, Config};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(
global = true,
short = 'f',
long = "error-format",
env = "CEDAR_ERROR_FORMAT",
default_value_t,
value_enum
)]
pub err_fmt: ErrorFormat,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum ErrorFormat {
#[default]
Human,
Plain,
Json,
}
impl Display for ErrorFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
ErrorFormat::Human => "human",
ErrorFormat::Plain => "plain",
ErrorFormat::Json => "json",
}
)
}
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Authorize(AuthorizeArgs),
Evaluate(EvaluateArgs),
Validate(ValidateArgs),
CheckParse(CheckParseArgs),
Link(LinkArgs),
Format(FormatArgs),
TranslatePolicy(TranslatePolicyArgs),
TranslateSchema(TranslateSchemaArgs),
Visualize(VisualizeArgs),
New(NewArgs),
PartiallyAuthorize(PartiallyAuthorizeArgs),
Tpe(TpeArgs),
#[clap(verbatim_doc_comment)] RunTests(RunTestsArgs),
Symcc(SymccArgs),
LanguageVersion,
}
#[derive(Args, Debug)]
pub struct TranslatePolicyArgs {
#[arg(long)]
pub direction: PolicyTranslationDirection,
#[arg(short = 'p', long = "policies", value_name = "FILE")]
pub input_file: Option<String>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum PolicyTranslationDirection {
CedarToJson,
JsonToCedar,
}
#[derive(Args, Debug)]
pub struct TranslateSchemaArgs {
#[arg(long)]
pub direction: SchemaTranslationDirection,
#[arg(short = 's', long = "schema", value_name = "FILE")]
pub input_file: Option<String>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum SchemaTranslationDirection {
JsonToCedar,
CedarToJson,
CedarToJsonWithResolvedTypes,
}
#[derive(Debug, Default, Clone, Copy, ValueEnum)]
pub enum SchemaFormat {
#[default]
Cedar,
Json,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ValidationMode {
Strict,
Permissive,
Partial,
}
#[derive(Args, Debug)]
pub struct ValidateArgs {
#[command(flatten)]
pub schema: SchemaArgs,
#[command(flatten)]
pub policies: PoliciesArgs,
#[arg(long)]
pub deny_warnings: bool,
#[arg(long, value_enum, default_value_t = ValidationMode::Strict)]
pub validation_mode: ValidationMode,
#[arg(long)]
pub level: Option<u32>,
}
#[derive(Args, Debug)]
pub struct CheckParseArgs {
#[command(flatten)]
pub policies: OptionalPoliciesArgs,
#[arg(long)]
pub expression: Option<String>,
#[command(flatten)]
pub schema: OptionalSchemaArgs,
#[arg(long = "entities", value_name = "FILE")]
pub entities_file: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct RequestArgs {
#[arg(short = 'l', long)]
pub principal: Option<String>,
#[arg(short, long)]
pub action: Option<String>,
#[arg(short, long)]
pub resource: Option<String>,
#[arg(short, long = "context", value_name = "FILE")]
pub context_json_file: Option<String>,
#[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
pub request_json_file: Option<String>,
#[arg(long = "request-validation", action = ArgAction::Set, default_value_t = true)]
pub request_validation: bool,
}
#[cfg(feature = "tpe")]
#[derive(Args, Debug)]
pub struct TpeRequestArgs {
#[arg(long)]
pub principal_type: Option<String>,
#[arg(long)]
pub principal_eid: Option<String>,
#[arg(short, long)]
pub action: Option<String>,
#[arg(long)]
pub resource_type: Option<String>,
#[arg(long)]
pub resource_eid: Option<String>,
#[arg(short, long = "context", value_name = "FILE")]
pub context_json_file: Option<String>,
#[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal_type", "principal_eid", "action", "resource_type", "resource_eid", "context_json_file"])]
pub request_json_file: Option<String>,
}
#[cfg(feature = "partial-eval")]
#[derive(Args, Debug)]
pub struct PartialRequestArgs {
#[arg(short = 'l', long)]
pub principal: Option<String>,
#[arg(short, long)]
pub action: Option<String>,
#[arg(short, long)]
pub resource: Option<String>,
#[arg(short, long = "context", value_name = "FILE")]
pub context_json_file: Option<String>,
#[arg(long = "request-json", value_name = "FILE", conflicts_with_all = &["principal", "action", "resource", "context_json_file"])]
pub request_json_file: Option<String>,
}
impl RequestArgs {
fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
match &self.request_json_file {
Some(jsonfile) => {
let jsonstring = std::fs::read_to_string(jsonfile)
.into_diagnostic()
.wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
let qjson: RequestJSON = serde_json::from_str(&jsonstring)
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?;
let principal = qjson.principal.parse().wrap_err_with(|| {
format!("failed to parse principal in {jsonfile} as entity Uid")
})?;
let action = qjson.action.parse().wrap_err_with(|| {
format!("failed to parse action in {jsonfile} as entity Uid")
})?;
let resource = qjson.resource.parse().wrap_err_with(|| {
format!("failed to parse resource in {jsonfile} as entity Uid")
})?;
let context = Context::from_json_value(qjson.context, schema.map(|s| (s, &action)))
.wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?;
Request::new(
principal,
action,
resource,
context,
if self.request_validation {
schema
} else {
None
},
)
.map_err(|e| miette!("{e}"))
}
None => {
let principal = self
.principal
.as_ref()
.map(|s| {
s.parse().wrap_err_with(|| {
format!("failed to parse principal {s} as entity Uid")
})
})
.transpose()?;
let action = self
.action
.as_ref()
.map(|s| {
s.parse()
.wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
})
.transpose()?;
let resource = self
.resource
.as_ref()
.map(|s| {
s.parse()
.wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
})
.transpose()?;
let context: Context = match &self.context_json_file {
None => Context::empty(),
Some(jsonfile) => match std::fs::OpenOptions::new().read(true).open(jsonfile) {
Ok(f) => Context::from_json_file(
f,
schema.and_then(|s| Some((s, action.as_ref()?))),
)
.wrap_err_with(|| format!("failed to create a context from {jsonfile}"))?,
Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
format!("error while loading context from {jsonfile}")
})?,
},
};
match (principal, action, resource) {
(Some(principal), Some(action), Some(resource)) => Request::new(
principal,
action,
resource,
context,
if self.request_validation {
schema
} else {
None
},
)
.map_err(|e| miette!("{e}")),
_ => Err(miette!(
"All three (`principal`, `action`, `resource`) variables must be specified"
)),
}
}
}
}
}
#[cfg(feature = "tpe")]
impl TpeRequestArgs {
fn get_request(&self, schema: &Schema) -> Result<PartialRequest> {
let qjson: TpeRequestJSON = match self.request_json_file.as_ref() {
Some(jsonfile) => {
let jsonstring = std::fs::read_to_string(jsonfile)
.into_diagnostic()
.wrap_err_with(|| format!("failed to open request json file {jsonfile}"))?;
serde_json::from_str(&jsonstring)
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse context-json file {jsonfile}"))?
}
None => TpeRequestJSON {
principal_type: self
.principal_type
.clone()
.ok_or_else(|| miette!("principal type must be specified"))?,
principal_eid: self.principal_eid.clone(),
action: self
.action
.clone()
.ok_or_else(|| miette!("action must be specified"))?,
resource_type: self
.resource_type
.clone()
.ok_or_else(|| miette!("resource type must be specified"))?,
resource_eid: self.resource_eid.clone(),
context: self
.context_json_file
.as_ref()
.map(|jsonfile| {
let jsonstring = std::fs::read_to_string(jsonfile)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to open context-json file {jsonfile}")
})?;
serde_json::from_str(&jsonstring)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to parse context-json file {jsonfile}")
})
})
.transpose()?,
},
};
let action: EntityUid = qjson.action.parse()?;
Ok(PartialRequest::new(
PartialEntityUid::new(
qjson.principal_type.parse()?,
qjson.principal_eid.as_ref().map(EntityId::new),
),
action.clone(),
PartialEntityUid::new(
qjson.resource_type.parse()?,
qjson.resource_eid.as_ref().map(EntityId::new),
),
qjson
.context
.map(|val| Context::from_json_value(val, Some((schema, &action))))
.transpose()?,
schema,
)?)
}
}
#[cfg(feature = "partial-eval")]
impl PartialRequestArgs {
fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
let mut builder = RequestBuilder::default();
let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
Some(jsonfile) => {
let jsonstring = std::fs::read_to_string(jsonfile)
.into_diagnostic()
.wrap_err_with(|| format!("failed to open request-json file {jsonfile}"))?;
serde_json::from_str(&jsonstring)
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse request-json file {jsonfile}"))?
}
None => PartialRequestJSON {
principal: self.principal.clone(),
action: self.action.clone(),
resource: self.resource.clone(),
context: self
.context_json_file
.as_ref()
.map(|jsonfile| {
let jsonstring = std::fs::read_to_string(jsonfile)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to open context-json file {jsonfile}")
})?;
serde_json::from_str(&jsonstring)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to parse context-json file {jsonfile}")
})
})
.transpose()?,
},
};
if let Some(principal) = qjson
.principal
.map(|s| {
s.parse()
.wrap_err_with(|| format!("failed to parse principal {s} as entity Uid"))
})
.transpose()?
{
builder = builder.principal(principal);
}
let action = qjson
.action
.map(|s| {
s.parse::<EntityUid>()
.wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
})
.transpose()?;
if let Some(action_ref) = &action {
builder = builder.action(action_ref.clone());
}
if let Some(resource) = qjson
.resource
.map(|s| {
s.parse()
.wrap_err_with(|| format!("failed to parse resource {s} as entity Uid"))
})
.transpose()?
{
builder = builder.resource(resource);
}
if let Some(context) = qjson
.context
.map(|json| {
Context::from_json_value(
json.clone(),
schema.and_then(|s| Some((s, action.as_ref()?))),
)
.wrap_err_with(|| format!("fail to convert context json {json} to Context"))
})
.transpose()?
{
builder = builder.context(context);
}
if let Some(schema) = schema {
builder
.schema(schema)
.build()
.wrap_err_with(|| "failed to build request with validation".to_string())
} else {
Ok(builder.build())
}
}
}
#[derive(Args, Debug)]
pub struct PoliciesArgs {
#[arg(short, long = "policies", value_name = "FILE")]
pub policies_file: Option<String>,
#[arg(long = "policy-format", default_value_t, value_enum)]
pub policy_format: PolicyFormat,
#[arg(short = 'k', long = "template-linked", value_name = "FILE")]
pub template_linked_file: Option<String>,
}
impl PoliciesArgs {
fn get_policy_set(&self) -> Result<PolicySet> {
let mut pset = match self.policy_format {
PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
}?;
if let Some(links_filename) = self.template_linked_file.as_ref() {
add_template_links_to_set(links_filename, &mut pset)?;
}
Ok(pset)
}
}
#[derive(Args, Debug)]
pub struct OptionalPoliciesArgs {
#[arg(short, long = "policies", value_name = "FILE")]
pub policies_file: Option<String>,
#[arg(long = "policy-format", default_value_t, value_enum)]
pub policy_format: PolicyFormat,
#[arg(short = 'k', long = "template-linked", value_name = "FILE")]
pub template_linked_file: Option<String>,
}
impl OptionalPoliciesArgs {
fn get_policy_set(&self) -> Result<Option<PolicySet>> {
match &self.policies_file {
None => Ok(None),
Some(policies_file) => {
let pargs = PoliciesArgs {
policies_file: Some(policies_file.clone()),
policy_format: self.policy_format,
template_linked_file: self.template_linked_file.clone(),
};
pargs.get_policy_set().map(Some)
}
}
}
}
#[derive(Args, Debug)]
pub struct SchemaArgs {
#[arg(short, long = "schema", value_name = "FILE")]
pub schema_file: PathBuf,
#[arg(long, value_enum, default_value_t)]
pub schema_format: SchemaFormat,
}
impl SchemaArgs {
fn get_schema(&self) -> Result<Schema> {
read_schema_from_file(&self.schema_file, self.schema_format)
}
}
#[derive(Args, Debug)]
pub struct OptionalSchemaArgs {
#[arg(short, long = "schema", value_name = "FILE")]
pub schema_file: Option<PathBuf>,
#[arg(long, value_enum, default_value_t)]
pub schema_format: SchemaFormat,
}
impl OptionalSchemaArgs {
fn get_schema(&self) -> Result<Option<Schema>> {
let Some(schema_file) = &self.schema_file else {
return Ok(None);
};
read_schema_from_file(schema_file, self.schema_format).map(Some)
}
}
fn read_schema_from_file(path: impl AsRef<Path>, format: SchemaFormat) -> Result<Schema> {
let path = path.as_ref();
let schema_src = read_from_file(path, "schema")?;
match format {
SchemaFormat::Json => Schema::from_json_str(&schema_src)
.wrap_err_with(|| format!("failed to parse schema from file {}", path.display())),
SchemaFormat::Cedar => {
let (schema, warnings) = Schema::from_cedarschema_str(&schema_src)
.wrap_err_with(|| format!("failed to parse schema from file {}", path.display()))?;
for warning in warnings {
let report = miette::Report::new(warning);
eprintln!("{report:?}");
}
Ok(schema)
}
}
}
#[derive(Args, Debug)]
pub struct AuthorizeArgs {
#[command(flatten)]
pub request: RequestArgs,
#[command(flatten)]
pub policies: PoliciesArgs,
#[command(flatten)]
pub schema: OptionalSchemaArgs,
#[arg(long = "entities", value_name = "FILE")]
pub entities_file: String,
#[arg(short, long)]
pub verbose: bool,
#[arg(short, long)]
pub timing: bool,
}
#[cfg(feature = "tpe")]
#[derive(Args, Debug)]
pub struct TpeArgs {
#[command(flatten)]
pub request: TpeRequestArgs,
#[command(flatten)]
pub policies: PoliciesArgs,
#[command(flatten)]
pub schema: SchemaArgs,
#[arg(long = "entities", value_name = "FILE")]
pub entities_file: String,
#[arg(short, long)]
pub timing: bool,
}
#[cfg(feature = "partial-eval")]
#[derive(Args, Debug)]
pub struct PartiallyAuthorizeArgs {
#[command(flatten)]
pub request: PartialRequestArgs,
#[command(flatten)]
pub policies: PoliciesArgs,
#[command(flatten)]
pub schema: OptionalSchemaArgs,
#[arg(long = "entities", value_name = "FILE")]
pub entities_file: String,
#[arg(short, long)]
pub timing: bool,
}
#[cfg(not(feature = "tpe"))]
#[derive(Debug, Args)]
pub struct TpeArgs;
#[cfg(not(feature = "partial-eval"))]
#[derive(Debug, Args)]
pub struct PartiallyAuthorizeArgs;
#[derive(Args, Debug)]
pub struct RunTestsArgs {
#[command(flatten)]
pub policies: PoliciesArgs,
#[arg(long, value_name = "FILE")]
pub tests: String,
#[command(flatten)]
pub schema: OptionalSchemaArgs,
}
#[derive(Args, Debug)]
pub struct VisualizeArgs {
#[arg(long = "entities", value_name = "FILE")]
pub entities_file: String,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum PolicyFormat {
#[default]
Cedar,
Json,
}
#[derive(Args, Debug)]
pub struct LinkArgs {
#[command(flatten)]
pub policies: PoliciesArgs,
#[arg(long)]
pub template_id: String,
#[arg(short, long)]
pub new_id: String,
#[arg(short, long)]
pub arguments: Arguments,
}
#[derive(Args, Debug)]
pub struct FormatArgs {
#[arg(short, long = "policies", value_name = "FILE")]
pub policies_file: Option<String>,
#[arg(short, long, value_name = "UINT", default_value_t = 80)]
pub line_width: usize,
#[arg(short, long, value_name = "INT", default_value_t = 2)]
pub indent_width: isize,
#[arg(short, long, group = "action", requires = "policies_file")]
pub write: bool,
#[arg(short, long, group = "action")]
pub check: bool,
}
#[derive(Args, Debug)]
pub struct NewArgs {
#[arg(short, long, value_name = "DIR")]
pub name: String,
}
#[derive(Args, Debug)]
pub struct SymccArgs {
#[command(subcommand)]
pub command: SymccCommands,
#[arg(long, env = "CVC5")]
pub cvc5_path: Option<PathBuf>,
#[arg(long)]
pub principal_type: String,
#[arg(long)]
pub action: String,
#[arg(long)]
pub resource_type: String,
#[command(flatten)]
pub schema: SchemaArgs,
#[arg(long, default_value_t = true, conflicts_with = "no_counterexample")]
pub counterexample: bool,
#[arg(long, default_value_t = false, conflicts_with = "counterexample")]
pub no_counterexample: bool,
#[arg(short, long)]
pub verbose: bool,
}
#[derive(Subcommand, Debug)]
pub enum SymccCommands {
NeverErrors(SymccPoliciesArgs),
AlwaysMatches(SymccPoliciesArgs),
NeverMatches(SymccPoliciesArgs),
MatchesEquivalent(TwoPolicyArgs),
MatchesImplies(TwoPolicyArgs),
MatchesDisjoint(TwoPolicyArgs),
AlwaysAllows(SymccPoliciesArgs),
AlwaysDenies(SymccPoliciesArgs),
Equivalent(SymccTwoPoliciesArgs),
Implies(SymccTwoPoliciesArgs),
Disjoint(SymccTwoPoliciesArgs),
}
#[derive(Args, Debug)]
pub struct SymccPoliciesArgs {
#[arg(short, long = "policies", value_name = "FILE")]
pub policies_file: Option<String>,
#[arg(long = "policy-format", default_value_t, value_enum)]
pub policy_format: PolicyFormat,
}
#[cfg(feature = "analyze")]
impl SymccPoliciesArgs {
fn get_policy_set(&self) -> Result<PolicySet> {
match self.policy_format {
PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
}
}
}
#[derive(Args, Debug)]
pub struct TwoPolicyArgs {
#[arg(long = "policy1", value_name = "FILE")]
pub policy1_file: Option<String>,
#[arg(long = "policy1-format", default_value_t, value_enum)]
pub policy1_format: PolicyFormat,
#[arg(long = "policy2", value_name = "FILE")]
pub policy2_file: Option<String>,
#[arg(long = "policy2-format", default_value_t, value_enum)]
pub policy2_format: PolicyFormat,
}
#[cfg(feature = "analyze")]
impl TwoPolicyArgs {
fn get_policy_set_1(&self) -> Result<PolicySet> {
let pargs = PoliciesArgs {
policies_file: self.policy1_file.clone(),
policy_format: self.policy1_format,
template_linked_file: None,
};
pargs.get_policy_set()
}
fn get_policy_set_2(&self) -> Result<PolicySet> {
let pargs = PoliciesArgs {
policies_file: self.policy2_file.clone(),
policy_format: self.policy2_format,
template_linked_file: None,
};
pargs.get_policy_set()
}
}
#[derive(Args, Debug)]
pub struct SymccTwoPoliciesArgs {
#[arg(long = "policies1", value_name = "FILE")]
pub policies1_file: Option<String>,
#[arg(long = "policies1-format", default_value_t, value_enum)]
pub policies1_format: PolicyFormat,
#[arg(long = "policies2", value_name = "FILE")]
pub policies2_file: Option<String>,
#[arg(long = "policies2-format", default_value_t, value_enum)]
pub policies2_format: PolicyFormat,
}
#[cfg(feature = "analyze")]
impl SymccTwoPoliciesArgs {
fn get_policy_set_1(&self) -> Result<PolicySet> {
let pargs = SymccPoliciesArgs {
policies_file: self.policies1_file.clone(),
policy_format: self.policies1_format,
};
pargs.get_policy_set()
}
fn get_policy_set_2(&self) -> Result<PolicySet> {
let pargs = SymccPoliciesArgs {
policies_file: self.policies2_file.clone(),
policy_format: self.policies2_format,
};
pargs.get_policy_set()
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "HashMap<String,String>")]
pub struct Arguments {
pub data: HashMap<SlotId, String>,
}
impl TryFrom<HashMap<String, String>> for Arguments {
type Error = String;
fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
Ok(Self {
data: value
.into_iter()
.map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
.collect::<Result<HashMap<SlotId, String>, String>>()?,
})
}
}
impl FromStr for Arguments {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Clone, Debug, Deserialize)]
struct RequestJSON {
#[serde(default)]
principal: String,
#[serde(default)]
action: String,
#[serde(default)]
resource: String,
context: serde_json::Value,
}
#[cfg(feature = "partial-eval")]
#[derive(Deserialize)]
struct PartialRequestJSON {
pub(self) principal: Option<String>,
pub(self) action: Option<String>,
pub(self) resource: Option<String>,
pub(self) context: Option<serde_json::Value>,
}
#[cfg(feature = "tpe")]
#[derive(Deserialize)]
struct TpeRequestJSON {
pub(self) principal_type: String,
pub(self) principal_eid: Option<String>,
pub(self) action: String,
pub(self) resource_type: String,
pub(self) resource_eid: Option<String>,
pub(self) context: Option<serde_json::Value>,
}
#[derive(Args, Debug)]
pub struct EvaluateArgs {
#[command(flatten)]
pub request: RequestArgs,
#[command(flatten)]
pub schema: OptionalSchemaArgs,
#[arg(long = "entities", value_name = "FILE")]
pub entities_file: Option<String>,
#[arg(value_name = "EXPRESSION")]
pub expression: String,
}
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
pub enum CedarExitCode {
Success,
Failure,
AuthorizeDeny,
ValidationFailure,
#[cfg(any(feature = "partial-eval", feature = "tpe"))]
Unknown,
}
impl Termination for CedarExitCode {
fn report(self) -> ExitCode {
match self {
CedarExitCode::Success => ExitCode::SUCCESS,
CedarExitCode::Failure => ExitCode::FAILURE,
CedarExitCode::AuthorizeDeny => ExitCode::from(2),
CedarExitCode::ValidationFailure => ExitCode::from(3),
#[cfg(any(feature = "partial-eval", feature = "tpe"))]
CedarExitCode::Unknown => ExitCode::SUCCESS,
}
}
}
pub fn check_parse(args: &CheckParseArgs) -> CedarExitCode {
if args.policies.policies_file.is_none()
&& args.schema.schema_file.is_none()
&& args.entities_file.is_none()
&& args.expression.is_none()
{
let pargs = PoliciesArgs {
policies_file: None, policy_format: args.policies.policy_format,
template_linked_file: args.policies.template_linked_file.clone(),
};
match pargs.get_policy_set() {
Ok(_) => return CedarExitCode::Success,
Err(e) => {
println!("{e:?}");
return CedarExitCode::Failure;
}
}
}
#[expect(
clippy::useless_let_if_seq,
reason = "exit_code is mutated by later expressions"
)]
let mut exit_code = CedarExitCode::Success;
if let Err(e) = args.policies.get_policy_set() {
println!("{e:?}");
exit_code = CedarExitCode::Failure;
}
if let Some(e) = args
.expression
.as_ref()
.and_then(|expr| Expression::from_str(expr).err())
{
println!("{:?}", Report::new(e));
exit_code = CedarExitCode::Failure;
}
let schema = match args.schema.get_schema() {
Ok(schema) => schema,
Err(e) => {
println!("{e:?}");
exit_code = CedarExitCode::Failure;
None
}
};
if let Some(e) = args
.entities_file
.as_ref()
.and_then(|e| load_entities(e, schema.as_ref()).err())
{
println!("{e:?}");
exit_code = CedarExitCode::Failure;
}
exit_code
}
pub fn validate(args: &ValidateArgs) -> CedarExitCode {
let mode = match args.validation_mode {
ValidationMode::Strict => cedar_policy::ValidationMode::Strict,
ValidationMode::Permissive => {
#[cfg(not(feature = "permissive-validate"))]
{
eprintln!("Error: arguments include the experimental option `--validation-mode permissive`, but this executable was not built with `permissive-validate` experimental feature enabled");
return CedarExitCode::Failure;
}
#[cfg(feature = "permissive-validate")]
cedar_policy::ValidationMode::Permissive
}
ValidationMode::Partial => {
#[cfg(not(feature = "partial-validate"))]
{
eprintln!("Error: arguments include the experimental option `--validation-mode partial`, but this executable was not built with `partial-validate` experimental feature enabled");
return CedarExitCode::Failure;
}
#[cfg(feature = "partial-validate")]
cedar_policy::ValidationMode::Partial
}
};
let pset = match args.policies.get_policy_set() {
Ok(pset) => pset,
Err(e) => {
println!("{e:?}");
return CedarExitCode::Failure;
}
};
let schema = match args.schema.get_schema() {
Ok(schema) => schema,
Err(e) => {
println!("{e:?}");
return CedarExitCode::Failure;
}
};
let validator = Validator::new(schema);
let result = if let Some(level) = args.level {
validator.validate_with_level(&pset, mode, level)
} else {
validator.validate(&pset, mode)
};
if !result.validation_passed()
|| (args.deny_warnings && !result.validation_passed_without_warnings())
{
println!(
"{:?}",
Report::new(result).wrap_err("policy set validation failed")
);
CedarExitCode::ValidationFailure
} else {
println!(
"{:?}",
Report::new(result).wrap_err("policy set validation passed")
);
CedarExitCode::Success
}
}
pub fn evaluate(args: &EvaluateArgs) -> (CedarExitCode, EvalResult) {
println!();
let schema = match args.schema.get_schema() {
Ok(opt) => opt,
Err(e) => {
println!("{e:?}");
return (CedarExitCode::Failure, EvalResult::Bool(false));
}
};
let request = match args.request.get_request(schema.as_ref()) {
Ok(q) => q,
Err(e) => {
println!("{e:?}");
return (CedarExitCode::Failure, EvalResult::Bool(false));
}
};
let expr =
match Expression::from_str(&args.expression).wrap_err("failed to parse the expression") {
Ok(expr) => expr,
Err(e) => {
println!("{:?}", e.with_source_code(args.expression.clone()));
return (CedarExitCode::Failure, EvalResult::Bool(false));
}
};
let entities = match &args.entities_file {
None => Entities::empty(),
Some(file) => match load_entities(file, schema.as_ref()) {
Ok(entities) => entities,
Err(e) => {
println!("{e:?}");
return (CedarExitCode::Failure, EvalResult::Bool(false));
}
},
};
match eval_expression(&request, &entities, &expr).wrap_err("failed to evaluate the expression")
{
Err(e) => {
println!("{e:?}");
return (CedarExitCode::Failure, EvalResult::Bool(false));
}
Ok(result) => {
println!("{result}");
return (CedarExitCode::Success, result);
}
}
}
pub fn link(args: &LinkArgs) -> CedarExitCode {
if let Err(err) = link_inner(args) {
println!("{err:?}");
CedarExitCode::Failure
} else {
CedarExitCode::Success
}
}
pub fn visualize(args: &VisualizeArgs) -> CedarExitCode {
match load_entities(&args.entities_file, None) {
Ok(entities) => {
println!("{}", entities.to_dot_str());
CedarExitCode::Success
}
Err(report) => {
eprintln!("{report:?}");
CedarExitCode::Failure
}
}
}
fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
let policies_str = read_from_file_or_stdin(args.policies_file.as_ref(), "policy set")?;
let config = Config {
line_width: args.line_width,
indent_width: args.indent_width,
};
let formatted_policy = policies_str_to_pretty(&policies_str, &config)?;
let are_policies_equivalent = policies_str == formatted_policy;
match &args.policies_file {
Some(policies_file) if args.write => {
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(policies_file)
.into_diagnostic()
.wrap_err(format!("failed to open {policies_file} for writing"))?;
file.write_all(formatted_policy.as_bytes())
.into_diagnostic()
.wrap_err(format!(
"failed to write formatted policies to {policies_file}"
))?;
}
_ => print!("{formatted_policy}"),
}
Ok(are_policies_equivalent)
}
pub fn format_policies(args: &FormatArgs) -> CedarExitCode {
match format_policies_inner(args) {
Ok(false) if args.check => CedarExitCode::Failure,
Err(err) => {
println!("{err:?}");
CedarExitCode::Failure
}
_ => CedarExitCode::Success,
}
}
fn translate_policy_to_cedar(
json_src: Option<impl AsRef<Path> + std::marker::Copy>,
) -> Result<String> {
let policy_set = read_json_policy_set(json_src)?;
policy_set.to_cedar().ok_or_else(|| {
miette!("Unable to translate policy set containing template linked policies.")
})
}
fn translate_policy_to_json(
cedar_src: Option<impl AsRef<Path> + std::marker::Copy>,
) -> Result<String> {
let policy_set = read_cedar_policy_set(cedar_src)?;
let output = policy_set.to_json()?.to_string();
Ok(output)
}
fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result<String> {
let translate = match args.direction {
PolicyTranslationDirection::CedarToJson => translate_policy_to_json,
PolicyTranslationDirection::JsonToCedar => translate_policy_to_cedar,
};
translate(args.input_file.as_ref())
}
pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode {
match translate_policy_inner(args) {
Ok(sf) => {
println!("{sf}");
CedarExitCode::Success
}
Err(err) => {
eprintln!("{err:?}");
CedarExitCode::Failure
}
}
}
fn translate_schema_to_cedar(json_src: impl AsRef<str>) -> Result<String> {
let fragment = SchemaFragment::from_json_str(json_src.as_ref())?;
let output = fragment.to_cedarschema()?;
Ok(output)
}
fn translate_schema_to_json(cedar_src: impl AsRef<str>) -> Result<String> {
let (fragment, warnings) = SchemaFragment::from_cedarschema_str(cedar_src.as_ref())?;
for warning in warnings {
let report = miette::Report::new(warning);
eprintln!("{report:?}");
}
let output = fragment.to_json_string()?;
Ok(output)
}
fn translate_schema_to_json_with_resolved_types(cedar_src: impl AsRef<str>) -> Result<String> {
match cedar_policy::schema_str_to_json_with_resolved_types(cedar_src.as_ref()) {
Ok((json_value, warnings)) => {
for warning in &warnings {
eprintln!("{warning}");
}
serde_json::to_string_pretty(&json_value).into_diagnostic()
}
Err(error) => {
Err(miette::Report::new(error))
}
}
}
fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result<String> {
let translate = match args.direction {
SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar,
SchemaTranslationDirection::CedarToJson => translate_schema_to_json,
SchemaTranslationDirection::CedarToJsonWithResolvedTypes => {
translate_schema_to_json_with_resolved_types
}
};
read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate)
}
pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode {
match translate_schema_inner(args) {
Ok(sf) => {
println!("{sf}");
CedarExitCode::Success
}
Err(err) => {
eprintln!("{err:?}");
CedarExitCode::Failure
}
}
}
fn generate_schema(path: &Path) -> Result<()> {
std::fs::write(
path,
serde_json::to_string_pretty(&serde_json::json!(
{
"": {
"entityTypes": {
"A": {
"memberOfTypes": [
"B"
]
},
"B": {
"memberOfTypes": []
},
"C": {
"memberOfTypes": []
}
},
"actions": {
"action": {
"appliesTo": {
"resourceTypes": [
"C"
],
"principalTypes": [
"A",
"B"
]
}
}
}
}
}))
.into_diagnostic()?,
)
.into_diagnostic()
}
fn generate_policy(path: &Path) -> Result<()> {
std::fs::write(
path,
r#"permit (
principal in A::"a",
action == Action::"action",
resource == C::"c"
) when { true };
"#,
)
.into_diagnostic()
}
fn generate_entities(path: &Path) -> Result<()> {
std::fs::write(
path,
serde_json::to_string_pretty(&serde_json::json!(
[
{
"uid": { "type": "A", "id": "a"} ,
"attrs": {},
"parents": [{"type": "B", "id": "b"}]
},
{
"uid": { "type": "B", "id": "b"} ,
"attrs": {},
"parents": []
},
{
"uid": { "type": "C", "id": "c"} ,
"attrs": {},
"parents": []
}
]))
.into_diagnostic()?,
)
.into_diagnostic()
}
fn new_inner(args: &NewArgs) -> Result<()> {
let dir = &std::env::current_dir().into_diagnostic()?.join(&args.name);
std::fs::create_dir(dir).into_diagnostic()?;
let schema_path = dir.join("schema.cedarschema.json");
let policy_path = dir.join("policy.cedar");
let entities_path = dir.join("entities.json");
generate_schema(&schema_path)?;
generate_policy(&policy_path)?;
generate_entities(&entities_path)
}
pub fn new(args: &NewArgs) -> CedarExitCode {
if let Err(err) = new_inner(args) {
println!("{err:?}");
CedarExitCode::Failure
} else {
CedarExitCode::Success
}
}
pub fn language_version() -> CedarExitCode {
let version = get_lang_version();
println!(
"Cedar language version: {}.{}",
version.major, version.minor
);
CedarExitCode::Success
}
#[cfg(not(feature = "analyze"))]
pub fn symcc(_: &SymccArgs) -> CedarExitCode {
eprintln!("Cannot run `symcc`: this Cedar CLI was built without the 'analyze' feature enabled");
CedarExitCode::Failure
}
#[cfg(feature = "analyze")]
pub fn symcc(args: &SymccArgs) -> CedarExitCode {
let rt = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
eprintln!("Failed to initialize async runtime: {e}");
return CedarExitCode::Failure;
}
};
rt.block_on(async {
match symcc_async(args).await {
Ok(()) => CedarExitCode::Success,
Err(e) => {
eprintln!("Analysis failed: {e:?}");
CedarExitCode::Failure
}
}
})
}
#[cfg(feature = "analyze")]
fn initialize_solver(
cvc5_path: &Option<PathBuf>,
) -> Result<cedar_policy_symcc::solver::LocalSolver> {
match cvc5_path {
Some(p) => cedar_policy_symcc::solver::LocalSolver::from_command(
tokio::process::Command::new(p).args(["--lang", "smt", "--tlimit=60000"]),
)
.map_err(|e| {
miette!(
"CVC5 solver not found or failed to start at '{}': {e}",
p.display()
)
}),
None => cedar_policy_symcc::solver::LocalSolver::cvc5()
.map_err(|e| miette!("CVC5 solver not found or failed to start: {e}")),
}
}
#[cfg(feature = "analyze")]
fn warn_if_contains_templates(pset: &PolicySet, name: &str) {
let num_templates = pset.templates().count();
if num_templates > 0 {
let report = miette!(
severity = miette::Severity::Warning,
"{name} contains {num_templates} policy template(s), which will be ignored by analysis"
);
eprintln!("{report:?}");
}
}
#[cfg(feature = "analyze")]
fn load_single_policy(
policies: &SymccPoliciesArgs,
schema_args: &SchemaArgs,
) -> Result<(Policy, Schema)> {
let pset = policies.get_policy_set()?;
let schema = schema_args.get_schema()?;
let policy = pset
.policies()
.exactly_one()
.map_err(|e| miette!("Expected exactly one policy, found {}", e.count()))?
.clone();
Ok((policy, schema))
}
#[cfg(feature = "analyze")]
fn load_two_policies(
args: &TwoPolicyArgs,
schema_args: &SchemaArgs,
) -> Result<(Policy, Policy, Schema)> {
let pset1 = args.get_policy_set_1()?;
let pset2 = args.get_policy_set_2()?;
let schema = schema_args.get_schema()?;
let p1 = pset1
.policies()
.exactly_one()
.map_err(|e| {
miette!(
"Expected exactly one policy in --policy1, found {}",
e.count()
)
})?
.clone();
let p2 = pset2
.policies()
.exactly_one()
.map_err(|e| {
miette!(
"Expected exactly one policy in --policy2, found {}",
e.count()
)
})?
.clone();
Ok((p1, p2, schema))
}
#[cfg(feature = "analyze")]
fn load_policy_set(
policies: &SymccPoliciesArgs,
schema_args: &SchemaArgs,
) -> Result<(PolicySet, Schema)> {
let pset = policies.get_policy_set()?;
warn_if_contains_templates(&pset, "policy set");
let schema = schema_args.get_schema()?;
Ok((pset, schema))
}
#[cfg(feature = "analyze")]
fn load_two_policy_sets(
args: &SymccTwoPoliciesArgs,
schema_args: &SchemaArgs,
) -> Result<(PolicySet, PolicySet, Schema)> {
let pset1 = args.get_policy_set_1()?;
let pset2 = args.get_policy_set_2()?;
warn_if_contains_templates(&pset1, "first policy set");
warn_if_contains_templates(&pset2, "second policy set");
let schema = schema_args.get_schema()?;
Ok((pset1, pset2, schema))
}
#[cfg(feature = "analyze")]
fn format_bool_result(holds: bool, property: &str) {
if holds {
println!("✓ {property}: VERIFIED");
} else {
println!("✗ {property}: DOES NOT HOLD");
}
}
#[cfg(feature = "analyze")]
fn format_counterexample_result(
cex: Option<cedar_policy_symcc::Env>,
property: &str,
verbose: bool,
) {
match cex {
None => {
println!("✓ {property}: VERIFIED");
if verbose {
println!(" No counterexample found — property holds for all well-formed inputs.");
}
}
Some(env) => {
println!("✗ {property}: DOES NOT HOLD");
println!(" Counterexample found:");
println!("{env}");
}
}
}
#[cfg(feature = "analyze")]
fn build_request_env(args: &SymccArgs) -> Result<RequestEnv> {
let principal_type: EntityTypeName = args
.principal_type
.parse()
.map_err(|e| miette!("Invalid --principal-type '{}': {e}", args.principal_type))?;
let action: EntityUid = args
.action
.parse()
.map_err(|e| miette!("Invalid --action '{}': {e}", args.action))?;
let resource_type: EntityTypeName = args
.resource_type
.parse()
.map_err(|e| miette!("Invalid --resource-type '{}': {e}", args.resource_type))?;
Ok(RequestEnv::new(principal_type, action, resource_type))
}
#[cfg(feature = "analyze")]
async fn symcc_async(args: &SymccArgs) -> Result<()> {
use cedar_policy_symcc::{CedarSymCompiler, CompiledPolicy, CompiledPolicySet};
let solver = initialize_solver(&args.cvc5_path)?;
let mut compiler = CedarSymCompiler::new(solver)
.map_err(|e| miette!("Failed to initialize SymCC compiler: {e}"))?;
let req_env = build_request_env(args)?;
match &args.command {
SymccCommands::NeverErrors(cmd_args) => {
let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_never_errors_with_counterexample_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(result, "Policy never errors", args.verbose);
} else {
let holds = compiler
.check_never_errors_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy never errors");
}
}
SymccCommands::AlwaysMatches(cmd_args) => {
let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_always_matches_with_counterexample_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(result, "Policy always matches", args.verbose);
} else {
let holds = compiler
.check_always_matches_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy always matches");
}
}
SymccCommands::NeverMatches(cmd_args) => {
let (policy, schema) = load_single_policy(cmd_args, &args.schema)?;
let compiled = CompiledPolicy::compile(&policy, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_never_matches_with_counterexample_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(result, "Policy never matches", args.verbose);
} else {
let holds = compiler
.check_never_matches_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy never matches");
}
}
SymccCommands::MatchesEquivalent(cmd_args) => {
let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy1: {e}"))?;
let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy2: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_matches_equivalent_with_counterexample_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(
result,
"Policies have equivalent match conditions",
args.verbose,
);
} else {
let holds = compiler
.check_matches_equivalent_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policies have equivalent match conditions");
}
}
SymccCommands::MatchesImplies(cmd_args) => {
let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy1: {e}"))?;
let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy2: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_matches_implies_with_counterexample_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(
result,
"Policy1 match implies Policy2 match",
args.verbose,
);
} else {
let holds = compiler
.check_matches_implies_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy1 match implies Policy2 match");
}
}
SymccCommands::MatchesDisjoint(cmd_args) => {
let (p1, p2, schema) = load_two_policies(cmd_args, &args.schema)?;
let compiled1 = CompiledPolicy::compile(&p1, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy1: {e}"))?;
let compiled2 = CompiledPolicy::compile(&p2, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy2: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_matches_disjoint_with_counterexample_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(
result,
"Policies have disjoint match conditions",
args.verbose,
);
} else {
let holds = compiler
.check_matches_disjoint_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policies have disjoint match conditions");
}
}
SymccCommands::AlwaysAllows(cmd_args) => {
let (pset, schema) = load_policy_set(cmd_args, &args.schema)?;
let compiled = CompiledPolicySet::compile(&pset, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy set: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_always_allows_with_counterexample_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(result, "Policy set always allows", args.verbose);
} else {
let holds = compiler
.check_always_allows_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy set always allows");
}
}
SymccCommands::AlwaysDenies(cmd_args) => {
let (pset, schema) = load_policy_set(cmd_args, &args.schema)?;
let compiled = CompiledPolicySet::compile(&pset, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy set: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_always_denies_with_counterexample_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(result, "Policy set always denies", args.verbose);
} else {
let holds = compiler
.check_always_denies_opt(&compiled)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy set always denies");
}
}
SymccCommands::Equivalent(cmd_args) => {
let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_equivalent_with_counterexample_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(result, "Policy sets are equivalent", args.verbose);
} else {
let holds = compiler
.check_equivalent_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy sets are equivalent");
}
}
SymccCommands::Implies(cmd_args) => {
let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_implies_with_counterexample_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(
result,
"Policy set 1 implies policy set 2",
args.verbose,
);
} else {
let holds = compiler
.check_implies_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy set 1 implies policy set 2");
}
}
SymccCommands::Disjoint(cmd_args) => {
let (pset1, pset2, schema) = load_two_policy_sets(cmd_args, &args.schema)?;
let compiled1 = CompiledPolicySet::compile(&pset1, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy set 1: {e}"))?;
let compiled2 = CompiledPolicySet::compile(&pset2, &req_env, &schema)
.map_err(|e| miette!("Failed to compile policy set 2: {e}"))?;
if args.counterexample && !args.no_counterexample {
let result = compiler
.check_disjoint_with_counterexample_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_counterexample_result(result, "Policy sets are disjoint", args.verbose);
} else {
let holds = compiler
.check_disjoint_opt(&compiled1, &compiled2)
.await
.map_err(|e| miette!("Verification failed: {e}"))?;
format_bool_result(holds, "Policy sets are disjoint");
}
}
}
Ok(())
}
fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
data.iter()
.map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
.collect::<Result<HashMap<SlotId, EntityUid>>>()
}
fn link_inner(args: &LinkArgs) -> Result<()> {
let mut policies = args.policies.get_policy_set()?;
let slotenv = create_slot_env(&args.arguments.data)?;
policies.link(
PolicyId::new(&args.template_id),
PolicyId::new(&args.new_id),
slotenv,
)?;
let linked = policies
.policy(&PolicyId::new(&args.new_id))
.ok_or_else(|| miette!("Failed to find newly-added template-linked policy"))?;
println!("Template-linked policy added: {linked}");
if let Some(links_filename) = args.policies.template_linked_file.as_ref() {
update_template_linked_file(
links_filename,
TemplateLinked {
template_id: args.template_id.clone(),
link_id: args.new_id.clone(),
args: args.arguments.data.clone(),
},
)?;
}
Ok(())
}
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(try_from = "LiteralTemplateLinked")]
#[serde(into = "LiteralTemplateLinked")]
struct TemplateLinked {
template_id: String,
link_id: String,
args: HashMap<SlotId, String>,
}
impl TryFrom<LiteralTemplateLinked> for TemplateLinked {
type Error = String;
fn try_from(value: LiteralTemplateLinked) -> Result<Self, Self::Error> {
Ok(Self {
template_id: value.template_id,
link_id: value.link_id,
args: value
.args
.into_iter()
.map(|(k, v)| parse_slot_id(k).map(|slot_id| (slot_id, v)))
.collect::<Result<HashMap<SlotId, String>, Self::Error>>()?,
})
}
}
fn parse_slot_id<S: AsRef<str>>(s: S) -> Result<SlotId, String> {
match s.as_ref() {
"?principal" => Ok(SlotId::principal()),
"?resource" => Ok(SlotId::resource()),
_ => Err(format!(
"Invalid SlotId! Expected ?principal|?resource, got: {}",
s.as_ref()
)),
}
}
#[derive(Serialize, Deserialize)]
struct LiteralTemplateLinked {
template_id: String,
link_id: String,
args: HashMap<String, String>,
}
impl From<TemplateLinked> for LiteralTemplateLinked {
fn from(i: TemplateLinked) -> Self {
Self {
template_id: i.template_id,
link_id: i.link_id,
args: i
.args
.into_iter()
.map(|(k, v)| (format!("{k}"), v))
.collect(),
}
}
}
fn add_template_links_to_set(path: impl AsRef<Path>, policy_set: &mut PolicySet) -> Result<()> {
for template_linked in load_links_from_file(path)? {
let slot_env = create_slot_env(&template_linked.args)?;
policy_set.link(
PolicyId::new(&template_linked.template_id),
PolicyId::new(&template_linked.link_id),
slot_env,
)?;
}
Ok(())
}
fn load_links_from_file(path: impl AsRef<Path>) -> Result<Vec<TemplateLinked>> {
let f = match std::fs::File::open(path) {
Ok(f) => f,
Err(_) => {
return Ok(vec![]);
}
};
if f.metadata()
.into_diagnostic()
.wrap_err("Failed to read metadata")?
.len()
== 0
{
Ok(vec![])
} else {
serde_json::from_reader(f)
.into_diagnostic()
.wrap_err("Deserialization error")
}
}
fn update_template_linked_file(path: impl AsRef<Path>, new_linked: TemplateLinked) -> Result<()> {
let mut template_linked = load_links_from_file(path.as_ref())?;
template_linked.push(new_linked);
write_template_linked_file(&template_linked, path.as_ref())
}
fn write_template_linked_file(linked: &[TemplateLinked], path: impl AsRef<Path>) -> Result<()> {
let f = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)
.into_diagnostic()?;
serde_json::to_writer(f, linked).into_diagnostic()
}
pub fn authorize(args: &AuthorizeArgs) -> CedarExitCode {
println!();
let ans = execute_request(
&args.request,
&args.policies,
&args.entities_file,
&args.schema,
args.timing,
);
match ans {
Ok(ans) => {
let status = match ans.decision() {
Decision::Allow => {
println!("ALLOW");
CedarExitCode::Success
}
Decision::Deny => {
println!("DENY");
CedarExitCode::AuthorizeDeny
}
};
if ans.diagnostics().errors().peekable().peek().is_some() {
println!();
for err in ans.diagnostics().errors() {
println!("{err}");
}
}
if args.verbose {
println!();
if ans.diagnostics().reason().peekable().peek().is_none() {
println!("note: no policies applied to this request");
} else {
println!("note: this decision was due to the following policies:");
for reason in ans.diagnostics().reason() {
println!(" {reason}");
}
println!();
}
}
status
}
Err(errs) => {
for err in errs {
println!("{err:?}");
}
CedarExitCode::Failure
}
}
}
#[cfg(not(feature = "partial-eval"))]
pub fn partial_authorize(_: &PartiallyAuthorizeArgs) -> CedarExitCode {
{
eprintln!("Error: option `partially-authorize` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
return CedarExitCode::Failure;
}
}
#[cfg(not(feature = "tpe"))]
pub fn tpe(_: &TpeArgs) -> CedarExitCode {
{
eprintln!("Error: option `tpe` is experimental, but this executable was not built with `partial-eval` experimental feature enabled");
return CedarExitCode::Failure;
}
}
#[cfg(feature = "tpe")]
pub fn tpe(args: &TpeArgs) -> CedarExitCode {
println!();
let ret = |errs| {
for err in errs {
println!("{err:?}");
}
CedarExitCode::Failure
};
let mut errs = vec![];
let policies = match args.policies.get_policy_set() {
Ok(pset) => pset,
Err(e) => {
errs.push(e);
PolicySet::new()
}
};
let schema: Schema = match args.schema.get_schema() {
Ok(opt) => opt,
Err(e) => {
errs.push(e);
return ret(errs);
}
};
let entities = match load_partial_entities(args.entities_file.clone(), &schema) {
Ok(entities) => entities,
Err(e) => {
errs.push(e);
PartialEntities::empty()
}
};
match args.request.get_request(&schema) {
Ok(request) if errs.is_empty() => {
let auth_start = Instant::now();
let ans = policies.tpe(&request, &entities, &schema);
let auth_dur = auth_start.elapsed();
match ans {
Ok(ans) => {
if args.timing {
println!(
"Authorization Time (micro seconds) : {}",
auth_dur.as_micros()
);
}
match ans.decision() {
Some(Decision::Allow) => {
println!("ALLOW");
CedarExitCode::Success
}
Some(Decision::Deny) => {
println!("DENY");
CedarExitCode::AuthorizeDeny
}
None => {
println!("UNKNOWN");
println!("All policy residuals:");
for p in ans.residual_policies() {
println!("{p}");
}
CedarExitCode::Unknown
}
}
}
Err(err) => {
errs.push(miette!("{err}"));
return ret(errs);
}
}
}
Ok(_) => {
return ret(errs);
}
Err(e) => {
errs.push(e.wrap_err("failed to parse request"));
return ret(errs);
}
}
}
#[cfg(feature = "partial-eval")]
pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
println!();
let ans = execute_partial_request(
&args.request,
&args.policies,
&args.entities_file,
&args.schema,
args.timing,
);
match ans {
Ok(ans) => match ans.decision() {
Some(Decision::Allow) => {
println!("ALLOW");
CedarExitCode::Success
}
Some(Decision::Deny) => {
println!("DENY");
CedarExitCode::AuthorizeDeny
}
None => {
println!("UNKNOWN");
println!("All policy residuals:");
for p in ans.nontrivial_residuals() {
println!("{p}");
}
CedarExitCode::Unknown
}
},
Err(errs) => {
for err in errs {
println!("{err:?}");
}
CedarExitCode::Failure
}
}
}
#[derive(Clone, Debug)]
enum TestResult {
Pass,
Fail(String),
}
fn compare_test_decisions(test: &TestCase, ans: &Response) -> TestResult {
if ans.decision() == test.decision.into() {
let mut errors = Vec::new();
let reason = ans.diagnostics().reason().collect::<BTreeSet<_>>();
let missing_reason = test
.reason
.iter()
.filter(|r| !reason.contains(&PolicyId::new(r)))
.collect::<Vec<_>>();
if !missing_reason.is_empty() {
errors.push(format!(
"missing reason(s): {}",
missing_reason
.into_iter()
.map(|r| format!("`{r}`"))
.collect::<Vec<_>>()
.join(", ")
));
}
let num_errors = ans.diagnostics().errors().count();
if num_errors != test.num_errors {
errors.push(format!(
"expected {} error(s), but got {} runtime error(s){}",
test.num_errors,
num_errors,
if num_errors == 0 {
"".to_string()
} else {
format!(
": {}",
ans.diagnostics()
.errors()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", ")
)
},
));
}
if errors.is_empty() {
TestResult::Pass
} else {
TestResult::Fail(errors.join("; "))
}
} else {
TestResult::Fail(format!(
"expected {:?}, got {:?}",
test.decision,
ans.decision()
))
}
}
fn run_one_test(
policies: &PolicySet,
test: &serde_json::Value,
validator: Option<&Validator>,
) -> Result<TestResult> {
let test = CheckedTestCaseSeed(validator.map(Validator::schema))
.deserialize(test.into_deserializer())
.into_diagnostic()?;
if let Some(validator) = validator {
let val_res = validator.validate(policies, cedar_policy::ValidationMode::Strict);
if !val_res.validation_passed_without_warnings() {
return Err(Report::new(val_res).wrap_err("policy set validation failed"));
}
}
let ans = Authorizer::new().is_authorized(&test.request, policies, &test.entities);
Ok(compare_test_decisions(&test, &ans))
}
fn run_tests_inner(args: &RunTestsArgs) -> Result<CedarExitCode> {
let policies = args.policies.get_policy_set()?;
let tests = load_partial_tests(&args.tests)?;
let validator = args.schema.get_schema()?.map(Validator::new);
let mut total_fails: usize = 0;
println!("running {} test(s)", tests.len());
for test in tests.iter() {
if let Some(name) = test["name"].as_str() {
print!(" test {name} ... ");
} else {
print!(" test (unnamed) ... ");
}
std::io::stdout().flush().into_diagnostic()?;
match run_one_test(&policies, test, validator.as_ref()) {
Ok(TestResult::Pass) => {
println!(
"{}",
"ok".if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
);
}
Ok(TestResult::Fail(reason)) => {
total_fails += 1;
println!(
"{}: {}",
"fail".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
reason
);
}
Err(e) => {
total_fails += 1;
println!(
"{}:\n {:?}",
"error".if_supports_color(owo_colors::Stream::Stdout, |s| s.red()),
e
);
}
}
}
println!(
"results: {} {}, {} {}",
tests.len() - total_fails,
if total_fails == 0 {
"passed"
.if_supports_color(owo_colors::Stream::Stdout, |s| s.green())
.to_string()
} else {
"passed".to_string()
},
total_fails,
if total_fails != 0 {
"failed"
.if_supports_color(owo_colors::Stream::Stdout, |s| s.red())
.to_string()
} else {
"failed".to_string()
},
);
Ok(if total_fails != 0 {
CedarExitCode::Failure
} else {
CedarExitCode::Success
})
}
pub fn run_tests(args: &RunTestsArgs) -> CedarExitCode {
run_tests_inner(args).unwrap_or_else(|e| {
println!("{e:?}");
CedarExitCode::Failure
})
}
#[derive(Copy, Clone, Debug, Deserialize)]
enum ExpectedDecision {
#[serde(rename = "allow")]
Allow,
#[serde(rename = "deny")]
Deny,
}
impl From<ExpectedDecision> for Decision {
fn from(value: ExpectedDecision) -> Self {
match value {
ExpectedDecision::Allow => Decision::Allow,
ExpectedDecision::Deny => Decision::Deny,
}
}
}
#[derive(Clone, Debug, Deserialize)]
struct UncheckedTestCase {
request: RequestJSON,
entities: serde_json::Value,
decision: ExpectedDecision,
reason: Vec<String>,
num_errors: usize,
}
#[derive(Clone, Debug)]
struct TestCase {
request: Request,
entities: Entities,
decision: ExpectedDecision,
reason: Vec<String>,
num_errors: usize,
}
struct CheckedTestCaseSeed<'a>(Option<&'a Schema>);
impl<'de, 'a> DeserializeSeed<'de> for CheckedTestCaseSeed<'a> {
type Value = TestCase;
fn deserialize<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let UncheckedTestCase {
request,
entities,
decision,
reason,
num_errors,
} = UncheckedTestCase::deserialize(deserializer)?;
let principal = request.principal.parse().map_err(|e| {
serde::de::Error::custom(format!(
"failed to parse principal `{}`: {}",
request.principal, e
))
})?;
let action = request.action.parse().map_err(|e| {
serde::de::Error::custom(format!(
"failed to parse action `{}`: {}",
request.action, e
))
})?;
let resource = request.resource.parse().map_err(|e| {
serde::de::Error::custom(format!(
"failed to parse resource `{}`: {}",
request.resource, e
))
})?;
let context = Context::from_json_value(request.context.clone(), None).map_err(|e| {
serde::de::Error::custom(format!(
"failed to parse context `{}`: {}",
request.context, e
))
})?;
let request = Request::new(principal, action, resource, context, self.0)
.map_err(|e| serde::de::Error::custom(format!("failed to create request: {e}")))?;
let entities = Entities::from_json_value(entities, self.0)
.map_err(|e| serde::de::Error::custom(format!("failed to parse entities: {e}")))?;
Ok(TestCase {
request,
entities,
decision,
reason,
num_errors,
})
}
}
fn load_partial_tests(tests_filename: impl AsRef<Path>) -> Result<Vec<serde_json::Value>> {
match std::fs::OpenOptions::new()
.read(true)
.open(tests_filename.as_ref())
{
Ok(f) => {
let reader = BufReader::new(f);
serde_json::from_reader(reader).map_err(|e| {
miette!(
"failed to parse tests from file {}: {e}",
tests_filename.as_ref().display()
)
})
}
Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
format!(
"failed to open test file {}",
tests_filename.as_ref().display()
)
}),
}
}
#[cfg(feature = "tpe")]
fn load_partial_entities(
entities_filename: impl AsRef<Path>,
schema: &Schema,
) -> Result<PartialEntities> {
match std::fs::OpenOptions::new()
.read(true)
.open(entities_filename.as_ref())
{
Ok(f) => {
PartialEntities::from_json_value(serde_json::from_reader(f).into_diagnostic()?, schema)
.map_err(|e| miette!("{e}"))
.wrap_err_with(|| {
format!(
"failed to parse entities from file {}",
entities_filename.as_ref().display()
)
})
}
Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
format!(
"failed to open entities file {}",
entities_filename.as_ref().display()
)
}),
}
}
fn load_entities(entities_filename: impl AsRef<Path>, schema: Option<&Schema>) -> Result<Entities> {
match std::fs::OpenOptions::new()
.read(true)
.open(entities_filename.as_ref())
{
Ok(f) => Entities::from_json_file(f, schema).wrap_err_with(|| {
format!(
"failed to parse entities from file {}",
entities_filename.as_ref().display()
)
}),
Err(e) => Err(e).into_diagnostic().wrap_err_with(|| {
format!(
"failed to open entities file {}",
entities_filename.as_ref().display()
)
}),
}
}
fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
let mut new_ps = PolicySet::new();
let t_iter = ps.templates().map(|t| match t.annotation("id") {
None => Ok(t.clone()),
Some(anno) => anno.parse().map(|a| t.new_id(a)),
});
for t in t_iter {
let template = t.unwrap_or_else(|never| match never {});
new_ps
.add_template(template)
.wrap_err("failed to add template to policy set")?;
}
let p_iter = ps.policies().map(|p| match p.annotation("id") {
None => Ok(p.clone()),
Some(anno) => anno.parse().map(|a| p.new_id(a)),
});
for p in p_iter {
let policy = p.unwrap_or_else(|never| match never {});
new_ps
.add(policy)
.wrap_err("failed to add template to policy set")?;
}
Ok(new_ps)
}
fn read_from_file_or_stdin(filename: Option<&impl AsRef<Path>>, context: &str) -> Result<String> {
let mut src_str = String::new();
match filename {
Some(path) => {
src_str = std::fs::read_to_string(path)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to open {context} file {}", path.as_ref().display())
})?;
}
None => {
std::io::Read::read_to_string(&mut std::io::stdin(), &mut src_str)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {context} from stdin"))?;
}
};
Ok(src_str)
}
fn read_from_file(filename: impl AsRef<Path>, context: &str) -> Result<String> {
read_from_file_or_stdin(Some(&filename), context)
}
fn read_cedar_policy_set(
filename: Option<impl AsRef<Path> + std::marker::Copy>,
) -> Result<PolicySet> {
let context = "policy set";
let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
let ps = PolicySet::from_str(&ps_str)
.map_err(|err| {
let name = filename.map_or_else(
|| "<stdin>".to_owned(),
|n| n.as_ref().display().to_string(),
);
Report::new(err).with_source_code(NamedSource::new(name, ps_str))
})
.wrap_err_with(|| format!("failed to parse {context}"))?;
rename_from_id_annotation(&ps)
}
fn read_json_policy_set(
filename: Option<impl AsRef<Path> + std::marker::Copy>,
) -> Result<PolicySet> {
let context = "JSON policy";
let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
let policy_type = get_json_policy_type(&json)?;
let add_json_source = |report: Report| {
let name = filename.map_or_else(
|| "<stdin>".to_owned(),
|n| n.as_ref().display().to_string(),
);
report.with_source_code(NamedSource::new(name, json_source.clone()))
};
match policy_type {
JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
Ok(policy) => PolicySet::from_policies([policy])
.wrap_err_with(|| format!("failed to create policy set from {context}")),
Err(_) => match Template::from_json(None, json)
.map_err(|err| add_json_source(Report::new(err)))
{
Ok(template) => {
let mut ps = PolicySet::new();
ps.add_template(template)?;
Ok(ps)
}
Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
},
},
JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
.map_err(|err| add_json_source(Report::new(err)))
.wrap_err_with(|| format!("failed to create policy set from {context}")),
}
}
fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
let json_has_property = |p| json.get(p).is_some();
let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
let has_any_policy_property = policy_properties.iter().any(json_has_property);
match (has_any_policy_set_property, has_any_policy_property) {
(false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
(true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
(true, _) => Ok(JsonPolicyType::PolicySet),
(_, true) => Ok(JsonPolicyType::SinglePolicy),
}
}
enum JsonPolicyType {
SinglePolicy,
PolicySet,
}
fn execute_request(
request: &RequestArgs,
policies: &PoliciesArgs,
entities_filename: impl AsRef<Path>,
schema: &OptionalSchemaArgs,
compute_duration: bool,
) -> Result<Response, Vec<Report>> {
let mut errs = vec![];
let policies = match policies.get_policy_set() {
Ok(pset) => pset,
Err(e) => {
errs.push(e);
PolicySet::new()
}
};
let schema = match schema.get_schema() {
Ok(opt) => opt,
Err(e) => {
errs.push(e);
None
}
};
let entities = match load_entities(entities_filename, schema.as_ref()) {
Ok(entities) => entities,
Err(e) => {
errs.push(e);
Entities::empty()
}
};
match request.get_request(schema.as_ref()) {
Ok(request) if errs.is_empty() => {
let authorizer = Authorizer::new();
let auth_start = Instant::now();
let ans = authorizer.is_authorized(&request, &policies, &entities);
let auth_dur = auth_start.elapsed();
if compute_duration {
println!(
"Authorization Time (micro seconds) : {}",
auth_dur.as_micros()
);
}
Ok(ans)
}
Ok(_) => Err(errs),
Err(e) => {
errs.push(e.wrap_err("failed to parse request"));
Err(errs)
}
}
}
#[cfg(feature = "partial-eval")]
fn execute_partial_request(
request: &PartialRequestArgs,
policies: &PoliciesArgs,
entities_filename: impl AsRef<Path>,
schema: &OptionalSchemaArgs,
compute_duration: bool,
) -> Result<PartialResponse, Vec<Report>> {
let mut errs = vec![];
let policies = match policies.get_policy_set() {
Ok(pset) => pset,
Err(e) => {
errs.push(e);
PolicySet::new()
}
};
let schema = match schema.get_schema() {
Ok(opt) => opt,
Err(e) => {
errs.push(e);
None
}
};
let entities = match load_entities(entities_filename, schema.as_ref()) {
Ok(entities) => entities,
Err(e) => {
errs.push(e);
Entities::empty()
}
};
match request.get_request(schema.as_ref()) {
Ok(request) if errs.is_empty() => {
let authorizer = Authorizer::new();
let auth_start = Instant::now();
let ans = authorizer.is_authorized_partial(&request, &policies, &entities);
let auth_dur = auth_start.elapsed();
if compute_duration {
println!(
"Authorization Time (micro seconds) : {}",
auth_dur.as_micros()
);
}
Ok(ans)
}
Ok(_) => Err(errs),
Err(e) => {
errs.push(e.wrap_err("failed to parse request"));
Err(errs)
}
}
}