Skip to content

Commit 5c99e27

Browse files
dvora-hchayim
andauthored
ACL SETUSER - add selectors and key based permissions (#2161)
* acl setuser * async tests Co-authored-by: Chayim <[email protected]>
1 parent fa7b3f6 commit 5c99e27

File tree

4 files changed

+77
-8
lines changed

4 files changed

+77
-8
lines changed

redis/client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,19 @@ def parse_acl_getuser(response, **options):
580580
data["flags"] = list(map(str_if_bytes, data["flags"]))
581581
data["passwords"] = list(map(str_if_bytes, data["passwords"]))
582582
data["commands"] = str_if_bytes(data["commands"])
583+
if isinstance(data["keys"], str) or isinstance(data["keys"], bytes):
584+
data["keys"] = list(str_if_bytes(data["keys"]).split(" "))
585+
if data["keys"] == [""]:
586+
data["keys"] = []
587+
if "channels" in data:
588+
if isinstance(data["channels"], str) or isinstance(data["channels"], bytes):
589+
data["channels"] = list(str_if_bytes(data["channels"]).split(" "))
590+
if data["channels"] == [""]:
591+
data["channels"] = []
592+
if "selectors" in data:
593+
data["selectors"] = [
594+
list(map(str_if_bytes, selector)) for selector in data["selectors"]
595+
]
583596

584597
# split 'commands' into separate 'categories' and 'commands' lists
585598
commands, categories = [], []

redis/commands/core.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,11 @@ def acl_setuser(
186186
nopass: bool = False,
187187
passwords: Union[str, Iterable[str], None] = None,
188188
hashed_passwords: Union[str, Iterable[str], None] = None,
189-
categories: Union[Iterable[str], None] = None,
190-
commands: Union[Iterable[str], None] = None,
191-
keys: Union[Iterable[KeyT], None] = None,
189+
categories: Optional[Iterable[str]] = None,
190+
commands: Optional[Iterable[str]] = None,
191+
keys: Optional[Iterable[KeyT]] = None,
192+
channels: Optional[Iterable[ChannelT]] = None,
193+
selectors: Optional[Iterable[Tuple[str, KeyT]]] = None,
192194
reset: bool = False,
193195
reset_keys: bool = False,
194196
reset_passwords: bool = False,
@@ -342,7 +344,29 @@ def acl_setuser(
342344
if keys:
343345
for key in keys:
344346
key = encoder.encode(key)
345-
pieces.append(b"~%s" % key)
347+
if not key.startswith(b"%") and not key.startswith(b"~"):
348+
key = b"~%s" % key
349+
pieces.append(key)
350+
351+
if channels:
352+
for channel in channels:
353+
channel = encoder.encode(channel)
354+
pieces.append(b"&%s" % channel)
355+
356+
if selectors:
357+
for cmd, key in selectors:
358+
cmd = encoder.encode(cmd)
359+
if not cmd.startswith(b"+") and not cmd.startswith(b"-"):
360+
raise DataError(
361+
f'Command "{encoder.decode(cmd, force=True)}" '
362+
'must be prefixed with "+" or "-"'
363+
)
364+
365+
key = encoder.encode(key)
366+
if not key.startswith(b"%") and not key.startswith(b"~"):
367+
key = b"~%s" % key
368+
369+
pieces.append(b"(%s %s)" % (cmd, key))
346370

347371
return self.execute_command("ACL SETUSER", *pieces, **kwargs)
348372

tests/test_asyncio/test_commands.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ async def test_acl_genpass(self, r: redis.Redis):
109109
assert isinstance(password, str)
110110

111111
@skip_if_server_version_lt(REDIS_6_VERSION)
112+
@skip_if_server_version_gte("7.0.0")
112113
async def test_acl_getuser_setuser(self, r: redis.Redis, request, event_loop):
113114
username = "redis-py-user"
114115

@@ -224,6 +225,7 @@ def teardown():
224225
assert len((await r.acl_getuser(username))["passwords"]) == 1
225226

226227
@skip_if_server_version_lt(REDIS_6_VERSION)
228+
@skip_if_server_version_gte("7.0.0")
227229
async def test_acl_list(self, r: redis.Redis, request, event_loop):
228230
username = "redis-py-user"
229231

tests/test_commands.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,14 @@ def test_acl_cat_with_category(self, r):
120120

121121
@skip_if_server_version_lt("7.0.0")
122122
@skip_if_redis_enterprise()
123-
def test_acl_dryrun(self, r):
123+
def test_acl_dryrun(self, r, request):
124124
username = "redis-py-user"
125+
126+
def teardown():
127+
r.acl_deluser(username)
128+
129+
request.addfinalizer(teardown)
130+
125131
r.acl_setuser(
126132
username,
127133
keys=["*"],
@@ -171,7 +177,7 @@ def test_acl_genpass(self, r):
171177
r.acl_genpass(555)
172178
assert isinstance(password, str)
173179

174-
@skip_if_server_version_lt("6.0.0")
180+
@skip_if_server_version_lt("7.0.0")
175181
@skip_if_redis_enterprise()
176182
def test_acl_getuser_setuser(self, r, request):
177183
username = "redis-py-user"
@@ -217,7 +223,7 @@ def teardown():
217223
assert set(acl["commands"]) == {"+get", "+mget", "-hset"}
218224
assert acl["enabled"] is True
219225
assert "on" in acl["flags"]
220-
assert set(acl["keys"]) == {b"cache:*", b"objects:*"}
226+
assert set(acl["keys"]) == {"~cache:*", "~objects:*"}
221227
assert len(acl["passwords"]) == 2
222228

223229
# test reset=False keeps existing ACL and applies new ACL on top
@@ -243,7 +249,7 @@ def teardown():
243249
assert set(acl["commands"]) == {"+get", "+mget"}
244250
assert acl["enabled"] is True
245251
assert "on" in acl["flags"]
246-
assert set(acl["keys"]) == {b"cache:*", b"objects:*"}
252+
assert set(acl["keys"]) == {"~cache:*", "~objects:*"}
247253
assert len(acl["passwords"]) == 2
248254

249255
# test removal of passwords
@@ -278,6 +284,30 @@ def teardown():
278284
)
279285
assert len(r.acl_getuser(username)["passwords"]) == 1
280286

287+
# test selectors
288+
assert r.acl_setuser(
289+
username,
290+
enabled=True,
291+
reset=True,
292+
passwords=["+pass1", "+pass2"],
293+
categories=["+set", "+@hash", "-geo"],
294+
commands=["+get", "+mget", "-hset"],
295+
keys=["cache:*", "objects:*"],
296+
channels=["message:*"],
297+
selectors=[("+set", "%W~app*")],
298+
)
299+
acl = r.acl_getuser(username)
300+
assert set(acl["categories"]) == {"-@all", "+@set", "+@hash"}
301+
assert set(acl["commands"]) == {"+get", "+mget", "-hset"}
302+
assert acl["enabled"] is True
303+
assert "on" in acl["flags"]
304+
assert set(acl["keys"]) == {"~cache:*", "~objects:*"}
305+
assert len(acl["passwords"]) == 2
306+
assert set(acl["channels"]) == {"&message:*"}
307+
assert acl["selectors"] == [
308+
["commands", "-@all +set", "keys", "%W~app*", "channels", ""]
309+
]
310+
281311
@skip_if_server_version_lt("6.0.0")
282312
def test_acl_help(self, r):
283313
res = r.acl_help()

0 commit comments

Comments
 (0)