diff --git a/redis/client.py b/redis/client.py index 5c95e9fa65..7bd85ea5a3 100755 --- a/redis/client.py +++ b/redis/client.py @@ -517,6 +517,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 @@ -585,6 +622,7 @@ class Redis(object): 'ACL GETUSER': parse_acl_getuser, 'ACL LIST': lambda r: list(map(nativestr, r)), 'ACL LOAD': bool_ok, + 'ACL LOG': parse_acl_log, 'ACL SAVE': bool_ok, 'ACL SETUSER': bool_ok, 'ACL USERS': lambda r: list(map(nativestr, r)), @@ -966,6 +1004,29 @@ def acl_list(self): "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/test_commands.py b/tests/test_commands.py index 91bcbb3098..2a134ea259 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -195,6 +195,41 @@ def teardown(): users = r.acl_list() 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() + + r_test = redis.Redis(host='master', port=6379, db=9, + username=username) + + # Valid operation and key + r_test.set('cache:0', 1) + r_test.get('cache:0') + + # Invalid key + with pytest.raises(exceptions.NoPermissionError): + r_test.get('violated_cache:0') + + # Invalid operation + with pytest.raises(exceptions.NoPermissionError): + r_test.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'