use std::cmp::Ordering;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use rocketmq_security_api::AuthorizationDecision;
use rocketmq_security_api::AuthorizationDenial;
use rocketmq_security_api::ResourcePattern;
use rocketmq_security_api::ResourceType;
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;
#[cfg(test)]
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;
use crate::AclMetadataRead;
use crate::AuthServiceError;
use crate::AuthServiceResult;
use crate::SubjectKey;
pub struct AclAuthorizationHandler<P: AclMetadataRead + ?Sized> {
metadata_provider: Arc<P>,
}
impl<P: AclMetadataRead + ?Sized> 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
}
}
impl<P: AclMetadataRead + ?Sized + 'static> AuthorizationHandler for AclAuthorizationHandler<P> {
fn handle<'a>(
&'a self,
context: &'a DefaultAuthorizationContext,
) -> Pin<Box<dyn Future<Output = AuthServiceResult<AuthorizationDecision>> + Send + 'a>> {
Box::pin(async move {
let subject_binding = context.subject();
let subject_wrapper = subject_binding
.as_ref()
.ok_or_else(|| AuthServiceError::invalid_context("subject is missing".to_owned()))?;
let subject = SubjectKey::new(subject_wrapper.subject_type(), subject_wrapper.subject_key());
let acl = self.metadata_provider.lookup_acl(&subject).await?;
let Some(acl) = acl else {
return Ok(AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown));
};
let Some(matched_entry) = self.match_policy_entries(context, &acl).await else {
return Ok(AuthorizationDecision::Deny(AuthorizationDenial::PolicyNotApplicable));
};
if matched_entry.decision() == Decision::Deny {
return Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied));
}
Ok(AuthorizationDecision::Allow)
})
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use rocketmq_security_api::Action;
use rocketmq_security_api::ResourcePattern;
use rocketmq_security_api::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_eq!(result.unwrap(), AuthorizationDecision::Allow);
}
#[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_eq!(
result.unwrap(),
AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied)
);
}
#[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_eq!(
result.unwrap(),
AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown)
);
}
#[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_eq!(
result.unwrap(),
AuthorizationDecision::Deny(AuthorizationDenial::PolicyNotApplicable)
);
}
#[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);
}
}