use std::cmp::Ordering;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use rocketmq_common::common::resource::resource_pattern::ResourcePattern;
use rocketmq_common::common::resource::resource_type::ResourceType;
use rocketmq_error::RocketMQError;
use super::handler::AuthorizationHandler;
use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
use crate::authorization::enums::decision::Decision;
use crate::authorization::enums::policy_type::PolicyType;
use crate::authorization::metadata_provider::AuthorizationMetadataProvider;
use crate::authorization::model::acl::Acl;
use crate::authorization::model::environment::Environment;
use crate::authorization::model::policy::Policy;
use crate::authorization::model::policy_entry::PolicyEntry;
pub struct AclAuthorizationHandler<P: AuthorizationMetadataProvider> {
metadata_provider: Arc<P>,
}
impl<P: AuthorizationMetadataProvider> AclAuthorizationHandler<P> {
pub fn new(metadata_provider: Arc<P>) -> Self {
Self { metadata_provider }
}
async fn match_policy_entries(&self, context: &DefaultAuthorizationContext, acl: &Acl) -> Option<PolicyEntry> {
let mut matched_entries = Vec::new();
for policy in acl
.policies()
.iter()
.filter(|policy| policy.policy_type() == PolicyType::Custom)
{
if let Some(mut entries) = self.match_policy_entries_from_policy(context, policy) {
matched_entries.append(&mut entries);
}
}
if matched_entries.is_empty() {
for policy in acl
.policies()
.iter()
.filter(|policy| policy.policy_type() == PolicyType::Default)
{
if let Some(mut entries) = self.match_policy_entries_from_policy(context, policy) {
matched_entries.append(&mut entries);
}
}
}
if matched_entries.is_empty() {
return None;
}
matched_entries.sort_by(|a, b| self.compare_policy_entries(a, b));
matched_entries.into_iter().next()
}
fn match_policy_entries_from_policy(
&self,
context: &DefaultAuthorizationContext,
policy: &Policy,
) -> Option<Vec<PolicyEntry>> {
let entries = policy.entries();
if entries.is_empty() {
return None;
}
let resource_binding = context.resource();
let resource = resource_binding.as_ref()?;
let actions = context.actions();
let source_ip_binding = context.source_ip();
let source_ip = source_ip_binding.unwrap_or("");
let environment = Environment::of(source_ip).unwrap_or_default();
let matched: Vec<PolicyEntry> = entries
.iter()
.filter(|entry| entry.is_match_resource(resource))
.filter(|entry| entry.is_match_action(actions))
.filter(|entry| entry.is_match_environment(&environment))
.cloned()
.collect();
if matched.is_empty() {
None
} else {
Some(matched)
}
}
fn compare_policy_entries(&self, o1: &PolicyEntry, o2: &PolicyEntry) -> Ordering {
let r1 = o1.resource();
let r2 = o2.resource();
if r1.resource_type != r2.resource_type {
if r1.resource_type == ResourceType::Any {
return Ordering::Greater; }
if r2.resource_type == ResourceType::Any {
return Ordering::Less; }
}
if r1.resource_pattern != r2.resource_pattern {
return match (r1.resource_pattern, r2.resource_pattern) {
(ResourcePattern::Literal, _) => Ordering::Less, (_, ResourcePattern::Literal) => Ordering::Greater,
(ResourcePattern::Prefixed, ResourcePattern::Any) => Ordering::Less,
(ResourcePattern::Any, ResourcePattern::Prefixed) => Ordering::Greater,
_ => Ordering::Equal,
};
}
if r1.resource_pattern == ResourcePattern::Prefixed {
if let (Some(name1), Some(name2)) = (&r1.resource_name, &r2.resource_name) {
let cmp = name2.len().cmp(&name1.len()); if cmp != Ordering::Equal {
return cmp;
}
}
}
let d1 = o1.decision();
let d2 = o2.decision();
if d1 != d2 {
return if d1 == Decision::Deny {
Ordering::Less } else {
Ordering::Greater };
}
Ordering::Equal
}
fn create_error(&self, context: &DefaultAuthorizationContext, detail: &str) -> RocketMQError {
let subject_key = context.subject().as_ref().map(|s| s.subject_key()).unwrap_or("unknown");
let resource_key = context
.resource()
.as_ref()
.and_then(|r| r.resource_key())
.unwrap_or_else(|| "unknown".to_string());
let source_ip_binding = context.source_ip();
let source_ip = source_ip_binding.unwrap_or("unknown");
RocketMQError::authentication_failed(format!(
"{} has no permission to access {} from {}, {}",
subject_key, resource_key, source_ip, detail
))
}
}
struct SubjectLookup {
key: String,
subject_type: crate::authentication::enums::subject_type::SubjectType,
}
impl crate::authentication::model::subject::Subject for SubjectLookup {
fn subject_key(&self) -> &str {
&self.key
}
fn subject_type(&self) -> crate::authentication::enums::subject_type::SubjectType {
self.subject_type
}
}
impl<P: AuthorizationMetadataProvider + 'static> AuthorizationHandler for AclAuthorizationHandler<P> {
fn handle<'a>(
&'a self,
context: &'a DefaultAuthorizationContext,
) -> Pin<Box<dyn Future<Output = Result<(), RocketMQError>> + Send + 'a>> {
Box::pin(async move {
let subject_binding = context.subject();
let subject_wrapper = subject_binding
.as_ref()
.ok_or_else(|| RocketMQError::authentication_failed("Subject not found in authorization context"))?;
let subject = SubjectLookup {
key: subject_wrapper.subject_key().to_string(),
subject_type: subject_wrapper.subject_type(),
};
let acl = self
.metadata_provider
.get_acl(&subject)
.await
.map_err(|e| RocketMQError::Internal(format!("Failed to fetch ACL: {}", e)))?
.ok_or_else(|| self.create_error(context, "no matched policies"))?;
let matched_entry = self
.match_policy_entries(context, &acl)
.await
.ok_or_else(|| self.create_error(context, "no matched policies"))?;
if matched_entry.decision() == Decision::Deny {
return Err(self.create_error(context, "the decision is deny"));
}
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use rocketmq_common::common::action::Action;
use rocketmq_common::common::resource::resource_pattern::ResourcePattern;
use rocketmq_common::common::resource::resource_type::ResourceType;
use super::*;
use crate::authentication::enums::subject_type::SubjectType;
use crate::authentication::model::user::User;
use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
use crate::authorization::metadata_provider::local::LocalAuthorizationMetadataProvider;
use crate::authorization::model::policy::Policy;
use crate::authorization::model::resource::Resource;
use crate::config::AuthConfig;
#[tokio::test]
async fn test_acl_authorization_handler_allow() {
let mut provider = LocalAuthorizationMetadataProvider::new();
provider.initialize(AuthConfig::default(), None).unwrap();
let provider = Arc::new(provider);
let user = User::of("alice");
let resource = Resource::of_topic("test-topic");
let policy = Policy::of(vec![resource.clone()], vec![Action::Pub], None, Decision::Allow);
let acl = Acl::of_subject_and_policy(&user, policy);
provider.create_acl(acl).await.unwrap();
let handler = AclAuthorizationHandler::new(provider);
let context = DefaultAuthorizationContext::of("alice", SubjectType::User, resource, Action::Pub, "127.0.0.1");
let result = handler.handle(&context).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_acl_authorization_handler_deny() {
let mut provider = LocalAuthorizationMetadataProvider::new();
provider.initialize(AuthConfig::default(), None).unwrap();
let provider = Arc::new(provider);
let user = User::of("bob");
let resource = Resource::of_topic("forbidden-topic");
let policy = Policy::of(vec![resource.clone()], vec![Action::Pub], None, Decision::Deny);
let acl = Acl::of_subject_and_policy(&user, policy);
provider.create_acl(acl).await.unwrap();
let handler = AclAuthorizationHandler::new(provider);
let context = DefaultAuthorizationContext::of("bob", SubjectType::User, resource, Action::Pub, "192.168.1.1");
let result = handler.handle(&context).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_acl_authorization_handler_no_acl() {
let mut provider = LocalAuthorizationMetadataProvider::new();
provider.initialize(AuthConfig::default(), None).unwrap();
let provider = Arc::new(provider);
let handler = AclAuthorizationHandler::new(provider);
let resource = Resource::of_topic("test-topic");
let context = DefaultAuthorizationContext::of("charlie", SubjectType::User, resource, Action::Pub, "10.0.0.1");
let result = handler.handle(&context).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_acl_authorization_handler_no_matching_policy() {
let mut provider = LocalAuthorizationMetadataProvider::new();
provider.initialize(AuthConfig::default(), None).unwrap();
let provider = Arc::new(provider);
let user = User::of("dave");
let allowed_resource = Resource::of_topic("topic-a");
let policy = Policy::of(vec![allowed_resource], vec![Action::Pub], None, Decision::Allow);
let acl = Acl::of_subject_and_policy(&user, policy);
provider.create_acl(acl).await.unwrap();
let handler = AclAuthorizationHandler::new(provider);
let requested_resource = Resource::of_topic("topic-b");
let context =
DefaultAuthorizationContext::of("dave", SubjectType::User, requested_resource, Action::Pub, "172.16.0.1");
let result = handler.handle(&context).await;
assert!(result.is_err());
}
#[test]
fn test_compare_policy_entries_resource_type() {
let provider = Arc::new(LocalAuthorizationMetadataProvider::new());
let handler = AclAuthorizationHandler::new(provider);
let topic_entry = PolicyEntry::of(
Resource::of(ResourceType::Topic, Some("t1".to_string()), ResourcePattern::Literal),
vec![Action::Pub],
None,
Decision::Allow,
);
let any_entry = PolicyEntry::of(
Resource::of(ResourceType::Any, None, ResourcePattern::Any),
vec![Action::Pub],
None,
Decision::Allow,
);
assert_eq!(handler.compare_policy_entries(&topic_entry, &any_entry), Ordering::Less);
assert_eq!(
handler.compare_policy_entries(&any_entry, &topic_entry),
Ordering::Greater
);
}
#[test]
fn test_compare_policy_entries_resource_pattern() {
let provider = Arc::new(LocalAuthorizationMetadataProvider::new());
let handler = AclAuthorizationHandler::new(provider);
let literal = PolicyEntry::of(
Resource::of(ResourceType::Topic, Some("test".to_string()), ResourcePattern::Literal),
vec![Action::Pub],
None,
Decision::Allow,
);
let prefixed = PolicyEntry::of(
Resource::of(ResourceType::Topic, Some("test".to_string()), ResourcePattern::Prefixed),
vec![Action::Pub],
None,
Decision::Allow,
);
assert_eq!(handler.compare_policy_entries(&literal, &prefixed), Ordering::Less);
}
#[test]
fn test_compare_policy_entries_decision() {
let provider = Arc::new(LocalAuthorizationMetadataProvider::new());
let handler = AclAuthorizationHandler::new(provider);
let deny = PolicyEntry::of(
Resource::of(ResourceType::Topic, Some("t1".to_string()), ResourcePattern::Literal),
vec![Action::Pub],
None,
Decision::Deny,
);
let allow = PolicyEntry::of(
Resource::of(ResourceType::Topic, Some("t1".to_string()), ResourcePattern::Literal),
vec![Action::Pub],
None,
Decision::Allow,
);
assert_eq!(handler.compare_policy_entries(&deny, &allow), Ordering::Less);
}
}