Unverified Commit 55c81f2c authored by Madelyn Olson's avatar Madelyn Olson Committed by GitHub
Browse files

ACL V2 - Selectors and key based permissions (#9974)



* Implemented selectors which provide multiple different sets of permissions to users
* Implemented key based permissions 
* Added a new ACL dry-run command to test permissions before execution
* Updated module APIs to support checking key based permissions
Co-authored-by: default avatarOran Agra <oran@redislabs.com>
parent 10bbeb68
...@@ -871,6 +871,10 @@ replica-priority 100 ...@@ -871,6 +871,10 @@ replica-priority 100
# commands. For instance ~* allows all the keys. The pattern # commands. For instance ~* allows all the keys. The pattern
# is a glob-style pattern like the one of KEYS. # is a glob-style pattern like the one of KEYS.
# It is possible to specify multiple patterns. # It is possible to specify multiple patterns.
# %R~<pattern> Add key read pattern that specifies which keys can be read
# from.
# %W~<pattern> Add key write pattern that specifies which keys can be
# written to.
# allkeys Alias for ~* # allkeys Alias for ~*
# resetkeys Flush the list of allowed keys patterns. # resetkeys Flush the list of allowed keys patterns.
# &<pattern> Add a glob-style pattern of Pub/Sub channels that can be # &<pattern> Add a glob-style pattern of Pub/Sub channels that can be
...@@ -896,6 +900,14 @@ replica-priority 100 ...@@ -896,6 +900,14 @@ replica-priority 100
# reset Performs the following actions: resetpass, resetkeys, off, # reset Performs the following actions: resetpass, resetkeys, off,
# -@all. The user returns to the same state it has immediately # -@all. The user returns to the same state it has immediately
# after its creation. # after its creation.
# (<options>) Create a new selector with the options specified within the
# parentheses and attach it to the user. Each option should be
# space separated. The first character must be ( and the last
# character must be ).
# clearselectors Remove all of the currently attached selectors.
# Note this does not change the "root" user permissions,
# which are the permissions directly applied onto the
# user (outside the parentheses).
# #
# ACL rules can be specified in any order: for instance you can start with # ACL rules can be specified in any order: for instance you can start with
# passwords, then flags, or key patterns. However note that the additive # passwords, then flags, or key patterns. However note that the additive
......
This diff is collapsed.
...@@ -6423,7 +6423,8 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in ...@@ -6423,7 +6423,8 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in
for (i = 0; i < ms->count; i++) { for (i = 0; i < ms->count; i++) {
struct redisCommand *mcmd; struct redisCommand *mcmd;
robj **margv; robj **margv;
int margc, *keyindex, numkeys, j; int margc, numkeys, j;
keyReference *keyindex;
mcmd = ms->commands[i].cmd; mcmd = ms->commands[i].cmd;
margc = ms->commands[i].argc; margc = ms->commands[i].argc;
...@@ -6434,7 +6435,7 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in ...@@ -6434,7 +6435,7 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in
keyindex = result.keys; keyindex = result.keys;
for (j = 0; j < numkeys; j++) { for (j = 0; j < numkeys; j++) {
robj *thiskey = margv[keyindex[j]]; robj *thiskey = margv[keyindex[j].pos];
int thisslot = keyHashSlot((char*)thiskey->ptr, int thisslot = keyHashSlot((char*)thiskey->ptr,
sdslen(thiskey->ptr)); sdslen(thiskey->ptr));
......
...@@ -3812,6 +3812,22 @@ struct redisCommandArg ACL_DELUSER_Args[] = { ...@@ -3812,6 +3812,22 @@ struct redisCommandArg ACL_DELUSER_Args[] = {
{0} {0}
}; };
/********** ACL DRYRUN ********************/
/* ACL DRYRUN history */
#define ACL_DRYRUN_History NULL
/* ACL DRYRUN tips */
#define ACL_DRYRUN_tips NULL
/* ACL DRYRUN argument table */
struct redisCommandArg ACL_DRYRUN_Args[] = {
{"username",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE},
{"command",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE},
{"arg",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE},
{0}
};
/********** ACL GENPASS ********************/ /********** ACL GENPASS ********************/
/* ACL GENPASS history */ /* ACL GENPASS history */
...@@ -3901,6 +3917,7 @@ struct redisCommandArg ACL_LOG_Args[] = { ...@@ -3901,6 +3917,7 @@ struct redisCommandArg ACL_LOG_Args[] = {
/* ACL SETUSER history */ /* ACL SETUSER history */
commandHistory ACL_SETUSER_History[] = { commandHistory ACL_SETUSER_History[] = {
{"6.2.0","Added Pub/Sub channel patterns."}, {"6.2.0","Added Pub/Sub channel patterns."},
{"7.0.0","Added selectors and key based permissions."},
{0} {0}
}; };
...@@ -3934,6 +3951,7 @@ struct redisCommandArg ACL_SETUSER_Args[] = { ...@@ -3934,6 +3951,7 @@ struct redisCommandArg ACL_SETUSER_Args[] = {
struct redisCommand ACL_Subcommands[] = { struct redisCommand ACL_Subcommands[] = {
{"cat","List the ACL categories or the commands inside a category","O(1) since the categories and commands are a fixed set.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_CAT_History,ACL_CAT_tips,aclCommand,-2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_CAT_Args}, {"cat","List the ACL categories or the commands inside a category","O(1) since the categories and commands are a fixed set.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_CAT_History,ACL_CAT_tips,aclCommand,-2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_CAT_Args},
{"deluser","Remove the specified ACL users and the associated rules","O(1) amortized time considering the typical user.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_DELUSER_History,ACL_DELUSER_tips,aclCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_DELUSER_Args}, {"deluser","Remove the specified ACL users and the associated rules","O(1) amortized time considering the typical user.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_DELUSER_History,ACL_DELUSER_tips,aclCommand,-3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_DELUSER_Args},
{"dryrun","Returns whether the user can execute the given command without executing the command.","O(1).","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_DRYRUN_History,ACL_DRYRUN_tips,aclCommand,-4,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_DRYRUN_Args},
{"genpass","Generate a pseudorandom secure password to use for ACL users","O(1)","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_GENPASS_History,ACL_GENPASS_tips,aclCommand,-2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_GENPASS_Args}, {"genpass","Generate a pseudorandom secure password to use for ACL users","O(1)","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_GENPASS_History,ACL_GENPASS_tips,aclCommand,-2,CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_GENPASS_Args},
{"getuser","Get the rules for a specific ACL user","O(N). Where N is the number of password, command and pattern rules that the user has.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_GETUSER_History,ACL_GETUSER_tips,aclCommand,3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_GETUSER_Args}, {"getuser","Get the rules for a specific ACL user","O(N). Where N is the number of password, command and pattern rules that the user has.","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_GETUSER_History,ACL_GETUSER_tips,aclCommand,3,CMD_ADMIN|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=ACL_GETUSER_Args},
{"help","Show helpful text about the different subcommands","O(1)","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_HELP_History,ACL_HELP_tips,aclCommand,2,CMD_LOADING|CMD_STALE|CMD_SENTINEL,0}, {"help","Show helpful text about the different subcommands","O(1)","6.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SERVER,ACL_HELP_History,ACL_HELP_tips,aclCommand,2,CMD_LOADING|CMD_STALE|CMD_SENTINEL,0},
...@@ -6830,8 +6848,8 @@ struct redisCommand redisCommandTable[] = { ...@@ -6830,8 +6848,8 @@ struct redisCommand redisCommandTable[] = {
{"renamenx","Rename a key, only if the new key does not exist","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,RENAMENX_History,RENAMENX_tips,renamenxCommand,3,CMD_WRITE|CMD_FAST,ACL_CATEGORY_KEYSPACE,{{CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{CMD_KEY_OW|CMD_KEY_INSERT,KSPEC_BS_INDEX,.bs.index={2},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=RENAMENX_Args}, {"renamenx","Rename a key, only if the new key does not exist","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,RENAMENX_History,RENAMENX_tips,renamenxCommand,3,CMD_WRITE|CMD_FAST,ACL_CATEGORY_KEYSPACE,{{CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{CMD_KEY_OW|CMD_KEY_INSERT,KSPEC_BS_INDEX,.bs.index={2},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=RENAMENX_Args},
{"restore","Create a key using the provided serialized value, previously obtained using DUMP.","O(1) to create the new key and additional O(N*M) to reconstruct the serialized value, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)+O(1*M) where M is small, so simply O(1). However for sorted set values the complexity is O(N*M*log(N)) because inserting values into sorted sets is O(log(N)).","2.6.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,RESTORE_History,RESTORE_tips,restoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_KEYSPACE|ACL_CATEGORY_DANGEROUS,{{CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=RESTORE_Args}, {"restore","Create a key using the provided serialized value, previously obtained using DUMP.","O(1) to create the new key and additional O(N*M) to reconstruct the serialized value, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)+O(1*M) where M is small, so simply O(1). However for sorted set values the complexity is O(N*M*log(N)) because inserting values into sorted sets is O(log(N)).","2.6.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,RESTORE_History,RESTORE_tips,restoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_KEYSPACE|ACL_CATEGORY_DANGEROUS,{{CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=RESTORE_Args},
{"scan","Incrementally iterate the keys space","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,SCAN_History,SCAN_tips,scanCommand,-2,CMD_READONLY,ACL_CATEGORY_KEYSPACE,.args=SCAN_Args}, {"scan","Incrementally iterate the keys space","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,SCAN_History,SCAN_tips,scanCommand,-2,CMD_READONLY,ACL_CATEGORY_KEYSPACE,.args=SCAN_Args},
{"sort","Sort the elements in a list, set or sorted set","O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is O(N).","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,SORT_History,SORT_tips,sortCommand,-2,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SET|ACL_CATEGORY_SORTEDSET|ACL_CATEGORY_LIST|ACL_CATEGORY_DANGEROUS,{{CMD_KEY_RO|CMD_KEY_ACCESS|CMD_KEY_INCOMPLETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{CMD_KEY_OW|CMD_KEY_UPDATE|CMD_KEY_INCOMPLETE,KSPEC_BS_UNKNOWN,{{0}},KSPEC_FK_UNKNOWN,{{0}}}},sortGetKeys,.args=SORT_Args}, {"sort","Sort the elements in a list, set or sorted set","O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is O(N).","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,SORT_History,SORT_tips,sortCommand,-2,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SET|ACL_CATEGORY_SORTEDSET|ACL_CATEGORY_LIST|ACL_CATEGORY_DANGEROUS,{{CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{CMD_KEY_RO|CMD_KEY_ACCESS|CMD_KEY_INCOMPLETE,KSPEC_BS_UNKNOWN,{{0}},KSPEC_FK_UNKNOWN,{{0}}},{CMD_KEY_OW|CMD_KEY_UPDATE|CMD_KEY_INCOMPLETE,KSPEC_BS_UNKNOWN,{{0}},KSPEC_FK_UNKNOWN,{{0}}}},sortGetKeys,.args=SORT_Args},
{"sort_ro","Sort the elements in a list, set or sorted set. Read-only variant of SORT.","O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is O(N).","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,SORT_RO_History,SORT_RO_tips,sortroCommand,-2,CMD_READONLY,ACL_CATEGORY_SET|ACL_CATEGORY_SORTEDSET|ACL_CATEGORY_LIST|ACL_CATEGORY_DANGEROUS,{{CMD_KEY_RO|CMD_KEY_ACCESS|CMD_KEY_INCOMPLETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=SORT_RO_Args}, {"sort_ro","Sort the elements in a list, set or sorted set. Read-only variant of SORT.","O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is O(N).","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,SORT_RO_History,SORT_RO_tips,sortroCommand,-2,CMD_READONLY,ACL_CATEGORY_SET|ACL_CATEGORY_SORTEDSET|ACL_CATEGORY_LIST|ACL_CATEGORY_DANGEROUS,{{CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{CMD_KEY_RO|CMD_KEY_ACCESS|CMD_KEY_INCOMPLETE,KSPEC_BS_UNKNOWN,{{0}},KSPEC_FK_UNKNOWN,{{0}}}},sortROGetKeys,.args=SORT_RO_Args},
{"touch","Alters the last access time of a key(s). Returns the number of existing keys specified.","O(N) where N is the number of keys that will be touched.","3.2.1",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,TOUCH_History,TOUCH_tips,touchCommand,-2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_KEYSPACE,{{CMD_KEY_RO,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={-1,1,0}}},.args=TOUCH_Args}, {"touch","Alters the last access time of a key(s). Returns the number of existing keys specified.","O(N) where N is the number of keys that will be touched.","3.2.1",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,TOUCH_History,TOUCH_tips,touchCommand,-2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_KEYSPACE,{{CMD_KEY_RO,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={-1,1,0}}},.args=TOUCH_Args},
{"ttl","Get the time to live for a key in seconds","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,TTL_History,TTL_tips,ttlCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_KEYSPACE,{{CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=TTL_Args}, {"ttl","Get the time to live for a key in seconds","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,TTL_History,TTL_tips,ttlCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_KEYSPACE,{{CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=TTL_Args},
{"type","Determine the type stored at key","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,TYPE_History,TYPE_tips,typeCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_KEYSPACE,{{CMD_KEY_RO,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=TYPE_Args}, {"type","Determine the type stored at key","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_GENERIC,TYPE_History,TYPE_tips,typeCommand,2,CMD_READONLY|CMD_FAST,ACL_CATEGORY_KEYSPACE,{{CMD_KEY_RO,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=TYPE_Args},
......
{
"DRYRUN": {
"summary": "Returns whether the user can execute the given command without executing the command.",
"complexity": "O(1).",
"group": "server",
"since": "7.0.0",
"arity": -4,
"container": "ACL",
"function": "aclCommand",
"history": [],
"command_flags": [
"ADMIN",
"NOSCRIPT",
"LOADING",
"STALE",
"SENTINEL"
],
"arguments": [
{
"name": "username",
"type": "string"
},
{
"name": "command",
"type": "string"
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
]
}
}
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
[ [
"6.2.0", "6.2.0",
"Added Pub/Sub channel patterns." "Added Pub/Sub channel patterns."
],
[
"7.0.0",
"Added selectors and key based permissions."
] ]
], ],
"command_flags": [ "command_flags": [
......
...@@ -21,8 +21,7 @@ ...@@ -21,8 +21,7 @@
{ {
"flags": [ "flags": [
"RO", "RO",
"ACCESS", "ACCESS"
"INCOMPLETE"
], ],
"begin_search": { "begin_search": {
"index": { "index": {
...@@ -37,6 +36,19 @@ ...@@ -37,6 +36,19 @@
} }
} }
}, },
{
"flags": [
"RO",
"ACCESS",
"INCOMPLETE"
],
"begin_search": {
"unknown": null
},
"find_keys": {
"unknown": null
}
},
{ {
"flags": [ "flags": [
"OW", "OW",
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
"since": "7.0.0", "since": "7.0.0",
"arity": -2, "arity": -2,
"function": "sortroCommand", "function": "sortroCommand",
"get_keys_function": "sortROGetKeys",
"command_flags": [ "command_flags": [
"READONLY" "READONLY"
], ],
...@@ -19,8 +20,7 @@ ...@@ -19,8 +20,7 @@
{ {
"flags": [ "flags": [
"RO", "RO",
"ACCESS", "ACCESS"
"INCOMPLETE"
], ],
"begin_search": { "begin_search": {
"index": { "index": {
...@@ -34,6 +34,19 @@ ...@@ -34,6 +34,19 @@
"limit": 0 "limit": 0
} }
} }
},
{
"flags": [
"RO",
"ACCESS",
"INCOMPLETE"
],
"begin_search": {
"unknown": null
},
"find_keys": {
"unknown": null
}
} }
], ],
"arguments": [ "arguments": [
......
...@@ -122,7 +122,7 @@ configEnum oom_score_adj_enum[] = { ...@@ -122,7 +122,7 @@ configEnum oom_score_adj_enum[] = {
}; };
configEnum acl_pubsub_default_enum[] = { configEnum acl_pubsub_default_enum[] = {
{"allchannels", USER_FLAG_ALLCHANNELS}, {"allchannels", SELECTOR_FLAG_ALLCHANNELS},
{"resetchannels", 0}, {"resetchannels", 0},
{NULL, 0} {NULL, 0}
}; };
...@@ -2811,7 +2811,7 @@ standardConfig configs[] = { ...@@ -2811,7 +2811,7 @@ standardConfig configs[] = {
createEnumConfig("maxmemory-policy", NULL, MODIFIABLE_CONFIG, maxmemory_policy_enum, server.maxmemory_policy, MAXMEMORY_NO_EVICTION, NULL, NULL), createEnumConfig("maxmemory-policy", NULL, MODIFIABLE_CONFIG, maxmemory_policy_enum, server.maxmemory_policy, MAXMEMORY_NO_EVICTION, NULL, NULL),
createEnumConfig("appendfsync", NULL, MODIFIABLE_CONFIG, aof_fsync_enum, server.aof_fsync, AOF_FSYNC_EVERYSEC, NULL, NULL), createEnumConfig("appendfsync", NULL, MODIFIABLE_CONFIG, aof_fsync_enum, server.aof_fsync, AOF_FSYNC_EVERYSEC, NULL, NULL),
createEnumConfig("oom-score-adj", NULL, MODIFIABLE_CONFIG, oom_score_adj_enum, server.oom_score_adj, OOM_SCORE_ADJ_NO, NULL, updateOOMScoreAdj), createEnumConfig("oom-score-adj", NULL, MODIFIABLE_CONFIG, oom_score_adj_enum, server.oom_score_adj, OOM_SCORE_ADJ_NO, NULL, updateOOMScoreAdj),
createEnumConfig("acl-pubsub-default", NULL, MODIFIABLE_CONFIG, acl_pubsub_default_enum, server.acl_pubsub_default, USER_FLAG_ALLCHANNELS, NULL, NULL), createEnumConfig("acl-pubsub-default", NULL, MODIFIABLE_CONFIG, acl_pubsub_default_enum, server.acl_pubsub_default, SELECTOR_FLAG_ALLCHANNELS, NULL, NULL),
createEnumConfig("sanitize-dump-payload", NULL, DEBUG_CONFIG | MODIFIABLE_CONFIG, sanitize_dump_payload_enum, server.sanitize_dump_payload, SANITIZE_DUMP_NO, NULL, NULL), createEnumConfig("sanitize-dump-payload", NULL, DEBUG_CONFIG | MODIFIABLE_CONFIG, sanitize_dump_payload_enum, server.sanitize_dump_payload, SANITIZE_DUMP_NO, NULL, NULL),
createEnumConfig("enable-protected-configs", NULL, IMMUTABLE_CONFIG, protected_action_enum, server.enable_protected_configs, PROTECTED_ACTION_ALLOWED_NO, NULL, NULL), createEnumConfig("enable-protected-configs", NULL, IMMUTABLE_CONFIG, protected_action_enum, server.enable_protected_configs, PROTECTED_ACTION_ALLOWED_NO, NULL, NULL),
createEnumConfig("enable-debug-command", NULL, IMMUTABLE_CONFIG, protected_action_enum, server.enable_debug_cmd, PROTECTED_ACTION_ALLOWED_NO, NULL, NULL), createEnumConfig("enable-debug-command", NULL, IMMUTABLE_CONFIG, protected_action_enum, server.enable_debug_cmd, PROTECTED_ACTION_ALLOWED_NO, NULL, NULL),
......
...@@ -1653,7 +1653,7 @@ int expireIfNeeded(redisDb *db, robj *key, int force_delete_expired) { ...@@ -1653,7 +1653,7 @@ int expireIfNeeded(redisDb *db, robj *key, int force_delete_expired) {
* This function must be called at least once before starting to populate * This function must be called at least once before starting to populate
* the result, and can be called repeatedly to enlarge the result array. * the result, and can be called repeatedly to enlarge the result array.
*/ */
int *getKeysPrepareResult(getKeysResult *result, int numkeys) { keyReference *getKeysPrepareResult(getKeysResult *result, int numkeys) {
/* GETKEYS_RESULT_INIT initializes keys to NULL, point it to the pre-allocated stack /* GETKEYS_RESULT_INIT initializes keys to NULL, point it to the pre-allocated stack
* buffer here. */ * buffer here. */
if (!result->keys) { if (!result->keys) {
...@@ -1665,12 +1665,12 @@ int *getKeysPrepareResult(getKeysResult *result, int numkeys) { ...@@ -1665,12 +1665,12 @@ int *getKeysPrepareResult(getKeysResult *result, int numkeys) {
if (numkeys > result->size) { if (numkeys > result->size) {
if (result->keys != result->keysbuf) { if (result->keys != result->keysbuf) {
/* We're not using a static buffer, just (re)alloc */ /* We're not using a static buffer, just (re)alloc */
result->keys = zrealloc(result->keys, numkeys * sizeof(int)); result->keys = zrealloc(result->keys, numkeys * sizeof(keyReference));
} else { } else {
/* We are using a static buffer, copy its contents */ /* We are using a static buffer, copy its contents */
result->keys = zmalloc(numkeys * sizeof(int)); result->keys = zmalloc(numkeys * sizeof(keyReference));
if (result->numkeys) if (result->numkeys)
memcpy(result->keys, result->keysbuf, result->numkeys * sizeof(int)); memcpy(result->keys, result->keysbuf, result->numkeys * sizeof(keyReference));
} }
result->size = numkeys; result->size = numkeys;
} }
...@@ -1678,12 +1678,183 @@ int *getKeysPrepareResult(getKeysResult *result, int numkeys) { ...@@ -1678,12 +1678,183 @@ int *getKeysPrepareResult(getKeysResult *result, int numkeys) {
return result->keys; return result->keys;
} }
/* Returns a bitmask with all the flags found in any of the key specs of the command.
* The 'inv' argument means we'll return a mask with all flags that are missing in at least one spec. */
int64_t getAllKeySpecsFlags(struct redisCommand *cmd, int inv) {
int64_t flags = 0;
for (int j = 0; j < cmd->key_specs_num; j++) {
keySpec *spec = cmd->key_specs + j;
flags |= inv? ~spec->flags : spec->flags;
}
return flags;
}
/* Fetch the keys based of the provided key specs. Returns the number of keys found, or -1 on error.
* There are several flags that can be used to modify how this function finds keys in a command.
*
* GET_KEYSPEC_INCLUDE_CHANNELS: Return channels as if they were keys.
* GET_KEYSPEC_RETURN_PARTIAL: Skips invalid and incomplete keyspecs but returns the keys
* found in other valid keyspecs.
*/
int getKeysUsingKeySpecs(struct redisCommand *cmd, robj **argv, int argc, int search_flags, getKeysResult *result) {
int j, i, k = 0, last, first, step;
keyReference *keys;
for (j = 0; j < cmd->key_specs_num; j++) {
keySpec *spec = cmd->key_specs + j;
serverAssert(spec->begin_search_type != KSPEC_BS_INVALID);
/* Skip specs that represent channels instead of keys */
if (spec->flags & (CMD_KEY_CHANNEL) && !(search_flags & GET_KEYSPEC_INCLUDE_CHANNELS)) {
continue;
}
first = 0;
if (spec->begin_search_type == KSPEC_BS_INDEX) {
first = spec->bs.index.pos;
} else if (spec->begin_search_type == KSPEC_BS_KEYWORD) {
int start_index = spec->bs.keyword.startfrom > 0 ? spec->bs.keyword.startfrom : argc+spec->bs.keyword.startfrom;
int end_index = spec->bs.keyword.startfrom > 0 ? argc-1: 1;
for (i = start_index; i != end_index; i = start_index <= end_index ? i + 1 : i - 1) {
if (i >= argc || i < 1)
break;
if (!strcasecmp((char*)argv[i]->ptr,spec->bs.keyword.keyword)) {
first = i+1;
break;
}
}
/* keyword not found */
if (!first) {
continue;
}
} else {
/* unknown spec */
goto invalid_spec;
}
if (spec->find_keys_type == KSPEC_FK_RANGE) {
step = spec->fk.range.keystep;
if (spec->fk.range.lastkey >= 0) {
last = first + spec->fk.range.lastkey;
} else {
if (!spec->fk.range.limit) {
last = argc + spec->fk.range.lastkey;
} else {
serverAssert(spec->fk.range.lastkey == -1);
last = first + ((argc-first)/spec->fk.range.limit + spec->fk.range.lastkey);
}
}
} else if (spec->find_keys_type == KSPEC_FK_KEYNUM) {
step = spec->fk.keynum.keystep;
long long numkeys;
if (spec->fk.keynum.keynumidx >= argc)
goto invalid_spec;
sds keynum_str = argv[first + spec->fk.keynum.keynumidx]->ptr;
if (!string2ll(keynum_str,sdslen(keynum_str),&numkeys) || numkeys < 0) {
/* Unable to parse the numkeys argument or it was invalid */
goto invalid_spec;
}
first += spec->fk.keynum.firstkey;
last = first + (int)numkeys-1;
} else {
/* unknown spec */
goto invalid_spec;
}
int count = ((last - first)+1);
keys = getKeysPrepareResult(result, count);
/* First or last is out of bounds, which indicates a syntax error */
if (last >= argc || last < first || first >= argc) {
goto invalid_spec;
}
for (i = first; i <= last; i += step) {
if (i >= argc || i < first) {
/* Modules commands, and standard commands with a not fixed number
* of arguments (negative arity parameter) do not have dispatch
* time arity checks, so we need to handle the case where the user
* passed an invalid number of arguments here. In this case we
* return no keys and expect the command implementation to report
* an arity or syntax error. */
if (cmd->flags & CMD_MODULE || cmd->arity < 0) {
continue;
} else {
serverPanic("Redis built-in command declared keys positions not matching the arity requirements.");
}
}
keys[k].pos = i;
keys[k++].flags = spec->flags;
}
/* Done with this spec */
continue;
invalid_spec:
if (search_flags & GET_KEYSPEC_RETURN_PARTIAL) {
continue;
} else {
result->numkeys = 0;
return -1;
}
}
result->numkeys = k;
return k;
}
/* Return all the arguments that are keys in the command passed via argc / argv.
* This function will eventually replace getKeysFromCommand.
*
* The command returns the positions of all the key arguments inside the array,
* so the actual return value is a heap allocated array of integers. The
* length of the array is returned by reference into *numkeys.
*
* Along with the position, this command also returns the flags that are
* associated with how Redis will access the key.
*
* 'cmd' must be point to the corresponding entry into the redisCommand
* table, according to the command name in argv[0].
*
* This function uses the command's key specs, which contain the key-spec flags,
* (e.g. RO / RW) and only resorts to the command-specific helper function if
* any of the keys-specs are marked as INCOMPLETE. */
int getKeysFromCommandWithSpecs(struct redisCommand *cmd, robj **argv, int argc, int search_flags, getKeysResult *result) {
if (cmd->flags & CMD_MODULE_GETKEYS) {
return moduleGetCommandKeysViaAPI(cmd,argv,argc,result);
} else {
if (!(getAllKeySpecsFlags(cmd, 0) & CMD_KEY_INCOMPLETE)) {
int ret = getKeysUsingKeySpecs(cmd,argv,argc,search_flags,result);
if (ret >= 0)
return ret;
}
if (!(cmd->flags & CMD_MODULE) && cmd->getkeys_proc)
return cmd->getkeys_proc(cmd,argv,argc,result);
return 0;
}
}
/* This function returns a sanity check if the command may have keys. */
int doesCommandHaveKeys(struct redisCommand *cmd) {
return (!(cmd->flags & CMD_MODULE) && cmd->getkeys_proc) || /* has getkeys_proc (non modules) */
(cmd->flags & CMD_MODULE_GETKEYS) || /* module with GETKEYS */
(getAllKeySpecsFlags(cmd, 1) & CMD_KEY_CHANNEL); /* has at least one key-spec not marked as CHANNEL */
}
/* The base case is to use the keys position as given in the command table /* The base case is to use the keys position as given in the command table
* (firstkey, lastkey, step). * (firstkey, lastkey, step).
* This function works only on command with the legacy_range_key_spec, * This function works only on command with the legacy_range_key_spec,
* all other commands should be handled by getkeys_proc. */ * all other commands should be handled by getkeys_proc.
*
* If the commands keyspec is incomplete, no keys will be returned, and the provided
* keys function should be called instead.
*
* NOTE: This function does not guarantee populating the flags for
* the keys, in order to get flags you should use getKeysUsingKeySpecs. */
int getKeysUsingLegacyRangeSpec(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) { int getKeysUsingLegacyRangeSpec(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
int j, i = 0, last, first, step, *keys; int j, i = 0, last, first, step;
keyReference *keys;
UNUSED(argv); UNUSED(argv);
if (cmd->legacy_range_key_spec.begin_search_type == KSPEC_BS_INVALID) { if (cmd->legacy_range_key_spec.begin_search_type == KSPEC_BS_INVALID) {
...@@ -1703,7 +1874,7 @@ int getKeysUsingLegacyRangeSpec(struct redisCommand *cmd, robj **argv, int argc, ...@@ -1703,7 +1874,7 @@ int getKeysUsingLegacyRangeSpec(struct redisCommand *cmd, robj **argv, int argc,
keys = getKeysPrepareResult(result, count); keys = getKeysPrepareResult(result, count);
for (j = first; j <= last; j += step) { for (j = first; j <= last; j += step) {
if (j >= argc) { if (j >= argc || j < first) {
/* Modules commands, and standard commands with a not fixed number /* Modules commands, and standard commands with a not fixed number
* of arguments (negative arity parameter) do not have dispatch * of arguments (negative arity parameter) do not have dispatch
* time arity checks, so we need to handle the case where the user * time arity checks, so we need to handle the case where the user
...@@ -1717,7 +1888,9 @@ int getKeysUsingLegacyRangeSpec(struct redisCommand *cmd, robj **argv, int argc, ...@@ -1717,7 +1888,9 @@ int getKeysUsingLegacyRangeSpec(struct redisCommand *cmd, robj **argv, int argc,
serverPanic("Redis built-in command declared keys positions not matching the arity requirements."); serverPanic("Redis built-in command declared keys positions not matching the arity requirements.");
} }
} }
keys[i++] = j; keys[i].pos = j;
/* Flags are omitted from legacy key specs */
keys[i++].flags = 0;
} }
result->numkeys = i; result->numkeys = i;
return i; return i;
...@@ -1761,10 +1934,12 @@ void getKeysFreeResult(getKeysResult *result) { ...@@ -1761,10 +1934,12 @@ void getKeysFreeResult(getKeysResult *result) {
* 'keyCountOfs': num-keys index. * 'keyCountOfs': num-keys index.
* 'firstKeyOfs': firstkey index. * 'firstKeyOfs': firstkey index.
* 'keyStep': the interval of each key, usually this value is 1. * 'keyStep': the interval of each key, usually this value is 1.
* */ *
* The commands using this functoin have a fully defined keyspec, so returning flags isn't needed. */
int genericGetKeys(int storeKeyOfs, int keyCountOfs, int firstKeyOfs, int keyStep, int genericGetKeys(int storeKeyOfs, int keyCountOfs, int firstKeyOfs, int keyStep,
robj **argv, int argc, getKeysResult *result) { robj **argv, int argc, getKeysResult *result) {
int i, num, *keys; int i, num;
keyReference *keys;
num = atoi(argv[keyCountOfs]->ptr); num = atoi(argv[keyCountOfs]->ptr);
/* Sanity check. Don't return any key if the command is going to /* Sanity check. Don't return any key if the command is going to
...@@ -1779,9 +1954,15 @@ int genericGetKeys(int storeKeyOfs, int keyCountOfs, int firstKeyOfs, int keySte ...@@ -1779,9 +1954,15 @@ int genericGetKeys(int storeKeyOfs, int keyCountOfs, int firstKeyOfs, int keySte
result->numkeys = numkeys; result->numkeys = numkeys;
/* Add all key positions for argv[firstKeyOfs...n] to keys[] */ /* Add all key positions for argv[firstKeyOfs...n] to keys[] */
for (i = 0; i < num; i++) keys[i] = firstKeyOfs+(i*keyStep); for (i = 0; i < num; i++) {
keys[i].pos = firstKeyOfs+(i*keyStep);
keys[i].flags = 0;
}
if (storeKeyOfs) keys[num] = storeKeyOfs; if (storeKeyOfs) {
keys[num].pos = storeKeyOfs;
keys[num].flags = 0;
}
return result->numkeys; return result->numkeys;
} }
...@@ -1830,20 +2011,46 @@ int bzmpopGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult ...@@ -1830,20 +2011,46 @@ int bzmpopGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult
return genericGetKeys(0, 2, 3, 1, argv, argc, result); return genericGetKeys(0, 2, 3, 1, argv, argc, result);
} }
/* Helper function to extract keys from the SORT RO command.
*
* SORT <sort-key>
*
* The second argument of SORT is always a key, however an arbitrary number of
* keys may be accessed while doing the sort (the BY and GET args), so the
* key-spec declares incomplete keys which is why we have to provide a concrete
* implementation to fetch the keys.
*
* This command declares incomplete keys, so the flags are correctly set for this function */
int sortROGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
keyReference *keys;
UNUSED(cmd);
UNUSED(argv);
UNUSED(argc);
keys = getKeysPrepareResult(result, 1);
keys[0].pos = 1; /* <sort-key> is always present. */
keys[0].flags = CMD_KEY_RO | CMD_KEY_ACCESS;
return 1;
}
/* Helper function to extract keys from the SORT command. /* Helper function to extract keys from the SORT command.
* *
* SORT <sort-key> ... STORE <store-key> ... * SORT <sort-key> ... STORE <store-key> ...
* *
* The first argument of SORT is always a key, however a list of options * The first argument of SORT is always a key, however a list of options
* follow in SQL-alike style. Here we parse just the minimum in order to * follow in SQL-alike style. Here we parse just the minimum in order to
* correctly identify keys in the "STORE" option. */ * correctly identify keys in the "STORE" option.
*
* This command declares incomplete keys, so the flags are correctly set for this function */
int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) { int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
int i, j, num, *keys, found_store = 0; int i, j, num, found_store = 0;
keyReference *keys;
UNUSED(cmd); UNUSED(cmd);
num = 0; num = 0;
keys = getKeysPrepareResult(result, 2); /* Alloc 2 places for the worst case. */ keys = getKeysPrepareResult(result, 2); /* Alloc 2 places for the worst case. */
keys[num++] = 1; /* <sort-key> is always present. */ keys[num].pos = 1; /* <sort-key> is always present. */
keys[num++].flags = CMD_KEY_RO | CMD_KEY_ACCESS;
/* Search for STORE option. By default we consider options to don't /* Search for STORE option. By default we consider options to don't
* have arguments, so if we find an unknown option name we scan the * have arguments, so if we find an unknown option name we scan the
...@@ -1869,7 +2076,8 @@ int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult * ...@@ -1869,7 +2076,8 @@ int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *
* to be sure to process the *last* "STORE" option if multiple * to be sure to process the *last* "STORE" option if multiple
* ones are provided. This is same behavior as SORT. */ * ones are provided. This is same behavior as SORT. */
found_store = 1; found_store = 1;
keys[num] = i+1; /* <store-key> */ keys[num].pos = i+1; /* <store-key> */
keys[num].flags = CMD_KEY_OW | CMD_KEY_UPDATE;
break; break;
} }
} }
...@@ -1878,8 +2086,10 @@ int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult * ...@@ -1878,8 +2086,10 @@ int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *
return result->numkeys; return result->numkeys;
} }
/* This command declares incomplete keys, so the flags are correctly set for this function */
int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) { int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
int i, num, first, *keys; int i, num, first;
keyReference *keys;
UNUSED(cmd); UNUSED(cmd);
/* Assume the obvious form. */ /* Assume the obvious form. */
...@@ -1900,7 +2110,10 @@ int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResul ...@@ -1900,7 +2110,10 @@ int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResul
} }
keys = getKeysPrepareResult(result, num); keys = getKeysPrepareResult(result, num);
for (i = 0; i < num; i++) keys[i] = first+i; for (i = 0; i < num; i++) {
keys[i].pos = first+i;
keys[i].flags = CMD_KEY_RW | CMD_KEY_ACCESS | CMD_KEY_DELETE;
}
result->numkeys = num; result->numkeys = num;
return num; return num;
} }
...@@ -1908,9 +2121,12 @@ int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResul ...@@ -1908,9 +2121,12 @@ int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResul
/* Helper function to extract keys from following commands: /* Helper function to extract keys from following commands:
* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC] * GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC]
* [COUNT count] [STORE key] [STOREDIST key] * [COUNT count] [STORE key] [STOREDIST key]
* GEORADIUSBYMEMBER key member radius unit ... options ... */ * GEORADIUSBYMEMBER key member radius unit ... options ...
*
* This command has a fully defined keyspec, so returning flags isn't needed. */
int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) { int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
int i, num, *keys; int i, num;
keyReference *keys;
UNUSED(cmd); UNUSED(cmd);
/* Check for the presence of the stored key in the command */ /* Check for the presence of the stored key in the command */
...@@ -1935,18 +2151,23 @@ int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysRes ...@@ -1935,18 +2151,23 @@ int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysRes
keys = getKeysPrepareResult(result, num); keys = getKeysPrepareResult(result, num);
/* Add all key positions to keys[] */ /* Add all key positions to keys[] */
keys[0] = 1; keys[0].pos = 1;
keys[0].flags = 0;
if(num > 1) { if(num > 1) {
keys[1] = stored_key; keys[1].pos = stored_key;
keys[1].flags = 0;
} }
result->numkeys = num; result->numkeys = num;
return num; return num;
} }
/* XREAD [BLOCK <milliseconds>] [COUNT <count>] [GROUP <groupname> <ttl>] /* XREAD [BLOCK <milliseconds>] [COUNT <count>] [GROUP <groupname> <ttl>]
* STREAMS key_1 key_2 ... key_N ID_1 ID_2 ... ID_N */ * STREAMS key_1 key_2 ... key_N ID_1 ID_2 ... ID_N
*
* This command has a fully defined keyspec, so returning flags isn't needed. */
int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) { int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
int i, num = 0, *keys; int i, num = 0;
keyReference *keys;
UNUSED(cmd); UNUSED(cmd);
/* We need to parse the options of the command in order to seek the first /* We need to parse the options of the command in order to seek the first
...@@ -1982,7 +2203,10 @@ int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult ...@@ -1982,7 +2203,10 @@ int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult
there are also the IDs, one per key. */ there are also the IDs, one per key. */
keys = getKeysPrepareResult(result, num); keys = getKeysPrepareResult(result, num);
for (i = streams_pos+1; i < argc-num; i++) keys[i-streams_pos-1] = i; for (i = streams_pos+1; i < argc-num; i++) {
keys[i-streams_pos-1].pos = i;
keys[i-streams_pos-1].flags = 0;
}
result->numkeys = num; result->numkeys = num;
return num; return num;
} }
...@@ -811,7 +811,7 @@ void RM_KeyAtPos(RedisModuleCtx *ctx, int pos) { ...@@ -811,7 +811,7 @@ void RM_KeyAtPos(RedisModuleCtx *ctx, int pos) {
getKeysPrepareResult(res, newsize); getKeysPrepareResult(res, newsize);
} }
res->keys[res->numkeys++] = pos; res->keys[res->numkeys++].pos = pos;
} }
/* Helper for RM_CreateCommand(). Turns a string representing command /* Helper for RM_CreateCommand(). Turns a string representing command
...@@ -864,10 +864,10 @@ int64_t commandKeySpecsFlagsFromString(const char *s) { ...@@ -864,10 +864,10 @@ int64_t commandKeySpecsFlagsFromString(const char *s) {
else if (!strcasecmp(t,"RW")) flags |= CMD_KEY_RW; else if (!strcasecmp(t,"RW")) flags |= CMD_KEY_RW;
else if (!strcasecmp(t,"OW")) flags |= CMD_KEY_OW; else if (!strcasecmp(t,"OW")) flags |= CMD_KEY_OW;
else if (!strcasecmp(t,"RM")) flags |= CMD_KEY_RM; else if (!strcasecmp(t,"RM")) flags |= CMD_KEY_RM;
else if (!strcasecmp(t,"ACCESS")) flags |= CMD_KEY_ACCESS; else if (!strcasecmp(t,"access")) flags |= CMD_KEY_ACCESS;
else if (!strcasecmp(t,"INSERT")) flags |= CMD_KEY_INSERT; else if (!strcasecmp(t,"insert")) flags |= CMD_KEY_INSERT;
else if (!strcasecmp(t,"UPDATE")) flags |= CMD_KEY_UPDATE; else if (!strcasecmp(t,"update")) flags |= CMD_KEY_UPDATE;
else if (!strcasecmp(t,"DELETE")) flags |= CMD_KEY_DELETE; else if (!strcasecmp(t,"delete")) flags |= CMD_KEY_DELETE;
else if (!strcasecmp(t,"channel")) flags |= CMD_KEY_CHANNEL; else if (!strcasecmp(t,"channel")) flags |= CMD_KEY_CHANNEL;
else if (!strcasecmp(t,"incomplete")) flags |= CMD_KEY_INCOMPLETE; else if (!strcasecmp(t,"incomplete")) flags |= CMD_KEY_INCOMPLETE;
else break; else break;
...@@ -1218,14 +1218,14 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCommand *command, int index, keyS ...@@ -1218,14 +1218,14 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCommand *command, int index, keyS
* if (RedisModule_CreateCommand(ctx,"kspec.smove",kspec_legacy,"",0,0,0) == REDISMODULE_ERR) * if (RedisModule_CreateCommand(ctx,"kspec.smove",kspec_legacy,"",0,0,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR; * return REDISMODULE_ERR;
* *
* if (RedisModule_AddCommandKeySpec(ctx,"kspec.smove","read write",&spec_id) == REDISMODULE_ERR) * if (RedisModule_AddCommandKeySpec(ctx,"kspec.smove","RW access delete",&spec_id) == REDISMODULE_ERR)
* return REDISMODULE_ERR; * return REDISMODULE_ERR;
* if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.smove",spec_id,1) == REDISMODULE_ERR) * if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.smove",spec_id,1) == REDISMODULE_ERR)
* return REDISMODULE_ERR; * return REDISMODULE_ERR;
* if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.smove",spec_id,0,1,0) == REDISMODULE_ERR) * if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.smove",spec_id,0,1,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR; * return REDISMODULE_ERR;
* *
* if (RedisModule_AddCommandKeySpec(ctx,"kspec.smove","write",&spec_id) == REDISMODULE_ERR) * if (RedisModule_AddCommandKeySpec(ctx,"kspec.smove","RW insert",&spec_id) == REDISMODULE_ERR)
* return REDISMODULE_ERR; * return REDISMODULE_ERR;
* if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.smove",spec_id,2) == REDISMODULE_ERR) * if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.smove",spec_id,2) == REDISMODULE_ERR)
* return REDISMODULE_ERR; * return REDISMODULE_ERR;
...@@ -1237,7 +1237,7 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCommand *command, int index, keyS ...@@ -1237,7 +1237,7 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCommand *command, int index, keyS
* *
* Example: * Example:
* *
* RedisModule_AddCommandKeySpec(ctx,"module.config|get","read",&spec_id) * RedisModule_AddCommandKeySpec(ctx,"module.object|encoding","RO",&spec_id)
* *
* Returns REDISMODULE_OK on success * Returns REDISMODULE_OK on success
*/ */
...@@ -7793,13 +7793,31 @@ int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **arg ...@@ -7793,13 +7793,31 @@ int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **arg
return REDISMODULE_OK; return REDISMODULE_OK;
} }
/* Check if the key can be accessed by the user, according to the ACLs associated with it. /* Check if the key can be accessed by the user, according to the ACLs associated with it
* and the flags used. The supported flags are:
* *
* If the user can access the key, REDISMODULE_OK is returned, otherwise * REDISMODULE_KEY_PERMISSION_READ: Can the module read data from the key.
* REDISMODULE_ERR is returned. */ * REDISMODULE_KEY_PERMISSION_WRITE: Can the module write data to the key.
int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key) { *
if (ACLCheckKey(user->user, key->ptr, sdslen(key->ptr)) != ACL_OK) * On success a REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned and errno is set to the following values:
*
* * EINVAL: The provided flags are invalid.
* * EACCESS: The user does not have permission to access the key.
*/
int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key, int flags) {
int acl_flags = 0;
if (flags & REDISMODULE_KEY_PERMISSION_READ) acl_flags |= ACL_READ_PERMISSION;
if (flags & REDISMODULE_KEY_PERMISSION_WRITE) acl_flags |= ACL_WRITE_PERMISSION;
if (!acl_flags || ((flags & REDISMODULE_KEY_PERMISSION_ALL) != flags)) {
errno = EINVAL;
return REDISMODULE_ERR; return REDISMODULE_ERR;
}
if (ACLUserCheckKeyPerm(user->user, key->ptr, sdslen(key->ptr), acl_flags) != ACL_OK) {
errno = EACCES;
return REDISMODULE_ERR;
}
return REDISMODULE_OK; return REDISMODULE_OK;
} }
...@@ -7811,7 +7829,7 @@ int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key) { ...@@ -7811,7 +7829,7 @@ int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key) {
* If the user can access the pubsub channel, REDISMODULE_OK is returned, otherwise * If the user can access the pubsub channel, REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned. */ * REDISMODULE_ERR is returned. */
int RM_ACLCheckChannelPermissions(RedisModuleUser *user, RedisModuleString *ch, int literal) { int RM_ACLCheckChannelPermissions(RedisModuleUser *user, RedisModuleString *ch, int literal) {
if (ACLCheckPubsubChannelPerm(ch->ptr, user->user->channels, literal) != ACL_OK) if (ACLUserCheckChannelPerm(user->user, ch->ptr, literal) != ACL_OK)
return REDISMODULE_ERR; return REDISMODULE_ERR;
return REDISMODULE_OK; return REDISMODULE_OK;
...@@ -10490,17 +10508,11 @@ int *RM_GetCommandKeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc, ...@@ -10490,17 +10508,11 @@ int *RM_GetCommandKeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc,
return NULL; return NULL;
} }
if (result.keys == result.keysbuf) { /* The return value here expects an array of key positions */
/* If the result is using a stack based array, copy it. */
unsigned long int size = sizeof(int) * result.numkeys; unsigned long int size = sizeof(int) * result.numkeys;
res = zmalloc(size); res = zmalloc(size);
memcpy(res, result.keys, size); for (int i = 0; i < result.numkeys; i++) {
} else { res[i] = result.keys[i].pos;
/* We return the heap based array and intentionally avoid calling
* getKeysFreeResult() here, as it is the caller's responsibility
* to free this array.
*/
res = result.keys;
} }
return res; return res;
......
...@@ -261,6 +261,12 @@ typedef enum { ...@@ -261,6 +261,12 @@ typedef enum {
#define REDISMODULE_CMD_ARG_MULTIPLE (1<<1) /* The argument may repeat itself (like key in DEL) */ #define REDISMODULE_CMD_ARG_MULTIPLE (1<<1) /* The argument may repeat itself (like key in DEL) */
#define REDISMODULE_CMD_ARG_MULTIPLE_TOKEN (1<<2) /* The argument may repeat itself, and so does its token (like `GET pattern` in SORT) */ #define REDISMODULE_CMD_ARG_MULTIPLE_TOKEN (1<<2) /* The argument may repeat itself, and so does its token (like `GET pattern` in SORT) */
/* Redis ACL key permission flags, which specify which permissions a module
* needs on a key. */
#define REDISMODULE_KEY_PERMISSION_READ (1<<0)
#define REDISMODULE_KEY_PERMISSION_WRITE (1<<1)
#define REDISMODULE_KEY_PERMISSION_ALL (REDISMODULE_KEY_PERMISSION_READ | REDISMODULE_KEY_PERMISSION_WRITE)
/* Eventloop definitions. */ /* Eventloop definitions. */
#define REDISMODULE_EVENTLOOP_READABLE 1 #define REDISMODULE_EVENTLOOP_READABLE 1
#define REDISMODULE_EVENTLOOP_WRITABLE 2 #define REDISMODULE_EVENTLOOP_WRITABLE 2
...@@ -978,7 +984,7 @@ REDISMODULE_API int (*RedisModule_SetModuleUserACL)(RedisModuleUser *user, const ...@@ -978,7 +984,7 @@ REDISMODULE_API int (*RedisModule_SetModuleUserACL)(RedisModuleUser *user, const
REDISMODULE_API RedisModuleString * (*RedisModule_GetCurrentUserName)(RedisModuleCtx *ctx) REDISMODULE_ATTR; REDISMODULE_API RedisModuleString * (*RedisModule_GetCurrentUserName)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleUser * (*RedisModule_GetModuleUserFromUserName)(RedisModuleString *name) REDISMODULE_ATTR; REDISMODULE_API RedisModuleUser * (*RedisModule_GetModuleUserFromUserName)(RedisModuleString *name) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ACLCheckCommandPermissions)(RedisModuleUser *user, RedisModuleString **argv, int argc) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_ACLCheckCommandPermissions)(RedisModuleUser *user, RedisModuleString **argv, int argc) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ACLCheckKeyPermissions)(RedisModuleUser *user, RedisModuleString *key) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_ACLCheckKeyPermissions)(RedisModuleUser *user, RedisModuleString *key, int flags) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ACLCheckChannelPermissions)(RedisModuleUser *user, RedisModuleString *ch, int literal) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_ACLCheckChannelPermissions)(RedisModuleUser *user, RedisModuleString *ch, int literal) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_ACLAddLogEntry)(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString *object) REDISMODULE_ATTR; REDISMODULE_API void (*RedisModule_ACLAddLogEntry)(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString *object) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_AuthenticateClientWithACLUser)(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_AuthenticateClientWithACLUser)(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) REDISMODULE_ATTR;
......
...@@ -4504,7 +4504,7 @@ void getKeysSubcommand(client *c) { ...@@ -4504,7 +4504,7 @@ void getKeysSubcommand(client *c) {
} }
} else { } else {
addReplyArrayLen(c,result.numkeys); addReplyArrayLen(c,result.numkeys);
for (j = 0; j < result.numkeys; j++) addReplyBulk(c,c->argv[result.keys[j]+2]); for (j = 0; j < result.numkeys; j++) addReplyBulk(c,c->argv[result.keys[j].pos+2]);
} }
getKeysFreeResult(&result); getKeysFreeResult(&result);
} }
......
...@@ -995,54 +995,32 @@ typedef struct readyList { ...@@ -995,54 +995,32 @@ typedef struct readyList {
is USER_COMMAND_BITS_COUNT-1. */ is USER_COMMAND_BITS_COUNT-1. */
#define USER_FLAG_ENABLED (1<<0) /* The user is active. */ #define USER_FLAG_ENABLED (1<<0) /* The user is active. */
#define USER_FLAG_DISABLED (1<<1) /* The user is disabled. */ #define USER_FLAG_DISABLED (1<<1) /* The user is disabled. */
#define USER_FLAG_ALLKEYS (1<<2) /* The user can mention any key. */ #define USER_FLAG_NOPASS (1<<2) /* The user requires no password, any
#define USER_FLAG_ALLCOMMANDS (1<<3) /* The user can run all commands. */
#define USER_FLAG_NOPASS (1<<4) /* The user requires no password, any
provided password will work. For the provided password will work. For the
default user, this also means that default user, this also means that
no AUTH is needed, and every no AUTH is needed, and every
connection is immediately connection is immediately
authenticated. */ authenticated. */
#define USER_FLAG_ALLCHANNELS (1<<5) /* The user can mention any Pub/Sub #define USER_FLAG_SANITIZE_PAYLOAD (1<<3) /* The user require a deep RESTORE
channel. */
#define USER_FLAG_SANITIZE_PAYLOAD (1<<6) /* The user require a deep RESTORE
* payload sanitization. */ * payload sanitization. */
#define USER_FLAG_SANITIZE_PAYLOAD_SKIP (1<<7) /* The user should skip the #define USER_FLAG_SANITIZE_PAYLOAD_SKIP (1<<4) /* The user should skip the
* deep sanitization of RESTORE * deep sanitization of RESTORE
* payload. */ * payload. */
#define SELECTOR_FLAG_ROOT (1<<0) /* This is the root user permission
* selector. */
#define SELECTOR_FLAG_ALLKEYS (1<<1) /* The user can mention any key. */
#define SELECTOR_FLAG_ALLCOMMANDS (1<<2) /* The user can run all commands. */
#define SELECTOR_FLAG_ALLCHANNELS (1<<3) /* The user can mention any Pub/Sub
channel. */
typedef struct { typedef struct {
sds name; /* The username as an SDS string. */ sds name; /* The username as an SDS string. */
uint64_t flags; /* See USER_FLAG_* */ uint32_t flags; /* See USER_FLAG_* */
/* The bit in allowed_commands is set if this user has the right to
* execute this command.
*
* If the bit for a given command is NOT set and the command has
* allowed first-args, Redis will also check allowed_firstargs in order to
* understand if the command can be executed. */
uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];
/* allowed_firstargs is used by ACL rules to block access to a command unless a
* specific argv[1] is given (or argv[2] in case it is applied on a sub-command).
* For example, a user can use the rule "-select +select|0" to block all
* SELECT commands, except "SELECT 0".
* And for a sub-command: "+config -config|set +config|set|loglevel"
*
* For each command ID (corresponding to the command bit set in allowed_commands),
* This array points to an array of SDS strings, terminated by a NULL pointer,
* with all the first-args that are allowed for this command. When no first-arg
* matching is used, the field is just set to NULL to avoid allocating
* USER_COMMAND_BITS_COUNT pointers. */
sds **allowed_firstargs;
list *passwords; /* A list of SDS valid passwords for this user. */ list *passwords; /* A list of SDS valid passwords for this user. */
list *patterns; /* A list of allowed key patterns. If this field is NULL list *selectors; /* A list of selectors this user validates commands
the user cannot mention any key in a command, unless against. This list will always contain at least
the flag ALLKEYS is set in the user. */ one selector for backwards compatibility. */
list *channels; /* A list of allowed Pub/Sub channel patterns. If this
field is NULL the user cannot mention any channel in a
`PUBLISH` or [P][UNSUBSCRIBE] command, unless the flag
ALLCHANNELS is set in the user. */
} user; } user;
/* With multiplexing we need to take per-client state. /* With multiplexing we need to take per-client state.
...@@ -1913,16 +1891,22 @@ struct redisServer { ...@@ -1913,16 +1891,22 @@ struct redisServer {
#define MAX_KEYS_BUFFER 256 #define MAX_KEYS_BUFFER 256
typedef struct {
int pos; /* The position of the key within the client array */
int flags; /* The flags associted with the key access, see
CMD_KEY_* for more information */
} keyReference;
/* A result structure for the various getkeys function calls. It lists the /* A result structure for the various getkeys function calls. It lists the
* keys as indices to the provided argv. * keys as indices to the provided argv.
*/ */
typedef struct { typedef struct {
int keysbuf[MAX_KEYS_BUFFER]; /* Pre-allocated buffer, to save heap allocations */ keyReference keysbuf[MAX_KEYS_BUFFER]; /* Pre-allocated buffer, to save heap allocations */
int *keys; /* Key indices array, points to keysbuf or heap */ keyReference *keys; /* Key indices array, points to keysbuf or heap */
int numkeys; /* Number of key indices return */ int numkeys; /* Number of key indices return */
int size; /* Available array size */ int size; /* Available array size */
} getKeysResult; } getKeysResult;
#define GETKEYS_RESULT_INIT { {0}, NULL, 0, MAX_KEYS_BUFFER } #define GETKEYS_RESULT_INIT { {{0}}, NULL, 0, MAX_KEYS_BUFFER }
/* Key specs definitions. /* Key specs definitions.
* *
...@@ -2714,14 +2698,19 @@ void ACLInit(void); ...@@ -2714,14 +2698,19 @@ void ACLInit(void);
#define ACL_LOG_CTX_MULTI 2 #define ACL_LOG_CTX_MULTI 2
#define ACL_LOG_CTX_MODULE 3 #define ACL_LOG_CTX_MODULE 3
/* ACL key permission types */
#define ACL_READ_PERMISSION (1<<0)
#define ACL_WRITE_PERMISSION (1<<1)
#define ACL_ALL_PERMISSION (ACL_READ_PERMISSION|ACL_WRITE_PERMISSION)
int ACLCheckUserCredentials(robj *username, robj *password); int ACLCheckUserCredentials(robj *username, robj *password);
int ACLAuthenticateUser(client *c, robj *username, robj *password); int ACLAuthenticateUser(client *c, robj *username, robj *password);
unsigned long ACLGetCommandID(const char *cmdname); unsigned long ACLGetCommandID(const char *cmdname);
void ACLClearCommandID(void); void ACLClearCommandID(void);
user *ACLGetUserByName(const char *name, size_t namelen); user *ACLGetUserByName(const char *name, size_t namelen);
int ACLCheckKey(const user *u, const char *key, int keylen); int ACLUserCheckKeyPerm(user *u, const char *key, int keylen, int flags);
int ACLCheckPubsubChannelPerm(sds channel, list *allowed, int literal); int ACLUserCheckChannelPerm(user *u, sds channel, int literal);
int ACLCheckAllUserCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr); int ACLCheckAllUserCommandPerm(user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr);
int ACLCheckAllPerm(client *c, int *idxptr); int ACLCheckAllPerm(client *c, int *idxptr);
int ACLSetUser(user *u, const char *op, ssize_t oplen); int ACLSetUser(user *u, const char *op, ssize_t oplen);
uint64_t ACLGetCommandCategoryFlagByName(const char *name); uint64_t ACLGetCommandCategoryFlagByName(const char *name);
...@@ -3003,9 +2992,14 @@ void freeObjAsync(robj *key, robj *obj, int dbid); ...@@ -3003,9 +2992,14 @@ void freeObjAsync(robj *key, robj *obj, int dbid);
void freeReplicationBacklogRefMemAsync(list *blocks, rax *index); void freeReplicationBacklogRefMemAsync(list *blocks, rax *index);
/* API to get key arguments from commands */ /* API to get key arguments from commands */
int *getKeysPrepareResult(getKeysResult *result, int numkeys); #define GET_KEYSPEC_DEFAULT 0
#define GET_KEYSPEC_INCLUDE_CHANNELS (1<<0) /* Consider channels as keys */
#define GET_KEYSPEC_RETURN_PARTIAL (1<<1) /* Return all keys that can be found */
int getKeysFromCommandWithSpecs(struct redisCommand *cmd, robj **argv, int argc, int search_flags, getKeysResult *result);
keyReference *getKeysPrepareResult(getKeysResult *result, int numkeys);
int getKeysFromCommand(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int getKeysFromCommand(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int getChannelsFromCommand(struct redisCommand *cmd, int argc, getKeysResult *result); int doesCommandHaveKeys(struct redisCommand *cmd);
void getKeysFreeResult(getKeysResult *result); void getKeysFreeResult(getKeysResult *result);
int sintercardGetKeys(struct redisCommand *cmd,robj **argv, int argc, getKeysResult *result); int sintercardGetKeys(struct redisCommand *cmd,robj **argv, int argc, getKeysResult *result);
int zunionInterDiffGetKeys(struct redisCommand *cmd,robj **argv, int argc, getKeysResult *result); int zunionInterDiffGetKeys(struct redisCommand *cmd,robj **argv, int argc, getKeysResult *result);
...@@ -3013,6 +3007,7 @@ int zunionInterDiffStoreGetKeys(struct redisCommand *cmd,robj **argv, int argc, ...@@ -3013,6 +3007,7 @@ int zunionInterDiffStoreGetKeys(struct redisCommand *cmd,robj **argv, int argc,
int evalGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int evalGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int functionGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int functionGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int sortROGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
......
...@@ -235,10 +235,10 @@ void trackingRememberKeys(client *c) { ...@@ -235,10 +235,10 @@ void trackingRememberKeys(client *c) {
return; return;
} }
int *keys = result.keys; keyReference *keys = result.keys;
for(int j = 0; j < numkeys; j++) { for(int j = 0; j < numkeys; j++) {
int idx = keys[j]; int idx = keys[j].pos;
sds sdskey = c->argv[idx]->ptr; sds sdskey = c->argv[idx]->ptr;
rax *ids = raxFind(TrackingTable,(unsigned char*)sdskey,sdslen(sdskey)); rax *ids = raxFind(TrackingTable,(unsigned char*)sdskey,sdslen(sdskey));
if (ids == raxNotFound) { if (ids == raxNotFound) {
......
user alice on (+get ~rw*)
user bob on (+set %W~w*) (+get %R~r*)
\ No newline at end of file
...@@ -2,17 +2,33 @@ ...@@ -2,17 +2,33 @@
#include "redismodule.h" #include "redismodule.h"
#include <errno.h> #include <errno.h>
#include <assert.h> #include <assert.h>
#include <string.h>
#include <strings.h>
/* A wrap for SET command with ACL check on the key. */ /* A wrap for SET command with ACL check on the key. */
int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc < 3) { if (argc < 4) {
return RedisModule_WrongArity(ctx); return RedisModule_WrongArity(ctx);
} }
int permissions;
const char *flags = RedisModule_StringPtrLen(argv[1], NULL);
if (!strcasecmp(flags, "W")) {
permissions = REDISMODULE_KEY_PERMISSION_WRITE;
} else if (!strcasecmp(flags, "R")) {
permissions = REDISMODULE_KEY_PERMISSION_READ;
} else if (!strcasecmp(flags, "*")) {
permissions = REDISMODULE_KEY_PERMISSION_ALL;
} else {
RedisModule_ReplyWithError(ctx, "INVALID FLAGS");
return REDISMODULE_OK;
}
/* Check that the key can be accessed */ /* Check that the key can be accessed */
RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx); RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name); RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
int ret = RedisModule_ACLCheckKeyPermissions(user, argv[1]); int ret = RedisModule_ACLCheckKeyPermissions(user, argv[2], permissions);
if (ret != 0) { if (ret != 0) {
RedisModule_ReplyWithError(ctx, "DENIED KEY"); RedisModule_ReplyWithError(ctx, "DENIED KEY");
RedisModule_FreeModuleUser(user); RedisModule_FreeModuleUser(user);
...@@ -20,7 +36,7 @@ int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { ...@@ -20,7 +36,7 @@ int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
return REDISMODULE_OK; return REDISMODULE_OK;
} }
RedisModuleCallReply *rep = RedisModule_Call(ctx, "SET", "v", argv + 1, argc - 1); RedisModuleCallReply *rep = RedisModule_Call(ctx, "SET", "v", argv + 2, argc - 2);
if (!rep) { if (!rep) {
RedisModule_ReplyWithError(ctx, "NULL reply returned"); RedisModule_ReplyWithError(ctx, "NULL reply returned");
} else { } else {
......
...@@ -37,6 +37,7 @@ set ::all_tests { ...@@ -37,6 +37,7 @@ set ::all_tests {
unit/quit unit/quit
unit/aofrw unit/aofrw
unit/acl unit/acl
unit/acl-v2
unit/latency-monitor unit/latency-monitor
integration/block-repl integration/block-repl
integration/replication integration/replication
......
start_server {tags {"acl external:skip"}} {
set r2 [redis_client]
test {Test basic multiple selectors} {
r ACL SETUSER selector-1 on -@all resetkeys nopass
$r2 auth selector-1 password
catch {$r2 ping} err
assert_match "*NOPERM*command*" $err
catch {$r2 set write::foo bar} err
assert_match "*NOPERM*command*" $err
catch {$r2 get read::foo} err
assert_match "*NOPERM*command*" $err
r ACL SETUSER selector-1 (+@write ~write::*) (+@read ~read::*)
catch {$r2 ping} err
assert_equal "OK" [$r2 set write::foo bar]
assert_equal "" [$r2 get read::foo]
catch {$r2 get write::foo} err
assert_match "*NOPERM*keys*" $err
catch {$r2 set read::foo bar} err
assert_match "*NOPERM*keys*" $err
}
test {Test ACL selectors by default have no permissions (except channels)} {
r ACL SETUSER selector-default reset ()
set user [r ACL GETUSER "selector-default"]
assert_equal 1 [llength [dict get $user selectors]]
assert_equal "" [dict get [lindex [dict get $user selectors] 0] keys]
assert_equal "&*" [dict get [lindex [dict get $user selectors] 0] channels]
assert_equal "-@all" [dict get [lindex [dict get $user selectors] 0] commands]
}
test {Test deleting selectors} {
r ACL SETUSER selector-del on "(~added-selector)"
set user [r ACL GETUSER "selector-del"]
assert_equal "~added-selector" [dict get [lindex [dict get $user selectors] 0] keys]
assert_equal [llength [dict get $user selectors]] 1
r ACL SETUSER selector-del clearselectors
set user [r ACL GETUSER "selector-del"]
assert_equal [llength [dict get $user selectors]] 0
}
test {Test selector syntax error reports the error in the selector context} {
catch {r ACL SETUSER selector-syntax on (this-is-invalid)} e
assert_match "*ERR Error in ACL SETUSER modifier '(*)*Syntax*" $e
catch {r ACL SETUSER selector-syntax on (&fail)} e
assert_match "*ERR Error in ACL SETUSER modifier '(*)*Adding a pattern after the*" $e
assert_equal "" [r ACL GETUSER selector-syntax]
}
test {Test flexible selector definition} {
# Test valid selectors
r ACL SETUSER selector-2 "(~key1 +get )" "( ~key2 +get )" "( ~key3 +get)" "(~key4 +get)"
r ACL SETUSER selector-2 (~key5 +get ) ( ~key6 +get ) ( ~key7 +get) (~key8 +get)
set user [r ACL GETUSER "selector-2"]
assert_equal "~key1" [dict get [lindex [dict get $user selectors] 0] keys]
assert_equal "~key2" [dict get [lindex [dict get $user selectors] 1] keys]
assert_equal "~key3" [dict get [lindex [dict get $user selectors] 2] keys]
assert_equal "~key4" [dict get [lindex [dict get $user selectors] 3] keys]
assert_equal "~key5" [dict get [lindex [dict get $user selectors] 4] keys]
assert_equal "~key6" [dict get [lindex [dict get $user selectors] 5] keys]
assert_equal "~key7" [dict get [lindex [dict get $user selectors] 6] keys]
assert_equal "~key8" [dict get [lindex [dict get $user selectors] 7] keys]
# Test invalid selector syntax
catch {r ACL SETUSER invalid-selector " () "} err
assert_match "*ERR*Syntax error*" $err
catch {r ACL SETUSER invalid-selector (} err
assert_match "*Unmatched parenthesis*" $err
catch {r ACL SETUSER invalid-selector )} err
assert_match "*ERR*Syntax error" $err
}
test {Test separate read permission} {
r ACL SETUSER key-permission-R on nopass %R~read* +@all
$r2 auth key-permission-R password
assert_equal PONG [$r2 PING]
r set readstr bar
assert_equal bar [$r2 get readstr]
catch {$r2 set readstr bar} err
assert_match "*NOPERM*keys*" $err
catch {$r2 get notread} err
assert_match "*NOPERM*keys*" $err
}
test {Test separate write permission} {
r ACL SETUSER key-permission-W on nopass %W~write* +@all
$r2 auth key-permission-W password
assert_equal PONG [$r2 PING]
# Note, SET is a RW command, so it's not used for testing
$r2 LPUSH writelist 10
catch {$r2 GET writestr} err
assert_match "*NOPERM*keys*" $err
catch {$r2 LPUSH notwrite 10} err
assert_match "*NOPERM*keys*" $err
}
test {Test separate read and write permissions} {
r ACL SETUSER key-permission-RW on nopass %R~read* %W~write* +@all
$r2 auth key-permission-RW password
assert_equal PONG [$r2 PING]
r set read bar
$r2 copy read write
catch {$r2 copy write read} err
assert_match "*NOPERM*keys*" $err
}
test {Test separate read and write permissions on different selectors are not additive} {
r ACL SETUSER key-permission-RW-selector on nopass "(%R~read* +@all)" "(%W~write* +@all)"
$r2 auth key-permission-RW-selector password
assert_equal PONG [$r2 PING]
# Verify write selector
$r2 LPUSH writelist 10
catch {$r2 GET writestr} err
assert_match "*NOPERM*keys*" $err
catch {$r2 LPUSH notwrite 10} err
assert_match "*NOPERM*keys*" $err
# Verify read selector
r set readstr bar
assert_equal bar [$r2 get readstr]
catch {$r2 set readstr bar} err
assert_match "*NOPERM*keys*" $err
catch {$r2 get notread} err
assert_match "*NOPERM*keys*" $err
# Verify they don't combine
catch {$r2 copy read write} err
assert_match "*NOPERM*keys*" $err
catch {$r2 copy write read} err
assert_match "*NOPERM*keys*" $err
}
test {Test ACL log correctly identifies the relevant item when selectors are used} {
r ACL SETUSER acl-log-test-selector on nopass
r ACL SETUSER acl-log-test-selector +mget ~key (+mget ~key ~otherkey)
$r2 auth acl-log-test-selector password
# Test that command is shown only if none of the selectors match
r ACL LOG RESET
catch {$r2 GET key} err
assert_match "*NOPERM*command*" $err
set entry [lindex [r ACL LOG] 0]
assert_equal [dict get $entry username] "acl-log-test-selector"
assert_equal [dict get $entry context] "toplevel"
assert_equal [dict get $entry reason] "command"
assert_equal [dict get $entry object] "get"
# Test two cases where the first selector matches less than the
# second selector. We should still show the logically first unmatched key.
r ACL LOG RESET
catch {$r2 MGET otherkey someotherkey} err
assert_match "*NOPERM*keys*" $err
set entry [lindex [r ACL LOG] 0]
assert_equal [dict get $entry username] "acl-log-test-selector"
assert_equal [dict get $entry context] "toplevel"
assert_equal [dict get $entry reason] "key"
assert_equal [dict get $entry object] "someotherkey"
r ACL LOG RESET
catch {$r2 MGET key otherkey someotherkey} err
assert_match "*NOPERM*keys*" $err
set entry [lindex [r ACL LOG] 0]
assert_equal [dict get $entry username] "acl-log-test-selector"
assert_equal [dict get $entry context] "toplevel"
assert_equal [dict get $entry reason] "key"
assert_equal [dict get $entry object] "someotherkey"
}
test {Test ACL GETUSER response information} {
r ACL setuser selector-info -@all +get resetchannels &channel1 %R~foo1 %W~bar1 ~baz1
r ACL setuser selector-info (-@all +set resetchannels &channel2 %R~foo2 %W~bar2 ~baz2)
set user [r ACL GETUSER "selector-info"]
# Root selector
assert_equal "%R~foo1 %W~bar1 ~baz1" [dict get $user keys]
assert_equal "&channel1" [dict get $user channels]
assert_equal "-@all +get" [dict get $user commands]
# Added selector
set secondary_selector [lindex [dict get $user selectors] 0]
assert_equal "%R~foo2 %W~bar2 ~baz2" [dict get $secondary_selector keys]
assert_equal "&channel2" [dict get $secondary_selector channels]
assert_equal "-@all +set" [dict get $secondary_selector commands]
}
test {Test ACL list idempotency} {
r ACL SETUSER user-idempotency off -@all +get resetchannels &channel1 %R~foo1 %W~bar1 ~baz1 (-@all +set resetchannels &channel2 %R~foo2 %W~bar2 ~baz2)
set response [lindex [r ACL LIST] [lsearch [r ACL LIST] "user user-idempotency*"]]
assert_match "*-@all*+get*(*)*" $response
assert_match "*resetchannels*&channel1*(*)*" $response
assert_match "*%R~foo1*%W~bar1*~baz1*(*)*" $response
assert_match "*(*-@all*+set*)*" $response
assert_match "*(*resetchannels*&channel2*)*" $response
assert_match "*(*%R~foo2*%W~bar2*~baz2*)*" $response
}
test {Test R+W is the same as all permissions} {
r ACL setuser selector-rw-info %R~foo %W~foo %RW~bar
set user [r ACL GETUSER selector-rw-info]
assert_equal "~foo ~bar" [dict get $user keys]
}
test {Test basic dry run functionality} {
r ACL setuser command-test +@all %R~read* %W~write* %RW~rw*
assert_equal "OK" [r ACL DRYRUN command-test GET read]
catch {r ACL DRYRUN not-a-user GET read} e
assert_equal "ERR User 'not-a-user' not found" $e
catch {r ACL DRYRUN command-test not-a-command read} e
assert_equal "ERR Command 'not-a-command' not found" $e
}
test {Test various odd commands for key permissions} {
r ACL setuser command-test +@all %R~read* %W~write* %RW~rw*
# Test migrate, which is marked with incomplete keys
assert_equal "OK" [r ACL DRYRUN command-test MIGRATE whatever whatever rw]
assert_equal "This user has no permissions to access the 'read' key" [r ACL DRYRUN command-test MIGRATE whatever whatever read]
assert_equal "This user has no permissions to access the 'write' key" [r ACL DRYRUN command-test MIGRATE whatever whatever write]
assert_equal "OK" [r ACL DRYRUN command-test MIGRATE whatever whatever "" 0 5000 KEYS rw]
assert_equal "This user has no permissions to access the 'read' key" [r ACL DRYRUN command-test MIGRATE whatever whatever "" 0 5000 KEYS read]
assert_equal "This user has no permissions to access the 'write' key" [r ACL DRYRUN command-test MIGRATE whatever whatever "" 0 5000 KEYS write]
# Test SORT, which is marked with incomplete keys
assert_equal "OK" [r ACL DRYRUN command-test SORT read STORE write]
assert_equal "This user has no permissions to access the 'read' key" [r ACL DRYRUN command-test SORT read STORE read]
assert_equal "This user has no permissions to access the 'write' key" [r ACL DRYRUN command-test SORT write STORE write]
# Test EVAL, which uses the numkey keyspec (Also test EVAL_RO)
assert_equal "OK" [r ACL DRYRUN command-test EVAL "" 1 rw1]
assert_equal "This user has no permissions to access the 'read' key" [r ACL DRYRUN command-test EVAL "" 1 read]
assert_equal "OK" [r ACL DRYRUN command-test EVAL_RO "" 1 rw1]
assert_equal "OK" [r ACL DRYRUN command-test EVAL_RO "" 1 read]
# Read is an optional argument and not a key here, make sure we don't treat it as a key
assert_equal "OK" [r ACL DRYRUN command-test EVAL "" 0 read]
# These are syntax errors, but it's 'OK' from an ACL perspective
assert_equal "OK" [r ACL DRYRUN command-test EVAL "" -1 read]
assert_equal "OK" [r ACL DRYRUN command-test EVAL "" 3 rw rw]
assert_equal "OK" [r ACL DRYRUN command-test EVAL "" 3 rw read]
# Test GEORADIUS which uses the last type of keyspec, keyword
assert_equal "OK" [r ACL DRYRUN command-test GEORADIUS read longitude latitude radius M STOREDIST write]
assert_equal "OK" [r ACL DRYRUN command-test GEORADIUS read longitude latitude radius M]
assert_equal "This user has no permissions to access the 'read2' key" [r ACL DRYRUN command-test GEORADIUS read1 longitude latitude radius M STOREDIST read2]
assert_equal "This user has no permissions to access the 'write1' key" [r ACL DRYRUN command-test GEORADIUS write1 longitude latitude radius M STOREDIST write2]
assert_equal "OK" [r ACL DRYRUN command-test GEORADIUS read longitude latitude radius M STORE write]
assert_equal "OK" [r ACL DRYRUN command-test GEORADIUS read longitude latitude radius M]
assert_equal "This user has no permissions to access the 'read2' key" [r ACL DRYRUN command-test GEORADIUS read1 longitude latitude radius M STORE read2]
assert_equal "This user has no permissions to access the 'write1' key" [r ACL DRYRUN command-test GEORADIUS write1 longitude latitude radius M STORE write2]
}
test {Test sharded channel permissions} {
r ACL setuser test-channels +@all resetchannels &channel
assert_equal "OK" [r ACL DRYRUN test-channels spublish channel foo]
assert_equal "OK" [r ACL DRYRUN test-channels ssubscribe channel]
assert_equal "OK" [r ACL DRYRUN test-channels sunsubscribe]
assert_equal "OK" [r ACL DRYRUN test-channels sunsubscribe channel]
assert_equal "OK" [r ACL DRYRUN test-channels sunsubscribe otherchannel]
assert_equal "This user has no permissions to access the 'otherchannel' channel" [r ACL DRYRUN test-channels spublish otherchannel foo]
assert_equal "This user has no permissions to access the 'otherchannel' channel" [r ACL DRYRUN test-channels ssubscribe otherchannel foo]
}
$r2 close
}
set server_path [tmpdir "selectors.acl"]
exec cp -f tests/assets/userwithselectors.acl $server_path
exec cp -f tests/assets/default.conf $server_path
start_server [list overrides [list "dir" $server_path "aclfile" "userwithselectors.acl"] tags [list "external:skip"]] {
test {Test behavior of loading ACLs} {
set selectors [dict get [r ACL getuser alice] selectors]
assert_equal [llength $selectors] 1
set test_selector [lindex $selectors 0]
assert_equal "-@all +get" [dict get $test_selector "commands"]
assert_equal "~rw*" [dict get $test_selector "keys"]
set selectors [dict get [r ACL getuser bob] selectors]
assert_equal [llength $selectors] 2
set test_selector [lindex $selectors 0]
assert_equal "-@all +set" [dict get $test_selector "commands"]
assert_equal "%W~w*" [dict get $test_selector "keys"]
set test_selector [lindex $selectors 1]
assert_equal "-@all +get" [dict get $test_selector "commands"]
assert_equal "%R~r*" [dict get $test_selector "keys"]
}
}
...@@ -733,27 +733,27 @@ start_server [list overrides [list "dir" $server_path "acl-pubsub-default" "rese ...@@ -733,27 +733,27 @@ start_server [list overrides [list "dir" $server_path "acl-pubsub-default" "rese
test {Default user has access to all channels irrespective of flag} { test {Default user has access to all channels irrespective of flag} {
set channelinfo [dict get [r ACL getuser default] channels] set channelinfo [dict get [r ACL getuser default] channels]
assert_equal "*" $channelinfo assert_equal "&*" $channelinfo
set channelinfo [dict get [r ACL getuser alice] channels] set channelinfo [dict get [r ACL getuser alice] channels]
assert_equal "" $channelinfo assert_equal "" $channelinfo
} }
test {Update acl-pubsub-default, existing users shouldn't get affected} { test {Update acl-pubsub-default, existing users shouldn't get affected} {
set channelinfo [dict get [r ACL getuser default] channels] set channelinfo [dict get [r ACL getuser default] channels]
assert_equal "*" $channelinfo assert_equal "&*" $channelinfo
r CONFIG set acl-pubsub-default allchannels r CONFIG set acl-pubsub-default allchannels
r ACL setuser mydefault r ACL setuser mydefault
set channelinfo [dict get [r ACL getuser mydefault] channels] set channelinfo [dict get [r ACL getuser mydefault] channels]
assert_equal "*" $channelinfo assert_equal "&*" $channelinfo
r CONFIG set acl-pubsub-default resetchannels r CONFIG set acl-pubsub-default resetchannels
set channelinfo [dict get [r ACL getuser mydefault] channels] set channelinfo [dict get [r ACL getuser mydefault] channels]
assert_equal "*" $channelinfo assert_equal "&*" $channelinfo
} }
test {Single channel is valid} { test {Single channel is valid} {
r ACL setuser onechannel &test r ACL setuser onechannel &test
set channelinfo [dict get [r ACL getuser onechannel] channels] set channelinfo [dict get [r ACL getuser onechannel] channels]
assert_equal test $channelinfo assert_equal "&test" $channelinfo
r ACL deluser onechannel r ACL deluser onechannel
} }
...@@ -772,7 +772,7 @@ start_server [list overrides [list "dir" $server_path "acl-pubsub-default" "rese ...@@ -772,7 +772,7 @@ start_server [list overrides [list "dir" $server_path "acl-pubsub-default" "rese
test {Only default user has access to all channels irrespective of flag} { test {Only default user has access to all channels irrespective of flag} {
set channelinfo [dict get [r ACL getuser default] channels] set channelinfo [dict get [r ACL getuser default] channels]
assert_equal "*" $channelinfo assert_equal "&*" $channelinfo
set channelinfo [dict get [r ACL getuser alice] channels] set channelinfo [dict get [r ACL getuser alice] channels]
assert_equal "" $channelinfo assert_equal "" $channelinfo
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment