use cedar_policy::{
Authorizer, Entities, Entity, Policy, PolicyId, PolicySet, Request as CedarRequest, Schema,
};
use std::collections::{HashMap, HashSet};
use std::marker::PhantomData;
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant, SystemTime};
use std::vec;
use crate::labels::LabelRegistry;
use crate::policy_match::{
action_match_reason, matches_effect, principal_match_reason, resource_match_reason,
};
use crate::policy_store::{
ExplicitPolicyStore, PolicyStoreId, PolicyStoreLayout, display_policy_id,
};
use crate::query::{ActionQuery, PrincipalQuery, ResourceQuery};
use crate::timers::PhaseTimer;
use crate::traits::CedarAtom;
use crate::types::{
Decision, DecisionDiagnostics, PermitPolicies, PermitPolicy, PolicyCandidates,
PolicyEffectFilter, PolicyMatchReason, PolicyVersion, Request, RequestContext, Resource,
};
use crate::{Groups, Principal};
use crate::{error::PolicyError, loader};
use arc_swap::ArcSwap;
use sha2::{Digest, Sha256};
use tracing::debug;
#[cfg(feature = "observability")]
use tracing::info_span;
#[cfg(feature = "observability")]
use crate::metrics::{
EvaluationObservation, EvaluationPhases, MatchedPolicySource, get_sink, metrics_enabled,
record_evaluation_observation, record_reload,
};
fn get_authorizer() -> &'static Authorizer {
static AUTHORIZER: OnceLock<Authorizer> = OnceLock::new();
AUTHORIZER.get_or_init(Authorizer::new)
}
#[derive(Debug)]
struct EvalTimers {
total_start: Option<Instant>,
measure_enabled: bool,
debug_enabled: bool,
labels: Duration,
construct_req: Duration,
entities: Duration,
groups: Duration,
authz: Duration,
}
impl EvalTimers {
fn start(measure_enabled: bool, debug_enabled: bool) -> Self {
Self {
total_start: measure_enabled.then(Instant::now),
measure_enabled,
debug_enabled,
labels: Duration::ZERO,
construct_req: Duration::ZERO,
entities: Duration::ZERO,
groups: Duration::ZERO,
authz: Duration::ZERO,
}
}
fn total_elapsed(&self) -> Duration {
self.total_start
.map_or(Duration::ZERO, |start| start.elapsed())
}
}
struct PreparedRequest {
cedar_req: CedarRequest,
entities: Entities,
timers: EvalTimers,
#[cfg(feature = "observability")]
sink: crate::metrics::SinkGuard,
#[cfg(feature = "observability")]
metrics_enabled: bool,
}
#[derive(Debug)]
enum PolicySets {
Monolithic(Box<PolicySet>),
Scoped {
layout: PolicyStoreLayout,
stores: Vec<PolicySet>,
},
}
enum PolicySetIter<'a> {
Monolithic(std::iter::Once<&'a PolicySet>),
Scoped(std::slice::Iter<'a, PolicySet>),
}
impl<'a> Iterator for PolicySetIter<'a> {
type Item = &'a PolicySet;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Monolithic(iter) => iter.next(),
Self::Scoped(iter) => iter.next(),
}
}
}
impl PolicySets {
fn layout(&self) -> Option<&PolicyStoreLayout> {
match self {
Self::Monolithic(_) => None,
Self::Scoped { layout, .. } => Some(layout),
}
}
fn resolve(&self, request: &Request) -> Result<&PolicySet, PolicyError> {
match self {
Self::Monolithic(set) => Ok(set),
Self::Scoped { layout, stores } => {
let index = layout.resolve_request(&request.action, &request.resource)?;
stores.get(index).ok_or_else(|| {
PolicyError::PolicyStoreRoutingError(format!(
"configured policy-store index {index} is outside layout length {}",
layout.stores().len()
))
})
}
}
}
fn iter(&self) -> PolicySetIter<'_> {
match self {
Self::Monolithic(set) => PolicySetIter::Monolithic(std::iter::once(set)),
Self::Scoped { stores, .. } => PolicySetIter::Scoped(stores.iter()),
}
}
fn store_ids(&self) -> Option<Vec<PolicyStoreId>> {
let Self::Scoped { layout, .. } = self else {
return None;
};
Some(
layout
.stores()
.iter()
.map(|store| store.id().clone())
.collect(),
)
}
}
#[derive(Debug)]
struct PolicySnapshot {
sets: PolicySets,
revision: PolicyRevision,
permit_policies: HashMap<PolicyId, PermitPolicy>,
forbid_policy_ids: HashMap<PolicyId, String>,
schema: Option<Arc<Schema>>,
}
#[derive(Debug)]
struct PolicyRevision {
hash: Arc<str>,
loaded_at: Arc<str>,
}
type Snapshot = Arc<PolicySnapshot>;
struct EngineState {
policy: Snapshot,
label_registry: Option<LabelRegistry>,
generation: u64,
}
type State = Arc<EngineState>;
impl EngineState {
fn version(&self) -> PolicyVersion {
PolicyVersion {
hash: Arc::clone(&self.policy.revision.hash),
loaded_at: Arc::clone(&self.policy.revision.loaded_at),
label_set: self
.label_registry
.as_ref()
.and_then(|registry| registry.version().cloned()),
generation: self.generation,
}
}
}
impl PolicySnapshot {
fn from_policy_text(policy_text: &str) -> Result<Self, PolicyError> {
Self::from_policy_text_with_schema_and_stores(policy_text, None, None)
}
fn from_policy_text_with_schema(
policy_text: &str,
schema: Option<Arc<Schema>>,
) -> Result<Self, PolicyError> {
Self::from_policy_text_with_schema_and_stores(policy_text, schema, None)
}
fn from_policy_text_with_schema_and_stores(
policy_text: &str,
schema: Option<Arc<Schema>>,
layout: Option<PolicyStoreLayout>,
) -> Result<Self, PolicyError> {
let set = match schema.as_deref() {
Some(schema) => loader::compile_policy_with_schema(policy_text, schema)?,
None => loader::compile_policy(policy_text)?,
};
let permit_policies = loader::precompute_permit_policies(&set)?;
let forbid_policy_ids = loader::precompute_forbid_policy_ids(&set);
let sets = match layout {
Some(layout) => partition_policy_set(&set, layout)?,
None => PolicySets::Monolithic(Box::new(set)),
};
let mut hasher = Sha256::new();
hasher.update(policy_text.as_bytes());
let digest = hasher.finalize();
let mut hash = String::with_capacity(digest.len() * 2);
const HEX: &[u8; 16] = b"0123456789abcdef";
for byte in digest {
hash.push(char::from(HEX[usize::from(byte >> 4)]));
hash.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
Ok(PolicySnapshot {
sets,
revision: PolicyRevision {
hash: hash.into(),
loaded_at: humantime::format_rfc3339(SystemTime::now())
.to_string()
.into(),
},
permit_policies,
forbid_policy_ids,
schema,
})
}
fn schema(&self) -> Option<&Schema> {
self.schema.as_deref()
}
}
fn partition_policy_set(
source: &PolicySet,
layout: PolicyStoreLayout,
) -> Result<PolicySets, PolicyError> {
let mut stores = (0..layout.stores().len())
.map(|_| PolicySet::new())
.collect::<Vec<_>>();
let mut found_global_policy_ids = HashSet::new();
for policy in source.policies() {
let display_id = display_policy_id(policy);
let registered_global = policy
.annotation("id")
.is_some_and(|id| layout.global_policy_ids().contains(id));
if registered_global {
found_global_policy_ids.insert(display_id.to_string());
}
let explicit = layout.explicit_policy_store(policy)?;
if registered_global
&& let Some(ExplicitPolicyStore::Store(store_index)) = explicit.as_ref()
{
return Err(PolicyError::PolicyStoreConfigError(format!(
"policy '{display_id}' is registered as global but @{POLICY_STORE_ANNOTATION} assigns it to store '{}'",
layout.stores()[*store_index].id(),
POLICY_STORE_ANNOTATION = crate::POLICY_STORE_ANNOTATION
)));
}
let target_indexes = if registered_global
|| matches!(explicit, Some(ExplicitPolicyStore::Global))
{
(0..layout.stores().len()).collect::<Vec<_>>()
} else {
let candidates = layout.policy_candidates(policy)?;
match explicit {
Some(ExplicitPolicyStore::Store(store_index)) => {
if candidates
.first()
.is_some_and(|candidate| *candidate != store_index)
{
return Err(PolicyError::PolicyStoreConfigError(format!(
"policy '{display_id}' is assigned to store '{}' but its scope identifies store '{}'",
layout.stores()[store_index].id(),
layout.stores()[candidates[0]].id()
)));
}
vec![store_index]
}
None => match candidates.as_slice() {
[store_index] => vec![*store_index],
[] => {
return Err(PolicyError::PolicyStoreConfigError(format!(
"policy '{display_id}' cannot be assigned from its configured namespace references; add @{POLICY_STORE_ANNOTATION}(\"store-id\") or mark it global",
POLICY_STORE_ANNOTATION = crate::POLICY_STORE_ANNOTATION
)));
}
_ => {
return Err(PolicyError::PolicyStoreConfigError(format!(
"policy '{display_id}' has an ambiguous policy-store assignment"
)));
}
},
Some(ExplicitPolicyStore::Global) => {
return Err(PolicyError::PolicyStoreConfigError(format!(
"policy '{display_id}' has an inconsistent global assignment"
)));
}
}
};
for target_index in target_indexes {
let target_id = layout.stores()[target_index].id();
let target = stores.get_mut(target_index).ok_or_else(|| {
PolicyError::PolicyStoreConfigError(format!(
"policy '{display_id}' resolved to missing store '{target_id}'"
))
})?;
target.add(policy.clone()).map_err(|error| {
PolicyError::PolicyStoreConfigError(format!(
"failed to add policy '{display_id}' to store '{target_id}': {error}"
))
})?;
}
}
let mut missing_global_policy_ids = layout
.global_policy_ids()
.difference(&found_global_policy_ids)
.cloned()
.collect::<Vec<_>>();
if !missing_global_policy_ids.is_empty() {
missing_global_policy_ids.sort();
return Err(PolicyError::PolicyStoreConfigError(format!(
"global policy IDs were not found in the policy source: {}",
missing_global_policy_ids.join(", ")
)));
}
Ok(PolicySets::Scoped { layout, stores })
}
#[inline]
fn extract_permit_policies(
snapshot: &PolicySnapshot,
result: &cedar_policy::Response,
) -> PermitPolicies {
if result.decision() != cedar_policy::Decision::Allow {
return PermitPolicies::empty();
}
result
.diagnostics()
.reason()
.filter_map(|reason| snapshot.permit_policies.get(reason))
.cloned()
.collect()
}
#[inline]
fn extract_forbid_policy_ids(
snapshot: &PolicySnapshot,
result: &cedar_policy::Response,
) -> Vec<String> {
if result.decision() != cedar_policy::Decision::Deny {
return Vec::new();
}
let mut ids: Vec<String> = result
.diagnostics()
.reason()
.filter_map(|reason| snapshot.forbid_policy_ids.get(reason).cloned())
.collect();
ids.sort();
ids.dedup();
ids
}
#[inline]
fn request_groups(request: &Request) -> Option<&Groups> {
match &request.principal {
Principal::User(user) => Some(user.groups()),
Principal::Group(_) => None,
}
}
#[inline]
fn apply_labels(
registry: &LabelRegistry,
resource: &crate::types::Resource,
timers: &mut EvalTimers,
) -> Option<crate::types::Resource> {
let measure_enabled = timers.measure_enabled;
let _timer = PhaseTimer::new_if(&mut timers.labels, measure_enabled);
#[cfg(feature = "observability")]
let _label_span = info_span!("apply_labels").entered();
registry.apply_to_clone_if_applicable(resource)
}
#[inline]
fn build_effective_context(
request_context: Option<&RequestContext>,
) -> Result<cedar_policy::Context, PolicyError> {
match request_context {
Some(context) if !context.is_empty() => context.to_cedar_context(),
_ => Ok(cedar_policy::Context::empty()),
}
}
#[inline]
fn build_cedar_req(
principal_uid: cedar_policy::EntityUid,
action_uid: cedar_policy::EntityUid,
resource_uid: cedar_policy::EntityUid,
context: cedar_policy::Context,
schema: Option<&Schema>,
timers: &mut EvalTimers,
) -> Result<CedarRequest, PolicyError> {
let measure_enabled = timers.measure_enabled;
let _timer = PhaseTimer::new_if(&mut timers.construct_req, measure_enabled);
#[cfg(feature = "observability")]
let _req_span = info_span!("construct_cedar_req").entered();
Ok(CedarRequest::new(
principal_uid,
action_uid,
resource_uid,
context,
schema,
)?)
}
#[inline]
fn build_entities(
principal_uid: cedar_policy::EntityUid,
resource_uid: cedar_policy::EntityUid,
resource: &crate::types::Resource,
groups: Option<&Groups>,
schema: Option<&Schema>,
timers: &mut EvalTimers,
) -> Result<Entities, PolicyError> {
let group_uids = {
let measure_enabled = timers.measure_enabled;
let _timer = PhaseTimer::new_if(&mut timers.groups, measure_enabled);
#[cfg(feature = "observability")]
let _groups_span = info_span!("resolve_groups").entered();
let mut group_uids = HashSet::with_capacity(groups.map_or(0, Groups::len));
if let Some(groups) = groups {
for group in groups {
group_uids.insert(group.cedar_entity_uid().clone());
}
}
group_uids
};
let entities = {
let measure_enabled = timers.measure_enabled;
let _timer = PhaseTimer::new_if(&mut timers.entities, measure_enabled);
#[cfg(feature = "observability")]
let _entity_span = info_span!("construct_entities").entered();
let resource_attrs = resource.cedar_attr();
let resource_entity =
cedar_policy::Entity::new(resource_uid, resource_attrs, Default::default())?;
let mut all_entities = Vec::with_capacity(group_uids.len() + 2);
all_entities.extend(group_uids.iter().cloned().map(Entity::with_uid));
let principal_entity = Entity::new(principal_uid, HashMap::new(), group_uids)?;
all_entities.push(principal_entity);
all_entities.push(resource_entity);
Entities::empty().add_entities(all_entities, schema)?
};
if timers.debug_enabled {
debug!(
event = "Request",
phase = "Entities",
time = timers.entities.as_micros(),
entity_count = entities.iter().count()
);
}
Ok(entities)
}
mod validation_mode_private {
pub trait Sealed {}
}
pub trait ValidationMode: validation_mode_private::Sealed {}
#[derive(Debug, Clone, Copy)]
pub struct SchemaFree;
#[derive(Debug, Clone, Copy)]
pub struct SchemaEnforcing;
impl validation_mode_private::Sealed for SchemaFree {}
impl validation_mode_private::Sealed for SchemaEnforcing {}
impl ValidationMode for SchemaFree {}
impl ValidationMode for SchemaEnforcing {}
#[derive(Clone)]
pub struct PolicyEngine<M: ValidationMode = SchemaFree> {
inner: Arc<ArcSwap<EngineState>>,
mode: PhantomData<fn() -> M>,
}
#[derive(Clone)]
pub struct EvaluationSession<M: ValidationMode = SchemaFree> {
state: State,
mode: PhantomData<fn() -> M>,
}
impl<M: ValidationMode> From<PolicyEngine<M>> for PolicyVersion {
fn from(engine: PolicyEngine<M>) -> Self {
engine.current_version()
}
}
impl<M: ValidationMode> From<&PolicyEngine<M>> for PolicyVersion {
fn from(engine: &PolicyEngine<M>) -> Self {
engine.current_version()
}
}
impl<M: ValidationMode> PolicyEngine<M> {
fn from_snapshot(snapshot: PolicySnapshot) -> Self {
let state = EngineState {
policy: Arc::new(snapshot),
label_registry: None,
generation: 1,
};
Self {
inner: Arc::new(ArcSwap::from(Arc::new(state))),
mode: PhantomData,
}
}
}
impl PolicyEngine<SchemaFree> {
pub fn new_from_str(policy_text: &str) -> Result<Self, PolicyError> {
Ok(Self::from_snapshot(PolicySnapshot::from_policy_text(
policy_text,
)?))
}
pub fn new_from_str_with_policy_stores(
policy_text: &str,
layout: PolicyStoreLayout,
) -> Result<Self, PolicyError> {
Ok(Self::from_snapshot(
PolicySnapshot::from_policy_text_with_schema_and_stores(
policy_text,
None,
Some(layout),
)?,
))
}
pub fn new_from_str_with_schema(
policy_text: &str,
schema: Schema,
) -> Result<PolicyEngine<SchemaEnforcing>, PolicyError> {
Ok(PolicyEngine::<SchemaEnforcing>::from_snapshot(
PolicySnapshot::from_policy_text_with_schema(policy_text, Some(Arc::new(schema)))?,
))
}
pub fn new_from_str_with_schema_and_policy_stores(
policy_text: &str,
schema: Schema,
layout: PolicyStoreLayout,
) -> Result<PolicyEngine<SchemaEnforcing>, PolicyError> {
Ok(PolicyEngine::<SchemaEnforcing>::from_snapshot(
PolicySnapshot::from_policy_text_with_schema_and_stores(
policy_text,
Some(Arc::new(schema)),
Some(layout),
)?,
))
}
pub fn new_from_str_with_cedarschema(
policy_text: &str,
schema_text: &str,
) -> Result<PolicyEngine<SchemaEnforcing>, PolicyError> {
let schema: Schema = schema_text
.parse()
.map_err(|e| PolicyError::ParseError(format!("failed to parse Cedar schema: {e}")))?;
Self::new_from_str_with_schema(policy_text, schema)
}
pub fn new_from_str_with_cedarschema_and_policy_stores(
policy_text: &str,
schema_text: &str,
layout: PolicyStoreLayout,
) -> Result<PolicyEngine<SchemaEnforcing>, PolicyError> {
let schema: Schema = schema_text
.parse()
.map_err(|e| PolicyError::ParseError(format!("failed to parse Cedar schema: {e}")))?;
Self::new_from_str_with_schema_and_policy_stores(policy_text, schema, layout)
}
}
impl<M: ValidationMode> PolicyEngine<M> {
pub fn with_label_registry(self, registry: LabelRegistry) -> Self {
self.set_label_registry(registry);
self
}
pub fn set_label_registry(&self, registry: LabelRegistry) {
self.inner.rcu(|current| {
Arc::new(EngineState {
policy: Arc::clone(¤t.policy),
label_registry: Some(registry.clone()),
generation: current.generation.saturating_add(1),
})
});
}
pub fn label_registry(&self) -> Option<LabelRegistry> {
self.current_state().label_registry.clone()
}
pub fn reload_from_str(&self, policy_text: &str) -> Result<(), PolicyError> {
let had_schema = 'compile: loop {
let mut expected = self.current_state();
let had_schema = expected.policy.schema.is_some();
let schema = expected.policy.schema.clone();
let layout = expected.policy.sets.layout().cloned();
let new_snapshot: Snapshot =
Arc::new(PolicySnapshot::from_policy_text_with_schema_and_stores(
policy_text,
schema,
layout,
)?);
loop {
match self.install_policy_if_current(&expected, Arc::clone(&new_snapshot)) {
Ok(()) => break 'compile had_schema,
Err(latest) if Arc::ptr_eq(&expected.policy, &latest.policy) => {
expected = latest;
}
Err(_) => {
continue 'compile;
}
}
}
};
debug!(
event = "PolicyReload",
schema_enabled = had_schema,
schema_reloaded = false
);
#[cfg(feature = "observability")]
record_reload();
Ok(())
}
fn current_state(&self) -> State {
self.inner.load_full()
}
fn current_snapshot(&self) -> Snapshot {
Arc::clone(&self.current_state().policy)
}
fn install_policy(&self, policy: Snapshot) {
self.inner.rcu(|current| {
Arc::new(EngineState {
policy: Arc::clone(&policy),
label_registry: current.label_registry.clone(),
generation: current.generation.saturating_add(1),
})
});
}
fn install_policy_if_current(&self, expected: &State, policy: Snapshot) -> Result<(), State> {
let replacement = Arc::new(EngineState {
policy,
label_registry: expected.label_registry.clone(),
generation: expected.generation.saturating_add(1),
});
let previous = self.inner.compare_and_swap(expected, replacement);
if Arc::ptr_eq(expected, &previous) {
Ok(())
} else {
Err(Arc::clone(&previous))
}
}
pub fn current_version(&self) -> PolicyVersion {
self.current_state().version()
}
pub fn session(&self) -> EvaluationSession<M> {
EvaluationSession {
state: self.current_state(),
mode: PhantomData,
}
}
pub fn policy_store_ids(&self) -> Option<Vec<PolicyStoreId>> {
self.current_state().policy.sets.store_ids()
}
fn prepare(
state: &EngineState,
request: &Request,
request_context: Option<&RequestContext>,
) -> Result<PreparedRequest, PolicyError> {
let schema = state.policy.schema();
#[cfg(feature = "observability")]
let sink = get_sink();
#[cfg(feature = "observability")]
let metrics_enabled = metrics_enabled(&sink);
#[cfg(not(feature = "observability"))]
let metrics_enabled = false;
let debug_enabled = tracing::enabled!(tracing::Level::DEBUG);
let mut timers = EvalTimers::start(debug_enabled || metrics_enabled, debug_enabled);
let groups = request_groups(request);
if timers.debug_enabled {
debug!(
event = "Request",
phase = "Evaluation",
group_count = groups.map_or(0, Groups::len)
);
}
let principal_uid = request.principal.cedar_entity_uid().clone();
let action_uid = request.action.cedar_entity_uid().clone();
let labelled_resource = if let Some(registry) = &state.label_registry {
let labelled_resource = apply_labels(registry, &request.resource, &mut timers);
if timers.debug_enabled {
let resource_for_metrics = labelled_resource.as_ref().unwrap_or(&request.resource);
debug!(
event = "Request",
phase = "LabelsApplied",
time = timers.labels.as_micros(),
attribute_count = resource_for_metrics.attributes().len()
);
}
labelled_resource
} else {
if timers.debug_enabled {
debug!(
event = "Request",
phase = "LabelsApplied",
time = timers.labels.as_micros()
);
}
None
};
let resource_for_entities = labelled_resource.as_ref().unwrap_or(&request.resource);
let resource_uid = resource_for_entities.cedar_entity_uid().clone();
let context = build_effective_context(request_context)?;
if timers.debug_enabled {
debug!(
event = "Request",
phase = "Parsed",
group_count = groups.map_or(0, Groups::len),
attribute_count = resource_for_entities.attributes().len(),
request_context_attribute_count = request_context.map_or(0, RequestContext::len)
);
}
let principal_uid_for_entities = principal_uid.clone();
let resource_uid_for_entities = resource_uid.clone();
let cedar_req = build_cedar_req(
principal_uid,
action_uid,
resource_uid,
context,
schema,
&mut timers,
)?;
let entities = build_entities(
principal_uid_for_entities,
resource_uid_for_entities,
resource_for_entities,
groups,
schema,
&mut timers,
)?;
if timers.debug_enabled {
debug!(
event = "Request",
phase = "GroupsResolved",
time = timers.groups.as_micros(),
);
}
Ok(PreparedRequest {
cedar_req,
entities,
timers,
#[cfg(feature = "observability")]
sink,
#[cfg(feature = "observability")]
metrics_enabled,
})
}
#[cfg_attr(
feature = "observability",
tracing::instrument(name = "policy_evaluation", skip_all)
)]
pub fn evaluate(&self, request: &Request) -> Result<Decision, PolicyError> {
Ok(self
.evaluate_internal(request, None, false)?
.into_decision())
}
pub fn evaluate_with_context(
&self,
request: &Request,
request_context: &RequestContext,
) -> Result<Decision, PolicyError> {
Ok(self
.evaluate_internal(request, Some(request_context), false)?
.into_decision())
}
pub fn evaluate_with_diagnostics(
&self,
request: &Request,
) -> Result<DecisionDiagnostics, PolicyError> {
self.evaluate_internal(request, None, true)
}
pub fn evaluate_with_context_and_diagnostics(
&self,
request: &Request,
request_context: &RequestContext,
) -> Result<DecisionDiagnostics, PolicyError> {
self.evaluate_internal(request, Some(request_context), true)
}
fn evaluate_internal(
&self,
request: &Request,
request_context: Option<&RequestContext>,
include_forbid_diagnostics: bool,
) -> Result<DecisionDiagnostics, PolicyError> {
let state = self.current_state();
Self::evaluate_state(&state, request, request_context, include_forbid_diagnostics)
}
fn evaluate_state(
state: &EngineState,
request: &Request,
request_context: Option<&RequestContext>,
include_forbid_diagnostics: bool,
) -> Result<DecisionDiagnostics, PolicyError> {
let policy_set = state.policy.sets.resolve(request)?;
let mut prepared = Self::prepare(state, request, request_context)?;
let result = {
let measure_enabled = prepared.timers.measure_enabled;
let _timer = PhaseTimer::new_if(&mut prepared.timers.authz, measure_enabled);
#[cfg(feature = "observability")]
let _authz_span = info_span!("authorize").entered();
get_authorizer().is_authorized(&prepared.cedar_req, policy_set, &prepared.entities)
};
if prepared.timers.debug_enabled {
debug!(
event = "Request",
phase = "Authorized",
time = prepared.timers.authz.as_micros(),
decision = ?result.decision(),
);
}
let version = state.version();
if prepared.timers.debug_enabled {
debug!(
event = "Request",
phase = "Result",
time = prepared.timers.total_elapsed().as_micros(),
result = ?result.decision(),
policy_hash = %version.hash,
policy_loaded_at = %version.loaded_at,
);
}
let permit_policies = extract_permit_policies(&state.policy, &result);
let collect_forbid_ids = include_forbid_diagnostics;
let forbid_policy_ids = if collect_forbid_ids {
extract_forbid_policy_ids(&state.policy, &result)
} else {
Vec::new()
};
let decision = Decision::from_cedar(result.decision(), permit_policies, version)?;
#[cfg(feature = "observability")]
{
if prepared.metrics_enabled {
let dur = prepared.timers.total_elapsed();
let allowed = result.decision() == cedar_policy::Decision::Allow;
let phases = EvaluationPhases {
apply_labels_ms: prepared.timers.labels.as_secs_f64() * 1000.0,
construct_entities_ms: prepared.timers.entities.as_secs_f64() * 1000.0,
resolve_groups_ms: prepared.timers.groups.as_secs_f64() * 1000.0,
authorize_ms: prepared.timers.authz.as_secs_f64() * 1000.0,
total_ms: dur.as_secs_f64() * 1000.0,
};
let matched_policies = match decision.permit_policies() {
Some(policies) => MatchedPolicySource::Allow(policies),
None => MatchedPolicySource::Deny {
diagnostics: result.diagnostics(),
policy_ids: &state.policy.forbid_policy_ids,
},
};
let observation = EvaluationObservation::new(
dur,
allowed,
&request.action,
phases,
matched_policies,
);
record_evaluation_observation(&prepared.sink, &observation);
}
}
Ok(DecisionDiagnostics::new(decision, forbid_policy_ids))
}
pub fn list_policies_for_user(
&self,
user: &str,
groups: &[&str],
namespace: &[&str],
) -> Result<PolicyCandidates, PolicyError> {
self.list_policies_for_user_with_resource_and_effect(
user,
groups,
namespace,
None,
PolicyEffectFilter::Permit,
)
}
pub fn list_policies(&self, request: &Request) -> Result<PolicyCandidates, PolicyError> {
self.list_policies_with_effect(request, PolicyEffectFilter::Permit)
}
pub fn list_policies_with_effect(
&self,
request: &Request,
effect_filter: PolicyEffectFilter,
) -> Result<PolicyCandidates, PolicyError> {
let principal = PrincipalQuery::from_principal(&request.principal);
let action = ActionQuery::from_action(&request.action);
self.list_policies_dispatch(
&request.principal.to_string(),
&principal,
Some(&action),
Some(&request.resource),
effect_filter,
)
}
pub fn list_policies_for_user_with_resource(
&self,
user: &str,
groups: &[&str],
namespace: &[&str],
resource: Option<&Resource>,
) -> Result<PolicyCandidates, PolicyError> {
self.list_policies_for_user_with_resource_and_effect(
user,
groups,
namespace,
resource,
PolicyEffectFilter::Permit,
)
}
pub fn list_policies_for_user_with_resource_and_effect(
&self,
user: &str,
groups: &[&str],
namespace: &[&str],
resource: Option<&Resource>,
effect_filter: PolicyEffectFilter,
) -> Result<PolicyCandidates, PolicyError> {
let principal = PrincipalQuery::for_user(user, groups, namespace)?;
self.list_policies_dispatch(user, &principal, None, resource, effect_filter)
}
pub fn list_policies_for_group(
&self,
group: &str,
namespace: &[&str],
) -> Result<PolicyCandidates, PolicyError> {
self.list_policies_for_group_with_resource_and_effect(
group,
namespace,
None,
PolicyEffectFilter::Permit,
)
}
pub fn list_policies_for_group_with_resource(
&self,
group: &str,
namespace: &[&str],
resource: Option<&Resource>,
) -> Result<PolicyCandidates, PolicyError> {
self.list_policies_for_group_with_resource_and_effect(
group,
namespace,
resource,
PolicyEffectFilter::Permit,
)
}
pub fn list_policies_for_group_with_resource_and_effect(
&self,
group: &str,
namespace: &[&str],
resource: Option<&Resource>,
effect_filter: PolicyEffectFilter,
) -> Result<PolicyCandidates, PolicyError> {
let principal = PrincipalQuery::for_group(group, namespace)?;
self.list_policies_dispatch(group, &principal, None, resource, effect_filter)
}
fn list_policies_dispatch(
&self,
principal_id: &str,
principal: &PrincipalQuery,
action: Option<&ActionQuery>,
resource: Option<&Resource>,
effect_filter: PolicyEffectFilter,
) -> Result<PolicyCandidates, PolicyError> {
let snapshot = self.current_snapshot();
let resource_query = resource.map(ResourceQuery::from_resource);
let mut matching_policies: Vec<(Policy, Vec<PolicyMatchReason>)> = Vec::new();
let mut seen_policy_ids = HashSet::new();
for set in snapshot.sets.iter() {
for policy in set.policies() {
if !seen_policy_ids.insert(policy.id().clone()) {
continue;
}
if !matches_effect(policy.effect(), effect_filter) {
continue;
}
let Some(principal_reason) =
principal_match_reason(policy.principal_constraint(), principal)
else {
continue;
};
let Some(action_reason) = action_match_reason(policy.action_constraint(), action)
else {
continue;
};
let Some(resource_reason) =
resource_match_reason(policy.resource_constraint(), resource_query.as_ref())
else {
continue;
};
let mut reasons = vec![principal_reason];
if let Some(action_reason) = action_reason {
reasons.push(action_reason);
}
if let Some(resource_reason) = resource_reason {
reasons.push(resource_reason);
}
matching_policies.push((policy.clone(), reasons));
}
}
Ok(PolicyCandidates::new_with_matches(
principal_id,
matching_policies,
))
}
pub fn policies(&self) -> Vec<Policy> {
let snapshot = self.current_snapshot();
match &snapshot.sets {
PolicySets::Monolithic(set) => set.policies().cloned().collect(),
PolicySets::Scoped { .. } => {
let mut seen_policy_ids = HashSet::new();
let mut policies = snapshot
.sets
.iter()
.flat_map(PolicySet::policies)
.filter(|policy| seen_policy_ids.insert(policy.id().clone()))
.cloned()
.collect::<Vec<_>>();
policies.sort_by(|left, right| left.id().cmp(right.id()));
policies
}
}
}
}
impl PolicyEngine<SchemaEnforcing> {
pub fn reload_from_str_with_schema(
&self,
policy_text: &str,
schema: Schema,
) -> Result<(), PolicyError> {
let current_state = self.current_state();
let current_snapshot = ¤t_state.policy;
let layout = current_snapshot.sets.layout().cloned();
let new_snapshot: Snapshot =
Arc::new(PolicySnapshot::from_policy_text_with_schema_and_stores(
policy_text,
Some(Arc::new(schema)),
layout,
)?);
self.install_policy(new_snapshot);
debug!(
event = "PolicyReload",
schema_enabled = true,
schema_reloaded = true,
schema_previously_enabled = true
);
#[cfg(feature = "observability")]
record_reload();
Ok(())
}
pub fn reload_from_str_with_cedarschema(
&self,
policy_text: &str,
schema_text: &str,
) -> Result<(), PolicyError> {
let schema: Schema = schema_text
.parse()
.map_err(|e| PolicyError::ParseError(format!("failed to parse Cedar schema: {e}")))?;
self.reload_from_str_with_schema(policy_text, schema)
}
}
impl<M: ValidationMode> EvaluationSession<M> {
pub fn version(&self) -> PolicyVersion {
self.state.version()
}
pub fn evaluate(&self, request: &Request) -> Result<Decision, PolicyError> {
Ok(PolicyEngine::<M>::evaluate_state(&self.state, request, None, false)?.into_decision())
}
pub fn evaluate_with_context(
&self,
request: &Request,
request_context: &RequestContext,
) -> Result<Decision, PolicyError> {
Ok(
PolicyEngine::<M>::evaluate_state(&self.state, request, Some(request_context), false)?
.into_decision(),
)
}
pub fn evaluate_with_diagnostics(
&self,
request: &Request,
) -> Result<DecisionDiagnostics, PolicyError> {
PolicyEngine::<M>::evaluate_state(&self.state, request, None, true)
}
pub fn evaluate_with_context_and_diagnostics(
&self,
request: &Request,
request_context: &RequestContext,
) -> Result<DecisionDiagnostics, PolicyError> {
PolicyEngine::<M>::evaluate_state(&self.state, request, Some(request_context), true)
}
}
#[cfg(test)]
mod tests;