use std::str::FromStr;
use cedar_policy::{Authorizer, Context, Entities, EntityUid, PolicySet, Request};
use crate::core::{Digest, PolicyDecision, PolicyEngine, PolicyRequest};
use crate::runtime::telemetry;
#[derive(Debug)]
pub struct CedarEngine {
policies: PolicySet,
entities: Entities,
digest: Digest,
}
#[derive(Debug, thiserror::Error)]
pub enum CedarError {
#[error("policy set does not parse: {0}")]
Parse(String),
}
impl CedarEngine {
pub fn new(source: &str) -> Result<Self, CedarError> {
let policies = PolicySet::from_str(source).map_err(|e| CedarError::Parse(e.to_string()))?;
let entities = Entities::empty();
let mut framed = Vec::with_capacity(source.len() + 24);
framed.extend_from_slice(b"agentplane.cedar.v1\0");
framed.extend_from_slice(source.as_bytes());
let digest = Digest::of(&framed);
Ok(Self {
policies,
entities,
digest,
})
}
fn request(r: &PolicyRequest<'_>) -> Result<Request, String> {
let principal = uid("Agent", r.principal)?;
let action = uid("Action", r.action)?;
let resource = uid("Resource", r.resource)?;
let context = Context::from_json_value(r.context.clone(), None)
.map_err(|e| format!("context is not a Cedar record: {e}"))?;
Request::new(principal, action, resource, context, None)
.map_err(|e| format!("request is not well formed: {e}"))
}
}
fn uid(kind: &str, id: &str) -> Result<EntityUid, String> {
let escaped = id.replace('\\', "\\\\").replace('"', "\\\"");
EntityUid::from_str(&format!("{kind}::\"{escaped}\""))
.map_err(|e| format!("'{id}' is not a usable Cedar entity id: {e}"))
}
impl PolicyEngine for CedarEngine {
fn authorize(&self, request: &PolicyRequest<'_>) -> PolicyDecision {
let req = match Self::request(request) {
Ok(r) => r,
Err(why) => {
tracing::error!(
target: telemetry::POLICY_DENIED,
action = %request.action,
resource = %request.resource,
malformed = true,
%why,
);
return PolicyDecision::deny(format!(
"the authorization request could not be expressed for evaluation \
({why}) — this is a defect, not a rule: every request of this \
shape is being denied"
));
}
};
let answer = Authorizer::new().is_authorized(&req, &self.policies, &self.entities);
let errors: Vec<String> = answer
.diagnostics()
.errors()
.map(ToString::to_string)
.collect();
if !errors.is_empty() {
tracing::error!(
target: telemetry::POLICY_DENIED,
action = %request.action,
resource = %request.resource,
policy_error = true,
detail = %errors.join("; "),
);
}
match answer.decision() {
cedar_policy::Decision::Allow if errors.is_empty() => PolicyDecision::Permit,
cedar_policy::Decision::Allow => PolicyDecision::Permit,
cedar_policy::Decision::Deny if !errors.is_empty() => PolicyDecision::deny(format!(
"denied while {} policy error(s) went unevaluated: {} — fix the \
policy set; this denial may not mean what it appears to",
errors.len(),
errors.join("; ")
)),
cedar_policy::Decision::Deny => {
let determining: Vec<String> = answer
.diagnostics()
.reason()
.map(ToString::to_string)
.collect();
if determining.is_empty() {
PolicyDecision::deny(format!(
"no policy permits '{}' on '{}'",
request.action, request.resource
))
} else {
PolicyDecision::deny(format!(
"'{}' on '{}' refused by {}",
request.action,
request.resource,
determining.join(", ")
))
}
}
}
}
fn digest(&self) -> Digest {
self.digest
}
}