Skip to content

Commit 7e66621

Browse files
committed
Merge remote-tracking branch 'upstream/master' into redis-6.0-tls
2 parents 2c37e62 + 26b5026 commit 7e66621

File tree

6 files changed

+605
-4
lines changed

6 files changed

+605
-4
lines changed

.travis.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,16 @@ cache:
1212
before_cache:
1313
- rm -rf /home/travis/.cargo/registry
1414

15-
before_install:
16-
- sudo apt-get update
17-
- sudo apt-get -y install stunnel # used to test TLS support
15+
# This is not a an official ppa but seems to be a well-known one. Replace it if
16+
# we can get Redis 6 from main stream package mananger.
17+
#
18+
# Refs:
19+
# - https://github.com/antirez/redis/issues/1732
20+
# - https://launchpad.net/~chris-lea/+archive/ubuntu/redis-server
21+
before_install: |
22+
sudo add-apt-repository ppa:chris-lea/redis-server -y
23+
sudo apt-get update
24+
sudo apt-get install redis-server=5:6.0.5-1chl1~xenial1 stunnel -y
1825
1926
matrix:
2027
include:

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ tokio-tls = { version = "0.3", optional = true }
5959
async-native-tls = { version = "0.3", optional = true }
6060

6161
[features]
62-
default = ["streams", "geospatial", "tokio-comp", "async-std-comp", "script"]
62+
default = ["acl", "streams", "geospatial", "tokio-comp", "async-std-comp", "script"]
63+
acl = []
6364
aio = ["bytes", "pin-project-lite", "futures-util", "futures-util/sink", "tokio/sync", "tokio/stream", "tokio/tcp", "tokio/uds", "tokio/io-util", "tokio-util", "tokio-util/codec", "combine/tokio-02"]
6465
tokio-rt-core = ["tokio-comp", "tokio/rt-core"]
6566
geospatial = []
@@ -98,6 +99,9 @@ required-features = ["async-std-comp"]
9899
name = "parser"
99100
required-features = ["aio"]
100101

102+
[[test]]
103+
name = "test_acl"
104+
101105
[[bench]]
102106
name = "bench_basic"
103107
harness = false

src/acl.rs

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
//! Defines types to use with the ACL commands.
2+
3+
use crate::types::{
4+
ErrorKind, FromRedisValue, RedisError, RedisResult, RedisWrite, ToRedisArgs, Value,
5+
};
6+
7+
macro_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)]
20+
pub 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+
68+
impl ToRedisArgs for Rule {
69+
fn write_redis_args<W>(&self, out: &mut W)
70+
where
71+
W: ?Sized + RedisWrite,
72+
{
73+
use self::Rule::*;
74+
75+
match self {
76+
On => out.write_arg(b"on"),
77+
Off => out.write_arg(b"off"),
78+
79+
AddCommand(cmd) => out.write_arg_fmt(format_args!("+{}", cmd)),
80+
RemoveCommand(cmd) => out.write_arg_fmt(format_args!("-{}", cmd)),
81+
AddCategory(cat) => out.write_arg_fmt(format_args!("+@{}", cat)),
82+
RemoveCategory(cat) => out.write_arg_fmt(format_args!("-@{}", cat)),
83+
AllCommands => out.write_arg(b"allcommands"),
84+
NoCommands => out.write_arg(b"nocommands"),
85+
86+
AddPass(pass) => out.write_arg_fmt(format_args!(">{}", pass)),
87+
RemovePass(pass) => out.write_arg_fmt(format_args!("<{}", pass)),
88+
AddHashedPass(pass) => out.write_arg_fmt(format_args!("#{}", pass)),
89+
RemoveHashedPass(pass) => out.write_arg_fmt(format_args!("!{}", pass)),
90+
NoPass => out.write_arg(b"nopass"),
91+
ResetPass => out.write_arg(b"resetpass"),
92+
93+
Pattern(pat) => out.write_arg_fmt(format_args!("~{}", pat)),
94+
AllKeys => out.write_arg(b"allkeys"),
95+
ResetKeys => out.write_arg(b"resetkeys"),
96+
97+
Reset => out.write_arg(b"reset"),
98+
};
99+
}
100+
}
101+
102+
/// An info dictionary type storing Redis ACL information as multiple `Rule`.
103+
/// This type collects key/value data returned by the [`ACL GETUSER`][1] command.
104+
///
105+
/// [1]: https://redis.io/commands/acl-getuser
106+
#[derive(Debug, Eq, PartialEq)]
107+
pub struct AclInfo {
108+
/// Describes flag rules for the user. Represented by [`Rule::On`][1],
109+
/// [`Rule::Off`][2], [`Rule::AllKeys`][3], [`Rule::AllCommands`][4] and
110+
/// [`Rule::NoPass`][5].
111+
///
112+
/// [1]: ./enum.Rule.html#variant.On
113+
/// [2]: ./enum.Rule.html#variant.Off
114+
/// [3]: ./enum.Rule.html#variant.AllKeys
115+
/// [4]: ./enum.Rule.html#variant.AllCommands
116+
/// [5]: ./enum.Rule.html#variant.NoPass
117+
pub flags: Vec<Rule>,
118+
/// Describes the user's passwords. Represented by [`Rule::AddHashedPass`][1].
119+
///
120+
/// [1]: ./enum.Rule.html#variant.AddHashedPass
121+
pub passwords: Vec<Rule>,
122+
/// Describes capabilities of which commands the user can call.
123+
/// Represented by [`Rule::AddCommand`][1], [`Rule::AddCategory`][2],
124+
/// [`Rule::RemoveCommand`][3] and [`Rule::RemoveCategory`][4].
125+
///
126+
/// [1]: ./enum.Rule.html#variant.AddCommand
127+
/// [2]: ./enum.Rule.html#variant.AddCategory
128+
/// [3]: ./enum.Rule.html#variant.RemoveCommand
129+
/// [4]: ./enum.Rule.html#variant.RemoveCategory
130+
pub commands: Vec<Rule>,
131+
/// Describes patterns of keys which the user can access. Represented by
132+
/// [`Rule::Pattern`][1].
133+
///
134+
/// [1]: ./enum.Rule.html#variant.Pattern
135+
pub keys: Vec<Rule>,
136+
}
137+
138+
impl FromRedisValue for AclInfo {
139+
fn from_redis_value(v: &Value) -> RedisResult<Self> {
140+
let mut it = v
141+
.as_sequence()
142+
.ok_or_else(|| not_convertible_error!(v, ""))?
143+
.iter()
144+
.skip(1)
145+
.step_by(2);
146+
147+
let (flags, passwords, commands, keys) = match (it.next(), it.next(), it.next(), it.next())
148+
{
149+
(Some(flags), Some(passwords), Some(commands), Some(keys)) => {
150+
// Parse flags
151+
// Ref: https://git.io/JfNyb
152+
let flags = flags
153+
.as_sequence()
154+
.ok_or_else(|| {
155+
not_convertible_error!(flags, "Expect a bulk response of ACL flags")
156+
})?
157+
.iter()
158+
.map(|flag| match flag {
159+
Value::Data(flag) => match flag.as_slice() {
160+
b"on" => Ok(Rule::On),
161+
b"off" => Ok(Rule::Off),
162+
b"allkeys" => Ok(Rule::AllKeys),
163+
b"allcommands" => Ok(Rule::AllCommands),
164+
b"nopass" => Ok(Rule::NoPass),
165+
_ => Err(not_convertible_error!(flag, "Expect a valid ACL flag")),
166+
},
167+
_ => Err(not_convertible_error!(
168+
flag,
169+
"Expect an arbitrary binary data"
170+
)),
171+
})
172+
.collect::<RedisResult<_>>()?;
173+
174+
let passwords = passwords
175+
.as_sequence()
176+
.ok_or_else(|| {
177+
not_convertible_error!(flags, "Expect a bulk response of ACL flags")
178+
})?
179+
.iter()
180+
.map(|pass| Ok(Rule::AddHashedPass(String::from_redis_value(pass)?)))
181+
.collect::<RedisResult<_>>()?;
182+
183+
let commands = match commands {
184+
Value::Data(cmd) => std::str::from_utf8(cmd)?,
185+
_ => {
186+
return Err(not_convertible_error!(
187+
commands,
188+
"Expect a valid UTF8 string"
189+
))
190+
}
191+
}
192+
.split_terminator(' ')
193+
.map(|cmd| match cmd {
194+
x if x.starts_with("+@") => Ok(Rule::AddCategory(x[2..].to_owned())),
195+
x if x.starts_with("-@") => Ok(Rule::RemoveCategory(x[2..].to_owned())),
196+
x if x.starts_with('+') => Ok(Rule::AddCommand(x[1..].to_owned())),
197+
x if x.starts_with('-') => Ok(Rule::RemoveCommand(x[1..].to_owned())),
198+
_ => Err(not_convertible_error!(
199+
cmd,
200+
"Expect a command addition/removal"
201+
)),
202+
})
203+
.collect::<RedisResult<_>>()?;
204+
205+
let keys = keys
206+
.as_sequence()
207+
.ok_or_else(|| not_convertible_error!(keys, ""))?
208+
.iter()
209+
.map(|pat| Ok(Rule::Pattern(String::from_redis_value(pat)?)))
210+
.collect::<RedisResult<_>>()?;
211+
212+
(flags, passwords, commands, keys)
213+
}
214+
_ => {
215+
return Err(not_convertible_error!(
216+
v,
217+
"Expect a resposne from `ACL GETUSER`"
218+
))
219+
}
220+
};
221+
222+
Ok(Self {
223+
flags,
224+
passwords,
225+
commands,
226+
keys,
227+
})
228+
}
229+
}
230+
231+
#[cfg(test)]
232+
mod tests {
233+
use super::*;
234+
235+
macro_rules! assert_args {
236+
($rule:expr, $arg:expr) => {
237+
assert_eq!($rule.to_redis_args(), vec![$arg.to_vec()]);
238+
};
239+
}
240+
241+
#[test]
242+
fn test_rule_to_arg() {
243+
use self::Rule::*;
244+
245+
assert_args!(On, b"on");
246+
assert_args!(Off, b"off");
247+
assert_args!(AddCommand("set".to_owned()), b"+set");
248+
assert_args!(RemoveCommand("set".to_owned()), b"-set");
249+
assert_args!(AddCategory("hyperloglog".to_owned()), b"+@hyperloglog");
250+
assert_args!(RemoveCategory("hyperloglog".to_owned()), b"-@hyperloglog");
251+
assert_args!(AllCommands, b"allcommands");
252+
assert_args!(NoCommands, b"nocommands");
253+
assert_args!(AddPass("mypass".to_owned()), b">mypass");
254+
assert_args!(RemovePass("mypass".to_owned()), b"<mypass");
255+
assert_args!(
256+
AddHashedPass(
257+
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2".to_owned()
258+
),
259+
b"#c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
260+
);
261+
assert_args!(
262+
RemoveHashedPass(
263+
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2".to_owned()
264+
),
265+
b"!c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
266+
);
267+
assert_args!(NoPass, b"nopass");
268+
assert_args!(Pattern("pat:*".to_owned()), b"~pat:*");
269+
assert_args!(AllKeys, b"allkeys");
270+
assert_args!(ResetKeys, b"resetkeys");
271+
assert_args!(Reset, b"reset");
272+
}
273+
274+
#[test]
275+
fn test_from_redis_value() {
276+
let redis_value = Value::Bulk(vec![
277+
Value::Data("flags".into()),
278+
Value::Bulk(vec![Value::Data("on".into())]),
279+
Value::Data("passwords".into()),
280+
Value::Bulk(vec![]),
281+
Value::Data("commands".into()),
282+
Value::Data("-@all +get".into()),
283+
Value::Data("keys".into()),
284+
Value::Bulk(vec![Value::Data("pat:*".into())]),
285+
]);
286+
let acl_info = AclInfo::from_redis_value(&redis_value).expect("Parse successfully");
287+
288+
assert_eq!(
289+
acl_info,
290+
AclInfo {
291+
flags: vec![Rule::On],
292+
passwords: vec![],
293+
commands: vec![
294+
Rule::RemoveCategory("all".to_owned()),
295+
Rule::AddCommand("get".to_owned()),
296+
],
297+
keys: vec![Rule::Pattern("pat:*".to_owned())],
298+
}
299+
);
300+
}
301+
}

0 commit comments

Comments
 (0)