redis/
acl.rs

1//! Defines types to use with the ACL commands.
2
3use crate::types::{
4    ErrorKind, FromRedisValue, RedisError, RedisResult, RedisWrite, ToRedisArgs, Value,
5};
6
7macro_rules! not_convertible_error {
8    ($v:expr, $det:expr) => {
9        RedisError::from((
10            ErrorKind::TypeError,
11            "Response type not convertible",
12            format!("{:?} (response was {:?})", $det, $v),
13        ))
14    };
15}
16
17/// ACL rules are used in order to activate or remove a flag, or to perform a
18/// given change to the user ACL, which under the hood are just single words.
19#[derive(Debug, Eq, PartialEq)]
20pub enum Rule {
21    /// Enable the user: it is possible to authenticate as this user.
22    On,
23    /// Disable the user: it's no longer possible to authenticate with this
24    /// user, however the already authenticated connections will still work.
25    Off,
26
27    /// Add the command to the list of commands the user can call.
28    AddCommand(String),
29    /// Remove the command to the list of commands the user can call.
30    RemoveCommand(String),
31    /// Add all the commands in such category to be called by the user.
32    AddCategory(String),
33    /// Remove the commands from such category the client can call.
34    RemoveCategory(String),
35    /// Alias for `+@all`. Note that it implies the ability to execute all the
36    /// future commands loaded via the modules system.
37    AllCommands,
38    /// Alias for `-@all`.
39    NoCommands,
40
41    /// Add this password to the list of valid password for the user.
42    AddPass(String),
43    /// Remove this password from the list of valid passwords.
44    RemovePass(String),
45    /// Add this SHA-256 hash value to the list of valid passwords for the user.
46    AddHashedPass(String),
47    /// Remove this hash value from from the list of valid passwords
48    RemoveHashedPass(String),
49    /// All the set passwords of the user are removed, and the user is flagged
50    /// as requiring no password: it means that every password will work
51    /// against this user.
52    NoPass,
53    /// Flush the list of allowed passwords. Moreover removes the _nopass_ status.
54    ResetPass,
55
56    /// Add a pattern of keys that can be mentioned as part of commands.
57    Pattern(String),
58    /// Alias for `~*`.
59    AllKeys,
60    /// Flush the list of allowed keys patterns.
61    ResetKeys,
62
63    /// Performs the following actions: `resetpass`, `resetkeys`, `off`, `-@all`.
64    /// The user returns to the same state it has immediately after its creation.
65    Reset,
66
67    /// Raw text of [`ACL rule`][1]  that not enumerated above.
68    ///
69    /// [1]: https://redis.io/docs/manual/security/acl
70    Other(String),
71}
72
73impl ToRedisArgs for Rule {
74    fn write_redis_args<W>(&self, out: &mut W)
75    where
76        W: ?Sized + RedisWrite,
77    {
78        use self::Rule::*;
79
80        match self {
81            On => out.write_arg(b"on"),
82            Off => out.write_arg(b"off"),
83
84            AddCommand(cmd) => out.write_arg_fmt(format_args!("+{cmd}")),
85            RemoveCommand(cmd) => out.write_arg_fmt(format_args!("-{cmd}")),
86            AddCategory(cat) => out.write_arg_fmt(format_args!("+@{cat}")),
87            RemoveCategory(cat) => out.write_arg_fmt(format_args!("-@{cat}")),
88            AllCommands => out.write_arg(b"allcommands"),
89            NoCommands => out.write_arg(b"nocommands"),
90
91            AddPass(pass) => out.write_arg_fmt(format_args!(">{pass}")),
92            RemovePass(pass) => out.write_arg_fmt(format_args!("<{pass}")),
93            AddHashedPass(pass) => out.write_arg_fmt(format_args!("#{pass}")),
94            RemoveHashedPass(pass) => out.write_arg_fmt(format_args!("!{pass}")),
95            NoPass => out.write_arg(b"nopass"),
96            ResetPass => out.write_arg(b"resetpass"),
97
98            Pattern(pat) => out.write_arg_fmt(format_args!("~{pat}")),
99            AllKeys => out.write_arg(b"allkeys"),
100            ResetKeys => out.write_arg(b"resetkeys"),
101
102            Reset => out.write_arg(b"reset"),
103
104            Other(rule) => out.write_arg(rule.as_bytes()),
105        };
106    }
107}
108
109/// An info dictionary type storing Redis ACL information as multiple `Rule`.
110/// This type collects key/value data returned by the [`ACL GETUSER`][1] command.
111///
112/// [1]: https://redis.io/commands/acl-getuser
113#[derive(Debug, Eq, PartialEq)]
114pub struct AclInfo {
115    /// Describes flag rules for the user. Represented by [`Rule::On`][1],
116    /// [`Rule::Off`][2], [`Rule::AllKeys`][3], [`Rule::AllCommands`][4] and
117    /// [`Rule::NoPass`][5].
118    ///
119    /// [1]: ./enum.Rule.html#variant.On
120    /// [2]: ./enum.Rule.html#variant.Off
121    /// [3]: ./enum.Rule.html#variant.AllKeys
122    /// [4]: ./enum.Rule.html#variant.AllCommands
123    /// [5]: ./enum.Rule.html#variant.NoPass
124    pub flags: Vec<Rule>,
125    /// Describes the user's passwords. Represented by [`Rule::AddHashedPass`][1].
126    ///
127    /// [1]: ./enum.Rule.html#variant.AddHashedPass
128    pub passwords: Vec<Rule>,
129    /// Describes capabilities of which commands the user can call.
130    /// Represented by [`Rule::AddCommand`][1], [`Rule::AddCategory`][2],
131    /// [`Rule::RemoveCommand`][3] and [`Rule::RemoveCategory`][4].
132    ///
133    /// [1]: ./enum.Rule.html#variant.AddCommand
134    /// [2]: ./enum.Rule.html#variant.AddCategory
135    /// [3]: ./enum.Rule.html#variant.RemoveCommand
136    /// [4]: ./enum.Rule.html#variant.RemoveCategory
137    pub commands: Vec<Rule>,
138    /// Describes patterns of keys which the user can access. Represented by
139    /// [`Rule::Pattern`][1].
140    ///
141    /// [1]: ./enum.Rule.html#variant.Pattern
142    pub keys: Vec<Rule>,
143}
144
145impl FromRedisValue for AclInfo {
146    fn from_redis_value(v: &Value) -> RedisResult<Self> {
147        let mut it = v
148            .as_sequence()
149            .ok_or_else(|| not_convertible_error!(v, ""))?
150            .iter()
151            .skip(1)
152            .step_by(2);
153
154        let (flags, passwords, commands, keys) = match (it.next(), it.next(), it.next(), it.next())
155        {
156            (Some(flags), Some(passwords), Some(commands), Some(keys)) => {
157                // Parse flags
158                // Ref: https://github.com/redis/redis/blob/0cabe0cfa7290d9b14596ec38e0d0a22df65d1df/src/acl.c#L83-L90
159                let flags = flags
160                    .as_sequence()
161                    .ok_or_else(|| {
162                        not_convertible_error!(flags, "Expect an array response of ACL flags")
163                    })?
164                    .iter()
165                    .map(|flag| match flag {
166                        Value::BulkString(flag) => match flag.as_slice() {
167                            b"on" => Ok(Rule::On),
168                            b"off" => Ok(Rule::Off),
169                            b"allkeys" => Ok(Rule::AllKeys),
170                            b"allcommands" => Ok(Rule::AllCommands),
171                            b"nopass" => Ok(Rule::NoPass),
172                            other => Ok(Rule::Other(String::from_utf8_lossy(other).into_owned())),
173                        },
174                        _ => Err(not_convertible_error!(
175                            flag,
176                            "Expect an arbitrary binary data"
177                        )),
178                    })
179                    .collect::<RedisResult<_>>()?;
180
181                let passwords = passwords
182                    .as_sequence()
183                    .ok_or_else(|| {
184                        not_convertible_error!(flags, "Expect an array response of ACL flags")
185                    })?
186                    .iter()
187                    .map(|pass| Ok(Rule::AddHashedPass(String::from_redis_value(pass)?)))
188                    .collect::<RedisResult<_>>()?;
189
190                let commands = match commands {
191                    Value::BulkString(cmd) => std::str::from_utf8(cmd)?,
192                    _ => {
193                        return Err(not_convertible_error!(
194                            commands,
195                            "Expect a valid UTF8 string"
196                        ))
197                    }
198                }
199                .split_terminator(' ')
200                .map(|cmd| match cmd {
201                    x if x.starts_with("+@") => Ok(Rule::AddCategory(x[2..].to_owned())),
202                    x if x.starts_with("-@") => Ok(Rule::RemoveCategory(x[2..].to_owned())),
203                    x if x.starts_with('+') => Ok(Rule::AddCommand(x[1..].to_owned())),
204                    x if x.starts_with('-') => Ok(Rule::RemoveCommand(x[1..].to_owned())),
205                    _ => Err(not_convertible_error!(
206                        cmd,
207                        "Expect a command addition/removal"
208                    )),
209                })
210                .collect::<RedisResult<_>>()?;
211
212                let keys = keys
213                    .as_sequence()
214                    .ok_or_else(|| not_convertible_error!(keys, ""))?
215                    .iter()
216                    .map(|pat| Ok(Rule::Pattern(String::from_redis_value(pat)?)))
217                    .collect::<RedisResult<_>>()?;
218
219                (flags, passwords, commands, keys)
220            }
221            _ => {
222                return Err(not_convertible_error!(
223                    v,
224                    "Expect a response from `ACL GETUSER`"
225                ))
226            }
227        };
228
229        Ok(Self {
230            flags,
231            passwords,
232            commands,
233            keys,
234        })
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    macro_rules! assert_args {
243        ($rule:expr, $arg:expr) => {
244            assert_eq!($rule.to_redis_args(), vec![$arg.to_vec()]);
245        };
246    }
247
248    #[test]
249    fn test_rule_to_arg() {
250        use self::Rule::*;
251
252        assert_args!(On, b"on");
253        assert_args!(Off, b"off");
254        assert_args!(AddCommand("set".to_owned()), b"+set");
255        assert_args!(RemoveCommand("set".to_owned()), b"-set");
256        assert_args!(AddCategory("hyperloglog".to_owned()), b"+@hyperloglog");
257        assert_args!(RemoveCategory("hyperloglog".to_owned()), b"-@hyperloglog");
258        assert_args!(AllCommands, b"allcommands");
259        assert_args!(NoCommands, b"nocommands");
260        assert_args!(AddPass("mypass".to_owned()), b">mypass");
261        assert_args!(RemovePass("mypass".to_owned()), b"<mypass");
262        assert_args!(
263            AddHashedPass(
264                "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2".to_owned()
265            ),
266            b"#c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
267        );
268        assert_args!(
269            RemoveHashedPass(
270                "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2".to_owned()
271            ),
272            b"!c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
273        );
274        assert_args!(NoPass, b"nopass");
275        assert_args!(Pattern("pat:*".to_owned()), b"~pat:*");
276        assert_args!(AllKeys, b"allkeys");
277        assert_args!(ResetKeys, b"resetkeys");
278        assert_args!(Reset, b"reset");
279        assert_args!(Other("resetchannels".to_owned()), b"resetchannels");
280    }
281
282    #[test]
283    fn test_from_redis_value() {
284        let redis_value = Value::Array(vec![
285            Value::BulkString("flags".into()),
286            Value::Array(vec![
287                Value::BulkString("on".into()),
288                Value::BulkString("allchannels".into()),
289            ]),
290            Value::BulkString("passwords".into()),
291            Value::Array(vec![]),
292            Value::BulkString("commands".into()),
293            Value::BulkString("-@all +get".into()),
294            Value::BulkString("keys".into()),
295            Value::Array(vec![Value::BulkString("pat:*".into())]),
296        ]);
297        let acl_info = AclInfo::from_redis_value(&redis_value).expect("Parse successfully");
298
299        assert_eq!(
300            acl_info,
301            AclInfo {
302                flags: vec![Rule::On, Rule::Other("allchannels".into())],
303                passwords: vec![],
304                commands: vec![
305                    Rule::RemoveCategory("all".to_owned()),
306                    Rule::AddCommand("get".to_owned()),
307                ],
308                keys: vec![Rule::Pattern("pat:*".to_owned())],
309            }
310        );
311    }
312}