From 71204f9632591dd6525cf5325ac2c661ba3ad3f0 Mon Sep 17 00:00:00 2001 From: Madelyn Olson <34459052+madolson@users.noreply.github.com> Date: Tue, 22 Feb 2022 01:00:03 -0800 Subject: [PATCH] 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. --- src/acl.c | 87 ++++++--------- src/commands.c | 6 +- src/commands/spublish.json | 2 +- src/commands/ssubscribe.json | 2 +- src/commands/sunsubscribe.json | 2 +- src/db.c | 88 +++++++++++++-- src/module.c | 155 ++++++++++++++++++++++----- src/redismodule.h | 19 ++-- src/server.c | 2 +- src/server.h | 19 +++- tests/modules/Makefile | 1 + tests/modules/aclcheck.c | 10 +- tests/modules/getchannels.c | 69 ++++++++++++ tests/unit/moduleapi/aclcheck.tcl | 6 ++ tests/unit/moduleapi/getchannels.tcl | 40 +++++++ 15 files changed, 404 insertions(+), 104 deletions(-) create mode 100644 tests/modules/getchannels.c create mode 100644 tests/unit/moduleapi/getchannels.tcl diff --git a/src/acl.c b/src/acl.c index d6e156d5f..7399ded74 100644 --- a/src/acl.c +++ b/src/acl.c @@ -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; } } diff --git a/src/commands.c b/src/commands.c index f3eec1ee1..70b0d4c99 100644 --- a/src/commands.c +++ b/src/commands.c @@ -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}, diff --git a/src/commands/spublish.json b/src/commands/spublish.json index e6c4fb062..2cbcdc19a 100644 --- a/src/commands/spublish.json +++ b/src/commands/spublish.json @@ -26,7 +26,7 @@ "key_specs": [ { "flags": [ - "CHANNEL" + "NOT_KEY" ], "begin_search": { "index": { diff --git a/src/commands/ssubscribe.json b/src/commands/ssubscribe.json index dd1070046..eb570ea53 100644 --- a/src/commands/ssubscribe.json +++ b/src/commands/ssubscribe.json @@ -22,7 +22,7 @@ "key_specs": [ { "flags": [ - "CHANNEL" + "NOT_KEY" ], "begin_search": { "index": { diff --git a/src/commands/sunsubscribe.json b/src/commands/sunsubscribe.json index f7271825e..481415490 100644 --- a/src/commands/sunsubscribe.json +++ b/src/commands/sunsubscribe.json @@ -23,7 +23,7 @@ "key_specs": [ { "flags": [ - "CHANNEL" + "NOT_KEY" ], "begin_search": { "index": { diff --git a/src/db.c b/src/db.c index b8a6ebae3..c2477d678 100644 --- a/src/db.c +++ b/src/db.c @@ -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 diff --git a/src/module.c b/src/module.c index d5528fc9b..6e549ac7c 100644 --- a/src/module.c +++ b/src/module.c @@ -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); diff --git a/src/redismodule.h b/src/redismodule.h index aea64174e..79ce2c697 100644 --- a/src/redismodule.h +++ b/src/redismodule.h @@ -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); diff --git a/src/server.c b/src/server.c index a68097835..8362b6b92 100644 --- a/src/server.c +++ b/src/server.c @@ -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} diff --git a/src/server.h b/src/server.h index bd1fed159..75ccda1ba 100644 --- a/src/server.h +++ b/src/server.h @@ -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); diff --git a/tests/modules/Makefile b/tests/modules/Makefile index 51405235e..ce842f3af 100644 --- a/tests/modules/Makefile +++ b/tests/modules/Makefile @@ -41,6 +41,7 @@ TEST_MODULES = \ keyspace_events.so \ blockedclient.so \ getkeys.so \ + getchannels.so \ test_lazyfree.so \ timer.so \ defragtest.so \ diff --git a/tests/modules/aclcheck.c b/tests/modules/aclcheck.c index cc8d263fd..0e9c9af29 100644 --- a/tests/modules/aclcheck.c +++ b/tests/modules/aclcheck.c @@ -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); diff --git a/tests/modules/getchannels.c b/tests/modules/getchannels.c new file mode 100644 index 000000000..330531d1a --- /dev/null +++ b/tests/modules/getchannels.c @@ -0,0 +1,69 @@ +#include "redismodule.h" +#include +#include +#include +#include + +/* 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] ...] + * 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; +} diff --git a/tests/unit/moduleapi/aclcheck.tcl b/tests/unit/moduleapi/aclcheck.tcl index a6df4f7c9..953f4bf05 100644 --- a/tests/unit/moduleapi/aclcheck.tcl +++ b/tests/unit/moduleapi/aclcheck.tcl @@ -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 diff --git a/tests/unit/moduleapi/getchannels.tcl b/tests/unit/moduleapi/getchannels.tcl new file mode 100644 index 000000000..e8f557dcc --- /dev/null +++ b/tests/unit/moduleapi/getchannels.tcl @@ -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] + } +}