use crate::EntityData;
use crate::TrustedIssuerLoadingInfo;
use crate::bootstrap_config::AuthorizationConfig;
use crate::common::default_entities::DefaultEntities;
use crate::common::policy_store::{PolicyStoreWithID, TrustedIssuer};
use crate::context_data_api::DataStore;
use crate::entity_builder::{
BuiltEntities, EntityBuilder, MultiIssuerSetupEntities, UnsignedPrincipalBuild,
};
use crate::jwt;
use crate::log::interface::LogWriter;
use crate::log::{
AuthorizationLogInfo, AuthorizeInfo, BaseLogEntry, Decision, DecisionLogEntry, Diagnostics,
DiagnosticsSummary, LogEntry, LogLevel, LogTokensInfo, Logger, PushedDataInfo, gen_uuid7,
};
use build_ctx::{build_context, build_multi_issuer_context};
use cedar_policy::{Entities, Entity, EntityUid, PolicyId};
use chrono::Utc;
use metrics::MetricsCollector;
use request::{
AuthorizeMultiIssuerRequest, BatchAuthorizeMultiIssuerRequest, BatchAuthorizeResponse,
BatchAuthorizeUnsignedRequest, BatchItem, RequestUnsigned,
};
use serde_json::json;
use smol_str::SmolStr;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
use uuid7::Uuid;
mod authorize_result;
mod build_ctx;
mod error_metrics;
mod errors;
pub(crate) mod metrics;
pub(crate) mod request;
pub use authorize_result::{AuthorizeResult, MultiIssuerAuthorizeResult};
pub use errors::*;
pub(crate) struct AuthzConfig {
pub log_service: Logger,
pub policy_store: PolicyStoreWithID,
pub jwt_service: Arc<jwt::JwtService>,
pub entity_builder: Arc<EntityBuilder>,
pub authorization: AuthorizationConfig,
pub data_store: Arc<DataStore>,
pub metrics: Arc<MetricsCollector>,
}
pub(super) struct Authz {
config: AuthzConfig,
authorizer: cedar_policy::Authorizer,
}
fn classify_batch_item_error(err: &AuthorizeError, item_index: usize) -> BatchItemError {
let message = err.to_string();
match err {
AuthorizeError::Action(_) | AuthorizeError::IdentifierParsing(_) => {
BatchItemError::ActionParse { message, item_index }
},
AuthorizeError::MultiIssuerEntity(_) => {
BatchItemError::MultiIssuerEntity { message, item_index }
},
AuthorizeError::BuildContext(_) | AuthorizeError::CreateContext(_) => {
BatchItemError::ContextBuild { message, item_index }
},
AuthorizeError::BuildEntity(_) => BatchItemError::ResourceBuild { message, item_index },
AuthorizeError::BuildUnsignedRoleEntity(_) => {
BatchItemError::PrincipalBuild { message, item_index }
},
AuthorizeError::ValidateEntities(_) | AuthorizeError::EntitiesToJson(_) => {
BatchItemError::SchemaValidation { message, item_index }
},
AuthorizeError::RequestValidation(_) | AuthorizeError::InvalidPrincipal(_) => {
BatchItemError::RequestValidation { message, item_index }
},
_ => {
debug_assert!(
false,
"batch-level error {err:?} reached per-item error classification for item {item_index}"
);
BatchItemError::SchemaValidation { message, item_index }
},
}
}
impl Authz {
pub(crate) fn new(config: AuthzConfig) -> Self {
config.log_service.log_any(
LogEntry::new(BaseLogEntry::new_system_opt_request_id(
LogLevel::INFO,
None,
))
.set_cedar_version()
.set_message("Cedarling Authz initialized successfully".to_string()),
);
Self {
config,
authorizer: cedar_policy::Authorizer::new(),
}
}
pub(crate) fn trusted_issuers(&self) -> Option<&HashMap<String, TrustedIssuer>> {
self.config.policy_store.trusted_issuers.as_ref()
}
pub(crate) fn clone_jwt_service(&self) -> Arc<jwt::JwtService> {
Arc::clone(&self.config.jwt_service)
}
fn get_pushed_data(&self) -> (HashMap<String, serde_json::Value>, Option<PushedDataInfo>) {
let pushed_data = self.config.data_store.get_all();
let pushed_data_info = if pushed_data.is_empty() {
None
} else {
Some(PushedDataInfo {
keys: pushed_data
.keys()
.map(|k| SmolStr::from(k.as_str()))
.collect(),
})
};
(pushed_data, pushed_data_info)
}
#[allow(clippy::too_many_lines)]
pub(super) fn authorize_multi_issuer(
&self,
request: &AuthorizeMultiIssuerRequest,
) -> Result<MultiIssuerAuthorizeResult, AuthorizeError> {
let start_time = Utc::now();
let request_id = gen_uuid7();
request.validate().inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let schema = &self.config.policy_store.schema;
let MultiIssuerSetup {
validated_tokens,
entities: setup_entities,
} = self.multi_issuer_setup(&request.tokens)?;
let resource = self
.config
.entity_builder
.build_resource_entity(&request.resource)
.inspect_err(|e| {
self.config.log_service.log_any(
LogEntry::new(BaseLogEntry::new_system_opt_request_id(
LogLevel::ERROR,
None,
))
.set_message(
"Failed to build resource entity for multi-issuer authorization".to_string(),
)
.set_error(e.to_string()),
);
})
.map_err(|e| {
let wrapped = crate::entity_builder::MultiIssuerEntityError::EntityCreationFailed(
e.to_string(),
);
self.config.metrics.record_error(&wrapped);
self.config.metrics.record_authz_error();
AuthorizeError::MultiIssuerEntity(wrapped)
})?;
let action = cedar_policy::EntityUid::from_str(request.action.as_str())
.map_err(AuthorizeError::from)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let (pushed_data, pushed_data_info) = self.get_pushed_data();
let schema_ref = schema.as_ref().map(|s| &s.schema);
let context = build_multi_issuer_context(
request.context.clone().unwrap_or(json!({})),
&setup_entities.tokens,
schema_ref,
&action,
&pushed_data,
)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let entities_data = AuthorizeEntitiesData {
issuers: setup_entities.issuers,
tokens: setup_entities.tokens,
resource,
default_entities: setup_entities.default_entities,
};
let resource_uid = entities_data.resource.uid();
let entities = entities_data
.entities(schema_ref)
.map_err(AuthorizeError::ValidateEntities)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let authz_result = self
.execute_authorize(ExecuteAuthorizeParameters {
entities: &entities,
principal: None,
action: action.clone(),
resource: resource_uid.clone(),
context,
})
.map_err(AuthorizeError::RequestValidation)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let authz_info = AuthorizeInfo {
principal: "None (multi-issuer)".to_string(),
diagnostics: Diagnostics::new(
authz_result.diagnostics(),
&self.config.policy_store.policies,
),
decision: authz_result.decision().into(),
};
let decision_time_micro_sec = calculate_elapsed_time(start_time);
let result = MultiIssuerAuthorizeResult::new(authz_result.clone(), request_id);
self.log_policy_evaluation_errors(
&authz_info.diagnostics,
"multi-issuer (no principal)",
request_id,
);
let tokens_logging_info = LogTokensInfo::new(
&validated_tokens,
self.config
.authorization
.decision_log_default_jwt_id
.as_str(),
);
let multi_diagnostics = std::slice::from_ref(&authz_info.diagnostics);
self.log_decision(
request_id,
&DecisionLogMetadata {
action: request.action.clone(),
resource: resource_uid.to_string(),
decision_diagnostics: multi_diagnostics,
decision_time: decision_time_micro_sec,
principal: DecisionLogEntry::principal(
false, false, ),
tokens_logging_info,
decision: result.decision,
pushed_data: pushed_data_info,
batch_id: None,
},
);
let debug_log_fn = BaseLogEntry::new_system(LogLevel::DEBUG, request_id).with_fn(|base| {
let entities_json: serde_json::Value = {
serialize_entities(&entities)
};
LogEntry::new(base)
.set_auth_info(AuthorizationLogInfo {
action: request.action.clone(),
context: request.context.clone().unwrap_or(json!({})),
resource: resource_uid.to_string(),
entities: entities_json,
authorize_info: vec![authz_info.clone()],
authorized: result.decision,
})
.set_message("Result of multi-issuer authorize.".to_string())
});
self.config.log_service.log_fn(debug_log_fn);
let decision = Decision::from(result.decision);
let policy_decisions = multi_diagnostics
.iter()
.flat_map(|d| d.reason.iter())
.map(|policy| (policy.id.as_str(), decision));
self.config.metrics.record_evaluation(
decision_time_micro_sec,
decision,
false,
policy_decisions,
);
Ok(result)
}
#[allow(clippy::too_many_lines)]
pub(super) fn authorize_multi_issuer_batch(
&self,
request: &BatchAuthorizeMultiIssuerRequest,
) -> Result<BatchAuthorizeResponse<Result<MultiIssuerAuthorizeResult, BatchItemError>>, AuthorizeError>
{
let batch_start_time = Utc::now();
let batch_id = gen_uuid7();
request.validate().inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let MultiIssuerSetup {
validated_tokens,
entities: setup_entities,
} = self.multi_issuer_setup(&request.tokens)?;
let (pushed_data, pushed_data_info) = self.get_pushed_data();
let schema = &self.config.policy_store.schema;
let schema_ref = schema.as_ref().map(|s| &s.schema);
let tokens_logging_info = LogTokensInfo::new(
&validated_tokens,
self.config
.authorization
.decision_log_default_jwt_id
.as_str(),
);
let mut results = Vec::with_capacity(request.items.len());
for (item_index, item) in request.items.iter().enumerate() {
let item_start = Utc::now();
let item_request_id = gen_uuid7();
let (response, resource_uid_str) = match self.try_batch_item_multi_issuer(
item,
&setup_entities,
&pushed_data,
schema_ref,
) {
Ok((resp, uid)) => (resp, uid.to_string()),
Err(e) => {
let item_err = classify_batch_item_error(&e, item_index);
self.config.metrics.record_error(&e);
self.config.metrics.record_authz_error();
let log_entry = LogEntry::new(BaseLogEntry::new_decision(item_request_id))
.set_batch_id(batch_id)
.set_message(format!(
"Batch item {item_index} failed setup ({}) for batch {batch_id}",
item_err.category(),
))
.set_error(item_err.to_string());
self.config.log_service.log_any(log_entry);
results.push(Err(item_err));
continue;
},
};
let decision = response.decision() == cedar_policy::Decision::Allow;
let decision_time_micro_sec = calculate_elapsed_time(item_start);
let diagnostics =
Diagnostics::new(response.diagnostics(), &self.config.policy_store.policies);
self.log_policy_evaluation_errors(
&diagnostics,
"multi-issuer (no principal)",
item_request_id,
);
let diagnostics_slice = std::slice::from_ref(&diagnostics);
self.log_decision(
item_request_id,
&DecisionLogMetadata {
action: item.action.clone(),
resource: resource_uid_str,
decision,
tokens_logging_info: tokens_logging_info.clone(),
decision_time: decision_time_micro_sec,
decision_diagnostics: diagnostics_slice,
principal: DecisionLogEntry::principal(false, false),
pushed_data: pushed_data_info.clone(),
batch_id: Some(batch_id),
},
);
if !decision {
self.log_failed_diagnostics(diagnostics_slice, item_request_id, Some(batch_id));
}
let cedar_decision = Decision::from(decision);
let policy_decisions = diagnostics
.reason
.iter()
.map(|p| (p.id.as_str(), cedar_decision));
self.config.metrics.record_evaluation(
decision_time_micro_sec,
cedar_decision,
false,
policy_decisions,
);
results.push(Ok(MultiIssuerAuthorizeResult::new(response, item_request_id)));
}
self.config
.metrics
.record_batch(request.items.len(), false);
let batch_time_micro_sec = calculate_elapsed_time(batch_start_time);
self.config.log_service.log_any(
LogEntry::new(BaseLogEntry::new_system(LogLevel::INFO, batch_id))
.set_batch_id(batch_id)
.set_message(format!(
"Batch authorize (multi-issuer): {} items in {batch_time_micro_sec}μs",
request.items.len(),
)),
);
Ok(BatchAuthorizeResponse::new(batch_id, results))
}
fn try_batch_item_multi_issuer(
&self,
item: &BatchItem,
setup: &MultiIssuerSetupEntities,
pushed_data: &HashMap<String, serde_json::Value>,
schema_ref: Option<&cedar_policy::Schema>,
) -> Result<(cedar_policy::Response, EntityUid), AuthorizeError> {
let action =
cedar_policy::EntityUid::from_str(&item.action).map_err(AuthorizeError::from)?;
let resource = self
.config
.entity_builder
.build_resource_entity(&item.resource)
.map_err(|e| {
AuthorizeError::MultiIssuerEntity(
crate::entity_builder::MultiIssuerEntityError::EntityCreationFailed(
e.to_string(),
),
)
})?;
let context = build_multi_issuer_context(
item.context.clone(),
&setup.tokens,
schema_ref,
&action,
pushed_data,
)?;
let entities_data = AuthorizeEntitiesData {
issuers: setup.issuers.clone(),
tokens: setup.tokens.clone(),
resource,
default_entities: setup.default_entities.clone(),
};
let resource_uid = entities_data.resource.uid();
let entities = entities_data
.entities(schema_ref)
.map_err(AuthorizeError::ValidateEntities)?;
let response = self
.execute_authorize(ExecuteAuthorizeParameters {
entities: &entities,
principal: None,
action,
resource: resource_uid.clone(),
context,
})
.map_err(AuthorizeError::RequestValidation)?;
Ok((response, resource_uid))
}
#[allow(clippy::too_many_lines)]
pub(super) fn authorize_unsigned(
&self,
request: &RequestUnsigned,
) -> Result<AuthorizeResult, AuthorizeError> {
let start_time = Utc::now();
let request_id = gen_uuid7();
let schema = &self.config.policy_store.schema;
let schema_ref = schema.as_ref().map(|s| &s.schema);
let action = cedar_policy::EntityUid::from_str(request.action.as_str())
.map_err(AuthorizeError::from)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let UnsignedSetup {
principal,
built_entities,
} = self.unsigned_setup(request.principal.as_ref())?;
let resource = self
.config
.entity_builder
.build_resource_entity(&request.resource)
.map_err(|e| {
let err = AuthorizeError::BuildEntity(e);
self.config.metrics.record_error(&err);
self.config.metrics.record_authz_error();
err
})?;
let principal_uid = principal.as_ref().map(cedar_policy::Entity::uid);
let resource_uid = resource.uid();
let (pushed_data, pushed_data_info) = self.get_pushed_data();
let context = build_context(
&self.config,
request.context.clone(),
&built_entities,
&action,
&pushed_data,
)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let entities = Entities::from_entities(principal.into_iter().chain([resource]), schema_ref)
.map_err(|e| AuthorizeError::ValidateEntities(Box::new(e)))
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let response = self
.execute_authorize(ExecuteAuthorizeParameters {
entities: &entities,
principal: principal_uid.clone(),
action: action.clone(),
resource: resource_uid.clone(),
context,
})
.map_err(AuthorizeError::RequestValidation)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let decision_time_micro_sec = calculate_elapsed_time(start_time);
let result = AuthorizeResult::new(response.clone(), request_id);
let principal_label = principal_uid
.as_ref()
.map_or_else(|| "None".to_string(), ToString::to_string);
let authz_info = AuthorizeInfo {
principal: principal_label,
diagnostics: Diagnostics::new(
response.diagnostics(),
&self.config.policy_store.policies,
),
decision: response.decision().into(),
};
let debug_authorize_info = vec![authz_info.clone()];
let diagnostics = std::slice::from_ref(&authz_info.diagnostics);
self.log_policy_evaluation_errors(
&authz_info.diagnostics,
&authz_info.principal,
request_id,
);
let logged_principals = principal_uid.as_slice();
self.log_decision(
request_id,
&DecisionLogMetadata {
action: request.action.clone(),
resource: resource_uid.to_string(),
decision: result.decision,
tokens_logging_info: LogTokensInfo::empty(),
decision_time: decision_time_micro_sec,
decision_diagnostics: diagnostics,
principal: DecisionLogEntry::all_principals(logged_principals),
pushed_data: pushed_data_info,
batch_id: None,
},
);
self.log_debug(
request_id,
&DebugLogMetadata {
action: request.action.clone(),
resource: resource_uid.to_string(),
context: &request.context,
entities: &entities,
debug_authz_info: debug_authorize_info,
decision: result.decision,
},
);
if !result.decision {
self.log_failed_diagnostics(diagnostics, request_id, None);
}
let decision = Decision::from(result.decision);
let policy_decisions = diagnostics
.iter()
.flat_map(|d| d.reason.iter())
.map(|policy| (policy.id.as_str(), decision));
self.config.metrics.record_evaluation(
decision_time_micro_sec,
decision,
true,
policy_decisions,
);
Ok(result)
}
#[allow(clippy::too_many_lines)]
pub(super) fn authorize_unsigned_batch(
&self,
request: &BatchAuthorizeUnsignedRequest,
) -> Result<BatchAuthorizeResponse<Result<AuthorizeResult, BatchItemError>>, AuthorizeError>
{
let batch_start_time = Utc::now();
let batch_id = gen_uuid7();
request.validate().inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let UnsignedSetup {
principal,
built_entities,
} = self.unsigned_setup(request.principal.as_ref())?;
let principal_uid = principal.as_ref().map(cedar_policy::Entity::uid);
let (pushed_data, pushed_data_info) = self.get_pushed_data();
let schema = &self.config.policy_store.schema;
let schema_ref = schema.as_ref().map(|s| &s.schema);
let mut results = Vec::with_capacity(request.items.len());
for (item_index, item) in request.items.iter().enumerate() {
let item_start = Utc::now();
let item_request_id = gen_uuid7();
let (response, resource_uid_str) = match self.try_batch_item_unsigned(
item,
principal.as_ref(),
principal_uid.as_ref(),
&built_entities,
&pushed_data,
schema_ref,
) {
Ok((resp, uid)) => {
let uid_str = uid.to_string();
(resp, uid_str)
},
Err(e) => {
let item_err = classify_batch_item_error(&e, item_index);
self.config.metrics.record_error(&e);
self.config.metrics.record_authz_error();
let log_entry = LogEntry::new(BaseLogEntry::new_decision(item_request_id))
.set_batch_id(batch_id)
.set_message(format!(
"Batch item {item_index} failed setup ({}) for batch {batch_id}",
item_err.category(),
))
.set_error(item_err.to_string());
self.config.log_service.log_any(log_entry);
results.push(Err(item_err));
continue;
},
};
let decision = response.decision() == cedar_policy::Decision::Allow;
let decision_time_micro_sec = calculate_elapsed_time(item_start);
let diagnostics =
Diagnostics::new(response.diagnostics(), &self.config.policy_store.policies);
let principal_label = principal_uid
.as_ref()
.map_or_else(|| "None".to_string(), ToString::to_string);
self.log_policy_evaluation_errors(&diagnostics, &principal_label, item_request_id);
let logged_principals: Vec<EntityUid> = principal_uid.iter().cloned().collect();
let diagnostics_slice = std::slice::from_ref(&diagnostics);
self.log_decision(
item_request_id,
&DecisionLogMetadata {
action: item.action.clone(),
resource: resource_uid_str,
decision,
tokens_logging_info: LogTokensInfo::empty(),
decision_time: decision_time_micro_sec,
decision_diagnostics: diagnostics_slice,
principal: DecisionLogEntry::all_principals(&logged_principals),
pushed_data: pushed_data_info.clone(),
batch_id: Some(batch_id),
},
);
if !decision {
self.log_failed_diagnostics(diagnostics_slice, item_request_id, Some(batch_id));
}
let cedar_decision = Decision::from(decision);
let policy_decisions = diagnostics
.reason
.iter()
.map(|p| (p.id.as_str(), cedar_decision));
self.config.metrics.record_evaluation(
decision_time_micro_sec,
cedar_decision,
true,
policy_decisions,
);
results.push(Ok(AuthorizeResult::new(response, item_request_id)));
}
self.config
.metrics
.record_batch(request.items.len(), true);
let batch_time_micro_sec = calculate_elapsed_time(batch_start_time);
self.config.log_service.log_any(
LogEntry::new(BaseLogEntry::new_system(LogLevel::INFO, batch_id))
.set_batch_id(batch_id)
.set_message(format!(
"Batch authorize (unsigned): {} items in {batch_time_micro_sec}μs",
request.items.len(),
)),
);
Ok(BatchAuthorizeResponse::new(batch_id, results))
}
fn try_batch_item_unsigned(
&self,
item: &BatchItem,
principal: Option<&Entity>,
principal_uid: Option<&EntityUid>,
built_entities: &BuiltEntities,
pushed_data: &HashMap<String, serde_json::Value>,
schema_ref: Option<&cedar_policy::Schema>,
) -> Result<(cedar_policy::Response, EntityUid), AuthorizeError> {
let action =
cedar_policy::EntityUid::from_str(&item.action).map_err(AuthorizeError::from)?;
let resource = self
.config
.entity_builder
.build_resource_entity(&item.resource)
.map_err(AuthorizeError::BuildEntity)?;
let context = build_context(
&self.config,
item.context.clone(),
built_entities,
&action,
pushed_data,
)?;
let resource_uid = resource.uid();
let entities =
Entities::from_entities(principal.cloned().into_iter().chain([resource]), schema_ref)
.map_err(|e| AuthorizeError::ValidateEntities(Box::new(e)))?;
let response = self
.execute_authorize(ExecuteAuthorizeParameters {
entities: &entities,
principal: principal_uid.cloned(),
action,
resource: resource_uid.clone(),
context,
})
.map_err(AuthorizeError::RequestValidation)?;
Ok((response, resource_uid))
}
fn unsigned_setup(
&self,
principal: Option<&EntityData>,
) -> Result<UnsignedSetup, AuthorizeError> {
let UnsignedPrincipalBuild {
principal,
built_entities,
} = self
.config
.entity_builder
.build_unsigned_principal(principal)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
Ok(UnsignedSetup {
principal,
built_entities,
})
}
fn multi_issuer_setup(
&self,
tokens: &[crate::TokenInput],
) -> Result<MultiIssuerSetup, AuthorizeError> {
let validated_tokens = self
.config
.jwt_service
.validate_multi_issuer_tokens(tokens)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let setup_entities = self
.config
.entity_builder
.build_multi_issuer_setup_entities(&validated_tokens, self.config.log_service.as_ref())
.map_err(|e| {
self.config.metrics.record_error(&e);
self.config.metrics.record_authz_error();
AuthorizeError::MultiIssuerEntity(e)
})?;
Ok(MultiIssuerSetup {
validated_tokens,
entities: setup_entities,
})
}
fn execute_authorize(
&self,
parameters: ExecuteAuthorizeParameters,
) -> Result<cedar_policy::Response, Box<cedar_policy::RequestValidationError>> {
let has_principal = parameters.principal.is_some();
let request_builder_base = cedar_policy::Request::builder()
.action(parameters.action)
.resource(parameters.resource)
.context(parameters.context);
let request = if let Some(schema) = &self.config.policy_store.schema {
let request_builder = request_builder_base.schema(&schema.schema);
match parameters.principal {
Some(principal) => request_builder.principal(principal).build()?,
None => request_builder.build()?,
}
} else {
match parameters.principal {
Some(principal) => request_builder_base.principal(principal).build(),
None => request_builder_base.build(),
}
};
if has_principal {
Ok(self.authorizer.is_authorized(
&request,
self.config.policy_store.policies.get_set(),
parameters.entities,
))
} else {
Ok(self.is_authorized_partial(&request, parameters.entities))
}
}
fn is_authorized_partial(
&self,
request: &cedar_policy::Request,
entities: &cedar_policy::Entities,
) -> cedar_policy::Response {
let partial = self.authorizer.is_authorized_partial(
request,
self.config.policy_store.policies.get_set(),
entities,
);
if partial.decision().is_some() {
partial.concretize()
} else {
let residual_ids: HashSet<cedar_policy::PolicyId> = partial
.nontrivial_residuals()
.map(|p| p.id().clone())
.collect();
let errors: Vec<cedar_policy::AuthorizationError> = partial
.concretize()
.diagnostics()
.errors()
.cloned()
.collect();
cedar_policy::Response::new(cedar_policy::Decision::Deny, residual_ids, errors)
}
}
fn log_policy_evaluation_errors(
&self,
diagnostics: &Diagnostics,
principal_name: &str,
request_id: Uuid,
) {
if !diagnostics.errors.is_empty() {
let log_entry = LogEntry::new(BaseLogEntry::new_decision(request_id))
.set_message(format!("Policy evaluation errors for {principal_name}"))
.set_error(format!("{:?}", diagnostics.errors));
self.config.log_service.log_any(log_entry);
}
}
fn log_failed_diagnostics(&self, diagnostics: &[Diagnostics], request_id: Uuid, batch_id: Option<Uuid>) {
let all_errors: Vec<_> = diagnostics.iter().flat_map(|d| &d.errors).collect();
if all_errors.is_empty() {
return;
}
let serialized_errors = serde_json::to_string(&all_errors)
.unwrap_or_else(|_| "failed to serialize diagnostics errors".to_string());
let mut log_entry = LogEntry::new(BaseLogEntry::new_decision(request_id))
.set_message(
"Authorization denied: summary of all policy evaluation errors".to_string(),
)
.set_error(serialized_errors);
if let Some(bid) = batch_id {
log_entry = log_entry.set_batch_id(bid);
}
self.config.log_service.log_any(log_entry);
}
fn log_decision(&self, request_id: Uuid, metadata: &DecisionLogMetadata) {
let entry = BaseLogEntry::new_decision(request_id).with_fn(|base| DecisionLogEntry {
base,
policystore_id: self.config.policy_store.id.as_str().into(),
policystore_version: self.config.policy_store.get_store_version().into(),
principal: metadata.principal.clone(),
lock_client_id: None,
action: metadata.action.clone(),
resource: metadata.resource.clone(),
decision: metadata.decision.into(),
tokens: metadata.tokens_logging_info.clone(),
decision_time_micro_sec: metadata.decision_time,
diagnostics: DiagnosticsSummary::from_diagnostics(metadata.decision_diagnostics),
pushed_data: metadata.pushed_data.clone(),
batch_id: metadata.batch_id,
});
self.config.log_service.log_fn(entry);
}
fn log_debug(&self, request_id: Uuid, metadata: &DebugLogMetadata) {
let debug_log_fn = BaseLogEntry::new_system(LogLevel::DEBUG, request_id).with_fn(|base| {
let entities_json: serde_json::Value = {
serialize_entities(metadata.entities)
};
LogEntry::new(base)
.set_auth_info(AuthorizationLogInfo {
action: metadata.action.clone(),
context: metadata.context.clone(),
resource: metadata.resource.clone(),
entities: entities_json,
authorize_info: metadata.debug_authz_info.clone(),
authorized: metadata.decision,
})
.set_message("Result of authorize.".to_string())
});
self.config.log_service.log_fn(debug_log_fn);
}
pub(super) fn get_matching_policies_unsigned(
&self,
principal: Option<&crate::EntityData>,
actions: &[String],
resources: &[crate::EntityData],
) -> Result<Vec<crate::PolicyMetadata>, AuthorizeError> {
let principal_types = match principal {
Some(p) => entity_data_to_type_names(std::slice::from_ref(p))?,
None => HashSet::new(),
};
let action_uids = parse_action_uids(actions)?;
let resource_types = entity_data_to_type_names(resources)?;
Ok(self.config.policy_store.policies.get_matching_policies(
&principal_types,
&action_uids,
&resource_types,
))
}
pub(super) fn get_matching_policies_multi_issuer(
&self,
tokens: &[crate::TokenInput],
actions: &[String],
resources: &[crate::EntityData],
) -> Result<Vec<crate::PolicyMetadata>, AuthorizeError> {
let validated_tokens = self
.config
.jwt_service
.validate_multi_issuer_tokens(tokens)?;
let principal_types: HashSet<cedar_policy::EntityTypeName> = validated_tokens
.keys()
.map(|mapping| {
cedar_policy::EntityTypeName::from_str(mapping)
.map_err(|e| AuthorizeError::IdentifierParsing(e.into()))
})
.collect::<Result<_, _>>()?;
let action_uids = parse_action_uids(actions)?;
let resource_types = entity_data_to_type_names(resources)?;
Ok(self.config.policy_store.policies.get_matching_policies(
&principal_types,
&action_uids,
&resource_types,
))
}
pub(super) fn annotations_map<'a>(
&self,
ids: impl IntoIterator<Item = &'a PolicyId>,
) -> HashMap<String, String> {
self.config.policy_store.policies.annotations_map(ids)
}
pub(super) fn annotation_values<'a>(
&self,
ids: impl IntoIterator<Item = &'a PolicyId>,
key: &str,
) -> Vec<String> {
self.config
.policy_store
.policies
.annotation_values(ids, key)
}
pub(super) fn annotations_by_policy<'a>(
&self,
ids: impl IntoIterator<Item = &'a PolicyId>,
) -> HashMap<String, HashMap<String, String>> {
self.config.policy_store.policies.annotations_by_policy(ids)
}
}
fn entity_data_to_type_names(
entities: &[crate::EntityData],
) -> Result<HashSet<cedar_policy::EntityTypeName>, AuthorizeError> {
entities
.iter()
.map(|e| {
cedar_policy::EntityTypeName::from_str(&e.cedar_mapping.entity_type)
.map_err(|e| AuthorizeError::IdentifierParsing(e.into()))
})
.collect()
}
fn parse_action_uids(actions: &[String]) -> Result<HashSet<EntityUid>, AuthorizeError> {
actions
.iter()
.map(|a| EntityUid::from_str(a).map_err(Into::into))
.collect()
}
impl TrustedIssuerLoadingInfo for Authz {
fn is_trusted_issuer_loaded_by_name(&self, issuer_id: &str) -> bool {
self.config
.jwt_service
.is_trusted_issuer_loaded_by_name(issuer_id)
}
fn is_trusted_issuer_loaded_by_iss(&self, iss_claim: &str) -> bool {
self.config
.jwt_service
.is_trusted_issuer_loaded_by_iss(iss_claim)
}
fn total_issuers(&self) -> usize {
self.config.jwt_service.total_issuers()
}
fn loaded_trusted_issuers_count(&self) -> usize {
self.config.jwt_service.loaded_trusted_issuers_count()
}
fn loaded_trusted_issuer_ids(&self) -> HashSet<String> {
self.config.jwt_service.loaded_trusted_issuer_ids()
}
fn failed_trusted_issuer_ids(&self) -> HashSet<String> {
self.config.jwt_service.failed_trusted_issuer_ids()
}
}
fn calculate_elapsed_time(start_time: chrono::DateTime<Utc>) -> i64 {
let since_start = Utc::now().signed_duration_since(start_time);
since_start.num_microseconds().unwrap_or(
i64::MAX,
)
}
fn serialize_entities(entities: &Entities) -> serde_json::Value {
entities.to_json_value().unwrap_or(serde_json::Value::Null)
}
struct DecisionLogMetadata<'a> {
action: String,
resource: String,
principal: Vec<smol_str::SmolStr>,
tokens_logging_info: LogTokensInfo,
decision_diagnostics: &'a [Diagnostics],
decision_time: i64,
decision: bool,
pushed_data: Option<PushedDataInfo>,
batch_id: Option<Uuid>,
}
struct DebugLogMetadata<'a> {
action: String,
resource: String,
context: &'a serde_json::Value,
entities: &'a Entities,
debug_authz_info: Vec<AuthorizeInfo>,
decision: bool,
}
struct ExecuteAuthorizeParameters<'a> {
entities: &'a Entities,
principal: Option<EntityUid>,
action: EntityUid,
resource: EntityUid,
context: cedar_policy::Context,
}
struct UnsignedSetup {
principal: Option<Entity>,
built_entities: BuiltEntities,
}
struct MultiIssuerSetup {
validated_tokens: HashMap<String, Arc<crate::jwt::Token>>,
entities: MultiIssuerSetupEntities,
}
#[derive(Debug)]
pub(super) struct AuthorizeEntitiesData {
pub issuers: HashSet<Entity>,
pub tokens: HashMap<String, Entity>,
pub resource: Entity,
pub default_entities: DefaultEntities,
}
impl AuthorizeEntitiesData {
fn into_iter(self) -> impl Iterator<Item = Entity> {
let capacity = 1usize .saturating_add(self.issuers.len())
.saturating_add(self.tokens.len())
.saturating_add(self.default_entities.inner.len());
let mut merged_entities: HashMap<EntityUid, Entity> = HashMap::with_capacity(capacity);
merged_entities.insert(self.resource.uid(), self.resource);
merged_entities.extend(self.issuers.into_iter().map(|e| (e.uid(), e)));
merged_entities.extend(self.tokens.into_values().map(|e| (e.uid(), e)));
merged_entities.extend(
Arc::try_unwrap(self.default_entities.inner)
.unwrap_or_else(|arc| (*arc).clone())
.into_values()
.map(|e| (e.uid(), e)),
);
merged_entities.into_values()
}
pub(crate) fn entities(
self,
schema: Option<&cedar_policy::Schema>,
) -> Result<cedar_policy::Entities, Box<cedar_policy::entities_errors::EntitiesError>> {
Entities::from_entities(self.into_iter(), schema).map_err(Box::new)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
fn to_entity(json: serde_json::Value) -> Entity {
Entity::from_json_value(json, None).expect("entity from json")
}
fn default_entities(jsons: &[serde_json::Value]) -> DefaultEntities {
let inner: HashMap<EntityUid, Entity> = jsons
.iter()
.map(|j| {
let entity = to_entity(j.clone());
(entity.uid().clone(), entity)
})
.collect();
DefaultEntities {
inner: Arc::new(inner),
}
}
#[test]
fn default_takes_precedence_over_resource_on_uid_collision() {
let data = AuthorizeEntitiesData {
issuers: HashSet::new(),
tokens: HashMap::new(),
resource: to_entity(
json!({"uid": {"type": "Jans::Org", "id": "org1"}, "attrs": {"name": "evil", "is_admin": false}, "parents": []}),
),
default_entities: default_entities(&[
json!({"uid": {"type": "Jans::Org", "id": "org1"}, "attrs": {"name": "trusted", "is_admin": true}, "parents": []}),
]),
};
let ents = data.entities(None).expect("entities");
let uid: EntityUid = "Jans::Org::\"org1\"".parse().unwrap();
let entity = ents.get(&uid).expect("org1 entity");
let json = entity.to_json_value().expect("to_json");
assert_eq!(
json.pointer("/attrs/name").and_then(|v| v.as_str()),
Some("trusted"),
"default org name should override request value"
);
assert_eq!(
json.pointer("/attrs/is_admin")
.and_then(serde_json::Value::as_bool),
Some(true),
"default is_admin should override request false"
);
}
#[test]
fn default_takes_precedence_over_issuer_on_uid_collision() {
let mut issuers = HashSet::new();
issuers.insert(to_entity(json!({"uid": {"type": "Jans::Issuer", "id": "iss1"}, "attrs": {"trusted": false}, "parents": []})));
let data = AuthorizeEntitiesData {
issuers,
tokens: HashMap::new(),
resource: to_entity(
json!({"uid": {"type": "Jans::Resource", "id": "res1"}, "attrs": {}, "parents": []}),
),
default_entities: default_entities(&[
json!({"uid": {"type": "Jans::Issuer", "id": "iss1"}, "attrs": {"trusted": true}, "parents": []}),
]),
};
let ents = data.entities(None).expect("entities");
let uid: EntityUid = "Jans::Issuer::\"iss1\"".parse().unwrap();
let json = ents
.get(&uid)
.expect("issuer entity")
.to_json_value()
.expect("to_json");
assert_eq!(
json.pointer("/attrs/trusted")
.and_then(serde_json::Value::as_bool),
Some(true),
"default issuer trusted=true should override request false"
);
}
#[test]
fn default_takes_precedence_over_token_on_uid_collision() {
let mut tokens = HashMap::new();
tokens.insert(
"tok1".to_string(),
to_entity(json!({"uid": {"type": "Jans::access_token", "id": "tok1"}, "attrs": {"scope": "evil"}, "parents": []})),
);
let data = AuthorizeEntitiesData {
issuers: HashSet::new(),
tokens,
resource: to_entity(
json!({"uid": {"type": "Jans::Resource", "id": "res1"}, "attrs": {}, "parents": []}),
),
default_entities: default_entities(&[
json!({"uid": {"type": "Jans::access_token", "id": "tok1"}, "attrs": {"scope": "read"}, "parents": []}),
]),
};
let ents = data.entities(None).expect("entities");
let uid: EntityUid = "Jans::access_token::\"tok1\"".parse().unwrap();
let json = ents
.get(&uid)
.expect("token entity")
.to_json_value()
.expect("to_json");
assert_eq!(
json.pointer("/attrs/scope").and_then(|v| v.as_str()),
Some("read"),
"default token scope should override request value"
);
}
#[test]
fn unique_uids_all_present() {
let data = AuthorizeEntitiesData {
issuers: HashSet::new(),
tokens: HashMap::new(),
resource: to_entity(
json!({"uid": {"type": "Jans::Resource", "id": "res1"}, "attrs": {}, "parents": []}),
),
default_entities: default_entities(&[
json!({"uid": {"type": "Jans::Org", "id": "org1"}, "attrs": {}, "parents": []}),
]),
};
let ents = data.entities(None).expect("entities");
assert!(
ents.get(&"Jans::Resource::\"res1\"".parse().unwrap())
.is_some(),
"resource entity should be present when no UID collision"
);
assert!(
ents.get(&"Jans::Org::\"org1\"".parse().unwrap()).is_some(),
"default entity should be present when no UID collision"
);
assert_eq!(
ents.iter().count(),
2,
"both entities should be present with unique UIDs"
);
}
#[test]
fn empty_defaults_produces_only_request_entities() {
let data = AuthorizeEntitiesData {
issuers: HashSet::new(),
tokens: HashMap::new(),
resource: to_entity(
json!({"uid": {"type": "Jans::Resource", "id": "res1"}, "attrs": {}, "parents": []}),
),
default_entities: DefaultEntities::default(),
};
let ents = data.entities(None).expect("entities");
assert!(
ents.get(&"Jans::Resource::\"res1\"".parse().unwrap())
.is_some(),
"resource entity should be present with empty defaults"
);
assert_eq!(
ents.iter().count(),
1,
"only resource entity expected with empty defaults"
);
}
#[test]
fn defaults_win_when_both_resource_and_issuer_collide() {
let mut issuers = HashSet::new();
issuers.insert(to_entity(json!({"uid": {"type": "Jans::Group", "id": "admin"}, "attrs": {"role": "user"}, "parents": []})));
let data = AuthorizeEntitiesData {
issuers,
tokens: HashMap::new(),
resource: to_entity(
json!({"uid": {"type": "Jans::Org", "id": "org1"}, "attrs": {"name": "evil"}, "parents": []}),
),
default_entities: default_entities(&[
json!({"uid": {"type": "Jans::Org", "id": "org1"}, "attrs": {"name": "trusted"}, "parents": []}),
json!({"uid": {"type": "Jans::Group", "id": "admin"}, "attrs": {"role": "admin"}, "parents": []}),
]),
};
let ents = data.entities(None).expect("entities");
let org_uid: EntityUid = "Jans::Org::\"org1\"".parse().unwrap();
let org_json = ents
.get(&org_uid)
.expect("org entity")
.to_json_value()
.expect("to_json");
assert_eq!(
org_json.pointer("/attrs/name").and_then(|v| v.as_str()),
Some("trusted"),
"default org name should override request value in multi-collision test"
);
let group_uid: EntityUid = "Jans::Group::\"admin\"".parse().unwrap();
let group_json = ents
.get(&group_uid)
.expect("group entity")
.to_json_value()
.expect("to_json");
assert_eq!(
group_json.pointer("/attrs/role").and_then(|v| v.as_str()),
Some("admin"),
"default group role should override request value in multi-collision test"
);
}
}