summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordvora-h <67596500+dvora-h@users.noreply.github.com>2022-05-03 14:04:14 +0300
committerGitHub <noreply@github.com>2022-05-03 14:04:14 +0300
commit5c99e27459a047cd1334e1b87fb0623ac2c881db (patch)
tree67434cb616fdcfa0dfaf852f42fba0934cf3a0fe
parentfa7b3f6213625f248764b134ed2c82fcdba95d62 (diff)
downloadredis-py-5c99e27459a047cd1334e1b87fb0623ac2c881db.tar.gz
ACL SETUSER - add selectors and key based permissions (#2161)
* acl setuser * async tests Co-authored-by: Chayim <chayim@users.noreply.github.com>
-rwxr-xr-xredis/client.py13
-rw-r--r--redis/commands/core.py32
-rw-r--r--tests/test_asyncio/test_commands.py2
-rw-r--r--tests/test_commands.py38
4 files changed, 77 insertions, 8 deletions
diff --git a/redis/client.py b/redis/client.py
index e44f5ab..87c7991 100755
--- a/redis/client.py
+++ b/redis/client.py
@@ -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 = [], []
diff --git a/redis/commands/core.py b/redis/commands/core.py
index 8bbcda3..6526ef1 100644
--- a/redis/commands/core.py
+++ b/redis/commands/core.py
@@ -186,9 +186,11 @@ class ACLCommands(CommandsProtocol):
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,
@@ -342,7 +344,29 @@ class ACLCommands(CommandsProtocol):
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)
diff --git a/tests/test_asyncio/test_commands.py b/tests/test_asyncio/test_commands.py
index 7822040..dee8755 100644
--- a/tests/test_asyncio/test_commands.py
+++ b/tests/test_asyncio/test_commands.py
@@ -109,6 +109,7 @@ class TestRedisCommands:
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"
@@ -224,6 +225,7 @@ class TestRedisCommands:
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"
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 5975412..b7287b4 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -120,8 +120,14 @@ class TestRedisCommands:
@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=["*"],
@@ -171,7 +177,7 @@ class TestRedisCommands:
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"
@@ -217,7 +223,7 @@ class TestRedisCommands:
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
@@ -243,7 +249,7 @@ class TestRedisCommands:
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
@@ -278,6 +284,30 @@ class TestRedisCommands:
)
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()