use cedar_policy::{Authorizer, Decision, Entities, PolicySet, Request, Response, Schema, Validator, ValidationMode};
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use super::config::CedarConfig;
use super::error::{CedarError, CedarResult};
use super::schema;
#[derive(Debug, Clone)]
pub struct CedarResponse {
allowed: bool,
matched_policies: Vec<String>,
has_errors: bool,
errors: Vec<String>,
}
impl CedarResponse {
pub fn allowed(&self) -> bool {
self.allowed
}
pub fn matched_policies(&self) -> &[String] {
&self.matched_policies
}
pub fn has_errors(&self) -> bool {
self.has_errors
}
pub fn errors(&self) -> &[String] {
&self.errors
}
}
impl From<Response> for CedarResponse {
fn from(response: Response) -> Self {
let allowed = response.decision() == Decision::Allow;
let matched_policies = response
.diagnostics()
.reason()
.map(|id| id.to_string())
.collect();
let errors: Vec<String> = response
.diagnostics()
.errors()
.map(|e| e.to_string())
.collect();
let has_errors = !errors.is_empty();
CedarResponse {
allowed,
matched_policies,
has_errors,
errors,
}
}
}
pub struct CedarAuthorizer {
authorizer: Authorizer,
policies: Arc<PolicySet>,
entities: Arc<Entities>,
}
impl CedarAuthorizer {
pub fn new(config: CedarConfig) -> CedarResult<Self> {
let schema = Self::load_schema(&config)?;
let policies = Self::load_policies(&config, schema.as_ref())?;
let entities = Entities::empty();
Ok(Self {
authorizer: Authorizer::new(),
policies: Arc::new(policies),
entities: Arc::new(entities),
})
}
pub fn with_entities(config: CedarConfig, entities: Entities) -> CedarResult<Self> {
let schema = Self::load_schema(&config)?;
let policies = Self::load_policies(&config, schema.as_ref())?;
Ok(Self {
authorizer: Authorizer::new(),
policies: Arc::new(policies),
entities: Arc::new(entities),
})
}
pub fn from_policy_str(policy_str: &str) -> CedarResult<Self> {
let policies: PolicySet = policy_str
.parse()
.map_err(|e| CedarError::PolicyLoad(format!("Failed to parse policy: {}", e)))?;
Ok(Self {
authorizer: Authorizer::new(),
policies: Arc::new(policies),
entities: Arc::new(Entities::empty()),
})
}
pub fn is_allowed(&self, request: &Request) -> CedarResponse {
let response = self
.authorizer
.is_authorized(request, &self.policies, &self.entities);
CedarResponse::from(response)
}
pub fn is_allowed_with_entities(
&self,
request: &Request,
entities: &Entities,
) -> CedarResponse {
let response = self
.authorizer
.is_authorized(request, &self.policies, entities);
CedarResponse::from(response)
}
pub fn reload_policies(&mut self, config: &CedarConfig) -> CedarResult<()> {
let schema = Self::load_schema(config)?;
let policies = Self::load_policies(config, schema.as_ref())?;
self.policies = Arc::new(policies);
Ok(())
}
pub fn policies(&self) -> &PolicySet {
&self.policies
}
pub fn entities(&self) -> &Entities {
&self.entities
}
fn load_schema(config: &CedarConfig) -> CedarResult<Option<Schema>> {
if let Some(ref schema_path) = config.schema_path {
let schema = schema::load_schema(schema_path)?;
Ok(Some(schema))
} else {
Ok(None)
}
}
fn load_policies(config: &CedarConfig, schema: Option<&Schema>) -> CedarResult<PolicySet> {
let mut policy_set = PolicySet::new();
let policy_path = &config.policy_path;
let path = Path::new(policy_path);
if path.is_dir() {
Self::load_policies_from_dir(&mut policy_set, path)?;
} else if path.exists() {
let content = std::fs::read_to_string(path).map_err(|e| {
CedarError::PolicyLoad(format!("Failed to read policy file {:?}: {}", path, e))
})?;
let file_stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let source = PolicySet::from_str(&content).map_err(|e| {
CedarError::PolicyLoad(format!("Failed to parse policy file {:?}: {}", path, e))
})?;
for policy in source.policies() {
let new_id = cedar_policy::PolicyId::new(format!(
"file_{}_{}",
file_stem,
policy.id().to_string().trim_start_matches("policy")
));
let text = policy.to_string();
let reparsed = cedar_policy::Policy::parse(Some(new_id), &text).map_err(|e| {
CedarError::PolicyLoad(format!("Failed to re-parse policy from {:?}: {}", path, e))
})?;
policy_set.add(reparsed).map_err(|e| {
CedarError::PolicyLoad(format!("Failed to add policy from {:?}: {}", path, e))
})?;
}
} else {
return Err(CedarError::PolicyLoad(format!(
"Policy path does not exist: {:?}",
path
)));
}
if let (Some(schema), true) = (schema, config.validate_on_load) {
let validator = Validator::new(schema.clone());
let validation = validator.validate(&policy_set, ValidationMode::default());
if !validation.validation_passed() {
let errors: Vec<String> = validation
.validation_errors()
.map(|e| e.to_string())
.collect();
return Err(CedarError::Validation(format!(
"Policy validation failed:\n{}",
errors.join("\n")
)));
}
}
Ok(policy_set)
}
fn load_policies_from_dir(policy_set: &mut PolicySet, dir: &Path) -> CedarResult<()> {
let entries = std::fs::read_dir(dir)
.map_err(|e| CedarError::PolicyLoad(format!("Failed to read policy directory: {}", e)))?;
let mut count = 0usize;
for entry in entries {
let entry = entry
.map_err(|e| CedarError::PolicyLoad(format!("Failed to read directory entry: {}", e)))?;
let path = entry.path();
if path.is_dir() {
Self::load_policies_from_dir(policy_set, &path)?;
} else if path.extension().is_some_and(|ext| ext == "cedar") {
let content = std::fs::read_to_string(&path)
.map_err(|e| CedarError::PolicyLoad(format!("Failed to read policy file {:?}: {}", path, e)))?;
let file_policy_set: PolicySet = content.parse().map_err(|e| {
CedarError::PolicyLoad(format!(
"Failed to parse policy file {:?}: {}",
path, e
))
})?;
for policy in file_policy_set.policies() {
let new_id = cedar_policy::PolicyId::new(format!(
"file_{}_{}",
path.file_stem().and_then(|s| s.to_str()).unwrap_or("unknown"),
count
));
let re_parsed = cedar_policy::Policy::parse(Some(new_id), policy.to_string().as_str())
.map_err(|e| CedarError::PolicyLoad(format!(
"Failed to re-parse policy {} from {:?}: {}",
policy.id(), path, e
)))?;
policy_set
.add(re_parsed)
.map_err(|e| CedarError::PolicyLoad(format!("Failed to add policy from {:?}: {}", path, e)))?;
count += 1;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use cedar_policy::{Context, EntityUid};
fn parse_uid(s: &str) -> EntityUid {
s.parse().unwrap()
}
#[test]
fn test_basic_permit() {
let policy = r#"
permit(
principal == User::"alice",
action == Action::"view",
resource == File::"doc1"
);
"#;
let authorizer = CedarAuthorizer::from_policy_str(policy).unwrap();
let request = Request::new(
parse_uid(r#"User::"alice""#),
parse_uid(r#"Action::"view""#),
parse_uid(r#"File::"doc1""#),
Context::empty(),
None,
)
.unwrap();
let response = authorizer.is_allowed(&request);
assert!(response.allowed());
}
#[test]
fn test_basic_deny() {
let policy = r#"
permit(
principal == User::"alice",
action == Action::"view",
resource == File::"doc1"
);
"#;
let authorizer = CedarAuthorizer::from_policy_str(policy).unwrap();
let request = Request::new(
parse_uid(r#"User::"bob""#),
parse_uid(r#"Action::"view""#),
parse_uid(r#"File::"doc1""#),
Context::empty(),
None,
)
.unwrap();
let response = authorizer.is_allowed(&request);
assert!(!response.allowed());
}
#[test]
fn test_forbid_overrides_permit() {
let policy = r#"
permit(
principal,
action == Action::"view",
resource
);
forbid(
principal == User::"bob",
action == Action::"view",
resource
);
"#;
let authorizer = CedarAuthorizer::from_policy_str(policy).unwrap();
let request = Request::new(
parse_uid(r#"User::"alice""#),
parse_uid(r#"Action::"view""#),
parse_uid(r#"File::"doc1""#),
Context::empty(),
None,
)
.unwrap();
assert!(authorizer.is_allowed(&request).allowed());
let request = Request::new(
parse_uid(r#"User::"bob""#),
parse_uid(r#"Action::"view""#),
parse_uid(r#"File::"doc1""#),
Context::empty(),
None,
)
.unwrap();
assert!(!authorizer.is_allowed(&request).allowed());
}
#[test]
fn test_role_based_policy() {
let policy = r#"
permit(
principal in Group::"Admins",
action == Action::"delete",
resource == Project::"*"
);
"#;
let authorizer = CedarAuthorizer::from_policy_str(policy).unwrap();
let request = Request::new(
parse_uid(r#"User::"alice""#),
parse_uid(r#"Action::"delete""#),
parse_uid(r#"Project::"my-project""#),
Context::empty(),
None,
)
.unwrap();
let response = authorizer.is_allowed(&request);
assert!(!response.allowed());
}
#[test]
fn test_context_based_policy() {
let policy = r#"
permit(
principal,
action == Action::"view",
resource
) when {
context.status == "active"
};
"#;
let authorizer = CedarAuthorizer::from_policy_str(policy).unwrap();
let context_json = serde_json::json!({"status": "active"});
let context = Context::from_json_value(context_json, None).unwrap();
let request = Request::new(
parse_uid(r#"User::"alice""#),
parse_uid(r#"Action::"view""#),
parse_uid(r#"Task::"task-1""#),
context,
None,
)
.unwrap();
let response = authorizer.is_allowed(&request);
assert!(response.allowed());
}
#[test]
fn test_invalid_policy_fails() {
let result = CedarAuthorizer::from_policy_str("invalid policy syntax!!!");
assert!(result.is_err());
}
#[test]
fn test_response_properties() {
let policy = r#"
permit(
principal == User::"alice",
action == Action::"view",
resource
);
"#;
let authorizer = CedarAuthorizer::from_policy_str(policy).unwrap();
let request = Request::new(
parse_uid(r#"User::"alice""#),
parse_uid(r#"Action::"view""#),
parse_uid(r#"File::"doc1""#),
Context::empty(),
None,
)
.unwrap();
let response = authorizer.is_allowed(&request);
assert!(response.allowed());
assert!(!response.has_errors());
}
}