Skip to content

Commit

Permalink
Treat subcommands as commands (redis#9504)
Browse files Browse the repository at this point in the history
## Intro

The purpose is to allow having different flags/ACL categories for
subcommands (Example: CONFIG GET is ok-loading but CONFIG SET isn't)

We create a small command table for every command that has subcommands
and each subcommand has its own flags, etc. (same as a "regular" command)

This commit also unites the Redis and the Sentinel command tables

## Affected commands

CONFIG
Used to have "admin ok-loading ok-stale no-script"
Changes:
1. Dropped "ok-loading" in all except GET (this doesn't change behavior since
there were checks in the code doing that)

XINFO
Used to have "read-only random"
Changes:
1. Dropped "random" in all except CONSUMERS

XGROUP
Used to have "write use-memory"
Changes:
1. Dropped "use-memory" in all except CREATE and CREATECONSUMER

COMMAND
No changes.

MEMORY
Used to have "random read-only"
Changes:
1. Dropped "random" in PURGE and USAGE

ACL
Used to have "admin no-script ok-loading ok-stale"
Changes:
1. Dropped "admin" in WHOAMI, GENPASS, and CAT

LATENCY
No changes.

MODULE
No changes.

SLOWLOG
Used to have "admin random ok-loading ok-stale"
Changes:
1. Dropped "random" in RESET

OBJECT
Used to have "read-only random"
Changes:
1. Dropped "random" in ENCODING and REFCOUNT

SCRIPT
Used to have "may-replicate no-script"
Changes:
1. Dropped "may-replicate" in all except FLUSH and LOAD

CLIENT
Used to have "admin no-script random ok-loading ok-stale"
Changes:
1. Dropped "random" in all except INFO and LIST
2. Dropped "admin" in ID, TRACKING, CACHING, GETREDIR, INFO, SETNAME, GETNAME, and REPLY

STRALGO
No changes.

PUBSUB
No changes.

CLUSTER
Changes:
1. Dropped "admin in countkeysinslots, getkeysinslot, info, nodes, keyslot, myid, and slots

SENTINEL
No changes.

(note that DEBUG also fits, but we decided not to convert it since it's for
debugging and anyway undocumented)

## New sub-command
This commit adds another element to the per-command output of COMMAND,
describing the list of subcommands, if any (in the same structure as "regular" commands)
Also, it adds a new subcommand:
```
COMMAND LIST [FILTERBY (MODULE <module-name>|ACLCAT <cat>|PATTERN <pattern>)]
```
which returns a set of all commands (unless filters), but excluding subcommands.

## Module API
A new module API, RM_CreateSubcommand, was added, in order to allow
module writer to define subcommands

## ACL changes:
1. Now, that each subcommand is actually a command, each has its own ACL id.
2. The old mechanism of allowed_subcommands is redundant
(blocking/allowing a subcommand is the same as blocking/allowing a regular command),
but we had to keep it, to support the widespread usage of allowed_subcommands
to block commands with certain args, that aren't subcommands (e.g. "-select +select|0").
3. I have renamed allowed_subcommands to allowed_firstargs to emphasize the difference.
4. Because subcommands are commands in ACL too, you can now use "-" to block subcommands
(e.g. "+client -client|kill"), which wasn't possible in the past.
5. It is also possible to use the allowed_firstargs mechanism with subcommand.
For example: `+config -config|set +config|set|loglevel` will block all CONFIG SET except
for setting the log level.
6. All of the ACL changes above required some amount of refactoring.

## Misc
1. There are two approaches: Either each subcommand has its own function or all
   subcommands use the same function, determining what to do according to argv[0].
   For now, I took the former approaches only with CONFIG and COMMAND,
   while other commands use the latter approach (for smaller blamelog diff).
2. Deleted memoryGetKeys: It is no longer needed because MEMORY USAGE now uses the "range" key spec.
4. Bugfix: GETNAME was missing from CLIENT's help message.
5. Sentinel and Redis now use the same table, with the same function pointer.
   Some commands have a different implementation in Sentinel, so we redirect
   them (these are ROLE, PUBLISH, and INFO).
6. Command stats now show the stats per subcommand (e.g. instead of stats just
   for "config" you will have stats for "config|set", "config|get", etc.)
7. It is now possible to use COMMAND directly on subcommands:
   COMMAND INFO CONFIG|GET (The pipeline syntax was inspired from ACL, and
   can be used in functions lookupCommandBySds and lookupCommandByCString)
8. STRALGO is now a container command (has "help")

## Breaking changes:
1. Command stats now show the stats per subcommand (see (5) above)
  • Loading branch information
guybe7 authored Oct 20, 2021
1 parent 4962c55 commit 43e736f
Show file tree
Hide file tree
Showing 25 changed files with 1,497 additions and 409 deletions.
14 changes: 8 additions & 6 deletions redis.conf
Original file line number Diff line number Diff line change
Expand Up @@ -818,19 +818,21 @@ replica-priority 100
# will still work.
# skip-sanitize-payload RESTORE dump-payload sanitation is skipped.
# sanitize-payload RESTORE dump-payload is sanitized (default).
# +<command> Allow the execution of that command
# -<command> Disallow the execution of that command
# +<command> Allow the execution of that command.
# May be used with `|` for allowing subcommands (e.g "+config|get")
# -<command> Disallow the execution of that command.
# May be used with `|` for blocking subcommands (e.g "-config|set")
# +@<category> Allow the execution of all the commands in such category
# with valid categories are like @admin, @set, @sortedset, ...
# and so forth, see the full list in the server.c file where
# the Redis command table is described and defined.
# The special category @all means all the commands, but currently
# present in the server, and that will be loaded in the future
# via modules.
# +<command>|subcommand Allow a specific subcommand of an otherwise
# disabled command. Note that this form is not
# allowed as negative like -DEBUG|SEGFAULT, but
# only additive starting with "+".
# +<command>|first-arg Allow a specific first argument of an otherwise
# disabled command. Note that this form is not
# allowed as negative like -SELECT|1, but
# only additive starting with "+".
# allcommands Alias for +@all. Note that it implies the ability to execute
# all the future commands loaded via the modules system.
# nocommands Alias for -@all.
Expand Down
1 change: 1 addition & 0 deletions runtest-moduleapi
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/datatype2 \
--single unit/moduleapi/cluster \
--single unit/moduleapi/aclcheck \
--single unit/moduleapi/subcommands \
"${@}"
288 changes: 173 additions & 115 deletions src/acl.c

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/aof.c
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ int loadAppendOnlyFile(char *filename) {
}

/* Command lookup */
cmd = lookupCommand(argv[0]->ptr);
cmd = lookupCommand(argv,argc);
if (!cmd) {
serverLog(LL_WARNING,
"Unknown command '%s' reading the append only file",
Expand Down
71 changes: 34 additions & 37 deletions src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ void loadServerConfigFromString(char *config) {
} else if (!strcasecmp(argv[0],"list-max-ziplist-value") && argc == 2) {
/* DEAD OPTION */
} else if (!strcasecmp(argv[0],"rename-command") && argc == 3) {
struct redisCommand *cmd = lookupCommand(argv[1]);
struct redisCommand *cmd = lookupCommandBySds(argv[1]);
int retval;

if (!cmd) {
Expand Down Expand Up @@ -2735,18 +2735,11 @@ standardConfig configs[] = {
};

/*-----------------------------------------------------------------------------
* CONFIG command entry point
* CONFIG HELP
*----------------------------------------------------------------------------*/

void configCommand(client *c) {
/* Only allow CONFIG GET while loading. */
if (server.loading && strcasecmp(c->argv[1]->ptr,"get")) {
addReplyError(c,"Only CONFIG GET is allowed during loading");
return;
}

if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
const char *help[] = {
void configHelpCommand(client *c) {
const char *help[] = {
"GET <pattern>",
" Return parameters matching the glob-like <pattern> and their values.",
"SET <directive> <value>",
Expand All @@ -2756,32 +2749,36 @@ void configCommand(client *c) {
"REWRITE",
" Rewrite the configuration file.",
NULL
};

addReplyHelp(c, help);
} else if (!strcasecmp(c->argv[1]->ptr,"set") && c->argc == 4) {
configSetCommand(c);
} else if (!strcasecmp(c->argv[1]->ptr,"get") && c->argc == 3) {
configGetCommand(c);
} else if (!strcasecmp(c->argv[1]->ptr,"resetstat") && c->argc == 2) {
resetServerStats();
resetCommandTableStats();
resetErrorTableStats();
addReply(c,shared.ok);
} else if (!strcasecmp(c->argv[1]->ptr,"rewrite") && c->argc == 2) {
if (server.configfile == NULL) {
addReplyError(c,"The server is running without a config file");
return;
}
if (rewriteConfig(server.configfile, 0) == -1) {
serverLog(LL_WARNING,"CONFIG REWRITE failed: %s", strerror(errno));
addReplyErrorFormat(c,"Rewriting config file: %s", strerror(errno));
} else {
serverLog(LL_WARNING,"CONFIG REWRITE executed with success.");
addReply(c,shared.ok);
}
} else {
addReplySubcommandSyntaxError(c);
};

addReplyHelp(c, help);
}

/*-----------------------------------------------------------------------------
* CONFIG RESETSTAT
*----------------------------------------------------------------------------*/

void configResetStatCommand(client *c) {
resetServerStats();
resetCommandTableStats(server.commands);
resetErrorTableStats();
addReply(c,shared.ok);
}

/*-----------------------------------------------------------------------------
* CONFIG REWRITE
*----------------------------------------------------------------------------*/

void configRewriteCommand(client *c) {
if (server.configfile == NULL) {
addReplyError(c,"The server is running without a config file");
return;
}
if (rewriteConfig(server.configfile, 0) == -1) {
serverLog(LL_WARNING,"CONFIG REWRITE failed: %s", strerror(errno));
addReplyErrorFormat(c,"Rewriting config file: %s", strerror(errno));
} else {
serverLog(LL_WARNING,"CONFIG REWRITE executed with success.");
addReply(c,shared.ok);
}
}
15 changes: 0 additions & 15 deletions src/db.c
Original file line number Diff line number Diff line change
Expand Up @@ -1873,21 +1873,6 @@ int lcsGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *r
return result->numkeys;
}

/* Helper function to extract keys from memory command.
* MEMORY USAGE <key> */
int memoryGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
UNUSED(cmd);

getKeysPrepareResult(result, 1);
if (argc >= 3 && !strcasecmp(argv[1]->ptr,"usage")) {
result->keys[0] = 2;
result->numkeys = 1;
return result->numkeys;
}
result->numkeys = 0;
return 0;
}

/* XREAD [BLOCK <milliseconds>] [COUNT <count>] [GROUP <groupname> <ttl>]
* STREAMS key_1 key_2 ... key_N ID_1 ID_2 ... ID_N */
int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result) {
Expand Down
106 changes: 89 additions & 17 deletions src/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,8 @@ int64_t commandKeySpecsFlagsFromString(const char *s) {
return flags;
}

RedisModuleCommandProxy *moduleCreateCommandProxy(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, int64_t flags, int firstkey, int lastkey, int keystep);

/* Register a new command in the Redis server, that will be handled by
* calling the function pointer 'cmdfunc' using the RedisModule calling
* convention. The function returns REDISMODULE_ERR if the specified command
Expand Down Expand Up @@ -888,7 +890,7 @@ int64_t commandKeySpecsFlagsFromString(const char *s) {
* Normally this is used by a command that is used
* to authenticate a client.
* * **"may-replicate"**: This command may generate replication traffic, even
* though it's not a write command.
* though it's not a write command.
*
* The last three parameters specify which arguments of the new command are
* Redis keys. See https://redis.io/commands/command for more information.
Expand Down Expand Up @@ -917,16 +919,24 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
if ((flags & CMD_MODULE_NO_CLUSTER) && server.cluster_enabled)
return REDISMODULE_ERR;

/* Check if the command name is busy. */
if (lookupCommandByCString(name) != NULL)
return REDISMODULE_ERR;

RedisModuleCommandProxy *cp = moduleCreateCommandProxy(ctx, name, cmdfunc, flags, firstkey, lastkey, keystep);
cp->rediscmd->arity = cmdfunc ? -1 : -2;

dictAdd(server.commands,sdsnew(name),cp->rediscmd);
dictAdd(server.orig_commands,sdsnew(name),cp->rediscmd);
cp->rediscmd->id = ACLGetCommandID(name); /* ID used for ACL. */
return REDISMODULE_OK;
}

RedisModuleCommandProxy *moduleCreateCommandProxy(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, int64_t flags, int firstkey, int lastkey, int keystep) {
struct redisCommand *rediscmd;
RedisModuleCommandProxy *cp;
sds cmdname = sdsnew(name);

/* Check if the command name is busy. */
if (lookupCommand(cmdname) != NULL) {
sdsfree(cmdname);
return REDISMODULE_ERR;
}

/* Create a command "proxy", which is a structure that is referenced
* in the command table, so that the generic command that works as
* binding between modules and Redis, can know what function to call
Expand All @@ -940,7 +950,6 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
cp->rediscmd = zmalloc(sizeof(*rediscmd));
cp->rediscmd->name = cmdname;
cp->rediscmd->proc = RedisModuleCommandDispatcher;
cp->rediscmd->arity = -1;
cp->rediscmd->flags = flags | CMD_MODULE;
cp->rediscmd->getkeys_proc = (redisGetKeysProc*)(unsigned long)cp;
cp->rediscmd->key_specs_max = STATIC_KEY_SPECS_NUM;
Expand All @@ -967,12 +976,70 @@ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc c
cp->rediscmd->calls = 0;
cp->rediscmd->rejected_calls = 0;
cp->rediscmd->failed_calls = 0;
dictAdd(server.commands,sdsdup(cmdname),cp->rediscmd);
dictAdd(server.orig_commands,sdsdup(cmdname),cp->rediscmd);
cp->rediscmd->id = ACLGetCommandID(cmdname); /* ID used for ACL. */
return cp;
}

/* Very similar to RedisModule_CreateCommand except that it is used to create
* a subcommand, associated with another, container, command.
*
* Example: If a module has a configuration command, MODULE.CONFIG, then
* GET and SET should be individual subcommands, while MODULE.CONFIG is
* a command, but should not be registered with a valid `funcptr`:
*
* if (RedisModule_CreateCommand(ctx,"module.config",NULL,"",0,0,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
* if (RedisModule_CreateSubcommand(ctx,"container.config","set",cmd_config_set,"",0,0,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
* if (RedisModule_CreateSubcommand(ctx,"container.config","get",cmd_config_get,"",0,0,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
*/
int RM_CreateSubcommand(RedisModuleCtx *ctx, const char *parent_name, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) {
int64_t flags = strflags ? commandFlagsFromString((char*)strflags) : 0;
if (flags == -1) return REDISMODULE_ERR;
if ((flags & CMD_MODULE_NO_CLUSTER) && server.cluster_enabled)
return REDISMODULE_ERR;

struct redisCommand *parent_cmd = lookupCommandByCString(parent_name);

if (!parent_cmd || !(parent_cmd->flags & CMD_MODULE))
return REDISMODULE_ERR;

if (parent_cmd->parent)
return REDISMODULE_ERR; /* We don't allow more than one level of subcommands */

RedisModuleCommandProxy *parent_cp = (void*)(unsigned long)parent_cmd->getkeys_proc;
if (parent_cp->module != ctx->module)
return REDISMODULE_ERR;

/* Check if the command name is busy within the parent command. */
if (parent_cmd->subcommands_dict && lookupCommandByCStringLogic(parent_cmd->subcommands_dict, name) != NULL)
return REDISMODULE_ERR;

RedisModuleCommandProxy *cp = moduleCreateCommandProxy(ctx, name, cmdfunc, flags, firstkey, lastkey, keystep);
cp->rediscmd->arity = -2;

commandAddSubcommand(parent_cmd, cp->rediscmd);
return REDISMODULE_OK;
}

/* Return `struct RedisModule *` as `void *` to avoid exposing it outside of module.c. */
void *moduleGetHandleByName(char *modulename) {
return dictFetchValue(modules,modulename);
}

/* Returns 1 if `cmd` is a command of the module `modulename`. 0 otherwise. */
int moduleIsModuleCommand(void *module_handle, struct redisCommand *cmd) {
if (cmd->proc != RedisModuleCommandDispatcher)
return 0;
if (module_handle == NULL)
return 0;
RedisModuleCommandProxy *cp = (void*)(unsigned long)cmd->getkeys_proc;
return (cp->module == module_handle);
}

void extendKeySpecsIfNeeded(struct redisCommand *cmd) {
/* We extend even if key_specs_num == key_specs_max because
* this function is called prior to adding a new spec */
Expand Down Expand Up @@ -1095,7 +1162,7 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCtx *ctx, const char *name, int i
*
* Example:
*
* if (RedisModule_CreateCommand(ctx,"kspec.smove",kspec_legacy,"",0,0,0) == REDISMODULE_ERR)
* if (RedisModule_CreateCommand(ctx,"kspec.smove",kspec_legacy,"",0,0,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
* if (RedisModule_AddCommandKeySpec(ctx,"kspec.smove","read write",&spec_id) == REDISMODULE_ERR)
Expand All @@ -1112,6 +1179,11 @@ int moduleSetCommandKeySpecFindKeys(RedisModuleCtx *ctx, const char *name, int i
* if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"kspec.smove",spec_id,0,1,0) == REDISMODULE_ERR)
* return REDISMODULE_ERR;
*
* It is also possible to use this API on subcommands (See RedisModule_CreateSubcommand).
* The name of the subcommand should be the name of the parent command + "|" + name of subcommand.
* Example:
* RedisModule_AddCommandKeySpec(ctx,"module.config|get","read",&spec_id)
*
* Returns REDISMODULE_OK on success
*/
int RM_AddCommandKeySpec(RedisModuleCtx *ctx, const char *name, const char *specflags, int *spec_id) {
Expand Down Expand Up @@ -4858,7 +4930,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
/* Lookup command now, after filters had a chance to make modifications
* if necessary.
*/
cmd = lookupCommand(c->argv[0]->ptr);
cmd = lookupCommand(c->argv,c->argc);
if (!cmd) {
errno = ENOENT;
goto cleanup;
Expand Down Expand Up @@ -7373,7 +7445,7 @@ int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **arg
struct redisCommand *cmd;

/* Find command */
if ((cmd = lookupCommand(argv[0]->ptr)) == NULL) {
if ((cmd = lookupCommand(argv, argc)) == NULL) {
errno = ENOENT;
return REDISMODULE_ERR;
}
Expand Down Expand Up @@ -9745,8 +9817,7 @@ void moduleCommand(client *c) {
NULL
};
addReplyHelp(c, help);
} else
if (!strcasecmp(subcmd,"load") && c->argc >= 3) {
} else if (!strcasecmp(subcmd,"load") && c->argc >= 3) {
robj **argv = NULL;
int argc = 0;

Expand Down Expand Up @@ -9964,7 +10035,7 @@ int *RM_GetCommandKeys(RedisModuleCtx *ctx, RedisModuleString **argv, int argc,
int *res = NULL;

/* Find command */
if ((cmd = lookupCommand(argv[0]->ptr)) == NULL) {
if ((cmd = lookupCommand(argv,argc)) == NULL) {
errno = ENOENT;
return NULL;
}
Expand Down Expand Up @@ -10243,6 +10314,7 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(Free);
REGISTER_API(Strdup);
REGISTER_API(CreateCommand);
REGISTER_API(CreateSubcommand);
REGISTER_API(SetModuleAttribs);
REGISTER_API(IsModuleNameBusy);
REGISTER_API(WrongArity);
Expand Down
Loading

0 comments on commit 43e736f

Please sign in to comment.