1use 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#[derive(Debug, Eq, PartialEq)]
20pub enum Rule {
21 On,
23 Off,
26
27 AddCommand(String),
29 RemoveCommand(String),
31 AddCategory(String),
33 RemoveCategory(String),
35 AllCommands,
38 NoCommands,
40
41 AddPass(String),
43 RemovePass(String),
45 AddHashedPass(String),
47 RemoveHashedPass(String),
49 NoPass,
53 ResetPass,
55
56 Pattern(String),
58 AllKeys,
60 ResetKeys,
62
63 Reset,
66
67 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#[derive(Debug, Eq, PartialEq)]
114pub struct AclInfo {
115 pub flags: Vec<Rule>,
125 pub passwords: Vec<Rule>,
129 pub commands: Vec<Rule>,
138 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 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}