use crate::CedarExitCode;
use clap::Args;
use crate::{PoliciesArgs, SchemaArgs};
use cedar_policy::{
Context, Decision, EntityId, EntityUid, PartialEntities, PartialEntityUid, PartialRequest,
PolicySet, Schema,
};
use miette::{miette, IntoDiagnostic, Result, WrapErr};
use serde::Deserialize;
use std::{path::Path, time::Instant};
#[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,
}
#[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>,
}
#[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>,
}
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,
)?)
}
}
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}"));
ret(errs)
}
}
}
Ok(_) => ret(errs),
Err(e) => {
errs.push(e.wrap_err("failed to parse request"));
ret(errs)
}
}
}
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()
)
}),
}
}