@@ -470,6 +470,30 @@ def parse_client_kill(response, **options):
470
470
return nativestr (response ) == 'OK'
471
471
472
472
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
+
473
497
class Redis (object ):
474
498
"""
475
499
Implementation of the Redis protocol.
@@ -526,6 +550,16 @@ class Redis(object):
526
550
string_keys_to_dict ('XREAD XREADGROUP' , parse_xread ),
527
551
string_keys_to_dict ('BGREWRITEAOF BGSAVE' , lambda r : True ),
528
552
{
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 ,
529
563
'CLIENT GETNAME' : lambda r : r and nativestr (r ),
530
564
'CLIENT ID' : int ,
531
565
'CLIENT KILL' : parse_client_kill ,
@@ -609,9 +643,9 @@ def from_url(cls, url, db=None, **kwargs):
609
643
610
644
For example::
611
645
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
615
649
616
650
Three URL schemes are supported:
617
651
@@ -640,7 +674,7 @@ def from_url(cls, url, db=None, **kwargs):
640
674
return cls (connection_pool = connection_pool )
641
675
642
676
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 ,
644
678
socket_connect_timeout = None ,
645
679
socket_keepalive = None , socket_keepalive_options = None ,
646
680
connection_pool = None , unix_socket_path = None ,
@@ -663,6 +697,7 @@ def __init__(self, host='localhost', port=6379,
663
697
664
698
kwargs = {
665
699
'db' : db ,
700
+ 'username' : username ,
666
701
'password' : password ,
667
702
'socket_timeout' : socket_timeout ,
668
703
'encoding' : encoding ,
@@ -867,6 +902,212 @@ def parse_response(self, connection, command_name, **options):
867
902
return response
868
903
869
904
# 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
+
870
1111
def bgrewriteaof (self ):
871
1112
"Tell the Redis server to rewrite the AOF file from data in memory."
872
1113
return self .execute_command ('BGREWRITEAOF' )
0 commit comments