#![cfg(feature = "enterprise")]
use anyhow::{Context as _, Result};
use cedar_policy::{
Authorizer, Context, Decision, Entities, Entity, EntityTypeName, EntityUid, PolicySet, Request,
RestrictedExpression, Schema,
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
pub const CEDAR_SCHEMA: &str = r#"
namespace AgentKernel {
entity User = {
email: String,
org_id: String,
roles: Set<String>,
mfa_verified: Bool,
};
entity Sandbox = {
name: String,
agent_type: String,
runtime: String,
};
action Run appliesTo {
principal: [User],
resource: [Sandbox],
};
action Exec appliesTo {
principal: [User],
resource: [Sandbox],
};
action Create appliesTo {
principal: [User],
resource: [Sandbox],
};
action Attach appliesTo {
principal: [User],
resource: [Sandbox],
};
action Mount appliesTo {
principal: [User],
resource: [Sandbox],
};
action Network appliesTo {
principal: [User],
resource: [Sandbox],
};
action PortMap appliesTo {
principal: [User],
resource: [Sandbox],
};
action SSH appliesTo {
principal: [User],
resource: [Sandbox],
};
action UseLlmProvider appliesTo {
principal: [User],
resource: [Sandbox],
};
}
"#;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Action {
Run,
Exec,
Create,
Attach,
Mount,
Network,
PortMap,
SSH,
UseLlmProvider,
}
impl Action {
pub fn cedar_uid(&self) -> String {
match self {
Action::Run => r#"AgentKernel::Action::"Run""#.to_string(),
Action::Exec => r#"AgentKernel::Action::"Exec""#.to_string(),
Action::Create => r#"AgentKernel::Action::"Create""#.to_string(),
Action::Attach => r#"AgentKernel::Action::"Attach""#.to_string(),
Action::Mount => r#"AgentKernel::Action::"Mount""#.to_string(),
Action::Network => r#"AgentKernel::Action::"Network""#.to_string(),
Action::PortMap => r#"AgentKernel::Action::"PortMap""#.to_string(),
Action::SSH => r#"AgentKernel::Action::"SSH""#.to_string(),
Action::UseLlmProvider => r#"AgentKernel::Action::"UseLlmProvider""#.to_string(),
}
}
}
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Action::Run => write!(f, "Run"),
Action::Exec => write!(f, "Exec"),
Action::Create => write!(f, "Create"),
Action::Attach => write!(f, "Attach"),
Action::Mount => write!(f, "Mount"),
Action::Network => write!(f, "Network"),
Action::PortMap => write!(f, "PortMap"),
Action::SSH => write!(f, "SSH"),
Action::UseLlmProvider => write!(f, "UseLlmProvider"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Principal {
pub id: String,
pub email: String,
pub org_id: String,
pub roles: Vec<String>,
pub mfa_verified: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource {
pub name: String,
pub agent_type: String,
pub runtime: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyDecision {
pub decision: PolicyEffect,
pub reason: String,
pub matched_policies: Vec<String>,
pub evaluation_time_us: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PolicyEffect {
Permit,
Deny,
}
impl PolicyDecision {
pub fn permit(reason: impl Into<String>, matched: Vec<String>, time_us: u64) -> Self {
Self {
decision: PolicyEffect::Permit,
reason: reason.into(),
matched_policies: matched,
evaluation_time_us: time_us,
}
}
pub fn deny(reason: impl Into<String>, matched: Vec<String>, time_us: u64) -> Self {
Self {
decision: PolicyEffect::Deny,
reason: reason.into(),
matched_policies: matched,
evaluation_time_us: time_us,
}
}
pub fn is_permit(&self) -> bool {
self.decision == PolicyEffect::Permit
}
}
pub struct CedarEngine {
policy_set: PolicySet,
schema: Schema,
authorizer: Authorizer,
}
impl CedarEngine {
pub fn new(policies_src: &str) -> Result<Self> {
let (schema, _warnings) =
Schema::from_cedarschema_str(CEDAR_SCHEMA).context("Failed to parse Cedar schema")?;
let policy_set: PolicySet = policies_src
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse Cedar policies: {}", e))?;
Ok(Self {
policy_set,
schema,
authorizer: Authorizer::new(),
})
}
pub fn with_schema(policies_src: &str, schema_src: &str) -> Result<Self> {
let (schema, _warnings) =
Schema::from_cedarschema_str(schema_src).context("Failed to parse Cedar schema")?;
let policy_set: PolicySet = policies_src
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse Cedar policies: {}", e))?;
Ok(Self {
policy_set,
schema,
authorizer: Authorizer::new(),
})
}
pub fn update_policies(&mut self, policies_src: &str) -> Result<()> {
let policy_set: PolicySet = policies_src
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse Cedar policies: {}", e))?;
self.policy_set = policy_set;
Ok(())
}
pub fn evaluate(
&self,
principal: &Principal,
action: Action,
resource: &Resource,
extra_context: Option<HashMap<String, String>>,
) -> PolicyDecision {
let start = std::time::Instant::now();
let entities = match self.build_entities(principal, resource) {
Ok(e) => e,
Err(e) => {
let elapsed = start.elapsed().as_micros() as u64;
return PolicyDecision::deny(
format!("Failed to build entities: {}", e),
vec![],
elapsed,
);
}
};
let request = match self.build_request(principal, action, resource, extra_context) {
Ok(r) => r,
Err(e) => {
let elapsed = start.elapsed().as_micros() as u64;
return PolicyDecision::deny(
format!("Failed to build request: {}", e),
vec![],
elapsed,
);
}
};
let response = self
.authorizer
.is_authorized(&request, &self.policy_set, &entities);
let elapsed = start.elapsed().as_micros() as u64;
let matched: Vec<String> = response
.diagnostics()
.reason()
.map(|id| id.to_string())
.collect();
match response.decision() {
Decision::Allow => {
PolicyDecision::permit("Policy evaluation: permit", matched, elapsed)
}
Decision::Deny => {
let errors: Vec<String> = response
.diagnostics()
.errors()
.map(|e| e.to_string())
.collect();
let reason = if errors.is_empty() {
"Policy evaluation: deny (no matching permit or explicit forbid)".to_string()
} else {
format!("Policy evaluation: deny (errors: {})", errors.join("; "))
};
PolicyDecision::deny(reason, matched, elapsed)
}
}
}
pub fn schema(&self) -> &Schema {
&self.schema
}
fn build_entities(&self, principal: &Principal, resource: &Resource) -> Result<Entities> {
let mut entities_vec = Vec::new();
let user_type = EntityTypeName::from_str("AgentKernel::User")
.map_err(|e| anyhow::anyhow!("Invalid entity type: {}", e))?;
let user_uid = EntityUid::from_type_name_and_id(
user_type,
principal
.id
.clone()
.parse()
.map_err(|e| anyhow::anyhow!("Invalid entity id: {}", e))?,
);
let roles_iter = principal
.roles
.iter()
.map(|r| RestrictedExpression::new_string(r.clone()));
let user_attrs: HashMap<String, RestrictedExpression> = [
(
"email".to_string(),
RestrictedExpression::new_string(principal.email.clone()),
),
(
"org_id".to_string(),
RestrictedExpression::new_string(principal.org_id.clone()),
),
(
"roles".to_string(),
RestrictedExpression::new_set(roles_iter),
),
(
"mfa_verified".to_string(),
RestrictedExpression::new_bool(principal.mfa_verified),
),
]
.into_iter()
.collect();
let user_entity = Entity::new(user_uid.clone(), user_attrs, HashSet::new())
.map_err(|e| anyhow::anyhow!("Failed to create User entity: {}", e))?;
entities_vec.push(user_entity);
let sandbox_type = EntityTypeName::from_str("AgentKernel::Sandbox")
.map_err(|e| anyhow::anyhow!("Invalid entity type: {}", e))?;
let sandbox_uid = EntityUid::from_type_name_and_id(
sandbox_type,
resource
.name
.clone()
.parse()
.map_err(|e| anyhow::anyhow!("Invalid entity id: {}", e))?,
);
let sandbox_attrs: HashMap<String, RestrictedExpression> = [
(
"name".to_string(),
RestrictedExpression::new_string(resource.name.clone()),
),
(
"agent_type".to_string(),
RestrictedExpression::new_string(resource.agent_type.clone()),
),
(
"runtime".to_string(),
RestrictedExpression::new_string(resource.runtime.clone()),
),
]
.into_iter()
.collect();
let sandbox_entity = Entity::new(sandbox_uid, sandbox_attrs, HashSet::new())
.map_err(|e| anyhow::anyhow!("Failed to create Sandbox entity: {}", e))?;
entities_vec.push(sandbox_entity);
Entities::from_entities(entities_vec, Some(&self.schema))
.context("Failed to build entity set")
}
fn build_request(
&self,
principal: &Principal,
action: Action,
resource: &Resource,
extra_context: Option<HashMap<String, String>>,
) -> Result<Request> {
let principal_uid: EntityUid = format!(r#"AgentKernel::User::"{}""#, principal.id)
.parse()
.map_err(|e| anyhow::anyhow!("Invalid principal UID: {}", e))?;
let action_uid: EntityUid = action
.cedar_uid()
.parse()
.map_err(|e| anyhow::anyhow!("Invalid action UID: {}", e))?;
let resource_uid: EntityUid = format!(r#"AgentKernel::Sandbox::"{}""#, resource.name)
.parse()
.map_err(|e| anyhow::anyhow!("Invalid resource UID: {}", e))?;
let context = if let Some(ctx_map) = extra_context {
Context::from_pairs(
ctx_map
.into_iter()
.map(|(k, v)| (k, RestrictedExpression::new_string(v))),
)
.context("Failed to build context")?
} else {
Context::empty()
};
Request::new(
principal_uid,
action_uid,
resource_uid,
context,
Some(&self.schema),
)
.map_err(|e| anyhow::anyhow!("Failed to create request: {}", e))
}
}
pub fn validate_cedar_syntax(src: &str) -> Result<()> {
let _: PolicySet = src
.parse()
.map_err(|e| anyhow::anyhow!("Invalid Cedar syntax: {}", e))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn test_principal() -> Principal {
Principal {
id: "alice".to_string(),
email: "alice@acme.com".to_string(),
org_id: "acme-corp".to_string(),
roles: vec!["developer".to_string()],
mfa_verified: true,
}
}
fn test_resource() -> Resource {
Resource {
name: "my-sandbox".to_string(),
agent_type: "claude".to_string(),
runtime: "python".to_string(),
}
}
#[test]
fn test_permit_policy() {
let policies = r#"
permit(
principal is AgentKernel::User,
action == AgentKernel::Action::"Run",
resource is AgentKernel::Sandbox
);
"#;
let engine = CedarEngine::new(policies).unwrap();
let decision = engine.evaluate(&test_principal(), Action::Run, &test_resource(), None);
assert!(decision.is_permit());
assert_eq!(decision.decision, PolicyEffect::Permit);
}
#[test]
fn test_deny_no_matching_policy() {
let policies = r#"
permit(
principal is AgentKernel::User,
action == AgentKernel::Action::"Run",
resource is AgentKernel::Sandbox
);
"#;
let engine = CedarEngine::new(policies).unwrap();
let decision = engine.evaluate(&test_principal(), Action::Exec, &test_resource(), None);
assert!(!decision.is_permit());
assert_eq!(decision.decision, PolicyEffect::Deny);
}
#[test]
fn test_explicit_forbid() {
let policies = r#"
permit(
principal is AgentKernel::User,
action == AgentKernel::Action::"Network",
resource is AgentKernel::Sandbox
);
forbid(
principal is AgentKernel::User,
action == AgentKernel::Action::"Network",
resource is AgentKernel::Sandbox
) when {
!principal.mfa_verified
};
"#;
let engine = CedarEngine::new(policies).unwrap();
let decision = engine.evaluate(&test_principal(), Action::Network, &test_resource(), None);
assert!(decision.is_permit());
let mut no_mfa = test_principal();
no_mfa.mfa_verified = false;
let decision = engine.evaluate(&no_mfa, Action::Network, &test_resource(), None);
assert!(!decision.is_permit());
}
#[test]
fn test_role_based_policy() {
let policies = r#"
permit(
principal is AgentKernel::User,
action == AgentKernel::Action::"Create",
resource is AgentKernel::Sandbox
) when {
principal.roles.contains("developer")
};
"#;
let engine = CedarEngine::new(policies).unwrap();
let decision = engine.evaluate(&test_principal(), Action::Create, &test_resource(), None);
assert!(decision.is_permit());
let mut viewer = test_principal();
viewer.roles = vec!["viewer".to_string()];
let decision = engine.evaluate(&viewer, Action::Create, &test_resource(), None);
assert!(!decision.is_permit());
}
#[test]
fn test_update_policies() {
let initial = r#"
permit(
principal is AgentKernel::User,
action == AgentKernel::Action::"Run",
resource is AgentKernel::Sandbox
);
"#;
let mut engine = CedarEngine::new(initial).unwrap();
let decision = engine.evaluate(&test_principal(), Action::Run, &test_resource(), None);
assert!(decision.is_permit());
let updated = r#"
permit(
principal is AgentKernel::User,
action == AgentKernel::Action::"Create",
resource is AgentKernel::Sandbox
);
"#;
engine.update_policies(updated).unwrap();
let decision = engine.evaluate(&test_principal(), Action::Run, &test_resource(), None);
assert!(!decision.is_permit());
let decision = engine.evaluate(&test_principal(), Action::Create, &test_resource(), None);
assert!(decision.is_permit());
}
#[test]
fn test_action_display() {
assert_eq!(Action::Run.to_string(), "Run");
assert_eq!(Action::Exec.to_string(), "Exec");
assert_eq!(Action::Create.to_string(), "Create");
assert_eq!(Action::Attach.to_string(), "Attach");
assert_eq!(Action::Mount.to_string(), "Mount");
assert_eq!(Action::Network.to_string(), "Network");
assert_eq!(Action::PortMap.to_string(), "PortMap");
}
#[test]
fn test_policy_decision_helpers() {
let permit = PolicyDecision::permit("ok", vec!["policy0".to_string()], 100);
assert!(permit.is_permit());
assert_eq!(permit.matched_policies, vec!["policy0"]);
assert_eq!(permit.evaluation_time_us, 100);
let deny = PolicyDecision::deny("nope", vec![], 50);
assert!(!deny.is_permit());
}
#[test]
fn test_empty_policies() {
let engine = CedarEngine::new("").unwrap();
let decision = engine.evaluate(&test_principal(), Action::Run, &test_resource(), None);
assert!(!decision.is_permit());
}
#[test]
fn test_ssh_action_evaluation() {
let policies = r#"
permit(
principal is AgentKernel::User,
action == AgentKernel::Action::"SSH",
resource is AgentKernel::Sandbox
) when {
principal.roles.contains("developer")
};
"#;
let engine = CedarEngine::new(policies).unwrap();
let decision = engine.evaluate(&test_principal(), Action::SSH, &test_resource(), None);
assert!(decision.is_permit());
let mut viewer = test_principal();
viewer.roles = vec!["viewer".to_string()];
let decision = engine.evaluate(&viewer, Action::SSH, &test_resource(), None);
assert!(!decision.is_permit());
}
#[test]
fn test_forbid_ssh_action() {
let policies = r#"
permit(
principal is AgentKernel::User,
action,
resource is AgentKernel::Sandbox
);
forbid(
principal is AgentKernel::User,
action == AgentKernel::Action::"SSH",
resource is AgentKernel::Sandbox
);
"#;
let engine = CedarEngine::new(policies).unwrap();
let decision = engine.evaluate(&test_principal(), Action::SSH, &test_resource(), None);
assert!(!decision.is_permit());
let decision = engine.evaluate(&test_principal(), Action::Run, &test_resource(), None);
assert!(decision.is_permit());
}
#[test]
fn test_ssh_action_display() {
assert_eq!(Action::SSH.to_string(), "SSH");
assert_eq!(Action::SSH.cedar_uid(), r#"AgentKernel::Action::"SSH""#);
}
}