Skip to content

Commit 8df8cd5

Browse files
committed
Added support for ACL commands
1 parent 1671ef2 commit 8df8cd5

File tree

7 files changed

+536
-20
lines changed

7 files changed

+536
-20
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ matrix:
2424
- python: 3.8
2525
env: TOXENV=py38-hiredis
2626
before_install:
27-
- wget http://download.redis.io/releases/redis-5.0.3.tar.gz && mkdir redis_install && tar -xvzf redis-5.0.3.tar.gz -C redis_install && cd redis_install/redis-5.0.3 && make && src/redis-server --daemonize yes && cd ../..
27+
- wget https://github.com/antirez/redis/archive/6.0-rc1.tar.gz && mkdir redis_install && tar -xvzf 6.0-rc1.tar.gz -C redis_install && cd redis_install/redis-6.0-rc1 && make && src/redis-server --daemonize yes && cd ../..
2828
- redis-cli info
2929
install:
3030
- pip install codecov tox

CHANGES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
without actually running any other commands. Thanks @brianmaissy.
55
#1233, #1234
66
* Removed support for end of life Python 3.4.
7+
* Added support for all ACL commands in Redis 6. Thanks @IAmATeaPot418
8+
for helping.
79
* 3.3.11
810
* Further fix for the SSLError -> TimeoutError mapping to work
911
on obscure releases of Python 2.7.

redis/client.py

Lines changed: 245 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,30 @@ def parse_client_kill(response, **options):
470470
return nativestr(response) == 'OK'
471471

472472

473+
def parse_acl_getuser(response, **options):
474+
if response is None:
475+
return None
476+
data = pairs_to_dict(response, decode_keys=True)
477+
478+
# convert everything but user-defined data in 'keys' to native strings
479+
data['flags'] = list(map(nativestr, data['flags']))
480+
data['passwords'] = list(map(nativestr, data['passwords']))
481+
data['commands'] = nativestr(data['commands'])
482+
483+
# split 'commands' into separate 'categories' and 'commands' lists
484+
commands, categories = [], []
485+
for command in data['commands'].split(' '):
486+
if '@' in command:
487+
categories.append(command)
488+
else:
489+
commands.append(command)
490+
491+
data['commands'] = commands
492+
data['categories'] = categories
493+
data['enabled'] = 'on' in data['flags']
494+
return data
495+
496+
473497
class Redis(object):
474498
"""
475499
Implementation of the Redis protocol.
@@ -526,6 +550,16 @@ class Redis(object):
526550
string_keys_to_dict('XREAD XREADGROUP', parse_xread),
527551
string_keys_to_dict('BGREWRITEAOF BGSAVE', lambda r: True),
528552
{
553+
'ACL CAT': lambda r: list(map(nativestr, r)),
554+
'ACL DELUSER': int,
555+
'ACL GENPASS': nativestr,
556+
'ACL GETUSER': parse_acl_getuser,
557+
'ACL LIST': lambda r: list(map(nativestr, r)),
558+
'ACL LOAD': bool_ok,
559+
'ACL SAVE': bool_ok,
560+
'ACL SETUSER': bool_ok,
561+
'ACL USERS': lambda r: list(map(nativestr, r)),
562+
'ACL WHOAMI': nativestr,
529563
'CLIENT GETNAME': lambda r: r and nativestr(r),
530564
'CLIENT ID': int,
531565
'CLIENT KILL': parse_client_kill,
@@ -609,9 +643,9 @@ def from_url(cls, url, db=None, **kwargs):
609643
610644
For example::
611645
612-
redis://[:password]@localhost:6379/0
613-
rediss://[:password]@localhost:6379/0
614-
unix://[:password]@/path/to/socket.sock?db=0
646+
redis://[[username]:[password]]@localhost:6379/0
647+
rediss://[[username]:[password]]@localhost:6379/0
648+
unix://[[username]:[password]]@/path/to/socket.sock?db=0
615649
616650
Three URL schemes are supported:
617651
@@ -640,7 +674,7 @@ def from_url(cls, url, db=None, **kwargs):
640674
return cls(connection_pool=connection_pool)
641675

642676
def __init__(self, host='localhost', port=6379,
643-
db=0, password=None, socket_timeout=None,
677+
db=0, username=None, password=None, socket_timeout=None,
644678
socket_connect_timeout=None,
645679
socket_keepalive=None, socket_keepalive_options=None,
646680
connection_pool=None, unix_socket_path=None,
@@ -663,6 +697,7 @@ def __init__(self, host='localhost', port=6379,
663697

664698
kwargs = {
665699
'db': db,
700+
'username': username,
666701
'password': password,
667702
'socket_timeout': socket_timeout,
668703
'encoding': encoding,
@@ -867,6 +902,212 @@ def parse_response(self, connection, command_name, **options):
867902
return response
868903

869904
# SERVER INFORMATION
905+
906+
# ACL methods
907+
def acl_cat(self, category=None):
908+
"""
909+
Returns a list of categories or commands within a category.
910+
911+
If ``category`` is not supplied, returns a list of all categories.
912+
If ``category`` is supplied, returns a list of all commands within
913+
that category.
914+
"""
915+
pieces = [category] if category else []
916+
return self.execute_command('ACL CAT', *pieces)
917+
918+
def acl_deluser(self, username):
919+
"Delete the ACL for the specified ``username``"
920+
return self.execute_command('ACL DELUSER', username)
921+
922+
def acl_genpass(self):
923+
"Generate a random password value"
924+
return self.execute_command('ACL GENPASS')
925+
926+
def acl_getuser(self, username):
927+
"""
928+
Get the ACL details for the specified ``username``.
929+
930+
If ``username`` does not exist, return None
931+
"""
932+
return self.execute_command('ACL GETUSER', username)
933+
934+
def acl_list(self):
935+
"Return a list of all ACLs on the server"
936+
return self.execute_command('ACL LIST')
937+
938+
def acl_load(self):
939+
"""
940+
Load ACL rules from the configured ``aclfile``.
941+
942+
Note that the server must be configured with the ``aclfile``
943+
directive to be able to load ACL rules from an aclfile.
944+
"""
945+
return self.execute_command('ACL LOAD')
946+
947+
def acl_save(self):
948+
"""
949+
Save ACL rules to the configured ``aclfile``.
950+
951+
Note that the server must be configured with the ``aclfile``
952+
directive to be able to save ACL rules to an aclfile.
953+
"""
954+
return self.execute_command('ACL SAVE')
955+
956+
def acl_setuser(self, username, enabled=False, nopass=False,
957+
passwords=None, hashed_passwords=None, categories=None,
958+
commands=None, keys=None, reset=False, reset_keys=False,
959+
reset_passwords=False):
960+
"""
961+
Create or update an ACL user.
962+
963+
Create or update the ACL for ``username``. If the user already exists,
964+
the existing ACL is completely overwritten and replaced with the
965+
specified values.
966+
967+
``enabled`` is a boolean indicating whether the user should be allowed
968+
to authenticate or not. Defaults to ``False``.
969+
970+
``nopass`` is a boolean indicating whether the can authenticate without
971+
a password. This cannot be True if ``passwords`` are also specified.
972+
973+
``passwords`` if specified is a list of plain text passwords
974+
to add to or remove from the user. Each password must be prefixed with
975+
a '+' to add or a '-' to remove. For convenience, the value of
976+
``add_passwords`` can be a simple prefixed string when adding or
977+
removing a single password.
978+
979+
``hashed_passwords`` if specified is a list of SHA-256 hashed passwords
980+
to add to or remove from the user. Each hashed password must be
981+
prefixed with a '+' to add or a '-' to remove. For convenience,
982+
the value of ``hashed_passwords`` can be a simple prefixed string when
983+
adding or removing a single password.
984+
985+
``categories`` if specified is a list of strings representing category
986+
permissions. Each string must be prefixed with either a '+' to add the
987+
category permission or a '-' to remove the category permission.
988+
989+
``commands`` if specified is a list of strings representing command
990+
permissions. Each string must be prefixed with either a '+' to add the
991+
command permission or a '-' to remove the command permission.
992+
993+
``keys`` if specified is a list of key patterns to grant the user
994+
access to. Keys patterns allow '*' to support wildcard matching. For
995+
example, '*' grants access to all keys while 'cache:*' grants access
996+
to all keys that are prefixed with 'cache:'. ``keys`` should not be
997+
prefixed with a '~'.
998+
999+
``reset`` is a boolean indicating whether the user should be fully
1000+
reset prior to applying the new ACL. Setting this to True will
1001+
remove all existing passwords, flags and privileges from the user and
1002+
then apply the specified rules. If this is False, the user's existing
1003+
passwords, flags and privileges will be kept and any new specified
1004+
rules will be applied on top.
1005+
1006+
``reset_keys`` is a boolean indicating whether the user's key
1007+
permissions should be reset prior to applying any new key permissions
1008+
specified in ``keys``. If this is False, the user's existing
1009+
key permissions will be kept and any new specified key permissions
1010+
will be applied on top.
1011+
1012+
``reset_passwords`` is a boolean indicating whether to remove all
1013+
existing passwords and the 'nopass' flag from the user prior to
1014+
applying any new passwords specified in 'passwords' or
1015+
'hashed_passwords'. If this is False, the user's existing passwords
1016+
and 'nopass' status will be kept and any new specified passwords
1017+
or hashed_passwords will be applied on top.
1018+
"""
1019+
encoder = self.connection_pool.get_encoder()
1020+
pieces = [username]
1021+
1022+
if reset:
1023+
pieces.append(b'reset')
1024+
1025+
if reset_keys:
1026+
pieces.append(b'resetkeys')
1027+
1028+
if reset_passwords:
1029+
pieces.append(b'resetpass')
1030+
1031+
if enabled:
1032+
pieces.append(b'on')
1033+
else:
1034+
pieces.append(b'off')
1035+
1036+
if (passwords or hashed_passwords) and nopass:
1037+
raise DataError('Cannot set \'nopass\' and supply '
1038+
'\'passwords\' or \'hashed_passwords\'')
1039+
1040+
if passwords:
1041+
# as most users will have only one password, allow remove_passwords
1042+
# to be specified as a simple string or a list
1043+
passwords = list_or_args(passwords, [])
1044+
for i, password in enumerate(passwords):
1045+
password = encoder.encode(password)
1046+
if password.startswith(b'+'):
1047+
pieces.append(b'>%s' % password[1:])
1048+
elif password.startswith(b'-'):
1049+
pieces.append(b'<%s' % password[1:])
1050+
else:
1051+
raise DataError('Password %d must be prefixeed with a '
1052+
'"+" to add or a "-" to remove' % i)
1053+
1054+
if hashed_passwords:
1055+
# as most users will have only one password, allow remove_passwords
1056+
# to be specified as a simple string or a list
1057+
hashed_passwords = list_or_args(hashed_passwords, [])
1058+
for i, hashed_password in enumerate(hashed_passwords):
1059+
hashed_password = encoder.encode(hashed_password)
1060+
if hashed_password.startswith(b'+'):
1061+
pieces.append(b'#%s' % hashed_password[1:])
1062+
elif hashed_password.startswith(b'-'):
1063+
pieces.append(b'!%s' % hashed_password[1:])
1064+
else:
1065+
raise DataError('Hashed %d password must be prefixeed '
1066+
'with a "+" to add or a "-" to remove' % i)
1067+
1068+
if nopass:
1069+
pieces.append(b'nopass')
1070+
1071+
if categories:
1072+
for category in categories:
1073+
category = encoder.encode(category)
1074+
# categories can be prefixed with one of (+@, +, -@, -)
1075+
if category.startswith(b'+@'):
1076+
pieces.append(category)
1077+
elif category.startswith(b'+'):
1078+
pieces.append(b'+@%s' % category[1:])
1079+
elif category.startswith(b'-@'):
1080+
pieces.append(category)
1081+
elif category.startswith(b'-'):
1082+
pieces.append(b'-@%s' % category[1:])
1083+
else:
1084+
raise DataError('Category "%s" must be prefixed with '
1085+
'"+" or "-"'
1086+
% encoder.decode(category, force=True))
1087+
if commands:
1088+
for cmd in commands:
1089+
cmd = encoder.encode(cmd)
1090+
if not cmd.startswith(b'+') and not cmd.startswith(b'-'):
1091+
raise DataError('Command "%s" must be prefixed with '
1092+
'"+" or "-"'
1093+
% encoder.decode(cmd, force=True))
1094+
pieces.append(cmd)
1095+
1096+
if keys:
1097+
for key in keys:
1098+
key = encoder.encode(key)
1099+
pieces.append(b'~%s' % key)
1100+
1101+
return self.execute_command('ACL SETUSER', *pieces)
1102+
1103+
def acl_users(self):
1104+
"Returns a list of all registered users on the server."
1105+
return self.execute_command('ACL USERS')
1106+
1107+
def acl_whoami(self):
1108+
"Get the username for the current connection"
1109+
return self.execute_command('ACL WHOAMI')
1110+
8701111
def bgrewriteaof(self):
8711112
"Tell the Redis server to rewrite the AOF file from data in memory."
8721113
return self.execute_command('BGREWRITEAOF')

0 commit comments

Comments
 (0)