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: Oran Agra <oran@redislabs.com>
This commit is contained in:
parent
10bbeb6837
commit
55c81f2cd3
12
redis.conf
12
redis.conf
@ -871,6 +871,10 @@ replica-priority 100
|
||||
# commands. For instance ~* allows all the keys. The pattern
|
||||
# is a glob-style pattern like the one of KEYS.
|
||||
# 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 ~*
|
||||
# resetkeys Flush the list of allowed keys patterns.
|
||||
# &<pattern> Add a glob-style pattern of Pub/Sub channels that can be
|
||||
@ -896,6 +900,14 @@ replica-priority 100
|
||||
# reset Performs the following actions: resetpass, resetkeys, off,
|
||||
# -@all. The user returns to the same state it has immediately
|
||||
# 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
|
||||
# passwords, then flags, or key patterns. However note that the additive
|
||||
|
@ -6423,7 +6423,8 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in
|
||||
for (i = 0; i < ms->count; i++) {
|
||||
struct redisCommand *mcmd;
|
||||
robj **margv;
|
||||
int margc, *keyindex, numkeys, j;
|
||||
int margc, numkeys, j;
|
||||
keyReference *keyindex;
|
||||
|
||||
mcmd = ms->commands[i].cmd;
|
||||
margc = ms->commands[i].argc;
|
||||
@ -6434,7 +6435,7 @@ clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, in
|
||||
keyindex = result.keys;
|
||||
|
||||
for (j = 0; j < numkeys; j++) {
|
||||
robj *thiskey = margv[keyindex[j]];
|
||||
robj *thiskey = margv[keyindex[j].pos];
|
||||
int thisslot = keyHashSlot((char*)thiskey->ptr,
|
||||
sdslen(thiskey->ptr));
|
||||
|
||||
|
@ -3812,6 +3812,22 @@ struct redisCommandArg ACL_DELUSER_Args[] = {
|
||||
{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 history */
|
||||
@ -3901,6 +3917,7 @@ struct redisCommandArg ACL_LOG_Args[] = {
|
||||
/* ACL SETUSER history */
|
||||
commandHistory ACL_SETUSER_History[] = {
|
||||
{"6.2.0","Added Pub/Sub channel patterns."},
|
||||
{"7.0.0","Added selectors and key based permissions."},
|
||||
{0}
|
||||
};
|
||||
|
||||
@ -3934,6 +3951,7 @@ struct redisCommandArg ACL_SETUSER_Args[] = {
|
||||
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},
|
||||
{"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},
|
||||
{"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},
|
||||
@ -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},
|
||||
{"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},
|
||||
{"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_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","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,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},
|
||||
{"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},
|
||||
|
35
src/commands/acl-dryrun.json
Normal file
35
src/commands/acl-dryrun.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"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 @@
|
||||
[
|
||||
"6.2.0",
|
||||
"Added Pub/Sub channel patterns."
|
||||
],
|
||||
[
|
||||
"7.0.0",
|
||||
"Added selectors and key based permissions."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
|
@ -21,8 +21,7 @@
|
||||
{
|
||||
"flags": [
|
||||
"RO",
|
||||
"ACCESS",
|
||||
"INCOMPLETE"
|
||||
"ACCESS"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
@ -37,6 +36,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"flags": [
|
||||
"RO",
|
||||
"ACCESS",
|
||||
"INCOMPLETE"
|
||||
],
|
||||
"begin_search": {
|
||||
"unknown": null
|
||||
},
|
||||
"find_keys": {
|
||||
"unknown": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"flags": [
|
||||
"OW",
|
||||
|
@ -6,6 +6,7 @@
|
||||
"since": "7.0.0",
|
||||
"arity": -2,
|
||||
"function": "sortroCommand",
|
||||
"get_keys_function": "sortROGetKeys",
|
||||
"command_flags": [
|
||||
"READONLY"
|
||||
],
|
||||
@ -19,8 +20,7 @@
|
||||
{
|
||||
"flags": [
|
||||
"RO",
|
||||
"ACCESS",
|
||||
"INCOMPLETE"
|
||||
"ACCESS"
|
||||
],
|
||||
"begin_search": {
|
||||
"index": {
|
||||
@ -34,6 +34,19 @@
|
||||
"limit": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"flags": [
|
||||
"RO",
|
||||
"ACCESS",
|
||||
"INCOMPLETE"
|
||||
],
|
||||
"begin_search": {
|
||||
"unknown": null
|
||||
},
|
||||
"find_keys": {
|
||||
"unknown": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"arguments": [
|
||||
|
@ -122,7 +122,7 @@ configEnum oom_score_adj_enum[] = {
|
||||
};
|
||||
|
||||
configEnum acl_pubsub_default_enum[] = {
|
||||
{"allchannels", USER_FLAG_ALLCHANNELS},
|
||||
{"allchannels", SELECTOR_FLAG_ALLCHANNELS},
|
||||
{"resetchannels", 0},
|
||||
{NULL, 0}
|
||||
};
|
||||
@ -2811,7 +2811,7 @@ standardConfig configs[] = {
|
||||
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("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("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),
|
||||
|
274
src/db.c
274
src/db.c
@ -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
|
||||
* 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
|
||||
* buffer here. */
|
||||
if (!result->keys) {
|
||||
@ -1665,12 +1665,12 @@ int *getKeysPrepareResult(getKeysResult *result, int numkeys) {
|
||||
if (numkeys > result->size) {
|
||||
if (result->keys != result->keysbuf) {
|
||||
/* 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 {
|
||||
/* We are using a static buffer, copy its contents */
|
||||
result->keys = zmalloc(numkeys * sizeof(int));
|
||||
result->keys = zmalloc(numkeys * sizeof(keyReference));
|
||||
if (result->numkeys)
|
||||
memcpy(result->keys, result->keysbuf, result->numkeys * sizeof(int));
|
||||
memcpy(result->keys, result->keysbuf, result->numkeys * sizeof(keyReference));
|
||||
}
|
||||
result->size = numkeys;
|
||||
}
|
||||
@ -1678,12 +1678,183 @@ int *getKeysPrepareResult(getKeysResult *result, int numkeys) {
|
||||
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
|
||||
* (firstkey, lastkey, step).
|
||||
* 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 j, i = 0, last, first, step, *keys;
|
||||
int j, i = 0, last, first, step;
|
||||
keyReference *keys;
|
||||
UNUSED(argv);
|
||||
|
||||
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,
|
||||
keys = getKeysPrepareResult(result, count);
|
||||
|
||||
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
|
||||
* of arguments (negative arity parameter) do not have dispatch
|
||||
* 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,
|
||||
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;
|
||||
return i;
|
||||
@ -1761,10 +1934,12 @@ void getKeysFreeResult(getKeysResult *result) {
|
||||
* 'keyCountOfs': num-keys index.
|
||||
* 'firstKeyOfs': firstkey index.
|
||||
* '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,
|
||||
robj **argv, int argc, getKeysResult *result) {
|
||||
int i, num, *keys;
|
||||
int i, num;
|
||||
keyReference *keys;
|
||||
|
||||
num = atoi(argv[keyCountOfs]->ptr);
|
||||
/* 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
|
||||
result->numkeys = numkeys;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@ -1830,20 +2011,46 @@ int bzmpopGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult
|
||||
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.
|
||||
*
|
||||
* SORT <sort-key> ... STORE <store-key> ...
|
||||
*
|
||||
* 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
|
||||
* 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 i, j, num, *keys, found_store = 0;
|
||||
int i, j, num, found_store = 0;
|
||||
keyReference *keys;
|
||||
UNUSED(cmd);
|
||||
|
||||
num = 0;
|
||||
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
|
||||
* 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 *
|
||||
* to be sure to process the *last* "STORE" option if multiple
|
||||
* ones are provided. This is same behavior as SORT. */
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1878,8 +2086,10 @@ int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *
|
||||
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 i, num, first, *keys;
|
||||
int i, num, first;
|
||||
keyReference *keys;
|
||||
UNUSED(cmd);
|
||||
|
||||
/* Assume the obvious form. */
|
||||
@ -1900,7 +2110,10 @@ int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResul
|
||||
}
|
||||
|
||||
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;
|
||||
return num;
|
||||
}
|
||||
@ -1908,9 +2121,12 @@ int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResul
|
||||
/* Helper function to extract keys from following commands:
|
||||
* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC]
|
||||
* [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 i, num, *keys;
|
||||
int i, num;
|
||||
keyReference *keys;
|
||||
UNUSED(cmd);
|
||||
|
||||
/* 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
|
||||
keys = getKeysPrepareResult(result, num);
|
||||
|
||||
/* Add all key positions to keys[] */
|
||||
keys[0] = 1;
|
||||
keys[0].pos = 1;
|
||||
keys[0].flags = 0;
|
||||
if(num > 1) {
|
||||
keys[1] = stored_key;
|
||||
keys[1].pos = stored_key;
|
||||
keys[1].flags = 0;
|
||||
}
|
||||
result->numkeys = num;
|
||||
return num;
|
||||
}
|
||||
|
||||
/* 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 i, num = 0, *keys;
|
||||
int i, num = 0;
|
||||
keyReference *keys;
|
||||
UNUSED(cmd);
|
||||
|
||||
/* 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
|
||||
there are also the IDs, one per key. */
|
||||
|
||||
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;
|
||||
return num;
|
||||
}
|
||||
|
62
src/module.c
62
src/module.c
@ -811,7 +811,7 @@ void RM_KeyAtPos(RedisModuleCtx *ctx, int pos) {
|
||||
getKeysPrepareResult(res, newsize);
|
||||
}
|
||||
|
||||
res->keys[res->numkeys++] = pos;
|
||||
res->keys[res->numkeys++].pos = pos;
|
||||
}
|
||||
|
||||
/* Helper for RM_CreateCommand(). Turns a string representing command
|
||||
@ -864,10 +864,10 @@ int64_t commandKeySpecsFlagsFromString(const char *s) {
|
||||
else if (!strcasecmp(t,"RW")) flags |= CMD_KEY_RW;
|
||||
else if (!strcasecmp(t,"OW")) flags |= CMD_KEY_OW;
|
||||
else if (!strcasecmp(t,"RM")) flags |= CMD_KEY_RM;
|
||||
else if (!strcasecmp(t,"ACCESS")) flags |= CMD_KEY_ACCESS;
|
||||
else if (!strcasecmp(t,"INSERT")) flags |= CMD_KEY_INSERT;
|
||||
else if (!strcasecmp(t,"UPDATE")) flags |= CMD_KEY_UPDATE;
|
||||
else if (!strcasecmp(t,"DELETE")) flags |= CMD_KEY_DELETE;
|
||||
else if (!strcasecmp(t,"access")) flags |= CMD_KEY_ACCESS;
|
||||
else if (!strcasecmp(t,"insert")) flags |= CMD_KEY_INSERT;
|
||||
else if (!strcasecmp(t,"update")) flags |= CMD_KEY_UPDATE;
|
||||
else if (!strcasecmp(t,"delete")) flags |= CMD_KEY_DELETE;
|
||||
else if (!strcasecmp(t,"channel")) flags |= CMD_KEY_CHANNEL;
|
||||
else if (!strcasecmp(t,"incomplete")) flags |= CMD_KEY_INCOMPLETE;
|
||||
else break;
|
||||
@ -1218,14 +1218,14 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCommand *command, int index, keyS
|
||||
* if (RedisModule_CreateCommand(ctx,"kspec.smove",kspec_legacy,"",0,0,0) == 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;
|
||||
* if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.smove",spec_id,1) == REDISMODULE_ERR)
|
||||
* return REDISMODULE_ERR;
|
||||
* if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.smove",spec_id,0,1,0) == 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;
|
||||
* if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"kspec.smove",spec_id,2) == REDISMODULE_ERR)
|
||||
* return REDISMODULE_ERR;
|
||||
@ -1237,7 +1237,7 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCommand *command, int index, keyS
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* RedisModule_AddCommandKeySpec(ctx,"module.config|get","read",&spec_id)
|
||||
* RedisModule_AddCommandKeySpec(ctx,"module.object|encoding","RO",&spec_id)
|
||||
*
|
||||
* Returns REDISMODULE_OK on success
|
||||
*/
|
||||
@ -7793,13 +7793,31 @@ int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **arg
|
||||
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_ERR is returned. */
|
||||
int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key) {
|
||||
if (ACLCheckKey(user->user, key->ptr, sdslen(key->ptr)) != ACL_OK)
|
||||
* REDISMODULE_KEY_PERMISSION_READ: Can the module read data from the key.
|
||||
* REDISMODULE_KEY_PERMISSION_WRITE: Can the module write data to the key.
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
||||
if (ACLUserCheckKeyPerm(user->user, key->ptr, sdslen(key->ptr), acl_flags) != ACL_OK) {
|
||||
errno = EACCES;
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
@ -7811,7 +7829,7 @@ int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key) {
|
||||
* If the user can access the pubsub channel, REDISMODULE_OK is returned, otherwise
|
||||
* REDISMODULE_ERR is returned. */
|
||||
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_OK;
|
||||
@ -10490,17 +10508,11 @@ int *RM_GetCommandKeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (result.keys == result.keysbuf) {
|
||||
/* If the result is using a stack based array, copy it. */
|
||||
unsigned long int size = sizeof(int) * result.numkeys;
|
||||
res = zmalloc(size);
|
||||
memcpy(res, result.keys, size);
|
||||
} else {
|
||||
/* 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;
|
||||
/* The return value here expects an array of key positions */
|
||||
unsigned long int size = sizeof(int) * result.numkeys;
|
||||
res = zmalloc(size);
|
||||
for (int i = 0; i < result.numkeys; i++) {
|
||||
res[i] = result.keys[i].pos;
|
||||
}
|
||||
|
||||
return res;
|
||||
|
@ -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_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. */
|
||||
#define REDISMODULE_EVENTLOOP_READABLE 1
|
||||
#define REDISMODULE_EVENTLOOP_WRITABLE 2
|
||||
@ -978,7 +984,7 @@ REDISMODULE_API int (*RedisModule_SetModuleUserACL)(RedisModuleUser *user, const
|
||||
REDISMODULE_API RedisModuleString * (*RedisModule_GetCurrentUserName)(RedisModuleCtx *ctx) 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_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 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;
|
||||
|
@ -4504,7 +4504,7 @@ void getKeysSubcommand(client *c) {
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
83
src/server.h
83
src/server.h
@ -995,54 +995,32 @@ typedef struct readyList {
|
||||
is USER_COMMAND_BITS_COUNT-1. */
|
||||
#define USER_FLAG_ENABLED (1<<0) /* The user is active. */
|
||||
#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_ALLCOMMANDS (1<<3) /* The user can run all commands. */
|
||||
#define USER_FLAG_NOPASS (1<<4) /* The user requires no password, any
|
||||
#define USER_FLAG_NOPASS (1<<2) /* The user requires no password, any
|
||||
provided password will work. For the
|
||||
default user, this also means that
|
||||
no AUTH is needed, and every
|
||||
connection is immediately
|
||||
authenticated. */
|
||||
#define USER_FLAG_ALLCHANNELS (1<<5) /* The user can mention any Pub/Sub
|
||||
channel. */
|
||||
#define USER_FLAG_SANITIZE_PAYLOAD (1<<6) /* The user require a deep RESTORE
|
||||
#define USER_FLAG_SANITIZE_PAYLOAD (1<<3) /* The user require a deep RESTORE
|
||||
* 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
|
||||
* 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 {
|
||||
sds name; /* The username as an SDS string. */
|
||||
uint64_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;
|
||||
uint32_t flags; /* See USER_FLAG_* */
|
||||
list *passwords; /* A list of SDS valid passwords for this user. */
|
||||
list *patterns; /* A list of allowed key patterns. If this field is NULL
|
||||
the user cannot mention any key in a command, unless
|
||||
the flag ALLKEYS is set in the user. */
|
||||
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. */
|
||||
list *selectors; /* A list of selectors this user validates commands
|
||||
against. This list will always contain at least
|
||||
one selector for backwards compatibility. */
|
||||
} user;
|
||||
|
||||
/* With multiplexing we need to take per-client state.
|
||||
@ -1913,16 +1891,22 @@ struct redisServer {
|
||||
|
||||
#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
|
||||
* keys as indices to the provided argv.
|
||||
*/
|
||||
typedef struct {
|
||||
int keysbuf[MAX_KEYS_BUFFER]; /* Pre-allocated buffer, to save heap allocations */
|
||||
int *keys; /* Key indices array, points to keysbuf or heap */
|
||||
keyReference keysbuf[MAX_KEYS_BUFFER]; /* Pre-allocated buffer, to save heap allocations */
|
||||
keyReference *keys; /* Key indices array, points to keysbuf or heap */
|
||||
int numkeys; /* Number of key indices return */
|
||||
int size; /* Available array size */
|
||||
} getKeysResult;
|
||||
#define GETKEYS_RESULT_INIT { {0}, NULL, 0, MAX_KEYS_BUFFER }
|
||||
#define GETKEYS_RESULT_INIT { {{0}}, NULL, 0, MAX_KEYS_BUFFER }
|
||||
|
||||
/* Key specs definitions.
|
||||
*
|
||||
@ -2714,14 +2698,19 @@ void ACLInit(void);
|
||||
#define ACL_LOG_CTX_MULTI 2
|
||||
#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 ACLAuthenticateUser(client *c, robj *username, robj *password);
|
||||
unsigned long ACLGetCommandID(const char *cmdname);
|
||||
void ACLClearCommandID(void);
|
||||
user *ACLGetUserByName(const char *name, size_t namelen);
|
||||
int ACLCheckKey(const user *u, const char *key, int keylen);
|
||||
int ACLCheckPubsubChannelPerm(sds channel, list *allowed, int literal);
|
||||
int ACLCheckAllUserCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr);
|
||||
int ACLUserCheckKeyPerm(user *u, const char *key, int keylen, int flags);
|
||||
int ACLUserCheckChannelPerm(user *u, sds channel, int literal);
|
||||
int ACLCheckAllUserCommandPerm(user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr);
|
||||
int ACLCheckAllPerm(client *c, int *idxptr);
|
||||
int ACLSetUser(user *u, const char *op, ssize_t oplen);
|
||||
uint64_t ACLGetCommandCategoryFlagByName(const char *name);
|
||||
@ -3003,9 +2992,14 @@ void freeObjAsync(robj *key, robj *obj, int dbid);
|
||||
void freeReplicationBacklogRefMemAsync(list *blocks, rax *index);
|
||||
|
||||
/* 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 getChannelsFromCommand(struct redisCommand *cmd, int argc, getKeysResult *result);
|
||||
int doesCommandHaveKeys(struct redisCommand *cmd);
|
||||
void getKeysFreeResult(getKeysResult *result);
|
||||
int sintercardGetKeys(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,
|
||||
int evalGetKeys(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 sortROGetKeys(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 xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
|
||||
|
@ -235,10 +235,10 @@ void trackingRememberKeys(client *c) {
|
||||
return;
|
||||
}
|
||||
|
||||
int *keys = result.keys;
|
||||
keyReference *keys = result.keys;
|
||||
|
||||
for(int j = 0; j < numkeys; j++) {
|
||||
int idx = keys[j];
|
||||
int idx = keys[j].pos;
|
||||
sds sdskey = c->argv[idx]->ptr;
|
||||
rax *ids = raxFind(TrackingTable,(unsigned char*)sdskey,sdslen(sdskey));
|
||||
if (ids == raxNotFound) {
|
||||
|
2
tests/assets/userwithselectors.acl
Normal file
2
tests/assets/userwithselectors.acl
Normal file
@ -0,0 +1,2 @@
|
||||
user alice on (+get ~rw*)
|
||||
user bob on (+set %W~w*) (+get %R~r*)
|
@ -2,17 +2,33 @@
|
||||
#include "redismodule.h"
|
||||
#include <errno.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
/* A wrap for SET command with ACL check on the key. */
|
||||
int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
if (argc < 3) {
|
||||
if (argc < 4) {
|
||||
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 */
|
||||
RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
|
||||
RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
|
||||
int ret = RedisModule_ACLCheckKeyPermissions(user, argv[1]);
|
||||
int ret = RedisModule_ACLCheckKeyPermissions(user, argv[2], permissions);
|
||||
if (ret != 0) {
|
||||
RedisModule_ReplyWithError(ctx, "DENIED KEY");
|
||||
RedisModule_FreeModuleUser(user);
|
||||
@ -20,7 +36,7 @@ int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
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) {
|
||||
RedisModule_ReplyWithError(ctx, "NULL reply returned");
|
||||
} else {
|
||||
|
@ -37,6 +37,7 @@ set ::all_tests {
|
||||
unit/quit
|
||||
unit/aofrw
|
||||
unit/acl
|
||||
unit/acl-v2
|
||||
unit/latency-monitor
|
||||
integration/block-repl
|
||||
integration/replication
|
||||
|
298
tests/unit/acl-v2.tcl
Normal file
298
tests/unit/acl-v2.tcl
Normal file
@ -0,0 +1,298 @@
|
||||
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
|
||||
|
||||
test {Default user has access to all channels irrespective of flag} {
|
||||
set channelinfo [dict get [r ACL getuser default] channels]
|
||||
assert_equal "*" $channelinfo
|
||||
assert_equal "&*" $channelinfo
|
||||
set channelinfo [dict get [r ACL getuser alice] channels]
|
||||
assert_equal "" $channelinfo
|
||||
}
|
||||
|
||||
test {Update acl-pubsub-default, existing users shouldn't get affected} {
|
||||
set channelinfo [dict get [r ACL getuser default] channels]
|
||||
assert_equal "*" $channelinfo
|
||||
assert_equal "&*" $channelinfo
|
||||
r CONFIG set acl-pubsub-default allchannels
|
||||
r ACL setuser mydefault
|
||||
set channelinfo [dict get [r ACL getuser mydefault] channels]
|
||||
assert_equal "*" $channelinfo
|
||||
assert_equal "&*" $channelinfo
|
||||
r CONFIG set acl-pubsub-default resetchannels
|
||||
set channelinfo [dict get [r ACL getuser mydefault] channels]
|
||||
assert_equal "*" $channelinfo
|
||||
assert_equal "&*" $channelinfo
|
||||
}
|
||||
|
||||
test {Single channel is valid} {
|
||||
r ACL setuser onechannel &test
|
||||
set channelinfo [dict get [r ACL getuser onechannel] channels]
|
||||
assert_equal test $channelinfo
|
||||
assert_equal "&test" $channelinfo
|
||||
r ACL deluser onechannel
|
||||
}
|
||||
|
||||
@ -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} {
|
||||
set channelinfo [dict get [r ACL getuser default] channels]
|
||||
assert_equal "*" $channelinfo
|
||||
assert_equal "&*" $channelinfo
|
||||
set channelinfo [dict get [r ACL getuser alice] channels]
|
||||
assert_equal "" $channelinfo
|
||||
}
|
||||
|
@ -20,11 +20,20 @@ start_server {tags {"modules acl"}} {
|
||||
|
||||
test {test module check acl for key perm} {
|
||||
# give permission for SET and block all keys but x
|
||||
r acl setuser default +set resetkeys ~x
|
||||
assert_equal [r aclcheck.set.check.key x 5] OK
|
||||
catch {r aclcheck.set.check.key y 5} e
|
||||
set e
|
||||
} {*DENIED KEY*}
|
||||
r acl setuser default +set resetkeys ~x %W~y %R~z
|
||||
|
||||
assert_equal [r aclcheck.set.check.key "*" x 5] OK
|
||||
catch {r aclcheck.set.check.key "*" v 5} e
|
||||
assert_match "*DENIED KEY*" $e
|
||||
|
||||
assert_equal [r aclcheck.set.check.key "W" y 5] OK
|
||||
catch {r aclcheck.set.check.key "W" v 5} e
|
||||
assert_match "*DENIED KEY*" $e
|
||||
|
||||
assert_equal [r aclcheck.set.check.key "R" z 5] OK
|
||||
catch {r aclcheck.set.check.key "R" v 5} e
|
||||
assert_match "*DENIED KEY*" $e
|
||||
}
|
||||
|
||||
test {test module check acl for module user} {
|
||||
# the module user has access to all keys
|
||||
|
Loading…
x
Reference in New Issue
Block a user