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::{BuiltEntitiesUnsigned, EntityBuilder};
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};
use chrono::Utc;
use metrics::MetricsCollector;
use request::{AuthorizeMultiIssuerRequest, 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,
}
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 validated_tokens = self
.config
.jwt_service
.validate_multi_issuer_tokens(&request.tokens)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
let entities_data = self
.config
.entity_builder
.build_multi_issuer_entities(
&validated_tokens,
&request.resource,
self.config.log_service.as_ref(),
)
.map_err(|e| {
self.config.metrics.record_error(&e);
self.config.metrics.record_authz_error();
AuthorizeError::MultiIssuerEntity(e)
})?;
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!({})),
&entities_data.tokens,
schema_ref,
&action,
pushed_data,
)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
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,
},
);
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_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 BuiltEntitiesUnsigned {
principal,
resource,
built_entities,
} = self
.config
.entity_builder
.build_entities_unsigned(request)
.inspect_err(|e| {
self.config.metrics.record_error(e);
self.config.metrics.record_authz_error();
})?;
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,
},
);
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);
}
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)
}
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) {
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 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);
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(),
});
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,
))
}
}
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>,
}
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,
}
#[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"
);
}
}