use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::OnceLock;
use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
use crate::authorization::builder::default_authorization_context_builder::DefaultAuthorizationContextBuilder;
use crate::authorization::builder::AuthorizationContextBuilder;
use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
use crate::authorization::evaluator::AuthorizationEvaluator;
use crate::authorization::manager::metadata_manager_impl::AuthorizationMetadataManagerImpl;
use crate::authorization::metadata_provider::AuthorizationMetadataProvider;
use crate::authorization::metadata_provider::LocalAuthorizationMetadataProvider;
use crate::authorization::provider::AuthorizationProvider;
use crate::authorization::provider::DefaultAuthorizationProvider;
use crate::authorization::strategy::AuthorizationStrategy;
use crate::authorization::strategy::StatefulAuthorizationStrategy;
use crate::authorization::strategy::StatelessAuthorizationStrategy;
use crate::config::AuthConfig;
use crate::AuthFailureKind;
use crate::AuthOperation;
use crate::AuthServiceError;
use crate::AuthServiceResult;
static INSTANCE_CACHE: OnceLock<Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>> = OnceLock::new();
const PROVIDER_PREFIX: &str = "PROVIDER_";
const METADATA_PROVIDER_PREFIX: &str = "METADATA_PROVIDER_";
const EVALUATOR_PREFIX: &str = "EVALUATOR_";
pub struct AuthorizationFactory;
impl AuthorizationFactory {
pub fn get_provider(config: &AuthConfig) -> AuthServiceResult<Arc<DefaultAuthorizationProvider>> {
if !is_blank_or_supported(
config.authorization_provider.as_str(),
&["DefaultAuthorizationProvider", "default"],
) {
return Err(unsupported(
"authorizationProvider",
config.authorization_provider.as_str(),
));
}
let key = format!("{}{}", PROVIDER_PREFIX, config.config_name);
Self::compute_if_absent(&key, || {
let provider = new_initialized_default_authorization_provider(config.clone(), None)?;
Ok(Arc::new(provider) as Arc<dyn Any + Send + Sync>)
})
.and_then(|value| {
value
.downcast::<DefaultAuthorizationProvider>()
.map_err(|_| AuthServiceError::new(AuthOperation::Initialize, AuthFailureKind::Internal))
})
}
pub fn get_metadata_provider(
config: &AuthConfig,
) -> AuthServiceResult<Option<Arc<LocalAuthorizationMetadataProvider>>> {
if config.authorization_metadata_provider.is_empty() {
return Ok(None);
}
if !is_supported(
config.authorization_metadata_provider.as_str(),
&["LocalAuthorizationMetadataProvider", "local"],
) {
return Err(unsupported(
"authorizationMetadataProvider",
config.authorization_metadata_provider.as_str(),
));
}
let key = format!("{}{}", METADATA_PROVIDER_PREFIX, config.config_name);
Self::compute_if_absent(&key, || {
let mut provider = LocalAuthorizationMetadataProvider::new();
provider.initialize(config.clone(), None)?;
Ok(Arc::new(provider) as Arc<dyn Any + Send + Sync>)
})
.and_then(|value| {
value
.downcast::<LocalAuthorizationMetadataProvider>()
.map_err(|_| AuthServiceError::new(AuthOperation::Initialize, AuthFailureKind::Internal))
})
.map(Some)
}
pub fn get_metadata_manager(config: &AuthConfig) -> AuthServiceResult<AuthorizationMetadataManagerImpl> {
AuthorizationMetadataManagerImpl::from_config(config)
}
pub fn get_strategy(
config: &AuthConfig,
metadata_service: Option<Box<dyn Any + Send + Sync>>,
) -> AuthServiceResult<Box<dyn AuthorizationStrategy>> {
let strategy = config.authorization_strategy.as_str();
if strategy.trim().is_empty()
|| strategy.ends_with("StatelessAuthorizationStrategy")
|| strategy.eq_ignore_ascii_case("stateless")
{
return StatelessAuthorizationStrategy::new(config.clone(), metadata_service)
.map(|strategy| Box::new(strategy) as Box<dyn AuthorizationStrategy>);
}
if strategy.ends_with("StatefulAuthorizationStrategy") || strategy.eq_ignore_ascii_case("stateful") {
return StatefulAuthorizationStrategy::new(config.clone(), metadata_service)
.map(|strategy| Box::new(strategy) as Box<dyn AuthorizationStrategy>);
}
Err(AuthServiceError::configuration_error(format!(
"Unsupported authorization strategy: {strategy}"
)))
}
pub fn get_evaluator(
config: &AuthConfig,
) -> AuthServiceResult<Arc<AuthorizationEvaluator<Box<dyn AuthorizationStrategy>>>> {
let key = format!("{}{}", EVALUATOR_PREFIX, config.config_name);
Self::compute_if_absent(&key, || {
let strategy = Self::get_strategy(config, None)?;
Ok(Arc::new(AuthorizationEvaluator::new(strategy)) as Arc<dyn Any + Send + Sync>)
})
.and_then(|value| {
value
.downcast::<AuthorizationEvaluator<Box<dyn AuthorizationStrategy>>>()
.map_err(|_| AuthServiceError::configuration_error("Failed to downcast authorization evaluator"))
})
}
pub fn new_contexts_from_command(
config: &AuthConfig,
auth_context: &crate::RemotingAuthContext,
command: &RemotingCommand,
) -> AuthServiceResult<Option<Vec<DefaultAuthorizationContext>>> {
let _provider = Self::get_provider(config)?;
let builder = DefaultAuthorizationContextBuilder::new(config.clone());
builder.build_from_remoting(auth_context, command).map(Some)
}
fn compute_if_absent<F>(key: &str, factory: F) -> AuthServiceResult<Arc<dyn Any + Send + Sync>>
where
F: FnOnce() -> AuthServiceResult<Arc<dyn Any + Send + Sync>>,
{
let cache = INSTANCE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
{
let guard = cache
.lock()
.map_err(|_| AuthServiceError::new(AuthOperation::Initialize, AuthFailureKind::InvalidConfiguration))?;
if let Some(value) = guard.get(key) {
return Ok(Arc::clone(value));
}
}
let value = factory()?;
let mut guard = cache
.lock()
.map_err(|_| AuthServiceError::new(AuthOperation::Initialize, AuthFailureKind::InvalidConfiguration))?;
if let Some(value) = guard.get(key) {
return Ok(Arc::clone(value));
}
guard.insert(key.to_string(), Arc::clone(&value));
Ok(value)
}
}
impl AuthorizationStrategy for Box<dyn AuthorizationStrategy> {
fn evaluate<'a>(
&'a self,
context: &'a crate::authorization::context::default_authorization_context::DefaultAuthorizationContext,
) -> crate::authorization::strategy::AuthorizationFuture<'a> {
self.as_ref().evaluate(context)
}
}
fn is_blank_or_supported(configured: &str, supported: &[&str]) -> bool {
configured.trim().is_empty() || is_supported(configured, supported)
}
fn is_supported(configured: &str, supported: &[&str]) -> bool {
supported
.iter()
.any(|value| configured.eq_ignore_ascii_case(value) || configured.ends_with(value))
}
fn unsupported(key: &'static str, configured: &str) -> AuthServiceError {
let _ = (key, configured);
AuthServiceError::new(AuthOperation::Initialize, AuthFailureKind::Unsupported)
}
fn new_initialized_default_authorization_provider(
config: AuthConfig,
metadata_service: Option<Box<dyn Any + Send + Sync>>,
) -> AuthServiceResult<DefaultAuthorizationProvider> {
let mut provider = DefaultAuthorizationProvider::new();
provider.initialize_with_metadata(config, metadata_service)?;
Ok(provider)
}
#[cfg(test)]
mod tests {
use cheetah_string::CheetahString;
use rocketmq_protocol::code::request_code::RequestCode;
use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
use rocketmq_security_api::Action;
use super::*;
fn config(name: &str) -> AuthConfig {
AuthConfig {
config_name: CheetahString::from(name),
cluster_name: CheetahString::from_static_str("DefaultCluster"),
..AuthConfig::default()
}
}
#[test]
fn get_provider_caches_by_config_name() {
let config = config("authorization-factory-cache");
let first = AuthorizationFactory::get_provider(&config).unwrap();
let second = AuthorizationFactory::get_provider(&config).unwrap();
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn new_contexts_from_command_uses_default_builder() {
let config = config("authorization-factory-contexts");
let mut fields = HashMap::new();
fields.insert(
CheetahString::from_static_str("AccessKey"),
CheetahString::from_static_str("alice"),
);
fields.insert(
CheetahString::from_static_str("topic"),
CheetahString::from_static_str("TopicA"),
);
let command =
RemotingCommand::create_remoting_command(RequestCode::SendMessage.to_i32()).set_ext_fields(fields);
let contexts = AuthorizationFactory::new_contexts_from_command(
&config,
&crate::RemotingAuthContext::embedded("test-session"),
&command,
)
.unwrap()
.unwrap();
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].subject_key(), Some("User:alice"));
assert_eq!(contexts[0].resource_key(), Some("Topic:TopicA".to_string()));
assert_eq!(contexts[0].actions(), &[Action::Pub]);
}
}