Skip to content

Commit

Permalink
ACL SETUSER - add selectors and key based permissions (#2161)
Browse files Browse the repository at this point in the history
* acl setuser

* async tests

Co-authored-by: Chayim <chayim@users.noreply.github.com>
  • Loading branch information
dvora-h and chayim authored May 3, 2022
1 parent fa7b3f6 commit 5c99e27
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 8 deletions.
13 changes: 13 additions & 0 deletions redis/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,19 @@ def parse_acl_getuser(response, **options):
data["flags"] = list(map(str_if_bytes, data["flags"]))
data["passwords"] = list(map(str_if_bytes, data["passwords"]))
data["commands"] = str_if_bytes(data["commands"])
if isinstance(data["keys"], str) or isinstance(data["keys"], bytes):
data["keys"] = list(str_if_bytes(data["keys"]).split(" "))
if data["keys"] == [""]:
data["keys"] = []
if "channels" in data:
if isinstance(data["channels"], str) or isinstance(data["channels"], bytes):
data["channels"] = list(str_if_bytes(data["channels"]).split(" "))
if data["channels"] == [""]:
data["channels"] = []
if "selectors" in data:
data["selectors"] = [
list(map(str_if_bytes, selector)) for selector in data["selectors"]
]

# split 'commands' into separate 'categories' and 'commands' lists
commands, categories = [], []
Expand Down
32 changes: 28 additions & 4 deletions redis/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,11 @@ def acl_setuser(
nopass: bool = False,
passwords: Union[str, Iterable[str], None] = None,
hashed_passwords: Union[str, Iterable[str], None] = None,
categories: Union[Iterable[str], None] = None,
commands: Union[Iterable[str], None] = None,
keys: Union[Iterable[KeyT], None] = None,
categories: Optional[Iterable[str]] = None,
commands: Optional[Iterable[str]] = None,
keys: Optional[Iterable[KeyT]] = None,
channels: Optional[Iterable[ChannelT]] = None,
selectors: Optional[Iterable[Tuple[str, KeyT]]] = None,
reset: bool = False,
reset_keys: bool = False,
reset_passwords: bool = False,
Expand Down Expand Up @@ -342,7 +344,29 @@ def acl_setuser(
if keys:
for key in keys:
key = encoder.encode(key)
pieces.append(b"~%s" % key)
if not key.startswith(b"%") and not key.startswith(b"~"):
key = b"~%s" % key
pieces.append(key)

if channels:
for channel in channels:
channel = encoder.encode(channel)
pieces.append(b"&%s" % channel)

if selectors:
for cmd, key in selectors:
cmd = encoder.encode(cmd)
if not cmd.startswith(b"+") and not cmd.startswith(b"-"):
raise DataError(
f'Command "{encoder.decode(cmd, force=True)}" '
'must be prefixed with "+" or "-"'
)

key = encoder.encode(key)
if not key.startswith(b"%") and not key.startswith(b"~"):
key = b"~%s" % key

pieces.append(b"(%s %s)" % (cmd, key))

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

Expand Down
2 changes: 2 additions & 0 deletions tests/test_asyncio/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ async def test_acl_genpass(self, r: redis.Redis):
assert isinstance(password, str)

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

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

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

Expand Down
38 changes: 34 additions & 4 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,14 @@ def test_acl_cat_with_category(self, r):

@skip_if_server_version_lt("7.0.0")
@skip_if_redis_enterprise()
def test_acl_dryrun(self, r):
def test_acl_dryrun(self, r, request):
username = "redis-py-user"

def teardown():
r.acl_deluser(username)

request.addfinalizer(teardown)

r.acl_setuser(
username,
keys=["*"],
Expand Down Expand Up @@ -171,7 +177,7 @@ def test_acl_genpass(self, r):
r.acl_genpass(555)
assert isinstance(password, str)

@skip_if_server_version_lt("6.0.0")
@skip_if_server_version_lt("7.0.0")
@skip_if_redis_enterprise()
def test_acl_getuser_setuser(self, r, request):
username = "redis-py-user"
Expand Down Expand Up @@ -217,7 +223,7 @@ def teardown():
assert set(acl["commands"]) == {"+get", "+mget", "-hset"}
assert acl["enabled"] is True
assert "on" in acl["flags"]
assert set(acl["keys"]) == {b"cache:*", b"objects:*"}
assert set(acl["keys"]) == {"~cache:*", "~objects:*"}
assert len(acl["passwords"]) == 2

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

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

# test selectors
assert r.acl_setuser(
username,
enabled=True,
reset=True,
passwords=["+pass1", "+pass2"],
categories=["+set", "+@hash", "-geo"],
commands=["+get", "+mget", "-hset"],
keys=["cache:*", "objects:*"],
channels=["message:*"],
selectors=[("+set", "%W~app*")],
)
acl = r.acl_getuser(username)
assert set(acl["categories"]) == {"-@all", "+@set", "+@hash"}
assert set(acl["commands"]) == {"+get", "+mget", "-hset"}
assert acl["enabled"] is True
assert "on" in acl["flags"]
assert set(acl["keys"]) == {"~cache:*", "~objects:*"}
assert len(acl["passwords"]) == 2
assert set(acl["channels"]) == {"&message:*"}
assert acl["selectors"] == [
["commands", "-@all +set", "keys", "%W~app*", "channels", ""]
]

@skip_if_server_version_lt("6.0.0")
def test_acl_help(self, r):
res = r.acl_help()
Expand Down

0 comments on commit 5c99e27

Please sign in to comment.