use std::sync::Arc;
use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
use rocketmq_security_api::AuthorizationDecision;
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::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::runtime::ProviderRegistry;
use crate::AuthFailureKind;
use crate::AuthMetrics;
use crate::AuthOperation;
use crate::AuthServiceError;
use crate::AuthServiceResult;
use crate::RemotingAuthContext;
#[allow(async_fn_in_trait)]
pub trait AuthorizationProvider: Send + Sync {
fn initialize(&mut self, config: AuthConfig) -> AuthServiceResult<()>;
fn initialize_with_metadata(
&mut self,
config: AuthConfig,
#[allow(unused_variables)] metadata_service: Option<Box<dyn std::any::Any + Send + Sync>>,
) -> AuthServiceResult<()> {
self.initialize(config)
}
async fn authorize(&self, context: &DefaultAuthorizationContext) -> AuthServiceResult<AuthorizationDecision>;
#[allow(unused_variables)]
fn new_contexts_from_grpc_metadata(
&self,
metadata: &dyn std::any::Any,
message: &dyn std::any::Any,
) -> AuthServiceResult<Vec<DefaultAuthorizationContext>> {
Ok(Vec::new())
}
#[allow(unused_variables)]
fn new_contexts_from_remoting_command(
&self,
auth_context: &RemotingAuthContext,
command: &RemotingCommand,
) -> AuthServiceResult<Vec<DefaultAuthorizationContext>> {
Ok(Vec::new())
}
}
#[cfg(test)]
struct NoopAuthorizationProvider;
#[cfg(test)]
impl NoopAuthorizationProvider {
fn new() -> Self {
Self
}
}
#[cfg(test)]
impl Default for NoopAuthorizationProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
impl AuthorizationProvider for NoopAuthorizationProvider {
fn initialize(&mut self, _config: AuthConfig) -> AuthServiceResult<()> {
Ok(())
}
async fn authorize(&self, _context: &DefaultAuthorizationContext) -> AuthServiceResult<AuthorizationDecision> {
Ok(AuthorizationDecision::Allow)
}
fn new_contexts_from_grpc_metadata(
&self,
_metadata: &dyn std::any::Any,
_message: &dyn std::any::Any,
) -> AuthServiceResult<Vec<DefaultAuthorizationContext>> {
Ok(Vec::new())
}
fn new_contexts_from_remoting_command(
&self,
_auth_context: &RemotingAuthContext,
_command: &RemotingCommand,
) -> AuthServiceResult<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<crate::UserMetadataHandle>>,
authorization_metadata_provider: Option<Arc<crate::AclMetadataHandle>>,
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<crate::UserMetadataHandle>> {
self.authentication_metadata_provider.clone()
}
pub fn authorization_metadata_provider(&self) -> Option<Arc<crate::AclMetadataHandle>> {
self.authorization_metadata_provider.clone()
}
pub fn initialize_with_registry(
&mut self,
config: AuthConfig,
provider_registry: ProviderRegistry,
) -> AuthServiceResult<()> {
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, result: &AuthServiceResult<AuthorizationDecision>) {
use tracing::debug;
use tracing::info;
use tracing::warn;
let action_count = context.actions().len();
match result {
Ok(AuthorizationDecision::Allow) => {
debug!(outcome = "allow", action_count, "authorization decision")
}
Ok(AuthorizationDecision::Deny(denial)) => info!(
outcome = "deny",
denial = ?denial,
action_count,
"authorization decision"
),
Err(_) => warn!(outcome = "error", action_count, "authorization evaluation failed"),
}
}
}
impl Default for DefaultAuthorizationProvider {
fn default() -> Self {
Self::new()
}
}
#[allow(async_fn_in_trait)]
impl AuthorizationProvider for DefaultAuthorizationProvider {
fn initialize(&mut self, config: AuthConfig) -> AuthServiceResult<()> {
self.initialize_with_metadata(config, None)
}
fn initialize_with_metadata(
&mut self,
config: AuthConfig,
metadata_service: Option<Box<dyn std::any::Any + Send + Sync>>,
) -> AuthServiceResult<()> {
use tracing::debug;
if let Some(service) = metadata_service {
let registry = service.downcast_ref::<ProviderRegistry>().ok_or_else(|| {
AuthServiceError::new(AuthOperation::InitializeProvider, AuthFailureKind::Unsupported)
})?;
return self.initialize_with_registry(config, registry.clone());
}
debug!("Initializing DefaultAuthorizationProvider");
self.config = Some(config.clone());
self.metadata_service = None;
self.context_builder = Some(DefaultAuthorizationContextBuilder::new(config.clone()));
let authentication_metadata_provider =
LocalAuthenticationMetadataProvider::with_config(&config).map_err(|source| {
AuthServiceError::with_source(AuthOperation::InitializeProvider, AuthFailureKind::Unavailable, source)
})?;
self.authentication_metadata_provider = Some(crate::UserMetadataHandle::legacy(Arc::new(
authentication_metadata_provider,
)));
let mut authorization_metadata_provider = LocalAuthorizationMetadataProvider::new();
authorization_metadata_provider.initialize(config, None)?;
self.authorization_metadata_provider = Some(crate::AclMetadataHandle::legacy(Arc::new(
authorization_metadata_provider,
)));
self.metrics = AuthMetrics::default();
Ok(())
}
async fn authorize(&self, context: &DefaultAuthorizationContext) -> AuthServiceResult<AuthorizationDecision> {
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(AuthServiceError::invalid_context(
"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(AuthServiceError::invalid_context(
"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(AuthServiceError::invalid_context(
"No actions specified in authorization context".to_string(),
));
}
debug!(
action_count = context.actions().len(),
"authorization evaluation started"
);
let authentication_metadata_provider = self
.authentication_metadata_provider
.as_ref()
.ok_or_else(|| AuthServiceError::not_initialized("Authentication metadata provider is not configured"))?;
let authorization_metadata_provider = self
.authorization_metadata_provider
.as_ref()
.ok_or_else(|| AuthServiceError::not_initialized("Authorization metadata provider is not configured"))?;
let result: AuthServiceResult<AuthorizationDecision> = async {
let user_handler = UserAuthorizationHandler::new(authentication_metadata_provider.clone());
match user_handler.authorize_subject(context).await? {
Some(decision) => Ok(decision),
None => {
let acl_handler = AclAuthorizationHandler::new(authorization_metadata_provider.clone());
acl_handler.handle(context).await
}
}
}
.await;
self.audit_log(context, &result);
self.metrics
.record_authorization_result(matches!(&result, Ok(AuthorizationDecision::Allow)));
result
}
fn new_contexts_from_remoting_command(
&self,
auth_context: &RemotingAuthContext,
command: &RemotingCommand,
) -> AuthServiceResult<Vec<DefaultAuthorizationContext>> {
let builder = self
.context_builder
.as_ref()
.ok_or_else(|| AuthServiceError::not_initialized("Authorization context builder is not configured"))?;
builder.build_from_remoting(auth_context, command)
}
}
#[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;
#[tokio::test]
async fn test_noop_provider_always_allows() {
let mut provider = NoopAuthorizationProvider::new();
let config = AuthConfig::default();
assert!(provider.initialize(config).is_ok());
assert_eq!(
provider
.authorize(&DefaultAuthorizationContext::default())
.await
.unwrap(),
AuthorizationDecision::Allow
);
}
#[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;
let error = result.unwrap_err();
assert_eq!(error.operation(), AuthOperation::BuildContext);
assert_eq!(error.kind(), AuthFailureKind::InvalidInput);
}
#[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;
let error = result.unwrap_err();
assert_eq!(error.operation(), AuthOperation::BuildContext);
assert_eq!(error.kind(), AuthFailureKind::InvalidInput);
}
#[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;
let error = result.unwrap_err();
assert_eq!(error.operation(), AuthOperation::BuildContext);
assert_eq!(error.kind(), AuthFailureKind::InvalidInput);
}
#[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_security_api::Action::Pub]);
context.set_source_ip("127.0.0.1");
assert_eq!(
provider.authorize(&context).await.unwrap(),
AuthorizationDecision::Allow
);
}
#[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_security_api::Action::Pub]);
context.set_source_ip("127.0.0.1");
assert_eq!(
provider.authorize(&context).await.unwrap(),
AuthorizationDecision::Allow
);
}
#[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_security_api::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_security_api::Action::Pub]);
context.set_source_ip("127.0.0.1");
assert_eq!(
provider.authorize(&context).await.unwrap(),
AuthorizationDecision::Allow
);
}
#[tokio::test]
async fn test_default_provider_unknown_user_is_subject_denial() {
use crate::authorization::model::resource::Resource;
use rocketmq_security_api::AuthorizationDenial;
let mut provider = DefaultAuthorizationProvider::new();
provider.initialize(AuthConfig::default()).unwrap();
let mut context = DefaultAuthorizationContext::default();
context.set_subject("missing-user", SubjectType::User);
context.set_resource(Resource::of_topic("test-topic"));
context.set_actions(vec![rocketmq_security_api::Action::Pub]);
context.set_source_ip("127.0.0.1");
assert_eq!(
provider.authorize(&context).await.unwrap(),
AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown)
);
}
}