use cedar_policy::{
Authorizer, Context, Decision, Entities, EntityUid, PolicySet, Request, Schema,
};
use std::str::FromStr;
use tracing::warn;
use super::error::AuthzError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthzDecision {
Allow,
Deny,
}
pub trait PolicyEvaluator: Send + Sync {
fn is_authorized(
&self,
entities: &Entities,
principal: EntityUid,
action: EntityUid,
resource: EntityUid,
context: Context,
) -> AuthzDecision;
fn schema(&self) -> Option<&Schema> {
None
}
}
pub struct PolicyStore {
policy_set: PolicySet,
schema: Schema,
authorizer: Authorizer,
}
impl PolicyStore {
pub fn from_text(policy_text: &str, schema_json: &str) -> Result<Self, AuthzError> {
let policy_set = PolicySet::from_str(policy_text)
.map_err(|e| AuthzError::PolicyParse(format!("{e:?}")))?;
let schema = Schema::from_json_str(schema_json)
.map_err(|e| AuthzError::SchemaParse(format!("{e:?}")))?;
Ok(Self {
policy_set,
schema,
authorizer: Authorizer::new(),
})
}
pub fn schema(&self) -> &Schema {
&self.schema
}
}
impl PolicyEvaluator for PolicyStore {
fn is_authorized(
&self,
entities: &Entities,
principal: EntityUid,
action: EntityUid,
resource: EntityUid,
context: Context,
) -> AuthzDecision {
let principal_str = principal.to_string();
let action_str = action.to_string();
let resource_str = resource.to_string();
let started = std::time::Instant::now();
let cedar_req = match Request::new(principal, action, resource, context, Some(&self.schema))
{
Ok(r) => r,
Err(e) => {
warn!("Cedar request validation failed: {e:?}");
tracing::info!(
target: "axess::authz::decision",
principal = %principal_str,
action = %action_str,
resource = %resource_str,
decision = "deny",
reason = "request_validation_failed",
latency_us = started.elapsed().as_micros() as u64,
);
return AuthzDecision::Deny;
}
};
let response = self
.authorizer
.is_authorized(&cedar_req, &self.policy_set, entities);
let diagnostics = response.diagnostics();
let reasons: Vec<String> = diagnostics.reason().map(|p| p.to_string()).collect();
let errors: Vec<String> = diagnostics.errors().map(|e| e.to_string()).collect();
if !errors.is_empty() {
tracing::warn!(
errors = ?errors,
"Cedar policy evaluation errors"
);
}
let latency_us = started.elapsed().as_micros() as u64;
match response.decision() {
Decision::Allow => {
tracing::trace!(reasons = ?reasons, "Cedar: allowed");
tracing::info!(
target: "axess::authz::decision",
principal = %principal_str,
action = %action_str,
resource = %resource_str,
decision = "allow",
reasons = ?reasons,
latency_us = latency_us,
);
AuthzDecision::Allow
}
Decision::Deny => {
tracing::debug!(reasons = ?reasons, "Cedar: denied");
tracing::info!(
target: "axess::authz::decision",
principal = %principal_str,
action = %action_str,
resource = %resource_str,
decision = "deny",
reasons = ?reasons,
latency_us = latency_us,
);
AuthzDecision::Deny
}
}
}
fn schema(&self) -> Option<&Schema> {
Some(&self.schema)
}
}
pub(super) fn make_uid(
namespace: &str,
type_name: &str,
id: &str,
) -> Result<EntityUid, AuthzError> {
EntityUid::from_str(&format!(r#"{namespace}::{type_name}::"{id}""#))
.map_err(|e| AuthzError::InvalidEntityUid(format!("{type_name}/{id}: {e:?}")))
}
#[cfg(test)]
mod authz_store_tests {
use super::*;
const SCHEMA_JSON: &str = r#"{
"TestApp": {
"entityTypes": {
"User": { "memberOfTypes": [] },
"Resource": { "memberOfTypes": [] }
},
"actions": {
"View": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Resource"]
}
}
}
}
}"#;
const POLICY_TEXT: &str = r#"permit(
principal == TestApp::User::"alice",
action == TestApp::Action::"View",
resource == TestApp::Resource::"doc1"
);"#;
fn build_store() -> PolicyStore {
PolicyStore::from_text(POLICY_TEXT, SCHEMA_JSON).unwrap()
}
#[test]
fn policy_evaluator_schema_returns_some_when_loaded() {
let store = build_store();
assert!(
<PolicyStore as PolicyEvaluator>::schema(&store).is_some(),
"PolicyStore::schema must return Some, not None"
);
}
#[test]
fn policy_store_schema_accessor_is_borrow_of_inner() {
let store = build_store();
let s1 = store.schema() as *const _;
let s2 = store.schema() as *const _;
assert_eq!(
s1, s2,
"schema() must borrow the stored Schema, not produce a new one"
);
}
}