use std::{
fmt,
hash::{Hash, Hasher},
mem,
};
use gxhash::{GxBuildHasher, HashMap, HashSet};
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
const DEFAULT_USER_NAME: &str = "default";
const NUM_HASH_BYTES: usize = 32;
#[inline]
pub fn constant_equals(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
#[derive(Debug, Clone)]
pub struct AclPassword {
pub hash: [u8; NUM_HASH_BYTES],
}
impl AclPassword {
pub fn from_string(password: &str) -> Self {
Self {
hash: Sha256::digest(password.as_bytes()).into(),
}
}
pub fn from_hash(hash_string: &str) -> Result<Self> {
let bytes = hash_string.as_bytes();
if bytes.len() != NUM_HASH_BYTES * 2 {
return Err(Error::Password);
}
let mut hash = [0u8; NUM_HASH_BYTES];
for (slot, pair) in hash.iter_mut().zip(bytes.as_chunks::<2>().0) {
let hi = hex_val(pair[0]).ok_or(Error::Password)?;
let lo = hex_val(pair[1]).ok_or(Error::Password)?;
*slot = hi << 4 | lo;
}
Ok(Self { hash })
}
}
#[inline]
const fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
impl PartialEq for AclPassword {
#[inline]
fn eq(&self, other: &Self) -> bool {
constant_equals(&self.hash, &other.hash)
}
}
impl Eq for AclPassword {}
impl Hash for AclPassword {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u8(self.hash[0]);
}
}
impl fmt::Display for AclPassword {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in self.hash {
write!(f, "{b:02x}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct CommandPermissionSet {
pub allow_all: bool,
pub allowed_commands: HashSet<String>,
pub denied_commands: HashSet<String>,
}
impl CommandPermissionSet {
pub fn check_permission(&self, cmd: &str) -> bool {
if self.denied_commands.contains(cmd) {
return false;
}
self.allow_all || self.allowed_commands.contains(cmd)
}
pub fn allow(&mut self, cmd: &str) {
self.allowed_commands.insert(cmd.to_string());
self.denied_commands.remove(cmd);
}
pub fn deny(&mut self, cmd: &str) {
self.denied_commands.insert(cmd.to_string());
self.allowed_commands.remove(cmd);
}
}
#[derive(Debug, Clone)]
pub struct User {
pub name: String,
pub is_enabled: bool,
pub is_passwordless: bool,
pub passwords: Vec<AclPassword>,
pub permissions: CommandPermissionSet,
}
impl User {
pub fn new(name: String) -> Self {
Self {
name,
is_enabled: false,
is_passwordless: false,
passwords: Vec::new(),
permissions: CommandPermissionSet::default(),
}
}
pub fn add_password_hash(&mut self, password: AclPassword) {
if !self.passwords.contains(&password) {
self.passwords.push(password);
}
}
pub fn remove_password_hash(&mut self, password: &AclPassword) {
self.passwords.retain(|p| p != password);
}
pub fn clear_passwords(&mut self) {
self.passwords.clear();
}
pub fn validate_password(&self, password: &AclPassword) -> bool {
if self.is_passwordless {
return true;
}
self.passwords.contains(password)
}
pub fn reset(&mut self) {
self.clear_passwords();
self.permissions = CommandPermissionSet::default();
self.is_enabled = false;
self.is_passwordless = false;
}
pub fn describe_user(&self) -> String {
let mut out = format!("user {}", self.name);
out.push_str(if self.is_enabled { " on" } else { " off" });
if self.is_passwordless {
out.push_str(" nopass");
}
for hash in &self.passwords {
out.push_str(&format!(" #{hash}"));
}
if self.permissions.allow_all {
out.push_str(" +@all");
}
let mut allowed: Vec<&String> = self.permissions.allowed_commands.iter().collect();
allowed.sort();
for cmd in allowed {
out.push_str(&format!(" +{cmd}"));
}
let mut denied: Vec<&String> = self.permissions.denied_commands.iter().collect();
denied.sort();
for cmd in denied {
out.push_str(&format!(" -{cmd}"));
}
out
}
}
#[derive(Debug, Clone, Default)]
pub struct UserHandle {
user: Option<Box<User>>,
}
impl UserHandle {
pub fn new(user: User) -> Self {
Self {
user: Some(Box::new(user)),
}
}
pub fn user(&self) -> Option<&User> {
self.user.as_deref()
}
pub fn user_mut(&mut self) -> Option<&mut User> {
self.user.as_deref_mut()
}
pub fn try_set_user(&mut self, new_user: User) -> bool {
if self.user.is_some() {
self.user = Some(Box::new(new_user));
true
} else {
false
}
}
}
pub struct AccessControlList {
users: HashMap<String, UserHandle>,
}
impl Default for AccessControlList {
fn default() -> Self {
Self::new()
}
}
impl AccessControlList {
pub fn new() -> Self {
let mut acl = Self {
users: HashMap::with_hasher(GxBuildHasher::default()),
};
let mut default_user = User::new(DEFAULT_USER_NAME.to_string());
default_user.permissions.allow_all = true;
default_user.is_enabled = true;
default_user.is_passwordless = true;
acl.add_user(default_user).ok();
acl
}
pub fn get_user(&self, username: &str) -> Option<&UserHandle> {
self.users.get(username)
}
pub fn get_user_mut(&mut self, username: &str) -> Option<&mut UserHandle> {
self.users.get_mut(username)
}
pub fn add_user(&mut self, user: User) -> Result<()> {
let mut user = user;
let name = mem::take(&mut user.name);
if self.users.contains_key(&name) {
return Err(Error::UserAlreadyExists(name));
}
self.users.insert(name, UserHandle::new(user));
Ok(())
}
pub fn delete_user(&mut self, username: &str) -> Result<bool> {
if username == DEFAULT_USER_NAME {
return Err(Error::Acl(
"The special 'default' user cannot be removed from the system".into(),
));
}
Ok(self.users.remove(username).is_some())
}
}
pub struct AclParser;
impl AclParser {
pub fn parse_rules(user: &mut User, rules: &[&str]) -> Result<()> {
for rule in rules {
Self::apply_op(user, rule)?;
}
Ok(())
}
pub fn apply_op(user: &mut User, rule: &str) -> Result<()> {
if rule.is_empty() {
return Ok(());
}
let eq_ic = |a: &str| rule.eq_ignore_ascii_case(a);
if eq_ic("on") {
user.is_enabled = true;
} else if eq_ic("off") {
user.is_enabled = false;
} else if eq_ic("nopass") {
user.clear_passwords();
user.is_passwordless = true;
} else if eq_ic("reset") {
user.reset();
} else if eq_ic("resetpass") {
user.clear_passwords();
user.is_passwordless = false;
} else if let Some(pwd) = rule.strip_prefix('>') {
user.add_password_hash(AclPassword::from_string(pwd));
} else if let Some(pwd) = rule.strip_prefix('<') {
user.remove_password_hash(&AclPassword::from_string(pwd));
} else if let Some(hash) = rule.strip_prefix('#') {
user.add_password_hash(AclPassword::from_hash(hash)?);
} else if let Some(hash) = rule.strip_prefix('!') {
user.remove_password_hash(&AclPassword::from_hash(hash)?);
} else if let Some(cat) = rule.strip_prefix("+@") {
if !cat.eq_ignore_ascii_case("all") {
return Err(Error::CategoryDoesNotExist(cat.to_string()));
}
user.permissions.allow_all = true;
} else if let Some(cat) = rule.strip_prefix("-@") {
if !cat.eq_ignore_ascii_case("all") {
return Err(Error::CategoryDoesNotExist(cat.to_string()));
}
user.permissions.allow_all = false;
} else if let Some(cmd) = rule.strip_prefix('+') {
user.permissions.allow(cmd);
} else if let Some(cmd) = rule.strip_prefix('-') {
user.permissions.deny(cmd);
} else if rule == "~*" || eq_ic("allkeys") || eq_ic("resetkeys") {
} else {
return Err(Error::UnknownOperation(rule.to_string()));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
const DUMMY_HASH: &str = "8f0e2f76e22b43e2855189877e7dc1e1e7d98c226c95db247cd1d547928334a9";
#[test]
fn password_hashing_matches_garnet_vectors() {
let p = AclPassword::from_string("passw0rd");
assert_eq!(p.to_string(), DUMMY_HASH);
assert_eq!(AclPassword::from_hash(DUMMY_HASH).unwrap(), p);
assert_eq!(
AclPassword::from_hash(&DUMMY_HASH.to_uppercase()).unwrap(),
p
);
assert!(AclPassword::from_hash("abcd").is_err());
assert!(AclPassword::from_hash(&"z".repeat(64)).is_err());
}
#[test]
fn constant_equals_semantics() {
assert!(constant_equals(b"abc", b"abc"));
assert!(!constant_equals(b"abc", b"abd"));
assert!(!constant_equals(b"abc", b"ab"));
}
#[test]
fn default_user_shape() {
let acl = AccessControlList::new();
let default = acl.get_user("default").unwrap().user().unwrap();
assert!(default.is_enabled);
assert!(default.is_passwordless);
assert!(default.permissions.check_permission("GET"));
let mut acl2 = AccessControlList::new();
assert!(acl2.delete_user("default").is_err());
}
#[test]
fn parse_rules_semantics() {
let mut u = User::new("alice".into());
AclParser::parse_rules(&mut u, &["on", ">passw0rd", &format!("#{DUMMY_HASH}")]).unwrap();
assert_eq!(u.passwords.len(), 1);
assert!(u.validate_password(&AclPassword::from_string("passw0rd")));
AclParser::parse_rules(&mut u, &["<passw0rd"]).unwrap();
assert!(!u.validate_password(&AclPassword::from_string("passw0rd")));
AclParser::parse_rules(&mut u, &["+get", "+set", "-set"]).unwrap();
assert!(u.permissions.check_permission("get"));
assert!(!u.permissions.check_permission("set"));
AclParser::parse_rules(&mut u, &["+@all", "-set"]).unwrap();
assert!(u.permissions.check_permission("anything"));
assert!(!u.permissions.check_permission("set"));
AclParser::parse_rules(&mut u, &["-@all", "+set"]).unwrap();
assert!(!u.permissions.check_permission("anything"));
assert!(u.permissions.check_permission("set"));
AclParser::parse_rules(&mut u, &["nopass"]).unwrap();
assert!(u.is_passwordless);
AclParser::parse_rules(&mut u, &["resetpass"]).unwrap();
assert!(!u.is_passwordless);
AclParser::parse_rules(&mut u, &["reset"]).unwrap();
assert!(!u.is_enabled);
assert!(!u.validate_password(&AclPassword::from_string("x")));
assert!(matches!(
AclParser::apply_op(&mut u, "whatsthis"),
Err(Error::UnknownOperation(_))
));
assert!(matches!(
AclParser::apply_op(&mut u, "+@nosuch"),
Err(Error::CategoryDoesNotExist(_))
));
AclParser::parse_rules(&mut u, &["~*", "allkeys", "resetkeys"]).unwrap();
}
#[test]
fn describe_user_shape() {
let mut u = User::new("bob".into());
AclParser::parse_rules(&mut u, &["on", ">secret", "+get", "+set", "-set"]).unwrap();
let secret_hash = AclPassword::from_string("secret").to_string();
assert_eq!(
u.describe_user(),
format!("user bob on #{secret_hash} +get -set")
);
let mut all = User::new("dave".into());
AclParser::parse_rules(&mut all, &["on", "+@all", "-set"]).unwrap();
assert_eq!(all.describe_user(), "user dave on +@all -set");
let mut nopass = User::new("carl".into());
AclParser::parse_rules(&mut nopass, &["nopass"]).unwrap();
assert_eq!(nopass.describe_user(), "user carl off nopass");
}
#[test]
fn duplicate_user_rejected() {
let mut acl = AccessControlList::new();
acl.add_user(User::new("bob".into())).unwrap();
assert!(matches!(
acl.add_user(User::new("bob".into())),
Err(Error::UserAlreadyExists(_))
));
assert!(acl.get_user("bob").unwrap().user().is_some());
assert!(acl.delete_user("bob").unwrap());
assert!(!acl.delete_user("bob").unwrap());
}
#[test]
fn user_handle_swap() {
let mut u = User::new("a".into());
u.is_enabled = true;
let mut h = UserHandle::new(u);
assert!(h.user().unwrap().is_enabled);
let mut replacement = User::new("a".into());
replacement.permissions.allow("get");
assert!(h.try_set_user(replacement));
assert!(h.user().unwrap().permissions.check_permission("get"));
h.user_mut().unwrap().is_enabled = true;
assert!(h.user().unwrap().is_enabled);
}
}