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.
This commit is contained in:
Madelyn Olson 2022-02-22 01:00:03 -08:00 committed by GitHub
parent c4c68f5d41
commit 71204f9632
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 404 additions and 104 deletions

View File

@ -1531,18 +1531,15 @@ static int ACLSelectorCheckKey(aclSelector *selector, const char *key, int keyle
return ACL_DENIED_KEY;
}
/* Returns if a given command may possibly access channels. For this context,
* the unsubscribe commands do not have channels. */
static int ACLDoesCommandHaveChannels(struct redisCommand *cmd) {
return (cmd->proc == publishCommand
|| cmd->proc == subscribeCommand
|| cmd->proc == psubscribeCommand
|| cmd->proc == spublishCommand
|| cmd->proc == ssubscribeCommand);
}
/* Checks a channel against a provide list of channels. */
static int ACLCheckChannelAgainstList(list *reference, const char *channel, int channellen, int literal) {
/* Checks a channel against a provided list of channels. The is_pattern
* argument should only be used when subscribing (not when publishing)
* and controls whether the input channel is evaluated as a channel pattern
* (like in PSUBSCRIBE) or a plain channel name (like in SUBSCRIBE).
*
* Note that a plain channel name like in PUBLISH or SUBSCRIBE can be
* matched against ACL channel patterns, but the pattern provided in PSUBSCRIBE
* 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) {
listIter li;
listNode *ln;
@ -1550,8 +1547,10 @@ static int ACLCheckChannelAgainstList(list *reference, const char *channel, int
while((ln = listNext(&li))) {
sds pattern = listNodeValue(ln);
size_t plen = sdslen(pattern);
if ((literal && !strcmp(pattern,channel)) ||
(!literal && stringmatchlen(pattern,plen,channel,channellen,0)))
/* Channel patterns are matched literally against the channels in
* the list. Regular channels perform pattern matching. */
if ((is_pattern && !strcmp(pattern,channel)) ||
(!is_pattern && stringmatchlen(pattern,plen,channel,channellen,0)))
{
return ACL_OK;
}
@ -1559,28 +1558,6 @@ static int ACLCheckChannelAgainstList(list *reference, const char *channel, int
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
* in between calls to the various selectors. */
typedef struct {
@ -1645,7 +1622,7 @@ static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd,
int idx = resultidx[j].pos;
ret = ACLSelectorCheckKey(selector, argv[idx]->ptr, sdslen(argv[idx]->ptr), resultidx[j].flags);
if (ret != ACL_OK) {
if (resultidx) *keyidxptr = resultidx[j].pos;
if (keyidxptr) *keyidxptr = resultidx[j].pos;
return ret;
}
}
@ -1653,26 +1630,30 @@ static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd,
/* Check if the user can execute commands explicitly touching the channels
* mentioned in the command arguments */
if (!(selector->flags & SELECTOR_FLAG_ALLCHANNELS) && ACLDoesCommandHaveChannels(cmd)) {
if (cmd->proc == publishCommand || cmd->proc == spublishCommand) {
ret = ACLSelectorCheckPubsubArguments(selector,argv, 1, 1, 0, keyidxptr);
} else if (cmd->proc == subscribeCommand || cmd->proc == ssubscribeCommand) {
ret = ACLSelectorCheckPubsubArguments(selector, argv, 1, argc-1, 0, keyidxptr);
} else if (cmd->proc == psubscribeCommand) {
ret = ACLSelectorCheckPubsubArguments(selector, argv, 1, argc-1, 1, keyidxptr);
} else {
serverPanic("Encountered a command declared with channels but not handled");
}
if (ret != ACL_OK) {
/* keyidxptr is set by ACLSelectorCheckPubsubArguments */
return ret;
const int channel_flags = CMD_CHANNEL_PUBLISH | CMD_CHANNEL_SUBSCRIBE;
if (!(selector->flags & SELECTOR_FLAG_ALLCHANNELS) && doesCommandHaveChannelsWithFlags(cmd, channel_flags)) {
getKeysResult channels = (getKeysResult) GETKEYS_RESULT_INIT;
getChannelsFromCommand(cmd, argv, argc, &channels);
keyReference *channelref = channels.keys;
for (int j = 0; j < channels.numkeys; j++) {
int idx = channelref[j].pos;
if (!(channelref[j].flags & channel_flags)) continue;
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 (keyidxptr) *keyidxptr = channelref[j].pos;
getKeysFreeResult(&channels);
return ret;
}
}
getKeysFreeResult(&channels);
}
return ACL_OK;
}
/* 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
* ACL_DENIED_KEY is returned. */
@ -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
* ACL_DENIED_CHANNEL is returned. */
int ACLUserCheckChannelPerm(user *u, sds channel, int literal) {
int ACLUserCheckChannelPerm(user *u, sds channel, int is_pattern) {
listIter li;
listNode *ln;
@ -1714,7 +1695,7 @@ int ACLUserCheckChannelPerm(user *u, sds channel, int literal) {
if (s->flags & SELECTOR_FLAG_ALLCHANNELS) return ACL_OK;
/* 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;
}
}

View File

@ -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},
{"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},
{"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},
{"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},
{"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_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},
{"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},
/* 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},

View File

@ -26,7 +26,7 @@
"key_specs": [
{
"flags": [
"CHANNEL"
"NOT_KEY"
],
"begin_search": {
"index": {

View File

@ -22,7 +22,7 @@
"key_specs": [
{
"flags": [
"CHANNEL"
"NOT_KEY"
],
"begin_search": {
"index": {

View File

@ -23,7 +23,7 @@
"key_specs": [
{
"flags": [
"CHANNEL"
"NOT_KEY"
],
"begin_search": {
"index": {

View File

@ -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.
* 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
* found in other valid keyspecs.
*/
@ -1703,8 +1703,8 @@ int getKeysUsingKeySpecs(struct redisCommand *cmd, robj **argv, int argc, int se
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)) {
/* Skip specs that represent 'fake' keys */
if ((spec->flags & CMD_KEY_NOT_KEY) && !(search_flags & GET_KEYSPEC_INCLUDE_NOT_KEYS)) {
continue;
}
@ -1823,8 +1823,8 @@ invalid_spec:
* 'cmd' must be point to the corresponding entry into the redisCommand
* table, according to the command name in argv[0]. */
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 */
int has_keyspec = (getAllKeySpecsFlags(cmd, 1) & CMD_KEY_CHANNEL);
/* The command has at least one key-spec not marked as NOT_KEY */
int has_keyspec = (getAllKeySpecsFlags(cmd, 1) & CMD_KEY_NOT_KEY);
/* The command has at least one key-spec marked as 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,
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 */
(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

View File

@ -154,7 +154,8 @@ struct RedisModuleCtx {
gets called for clients blocked
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;
struct RedisModulePoolAllocBlock *pa_head;
@ -173,6 +174,7 @@ typedef struct RedisModuleCtx RedisModuleCtx;
when the context is destroyed */
#define REDISMODULE_CTX_NEW_CLIENT (1<<7) /* Free client object when the
context is destroyed */
#define REDISMODULE_CTX_CHANNELS_POS_REQUEST (1<<8)
/* This represents a Redis key opened with RM_OpenKey(). */
struct RedisModuleKey {
@ -781,6 +783,25 @@ int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc,
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
*
@ -842,6 +863,62 @@ void RM_KeyAtPos(RedisModuleCtx *ctx, int pos) {
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
* flags into the command flags used by the Redis core.
*
@ -868,6 +945,7 @@ int64_t commandFlagsFromString(char *s) {
else if (!strcasecmp(t,"no-auth")) flags |= CMD_NO_AUTH;
else if (!strcasecmp(t,"may-replicate")) flags |= CMD_MAY_REPLICATE;
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-mandatory-keys")) flags |= CMD_NO_MANDATORY_KEYS;
else if (!strcasecmp(t,"allow-busy")) flags |= CMD_ALLOW_BUSY;
@ -947,6 +1025,8 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec
* * **"allow-busy"**: Permit the command while the server is blocked either by
* a script or by a slow module command, see
* 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
* Redis keys. See https://redis.io/commands/command for more information.
@ -1359,9 +1439,8 @@ moduleCmdArgAt(const RedisModuleCommandInfoVersion *version,
*
* Other flags:
*
* * `REDISMODULE_CMD_KEY_CHANNEL`: The key is not actually a key, but a
* shard channel as used by sharded pubsub commands like `SSUBSCRIBE` and
* `SPUBLISH` commands.
* * `REDISMODULE_CMD_KEY_NOT_KEY`: The key is not actually a key, but
* should be routed in cluster mode as if it was a key.
*
* * `REDISMODULE_CMD_KEY_INCOMPLETE`: The keyspec might not point out all
* the keys it should cover.
@ -1703,7 +1782,7 @@ static int64_t moduleConvertKeySpecsFlags(int64_t flags, int from_api) {
{REDISMODULE_CMD_KEY_INSERT, CMD_KEY_INSERT},
{REDISMODULE_CMD_KEY_UPDATE, CMD_KEY_UPDATE},
{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_VARIABLE_FLAGS, CMD_KEY_VARIABLE_FLAGS},
{0,0}};
@ -8358,28 +8437,34 @@ 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
* and the flags used. The supported flags are:
/* Check if the key can be accessed by the user according to the ACLs attached to the user
* 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
* the REDISMODULE_CMD_KEY_ACCESS, REDISMODULE_CMD_KEY_UPDATE, REDISMODULE_CMD_KEY_INSERT,
* 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.
*
* 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:
* If the user is able to access the key then REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned and errno is set to one of 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)) {
const int allow_mask = (REDISMODULE_CMD_KEY_ACCESS
| REDISMODULE_CMD_KEY_INSERT
| REDISMODULE_CMD_KEY_DELETE
| REDISMODULE_CMD_KEY_UPDATE);
if ((flags & allow_mask) != flags) {
errno = EINVAL;
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;
return REDISMODULE_ERR;
}
@ -8387,14 +8472,34 @@ int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key, int
return REDISMODULE_OK;
}
/* Check if the pubsub channel can be accessed by the user, according to the ACLs associated with it.
* Glob-style pattern matching is employed, unless the literal flag is
* set.
/* Check if the pubsub channel can be accessed by the user based off of the given
* access flags. See RM_ChannelAtPosWithFlags for more information about the
* possible flags that can be passed in.
*
* 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 (ACLUserCheckChannelPerm(user->user, ch->ptr, literal) != ACL_OK)
* If the user is able to acecss the pubsub channel then REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned and errno is set to one of the following values:
*
* * 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_OK;
@ -11502,6 +11607,8 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(IsKeysPositionRequest);
REGISTER_API(KeyAtPos);
REGISTER_API(KeyAtPosWithFlags);
REGISTER_API(IsChannelsPositionRequest);
REGISTER_API(ChannelAtPosWithFlags);
REGISTER_API(GetClientId);
REGISTER_API(GetClientUserNameById);
REGISTER_API(GetContextFlags);

View File

@ -292,10 +292,17 @@ typedef enum {
#define REDISMODULE_CMD_KEY_UPDATE (1ULL<<5)
#define REDISMODULE_CMD_KEY_INSERT (1ULL<<6)
#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_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 {
const char *name;
RedisModuleCommandArgType type;
@ -396,12 +403,6 @@ typedef struct {
RedisModuleCommandArg *args;
} 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. */
#define REDISMODULE_EVENTLOOP_READABLE 1
#define REDISMODULE_EVENTLOOP_WRITABLE 2
@ -946,6 +947,8 @@ REDISMODULE_API long long (*RedisModule_StreamTrimByID)(RedisModuleKey *key, int
REDISMODULE_API int (*RedisModule_IsKeysPositionRequest)(RedisModuleCtx *ctx) 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 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 RedisModuleString * (*RedisModule_GetClientUserNameById)(RedisModuleCtx *ctx, 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
REDISMODULE_GET_API(IsKeysPositionRequest);
REDISMODULE_GET_API(KeyAtPos);
REDISMODULE_GET_API(KeyAtPosWithFlags);
REDISMODULE_GET_API(IsChannelsPositionRequest);
REDISMODULE_GET_API(ChannelAtPosWithFlags);
REDISMODULE_GET_API(GetClientId);
REDISMODULE_GET_API(GetClientUserNameById);
REDISMODULE_GET_API(GetContextFlags);

View File

@ -4182,7 +4182,7 @@ void addReplyFlagsForKeyArgs(client *c, uint64_t flags) {
{CMD_KEY_UPDATE, "update"},
{CMD_KEY_INSERT, "insert"},
{CMD_KEY_DELETE, "delete"},
{CMD_KEY_CHANNEL, "channel"},
{CMD_KEY_NOT_KEY, "not_key"},
{CMD_KEY_INCOMPLETE, "incomplete"},
{CMD_KEY_VARIABLE_FLAGS, "variable_flags"},
{0,NULL}

View File

@ -214,6 +214,7 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
#define CMD_NO_MULTI (1ULL<<24)
#define CMD_MOVABLE_KEYS (1ULL<<25) /* populated by populateCommandMovableKeys */
#define CMD_ALLOW_BUSY ((1ULL<<26))
#define CMD_MODULE_GETCHANNELS (1ULL<<27) /* Use the modules getchannels interface. */
/* Command flags that describe ACLs categories. */
#define ACL_CATEGORY_KEYSPACE (1ULL<<0)
@ -266,7 +267,9 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
#define CMD_KEY_DELETE (1ULL<<7) /* Explicitly deletes some content
* from the value of the key. */
/* 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
* out to all keys it should cover */
#define CMD_KEY_VARIABLE_FLAGS (1ULL<<10) /* Means that some keys might have
@ -275,6 +278,12 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
/* Key flags for when access type is unknown */
#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 */
#define AOF_OFF 0 /* AOF is off */
#define AOF_ON 1 /* AOF is on */
@ -1906,7 +1915,8 @@ typedef struct {
} keyReference;
/* 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 {
keyReference keysbuf[MAX_KEYS_BUFFER]; /* Pre-allocated buffer, to save heap allocations */
@ -2326,6 +2336,7 @@ void modulesCron(void);
int moduleLoad(const char *path, void **argv, int argc);
void moduleLoadFromQueue(void);
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);
void moduleTypeNameByID(char *name, uint64_t moduleid);
const char *moduleTypeModuleName(moduleType *mt);
@ -3011,13 +3022,15 @@ void freeReplicationBacklogRefMemAsync(list *blocks, rax *index);
/* API to get key arguments from commands */
#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 */
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 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);
int sintercardGetKeys(struct redisCommand *cmd,robj **argv, int argc, getKeysResult *result);
int zunionInterDiffGetKeys(struct redisCommand *cmd,robj **argv, int argc, getKeysResult *result);

View File

@ -41,6 +41,7 @@ TEST_MODULES = \
keyspace_events.so \
blockedclient.so \
getkeys.so \
getchannels.so \
test_lazyfree.so \
timer.so \
defragtest.so \

View File

@ -15,11 +15,13 @@ int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
const char *flags = RedisModule_StringPtrLen(argv[1], NULL);
if (!strcasecmp(flags, "W")) {
permissions = REDISMODULE_KEY_PERMISSION_WRITE;
permissions = REDISMODULE_CMD_KEY_UPDATE;
} else if (!strcasecmp(flags, "R")) {
permissions = REDISMODULE_KEY_PERMISSION_READ;
permissions = REDISMODULE_CMD_KEY_ACCESS;
} 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 {
RedisModule_ReplyWithError(ctx, "INVALID FLAGS");
return REDISMODULE_OK;
@ -58,7 +60,7 @@ int publish_aclcheck_channel(RedisModuleCtx *ctx, RedisModuleString **argv, int
/* Check that the pubsub channel can be accessed */
RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
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) {
RedisModule_ReplyWithError(ctx, "DENIED CHANNEL");
RedisModule_FreeModuleUser(user);

View File

@ -0,0 +1,69 @@
#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;
}

View File

@ -26,6 +26,12 @@ start_server {tags {"modules acl"}} {
catch {r aclcheck.set.check.key "*" v 5} 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
catch {r aclcheck.set.check.key "W" v 5} e
assert_match "*DENIED KEY*" $e

View File

@ -0,0 +1,40 @@
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]
}
}