diff options
| author | Andy McCurdy <andy@andymccurdy.com> | 2020-08-19 10:46:02 -0700 |
|---|---|---|
| committer | Andy McCurdy <andy@andymccurdy.com> | 2020-08-19 10:46:02 -0700 |
| commit | b80d423cc531c5db972d35ba72424cf9cf8772ff (patch) | |
| tree | d1af550e0d749fa3f53d31b03dd5e84e0537108e | |
| parent | f9ab1d35f8ec633a25c223ed979cef34a68072fe (diff) | |
| download | redis-py-b80d423cc531c5db972d35ba72424cf9cf8772ff.tar.gz | |
Added the ACL LOG command available in Redis 6
`acl_log()` returns a list of dictionaries, each describing a log entry.
`acl_log_reset()` instructs the server to truncate the log.
Thanks @2014BDuck
Fixes #1307
| -rw-r--r-- | CHANGES | 2 | ||||
| -rwxr-xr-x | redis/client.py | 61 | ||||
| -rw-r--r-- | tests/conftest.py | 29 | ||||
| -rw-r--r-- | tests/test_commands.py | 44 |
4 files changed, 126 insertions, 10 deletions
@@ -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' |
