Unverified Commit 71204f96 authored by Madelyn Olson's avatar Madelyn Olson Committed by GitHub
Browse files

Implemented module getchannels api and renamed channel keyspec (#10299)

This implements the following main pieces of functionality:
* Renames key spec "CHANNEL" to be "NOT_KEY", and update the documentation to
  indicate it's for cluster routing and not for any other key related purpose.
* Add the getchannels-api, so that modules can now define commands that are subject to
  ACL channel permission checks. 
* Add 4 new flags that describe how a module interacts with a command (SUBSCRIBE, PUBLISH,
  UNSUBSCRIBE, and PATTERN). They are all technically composable, however not sure how a
  command could both subscribe and unsubscribe from a command at once, but didn't see
  a reason to add explicit validation there.
* Add two new module apis RM_ChannelAtPosWithFlags and RM_IsChannelsPositionRequest to
  duplicate the functionality provided by the keys position APIs.
* The RM_ACLCheckChannelPermissions (only released in 7.0 RC1) was changed to take flags
  rather than a boolean literal.
* The RM_ACLCheckKeyPermissions (only released in 7.0 RC1) was changed to take flags
  corresponding to keyspecs instead of custom permission flags. These keyspec flags mimic
  the flags for ACLCheckChannelPermissions.
parent c4c68f5d
...@@ -1531,18 +1531,15 @@ static int ACLSelectorCheckKey(aclSelector *selector, const char *key, int keyle ...@@ -1531,18 +1531,15 @@ static int ACLSelectorCheckKey(aclSelector *selector, const char *key, int keyle
return ACL_DENIED_KEY; return ACL_DENIED_KEY;
} }
/* Returns if a given command may possibly access channels. For this context, /* Checks a channel against a provided list of channels. The is_pattern
* the unsubscribe commands do not have channels. */ * argument should only be used when subscribing (not when publishing)
static int ACLDoesCommandHaveChannels(struct redisCommand *cmd) { * and controls whether the input channel is evaluated as a channel pattern
return (cmd->proc == publishCommand * (like in PSUBSCRIBE) or a plain channel name (like in SUBSCRIBE).
|| cmd->proc == subscribeCommand *
|| cmd->proc == psubscribeCommand * Note that a plain channel name like in PUBLISH or SUBSCRIBE can be
|| cmd->proc == spublishCommand * matched against ACL channel patterns, but the pattern provided in PSUBSCRIBE
|| cmd->proc == ssubscribeCommand); * can only be matched as a literal against an ACL pattern (using plain string compare). */
} static int ACLCheckChannelAgainstList(list *reference, const char *channel, int channellen, int is_pattern) {
/* Checks a channel against a provide list of channels. */
static int ACLCheckChannelAgainstList(list *reference, const char *channel, int channellen, int literal) {
listIter li; listIter li;
listNode *ln; listNode *ln;
...@@ -1550,8 +1547,10 @@ static int ACLCheckChannelAgainstList(list *reference, const char *channel, int ...@@ -1550,8 +1547,10 @@ static int ACLCheckChannelAgainstList(list *reference, const char *channel, int
while((ln = listNext(&li))) { while((ln = listNext(&li))) {
sds pattern = listNodeValue(ln); sds pattern = listNodeValue(ln);
size_t plen = sdslen(pattern); size_t plen = sdslen(pattern);
if ((literal && !strcmp(pattern,channel)) || /* Channel patterns are matched literally against the channels in
(!literal && stringmatchlen(pattern,plen,channel,channellen,0))) * the list. Regular channels perform pattern matching. */
if ((is_pattern && !strcmp(pattern,channel)) ||
(!is_pattern && stringmatchlen(pattern,plen,channel,channellen,0)))
{ {
return ACL_OK; return ACL_OK;
} }
...@@ -1559,28 +1558,6 @@ static int ACLCheckChannelAgainstList(list *reference, const char *channel, int ...@@ -1559,28 +1558,6 @@ static int ACLCheckChannelAgainstList(list *reference, const char *channel, int
return ACL_DENIED_CHANNEL; return ACL_DENIED_CHANNEL;
} }
/* Check if the pub/sub channels of the command can be executed
* according to the ACL channels associated with the specified selector.
*
* idx and count are the index and count of channel arguments from the
* command. The literal argument controls whether the selector's ACL channels are
* evaluated as literal values or matched as glob-like patterns.
*
* If the selector can execute the command ACL_OK is returned, otherwise
* ACL_DENIED_CHANNEL. */
static int ACLSelectorCheckPubsubArguments(aclSelector *s, robj **argv, int idx, int count, int literal, int *idxptr) {
for (int j = idx; j < idx+count; j++) {
if (ACLCheckChannelAgainstList(s->channels, argv[j]->ptr, sdslen(argv[j]->ptr), literal != ACL_OK)) {
if (idxptr) *idxptr = j;
return ACL_DENIED_CHANNEL;
}
}
/* If we survived all the above checks, the selector can execute the
* command. */
return ACL_OK;
}
/* To prevent duplicate calls to getKeysResult, a cache is maintained /* To prevent duplicate calls to getKeysResult, a cache is maintained
* in between calls to the various selectors. */ * in between calls to the various selectors. */
typedef struct { typedef struct {
...@@ -1645,7 +1622,7 @@ static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd, ...@@ -1645,7 +1622,7 @@ static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd,
int idx = resultidx[j].pos; int idx = resultidx[j].pos;
ret = ACLSelectorCheckKey(selector, argv[idx]->ptr, sdslen(argv[idx]->ptr), resultidx[j].flags); ret = ACLSelectorCheckKey(selector, argv[idx]->ptr, sdslen(argv[idx]->ptr), resultidx[j].flags);
if (ret != ACL_OK) { if (ret != ACL_OK) {
if (resultidx) *keyidxptr = resultidx[j].pos; if (keyidxptr) *keyidxptr = resultidx[j].pos;
return ret; return ret;
} }
} }
...@@ -1653,26 +1630,30 @@ static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd, ...@@ -1653,26 +1630,30 @@ static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd,
/* Check if the user can execute commands explicitly touching the channels /* Check if the user can execute commands explicitly touching the channels
* mentioned in the command arguments */ * mentioned in the command arguments */
if (!(selector->flags & SELECTOR_FLAG_ALLCHANNELS) && ACLDoesCommandHaveChannels(cmd)) { const int channel_flags = CMD_CHANNEL_PUBLISH | CMD_CHANNEL_SUBSCRIBE;
if (cmd->proc == publishCommand || cmd->proc == spublishCommand) { if (!(selector->flags & SELECTOR_FLAG_ALLCHANNELS) && doesCommandHaveChannelsWithFlags(cmd, channel_flags)) {
ret = ACLSelectorCheckPubsubArguments(selector,argv, 1, 1, 0, keyidxptr); getKeysResult channels = (getKeysResult) GETKEYS_RESULT_INIT;
} else if (cmd->proc == subscribeCommand || cmd->proc == ssubscribeCommand) { getChannelsFromCommand(cmd, argv, argc, &channels);
ret = ACLSelectorCheckPubsubArguments(selector, argv, 1, argc-1, 0, keyidxptr); keyReference *channelref = channels.keys;
} else if (cmd->proc == psubscribeCommand) { for (int j = 0; j < channels.numkeys; j++) {
ret = ACLSelectorCheckPubsubArguments(selector, argv, 1, argc-1, 1, keyidxptr); int idx = channelref[j].pos;
} else { if (!(channelref[j].flags & channel_flags)) continue;
serverPanic("Encountered a command declared with channels but not handled"); int is_pattern = channelref[j].flags & CMD_CHANNEL_PATTERN;
} int ret = ACLCheckChannelAgainstList(selector->channels, argv[idx]->ptr, sdslen(argv[idx]->ptr), is_pattern);
if (ret != ACL_OK) { if (ret != ACL_OK) {
/* keyidxptr is set by ACLSelectorCheckPubsubArguments */ if (keyidxptr) *keyidxptr = channelref[j].pos;
return ret; getKeysFreeResult(&channels);
return ret;
}
} }
getKeysFreeResult(&channels);
} }
return ACL_OK; return ACL_OK;
} }
/* Check if the key can be accessed by the client according to /* Check if the key can be accessed by the client according to
* the ACLs associated with the specified user. * the ACLs associated with the specified user according to the
* keyspec access flags.
* *
* If the user can access the key, ACL_OK is returned, otherwise * If the user can access the key, ACL_OK is returned, otherwise
* ACL_DENIED_KEY is returned. */ * ACL_DENIED_KEY is returned. */
...@@ -1699,7 +1680,7 @@ int ACLUserCheckKeyPerm(user *u, const char *key, int keylen, int flags) { ...@@ -1699,7 +1680,7 @@ int ACLUserCheckKeyPerm(user *u, const char *key, int keylen, int flags) {
* *
* If the user can access the key, ACL_OK is returned, otherwise * If the user can access the key, ACL_OK is returned, otherwise
* ACL_DENIED_CHANNEL is returned. */ * ACL_DENIED_CHANNEL is returned. */
int ACLUserCheckChannelPerm(user *u, sds channel, int literal) { int ACLUserCheckChannelPerm(user *u, sds channel, int is_pattern) {
listIter li; listIter li;
listNode *ln; listNode *ln;
...@@ -1714,7 +1695,7 @@ int ACLUserCheckChannelPerm(user *u, sds channel, int literal) { ...@@ -1714,7 +1695,7 @@ int ACLUserCheckChannelPerm(user *u, sds channel, int literal) {
if (s->flags & SELECTOR_FLAG_ALLCHANNELS) return ACL_OK; if (s->flags & SELECTOR_FLAG_ALLCHANNELS) return ACL_OK;
/* Otherwise, loop over the selectors list and check each channel */ /* Otherwise, loop over the selectors list and check each channel */
if (ACLCheckChannelAgainstList(s->channels, channel, sdslen(channel), literal) == ACL_OK) { if (ACLCheckChannelAgainstList(s->channels, channel, sdslen(channel), is_pattern) == ACL_OK) {
return ACL_OK; return ACL_OK;
} }
} }
......
...@@ -6937,10 +6937,10 @@ struct redisCommand redisCommandTable[] = { ...@@ -6937,10 +6937,10 @@ struct redisCommand redisCommandTable[] = {
{"publish","Post a message to a channel","O(N+M) where N is the number of clients subscribed to the receiving channel and M is the total number of subscribed patterns (by any client).","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,PUBLISH_History,PUBLISH_tips,publishCommand,3,CMD_PUBSUB|CMD_LOADING|CMD_STALE|CMD_FAST|CMD_MAY_REPLICATE|CMD_SENTINEL,0,.args=PUBLISH_Args}, {"publish","Post a message to a channel","O(N+M) where N is the number of clients subscribed to the receiving channel and M is the total number of subscribed patterns (by any client).","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,PUBLISH_History,PUBLISH_tips,publishCommand,3,CMD_PUBSUB|CMD_LOADING|CMD_STALE|CMD_FAST|CMD_MAY_REPLICATE|CMD_SENTINEL,0,.args=PUBLISH_Args},
{"pubsub","A container for Pub/Sub commands","Depends on subcommand.","2.8.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,PUBSUB_History,PUBSUB_tips,NULL,-2,0,0,.subcommands=PUBSUB_Subcommands}, {"pubsub","A container for Pub/Sub commands","Depends on subcommand.","2.8.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,PUBSUB_History,PUBSUB_tips,NULL,-2,0,0,.subcommands=PUBSUB_Subcommands},
{"punsubscribe","Stop listening for messages posted to channels matching the given patterns","O(N+M) where N is the number of patterns the client is already subscribed and M is the number of total patterns subscribed in the system (by any client).","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,PUNSUBSCRIBE_History,PUNSUBSCRIBE_tips,punsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=PUNSUBSCRIBE_Args}, {"punsubscribe","Stop listening for messages posted to channels matching the given patterns","O(N+M) where N is the number of patterns the client is already subscribed and M is the number of total patterns subscribed in the system (by any client).","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,PUNSUBSCRIBE_History,PUNSUBSCRIBE_tips,punsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=PUNSUBSCRIBE_Args},
{"spublish","Post a message to a shard channel","O(N) where N is the number of clients subscribed to the receiving shard channel.","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,SPUBLISH_History,SPUBLISH_tips,spublishCommand,3,CMD_PUBSUB|CMD_LOADING|CMD_STALE|CMD_FAST|CMD_MAY_REPLICATE,0,{{NULL,CMD_KEY_CHANNEL,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=SPUBLISH_Args}, {"spublish","Post a message to a shard channel","O(N) where N is the number of clients subscribed to the receiving shard channel.","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,SPUBLISH_History,SPUBLISH_tips,spublishCommand,3,CMD_PUBSUB|CMD_LOADING|CMD_STALE|CMD_FAST|CMD_MAY_REPLICATE,0,{{NULL,CMD_KEY_NOT_KEY,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=SPUBLISH_Args},
{"ssubscribe","Listen for messages published to the given shard channels","O(N) where N is the number of shard channels to subscribe to.","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,SSUBSCRIBE_History,SSUBSCRIBE_tips,ssubscribeCommand,-2,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,0,{{NULL,CMD_KEY_CHANNEL,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={-1,1,0}}},.args=SSUBSCRIBE_Args}, {"ssubscribe","Listen for messages published to the given shard channels","O(N) where N is the number of shard channels to subscribe to.","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,SSUBSCRIBE_History,SSUBSCRIBE_tips,ssubscribeCommand,-2,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,0,{{NULL,CMD_KEY_NOT_KEY,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={-1,1,0}}},.args=SSUBSCRIBE_Args},
{"subscribe","Listen for messages published to the given channels","O(N) where N is the number of channels to subscribe to.","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,SUBSCRIBE_History,SUBSCRIBE_tips,subscribeCommand,-2,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=SUBSCRIBE_Args}, {"subscribe","Listen for messages published to the given channels","O(N) where N is the number of channels to subscribe to.","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,SUBSCRIBE_History,SUBSCRIBE_tips,subscribeCommand,-2,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=SUBSCRIBE_Args},
{"sunsubscribe","Stop listening for messages posted to the given shard channels","O(N) where N is the number of clients already subscribed to a channel.","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,SUNSUBSCRIBE_History,SUNSUBSCRIBE_tips,sunsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,0,{{NULL,CMD_KEY_CHANNEL,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={-1,1,0}}},.args=SUNSUBSCRIBE_Args}, {"sunsubscribe","Stop listening for messages posted to the given shard channels","O(N) where N is the number of clients already subscribed to a channel.","7.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,SUNSUBSCRIBE_History,SUNSUBSCRIBE_tips,sunsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE,0,{{NULL,CMD_KEY_NOT_KEY,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={-1,1,0}}},.args=SUNSUBSCRIBE_Args},
{"unsubscribe","Stop listening for messages posted to the given channels","O(N) where N is the number of clients already subscribed to a channel.","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,UNSUBSCRIBE_History,UNSUBSCRIBE_tips,unsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=UNSUBSCRIBE_Args}, {"unsubscribe","Stop listening for messages posted to the given channels","O(N) where N is the number of clients already subscribed to a channel.","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_PUBSUB,UNSUBSCRIBE_History,UNSUBSCRIBE_tips,unsubscribeCommand,-1,CMD_PUBSUB|CMD_NOSCRIPT|CMD_LOADING|CMD_STALE|CMD_SENTINEL,0,.args=UNSUBSCRIBE_Args},
/* scripting */ /* scripting */
{"eval","Execute a Lua script server side","Depends on the script that is executed.","2.6.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SCRIPTING,EVAL_History,EVAL_tips,evalCommand,-3,CMD_NOSCRIPT|CMD_SKIP_MONITOR|CMD_MAY_REPLICATE|CMD_NO_MANDATORY_KEYS|CMD_STALE,ACL_CATEGORY_SCRIPTING,{{"We cannot tell how the keys will be used so we assume the worst, RW and UPDATE",CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={2},KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},evalGetKeys,.args=EVAL_Args}, {"eval","Execute a Lua script server side","Depends on the script that is executed.","2.6.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SCRIPTING,EVAL_History,EVAL_tips,evalCommand,-3,CMD_NOSCRIPT|CMD_SKIP_MONITOR|CMD_MAY_REPLICATE|CMD_NO_MANDATORY_KEYS|CMD_STALE,ACL_CATEGORY_SCRIPTING,{{"We cannot tell how the keys will be used so we assume the worst, RW and UPDATE",CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={2},KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},evalGetKeys,.args=EVAL_Args},
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
"key_specs": [ "key_specs": [
{ {
"flags": [ "flags": [
"CHANNEL" "NOT_KEY"
], ],
"begin_search": { "begin_search": {
"index": { "index": {
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
"key_specs": [ "key_specs": [
{ {
"flags": [ "flags": [
"CHANNEL" "NOT_KEY"
], ],
"begin_search": { "begin_search": {
"index": { "index": {
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
"key_specs": [ "key_specs": [
{ {
"flags": [ "flags": [
"CHANNEL" "NOT_KEY"
], ],
"begin_search": { "begin_search": {
"index": { "index": {
......
...@@ -1692,7 +1692,7 @@ int64_t getAllKeySpecsFlags(struct redisCommand *cmd, int inv) { ...@@ -1692,7 +1692,7 @@ int64_t getAllKeySpecsFlags(struct redisCommand *cmd, int inv) {
/* Fetch the keys based of the provided key specs. Returns the number of keys found, or -1 on error. /* 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. * 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_INCLUDE_NOT_KEYS: Return 'fake' keys as if they were keys.
* GET_KEYSPEC_RETURN_PARTIAL: Skips invalid and incomplete keyspecs but returns the keys * GET_KEYSPEC_RETURN_PARTIAL: Skips invalid and incomplete keyspecs but returns the keys
* found in other valid keyspecs. * found in other valid keyspecs.
*/ */
...@@ -1703,8 +1703,8 @@ int getKeysUsingKeySpecs(struct redisCommand *cmd, robj **argv, int argc, int se ...@@ -1703,8 +1703,8 @@ int getKeysUsingKeySpecs(struct redisCommand *cmd, robj **argv, int argc, int se
for (j = 0; j < cmd->key_specs_num; j++) { for (j = 0; j < cmd->key_specs_num; j++) {
keySpec *spec = cmd->key_specs + j; keySpec *spec = cmd->key_specs + j;
serverAssert(spec->begin_search_type != KSPEC_BS_INVALID); serverAssert(spec->begin_search_type != KSPEC_BS_INVALID);
/* Skip specs that represent channels instead of keys */ /* Skip specs that represent 'fake' keys */
if ((spec->flags & CMD_KEY_CHANNEL) && !(search_flags & GET_KEYSPEC_INCLUDE_CHANNELS)) { if ((spec->flags & CMD_KEY_NOT_KEY) && !(search_flags & GET_KEYSPEC_INCLUDE_NOT_KEYS)) {
continue; continue;
} }
...@@ -1823,8 +1823,8 @@ invalid_spec: ...@@ -1823,8 +1823,8 @@ invalid_spec:
* 'cmd' must be point to the corresponding entry into the redisCommand * 'cmd' must be point to the corresponding entry into the redisCommand
* table, according to the command name in argv[0]. */ * table, according to the command name in argv[0]. */
int getKeysFromCommandWithSpecs(struct redisCommand *cmd, robj **argv, int argc, int search_flags, getKeysResult *result) { int getKeysFromCommandWithSpecs(struct redisCommand *cmd, robj **argv, int argc, int search_flags, getKeysResult *result) {
/* The command has at least one key-spec not marked as CHANNEL */ /* The command has at least one key-spec not marked as NOT_KEY */
int has_keyspec = (getAllKeySpecsFlags(cmd, 1) & CMD_KEY_CHANNEL); int has_keyspec = (getAllKeySpecsFlags(cmd, 1) & CMD_KEY_NOT_KEY);
/* The command has at least one key-spec marked as VARIABLE_FLAGS */ /* The command has at least one key-spec marked as VARIABLE_FLAGS */
int has_varflags = (getAllKeySpecsFlags(cmd, 0) & CMD_KEY_VARIABLE_FLAGS); int has_varflags = (getAllKeySpecsFlags(cmd, 0) & CMD_KEY_VARIABLE_FLAGS);
...@@ -1861,7 +1861,83 @@ int getKeysFromCommandWithSpecs(struct redisCommand *cmd, robj **argv, int argc, ...@@ -1861,7 +1861,83 @@ int getKeysFromCommandWithSpecs(struct redisCommand *cmd, robj **argv, int argc,
int doesCommandHaveKeys(struct redisCommand *cmd) { int doesCommandHaveKeys(struct redisCommand *cmd) {
return (!(cmd->flags & CMD_MODULE) && cmd->getkeys_proc) || /* has getkeys_proc (non modules) */ return (!(cmd->flags & CMD_MODULE) && cmd->getkeys_proc) || /* has getkeys_proc (non modules) */
(cmd->flags & CMD_MODULE_GETKEYS) || /* module with GETKEYS */ (cmd->flags & CMD_MODULE_GETKEYS) || /* module with GETKEYS */
(getAllKeySpecsFlags(cmd, 1) & CMD_KEY_CHANNEL); /* has at least one key-spec not marked as CHANNEL */ (getAllKeySpecsFlags(cmd, 1) & CMD_KEY_NOT_KEY); /* has at least one key-spec not marked as NOT_KEY */
}
/* A simplified channel spec table that contains all of the redis commands
* and which channels they have and how they are accessed. */
typedef struct ChannelSpecs {
redisCommandProc *proc; /* Command procedure to match against */
uint64_t flags; /* CMD_CHANNEL_* flags for this command */
int start; /* The initial position of the first channel */
int count; /* The number of channels, or -1 if all remaining
* arguments are channels. */
} ChannelSpecs;
ChannelSpecs commands_with_channels[] = {
{subscribeCommand, CMD_CHANNEL_SUBSCRIBE, 1, -1},
{ssubscribeCommand, CMD_CHANNEL_SUBSCRIBE, 1, -1},
{unsubscribeCommand, CMD_CHANNEL_UNSUBSCRIBE, 1, -1},
{sunsubscribeCommand, CMD_CHANNEL_UNSUBSCRIBE, 1, -1},
{psubscribeCommand, CMD_CHANNEL_PATTERN | CMD_CHANNEL_SUBSCRIBE, 1, -1},
{punsubscribeCommand, CMD_CHANNEL_PATTERN | CMD_CHANNEL_UNSUBSCRIBE, 1, -1},
{publishCommand, CMD_CHANNEL_PUBLISH, 1, 1},
{spublishCommand, CMD_CHANNEL_PUBLISH, 1, 1},
{NULL,0} /* Terminator. */
};
/* Returns 1 if the command may access any channels matched by the flags
* argument. */
int doesCommandHaveChannelsWithFlags(struct redisCommand *cmd, int flags) {
/* If a module declares get channels, we are just going to assume
* has channels. This API is allowed to return false positives. */
if (cmd->flags & CMD_MODULE_GETCHANNELS) {
return 1;
}
for (ChannelSpecs *spec = commands_with_channels; spec->proc != NULL; spec += 1) {
if (cmd->proc == spec->proc) {
return !!(spec->flags & flags);
}
}
return 0;
}
/* Return all the arguments that are channels in the command passed via argc / argv.
* This function behaves similar to getKeysFromCommandWithSpecs, but with channels
* instead of keys.
*
* The command returns the positions of all the channel 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 channel.
*
* 'cmd' must be point to the corresponding entry into the redisCommand
* table, according to the command name in argv[0]. */
int getChannelsFromCommand(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
keyReference *keys;
/* If a module declares get channels, use that. */
if (cmd->flags & CMD_MODULE_GETCHANNELS) {
return moduleGetCommandChannelsViaAPI(cmd, argv, argc, result);
}
/* Otherwise check the channel spec table */
for (ChannelSpecs *spec = commands_with_channels; spec != NULL; spec += 1) {
if (cmd->proc == spec->proc) {
int start = spec->start;
int stop = (spec->count == -1) ? argc : start + spec->count;
if (stop > argc) stop = argc;
int count = 0;
keys = getKeysPrepareResult(result, stop - start);
for (int i = start; i < stop; i++ ) {
keys[count].pos = i;
keys[count++].flags = spec->flags;
}
result->numkeys = count;
return count;
}
}
return 0;
} }
/* 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
......
...@@ -154,7 +154,8 @@ struct RedisModuleCtx { ...@@ -154,7 +154,8 @@ struct RedisModuleCtx {
gets called for clients blocked gets called for clients blocked
on keys. */ on keys. */
/* Used if there is the REDISMODULE_CTX_KEYS_POS_REQUEST flag set. */ /* Used if there is the REDISMODULE_CTX_KEYS_POS_REQUEST or
* REDISMODULE_CTX_CHANNEL_POS_REQUEST flag set. */
getKeysResult *keys_result; getKeysResult *keys_result;
struct RedisModulePoolAllocBlock *pa_head; struct RedisModulePoolAllocBlock *pa_head;
...@@ -173,6 +174,7 @@ typedef struct RedisModuleCtx RedisModuleCtx; ...@@ -173,6 +174,7 @@ typedef struct RedisModuleCtx RedisModuleCtx;
when the context is destroyed */ when the context is destroyed */
#define REDISMODULE_CTX_NEW_CLIENT (1<<7) /* Free client object when the #define REDISMODULE_CTX_NEW_CLIENT (1<<7) /* Free client object when the
context is destroyed */ context is destroyed */
#define REDISMODULE_CTX_CHANNELS_POS_REQUEST (1<<8)
/* This represents a Redis key opened with RM_OpenKey(). */ /* This represents a Redis key opened with RM_OpenKey(). */
struct RedisModuleKey { struct RedisModuleKey {
...@@ -781,6 +783,25 @@ int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc, ...@@ -781,6 +783,25 @@ int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc,
return result->numkeys; return result->numkeys;
} }
/* This function returns the list of channels, with the same interface as
* moduleGetCommandKeysViaAPI, for modules that declare "getchannels-api"
* during registration. Unlike keys, this is the only way to declare channels. */
int moduleGetCommandChannelsViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
RedisModuleCommand *cp = (void*)(unsigned long)cmd->getkeys_proc;
RedisModuleCtx ctx;
moduleCreateContext(&ctx, cp->module, REDISMODULE_CTX_CHANNELS_POS_REQUEST);
/* Initialize getKeysResult */
getKeysPrepareResult(result, MAX_KEYS_BUFFER);
ctx.keys_result = result;
cp->func(&ctx,(void**)argv,argc);
/* We currently always use the array allocated by RM_RM_ChannelAtPosWithFlags() and don't try
* to optimize for the pre-allocated buffer. */
moduleFreeContext(&ctx);
return result->numkeys;
}
/* -------------------------------------------------------------------------- /* --------------------------------------------------------------------------
* ## Commands API * ## Commands API
* *
...@@ -842,6 +863,62 @@ void RM_KeyAtPos(RedisModuleCtx *ctx, int pos) { ...@@ -842,6 +863,62 @@ void RM_KeyAtPos(RedisModuleCtx *ctx, int pos) {
RM_KeyAtPosWithFlags(ctx, pos, flags); RM_KeyAtPosWithFlags(ctx, pos, flags);
} }
/* Return non-zero if a module command, that was declared with the
* flag "getchannels-api", is called in a special way to get the channel positions
* and not to get executed. Otherwise zero is returned. */
int RM_IsChannelsPositionRequest(RedisModuleCtx *ctx) {
return (ctx->flags & REDISMODULE_CTX_CHANNELS_POS_REQUEST) != 0;
}
/* When a module command is called in order to obtain the position of
* channels, since it was flagged as "getchannels-api" during the
* registration, the command implementation checks for this special call
* using the RedisModule_IsChannelsPositionRequest() API and uses this
* function in order to report the channels.
*
* The supported flags are:
* * REDISMODULE_CMD_CHANNEL_SUBSCRIBE: This command will subscribe to the channel.
* * REDISMODULE_CMD_CHANNEL_UNSUBSCRIBE: This command will unsubscribe from this channel.
* * REDISMODULE_CMD_CHANNEL_PUBLISH: This command will publish to this channel.
* * REDISMODULE_CMD_CHANNEL_PATTERN: Instead of acting on a specific channel, will act on any
* channel specified by the pattern. This is the same access
* used by the PSUBSCRIBE and PUNSUBSCRIBE commands available
* in Redis. Not intended to be used with PUBLISH permissions.
*
* The following is an example of how it could be used:
*
* if (RedisModule_IsChannelsPositionRequest(ctx)) {
* RedisModule_ChannelAtPosWithFlags(ctx, 1, REDISMODULE_CMD_CHANNEL_SUBSCRIBE | REDISMODULE_CMD_CHANNEL_PATTERN);
* RedisModule_ChannelAtPosWithFlags(ctx, 1, REDISMODULE_CMD_CHANNEL_PUBLISH);
* }
*
* Note: One usage of declaring channels is for evaluating ACL permissions. In this context,
* unsubscribing is always allowed, so commands will only be checked against subscribe and
* publish permissions. This is preferred over using RM_ACLCheckChannelPermissions, since
* it allows the ACLs to be checked before the command is executed. */
void RM_ChannelAtPosWithFlags(RedisModuleCtx *ctx, int pos, int flags) {
if (!(ctx->flags & REDISMODULE_CTX_CHANNELS_POS_REQUEST) || !ctx->keys_result) return;
if (pos <= 0) return;
getKeysResult *res = ctx->keys_result;
/* Check overflow */
if (res->numkeys == res->size) {
int newsize = res->size + (res->size > 8192 ? 8192 : res->size);
getKeysPrepareResult(res, newsize);
}
int new_flags = 0;
if (flags & REDISMODULE_CMD_CHANNEL_SUBSCRIBE) new_flags |= CMD_CHANNEL_SUBSCRIBE;
if (flags & REDISMODULE_CMD_CHANNEL_UNSUBSCRIBE) new_flags |= CMD_CHANNEL_UNSUBSCRIBE;
if (flags & REDISMODULE_CMD_CHANNEL_PUBLISH) new_flags |= CMD_CHANNEL_PUBLISH;
if (flags & REDISMODULE_CMD_CHANNEL_PATTERN) new_flags |= CMD_CHANNEL_PATTERN;
res->keys[res->numkeys].pos = pos;
res->keys[res->numkeys].flags = new_flags;
res->numkeys++;
}
/* Helper for RM_CreateCommand(). Turns a string representing command /* Helper for RM_CreateCommand(). Turns a string representing command
* flags into the command flags used by the Redis core. * flags into the command flags used by the Redis core.
* *
...@@ -868,6 +945,7 @@ int64_t commandFlagsFromString(char *s) { ...@@ -868,6 +945,7 @@ int64_t commandFlagsFromString(char *s) {
else if (!strcasecmp(t,"no-auth")) flags |= CMD_NO_AUTH; else if (!strcasecmp(t,"no-auth")) flags |= CMD_NO_AUTH;
else if (!strcasecmp(t,"may-replicate")) flags |= CMD_MAY_REPLICATE; else if (!strcasecmp(t,"may-replicate")) flags |= CMD_MAY_REPLICATE;
else if (!strcasecmp(t,"getkeys-api")) flags |= CMD_MODULE_GETKEYS; else if (!strcasecmp(t,"getkeys-api")) flags |= CMD_MODULE_GETKEYS;
else if (!strcasecmp(t,"getchannels-api")) flags |= CMD_MODULE_GETCHANNELS;
else if (!strcasecmp(t,"no-cluster")) flags |= CMD_MODULE_NO_CLUSTER; else if (!strcasecmp(t,"no-cluster")) flags |= CMD_MODULE_NO_CLUSTER;
else if (!strcasecmp(t,"no-mandatory-keys")) flags |= CMD_NO_MANDATORY_KEYS; else if (!strcasecmp(t,"no-mandatory-keys")) flags |= CMD_NO_MANDATORY_KEYS;
else if (!strcasecmp(t,"allow-busy")) flags |= CMD_ALLOW_BUSY; else if (!strcasecmp(t,"allow-busy")) flags |= CMD_ALLOW_BUSY;
...@@ -947,6 +1025,8 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec ...@@ -947,6 +1025,8 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec
* * **"allow-busy"**: Permit the command while the server is blocked either by * * **"allow-busy"**: Permit the command while the server is blocked either by
* a script or by a slow module command, see * a script or by a slow module command, see
* RM_Yield. * RM_Yield.
* * **"getchannels-api"**: The command implements the interface to return
* the arguments that are channels.
* *
* The last three parameters specify which arguments of the new command are * The last three parameters specify which arguments of the new command are
* Redis keys. See https://redis.io/commands/command for more information. * Redis keys. See https://redis.io/commands/command for more information.
...@@ -1359,9 +1439,8 @@ moduleCmdArgAt(const RedisModuleCommandInfoVersion *version, ...@@ -1359,9 +1439,8 @@ moduleCmdArgAt(const RedisModuleCommandInfoVersion *version,
* *
* Other flags: * Other flags:
* *
* * `REDISMODULE_CMD_KEY_CHANNEL`: The key is not actually a key, but a * * `REDISMODULE_CMD_KEY_NOT_KEY`: The key is not actually a key, but
* shard channel as used by sharded pubsub commands like `SSUBSCRIBE` and * should be routed in cluster mode as if it was a key.
* `SPUBLISH` commands.
* *
* * `REDISMODULE_CMD_KEY_INCOMPLETE`: The keyspec might not point out all * * `REDISMODULE_CMD_KEY_INCOMPLETE`: The keyspec might not point out all
* the keys it should cover. * the keys it should cover.
...@@ -1703,7 +1782,7 @@ static int64_t moduleConvertKeySpecsFlags(int64_t flags, int from_api) { ...@@ -1703,7 +1782,7 @@ static int64_t moduleConvertKeySpecsFlags(int64_t flags, int from_api) {
{REDISMODULE_CMD_KEY_INSERT, CMD_KEY_INSERT}, {REDISMODULE_CMD_KEY_INSERT, CMD_KEY_INSERT},
{REDISMODULE_CMD_KEY_UPDATE, CMD_KEY_UPDATE}, {REDISMODULE_CMD_KEY_UPDATE, CMD_KEY_UPDATE},
{REDISMODULE_CMD_KEY_DELETE, CMD_KEY_DELETE}, {REDISMODULE_CMD_KEY_DELETE, CMD_KEY_DELETE},
{REDISMODULE_CMD_KEY_CHANNEL, CMD_KEY_CHANNEL}, {REDISMODULE_CMD_KEY_NOT_KEY, CMD_KEY_NOT_KEY},
{REDISMODULE_CMD_KEY_INCOMPLETE, CMD_KEY_INCOMPLETE}, {REDISMODULE_CMD_KEY_INCOMPLETE, CMD_KEY_INCOMPLETE},
{REDISMODULE_CMD_KEY_VARIABLE_FLAGS, CMD_KEY_VARIABLE_FLAGS}, {REDISMODULE_CMD_KEY_VARIABLE_FLAGS, CMD_KEY_VARIABLE_FLAGS},
{0,0}}; {0,0}};
...@@ -8358,28 +8437,34 @@ int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **arg ...@@ -8358,28 +8437,34 @@ 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 attached to the user
* and the flags used. The supported flags are: * and the flags representing the key access. The flags are the same that are used in the
* * keyspec for logical operations. These flags are documented in RedisModule_SetCommandInfo as
* REDISMODULE_KEY_PERMISSION_READ: Can the module read data from the key. * the REDISMODULE_CMD_KEY_ACCESS, REDISMODULE_CMD_KEY_UPDATE, REDISMODULE_CMD_KEY_INSERT,
* REDISMODULE_KEY_PERMISSION_WRITE: Can the module write data to the key. * and REDISMODULE_CMD_KEY_DELETE flags.
*
* If no flags are supplied, the user is still required to have some access to the key for
* this command to return successfully.
* *
* On success a REDISMODULE_OK is returned, otherwise * If the user is able to access the key then REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned and errno is set to the following values: * REDISMODULE_ERR is returned and errno is set to one of the following values:
* *
* * EINVAL: The provided flags are invalid. * * EINVAL: The provided flags are invalid.
* * EACCESS: The user does not have permission to access the key. * * EACCESS: The user does not have permission to access the key.
*/ */
int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key, int flags) { int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key, int flags) {
int acl_flags = 0; const int allow_mask = (REDISMODULE_CMD_KEY_ACCESS
if (flags & REDISMODULE_KEY_PERMISSION_READ) acl_flags |= ACL_READ_PERMISSION; | REDISMODULE_CMD_KEY_INSERT
if (flags & REDISMODULE_KEY_PERMISSION_WRITE) acl_flags |= ACL_WRITE_PERMISSION; | REDISMODULE_CMD_KEY_DELETE
if (!acl_flags || ((flags & REDISMODULE_KEY_PERMISSION_ALL) != flags)) { | REDISMODULE_CMD_KEY_UPDATE);
if ((flags & allow_mask) != flags) {
errno = EINVAL; errno = EINVAL;
return REDISMODULE_ERR; return REDISMODULE_ERR;
} }
if (ACLUserCheckKeyPerm(user->user, key->ptr, sdslen(key->ptr), acl_flags) != ACL_OK) { int keyspec_flags = moduleConvertKeySpecsFlags(flags, 0);
if (ACLUserCheckKeyPerm(user->user, key->ptr, sdslen(key->ptr), keyspec_flags) != ACL_OK) {
errno = EACCES; errno = EACCES;
return REDISMODULE_ERR; return REDISMODULE_ERR;
} }
...@@ -8387,14 +8472,34 @@ int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key, int ...@@ -8387,14 +8472,34 @@ int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key, int
return REDISMODULE_OK; return REDISMODULE_OK;
} }
/* Check if the pubsub channel can be accessed by the user, according to the ACLs associated with it. /* Check if the pubsub channel can be accessed by the user based off of the given
* Glob-style pattern matching is employed, unless the literal flag is * access flags. See RM_ChannelAtPosWithFlags for more information about the
* set. * possible flags that can be passed in.
* *
* If the user can access the pubsub channel, REDISMODULE_OK is returned, otherwise * If the user is able to acecss the pubsub channel then REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned. */ * REDISMODULE_ERR is returned and errno is set to one of the following values:
int RM_ACLCheckChannelPermissions(RedisModuleUser *user, RedisModuleString *ch, int literal) { *
if (ACLUserCheckChannelPerm(user->user, ch->ptr, literal) != ACL_OK) * * EINVAL: The provided flags are invalid.
* * EACCESS: The user does not have permission to access the pubsub channel.
*/
int RM_ACLCheckChannelPermissions(RedisModuleUser *user, RedisModuleString *ch, int flags) {
const int allow_mask = (REDISMODULE_CMD_CHANNEL_PUBLISH
| REDISMODULE_CMD_CHANNEL_SUBSCRIBE
| REDISMODULE_CMD_CHANNEL_UNSUBSCRIBE
| REDISMODULE_CMD_CHANNEL_PATTERN);
if ((flags & allow_mask) != flags) {
errno = EINVAL;
return REDISMODULE_ERR;
}
/* Unsubscribe permissions are currently always allowed. */
if (flags & REDISMODULE_CMD_CHANNEL_UNSUBSCRIBE){
return REDISMODULE_OK;
}
int is_pattern = flags & REDISMODULE_CMD_CHANNEL_PATTERN;
if (ACLUserCheckChannelPerm(user->user, ch->ptr, is_pattern) != ACL_OK)
return REDISMODULE_ERR; return REDISMODULE_ERR;
return REDISMODULE_OK; return REDISMODULE_OK;
...@@ -11502,6 +11607,8 @@ void moduleRegisterCoreAPI(void) { ...@@ -11502,6 +11607,8 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(IsKeysPositionRequest); REGISTER_API(IsKeysPositionRequest);
REGISTER_API(KeyAtPos); REGISTER_API(KeyAtPos);
REGISTER_API(KeyAtPosWithFlags); REGISTER_API(KeyAtPosWithFlags);
REGISTER_API(IsChannelsPositionRequest);
REGISTER_API(ChannelAtPosWithFlags);
REGISTER_API(GetClientId); REGISTER_API(GetClientId);
REGISTER_API(GetClientUserNameById); REGISTER_API(GetClientUserNameById);
REGISTER_API(GetContextFlags); REGISTER_API(GetContextFlags);
......
...@@ -292,10 +292,17 @@ typedef enum { ...@@ -292,10 +292,17 @@ typedef enum {
#define REDISMODULE_CMD_KEY_UPDATE (1ULL<<5) #define REDISMODULE_CMD_KEY_UPDATE (1ULL<<5)
#define REDISMODULE_CMD_KEY_INSERT (1ULL<<6) #define REDISMODULE_CMD_KEY_INSERT (1ULL<<6)
#define REDISMODULE_CMD_KEY_DELETE (1ULL<<7) #define REDISMODULE_CMD_KEY_DELETE (1ULL<<7)
#define REDISMODULE_CMD_KEY_CHANNEL (1ULL<<8) #define REDISMODULE_CMD_KEY_NOT_KEY (1ULL<<8)
#define REDISMODULE_CMD_KEY_INCOMPLETE (1ULL<<9) #define REDISMODULE_CMD_KEY_INCOMPLETE (1ULL<<9)
#define REDISMODULE_CMD_KEY_VARIABLE_FLAGS (1ULL<<10) #define REDISMODULE_CMD_KEY_VARIABLE_FLAGS (1ULL<<10)
/* Channel flags, for details see the documentation of
* RedisModule_ChannelAtPosWithFlags. */
#define REDISMODULE_CMD_CHANNEL_PATTERN (1ULL<<0)
#define REDISMODULE_CMD_CHANNEL_PUBLISH (1ULL<<1)
#define REDISMODULE_CMD_CHANNEL_SUBSCRIBE (1ULL<<2)
#define REDISMODULE_CMD_CHANNEL_UNSUBSCRIBE (1ULL<<3)
typedef struct RedisModuleCommandArg { typedef struct RedisModuleCommandArg {
const char *name; const char *name;
RedisModuleCommandArgType type; RedisModuleCommandArgType type;
...@@ -396,12 +403,6 @@ typedef struct { ...@@ -396,12 +403,6 @@ typedef struct {
RedisModuleCommandArg *args; RedisModuleCommandArg *args;
} RedisModuleCommandInfo; } RedisModuleCommandInfo;
/* 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
...@@ -946,6 +947,8 @@ REDISMODULE_API long long (*RedisModule_StreamTrimByID)(RedisModuleKey *key, int ...@@ -946,6 +947,8 @@ REDISMODULE_API long long (*RedisModule_StreamTrimByID)(RedisModuleKey *key, int
REDISMODULE_API int (*RedisModule_IsKeysPositionRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_IsKeysPositionRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_KeyAtPos)(RedisModuleCtx *ctx, int pos) REDISMODULE_ATTR; REDISMODULE_API void (*RedisModule_KeyAtPos)(RedisModuleCtx *ctx, int pos) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_KeyAtPosWithFlags)(RedisModuleCtx *ctx, int pos, int flags) REDISMODULE_ATTR; REDISMODULE_API void (*RedisModule_KeyAtPosWithFlags)(RedisModuleCtx *ctx, int pos, int flags) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_IsChannelsPositionRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_ChannelAtPosWithFlags)(RedisModuleCtx *ctx, int pos, int flags) REDISMODULE_ATTR;
REDISMODULE_API unsigned long long (*RedisModule_GetClientId)(RedisModuleCtx *ctx) REDISMODULE_ATTR; REDISMODULE_API unsigned long long (*RedisModule_GetClientId)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleString * (*RedisModule_GetClientUserNameById)(RedisModuleCtx *ctx, uint64_t id) REDISMODULE_ATTR; REDISMODULE_API RedisModuleString * (*RedisModule_GetClientUserNameById)(RedisModuleCtx *ctx, uint64_t id) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_GetClientInfoById)(void *ci, uint64_t id) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_GetClientInfoById)(void *ci, uint64_t id) REDISMODULE_ATTR;
...@@ -1267,6 +1270,8 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int ...@@ -1267,6 +1270,8 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(IsKeysPositionRequest); REDISMODULE_GET_API(IsKeysPositionRequest);
REDISMODULE_GET_API(KeyAtPos); REDISMODULE_GET_API(KeyAtPos);
REDISMODULE_GET_API(KeyAtPosWithFlags); REDISMODULE_GET_API(KeyAtPosWithFlags);
REDISMODULE_GET_API(IsChannelsPositionRequest);
REDISMODULE_GET_API(ChannelAtPosWithFlags);
REDISMODULE_GET_API(GetClientId); REDISMODULE_GET_API(GetClientId);
REDISMODULE_GET_API(GetClientUserNameById); REDISMODULE_GET_API(GetClientUserNameById);
REDISMODULE_GET_API(GetContextFlags); REDISMODULE_GET_API(GetContextFlags);
......
...@@ -4182,7 +4182,7 @@ void addReplyFlagsForKeyArgs(client *c, uint64_t flags) { ...@@ -4182,7 +4182,7 @@ void addReplyFlagsForKeyArgs(client *c, uint64_t flags) {
{CMD_KEY_UPDATE, "update"}, {CMD_KEY_UPDATE, "update"},
{CMD_KEY_INSERT, "insert"}, {CMD_KEY_INSERT, "insert"},
{CMD_KEY_DELETE, "delete"}, {CMD_KEY_DELETE, "delete"},
{CMD_KEY_CHANNEL, "channel"}, {CMD_KEY_NOT_KEY, "not_key"},
{CMD_KEY_INCOMPLETE, "incomplete"}, {CMD_KEY_INCOMPLETE, "incomplete"},
{CMD_KEY_VARIABLE_FLAGS, "variable_flags"}, {CMD_KEY_VARIABLE_FLAGS, "variable_flags"},
{0,NULL} {0,NULL}
......
...@@ -214,6 +214,7 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT]; ...@@ -214,6 +214,7 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
#define CMD_NO_MULTI (1ULL<<24) #define CMD_NO_MULTI (1ULL<<24)
#define CMD_MOVABLE_KEYS (1ULL<<25) /* populated by populateCommandMovableKeys */ #define CMD_MOVABLE_KEYS (1ULL<<25) /* populated by populateCommandMovableKeys */
#define CMD_ALLOW_BUSY ((1ULL<<26)) #define CMD_ALLOW_BUSY ((1ULL<<26))
#define CMD_MODULE_GETCHANNELS (1ULL<<27) /* Use the modules getchannels interface. */
/* Command flags that describe ACLs categories. */ /* Command flags that describe ACLs categories. */
#define ACL_CATEGORY_KEYSPACE (1ULL<<0) #define ACL_CATEGORY_KEYSPACE (1ULL<<0)
...@@ -266,7 +267,9 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT]; ...@@ -266,7 +267,9 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
#define CMD_KEY_DELETE (1ULL<<7) /* Explicitly deletes some content #define CMD_KEY_DELETE (1ULL<<7) /* Explicitly deletes some content
* from the value of the key. */ * from the value of the key. */
/* Other flags: */ /* Other flags: */
#define CMD_KEY_CHANNEL (1ULL<<8) /* PUBSUB shard channel */ #define CMD_KEY_NOT_KEY (1ULL<<8) /* A 'fake' key that should be routed
* like a key in cluster mode but is
* excluded from other key checks. */
#define CMD_KEY_INCOMPLETE (1ULL<<9) /* Means that the keyspec might not point #define CMD_KEY_INCOMPLETE (1ULL<<9) /* Means that the keyspec might not point
* out to all keys it should cover */ * out to all keys it should cover */
#define CMD_KEY_VARIABLE_FLAGS (1ULL<<10) /* Means that some keys might have #define CMD_KEY_VARIABLE_FLAGS (1ULL<<10) /* Means that some keys might have
...@@ -275,6 +278,12 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT]; ...@@ -275,6 +278,12 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
/* Key flags for when access type is unknown */ /* Key flags for when access type is unknown */
#define CMD_KEY_FULL_ACCESS (CMD_KEY_RW | CMD_KEY_ACCESS | CMD_KEY_UPDATE) #define CMD_KEY_FULL_ACCESS (CMD_KEY_RW | CMD_KEY_ACCESS | CMD_KEY_UPDATE)
/* Channel flags share the same flag space as the key flags */
#define CMD_CHANNEL_PATTERN (1ULL<<11) /* The argument is a channel pattern */
#define CMD_CHANNEL_SUBSCRIBE (1ULL<<12) /* The command subscribes to channels */
#define CMD_CHANNEL_UNSUBSCRIBE (1ULL<<13) /* The command unsubscribes to channels */
#define CMD_CHANNEL_PUBLISH (1ULL<<14) /* The command publishes to channels. */
/* AOF states */ /* AOF states */
#define AOF_OFF 0 /* AOF is off */ #define AOF_OFF 0 /* AOF is off */
#define AOF_ON 1 /* AOF is on */ #define AOF_ON 1 /* AOF is on */
...@@ -1906,7 +1915,8 @@ typedef struct { ...@@ -1906,7 +1915,8 @@ typedef struct {
} keyReference; } 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. This functionality is also re-used
* for returning channel information.
*/ */
typedef struct { typedef struct {
keyReference keysbuf[MAX_KEYS_BUFFER]; /* Pre-allocated buffer, to save heap allocations */ keyReference keysbuf[MAX_KEYS_BUFFER]; /* Pre-allocated buffer, to save heap allocations */
...@@ -2326,6 +2336,7 @@ void modulesCron(void); ...@@ -2326,6 +2336,7 @@ void modulesCron(void);
int moduleLoad(const char *path, void **argv, int argc); int moduleLoad(const char *path, void **argv, int argc);
void moduleLoadFromQueue(void); void moduleLoadFromQueue(void);
int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int moduleGetCommandChannelsViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
moduleType *moduleTypeLookupModuleByID(uint64_t id); moduleType *moduleTypeLookupModuleByID(uint64_t id);
void moduleTypeNameByID(char *name, uint64_t moduleid); void moduleTypeNameByID(char *name, uint64_t moduleid);
const char *moduleTypeModuleName(moduleType *mt); const char *moduleTypeModuleName(moduleType *mt);
...@@ -3011,13 +3022,15 @@ void freeReplicationBacklogRefMemAsync(list *blocks, rax *index); ...@@ -3011,13 +3022,15 @@ void freeReplicationBacklogRefMemAsync(list *blocks, rax *index);
/* API to get key arguments from commands */ /* API to get key arguments from commands */
#define GET_KEYSPEC_DEFAULT 0 #define GET_KEYSPEC_DEFAULT 0
#define GET_KEYSPEC_INCLUDE_CHANNELS (1<<0) /* Consider channels as keys */ #define GET_KEYSPEC_INCLUDE_NOT_KEYS (1<<0) /* Consider 'fake' keys as keys */
#define GET_KEYSPEC_RETURN_PARTIAL (1<<1) /* Return all keys that can be found */ #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); int getKeysFromCommandWithSpecs(struct redisCommand *cmd, robj **argv, int argc, int search_flags, getKeysResult *result);
keyReference *getKeysPrepareResult(getKeysResult *result, int numkeys); 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 doesCommandHaveKeys(struct redisCommand *cmd); int doesCommandHaveKeys(struct redisCommand *cmd);
int getChannelsFromCommand(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int doesCommandHaveChannelsWithFlags(struct redisCommand *cmd, int flags);
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);
......
...@@ -41,6 +41,7 @@ TEST_MODULES = \ ...@@ -41,6 +41,7 @@ TEST_MODULES = \
keyspace_events.so \ keyspace_events.so \
blockedclient.so \ blockedclient.so \
getkeys.so \ getkeys.so \
getchannels.so \
test_lazyfree.so \ test_lazyfree.so \
timer.so \ timer.so \
defragtest.so \ defragtest.so \
......
...@@ -15,11 +15,13 @@ int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { ...@@ -15,11 +15,13 @@ int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
const char *flags = RedisModule_StringPtrLen(argv[1], NULL); const char *flags = RedisModule_StringPtrLen(argv[1], NULL);
if (!strcasecmp(flags, "W")) { if (!strcasecmp(flags, "W")) {
permissions = REDISMODULE_KEY_PERMISSION_WRITE; permissions = REDISMODULE_CMD_KEY_UPDATE;
} else if (!strcasecmp(flags, "R")) { } else if (!strcasecmp(flags, "R")) {
permissions = REDISMODULE_KEY_PERMISSION_READ; permissions = REDISMODULE_CMD_KEY_ACCESS;
} else if (!strcasecmp(flags, "*")) { } else if (!strcasecmp(flags, "*")) {
permissions = REDISMODULE_KEY_PERMISSION_ALL; permissions = REDISMODULE_CMD_KEY_UPDATE | REDISMODULE_CMD_KEY_ACCESS;
} else if (!strcasecmp(flags, "~")) {
permissions = 0; /* Requires either read or write */
} else { } else {
RedisModule_ReplyWithError(ctx, "INVALID FLAGS"); RedisModule_ReplyWithError(ctx, "INVALID FLAGS");
return REDISMODULE_OK; return REDISMODULE_OK;
...@@ -58,7 +60,7 @@ int publish_aclcheck_channel(RedisModuleCtx *ctx, RedisModuleString **argv, int ...@@ -58,7 +60,7 @@ int publish_aclcheck_channel(RedisModuleCtx *ctx, RedisModuleString **argv, int
/* Check that the pubsub channel can be accessed */ /* Check that the pubsub channel 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_ACLCheckChannelPermissions(user, argv[1], 1); int ret = RedisModule_ACLCheckChannelPermissions(user, argv[1], REDISMODULE_CMD_CHANNEL_SUBSCRIBE);
if (ret != 0) { if (ret != 0) {
RedisModule_ReplyWithError(ctx, "DENIED CHANNEL"); RedisModule_ReplyWithError(ctx, "DENIED CHANNEL");
RedisModule_FreeModuleUser(user); RedisModule_FreeModuleUser(user);
......
#include "redismodule.h"
#include <strings.h>
#include <assert.h>
#include <unistd.h>
#include <errno.h>
/* A sample with declarable channels, that are used to validate against ACLs */
int getChannels_subscribe(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if ((argc - 1) % 3 != 0) {
RedisModule_WrongArity(ctx);
return REDISMODULE_OK;
}
char *err = NULL;
/* getchannels.command [[subscribe|unsubscribe|publish] [pattern|literal] <channel> ...]
* This command marks the given channel is accessed based on the
* provided modifiers. */
for (int i = 1; i < argc; i += 3) {
const char *operation = RedisModule_StringPtrLen(argv[i], NULL);
const char *type = RedisModule_StringPtrLen(argv[i+1], NULL);
int flags = 0;
if (!strcasecmp(operation, "subscribe")) {
flags |= REDISMODULE_CMD_CHANNEL_SUBSCRIBE;
} else if (!strcasecmp(operation, "unsubscribe")) {
flags |= REDISMODULE_CMD_CHANNEL_UNSUBSCRIBE;
} else if (!strcasecmp(operation, "publish")) {
flags |= REDISMODULE_CMD_CHANNEL_PUBLISH;
} else {
err = "Invalid channel operation";
break;
}
if (!strcasecmp(type, "literal")) {
/* No op */
} else if (!strcasecmp(type, "pattern")) {
flags |= REDISMODULE_CMD_CHANNEL_PATTERN;
} else {
err = "Invalid channel type";
break;
}
if (RedisModule_IsChannelsPositionRequest(ctx)) {
RedisModule_ChannelAtPosWithFlags(ctx, i+2, flags);
}
}
if (!RedisModule_IsChannelsPositionRequest(ctx)) {
if (err) {
RedisModule_ReplyWithError(ctx, err);
} else {
/* Normal implementation would go here, but for tests just return okay */
RedisModule_ReplyWithSimpleString(ctx, "OK");
}
}
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
if (RedisModule_Init(ctx, "getchannels", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx, "getchannels.command", getChannels_subscribe, "getchannels-api", 0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
...@@ -26,6 +26,12 @@ start_server {tags {"modules acl"}} { ...@@ -26,6 +26,12 @@ start_server {tags {"modules acl"}} {
catch {r aclcheck.set.check.key "*" v 5} e catch {r aclcheck.set.check.key "*" v 5} e
assert_match "*DENIED KEY*" $e assert_match "*DENIED KEY*" $e
assert_equal [r aclcheck.set.check.key "~" x 5] OK
assert_equal [r aclcheck.set.check.key "~" y 5] OK
assert_equal [r aclcheck.set.check.key "~" z 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 assert_equal [r aclcheck.set.check.key "W" y 5] OK
catch {r aclcheck.set.check.key "W" v 5} e catch {r aclcheck.set.check.key "W" v 5} e
assert_match "*DENIED KEY*" $e assert_match "*DENIED KEY*" $e
......
set testmodule [file normalize tests/modules/getchannels.so]
start_server {tags {"modules"}} {
r module load $testmodule
# Channels are currently used to just validate ACLs, so test them here
r ACL setuser testuser +@all resetchannels &channel &pattern*
test "module getchannels-api with literals - ACL" {
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command subscribe literal channel subscribe literal pattern1]
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command publish literal channel publish literal pattern1]
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command unsubscribe literal channel unsubscribe literal pattern1]
assert_equal "This user has no permissions to access the 'nopattern1' channel" [r ACL DRYRUN testuser getchannels.command subscribe literal channel subscribe literal nopattern1]
assert_equal "This user has no permissions to access the 'nopattern1' channel" [r ACL DRYRUN testuser getchannels.command publish literal channel subscribe literal nopattern1]
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command unsubscribe literal channel unsubscribe literal nopattern1]
assert_equal "This user has no permissions to access the 'otherchannel' channel" [r ACL DRYRUN testuser getchannels.command subscribe literal otherchannel subscribe literal pattern1]
assert_equal "This user has no permissions to access the 'otherchannel' channel" [r ACL DRYRUN testuser getchannels.command publish literal otherchannel subscribe literal pattern1]
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command unsubscribe literal otherchannel unsubscribe literal pattern1]
}
test "module getchannels-api with patterns - ACL" {
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command subscribe pattern pattern*]
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command publish pattern pattern*]
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command unsubscribe pattern pattern*]
assert_equal "This user has no permissions to access the 'pattern1' channel" [r ACL DRYRUN testuser getchannels.command subscribe pattern pattern1 subscribe pattern pattern*]
assert_equal "This user has no permissions to access the 'pattern1' channel" [r ACL DRYRUN testuser getchannels.command publish pattern pattern1 subscribe pattern pattern*]
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command unsubscribe pattern pattern1 unsubscribe pattern pattern*]
assert_equal "This user has no permissions to access the 'otherpattern*' channel" [r ACL DRYRUN testuser getchannels.command subscribe pattern otherpattern* subscribe pattern pattern*]
assert_equal "This user has no permissions to access the 'otherpattern*' channel" [r ACL DRYRUN testuser getchannels.command publish pattern otherpattern* subscribe pattern pattern*]
assert_equal "OK" [r ACL DRYRUN testuser getchannels.command unsubscribe pattern otherpattern* unsubscribe pattern pattern*]
}
test "Unload the module - getchannels" {
assert_equal {OK} [r module unload getchannels]
}
}
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