use std::sync::Arc;
use rocketmq_error::RocketMQError;
use rocketmq_remoting::protocol::remoting_command::RemotingCommand;
use crate::authentication::provider::LocalAuthenticationMetadataProvider;
use crate::authorization::builder::default_authorization_context_builder::DefaultAuthorizationContextBuilder;
use crate::authorization::builder::AuthorizationContextBuilder;
use crate::authorization::chain::AclAuthorizationHandler;
use crate::authorization::chain::AuthorizationHandler;
use crate::authorization::chain::UserAuthorizationDecision;
use crate::authorization::chain::UserAuthorizationHandler;
use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
use crate::authorization::metadata_provider::AuthorizationMetadataProvider;
use crate::authorization::metadata_provider::LocalAuthorizationMetadataProvider;
use crate::config::AuthConfig;
use crate::observability::AuthMetrics;
use crate::runtime::ProviderRegistry;
pub type AuthorizationResult<T> = Result<T, AuthorizationError>;
#[derive(Debug, Clone, thiserror::Error)]
pub enum AuthorizationError {
#[error("Authorization denied for subject '{subject}' on resource '{resource}': {reason}")]
PermissionDenied {
subject: String,
resource: String,
reason: String,
},
#[error("Policy evaluation failed: {0}")]
PolicyEvaluationFailed(String),
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Subject '{0}' not found")]
SubjectNotFound(String),
#[error("Resource '{0}' not found or invalid")]
ResourceNotFound(String),
#[error("Authorization provider not initialized: {0}")]
NotInitialized(String),
#[error("Internal authorization error: {0}")]
InternalError(String),
#[error("Metadata service error: {0}")]
MetadataServiceError(String),
#[error("Invalid authorization context: {0}")]
InvalidContext(String),
}
impl From<AuthorizationError> for RocketMQError {
fn from(error: AuthorizationError) -> Self {
let message = error.to_string();
match error {
AuthorizationError::PermissionDenied { .. } => RocketMQError::BrokerPermissionDenied { operation: message },
AuthorizationError::SubjectNotFound(_)
| AuthorizationError::ResourceNotFound(_)
| AuthorizationError::InvalidContext(_) => RocketMQError::illegal_argument(message),
AuthorizationError::ConfigurationError(_) | AuthorizationError::NotInitialized(_) => {
RocketMQError::auth_config_invalid("auth.authorization", message)
}
AuthorizationError::PolicyEvaluationFailed(_)
| AuthorizationError::InternalError(_)
| AuthorizationError::MetadataServiceError(_) => RocketMQError::Internal(message),
}
}
}
#[allow(async_fn_in_trait)]
pub trait AuthorizationProvider: Send + Sync {
fn initialize(&mut self, config: AuthConfig) -> AuthorizationResult<()>;
fn initialize_with_metadata(
&mut self,
config: AuthConfig,
#[allow(unused_variables)] metadata_service: Option<Box<dyn std::any::Any + Send + Sync>>,
) -> AuthorizationResult<()> {
self.initialize(config)
}
async fn authorize(&self, context: &DefaultAuthorizationContext) -> AuthorizationResult<()>;
#[allow(unused_variables)]
fn new_contexts_from_grpc_metadata(
&self,
metadata: &dyn std::any::Any,
message: &dyn std::any::Any,
) -> AuthorizationResult<Vec<DefaultAuthorizationContext>> {
Ok(Vec::new())
}
#[allow(unused_variables)]
fn new_contexts_from_remoting_command(
&self,
channel_context: &(dyn std::any::Any + Send + Sync),
command: &RemotingCommand,
) -> AuthorizationResult<Vec<DefaultAuthorizationContext>> {
Ok(Vec::new())
}
}
pub struct NoopAuthorizationProvider;
impl NoopAuthorizationProvider {
pub fn new() -> Self {
Self
}
}
impl Default for NoopAuthorizationProvider {
fn default() -> Self {
Self::new()
}
}
impl AuthorizationProvider for NoopAuthorizationProvider {
fn initialize(&mut self, _config: AuthConfig) -> AuthorizationResult<()> {
Ok(())
}
async fn authorize(&self, _context: &DefaultAuthorizationContext) -> AuthorizationResult<()> {
Ok(())
}
fn new_contexts_from_grpc_metadata(
&self,
_metadata: &dyn std::any::Any,
_message: &dyn std::any::Any,
) -> AuthorizationResult<Vec<DefaultAuthorizationContext>> {
Ok(Vec::new())
}
fn new_contexts_from_remoting_command(
&self,
_channel_context: &(dyn std::any::Any + Send + Sync),
_command: &RemotingCommand,
) -> AuthorizationResult<Vec<DefaultAuthorizationContext>> {
Ok(Vec::new())
}
}
pub struct DefaultAuthorizationProvider {
config: Option<AuthConfig>,
metadata_service: Option<Box<dyn std::any::Any + Send + Sync>>,
authentication_metadata_provider: Option<Arc<LocalAuthenticationMetadataProvider>>,
authorization_metadata_provider: Option<Arc<LocalAuthorizationMetadataProvider>>,
context_builder: Option<DefaultAuthorizationContextBuilder>,
metrics: AuthMetrics,
}
impl DefaultAuthorizationProvider {
pub fn new() -> Self {
Self {
config: None,
metadata_service: None,
authentication_metadata_provider: None,
authorization_metadata_provider: None,
context_builder: None,
metrics: AuthMetrics::default(),
}
}
pub fn authentication_metadata_provider(&self) -> Option<Arc<LocalAuthenticationMetadataProvider>> {
self.authentication_metadata_provider.clone()
}
pub fn authorization_metadata_provider(&self) -> Option<Arc<LocalAuthorizationMetadataProvider>> {
self.authorization_metadata_provider.clone()
}
pub fn initialize_with_registry(
&mut self,
config: AuthConfig,
provider_registry: ProviderRegistry,
) -> AuthorizationResult<()> {
self.config = Some(config.clone());
self.metadata_service = None;
self.context_builder = Some(DefaultAuthorizationContextBuilder::new(config));
self.authentication_metadata_provider = Some(provider_registry.authentication_metadata_provider());
self.authorization_metadata_provider = Some(provider_registry.authorization_metadata_provider());
self.metrics = provider_registry.metrics();
Ok(())
}
fn audit_log(&self, context: &DefaultAuthorizationContext, error: Option<&AuthorizationError>) {
use tracing::debug;
use tracing::info;
let subject_key = match context.subject_key() {
Some(key) => key,
None => return, };
let decision = if error.is_some() { "DENY" } else { "ALLOW" };
let actions = context
.actions()
.iter()
.map(|a| format!("{:?}", a))
.collect::<Vec<_>>()
.join(",");
let source_ip = context.source_ip().unwrap_or("unknown");
let resource = context
.resource()
.map(|r| format!("{:?}", r))
.unwrap_or_else(|| "unknown".to_string());
let rpc_code = context.rpc_code().unwrap_or("unknown");
if error.is_none() {
debug!(
"[AUTHORIZATION] Subject = {} is {} Action = {} from sourceIp = {} on resource = {} for request = {}.",
subject_key, decision, actions, source_ip, resource, rpc_code
);
} else {
info!(
"[AUTHORIZATION] Subject = {} is {} Action = {} from sourceIp = {} on resource = {} for request = {}.",
subject_key, decision, actions, source_ip, resource, rpc_code
);
}
}
}
impl Default for DefaultAuthorizationProvider {
fn default() -> Self {
Self::new()
}
}
#[allow(async_fn_in_trait)]
impl AuthorizationProvider for DefaultAuthorizationProvider {
fn initialize(&mut self, config: AuthConfig) -> AuthorizationResult<()> {
self.initialize_with_metadata(config, None)
}
fn initialize_with_metadata(
&mut self,
config: AuthConfig,
metadata_service: Option<Box<dyn std::any::Any + Send + Sync>>,
) -> AuthorizationResult<()> {
use tracing::debug;
debug!("Initializing DefaultAuthorizationProvider");
self.config = Some(config.clone());
self.metadata_service = metadata_service;
self.context_builder = Some(DefaultAuthorizationContextBuilder::new(config.clone()));
let authentication_metadata_provider = LocalAuthenticationMetadataProvider::with_config(&config)
.map_err(|error| AuthorizationError::ConfigurationError(error.to_string()))?;
self.authentication_metadata_provider = Some(Arc::new(authentication_metadata_provider));
let mut authorization_metadata_provider = LocalAuthorizationMetadataProvider::new();
authorization_metadata_provider.initialize(config, None)?;
self.authorization_metadata_provider = Some(Arc::new(authorization_metadata_provider));
self.metrics = AuthMetrics::default();
Ok(())
}
async fn authorize(&self, context: &DefaultAuthorizationContext) -> AuthorizationResult<()> {
use tracing::debug;
use tracing::warn;
if context.subject_key().is_none() {
warn!("Authorization context missing subject");
self.metrics.record_authorization_result(false);
return Err(AuthorizationError::InvalidContext(
"Missing subject in authorization context".to_string(),
));
}
if context.resource().is_none() {
warn!("Authorization context missing resource");
self.metrics.record_authorization_result(false);
return Err(AuthorizationError::InvalidContext(
"Missing resource in authorization context".to_string(),
));
}
if context.actions().is_empty() {
warn!("Authorization context has no actions");
self.metrics.record_authorization_result(false);
return Err(AuthorizationError::InvalidContext(
"No actions specified in authorization context".to_string(),
));
}
debug!(
"Authorizing subject={:?} resource={:?} actions={:?}",
context.subject_key(),
context.resource(),
context.actions()
);
let authentication_metadata_provider = self.authentication_metadata_provider.as_ref().ok_or_else(|| {
AuthorizationError::NotInitialized("Authentication metadata provider is not configured".to_string())
})?;
let authorization_metadata_provider = self.authorization_metadata_provider.as_ref().ok_or_else(|| {
AuthorizationError::NotInitialized("Authorization metadata provider is not configured".to_string())
})?;
let result = async {
let user_handler = UserAuthorizationHandler::new(authentication_metadata_provider.clone());
match user_handler
.authorize_subject(context)
.await
.map_err(|error| map_handler_error(context, error))?
{
UserAuthorizationDecision::SuperUser => Ok(()),
UserAuthorizationDecision::Continue => {
let acl_handler = AclAuthorizationHandler::new(authorization_metadata_provider.clone());
acl_handler
.handle(context)
.await
.map_err(|error| map_handler_error(context, error))
}
}
}
.await;
self.audit_log(context, result.as_ref().err());
self.metrics.record_authorization_result(result.is_ok());
result
}
fn new_contexts_from_remoting_command(
&self,
channel_context: &(dyn std::any::Any + Send + Sync),
command: &RemotingCommand,
) -> AuthorizationResult<Vec<DefaultAuthorizationContext>> {
let builder = self.context_builder.as_ref().ok_or_else(|| {
AuthorizationError::NotInitialized("Authorization context builder is not configured".to_string())
})?;
builder.build_from_remoting(channel_context, command)
}
}
fn map_handler_error(
context: &DefaultAuthorizationContext,
error: rocketmq_error::RocketMQError,
) -> AuthorizationError {
AuthorizationError::PermissionDenied {
subject: context.subject_key().unwrap_or("unknown").to_string(),
resource: context.resource_key().unwrap_or_else(|| "unknown".to_string()),
reason: error.to_string(),
}
}
#[cfg(test)]
mod tests {
use cheetah_string::CheetahString;
use tempfile::TempDir;
use super::*;
use crate::authentication::enums::subject_type::SubjectType;
use crate::authentication::provider::authentication_metadata_provider::AuthenticationMetadataProvider;
use crate::authorization::metadata_provider::AuthorizationMetadataProvider;
#[tokio::test]
async fn test_noop_provider_always_allows() {
let mut provider = NoopAuthorizationProvider::new();
let config = AuthConfig::default();
assert!(provider.initialize(config).is_ok());
}
#[test]
fn test_authorization_error_display() {
let error = AuthorizationError::PermissionDenied {
subject: "user:alice".to_string(),
resource: "topic:test".to_string(),
reason: "insufficient permissions".to_string(),
};
let msg = format!("{}", error);
assert!(msg.contains("alice"));
assert!(msg.contains("test"));
assert!(msg.contains("insufficient permissions"));
}
#[test]
fn test_authorization_error_variants() {
let errors = vec![
AuthorizationError::SubjectNotFound("user:alice".to_string()),
AuthorizationError::ResourceNotFound("topic:test".to_string()),
AuthorizationError::PolicyEvaluationFailed("invalid policy".to_string()),
AuthorizationError::ConfigurationError("missing config".to_string()),
AuthorizationError::NotInitialized("provider not ready".to_string()),
AuthorizationError::InternalError("unexpected error".to_string()),
AuthorizationError::MetadataServiceError("db connection failed".to_string()),
AuthorizationError::InvalidContext("missing subject".to_string()),
];
for error in errors {
let _msg = format!("{}", error);
let _debug = format!("{:?}", error);
}
}
#[test]
fn test_authorization_error_rocketmq_mapping_categories() {
let denied = RocketMQError::from(AuthorizationError::PermissionDenied {
subject: "user:alice".to_string(),
resource: "topic:test".to_string(),
reason: "insufficient permissions".to_string(),
});
assert!(matches!(denied, RocketMQError::BrokerPermissionDenied { .. }));
let invalid = RocketMQError::from(AuthorizationError::InvalidContext("missing subject".to_string()));
assert!(matches!(invalid, RocketMQError::IllegalArgument(_)));
let config = RocketMQError::from(AuthorizationError::ConfigurationError("missing config".to_string()));
assert!(matches!(
config,
RocketMQError::AuthConfigInvalid {
key: "auth.authorization",
..
}
));
let internal = RocketMQError::from(AuthorizationError::InternalError("unexpected error".to_string()));
assert!(matches!(internal, RocketMQError::Internal(_)));
}
#[tokio::test]
async fn test_default_provider_initialization() {
let mut provider = DefaultAuthorizationProvider::new();
let config = AuthConfig::default();
let result = provider.initialize(config);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_default_provider_authorize_missing_subject() {
let mut provider = DefaultAuthorizationProvider::new();
provider.initialize(AuthConfig::default()).unwrap();
let context = DefaultAuthorizationContext::default();
let result = provider.authorize(&context).await;
assert!(result.is_err());
match result.unwrap_err() {
AuthorizationError::InvalidContext(msg) => {
assert!(msg.contains("subject"));
}
_ => panic!("Expected InvalidContext error"),
}
}
#[tokio::test]
async fn test_default_provider_authorize_missing_resource() {
use crate::authentication::enums::subject_type::SubjectType;
let mut provider = DefaultAuthorizationProvider::new();
provider.initialize(AuthConfig::default()).unwrap();
let mut context = DefaultAuthorizationContext::default();
context.set_subject("user:test", SubjectType::User);
let result = provider.authorize(&context).await;
assert!(result.is_err());
match result.unwrap_err() {
AuthorizationError::InvalidContext(msg) => {
assert!(msg.contains("resource"));
}
_ => panic!("Expected InvalidContext error"),
}
}
#[tokio::test]
async fn test_default_provider_authorize_missing_actions() {
use crate::authentication::enums::subject_type::SubjectType;
use crate::authorization::model::resource::Resource;
let mut provider = DefaultAuthorizationProvider::new();
provider.initialize(AuthConfig::default()).unwrap();
let mut context = DefaultAuthorizationContext::default();
context.set_subject("user:test", SubjectType::User);
context.set_resource(Resource::of_topic("test-topic"));
let result = provider.authorize(&context).await;
assert!(result.is_err());
match result.unwrap_err() {
AuthorizationError::InvalidContext(msg) => {
assert!(msg.contains("actions"));
}
_ => panic!("Expected InvalidContext error"),
}
}
#[test]
fn test_default_provider_default_construction() {
let provider1 = DefaultAuthorizationProvider::new();
let provider2 = DefaultAuthorizationProvider::default();
assert!(provider1.config.is_none());
assert!(provider2.config.is_none());
}
#[tokio::test]
async fn test_default_provider_super_user_bypass() {
use crate::authentication::enums::user_status::UserStatus;
use crate::authentication::enums::user_type::UserType;
use crate::authorization::model::resource::Resource;
let mut provider = DefaultAuthorizationProvider::new();
provider.initialize(AuthConfig::default()).unwrap();
let auth_provider = provider.authentication_metadata_provider().unwrap();
let mut user = crate::authentication::model::user::User::of_with_type("alice", "secret", UserType::Super);
user.set_user_status(UserStatus::Enable);
auth_provider.create_user(user).await.unwrap();
let mut context = DefaultAuthorizationContext::default();
context.set_subject("alice", SubjectType::User);
context.set_resource(Resource::of_topic("test-topic"));
context.set_actions(vec![rocketmq_common::common::action::Action::Pub]);
context.set_source_ip("127.0.0.1");
assert!(provider.authorize(&context).await.is_ok());
}
#[tokio::test]
async fn default_provider_loads_persisted_authentication_users() {
use crate::authentication::enums::user_status::UserStatus;
use crate::authentication::enums::user_type::UserType;
use crate::authorization::model::resource::Resource;
let temp = TempDir::new().unwrap();
let config = AuthConfig {
auth_config_path: CheetahString::from_string(temp.path().join("auth.yml").to_string_lossy().into_owned()),
..AuthConfig::default()
};
let mut seed_provider = LocalAuthenticationMetadataProvider::new();
seed_provider.initialize(config.clone(), None).await.unwrap();
let mut user = crate::authentication::model::user::User::of_with_type("persisted", "secret", UserType::Super);
user.set_user_status(UserStatus::Enable);
seed_provider.create_user(user).await.unwrap();
let mut provider = DefaultAuthorizationProvider::new();
provider.initialize(config).unwrap();
let mut context = DefaultAuthorizationContext::default();
context.set_subject("persisted", SubjectType::User);
context.set_resource(Resource::of_topic("test-topic"));
context.set_actions(vec![rocketmq_common::common::action::Action::Pub]);
context.set_source_ip("127.0.0.1");
assert!(provider.authorize(&context).await.is_ok());
}
#[tokio::test]
async fn test_default_provider_acl_authorization() {
use crate::authentication::enums::user_status::UserStatus;
use crate::authentication::enums::user_type::UserType;
use crate::authorization::enums::decision::Decision;
use crate::authorization::model::acl::Acl;
use crate::authorization::model::policy::Policy;
use crate::authorization::model::resource::Resource;
let mut provider = DefaultAuthorizationProvider::new();
provider.initialize(AuthConfig::default()).unwrap();
let auth_provider = provider.authentication_metadata_provider().unwrap();
let mut user = crate::authentication::model::user::User::of_with_type("alice", "secret", UserType::Normal);
user.set_user_status(UserStatus::Enable);
auth_provider.create_user(user).await.unwrap();
let acl_provider = provider.authorization_metadata_provider().unwrap();
let resource = Resource::of_topic("test-topic");
let acl = Acl::of(
"alice",
SubjectType::User,
Policy::of(
vec![resource.clone()],
vec![rocketmq_common::common::action::Action::Pub],
None,
Decision::Allow,
),
);
acl_provider.create_acl(acl).await.unwrap();
let mut context = DefaultAuthorizationContext::default();
context.set_subject("alice", SubjectType::User);
context.set_resource(resource);
context.set_actions(vec![rocketmq_common::common::action::Action::Pub]);
context.set_source_ip("127.0.0.1");
assert!(provider.authorize(&context).await.is_ok());
}
}