use cedar_policy::{
Authorizer, Entities, Entity, Policy, PolicyId, PolicySet, Request as CedarRequest, Schema,
};
use std::collections::{HashMap, HashSet};
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, FromDecisionWithPolicy, PermitPolicies, PermitPolicy,
PolicyEffectFilter, PolicyMatchReason, PolicyVersion, Request, RequestContext, Resource,
UserPolicies,
};
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,
snapshot: Snapshot,
selected_store: Option<usize>,
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_store(&self, request: &Request) -> Result<Option<usize>, PolicyError> {
match self {
Self::Monolithic(_) => Ok(None),
Self::Scoped { layout, .. } => layout
.resolve_request(&request.action, &request.resource)
.map(Some),
}
}
fn selected(&self, store: Option<usize>) -> Result<&PolicySet, PolicyError> {
match (self, store) {
(Self::Monolithic(set), None) => Ok(set),
(Self::Scoped { layout, stores }, Some(index)) => stores.get(index).ok_or_else(|| {
PolicyError::PolicyStoreRoutingError(format!(
"configured policy-store index {index} is outside layout length {}",
layout.stores().len()
))
}),
(Self::Monolithic(_), Some(index)) => Err(PolicyError::PolicyStoreRoutingError(
format!("monolithic engine was given policy-store index {index}"),
)),
(Self::Scoped { .. }, None) => Err(PolicyError::PolicyStoreRoutingError(
"partitioned engine did not resolve a policy store".to_string(),
)),
}
}
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,
version: PolicyVersion,
permit_policies: HashMap<PolicyId, PermitPolicy>,
forbid_policy_ids: HashMap<PolicyId, String>,
schema: Option<Arc<Schema>>,
}
type Snapshot = Arc<PolicySnapshot>;
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,
version: PolicyVersion {
hash: hash.into(),
loaded_at: humantime::format_rfc3339(SystemTime::now())
.to_string()
.into(),
},
permit_policies,
forbid_policy_ids,
schema,
})
}
fn version(&self) -> PolicyVersion {
self.version.clone()
}
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()?);
}
}
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)
}
#[derive(Clone)]
pub struct PolicyEngine {
inner: Arc<ArcSwap<PolicySnapshot>>,
label_registry: Option<Arc<LabelRegistry>>,
}
impl From<PolicyEngine> for PolicyVersion {
fn from(engine: PolicyEngine) -> Self {
engine.current_version()
}
}
impl From<&PolicyEngine> for PolicyVersion {
fn from(engine: &PolicyEngine) -> Self {
engine.current_version()
}
}
impl PolicyEngine {
fn from_snapshot(snapshot: PolicySnapshot) -> Self {
Self {
inner: Arc::new(ArcSwap::from(Arc::new(snapshot))),
label_registry: None,
}
}
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<Self, PolicyError> {
Ok(Self::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<Self, PolicyError> {
Ok(Self::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<Self, 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<Self, 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)
}
pub fn with_label_registry(mut self, registry: LabelRegistry) -> Self {
self.label_registry = Some(Arc::new(registry));
self
}
pub fn set_label_registry(&mut self, registry: LabelRegistry) {
self.label_registry = Some(Arc::new(registry));
}
pub fn label_registry(&self) -> Option<&LabelRegistry> {
self.label_registry.as_deref()
}
pub fn reload_from_str(&self, policy_text: &str) -> Result<(), PolicyError> {
let current_snapshot = self.current_snapshot();
let had_schema = current_snapshot.schema.is_some();
let schema = current_snapshot.schema.clone();
let layout = current_snapshot.sets.layout().cloned();
let new_snapshot: Snapshot = Arc::new(
PolicySnapshot::from_policy_text_with_schema_and_stores(policy_text, schema, layout)?,
);
self.inner.store(new_snapshot);
debug!(
event = "PolicyReload",
schema_enabled = had_schema,
schema_reloaded = false
);
#[cfg(feature = "observability")]
record_reload();
Ok(())
}
pub fn reload_from_str_with_schema(
&self,
policy_text: &str,
schema: Schema,
) -> Result<(), PolicyError> {
let current_snapshot = self.current_snapshot();
let had_schema = current_snapshot.schema.is_some();
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.inner.store(new_snapshot);
debug!(
event = "PolicyReload",
schema_enabled = true,
schema_reloaded = true,
schema_previously_enabled = had_schema
);
#[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)
}
fn current_snapshot(&self) -> Snapshot {
self.inner.load_full()
}
pub fn current_version(&self) -> PolicyVersion {
self.current_snapshot().version()
}
pub fn policy_store_ids(&self) -> Option<Vec<PolicyStoreId>> {
self.current_snapshot().sets.store_ids()
}
fn prepare(
&self,
request: &Request,
request_context: Option<&RequestContext>,
) -> Result<PreparedRequest, PolicyError> {
let snapshot = self.current_snapshot();
let selected_store = snapshot.sets.resolve_store(request)?;
let schema = snapshot.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()?;
let action_uid = request.action.cedar_entity_uid()?;
let labelled_resource = if let Some(registry) = &self.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()?;
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,
snapshot,
selected_store,
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)?.decision)
}
pub fn evaluate_with_context(
&self,
request: &Request,
request_context: &RequestContext,
) -> Result<Decision, PolicyError> {
Ok(self
.evaluate_internal(request, Some(request_context), false)?
.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 mut prepared = self.prepare(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();
let policy_set = prepared.snapshot.sets.selected(prepared.selected_store)?;
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 = prepared.snapshot.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(&prepared.snapshot, &result);
let collect_forbid_ids = include_forbid_diagnostics;
let forbid_policy_ids = if collect_forbid_ids {
extract_forbid_policy_ids(&prepared.snapshot, &result)
} else {
Vec::new()
};
let decision =
Decision::from_decision_with_policy(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 {
Decision::Allow { policies, .. } => MatchedPolicySource::Allow(policies),
Decision::Deny { .. } => MatchedPolicySource::Deny {
diagnostics: result.diagnostics(),
policy_ids: &prepared.snapshot.forbid_policy_ids,
},
};
let observation = EvaluationObservation::new(
dur,
allowed,
&request.action,
phases,
matched_policies,
);
record_evaluation_observation(&prepared.sink, &observation);
}
}
Ok(DecisionDiagnostics {
decision,
matched_forbid_policy_ids: forbid_policy_ids,
})
}
pub fn list_policies_for_user(
&self,
user: &str,
groups: &[&str],
namespace: &[&str],
) -> Result<UserPolicies, PolicyError> {
self.list_policies_for_user_with_resource_and_effect(
user,
groups,
namespace,
None,
PolicyEffectFilter::Permit,
)
}
pub fn list_policies(&self, request: &Request) -> Result<UserPolicies, PolicyError> {
self.list_policies_with_effect(request, PolicyEffectFilter::Permit)
}
pub fn list_policies_with_effect(
&self,
request: &Request,
effect_filter: PolicyEffectFilter,
) -> Result<UserPolicies, 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<UserPolicies, 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<UserPolicies, 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<UserPolicies, 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<UserPolicies, 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<UserPolicies, 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<UserPolicies, PolicyError> {
let snapshot = self.current_snapshot();
let resource_query = match resource {
Some(resource) => Some(ResourceQuery::from_resource(resource)?),
None => None,
};
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(UserPolicies::new_with_matches(
principal_id,
matching_policies,
))
}
pub fn policies(&self) -> Result<Vec<Policy>, PolicyError> {
let snapshot = self.current_snapshot();
match &snapshot.sets {
PolicySets::Monolithic(set) => Ok(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()));
Ok(policies)
}
}
}
}
#[cfg(test)]
mod tests;