use axum::http::StatusCode;
use cedar_policy::{Context, Entities, Entity, Request, RestrictedExpression};
use pep::cedar::CedarAuthorizer;
use pep::oidc::types::JwtClaims;
use crate::auth::AuthenticatedUser;
pub fn extract_claims_for_cedar(user: &AuthenticatedUser) -> JwtClaims {
user.to_cedar_claims()
}
pub const ADMIN_GROUP_METADATA_KEY: &str = "admin_group";
pub fn check_permission(
authorizer: &CedarAuthorizer,
claims: &JwtClaims,
action: &str,
entity_type: &str,
entity_id: &str,
) -> Result<(), StatusCode> {
let principal = pep::cedar::build_principal_uid(claims).map_err(|e| {
tracing::error!("Cedar principal build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let action_uid = pep::cedar::build_action_uid(action).map_err(|e| {
tracing::error!("Cedar action build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let resource = pep::cedar::entity::ResourceInfo::new(entity_type, entity_id)
.to_cedar_uid()
.map_err(|e| {
tracing::error!("Cedar resource build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let principal_entity = crate::cedar::entity::user_to_cedar_principal(claims);
let entities = Entities::from_entities([principal_entity], None).map_err(|e| {
tracing::error!("Cedar entities build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let request =
Request::new(principal, action_uid, resource, Context::empty(), None).map_err(|e| {
tracing::error!("Cedar request build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
tracing::debug!(
"Cedar eval: action={}, resource={}:{}",
action,
entity_type,
entity_id
);
let response = authorizer.is_allowed_with_entities(&request, &entities);
if response.has_errors() {
tracing::error!(
"Cedar evaluation ERRORS for {} {} on {}:{} for user {}: {:?}",
action,
entity_type,
entity_type,
entity_id,
claims.sub,
response.errors()
);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
if response.allowed() {
Ok(())
} else {
tracing::warn!(
"Cedar denied {} {} on {}:{} for user {}",
action,
entity_type,
entity_type,
entity_id,
claims.sub
);
Err(StatusCode::FORBIDDEN)
}
}
pub fn is_allowed(
authorizer: &CedarAuthorizer,
claims: &JwtClaims,
action: &str,
entity_type: &str,
entity_id: &str,
) -> bool {
check_permission(authorizer, claims, action, entity_type, entity_id).is_ok()
}
pub fn check_permission_scoped(
authorizer: &CedarAuthorizer,
claims: &JwtClaims,
action: &str,
entity_type: &str,
entity_id: &str,
resource_attrs: &[(String, String)],
) -> Result<(), StatusCode> {
let principal = pep::cedar::build_principal_uid(claims).map_err(|e| {
tracing::error!("Cedar principal build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let action_uid = pep::cedar::build_action_uid(action).map_err(|e| {
tracing::error!("Cedar action build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let resource_uid = pep::cedar::entity::ResourceInfo::new(entity_type, entity_id)
.to_cedar_uid()
.map_err(|e| {
tracing::error!("Cedar resource build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let principal_entity = crate::cedar::entity::user_to_cedar_principal(claims);
let attrs: std::collections::HashMap<String, RestrictedExpression> = resource_attrs
.iter()
.cloned()
.map(|(k, v)| (k, RestrictedExpression::new_string(v)))
.collect();
let resource_entity = Entity::new(
resource_uid.clone(),
attrs,
std::collections::HashSet::new(),
)
.map_err(|e| {
tracing::error!("Cedar resource entity build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let entities =
Entities::from_entities([principal_entity, resource_entity], None).map_err(|e| {
tracing::error!("Cedar entities build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let request = Request::new(principal, action_uid, resource_uid, Context::empty(), None)
.map_err(|e| {
tracing::error!("Cedar request build failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let response = authorizer.is_allowed_with_entities(&request, &entities);
if response.has_errors() {
tracing::error!(
"Cedar evaluation ERRORS for {} on {}:{} for user {}: {:?}",
action,
entity_type,
entity_id,
claims.sub,
response.errors()
);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
if response.allowed() {
Ok(())
} else {
tracing::warn!(
"Cedar denied {} on {}:{} for user {} (scoped: {} attrs)",
action,
entity_type,
entity_id,
claims.sub,
resource_attrs.len()
);
Err(StatusCode::FORBIDDEN)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::AuthenticatedUser;
fn test_cedar_config() -> pep::cedar::CedarConfig {
use pep::cedar::config::DefaultDecision;
pep::cedar::CedarConfig {
policy_path: std::path::PathBuf::from("/nonexistent/fame-test-policies"),
schema_path: None,
entities_path: None,
default_decision: DefaultDecision::Deny,
validate_on_load: true,
policy_store_url: None,
policy_store_token: None,
embedded_policy: Some(include_str!("../../policies/rbac.cedar")),
embedded_schema: Some(include_str!("../../policies/schema.cedarschema")),
}
}
fn claims_with_role(role: Option<&str>) -> JwtClaims {
let user = AuthenticatedUser {
user_id: format!("user-{}", role.unwrap_or("norole")),
username: Some("tester".to_string()),
email: Some("tester@example.com".to_string()),
claims_extra: role.map(|r| {
let mut extra = std::collections::HashMap::new();
extra.insert("role".to_string(), serde_json::Value::String(r.to_string()));
extra
}),
};
user.to_cedar_claims()
}
#[test]
fn cedar_enforces_admin_vs_viewer() {
let authorizer = pep::cedar::CedarAuthorizer::new(test_cedar_config())
.expect("Cedar must initialize from embedded policy/schema");
let resource_type = crate::entity_config::slug_to_cedar_type("semantic-memory");
let admin = claims_with_role(Some("admin"));
assert_eq!(
check_permission(
&authorizer,
&admin,
"CreateSemanticMemory",
&resource_type,
"<_>"
),
Ok(())
);
let viewer = claims_with_role(Some("viewer"));
assert_eq!(
check_permission(
&authorizer,
&viewer,
"CreateSemanticMemory",
&resource_type,
"<_>"
),
Err(StatusCode::FORBIDDEN)
);
let norole = claims_with_role(None);
assert_eq!(
check_permission(
&authorizer,
&norole,
"CreateSemanticMemory",
&resource_type,
"<_>"
),
Err(StatusCode::FORBIDDEN)
);
}
#[test]
fn cedar_scopes_agent_role() {
let authorizer = pep::cedar::CedarAuthorizer::new(test_cedar_config())
.expect("Cedar must initialize from embedded policy/schema");
let agent = claims_with_role(Some("agent"));
assert_eq!(
check_permission(
&authorizer,
&agent,
"CreateEpisodicMemory",
&crate::entity_config::slug_to_cedar_type("episodic-memory"),
"<_>"
),
Ok(())
);
assert_eq!(
check_permission(
&authorizer,
&agent,
"DeleteSemanticMemory",
&crate::entity_config::slug_to_cedar_type("semantic-memory"),
"<_>"
),
Err(StatusCode::FORBIDDEN)
);
assert_eq!(
check_permission(
&authorizer,
&agent,
"CreateAgentIdentity",
&crate::entity_config::slug_to_cedar_type("agent-identity"),
"<_>"
),
Ok(())
);
}
#[test]
fn cedar_user_role_identity_permits() {
let authorizer = pep::cedar::CedarAuthorizer::new(test_cedar_config())
.expect("Cedar must initialize from embedded policy/schema");
let user = claims_with_role(Some("user"));
let identity = crate::entity_config::slug_to_cedar_type("agent-identity");
let ws_group = "ws-demo-admins".to_string();
let ws_member = claims_with_role_and_groups("user", &[&ws_group]);
assert_eq!(
check_permission_scoped(
&authorizer,
&ws_member,
"CreateAgentIdentity",
&identity,
"<_>",
&[(ADMIN_GROUP_METADATA_KEY.to_string(), ws_group.clone())]
),
Ok(())
);
assert_eq!(
check_permission(
&authorizer,
&user,
"ViewAgentIdentity",
&identity,
"some-agent-id"
),
Ok(())
);
let memory = crate::entity_config::slug_to_cedar_type("semantic-memory");
assert_eq!(
check_permission(
&authorizer,
&user,
"ViewSemanticMemory",
&memory,
"some-memory-id"
),
Ok(())
);
assert_eq!(
check_permission(&authorizer, &user, "CreateSemanticMemory", &memory, "<_>"),
Err(StatusCode::FORBIDDEN)
);
assert_eq!(
check_permission(
&authorizer,
&user,
"DeleteAgentIdentity",
&identity,
"some-agent-id"
),
Err(StatusCode::FORBIDDEN)
);
}
fn claims_with_role_and_groups(role: &str, groups: &[&str]) -> JwtClaims {
let user = AuthenticatedUser {
user_id: format!("user-{}", role),
username: Some("tester".to_string()),
email: Some("tester@example.com".to_string()),
claims_extra: Some({
let mut extra = std::collections::HashMap::new();
extra.insert(
"role".to_string(),
serde_json::Value::String(role.to_string()),
);
extra.insert(
"groups".to_string(),
serde_json::Value::Array(
groups
.iter()
.map(|g| serde_json::Value::String(g.to_string()))
.collect(),
),
);
extra
}),
};
user.to_cedar_claims()
}
#[test]
fn cedar_workspace_scoped_identity() {
let authorizer = pep::cedar::CedarAuthorizer::new(test_cedar_config())
.expect("Cedar must initialize from embedded policy/schema");
let identity = crate::entity_config::slug_to_cedar_type("agent-identity");
let group = "ws-4b31ec2d-3c10-4b5d-90b5-9e92ccc77cef-admins".to_string();
let scoped = vec![(ADMIN_GROUP_METADATA_KEY.to_string(), group.clone())];
let member = claims_with_role_and_groups("user", &[&group]);
assert_eq!(
check_permission_scoped(
&authorizer,
&member,
"CreateAgentIdentity",
&identity,
"<_>",
&scoped
),
Ok(())
);
assert_eq!(
check_permission_scoped(
&authorizer,
&member,
"EditAgentIdentity",
&identity,
"agent-abc",
&scoped
),
Ok(())
);
let outsider = claims_with_role_and_groups("user", &["ws-other-admins"]);
assert_eq!(
check_permission_scoped(
&authorizer,
&outsider,
"EditAgentIdentity",
&identity,
"agent-abc",
&scoped
),
Err(StatusCode::FORBIDDEN)
);
let groupless = claims_with_role(Some("user"));
assert_eq!(
check_permission_scoped(
&authorizer,
&groupless,
"CreateAgentIdentity",
&identity,
"<_>",
&scoped
),
Err(StatusCode::FORBIDDEN)
);
let admin = claims_with_role(Some("admin"));
assert_eq!(
check_permission_scoped(
&authorizer,
&groupless,
"CreateAgentIdentity",
&identity,
"<_>",
&[]
),
Err(StatusCode::FORBIDDEN)
);
assert_eq!(
check_permission_scoped(
&authorizer,
&admin,
"CreateAgentIdentity",
&identity,
"<_>",
&[]
),
Ok(())
);
}
}