use std::any::Any;
use std::collections::HashSet;
use std::sync::Arc;
use tracing::debug;
use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
use crate::authorization::factory::AuthorizationFactory;
use crate::authorization::provider::AuthorizationError;
use crate::authorization::provider::AuthorizationProvider;
use crate::authorization::provider::DefaultAuthorizationProvider;
use crate::config::AuthConfig;
pub type StrategyResult<T> = Result<T, AuthorizationError>;
pub trait AuthorizationStrategy: Send + Sync {
fn evaluate(&self, context: &DefaultAuthorizationContext) -> StrategyResult<()>;
}
pub struct AbstractAuthorizationStrategy {
auth_config: AuthConfig,
authorization_whitelist: HashSet<String>,
authorization_provider: Option<Arc<DefaultAuthorizationProvider>>,
}
impl AbstractAuthorizationStrategy {
pub fn new(auth_config: AuthConfig, _metadata_service: Option<Box<dyn Any + Send + Sync>>) -> StrategyResult<Self> {
let mut authorization_whitelist = HashSet::new();
let whitelist_str = auth_config.authorization_whitelist.as_str();
if !whitelist_str.is_empty() {
for rpc_code in whitelist_str.split(',') {
let trimmed = rpc_code.trim();
if !trimmed.is_empty() {
authorization_whitelist.insert(trimmed.to_string());
debug!("Added RPC code '{}' to authorization whitelist", trimmed);
}
}
}
debug!(
"AbstractAuthorizationStrategy initialized with {} whitelisted RPC codes",
authorization_whitelist.len()
);
let authorization_provider = Some(
AuthorizationFactory::get_provider(&auth_config)
.map_err(|error| AuthorizationError::ConfigurationError(error.to_string()))?,
);
Ok(Self {
auth_config,
authorization_whitelist,
authorization_provider,
})
}
pub async fn do_evaluate(&self, context: &DefaultAuthorizationContext) -> StrategyResult<()> {
if context.subject().is_none() {
debug!("Authorization skipped: context has no subject");
return Ok(());
}
if !self.auth_config.authorization_enabled {
debug!("Authorization disabled in configuration, allowing access");
return Ok(());
}
if let Some(rpc_code) = context.rpc_code() {
if self.authorization_whitelist.contains(rpc_code) {
debug!("RPC code '{}' is whitelisted, allowing access", rpc_code);
return Ok(());
}
}
debug!(
"Authorization evaluation for subject: {:?}, resource: {:?}, actions: {:?}",
context.subject().map(|s| s.subject_key()),
context.resource(),
context.actions()
);
let provider = self.authorization_provider.as_ref().ok_or_else(|| {
AuthorizationError::NotInitialized("authorization strategy does not have a concrete provider".to_owned())
})?;
provider.authorize(context).await
}
pub fn auth_config(&self) -> &AuthConfig {
&self.auth_config
}
pub fn authorization_whitelist(&self) -> &HashSet<String> {
&self.authorization_whitelist
}
pub fn has_authorization_provider(&self) -> bool {
self.authorization_provider.is_some()
}
pub fn is_whitelisted(&self, rpc_code: &str) -> bool {
self.authorization_whitelist.contains(rpc_code)
}
}
#[cfg(test)]
mod tests {
use cheetah_string::CheetahString;
use rocketmq_common::common::action::Action;
use super::*;
use crate::authentication::enums::subject_type::SubjectType;
use crate::authorization::model::resource::Resource;
fn create_test_config(enabled: bool, whitelist: &str) -> AuthConfig {
AuthConfig {
config_name: CheetahString::from("test"),
cluster_name: CheetahString::from("test-cluster"),
auth_config_path: CheetahString::from("/tmp/auth"),
acl_file: CheetahString::new(),
acl_file_watch_enabled: false,
acl_file_watch_interval_millis: 5_000,
authentication_enabled: false,
authentication_provider: CheetahString::new(),
authentication_metadata_provider: CheetahString::new(),
authentication_strategy: CheetahString::new(),
authentication_whitelist: CheetahString::new(),
init_authentication_user: CheetahString::new(),
inner_client_authentication_credentials: CheetahString::new(),
signature_algorithm: Default::default(),
request_timestamp_expired_millis: 0,
authorization_enabled: enabled,
authorization_provider: CheetahString::new(),
authorization_metadata_provider: CheetahString::new(),
authorization_strategy: CheetahString::new(),
authorization_whitelist: CheetahString::from(whitelist),
migrate_auth_from_v1_enabled: false,
user_cache_max_num: 1000,
user_cache_expired_second: 300,
user_cache_refresh_second: 60,
acl_cache_max_num: 1000,
acl_cache_expired_second: 300,
acl_cache_refresh_second: 60,
stateful_authentication_cache_max_num: 1000,
stateful_authentication_cache_expired_second: 300,
stateful_authorization_cache_max_num: 1000,
stateful_authorization_cache_expired_second: 300,
}
}
#[test]
fn test_whitelist_parsing() {
let config = create_test_config(true, "10,11,12");
let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
assert_eq!(strategy.authorization_whitelist().len(), 3);
assert!(strategy.is_whitelisted("10"));
assert!(strategy.is_whitelisted("11"));
assert!(strategy.is_whitelisted("12"));
assert!(!strategy.is_whitelisted("13"));
}
#[test]
fn test_whitelist_with_spaces() {
let config = create_test_config(true, " 10 , 11 , 12 ");
let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
assert_eq!(strategy.authorization_whitelist().len(), 3);
assert!(strategy.is_whitelisted("10"));
assert!(strategy.is_whitelisted("11"));
assert!(strategy.is_whitelisted("12"));
}
#[test]
fn test_empty_whitelist() {
let config = create_test_config(true, "");
let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
assert_eq!(strategy.authorization_whitelist().len(), 0);
assert!(!strategy.is_whitelisted("10"));
}
#[test]
fn test_disabled_authorization() {
let config = create_test_config(false, "");
let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
assert!(!strategy.auth_config().authorization_enabled);
}
#[test]
fn test_provider_is_initialized() {
let config = create_test_config(true, "");
let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
assert!(strategy.has_authorization_provider());
}
#[tokio::test]
async fn test_do_evaluate_disabled() {
let config = create_test_config(false, "");
let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
let context = DefaultAuthorizationContext::default();
let result = strategy.do_evaluate(&context).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_do_evaluate_delegates_to_provider() {
let config = create_test_config(true, "");
let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
let context = DefaultAuthorizationContext::of(
"alice",
SubjectType::User,
Resource::of_topic("topic-a"),
Action::Get,
"127.0.0.1",
);
let result = strategy.do_evaluate(&context).await;
assert!(matches!(result, Err(AuthorizationError::PermissionDenied { .. })));
}
}