1use 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#[derive(Debug, Eq, PartialEq)]
15#[non_exhaustive]
16pub enum Rule {
17 On,
19 Off,
22
23 AddCommand(String),
25 RemoveCommand(String),
27 AddCategory(String),
29 RemoveCategory(String),
31 AllCommands,
34 NoCommands,
36
37 AddPass(String),
39 RemovePass(String),
41 AddHashedPass(String),
43 RemoveHashedPass(String),
45 NoPass,
49 ResetPass,
51
52 Pattern(String),
54 AllKeys,
56 ResetKeys,
58
59 Reset,
62
63 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#[derive(Debug, Eq, PartialEq)]
110pub struct AclInfo {
111 pub flags: Vec<Rule>,
121 pub passwords: Vec<Rule>,
125 pub commands: Vec<Rule>,
134 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 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}