use crate::error::{MqttError, Result};
use crate::validation::topic_matches_filter;
use std::borrow::Cow;
use std::collections::HashSet;
fn username_contains_mqtt_special_chars(name: &str) -> bool {
name.contains('+') || name.contains('#') || name.contains('/')
}
fn expand_pattern<'a>(pattern: &'a str, username: Option<&str>) -> Option<Cow<'a, str>> {
if !pattern.contains("%u") {
return Some(Cow::Borrowed(pattern));
}
let name = username?;
if username_contains_mqtt_special_chars(name) {
return None;
}
Some(Cow::Owned(pattern.replace("%u", name)))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Permission {
Read,
Write,
ReadWrite,
Deny,
}
impl Permission {
#[must_use]
pub fn allows_read(&self) -> bool {
matches!(self, Permission::Read | Permission::ReadWrite)
}
#[must_use]
pub fn allows_write(&self) -> bool {
matches!(self, Permission::Write | Permission::ReadWrite)
}
#[must_use]
pub fn is_deny(&self) -> bool {
matches!(self, Permission::Deny)
}
}
impl std::str::FromStr for Permission {
type Err = MqttError;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"read" | "subscribe" => Ok(Permission::Read),
"write" | "publish" => Ok(Permission::Write),
"readwrite" | "rw" | "all" => Ok(Permission::ReadWrite),
"deny" | "none" => Ok(Permission::Deny),
_ => Err(MqttError::Configuration(format!("Invalid permission: {s}"))),
}
}
}
#[derive(Debug, Clone)]
pub struct AclRule {
pub username: String,
pub topic_pattern: String,
pub permission: Permission,
}
impl AclRule {
#[must_use]
pub fn new(
username: impl Into<String>,
topic_pattern: impl Into<String>,
permission: Permission,
) -> Self {
Self {
username: username.into(),
topic_pattern: topic_pattern.into(),
permission,
}
}
#[must_use]
pub fn matches(&self, username: Option<&str>, topic: &str) -> bool {
let username_matches = match username {
Some(user) => self.username == "*" || self.username == user,
None => self.username == "*" || self.username == "anonymous",
};
if !username_matches {
return false;
}
let Some(expanded) = expand_pattern(&self.topic_pattern, username) else {
return false;
};
topic_matches_filter(topic, &expanded)
}
}
#[derive(Debug, Clone)]
pub struct RoleRule {
pub topic_pattern: String,
pub permission: Permission,
}
impl RoleRule {
#[must_use]
pub fn new(topic_pattern: impl Into<String>, permission: Permission) -> Self {
Self {
topic_pattern: topic_pattern.into(),
permission,
}
}
#[must_use]
pub fn matches(&self, username: Option<&str>, topic: &str) -> bool {
let Some(expanded) = expand_pattern(&self.topic_pattern, username) else {
return false;
};
topic_matches_filter(topic, &expanded)
}
}
#[derive(Debug, Clone)]
pub struct Role {
pub name: String,
pub rules: Vec<RoleRule>,
}
impl Role {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
rules: Vec::new(),
}
}
pub fn add_rule(&mut self, rule: RoleRule) {
self.rules.push(rule);
}
pub fn remove_rule(&mut self, topic_pattern: &str) -> bool {
let len_before = self.rules.len();
self.rules.retain(|r| r.topic_pattern != topic_pattern);
self.rules.len() < len_before
}
}
#[derive(Debug, Clone)]
pub struct FederatedRoleEntry {
pub roles: HashSet<String>,
pub issuer: String,
pub mode: crate::broker::config::FederatedAuthMode,
pub session_bound: bool,
}
impl FederatedRoleEntry {
#[must_use]
pub fn new(
roles: HashSet<String>,
issuer: String,
mode: crate::broker::config::FederatedAuthMode,
session_bound: bool,
) -> Self {
Self {
roles,
issuer,
mode,
session_bound,
}
}
}