summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES2
-rwxr-xr-xredis/client.py61
-rw-r--r--tests/conftest.py29
-rw-r--r--tests/test_commands.py44
4 files changed, 126 insertions, 10 deletions
diff --git a/CHANGES b/CHANGES
index 2b7dbb8..dfae2a1 100644
--- a/CHANGES
+++ b/CHANGES
@@ -8,6 +8,8 @@
@abrookins. #1365
* Added support for the LPOS command available in Redis 6.0.6. Thanks
@aparcar #1353/#1354
+ * Added support for the ACL LOG command available in Redis 6. Thanks
+ @2014BDuck. #1307
* 3.5.3 (June 1, 2020)
* Restore try/except clauses to __del__ methods. These will be removed
in 4.0 when more explicit resource management if enforced. #1339
diff --git a/redis/client.py b/redis/client.py
index 02db578..1560c96 100755
--- a/redis/client.py
+++ b/redis/client.py
@@ -495,6 +495,43 @@ def parse_acl_getuser(response, **options):
return data
+def parse_acl_log(response, **options):
+ if response is None:
+ return None
+ if isinstance(response, list):
+ data = []
+ for log in response:
+ log_data = pairs_to_dict(log, True, True)
+ client_info = log_data.get('client-info', '')
+ log_data["client-info"] = parse_client_info(client_info)
+
+ # float() is lossy comparing to the "double" in C
+ log_data["age-seconds"] = float(log_data["age-seconds"])
+ data.append(log_data)
+ else:
+ data = bool_ok(response)
+ return data
+
+
+def parse_client_info(value):
+ """
+ Parsing client-info in ACL Log in following format.
+ "key1=value1 key2=value2 key3=value3"
+ """
+ client_info = {}
+ infos = value.split(" ")
+ for info in infos:
+ key, value = info.split("=")
+ client_info[key] = value
+
+ # Those fields are definded as int in networking.c
+ for int_key in {"id", "age", "idle", "db", "sub", "psub",
+ "multi", "qbuf", "qbuf-free", "obl",
+ "oll", "omem"}:
+ client_info[int_key] = int(client_info[int_key])
+ return client_info
+
+
def parse_module_result(response):
if isinstance(response, ModuleError):
raise response
@@ -563,6 +600,7 @@ class Redis:
'ACL GETUSER': parse_acl_getuser,
'ACL LIST': lambda r: list(map(str_if_bytes, r)),
'ACL LOAD': bool_ok,
+ 'ACL LOG': parse_acl_log,
'ACL SAVE': bool_ok,
'ACL SETUSER': bool_ok,
'ACL USERS': lambda r: list(map(str_if_bytes, r)),
@@ -949,6 +987,29 @@ class Redis:
"Return a list of all ACLs on the server"
return self.execute_command('ACL LIST')
+ def acl_log(self, count=None):
+ """
+ Get ACL logs as a list.
+ :param int count: Get logs[0:count].
+ :rtype: List.
+ """
+ args = []
+ if count is not None:
+ if not isinstance(count, int):
+ raise DataError('ACL LOG count must be an '
+ 'integer')
+ args.append(count)
+
+ return self.execute_command('ACL LOG', *args)
+
+ def acl_log_reset(self):
+ """
+ Reset ACL logs.
+ :rtype: Boolean.
+ """
+ args = [b'RESET']
+ return self.execute_command('ACL LOG', *args)
+
def acl_load(self):
"""
Load ACL rules from the configured ``aclfile``.
diff --git a/tests/conftest.py b/tests/conftest.py
index 26893db..cd4d489 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,6 +2,7 @@ import pytest
import random
import redis
from distutils.version import StrictVersion
+from redis.connection import parse_url
from unittest.mock import Mock
from urllib.parse import urlparse
@@ -60,19 +61,31 @@ def skip_unless_arch_bits(arch_bits):
reason="server is not {}-bit".format(arch_bits))
-def _get_client(cls, request, single_connection_client=True, **kwargs):
+def _get_client(cls, request, single_connection_client=True, flushdb=True,
+ **kwargs):
+ """
+ Helper for fixtures or tests that need a Redis client
+
+ Uses the "--redis-url" command line argument for connection info. Unlike
+ ConnectionPool.from_url, keyword arguments to this function override
+ values specified in the URL.
+ """
redis_url = request.config.getoption("--redis-url")
- client = cls.from_url(redis_url, **kwargs)
+ url_options = parse_url(redis_url)
+ url_options.update(kwargs)
+ pool = redis.ConnectionPool(**url_options)
+ client = cls(connection_pool=pool)
if single_connection_client:
client = client.client()
if request:
def teardown():
- try:
- client.flushdb()
- except redis.ConnectionError:
- # handle cases where a test disconnected a client
- # just manually retry the flushdb
- client.flushdb()
+ if flushdb:
+ try:
+ client.flushdb()
+ except redis.ConnectionError:
+ # handle cases where a test disconnected a client
+ # just manually retry the flushdb
+ client.flushdb()
client.close()
client.connection_pool.disconnect()
request.addfinalizer(teardown)
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 38e2d1a..2113078 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -9,8 +9,13 @@ from string import ascii_letters
from redis.client import parse_info
from redis import exceptions
-from .conftest import (skip_if_server_version_lt, skip_if_server_version_gte,
- skip_unless_arch_bits, REDIS_6_VERSION)
+from .conftest import (
+ _get_client,
+ REDIS_6_VERSION,
+ skip_if_server_version_gte,
+ skip_if_server_version_lt,
+ skip_unless_arch_bits,
+)
@pytest.fixture()
@@ -194,6 +199,41 @@ class TestRedisCommands:
assert 'user %s off -@all' % username in users
@skip_if_server_version_lt(REDIS_6_VERSION)
+ def test_acl_log(self, r, request):
+ username = 'redis-py-user'
+
+ def teardown():
+ r.acl_deluser(username)
+
+ request.addfinalizer(teardown)
+ r.acl_setuser(username, enabled=True, reset=True,
+ commands=['+get', '+set', '+select'],
+ keys=['cache:*'], nopass=True)
+ r.acl_log_reset()
+
+ user_client = _get_client(redis.Redis, request, flushdb=False,
+ username=username)
+
+ # Valid operation and key
+ assert user_client.set('cache:0', 1)
+ assert user_client.get('cache:0') == b'1'
+
+ # Invalid key
+ with pytest.raises(exceptions.NoPermissionError):
+ user_client.get('violated_cache:0')
+
+ # Invalid operation
+ with pytest.raises(exceptions.NoPermissionError):
+ user_client.hset('cache:0', 'hkey', 'hval')
+
+ assert isinstance(r.acl_log(), list)
+ assert len(r.acl_log()) == 2
+ assert len(r.acl_log(count=1)) == 1
+ assert isinstance(r.acl_log()[0], dict)
+ assert 'client-info' in r.acl_log(count=1)[0]
+ assert r.acl_log_reset()
+
+ @skip_if_server_version_lt(REDIS_6_VERSION)
def test_acl_setuser_categories_without_prefix_fails(self, r, request):
username = 'redis-py-user'