use std::sync::Arc;
use gxhash::{HashSet, HashSetExt};
use super::command_catalog::{LAST_VALID_COMMAND, expand_for_acls, is_no_auth, normalize_for_acls};
use crate::types::RespCommand;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Special {
All,
None,
Set,
}
pub struct CommandPermissionSet {
special: Special,
command_list: Box<[u64]>,
custom_allowed: Arc<HashSet<String>>,
custom_denied: Arc<HashSet<String>>,
pub description: String,
}
impl CommandPermissionSet {
pub fn all() -> Self {
Self {
special: Special::All,
command_list: vec![0; Self::get_command_list_length()].into(),
custom_allowed: Self::empty_custom(),
custom_denied: Self::empty_custom(),
description: "+@all".to_string(),
}
}
pub fn none() -> Self {
Self {
special: Special::None,
command_list: vec![0; Self::get_command_list_length()].into(),
custom_allowed: Self::empty_custom(),
custom_denied: Self::empty_custom(),
description: String::new(),
}
}
#[inline]
pub fn is_all(&self) -> bool {
matches!(self.special, Special::All)
}
#[inline]
pub fn is_none(&self) -> bool {
matches!(self.special, Special::None)
}
pub const fn get_command_list_length() -> usize {
((LAST_VALID_COMMAND as u16 as usize) + 1).div_ceil(64)
}
#[inline]
fn empty_custom() -> Arc<HashSet<String>> {
Arc::new(HashSet::new())
}
#[inline]
fn bit_on(&self, cmd: RespCommand) -> bool {
let index = cmd as u16 as usize;
(self.command_list[index / 64] >> (index % 64)) & 1 == 1
}
#[inline]
pub fn can_run_command(&self, command: RespCommand) -> bool {
if self.is_all() {
return true;
}
self.bit_on(command)
}
#[inline]
pub fn can_run_custom_command(&self, generic_cmd: RespCommand, custom_name: &str) -> bool {
if self.is_all() {
return true;
}
if contains_ignore(&self.custom_denied, custom_name) {
return false;
}
if contains_ignore(&self.custom_allowed, custom_name) {
return true;
}
self.bit_on(generic_cmd)
}
pub fn copy(&self) -> Self {
let mut command_list = self.command_list.clone();
if self.is_all() {
command_list.iter_mut().for_each(|w| *w = u64::MAX);
}
Self {
special: Special::Set,
command_list,
custom_allowed: Arc::clone(&self.custom_allowed),
custom_denied: Arc::clone(&self.custom_denied),
description: self.description.clone(),
}
}
pub fn add_custom_command(&mut self, normalized_name: &str) {
self.custom_allowed = insert_custom(&self.custom_allowed, normalized_name);
self.custom_denied = remove_custom(&self.custom_denied, normalized_name);
}
pub fn remove_custom_command(&mut self, normalized_name: &str) {
self.custom_denied = insert_custom(&self.custom_denied, normalized_name);
self.custom_allowed = remove_custom(&self.custom_allowed, normalized_name);
}
pub fn custom_allowed(&self) -> &HashSet<String> {
&self.custom_allowed
}
pub fn custom_denied(&self) -> &HashSet<String> {
&self.custom_denied
}
pub fn add_command(&mut self, command: RespCommand) {
debug_assert!(
normalize_for_acls(command) == command,
"Cannot control access to this command, it's an implementation detail"
);
let index = command as u16 as usize;
self.command_list[index / 64] |= 1u64 << (index % 64);
for extra in expand_for_acls(command) {
let i = *extra as u16 as usize;
self.command_list[i / 64] |= 1u64 << (i % 64);
}
}
pub fn remove_command(&mut self, command: RespCommand) {
debug_assert!(
normalize_for_acls(command) == command,
"Cannot control access to this command, it's an implementation detail"
);
if is_no_auth(command) {
return;
}
let index = command as u16 as usize;
self.command_list[index / 64] &= !(1u64 << (index % 64));
for extra in expand_for_acls(command) {
let i = *extra as u16 as usize;
self.command_list[i / 64] &= !(1u64 << (i % 64));
}
}
pub fn is_equivalent_to(&self, other: &Self) -> bool {
if self.is_all() {
other.is_all()
} else {
self.command_list == other.command_list
&& set_eq_ignore(&self.custom_allowed, &other.custom_allowed)
&& set_eq_ignore(&self.custom_denied, &other.custom_denied)
}
}
}
#[inline]
fn contains_ignore(set: &HashSet<String>, name: &str) -> bool {
set.iter().any(|s| s.eq_ignore_ascii_case(name))
}
#[inline]
fn set_eq_ignore(a: &HashSet<String>, b: &HashSet<String>) -> bool {
a.len() == b.len() && a.iter().all(|x| contains_ignore(b, x))
}
#[inline]
fn insert_custom(set: &HashSet<String>, normalized_name: &str) -> Arc<HashSet<String>> {
let mut next = HashSet::new();
next.extend(set.iter().cloned());
next.insert(normalized_name.to_string());
Arc::new(next)
}
#[inline]
fn remove_custom(set: &HashSet<String>, normalized_name: &str) -> Arc<HashSet<String>> {
let mut next = HashSet::new();
next.extend(
set
.iter()
.filter(|s| !s.eq_ignore_ascii_case(normalized_name))
.cloned(),
);
Arc::new(next)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::acl::RespAclCategories;
#[test]
fn sentinels_and_copy() {
let none = CommandPermissionSet::none();
assert!(none.is_none());
assert!(!none.can_run_command(RespCommand::Get));
assert_eq!(none.description, "");
let all = CommandPermissionSet::all();
assert!(all.is_all());
assert!(all.can_run_command(RespCommand::Get));
let materialized = all.copy();
assert!(!materialized.is_all());
assert!(materialized.can_run_command(RespCommand::Get));
assert!(!materialized.is_equivalent_to(&all));
assert!(all.is_equivalent_to(&CommandPermissionSet::all()));
let empty = none.copy();
assert!(empty.is_equivalent_to(&CommandPermissionSet::none()));
assert!(empty.is_equivalent_to(&all));
}
#[test]
fn add_remove_command_with_expansion() {
let mut set = CommandPermissionSet::none().copy();
set.add_command(RespCommand::Set);
for cmd in [
RespCommand::Set,
RespCommand::Setexnx,
RespCommand::Setexxx,
RespCommand::Setkeepttl,
RespCommand::Setkeepttlxx,
] {
assert!(set.can_run_command(cmd));
}
set.remove_command(RespCommand::Set);
assert!(!set.can_run_command(RespCommand::Set));
assert!(!set.can_run_command(RespCommand::Setkeepttl));
}
#[test]
fn no_auth_commands_cannot_be_removed() {
let mut set = CommandPermissionSet::all().copy();
set.remove_command(RespCommand::Auth);
set.remove_command(RespCommand::Hello);
set.remove_command(RespCommand::Quit);
for cmd in [RespCommand::Auth, RespCommand::Hello, RespCommand::Quit] {
assert!(set.can_run_command(cmd));
}
}
#[test]
fn custom_command_deny_precedence() {
let mut set = CommandPermissionSet::none().copy();
assert!(!set.can_run_custom_command(RespCommand::Customrawstringcmd, "json.set"));
set.add_custom_command("JSON.SET");
assert!(set.can_run_custom_command(RespCommand::Customrawstringcmd, "json.set"));
assert!(set.can_run_custom_command(RespCommand::Customrawstringcmd, "JSON.SET"));
set.remove_custom_command("json.set");
assert!(!set.can_run_custom_command(RespCommand::Customrawstringcmd, "json.set"));
set.add_command(RespCommand::Customrawstringcmd);
assert!(set.can_run_custom_command(RespCommand::Customrawstringcmd, "other.cmd"));
}
#[test]
fn command_list_length_covers_all_commands() {
let len = CommandPermissionSet::get_command_list_length();
assert_eq!(len, 6);
assert!(len * 64 > RespCommand::Quit as u16 as usize);
}
#[test]
fn category_bits_sanity() {
assert_eq!(RespAclCategories::ALL.bits(), (1 << 24) - 1);
assert!(RespAclCategories::ALL.contains(RespAclCategories::ADMIN));
assert!(RespAclCategories::ALL.contains(RespAclCategories::VECTOR));
assert!(!RespAclCategories::ADMIN.contains(RespAclCategories::BITMAP));
}
}