redis/commands/
acl.rs

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