use super::trait_::{Gate, GateDecision, GateRequest};
use crate::tenant::current_tenant;
use crate::types::TenantId;
use async_trait::async_trait;
use cedar_policy::{Authorizer, Context, Entities, EntityUid, PolicySet, Request};
use std::collections::HashMap;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum PolicyGateError {
#[error("cedar policy parse for {tenant_label}: {message}")]
Parse {
tenant_label: String,
message: String,
},
}
pub struct PolicyGate {
bundles: HashMap<Option<TenantId>, PolicySet>,
authorizer: Authorizer,
}
impl PolicyGate {
pub fn from_cedar_source(src: &str) -> Result<Self, PolicyGateError> {
let policies = PolicySet::from_str(src).map_err(|e| PolicyGateError::Parse {
tenant_label: "default".into(),
message: e.to_string(),
})?;
let mut bundles = HashMap::new();
bundles.insert(None, policies);
Ok(Self {
bundles,
authorizer: Authorizer::new(),
})
}
pub fn per_tenant(per_tenant: HashMap<TenantId, String>) -> Result<Self, PolicyGateError> {
let mut bundles = HashMap::with_capacity(per_tenant.len());
for (tenant, src) in per_tenant {
let policies = PolicySet::from_str(&src).map_err(|e| PolicyGateError::Parse {
tenant_label: tenant.0.clone(),
message: e.to_string(),
})?;
bundles.insert(Some(tenant), policies);
}
Ok(Self {
bundles,
authorizer: Authorizer::new(),
})
}
pub fn with_default_and_per_tenant(
default_src: &str,
per_tenant: HashMap<TenantId, String>,
) -> Result<Self, PolicyGateError> {
let mut gate = Self::per_tenant(per_tenant)?;
let default_policies =
PolicySet::from_str(default_src).map_err(|e| PolicyGateError::Parse {
tenant_label: "default".into(),
message: e.to_string(),
})?;
gate.bundles.insert(None, default_policies);
Ok(gate)
}
pub fn with_tenant_bundle(
mut self,
tenant: TenantId,
src: &str,
) -> Result<Self, PolicyGateError> {
let policies = PolicySet::from_str(src).map_err(|e| PolicyGateError::Parse {
tenant_label: tenant.0.clone(),
message: e.to_string(),
})?;
self.bundles.insert(Some(tenant), policies);
Ok(self)
}
fn resolve_bundle(&self, tenant: &Option<TenantId>) -> Option<&PolicySet> {
if let Some(t) = tenant {
if let Some(p) = self.bundles.get(&Some(t.clone())) {
return Some(p);
}
}
self.bundles.get(&None)
}
fn build_uid(prefix: &str, name: &str) -> Result<EntityUid, GateDecision> {
EntityUid::from_str(&format!("{prefix}::\"{name}\"")).map_err(|e| GateDecision::Deny {
code: format!("cedar.{}.invalid", prefix.to_ascii_lowercase()),
reason: format!("could not form {prefix} uid: {e}"),
})
}
}
#[async_trait]
impl Gate for PolicyGate {
async fn evaluate(&self, req: GateRequest) -> GateDecision {
let tenant = current_tenant();
let Some(policies) = self.resolve_bundle(&tenant) else {
return GateDecision::Deny {
code: "policy.no_bundle_for_tenant".into(),
reason: format!(
"no Cedar bundle registered for tenant {tenant:?} and no default fallback present"
),
};
};
let principal = match Self::build_uid("Tool", &req.tool_name) {
Ok(u) => u,
Err(d) => return d,
};
let action = match EntityUid::from_str("Action::\"invoke\"") {
Ok(u) => u,
Err(e) => {
return GateDecision::Deny {
code: "cedar.action.invalid".into(),
reason: format!("could not form action uid: {e}"),
};
}
};
let resource = match Self::build_uid("Args", &req.tool_name) {
Ok(u) => u,
Err(d) => return d,
};
let ctx_pairs: Vec<(String, cedar_policy::RestrictedExpression)> = match &req.args {
serde_json::Value::Object(map) => {
let mut pairs = Vec::with_capacity(map.len());
for (k, v) in map {
match json_to_cedar_expr(v) {
Some(expr) => pairs.push((k.clone(), expr)),
None => {
return GateDecision::Deny {
code: "cedar.context.unrepresentable".into(),
reason: format!("arg `{k}` is not Cedar-representable"),
};
}
}
}
pairs
}
_ => Vec::new(),
};
let context = match Context::from_pairs(ctx_pairs) {
Ok(c) => c,
Err(e) => {
return GateDecision::Deny {
code: "cedar.context.invalid".into(),
reason: format!("context build: {e}"),
};
}
};
let request = match Request::new(principal, action, resource, context, None) {
Ok(r) => r,
Err(e) => {
return GateDecision::Deny {
code: "cedar.request.invalid".into(),
reason: format!("request build: {e}"),
};
}
};
let entities = Entities::empty();
let response = self.authorizer.is_authorized(&request, policies, &entities);
match response.decision() {
cedar_policy::Decision::Allow => GateDecision::Allow,
cedar_policy::Decision::Deny => GateDecision::Deny {
code: "cedar.deny".into(),
reason: "cedar deny".to_string(),
},
}
}
fn name(&self) -> &'static str {
"PolicyGate"
}
}
fn json_to_cedar_expr(v: &serde_json::Value) -> Option<cedar_policy::RestrictedExpression> {
use cedar_policy::RestrictedExpression as RE;
match v {
serde_json::Value::Bool(b) => Some(RE::new_bool(*b)),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Some(RE::new_long(i))
} else if let Some(u) = n.as_u64() {
i64::try_from(u).ok().map(RE::new_long)
} else {
n.as_f64().map(|f| RE::new_decimal(format!("{f:.4}")))
}
}
serde_json::Value::String(s) => Some(RE::new_string(s.clone())),
serde_json::Value::Array(xs) => {
let elems: Option<Vec<RE>> = xs.iter().map(json_to_cedar_expr).collect();
elems.map(RE::new_set)
}
serde_json::Value::Object(map) => {
let pairs: Option<Vec<(String, RE)>> = map
.iter()
.map(|(k, v)| json_to_cedar_expr(v).map(|expr| (k.clone(), expr)))
.collect();
pairs.and_then(|p| RE::new_record(p).ok())
}
serde_json::Value::Null => None,
}
}