blob: 00f5195862a5fd38374a5eb155a1a28a0102fe50 [file] [log] [blame]
//! Defines types to use with the ACL commands.
use crate::types::{
ErrorKind, FromRedisValue, RedisError, RedisResult, RedisWrite, ToRedisArgs, Value,
};
macro_rules! not_convertible_error {
($v:expr, $det:expr) => {
RedisError::from((
ErrorKind::TypeError,
"Response type not convertible",
format!("{:?} (response was {:?})", $det, $v),
))
};
}
/// ACL rules are used in order to activate or remove a flag, or to perform a
/// given change to the user ACL, which under the hood are just single words.
#[derive(Debug, Eq, PartialEq)]
pub enum Rule {
/// Enable the user: it is possible to authenticate as this user.
On,
/// Disable the user: it's no longer possible to authenticate with this
/// user, however the already authenticated connections will still work.
Off,
/// Add the command to the list of commands the user can call.
AddCommand(String),
/// Remove the command to the list of commands the user can call.
RemoveCommand(String),
/// Add all the commands in such category to be called by the user.
AddCategory(String),
/// Remove the commands from such category the client can call.
RemoveCategory(String),
/// Alias for `+@all`. Note that it implies the ability to execute all the
/// future commands loaded via the modules system.
AllCommands,
/// Alias for `-@all`.
NoCommands,
/// Add this password to the list of valid password for the user.
AddPass(String),
/// Remove this password from the list of valid passwords.
RemovePass(String),
/// Add this SHA-256 hash value to the list of valid passwords for the user.
AddHashedPass(String),
/// Remove this hash value from from the list of valid passwords
RemoveHashedPass(String),
/// All the set passwords of the user are removed, and the user is flagged
/// as requiring no password: it means that every password will work
/// against this user.
NoPass,
/// Flush the list of allowed passwords. Moreover removes the _nopass_ status.
ResetPass,
/// Add a pattern of keys that can be mentioned as part of commands.
Pattern(String),
/// Alias for `~*`.
AllKeys,
/// Flush the list of allowed keys patterns.
ResetKeys,
/// Performs the following actions: `resetpass`, `resetkeys`, `off`, `-@all`.
/// The user returns to the same state it has immediately after its creation.
Reset,
/// Raw text of [`ACL rule`][1] that not enumerated above.
///
/// [1]: https://redis.io/docs/manual/security/acl
Other(String),
}
impl ToRedisArgs for Rule {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
use self::Rule::*;
match self {
On => out.write_arg(b"on"),
Off => out.write_arg(b"off"),
AddCommand(cmd) => out.write_arg_fmt(format_args!("+{}", cmd)),
RemoveCommand(cmd) => out.write_arg_fmt(format_args!("-{}", cmd)),
AddCategory(cat) => out.write_arg_fmt(format_args!("+@{}", cat)),
RemoveCategory(cat) => out.write_arg_fmt(format_args!("-@{}", cat)),
AllCommands => out.write_arg(b"allcommands"),
NoCommands => out.write_arg(b"nocommands"),
AddPass(pass) => out.write_arg_fmt(format_args!(">{}", pass)),
RemovePass(pass) => out.write_arg_fmt(format_args!("<{}", pass)),
AddHashedPass(pass) => out.write_arg_fmt(format_args!("#{}", pass)),
RemoveHashedPass(pass) => out.write_arg_fmt(format_args!("!{}", pass)),
NoPass => out.write_arg(b"nopass"),
ResetPass => out.write_arg(b"resetpass"),
Pattern(pat) => out.write_arg_fmt(format_args!("~{}", pat)),
AllKeys => out.write_arg(b"allkeys"),
ResetKeys => out.write_arg(b"resetkeys"),
Reset => out.write_arg(b"reset"),
Other(rule) => out.write_arg(rule.as_bytes()),
};
}
}
/// An info dictionary type storing Redis ACL information as multiple `Rule`.
/// This type collects key/value data returned by the [`ACL GETUSER`][1] command.
///
/// [1]: https://redis.io/commands/acl-getuser
#[derive(Debug, Eq, PartialEq)]
pub struct AclInfo {
/// Describes flag rules for the user. Represented by [`Rule::On`][1],
/// [`Rule::Off`][2], [`Rule::AllKeys`][3], [`Rule::AllCommands`][4] and
/// [`Rule::NoPass`][5].
///
/// [1]: ./enum.Rule.html#variant.On
/// [2]: ./enum.Rule.html#variant.Off
/// [3]: ./enum.Rule.html#variant.AllKeys
/// [4]: ./enum.Rule.html#variant.AllCommands
/// [5]: ./enum.Rule.html#variant.NoPass
pub flags: Vec<Rule>,
/// Describes the user's passwords. Represented by [`Rule::AddHashedPass`][1].
///
/// [1]: ./enum.Rule.html#variant.AddHashedPass
pub passwords: Vec<Rule>,
/// Describes capabilities of which commands the user can call.
/// Represented by [`Rule::AddCommand`][1], [`Rule::AddCategory`][2],
/// [`Rule::RemoveCommand`][3] and [`Rule::RemoveCategory`][4].
///
/// [1]: ./enum.Rule.html#variant.AddCommand
/// [2]: ./enum.Rule.html#variant.AddCategory
/// [3]: ./enum.Rule.html#variant.RemoveCommand
/// [4]: ./enum.Rule.html#variant.RemoveCategory
pub commands: Vec<Rule>,
/// Describes patterns of keys which the user can access. Represented by
/// [`Rule::Pattern`][1].
///
/// [1]: ./enum.Rule.html#variant.Pattern
pub keys: Vec<Rule>,
}
impl FromRedisValue for AclInfo {
fn from_redis_value(v: &Value) -> RedisResult<Self> {
let mut it = v
.as_sequence()
.ok_or_else(|| not_convertible_error!(v, ""))?
.iter()
.skip(1)
.step_by(2);
let (flags, passwords, commands, keys) = match (it.next(), it.next(), it.next(), it.next())
{
(Some(flags), Some(passwords), Some(commands), Some(keys)) => {
// Parse flags
// Ref: https://github.com/redis/redis/blob/0cabe0cfa7290d9b14596ec38e0d0a22df65d1df/src/acl.c#L83-L90
let flags = flags
.as_sequence()
.ok_or_else(|| {
not_convertible_error!(flags, "Expect a bulk response of ACL flags")
})?
.iter()
.map(|flag| match flag {
Value::Data(flag) => match flag.as_slice() {
b"on" => Ok(Rule::On),
b"off" => Ok(Rule::Off),
b"allkeys" => Ok(Rule::AllKeys),
b"allcommands" => Ok(Rule::AllCommands),
b"nopass" => Ok(Rule::NoPass),
other => Ok(Rule::Other(String::from_utf8_lossy(other).into_owned())),
},
_ => Err(not_convertible_error!(
flag,
"Expect an arbitrary binary data"
)),
})
.collect::<RedisResult<_>>()?;
let passwords = passwords
.as_sequence()
.ok_or_else(|| {
not_convertible_error!(flags, "Expect a bulk response of ACL flags")
})?
.iter()
.map(|pass| Ok(Rule::AddHashedPass(String::from_redis_value(pass)?)))
.collect::<RedisResult<_>>()?;
let commands = match commands {
Value::Data(cmd) => std::str::from_utf8(cmd)?,
_ => {
return Err(not_convertible_error!(
commands,
"Expect a valid UTF8 string"
))
}
}
.split_terminator(' ')
.map(|cmd| match cmd {
x if x.starts_with("+@") => Ok(Rule::AddCategory(x[2..].to_owned())),
x if x.starts_with("-@") => Ok(Rule::RemoveCategory(x[2..].to_owned())),
x if x.starts_with('+') => Ok(Rule::AddCommand(x[1..].to_owned())),
x if x.starts_with('-') => Ok(Rule::RemoveCommand(x[1..].to_owned())),
_ => Err(not_convertible_error!(
cmd,
"Expect a command addition/removal"
)),
})
.collect::<RedisResult<_>>()?;
let keys = keys
.as_sequence()
.ok_or_else(|| not_convertible_error!(keys, ""))?
.iter()
.map(|pat| Ok(Rule::Pattern(String::from_redis_value(pat)?)))
.collect::<RedisResult<_>>()?;
(flags, passwords, commands, keys)
}
_ => {
return Err(not_convertible_error!(
v,
"Expect a resposne from `ACL GETUSER`"
))
}
};
Ok(Self {
flags,
passwords,
commands,
keys,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_args {
($rule:expr, $arg:expr) => {
assert_eq!($rule.to_redis_args(), vec![$arg.to_vec()]);
};
}
#[test]
fn test_rule_to_arg() {
use self::Rule::*;
assert_args!(On, b"on");
assert_args!(Off, b"off");
assert_args!(AddCommand("set".to_owned()), b"+set");
assert_args!(RemoveCommand("set".to_owned()), b"-set");
assert_args!(AddCategory("hyperloglog".to_owned()), b"+@hyperloglog");
assert_args!(RemoveCategory("hyperloglog".to_owned()), b"-@hyperloglog");
assert_args!(AllCommands, b"allcommands");
assert_args!(NoCommands, b"nocommands");
assert_args!(AddPass("mypass".to_owned()), b">mypass");
assert_args!(RemovePass("mypass".to_owned()), b"<mypass");
assert_args!(
AddHashedPass(
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2".to_owned()
),
b"#c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
);
assert_args!(
RemoveHashedPass(
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2".to_owned()
),
b"!c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
);
assert_args!(NoPass, b"nopass");
assert_args!(Pattern("pat:*".to_owned()), b"~pat:*");
assert_args!(AllKeys, b"allkeys");
assert_args!(ResetKeys, b"resetkeys");
assert_args!(Reset, b"reset");
assert_args!(Other("resetchannels".to_owned()), b"resetchannels");
}
#[test]
fn test_from_redis_value() {
let redis_value = Value::Bulk(vec![
Value::Data("flags".into()),
Value::Bulk(vec![
Value::Data("on".into()),
Value::Data("allchannels".into()),
]),
Value::Data("passwords".into()),
Value::Bulk(vec![]),
Value::Data("commands".into()),
Value::Data("-@all +get".into()),
Value::Data("keys".into()),
Value::Bulk(vec![Value::Data("pat:*".into())]),
]);
let acl_info = AclInfo::from_redis_value(&redis_value).expect("Parse successfully");
assert_eq!(
acl_info,
AclInfo {
flags: vec![Rule::On, Rule::Other("allchannels".into())],
passwords: vec![],
commands: vec![
Rule::RemoveCategory("all".to_owned()),
Rule::AddCommand("get".to_owned()),
],
keys: vec![Rule::Pattern("pat:*".to_owned())],
}
);
}
}