use crate::context::Context;
use crate::enum_str_method;
use crate::error::{Mecha10Error, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Permission {
Publish,
Subscribe,
}
enum_str_method!(
#[allow(dead_code)]
fn as_str(Permission) -> &str {
Publish => "publish",
Subscribe => "subscribe",
}
);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
pub permission: Permission,
pub topic_pattern: String,
}
impl Rule {
pub fn new(permission: Permission, topic_pattern: impl Into<String>) -> Self {
Self {
permission,
topic_pattern: topic_pattern.into(),
}
}
pub fn matches(&self, topic: &str) -> bool {
matches_pattern(&self.topic_pattern, topic)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Policy {
pub role: String,
pub rules: Vec<Rule>,
#[serde(default)]
pub description: String,
}
impl Policy {
pub fn new(role: impl Into<String>) -> Self {
Self {
role: role.into(),
rules: Vec::new(),
description: String::new(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn allow(mut self, permission: Permission, topic_pattern: impl Into<String>) -> Self {
self.rules.push(Rule::new(permission, topic_pattern));
self
}
pub fn allow_publish(self, topic_pattern: impl Into<String>) -> Self {
self.allow(Permission::Publish, topic_pattern)
}
pub fn allow_subscribe(self, topic_pattern: impl Into<String>) -> Self {
self.allow(Permission::Subscribe, topic_pattern)
}
pub fn allows(&self, permission: Permission, topic: &str) -> bool {
self.rules
.iter()
.any(|rule| rule.permission == permission && rule.matches(topic))
}
fn redis_key(&self) -> String {
format!("mecha10:auth:policies:{}", self.role)
}
fn redis_list_key() -> String {
"mecha10:auth:policies:list".to_string()
}
fn redis_role_key(node_id: &str) -> String {
format!("mecha10:auth:roles:{}", node_id)
}
}
pub trait AuthExt {
fn register_policy(&self, policy: &Policy) -> impl std::future::Future<Output = Result<()>> + Send;
fn get_policy(&self, role: &str) -> impl std::future::Future<Output = Result<Option<Policy>>> + Send;
fn list_policies(&self) -> impl std::future::Future<Output = Result<Vec<Policy>>> + Send;
fn assign_role(&self, node_id: &str, role: &str) -> impl std::future::Future<Output = Result<()>> + Send;
fn get_role(&self, node_id: &str) -> impl std::future::Future<Output = Result<Option<String>>> + Send;
fn check_permission(
&self,
node_id: &str,
permission: Permission,
topic: &str,
) -> impl std::future::Future<Output = Result<bool>> + Send;
fn delete_policy(&self, role: &str) -> impl std::future::Future<Output = Result<()>> + Send;
fn unassign_role(&self, node_id: &str) -> impl std::future::Future<Output = Result<()>> + Send;
}
impl AuthExt for Context {
async fn register_policy(&self, policy: &Policy) -> Result<()> {
#[cfg(feature = "messaging")]
{
use redis::AsyncCommands;
let mut conn = crate::redis_list_util::get_redis_connection().await?;
let policy_json = serde_json::to_string(&policy)
.map_err(|e| Mecha10Error::Other(format!("Failed to serialize policy: {}", e)))?;
let key = policy.redis_key();
conn.set::<_, _, ()>(&key, &policy_json)
.await
.map_err(|e| Mecha10Error::MessagingError {
message: format!("Failed to register policy: {}", e),
suggestion: "Check Redis connection".to_string(),
})?;
let list_key = Policy::redis_list_key();
conn.sadd::<_, _, ()>(&list_key, &policy.role)
.await
.map_err(|e| Mecha10Error::MessagingError {
message: format!("Failed to update policy list: {}", e),
suggestion: "Check Redis connection".to_string(),
})?;
Ok(())
}
#[cfg(not(feature = "messaging"))]
{
Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
}
}
async fn get_policy(&self, role: &str) -> Result<Option<Policy>> {
#[cfg(feature = "messaging")]
{
use redis::AsyncCommands;
let mut conn = crate::redis_list_util::get_redis_connection().await?;
let key = format!("mecha10:auth:policies:{}", role);
let json: Option<String> = conn.get(&key).await.map_err(|e| Mecha10Error::MessagingError {
message: format!("Failed to get policy: {}", e),
suggestion: "Check Redis connection".to_string(),
})?;
if let Some(json) = json {
let policy = serde_json::from_str::<Policy>(&json)
.map_err(|e| Mecha10Error::Other(format!("Failed to parse policy: {}", e)))?;
Ok(Some(policy))
} else {
Ok(None)
}
}
#[cfg(not(feature = "messaging"))]
{
Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
}
}
async fn list_policies(&self) -> Result<Vec<Policy>> {
#[cfg(feature = "messaging")]
{
crate::redis_list_util::collect_from_redis_set(&Policy::redis_list_key(), |role| async move {
self.get_policy(&role).await
})
.await
}
#[cfg(not(feature = "messaging"))]
{
Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
}
}
async fn assign_role(&self, node_id: &str, role: &str) -> Result<()> {
#[cfg(feature = "messaging")]
{
use redis::AsyncCommands;
let mut conn = crate::redis_list_util::get_redis_connection().await?;
let key = Policy::redis_role_key(node_id);
conn.set::<_, _, ()>(&key, role)
.await
.map_err(|e| Mecha10Error::MessagingError {
message: format!("Failed to assign role: {}", e),
suggestion: "Check Redis connection".to_string(),
})?;
Ok(())
}
#[cfg(not(feature = "messaging"))]
{
Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
}
}
async fn get_role(&self, node_id: &str) -> Result<Option<String>> {
#[cfg(feature = "messaging")]
{
let key = Policy::redis_role_key(node_id);
crate::redis_list_util::get_optional_from_redis(&key, "Failed to get role").await
}
#[cfg(not(feature = "messaging"))]
{
Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
}
}
async fn check_permission(&self, node_id: &str, permission: Permission, topic: &str) -> Result<bool> {
let role = match self.get_role(node_id).await? {
Some(role) => role,
None => return Ok(false),
};
let policy = match self.get_policy(&role).await? {
Some(policy) => policy,
None => return Ok(false),
};
Ok(policy.allows(permission, topic))
}
async fn delete_policy(&self, role: &str) -> Result<()> {
#[cfg(feature = "messaging")]
{
use redis::AsyncCommands;
let mut conn = crate::redis_list_util::get_redis_connection().await?;
let key = format!("mecha10:auth:policies:{}", role);
conn.del::<_, ()>(&key)
.await
.map_err(|e| Mecha10Error::MessagingError {
message: format!("Failed to delete policy: {}", e),
suggestion: "Check Redis connection".to_string(),
})?;
let list_key = Policy::redis_list_key();
conn.srem::<_, _, ()>(&list_key, role)
.await
.map_err(|e| Mecha10Error::MessagingError {
message: format!("Failed to update policy list: {}", e),
suggestion: "Check Redis connection".to_string(),
})?;
Ok(())
}
#[cfg(not(feature = "messaging"))]
{
Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
}
}
async fn unassign_role(&self, node_id: &str) -> Result<()> {
#[cfg(feature = "messaging")]
{
use redis::AsyncCommands;
let mut conn = crate::redis_list_util::get_redis_connection().await?;
let key = Policy::redis_role_key(node_id);
conn.del::<_, ()>(&key)
.await
.map_err(|e| Mecha10Error::MessagingError {
message: format!("Failed to unassign role: {}", e),
suggestion: "Check Redis connection".to_string(),
})?;
Ok(())
}
#[cfg(not(feature = "messaging"))]
{
Err(Mecha10Error::Other("Messaging feature not enabled".to_string()))
}
}
}
pub fn matches_pattern(pattern: &str, topic: &str) -> bool {
crate::aggregated::pattern::matches_pattern(topic, pattern)
}