mirror of
https://codeberg.org/redict/redict.git
synced 2025-01-21 23:58:51 -05:00
Treat subcommands as commands (#9504)
## 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)
This commit is contained in:
parent
4962c5526d
commit
43e736f79b
14
redis.conf
14
redis.conf
@ -818,8 +818,10 @@ 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
|
||||
@ -827,10 +829,10 @@ replica-priority 100
|
||||
# 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.
|
||||
|
@ -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
src/acl.c
288
src/acl.c
@ -101,9 +101,9 @@ struct ACLUserFlag {
|
||||
{NULL,0} /* Terminator. */
|
||||
};
|
||||
|
||||
void ACLResetSubcommandsForCommand(user *u, unsigned long id);
|
||||
void ACLResetSubcommands(user *u);
|
||||
void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub);
|
||||
void ACLResetFirstArgsForCommand(user *u, unsigned long id);
|
||||
void ACLResetFirstArgs(user *u);
|
||||
void ACLAddAllowedFirstArg(user *u, unsigned long id, const char *sub);
|
||||
void ACLFreeLogEntry(void *le);
|
||||
|
||||
/* The length of the string representation of a hashed password. */
|
||||
@ -254,7 +254,7 @@ user *ACLCreateUser(const char *name, size_t namelen) {
|
||||
user *u = zmalloc(sizeof(*u));
|
||||
u->name = sdsnewlen(name,namelen);
|
||||
u->flags = USER_FLAG_DISABLED | server.acl_pubsub_default;
|
||||
u->allowed_subcommands = NULL;
|
||||
u->allowed_firstargs = NULL;
|
||||
u->passwords = listCreate();
|
||||
u->patterns = listCreate();
|
||||
u->channels = listCreate();
|
||||
@ -296,7 +296,7 @@ void ACLFreeUser(user *u) {
|
||||
listRelease(u->passwords);
|
||||
listRelease(u->patterns);
|
||||
listRelease(u->channels);
|
||||
ACLResetSubcommands(u);
|
||||
ACLResetFirstArgs(u);
|
||||
zfree(u);
|
||||
}
|
||||
|
||||
@ -343,15 +343,15 @@ void ACLCopyUser(user *dst, user *src) {
|
||||
memcpy(dst->allowed_commands,src->allowed_commands,
|
||||
sizeof(dst->allowed_commands));
|
||||
dst->flags = src->flags;
|
||||
ACLResetSubcommands(dst);
|
||||
/* Copy the allowed subcommands array of array of SDS strings. */
|
||||
if (src->allowed_subcommands) {
|
||||
ACLResetFirstArgs(dst);
|
||||
/* Copy the allowed first-args array of array of SDS strings. */
|
||||
if (src->allowed_firstargs) {
|
||||
for (int j = 0; j < USER_COMMAND_BITS_COUNT; j++) {
|
||||
if (src->allowed_subcommands[j]) {
|
||||
for (int i = 0; src->allowed_subcommands[j][i]; i++)
|
||||
if (src->allowed_firstargs[j]) {
|
||||
for (int i = 0; src->allowed_firstargs[j][i]; i++)
|
||||
{
|
||||
ACLAddAllowedSubcommand(dst, j,
|
||||
src->allowed_subcommands[j][i]);
|
||||
ACLAddAllowedFirstArg(dst, j,
|
||||
src->allowed_firstargs[j][i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -413,6 +413,38 @@ void ACLSetUserCommandBit(user *u, unsigned long id, int value) {
|
||||
}
|
||||
}
|
||||
|
||||
/* This function is used to allow/block a specific command.
|
||||
* Allowing/blocking a container command also applies for its subcommands */
|
||||
void ACLChangeCommandPerm(user *u, struct redisCommand *cmd, int allow) {
|
||||
unsigned long id = cmd->id;
|
||||
ACLSetUserCommandBit(u,id,allow);
|
||||
ACLResetFirstArgsForCommand(u,id);
|
||||
if (cmd->subcommands_dict) {
|
||||
dictEntry *de;
|
||||
dictIterator *di = dictGetSafeIterator(cmd->subcommands_dict);
|
||||
while((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *sub = (struct redisCommand *)dictGetVal(de);
|
||||
ACLSetUserCommandBit(u,sub->id,allow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACLSetUserCommandBitsForCategoryLogic(dict *commands, user *u, uint64_t cflag, int value) {
|
||||
dictIterator *di = dictGetIterator(commands);
|
||||
dictEntry *de;
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
if (cmd->flags & CMD_MODULE) continue; /* Ignore modules commands. */
|
||||
if (cmd->flags & cflag) {
|
||||
ACLChangeCommandPerm(u,cmd,value);
|
||||
}
|
||||
if (cmd->subcommands_dict) {
|
||||
ACLSetUserCommandBitsForCategoryLogic(cmd->subcommands_dict, u, cflag, value);
|
||||
}
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
}
|
||||
|
||||
/* This is like ACLSetUserCommandBit(), but instead of setting the specified
|
||||
* ID, it will check all the commands in the category specified as argument,
|
||||
* and will set all the bits corresponding to such commands to the specified
|
||||
@ -422,18 +454,26 @@ void ACLSetUserCommandBit(user *u, unsigned long id, int value) {
|
||||
int ACLSetUserCommandBitsForCategory(user *u, const char *category, int value) {
|
||||
uint64_t cflag = ACLGetCommandCategoryFlagByName(category);
|
||||
if (!cflag) return C_ERR;
|
||||
dictIterator *di = dictGetIterator(server.orig_commands);
|
||||
ACLSetUserCommandBitsForCategoryLogic(server.orig_commands, u, cflag, value);
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
void ACLCountCategoryBitsForCommands(dict *commands, user *u, unsigned long *on, unsigned long *off, uint64_t cflag) {
|
||||
dictIterator *di = dictGetIterator(commands);
|
||||
dictEntry *de;
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
if (cmd->flags & CMD_MODULE) continue; /* Ignore modules commands. */
|
||||
if (cmd->flags & cflag) {
|
||||
ACLSetUserCommandBit(u,cmd->id,value);
|
||||
ACLResetSubcommandsForCommand(u,cmd->id);
|
||||
if (ACLGetUserCommandBit(u,cmd->id))
|
||||
(*on)++;
|
||||
else
|
||||
(*off)++;
|
||||
}
|
||||
if (cmd->subcommands_dict) {
|
||||
ACLCountCategoryBitsForCommands(cmd->subcommands_dict, u, on, off, cflag);
|
||||
}
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
/* Return the number of commands allowed (on) and denied (off) for the user 'u'
|
||||
@ -447,19 +487,46 @@ int ACLCountCategoryBitsForUser(user *u, unsigned long *on, unsigned long *off,
|
||||
if (!cflag) return C_ERR;
|
||||
|
||||
*on = *off = 0;
|
||||
dictIterator *di = dictGetIterator(server.orig_commands);
|
||||
ACLCountCategoryBitsForCommands(server.orig_commands, u, on, off, cflag);
|
||||
return C_OK;
|
||||
}
|
||||
|
||||
sds ACLDescribeUserCommandRulesSingleCommands(user *u, user *fakeuser, sds rules, dict *commands) {
|
||||
dictIterator *di = dictGetIterator(commands);
|
||||
dictEntry *de;
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
if (cmd->flags & cflag) {
|
||||
if (ACLGetUserCommandBit(u,cmd->id))
|
||||
(*on)++;
|
||||
else
|
||||
(*off)++;
|
||||
int userbit = ACLGetUserCommandBit(u,cmd->id);
|
||||
int fakebit = ACLGetUserCommandBit(fakeuser,cmd->id);
|
||||
if (userbit != fakebit) {
|
||||
rules = sdscatlen(rules, userbit ? "+" : "-", 1);
|
||||
sds fullname = getFullCommandName(cmd);
|
||||
rules = sdscat(rules,fullname);
|
||||
sdsfree(fullname);
|
||||
rules = sdscatlen(rules," ",1);
|
||||
ACLChangeCommandPerm(fakeuser,cmd,userbit);
|
||||
}
|
||||
|
||||
if (cmd->subcommands_dict)
|
||||
rules = ACLDescribeUserCommandRulesSingleCommands(u,fakeuser,rules,cmd->subcommands_dict);
|
||||
|
||||
/* Emit the first-args if there are any. */
|
||||
if (userbit == 0 && u->allowed_firstargs &&
|
||||
u->allowed_firstargs[cmd->id])
|
||||
{
|
||||
for (int j = 0; u->allowed_firstargs[cmd->id][j]; j++) {
|
||||
rules = sdscatlen(rules,"+",1);
|
||||
sds fullname = getFullCommandName(cmd);
|
||||
rules = sdscat(rules,fullname);
|
||||
sdsfree(fullname);
|
||||
rules = sdscatlen(rules,"|",1);
|
||||
rules = sdscatsds(rules,u->allowed_firstargs[cmd->id][j]);
|
||||
rules = sdscatlen(rules," ",1);
|
||||
}
|
||||
}
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
return C_OK;
|
||||
return rules;
|
||||
}
|
||||
|
||||
/* This function returns an SDS string representing the specified user ACL
|
||||
@ -563,33 +630,7 @@ sds ACLDescribeUserCommandRules(user *u) {
|
||||
}
|
||||
|
||||
/* Fix the final ACLs with single commands differences. */
|
||||
dictIterator *di = dictGetIterator(server.orig_commands);
|
||||
dictEntry *de;
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
int userbit = ACLGetUserCommandBit(u,cmd->id);
|
||||
int fakebit = ACLGetUserCommandBit(fakeuser,cmd->id);
|
||||
if (userbit != fakebit) {
|
||||
rules = sdscatlen(rules, userbit ? "+" : "-", 1);
|
||||
rules = sdscat(rules,cmd->name);
|
||||
rules = sdscatlen(rules," ",1);
|
||||
ACLSetUserCommandBit(fakeuser,cmd->id,userbit);
|
||||
}
|
||||
|
||||
/* Emit the subcommands if there are any. */
|
||||
if (userbit == 0 && u->allowed_subcommands &&
|
||||
u->allowed_subcommands[cmd->id])
|
||||
{
|
||||
for (int j = 0; u->allowed_subcommands[cmd->id][j]; j++) {
|
||||
rules = sdscatlen(rules,"+",1);
|
||||
rules = sdscat(rules,cmd->name);
|
||||
rules = sdscatlen(rules,"|",1);
|
||||
rules = sdscatsds(rules,u->allowed_subcommands[cmd->id][j]);
|
||||
rules = sdscatlen(rules," ",1);
|
||||
}
|
||||
}
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
rules = ACLDescribeUserCommandRulesSingleCommands(u,fakeuser,rules,server.orig_commands);
|
||||
|
||||
/* Trim the final useless space. */
|
||||
sdsrange(rules,0,-2);
|
||||
@ -683,67 +724,66 @@ sds ACLDescribeUser(user *u) {
|
||||
struct redisCommand *ACLLookupCommand(const char *name) {
|
||||
struct redisCommand *cmd;
|
||||
sds sdsname = sdsnew(name);
|
||||
cmd = dictFetchValue(server.orig_commands, sdsname);
|
||||
cmd = lookupCommandBySdsLogic(server.orig_commands,sdsname);
|
||||
sdsfree(sdsname);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/* Flush the array of allowed subcommands for the specified user
|
||||
/* Flush the array of allowed first-args for the specified user
|
||||
* and command ID. */
|
||||
void ACLResetSubcommandsForCommand(user *u, unsigned long id) {
|
||||
if (u->allowed_subcommands && u->allowed_subcommands[id]) {
|
||||
for (int i = 0; u->allowed_subcommands[id][i]; i++)
|
||||
sdsfree(u->allowed_subcommands[id][i]);
|
||||
zfree(u->allowed_subcommands[id]);
|
||||
u->allowed_subcommands[id] = NULL;
|
||||
void ACLResetFirstArgsForCommand(user *u, unsigned long id) {
|
||||
if (u->allowed_firstargs && u->allowed_firstargs[id]) {
|
||||
for (int i = 0; u->allowed_firstargs[id][i]; i++)
|
||||
sdsfree(u->allowed_firstargs[id][i]);
|
||||
zfree(u->allowed_firstargs[id]);
|
||||
u->allowed_firstargs[id] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Flush the entire table of subcommands. This is useful on +@all, -@all
|
||||
/* Flush the entire table of first-args. This is useful on +@all, -@all
|
||||
* or similar to return back to the minimal memory usage (and checks to do)
|
||||
* for the user. */
|
||||
void ACLResetSubcommands(user *u) {
|
||||
if (u->allowed_subcommands == NULL) return;
|
||||
void ACLResetFirstArgs(user *u) {
|
||||
if (u->allowed_firstargs == NULL) return;
|
||||
for (int j = 0; j < USER_COMMAND_BITS_COUNT; j++) {
|
||||
if (u->allowed_subcommands[j]) {
|
||||
for (int i = 0; u->allowed_subcommands[j][i]; i++)
|
||||
sdsfree(u->allowed_subcommands[j][i]);
|
||||
zfree(u->allowed_subcommands[j]);
|
||||
if (u->allowed_firstargs[j]) {
|
||||
for (int i = 0; u->allowed_firstargs[j][i]; i++)
|
||||
sdsfree(u->allowed_firstargs[j][i]);
|
||||
zfree(u->allowed_firstargs[j]);
|
||||
}
|
||||
}
|
||||
zfree(u->allowed_subcommands);
|
||||
u->allowed_subcommands = NULL;
|
||||
zfree(u->allowed_firstargs);
|
||||
u->allowed_firstargs = NULL;
|
||||
}
|
||||
|
||||
/* Add a subcommand to the list of subcommands for the user 'u' and
|
||||
/* Add a first-arh to the list of subcommands for the user 'u' and
|
||||
* the command id specified. */
|
||||
void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
|
||||
/* If this is the first subcommand to be configured for
|
||||
* this user, we have to allocate the subcommands array. */
|
||||
if (u->allowed_subcommands == NULL) {
|
||||
u->allowed_subcommands = zcalloc(USER_COMMAND_BITS_COUNT *
|
||||
sizeof(sds*));
|
||||
void ACLAddAllowedFirstArg(user *u, unsigned long id, const char *sub) {
|
||||
/* If this is the first first-arg to be configured for
|
||||
* this user, we have to allocate the first-args array. */
|
||||
if (u->allowed_firstargs == NULL) {
|
||||
u->allowed_firstargs = zcalloc(USER_COMMAND_BITS_COUNT * sizeof(sds*));
|
||||
}
|
||||
|
||||
/* We also need to enlarge the allocation pointing to the
|
||||
* null terminated SDS array, to make space for this one.
|
||||
* To start check the current size, and while we are here
|
||||
* make sure the subcommand is not already specified inside. */
|
||||
* make sure the first-arg is not already specified inside. */
|
||||
long items = 0;
|
||||
if (u->allowed_subcommands[id]) {
|
||||
while(u->allowed_subcommands[id][items]) {
|
||||
if (u->allowed_firstargs[id]) {
|
||||
while(u->allowed_firstargs[id][items]) {
|
||||
/* If it's already here do not add it again. */
|
||||
if (!strcasecmp(u->allowed_subcommands[id][items],sub)) return;
|
||||
if (!strcasecmp(u->allowed_firstargs[id][items],sub))
|
||||
return;
|
||||
items++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Now we can make space for the new item (and the null term). */
|
||||
items += 2;
|
||||
u->allowed_subcommands[id] = zrealloc(u->allowed_subcommands[id],
|
||||
sizeof(sds)*items);
|
||||
u->allowed_subcommands[id][items-2] = sdsnew(sub);
|
||||
u->allowed_subcommands[id][items-1] = NULL;
|
||||
u->allowed_firstargs[id] = zrealloc(u->allowed_firstargs[id], sizeof(sds)*items);
|
||||
u->allowed_firstargs[id][items-2] = sdsnew(sub);
|
||||
u->allowed_firstargs[id][items-1] = NULL;
|
||||
}
|
||||
|
||||
/* Set user properties according to the string "op". The following
|
||||
@ -753,8 +793,10 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
|
||||
* off Disable the user: it's no longer possible to authenticate
|
||||
* with this user, however the already authenticated connections
|
||||
* will still work.
|
||||
* +<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
|
||||
@ -762,10 +804,10 @@ void ACLAddAllowedSubcommand(user *u, unsigned long id, const char *sub) {
|
||||
* 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.
|
||||
@ -866,13 +908,13 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
|
||||
{
|
||||
memset(u->allowed_commands,255,sizeof(u->allowed_commands));
|
||||
u->flags |= USER_FLAG_ALLCOMMANDS;
|
||||
ACLResetSubcommands(u);
|
||||
ACLResetFirstArgs(u);
|
||||
} else if (!strcasecmp(op,"nocommands") ||
|
||||
!strcasecmp(op,"-@all"))
|
||||
{
|
||||
memset(u->allowed_commands,0,sizeof(u->allowed_commands));
|
||||
u->flags &= ~USER_FLAG_ALLCOMMANDS;
|
||||
ACLResetSubcommands(u);
|
||||
ACLResetFirstArgs(u);
|
||||
} else if (!strcasecmp(op,"nopass")) {
|
||||
u->flags |= USER_FLAG_NOPASS;
|
||||
listEmpty(u->passwords);
|
||||
@ -952,24 +994,25 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
|
||||
sdsfree(newpat);
|
||||
u->flags &= ~USER_FLAG_ALLCHANNELS;
|
||||
} else if (op[0] == '+' && op[1] != '@') {
|
||||
if (strchr(op,'|') == NULL) {
|
||||
if (ACLLookupCommand(op+1) == NULL) {
|
||||
if (strrchr(op,'|') == NULL) {
|
||||
struct redisCommand *cmd = ACLLookupCommand(op+1);
|
||||
if (cmd == NULL) {
|
||||
errno = ENOENT;
|
||||
return C_ERR;
|
||||
}
|
||||
unsigned long id = ACLGetCommandID(op+1);
|
||||
ACLSetUserCommandBit(u,id,1);
|
||||
ACLResetSubcommandsForCommand(u,id);
|
||||
ACLChangeCommandPerm(u,cmd,1);
|
||||
} else {
|
||||
/* Split the command and subcommand parts. */
|
||||
char *copy = zstrdup(op+1);
|
||||
char *sub = strchr(copy,'|');
|
||||
char *sub = strrchr(copy,'|');
|
||||
sub[0] = '\0';
|
||||
sub++;
|
||||
|
||||
struct redisCommand *cmd = ACLLookupCommand(copy);
|
||||
|
||||
/* Check if the command exists. We can't check the
|
||||
* subcommand to see if it is valid. */
|
||||
if (ACLLookupCommand(copy) == NULL) {
|
||||
if (cmd == NULL) {
|
||||
zfree(copy);
|
||||
errno = ENOENT;
|
||||
return C_ERR;
|
||||
@ -983,22 +1026,38 @@ int ACLSetUser(user *u, const char *op, ssize_t oplen) {
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
unsigned long id = ACLGetCommandID(copy);
|
||||
/* Add the subcommand to the list of valid ones, if the command is not set. */
|
||||
if (ACLGetUserCommandBit(u,id) == 0) {
|
||||
ACLAddAllowedSubcommand(u,id,sub);
|
||||
if (cmd->subcommands_dict) {
|
||||
/* If user is trying to allow a valid subcommand we can just add its unique ID */
|
||||
struct redisCommand *cmd = ACLLookupCommand(op+1);
|
||||
if (cmd == NULL) {
|
||||
zfree(copy);
|
||||
errno = ENOENT;
|
||||
return C_ERR;
|
||||
}
|
||||
ACLChangeCommandPerm(u,cmd,1);
|
||||
} else {
|
||||
/* If user is trying to use the ACL mech to block SELECT except SELECT 0 or
|
||||
* block DEBUG except DEBUG OBJECT (DEBUG subcommands are not considered
|
||||
* subcommands for now) we use the allowed_firstargs mechanism. */
|
||||
struct redisCommand *cmd = ACLLookupCommand(copy);
|
||||
if (cmd == NULL) {
|
||||
zfree(copy);
|
||||
errno = ENOENT;
|
||||
return C_ERR;
|
||||
}
|
||||
/* Add the first-arg to the list of valid ones. */
|
||||
ACLAddAllowedFirstArg(u,cmd->id,sub);
|
||||
}
|
||||
|
||||
zfree(copy);
|
||||
}
|
||||
} else if (op[0] == '-' && op[1] != '@') {
|
||||
if (ACLLookupCommand(op+1) == NULL) {
|
||||
struct redisCommand *cmd = ACLLookupCommand(op+1);
|
||||
if (cmd == NULL) {
|
||||
errno = ENOENT;
|
||||
return C_ERR;
|
||||
}
|
||||
unsigned long id = ACLGetCommandID(op+1);
|
||||
ACLSetUserCommandBit(u,id,0);
|
||||
ACLResetSubcommandsForCommand(u,id);
|
||||
ACLChangeCommandPerm(u,cmd,0);
|
||||
} else if ((op[0] == '+' || op[0] == '-') && op[1] == '@') {
|
||||
int bitval = op[0] == '+' ? 1 : 0;
|
||||
if (ACLSetUserCommandBitsForCategory(u,op+2,bitval) == C_ERR) {
|
||||
@ -1138,7 +1197,6 @@ int ACLAuthenticateUser(client *c, robj *username, robj *password) {
|
||||
* command name, so that a command retains the same ID in case of modules that
|
||||
* are unloaded and later reloaded. */
|
||||
unsigned long ACLGetCommandID(const char *cmdname) {
|
||||
|
||||
sds lowername = sdsnew(cmdname);
|
||||
sdstolower(lowername);
|
||||
if (commandId == NULL) commandId = raxNew();
|
||||
@ -1225,23 +1283,23 @@ int ACLCheckCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, in
|
||||
if (!(u->flags & USER_FLAG_ALLCOMMANDS) && !(cmd->flags & CMD_NO_AUTH))
|
||||
{
|
||||
/* If the bit is not set we have to check further, in case the
|
||||
* command is allowed just with that specific subcommand. */
|
||||
* command is allowed just with that specific first argument. */
|
||||
if (ACLGetUserCommandBit(u,id) == 0) {
|
||||
/* Check if the subcommand matches. */
|
||||
/* Check if the first argument matches. */
|
||||
if (argc < 2 ||
|
||||
u->allowed_subcommands == NULL ||
|
||||
u->allowed_subcommands[id] == NULL)
|
||||
u->allowed_firstargs == NULL ||
|
||||
u->allowed_firstargs[id] == NULL)
|
||||
{
|
||||
return ACL_DENIED_CMD;
|
||||
}
|
||||
|
||||
long subid = 0;
|
||||
while (1) {
|
||||
if (u->allowed_subcommands[id][subid] == NULL)
|
||||
if (u->allowed_firstargs[id][subid] == NULL)
|
||||
return ACL_DENIED_CMD;
|
||||
if (!strcasecmp(argv[1]->ptr,
|
||||
u->allowed_subcommands[id][subid]))
|
||||
break; /* Subcommand match found. Stop here. */
|
||||
int idx = cmd->parent ? 2 : 1;
|
||||
if (!strcasecmp(argv[idx]->ptr,u->allowed_firstargs[id][subid]))
|
||||
break; /* First argument match found. Stop here. */
|
||||
subid++;
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
69
src/config.c
69
src/config.c
@ -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) {
|
||||
@ -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>",
|
||||
@ -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
src/db.c
15
src/db.c
@ -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) {
|
||||
|
106
src/module.c
106
src/module.c
@ -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
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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;
|
||||
@ -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 */
|
||||
@ -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)
|
||||
@ -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) {
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
|
@ -2399,7 +2399,8 @@ sds catClientInfoString(sds s, client *client) {
|
||||
/* Compute the total memory consumed by this client. */
|
||||
size_t obufmem, total_mem = getClientMemoryUsage(client, &obufmem);
|
||||
|
||||
return sdscatfmt(s,
|
||||
sds cmdname = client->lastcmd ? getFullCommandName(client->lastcmd) : NULL;
|
||||
sds ret = sdscatfmt(s,
|
||||
"id=%U addr=%s laddr=%s %s name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i multi=%i qbuf=%U qbuf-free=%U argv-mem=%U multi-mem=%U obl=%U oll=%U omem=%U tot-mem=%U events=%s cmd=%s user=%s redir=%I resp=%i",
|
||||
(unsigned long long) client->id,
|
||||
getClientPeerId(client),
|
||||
@ -2422,10 +2423,13 @@ sds catClientInfoString(sds s, client *client) {
|
||||
(unsigned long long) obufmem, /* should not include client->buf since we want to see 0 for static clients. */
|
||||
(unsigned long long) total_mem,
|
||||
events,
|
||||
client->lastcmd ? client->lastcmd->name : "NULL",
|
||||
cmdname ? cmdname : "NULL",
|
||||
client->user ? client->user->name : "(superuser)",
|
||||
(client->flags & CLIENT_TRACKING) ? (long long) client->client_tracking_redirection : -1,
|
||||
client->resp);
|
||||
if (cmdname)
|
||||
sdsfree(cmdname);
|
||||
return ret;
|
||||
}
|
||||
|
||||
sds getAllClientsInfoString(int type) {
|
||||
@ -2568,6 +2572,8 @@ void clientCommand(client *c) {
|
||||
" Control the replies sent to the current connection.",
|
||||
"SETNAME <name>",
|
||||
" Assign the name <name> to the current connection.",
|
||||
"GETNAME",
|
||||
" Get the name of the current connection.",
|
||||
"UNBLOCK <clientid> [TIMEOUT|ERROR]",
|
||||
" Unblock the specified blocked client.",
|
||||
"TRACKING (ON|OFF) [REDIRECT <id>] [BCAST] [PREFIX <prefix> [...]]",
|
||||
@ -2575,6 +2581,8 @@ void clientCommand(client *c) {
|
||||
" Control server assisted client side caching.",
|
||||
"TRACKINGINFO",
|
||||
" Report tracking status for the current connection.",
|
||||
"NO-EVICT (ON|OFF)",
|
||||
" Protect current client connection from eviction.",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
@ -2638,7 +2646,7 @@ NULL
|
||||
return;
|
||||
}
|
||||
} else if (!strcasecmp(c->argv[1]->ptr,"no-evict") && c->argc == 3) {
|
||||
/* CLIENT PROTECT ON|OFF */
|
||||
/* CLIENT NO-EVICT ON|OFF */
|
||||
if (!strcasecmp(c->argv[2]->ptr,"on")) {
|
||||
c->flags |= CLIENT_NO_EVICT;
|
||||
addReply(c,shared.ok);
|
||||
@ -3195,7 +3203,7 @@ void replaceClientCommandVector(client *c, int argc, robj **argv) {
|
||||
for (j = 0; j < c->argc; j++)
|
||||
if (c->argv[j])
|
||||
c->argv_len_sum += getStringObjectLen(c->argv[j]);
|
||||
c->cmd = lookupCommandOrOriginal(c->argv[0]->ptr);
|
||||
c->cmd = lookupCommandOrOriginal(c->argv,c->argc);
|
||||
serverAssertWithInfo(c,NULL,c->cmd != NULL);
|
||||
}
|
||||
|
||||
@ -3227,7 +3235,7 @@ void rewriteClientCommandArgument(client *c, int i, robj *newval) {
|
||||
|
||||
/* If this is the command name make sure to fix c->cmd. */
|
||||
if (i == 0) {
|
||||
c->cmd = lookupCommandOrOriginal(c->argv[0]->ptr);
|
||||
c->cmd = lookupCommandOrOriginal(c->argv,c->argc);
|
||||
serverAssertWithInfo(c,NULL,c->cmd != NULL);
|
||||
}
|
||||
}
|
||||
|
@ -406,6 +406,11 @@ void punsubscribeCommand(client *c) {
|
||||
|
||||
/* PUBLISH <channel> <message> */
|
||||
void publishCommand(client *c) {
|
||||
if (server.sentinel_mode) {
|
||||
sentinelPublishCommand(c);
|
||||
return;
|
||||
}
|
||||
|
||||
int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
|
||||
if (server.cluster_enabled)
|
||||
clusterPropagatePublish(c->argv[1],c->argv[2]);
|
||||
|
@ -604,6 +604,7 @@ REDISMODULE_API void * (*RedisModule_Calloc)(size_t nmemb, size_t size) REDISMOD
|
||||
REDISMODULE_API char * (*RedisModule_Strdup)(const char *str) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_GetApi)(const char *, void *) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_CreateSubcommand)(RedisModuleCtx *ctx, const char *parent_name, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) REDISMODULE_ATTR;
|
||||
REDISMODULE_API void (*RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_IsModuleNameBusy)(const char *name) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_WrongArity)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
|
||||
@ -924,6 +925,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
||||
REDISMODULE_GET_API(Realloc);
|
||||
REDISMODULE_GET_API(Strdup);
|
||||
REDISMODULE_GET_API(CreateCommand);
|
||||
REDISMODULE_GET_API(CreateSubcommand);
|
||||
REDISMODULE_GET_API(SetModuleAttribs);
|
||||
REDISMODULE_GET_API(IsModuleNameBusy);
|
||||
REDISMODULE_GET_API(WrongArity);
|
||||
|
@ -2812,6 +2812,11 @@ void replicaofCommand(client *c) {
|
||||
* (master or slave) and additional information related to replication
|
||||
* in an easy to process format. */
|
||||
void roleCommand(client *c) {
|
||||
if (server.sentinel_mode) {
|
||||
sentinelRoleCommand(c);
|
||||
return;
|
||||
}
|
||||
|
||||
if (server.masterhost == NULL) {
|
||||
listIter li;
|
||||
listNode *ln;
|
||||
|
@ -817,7 +817,7 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
|
||||
}
|
||||
|
||||
/* Command lookup */
|
||||
cmd = lookupCommand(argv[0]->ptr);
|
||||
cmd = lookupCommand(argv,argc);
|
||||
if (!cmd || ((cmd->arity > 0 && cmd->arity != argc) ||
|
||||
(argc < -cmd->arity)))
|
||||
{
|
||||
|
@ -456,32 +456,10 @@ dictType renamedCommandsDictType = {
|
||||
|
||||
/* =========================== Initialization =============================== */
|
||||
|
||||
void sentinelCommand(client *c);
|
||||
void sentinelInfoCommand(client *c);
|
||||
void sentinelSetCommand(client *c);
|
||||
void sentinelPublishCommand(client *c);
|
||||
void sentinelRoleCommand(client *c);
|
||||
void sentinelConfigGetCommand(client *c);
|
||||
void sentinelConfigSetCommand(client *c);
|
||||
|
||||
struct redisCommand sentinelcmds[] = {
|
||||
{"ping",pingCommand,1,"fast @connection"},
|
||||
{"sentinel",sentinelCommand,-2,"admin"},
|
||||
{"subscribe",subscribeCommand,-2,"pub-sub"},
|
||||
{"unsubscribe",unsubscribeCommand,-1,"pub-sub"},
|
||||
{"psubscribe",psubscribeCommand,-2,"pub-sub"},
|
||||
{"punsubscribe",punsubscribeCommand,-1,"pub-sub"},
|
||||
{"publish",sentinelPublishCommand,3,"pub-sub fast"},
|
||||
{"info",sentinelInfoCommand,-1,"random @dangerous"},
|
||||
{"role",sentinelRoleCommand,1,"fast read-only @dangerous"},
|
||||
{"client",clientCommand,-2,"admin random @connection"},
|
||||
{"shutdown",shutdownCommand,-1,"admin"},
|
||||
{"auth",authCommand,-2,"no-auth fast @connection"},
|
||||
{"hello",helloCommand,-1,"no-auth fast @connection"},
|
||||
{"acl",aclCommand,-2,"admin"},
|
||||
{"command",commandCommand,-1, "random @connection"}
|
||||
};
|
||||
|
||||
/* this array is used for sentinel config lookup, which need to be loaded
|
||||
* before monitoring masters config to avoid dependency issues */
|
||||
const char *preMonitorCfgName[] = {
|
||||
@ -507,28 +485,6 @@ void freeSentinelLoadQueueEntry(void *item);
|
||||
|
||||
/* Perform the Sentinel mode initialization. */
|
||||
void initSentinel(void) {
|
||||
unsigned int j;
|
||||
|
||||
/* Remove usual Redis commands from the command table, then just add
|
||||
* the SENTINEL command. */
|
||||
dictEmpty(server.commands,NULL);
|
||||
dictEmpty(server.orig_commands,NULL);
|
||||
ACLClearCommandID();
|
||||
for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
|
||||
int retval;
|
||||
struct redisCommand *cmd = sentinelcmds+j;
|
||||
cmd->id = ACLGetCommandID(cmd->name); /* Assign the ID used for ACL. */
|
||||
retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
|
||||
serverAssert(retval == DICT_OK);
|
||||
retval = dictAdd(server.orig_commands, sdsnew(cmd->name), cmd);
|
||||
serverAssert(retval == DICT_OK);
|
||||
|
||||
/* Translate the command string flags description into an actual
|
||||
* set of flags. */
|
||||
if (populateSingleCommand(cmd,cmd->sflags) == C_ERR)
|
||||
serverPanic("Unsupported command flag");
|
||||
}
|
||||
|
||||
/* Initialize various data structures. */
|
||||
sentinel.current_epoch = 0;
|
||||
sentinel.masters = dictCreate(&instancesDictType);
|
||||
|
1029
src/server.c
1029
src/server.c
File diff suppressed because it is too large
Load Diff
61
src/server.h
61
src/server.h
@ -233,6 +233,9 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
|
||||
#define CMD_CATEGORY_TRANSACTION (1ULL<<38)
|
||||
#define CMD_CATEGORY_SCRIPTING (1ULL<<39)
|
||||
|
||||
#define CMD_SENTINEL (1ULL<<40) /* "sentinel" flag */
|
||||
#define CMD_ONLY_SENTINEL (1ULL<<41) /* "only-sentinel" flag */
|
||||
|
||||
/* AOF states */
|
||||
#define AOF_OFF 0 /* AOF is off */
|
||||
#define AOF_ON 1 /* AOF is on */
|
||||
@ -886,20 +889,29 @@ typedef struct {
|
||||
uint64_t flags; /* See USER_FLAG_* */
|
||||
|
||||
/* The bit in allowed_commands is set if this user has the right to
|
||||
* execute this command. In commands having subcommands, if this bit is
|
||||
* set, then all the subcommands are also available.
|
||||
* execute this command.
|
||||
*
|
||||
* If the bit for a given command is NOT set and the command has
|
||||
* subcommands, Redis will also check allowed_subcommands in order to
|
||||
* allowed first-args, Redis will also check allowed_firstargs in order to
|
||||
* understand if the command can be executed. */
|
||||
uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];
|
||||
|
||||
/* This array points, for each command ID (corresponding to the command
|
||||
/* NOTE: allowed_firstargs is a transformation of the old mechanism for allowing
|
||||
* subcommands (now, subcommands are actually commands, with their own
|
||||
* ACL ID)
|
||||
* We had to keep allowed_firstargs (previously called allowed_subcommands)
|
||||
* in order to support the widespread abuse of ACL rules to block a command
|
||||
* with a specific argv[1] (which is not a subcommand at all).
|
||||
* For example, a user can use the rule "-select +select|0" to block all
|
||||
* SELECT commands, except "SELECT 0".
|
||||
* It can also be applied for subcommands: "+config -config|set +config|set|loglevel"
|
||||
*
|
||||
* This array points, for each command ID (corresponding to the command
|
||||
* bit set in allowed_commands), to an array of SDS strings, terminated by
|
||||
* a NULL pointer, with all the sub commands that can be executed for
|
||||
* this command. When no subcommands matching is used, the field is just
|
||||
* a NULL pointer, with all the first-args that are allowed for
|
||||
* this command. When no first-arg matching is used, the field is just
|
||||
* set to NULL to avoid allocating USER_COMMAND_BITS_COUNT pointers. */
|
||||
sds **allowed_subcommands;
|
||||
sds **allowed_firstargs;
|
||||
list *passwords; /* A list of SDS valid passwords for this user. */
|
||||
list *patterns; /* A list of allowed key patterns. If this field is NULL
|
||||
the user cannot mention any key in a command, unless
|
||||
@ -1824,8 +1836,10 @@ struct redisCommand {
|
||||
char *sflags; /* Flags as string representation, one char per flag. */
|
||||
keySpec key_specs_static[STATIC_KEY_SPECS_NUM];
|
||||
/* Use a function to determine keys arguments in a command line.
|
||||
* Used for Redis Cluster redirect. */
|
||||
* Used for Redis Cluster redirect (may be NULL) */
|
||||
redisGetKeysProc *getkeys_proc;
|
||||
/* Array of subcommands (may be NULL) */
|
||||
struct redisCommand *subcommands;
|
||||
|
||||
/* Runtime data */
|
||||
uint64_t flags; /* The actual flags, obtained from the 'sflags' field. */
|
||||
@ -1844,6 +1858,8 @@ struct redisCommand {
|
||||
int key_specs_num;
|
||||
int key_specs_max;
|
||||
int movablekeys; /* See populateCommandMovableKeys */
|
||||
dict *subcommands_dict;
|
||||
struct redisCommand *parent;
|
||||
};
|
||||
|
||||
struct redisError {
|
||||
@ -1980,6 +1996,8 @@ robj *moduleTypeDupOrReply(client *c, robj *fromkey, robj *tokey, int todb, robj
|
||||
int moduleDefragValue(robj *key, robj *obj, long *defragged, int dbid);
|
||||
int moduleLateDefrag(robj *key, robj *value, unsigned long *cursor, long long endtime, long long *defragged, int dbid);
|
||||
long moduleDefragGlobals(void);
|
||||
void *moduleGetHandleByName(char *modulename);
|
||||
int moduleIsModuleCommand(void *module_handle, struct redisCommand *cmd);
|
||||
|
||||
/* Utils */
|
||||
long long ustime(void);
|
||||
@ -2420,9 +2438,12 @@ void removeSignalHandlers(void);
|
||||
int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler);
|
||||
int changeListenPort(int port, socketFds *sfd, aeFileProc *accept_handler);
|
||||
int changeBindAddr(sds *addrlist, int addrlist_len);
|
||||
struct redisCommand *lookupCommand(sds name);
|
||||
struct redisCommand *lookupCommand(robj **argv ,int argc);
|
||||
struct redisCommand *lookupCommandBySdsLogic(dict *commands, sds s);
|
||||
struct redisCommand *lookupCommandBySds(sds s);
|
||||
struct redisCommand *lookupCommandByCStringLogic(dict *commands, const char *s);
|
||||
struct redisCommand *lookupCommandByCString(const char *s);
|
||||
struct redisCommand *lookupCommandOrOriginal(sds name);
|
||||
struct redisCommand *lookupCommandOrOriginal(robj **argv ,int argc);
|
||||
void call(client *c, int flags);
|
||||
void propagate(int dbid, robj **argv, int argc, int flags);
|
||||
void alsoPropagate(int dbid, robj **argv, int argc, int target);
|
||||
@ -2448,7 +2469,7 @@ void usage(void);
|
||||
void updateDictResizePolicy(void);
|
||||
int htNeedsResize(dict *dict);
|
||||
void populateCommandTable(void);
|
||||
void resetCommandTableStats(void);
|
||||
void resetCommandTableStats(dict* commands);
|
||||
void resetErrorTableStats(void);
|
||||
void adjustOpenFilesLimit(void);
|
||||
void incrementErrorCount(const char *fullerr, size_t namelen);
|
||||
@ -2606,7 +2627,6 @@ int sortGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *
|
||||
int migrateGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
|
||||
int georadiusGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
|
||||
int xreadGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
|
||||
int memoryGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
|
||||
int lcsGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
|
||||
int lmpopGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
|
||||
int blmpopGetKeys(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
|
||||
@ -2624,6 +2644,10 @@ void queueSentinelConfig(sds *argv, int argc, int linenum, sds line);
|
||||
void loadSentinelConfigFromQueue(void);
|
||||
void sentinelIsRunning(void);
|
||||
void sentinelCheckConfigFile(void);
|
||||
void sentinelCommand(client *c);
|
||||
void sentinelInfoCommand(client *c);
|
||||
void sentinelPublishCommand(client *c);
|
||||
void sentinelRoleCommand(client *c);
|
||||
|
||||
/* redis-check-rdb & aof */
|
||||
int redis_check_rdb(char *rdbfilename, FILE *fp);
|
||||
@ -2694,6 +2718,11 @@ void authCommand(client *c);
|
||||
void pingCommand(client *c);
|
||||
void echoCommand(client *c);
|
||||
void commandCommand(client *c);
|
||||
void commandCountCommand(client *c);
|
||||
void commandListCommand(client *c);
|
||||
void commandInfoCommand(client *c);
|
||||
void commandGetKeysCommand(client *c);
|
||||
void commandHelpCommand(client *c);
|
||||
void setCommand(client *c);
|
||||
void setnxCommand(client *c);
|
||||
void setexCommand(client *c);
|
||||
@ -2846,7 +2875,11 @@ void hgetallCommand(client *c);
|
||||
void hexistsCommand(client *c);
|
||||
void hscanCommand(client *c);
|
||||
void hrandfieldCommand(client *c);
|
||||
void configCommand(client *c);
|
||||
void configSetCommand(client *c);
|
||||
void configGetCommand(client *c);
|
||||
void configResetStatCommand(client *c);
|
||||
void configRewriteCommand(client *c);
|
||||
void configHelpCommand(client *c);
|
||||
void hincrbyCommand(client *c);
|
||||
void hincrbyfloatCommand(client *c);
|
||||
void subscribeCommand(client *c);
|
||||
@ -2937,6 +2970,7 @@ void _serverPanic(const char *file, int line, const char *msg, ...);
|
||||
#endif
|
||||
void serverLogObjectDebugInfo(const robj *o);
|
||||
void sigsegvHandler(int sig, siginfo_t *info, void *secret);
|
||||
sds getFullCommandName(struct redisCommand *cmd);
|
||||
const char *getSafeInfoString(const char *s, size_t len, char **tmp);
|
||||
sds genRedisInfoString(const char *section);
|
||||
sds genModulesInfoString(sds info);
|
||||
@ -2948,6 +2982,7 @@ int memtest_preserving_test(unsigned long *m, size_t bytes, int passes);
|
||||
void mixDigest(unsigned char *digest, void *ptr, size_t len);
|
||||
void xorDigest(unsigned char *digest, void *ptr, size_t len);
|
||||
int populateSingleCommand(struct redisCommand *c, char *strflags);
|
||||
void commandAddSubcommand(struct redisCommand *parent, struct redisCommand *subcommand);
|
||||
void populateCommandMovableKeys(struct redisCommand *cmd);
|
||||
void debugDelay(int usec);
|
||||
void killIOThreads(void);
|
||||
|
@ -724,11 +724,20 @@ void strlenCommand(client *c) {
|
||||
* STRALGO <algorithm> ... arguments ... */
|
||||
void stralgoLCS(client *c); /* This implements the LCS algorithm. */
|
||||
void stralgoCommand(client *c) {
|
||||
/* Select the algorithm. */
|
||||
if (!strcasecmp(c->argv[1]->ptr,"lcs")) {
|
||||
char *subcmd = c->argv[1]->ptr;
|
||||
|
||||
if (c->argc == 2 && !strcasecmp(subcmd,"help")) {
|
||||
const char *help[] = {
|
||||
"LCS",
|
||||
" Run the longest common subsequence algorithm.",
|
||||
NULL
|
||||
};
|
||||
addReplyHelp(c, help);
|
||||
} else if (!strcasecmp(subcmd,"lcs")) {
|
||||
stralgoLCS(c);
|
||||
} else {
|
||||
addReplyErrorObject(c,shared.syntaxerr);
|
||||
addReplySubcommandSyntaxError(c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,8 @@ TEST_MODULES = \
|
||||
zset.so \
|
||||
stream.so \
|
||||
aclcheck.so \
|
||||
list.so
|
||||
list.so \
|
||||
subcommands.so
|
||||
|
||||
|
||||
.PHONY: all
|
||||
|
55
tests/modules/subcommands.c
Normal file
55
tests/modules/subcommands.c
Normal file
@ -0,0 +1,55 @@
|
||||
#include "redismodule.h"
|
||||
|
||||
#define UNUSED(V) ((void) V)
|
||||
|
||||
int cmd_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
UNUSED(argv);
|
||||
UNUSED(argc);
|
||||
RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
int cmd_get(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
UNUSED(argv);
|
||||
UNUSED(argc);
|
||||
RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
int spec_id;
|
||||
|
||||
if (RedisModule_Init(ctx, "subcommands", 1, REDISMODULE_APIVER_1)== REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"subcommands.bitarray",NULL,"",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateSubcommand(ctx,"subcommands.bitarray","set",cmd_set,"",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
if (RedisModule_AddCommandKeySpec(ctx,"subcommands.bitarray|set","write",&spec_id) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"subcommands.bitarray|set",spec_id,1) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"subcommands.bitarray|set",spec_id,0,1,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateSubcommand(ctx,"subcommands.bitarray","get",cmd_get,"",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
if (RedisModule_AddCommandKeySpec(ctx,"subcommands.bitarray|get","read",&spec_id) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
if (RedisModule_SetCommandKeySpecBeginSearchIndex(ctx,"subcommands.bitarray|get",spec_id,1) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
if (RedisModule_SetCommandKeySpecFindKeysRange(ctx,"subcommands.bitarray|get",spec_id,0,1,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
/* Sanity */
|
||||
RedisModule_Assert(RedisModule_CreateSubcommand(ctx,"bitarray","get",NULL,"",0,0,0) == REDISMODULE_ERR);
|
||||
RedisModule_Assert(RedisModule_CreateSubcommand(ctx,"subcommands.bitarray","get",NULL,"",0,0,0) == REDISMODULE_ERR);
|
||||
RedisModule_Assert(RedisModule_CreateSubcommand(ctx,"subcommands.bitarray|get","get",NULL,"",0,0,0) == REDISMODULE_ERR);
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
@ -251,12 +251,71 @@ start_server {tags {"acl external:skip"}} {
|
||||
test {ACLs can include single subcommands} {
|
||||
r ACL setuser newuser +@all -client
|
||||
r ACL setuser newuser +client|id +client|setname
|
||||
set cmdstr [dict get [r ACL getuser newuser] commands]
|
||||
assert_match {+@all*-client*+client|id*} $cmdstr
|
||||
assert_match {+@all*-client*+client|setname*} $cmdstr
|
||||
r CLIENT ID; # Should not fail
|
||||
r CLIENT SETNAME foo ; # Should not fail
|
||||
catch {r CLIENT KILL type master} e
|
||||
set e
|
||||
} {*NOPERM*}
|
||||
|
||||
test {ACLs can exclude single subcommands, case 1} {
|
||||
r ACL setuser newuser +@all -client|kill
|
||||
set cmdstr [dict get [r ACL getuser newuser] commands]
|
||||
assert_equal {+@all -client|kill} $cmdstr
|
||||
r CLIENT ID; # Should not fail
|
||||
r CLIENT SETNAME foo ; # Should not fail
|
||||
catch {r CLIENT KILL type master} e
|
||||
set e
|
||||
} {*NOPERM*}
|
||||
|
||||
test {ACLs can exclude single subcommands, case 2} {
|
||||
r ACL setuser newuser -@all +acl +config -config|set
|
||||
set cmdstr [dict get [r ACL getuser newuser] commands]
|
||||
assert_match {*+config*} $cmdstr
|
||||
assert_match {*-config|set*} $cmdstr
|
||||
r CONFIG GET loglevel; # Should not fail
|
||||
catch {r CONFIG SET loglevel debug} e
|
||||
set e
|
||||
} {*NOPERM*}
|
||||
|
||||
test {ACLs can include a subcommand with a specific arg} {
|
||||
r ACL setuser newuser +@all -config|get
|
||||
r ACL setuser newuser +config|get|appendonly
|
||||
set cmdstr [dict get [r ACL getuser newuser] commands]
|
||||
assert_match {*-config|get*} $cmdstr
|
||||
assert_match {*+config|get|appendonly*} $cmdstr
|
||||
r CONFIG GET appendonly; # Should not fail
|
||||
catch {r CONFIG GET loglevel} e
|
||||
set e
|
||||
} {*NOPERM*}
|
||||
|
||||
test {ACLs including of a type includes also subcommands} {
|
||||
r ACL setuser newuser -@all +acl +@stream
|
||||
r XADD key * field value
|
||||
r XINFO STREAM key
|
||||
}
|
||||
|
||||
test {ACLs can block SELECT of all but a specific DB} {
|
||||
r ACL setuser newuser -@all +acl +select|0
|
||||
set cmdstr [dict get [r ACL getuser newuser] commands]
|
||||
assert_match {*+select|0*} $cmdstr
|
||||
r SELECT 0
|
||||
catch {r SELECT 1} e
|
||||
set e
|
||||
} {*NOPERM*}
|
||||
|
||||
test {ACLs can block all DEBUG subcommands except one} {
|
||||
r ACL setuser newuser -@all +acl +incr +debug|object
|
||||
set cmdstr [dict get [r ACL getuser newuser] commands]
|
||||
assert_match {*+debug|object*} $cmdstr
|
||||
r INCR key
|
||||
r DEBUG OBJECT key
|
||||
catch {r DEBUG SEGFAULT} e
|
||||
set e
|
||||
} {*NOPERM*}
|
||||
|
||||
test {ACLs set can include subcommands, if already full command exists} {
|
||||
r ACL setuser bob +memory|doctor
|
||||
set cmdstr [dict get [r ACL getuser bob] commands]
|
||||
@ -272,10 +331,56 @@ start_server {tags {"acl external:skip"}} {
|
||||
# Validate the new commands has got engulfed to +@all.
|
||||
set cmdstr [dict get [r ACL getuser bob] commands]
|
||||
assert_equal {+@all} $cmdstr
|
||||
|
||||
r ACL setuser bob >passwd1 on
|
||||
r AUTH bob passwd1
|
||||
r CLIENT ID; # Should not fail
|
||||
r MEMORY DOCTOR; # Should not fail
|
||||
}
|
||||
|
||||
test {ACLs set can exclude subcommands, if already full command exists} {
|
||||
r ACL setuser alice +@all -memory|doctor
|
||||
set cmdstr [dict get [r ACL getuser alice] commands]
|
||||
assert_equal {+@all -memory|doctor} $cmdstr
|
||||
|
||||
r ACL setuser alice >passwd1 on
|
||||
r AUTH alice passwd1
|
||||
|
||||
catch {r MEMORY DOCTOR} e
|
||||
assert_match {*NOPERM*} $e
|
||||
r MEMORY STATS ;# should work
|
||||
|
||||
# Validate the commands have got engulfed to -memory.
|
||||
r ACL setuser alice +@all -memory
|
||||
set cmdstr [dict get [r ACL getuser alice] commands]
|
||||
assert_equal {+@all -memory} $cmdstr
|
||||
|
||||
catch {r MEMORY DOCTOR} e
|
||||
assert_match {*NOPERM*} $e
|
||||
catch {r MEMORY STATS} e
|
||||
assert_match {*NOPERM*} $e
|
||||
|
||||
# Appending to the existing access string of alice.
|
||||
r ACL setuser alice -@all
|
||||
|
||||
# Now, alice can't do anything, we need to auth newuser to execute ACL GETUSER
|
||||
r AUTH newuser passwd1
|
||||
|
||||
# Validate the new commands has got engulfed to -@all.
|
||||
set cmdstr [dict get [r ACL getuser alice] commands]
|
||||
assert_equal {-@all} $cmdstr
|
||||
|
||||
r AUTH alice passwd1
|
||||
|
||||
catch {r GET key} e
|
||||
assert_match {*NOPERM*} $e
|
||||
catch {r MEMORY STATS} e
|
||||
assert_match {*NOPERM*} $e
|
||||
|
||||
# Auth newuser before the next test
|
||||
r AUTH newuser passwd1
|
||||
}
|
||||
|
||||
# Note that the order of the generated ACL rules is not stable in Redis
|
||||
# so we need to match the different parts and not as a whole string.
|
||||
test {ACL GETUSER is able to translate back command permissions} {
|
||||
@ -459,7 +564,7 @@ start_server {tags {"acl external:skip"}} {
|
||||
|
||||
test {ACL HELP should not have unexpected options} {
|
||||
catch {r ACL help xxx} e
|
||||
assert_match "*Unknown subcommand or wrong number of arguments*" $e
|
||||
assert_match "*wrong number of arguments*" $e
|
||||
}
|
||||
|
||||
test {Delete a user that the client doesn't use} {
|
||||
|
@ -51,7 +51,7 @@ start_server {tags {"info" "external:skip"}} {
|
||||
assert_equal [s total_error_replies] 0
|
||||
catch {r eval {redis.pcall('XGROUP', 'CREATECONSUMER', 's1', 'mygroup', 'consumer') return } 0} e
|
||||
assert_match {*count=1*} [errorstat ERR]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup\\|createconsumer]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdstat eval]
|
||||
|
||||
# EVAL command errors should still be pinpointed to him
|
||||
@ -83,7 +83,7 @@ start_server {tags {"info" "external:skip"}} {
|
||||
catch {r XGROUP CREATECONSUMER mystream mygroup consumer} e
|
||||
assert_match {NOGROUP*} $e
|
||||
assert_match {*count=1*} [errorstat NOGROUP]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup]
|
||||
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat xgroup\\|createconsumer]
|
||||
r config resetstat
|
||||
assert_match {} [errorstat NOGROUP]
|
||||
}
|
||||
|
@ -76,4 +76,26 @@ start_server {tags {"introspection"}} {
|
||||
assert_match {*calls=1,*} [cmdstat expire]
|
||||
assert_match {*calls=1,*} [cmdstat geoadd]
|
||||
} {} {needs:config-resetstat}
|
||||
|
||||
test {COMMAND GETKEYS GET} {
|
||||
assert_equal {key} [r command getkeys get key]
|
||||
}
|
||||
|
||||
test {COMMAND GETKEYS MEMORY USAGE} {
|
||||
assert_equal {key} [r command getkeys memory usage key]
|
||||
}
|
||||
|
||||
test {COMMAND GETKEYS XGROUP} {
|
||||
assert_equal {key} [r command getkeys xgroup create key groupname $]
|
||||
}
|
||||
|
||||
test "COMMAND LIST FILTERBY ACLCAT" {
|
||||
set reply [r command list filterby aclcat hyperloglog]
|
||||
assert_equal [lsort $reply] {pfadd pfcount pfdebug pfmerge pfselftest}
|
||||
}
|
||||
|
||||
test "COMMAND LIST FILTERBY PATTERN" {
|
||||
set reply [r command list filterby pattern pf*]
|
||||
assert_equal [lsort $reply] {pfadd pfcount pfdebug pfmerge pfselftest}
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,6 @@ start_server {tags {"latency-monitor needs:latency"}} {
|
||||
|
||||
test {LATENCY HELP should not have unexpected options} {
|
||||
catch {r LATENCY help xxx} e
|
||||
assert_match "*Unknown subcommand or wrong number of arguments*" $e
|
||||
assert_match "*wrong number of arguments*" $e
|
||||
}
|
||||
}
|
||||
|
@ -5,16 +5,22 @@ start_server {tags {"modules"}} {
|
||||
|
||||
test "Module key specs: Legacy" {
|
||||
set reply [r command info kspec.legacy]
|
||||
assert_equal $reply {{kspec.legacy -1 {} 1 2 1 {} {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}}}}
|
||||
assert_equal $reply {{kspec.legacy -1 {} 1 2 1 {} {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}}}
|
||||
}
|
||||
|
||||
test "Module key specs: Complex specs, case 1" {
|
||||
set reply [r command info kspec.complex1]
|
||||
assert_equal $reply {{kspec.complex1 -1 movablekeys 1 1 1 {} {{flags {} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type keyword spec {keyword STORE startfrom 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type keyword spec {keyword KEYS startfrom 2}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}}}}}
|
||||
assert_equal $reply {{kspec.complex1 -1 movablekeys 1 1 1 {} {{flags {} begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type keyword spec {keyword STORE startfrom 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type keyword spec {keyword KEYS startfrom 2}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}}} {}}}
|
||||
}
|
||||
|
||||
test "Module key specs: Complex specs, case 2" {
|
||||
set reply [r command info kspec.complex2]
|
||||
assert_equal $reply {{kspec.complex2 -1 movablekeys 1 2 1 {} {{flags write begin_search {type keyword spec {keyword STORE startfrom 5}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 3}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}} {flags write begin_search {type keyword spec {keyword MOREKEYS startfrom 5}} find_keys {type range spec {lastkey -1 keystep 1 limit 0}}}}}}
|
||||
assert_equal $reply {{kspec.complex2 -1 movablekeys 1 2 1 {} {{flags write begin_search {type keyword spec {keyword STORE startfrom 5}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags read begin_search {type index spec {index 2}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}} {flags write begin_search {type index spec {index 3}} find_keys {type keynum spec {keynumidx 0 firstkey 1 keystep 1}}} {flags write begin_search {type keyword spec {keyword MOREKEYS startfrom 5}} find_keys {type range spec {lastkey -1 keystep 1 limit 0}}}} {}}}
|
||||
}
|
||||
|
||||
test "Module command list filtering" {
|
||||
;# Note: we piggyback this tcl file to test the general functionality of command list filtering
|
||||
set reply [r command list filterby module keyspecs]
|
||||
assert_equal [lsort $reply] {kspec.complex1 kspec.complex2 kspec.legacy}
|
||||
}
|
||||
}
|
||||
|
19
tests/unit/moduleapi/subcommands.tcl
Normal file
19
tests/unit/moduleapi/subcommands.tcl
Normal file
@ -0,0 +1,19 @@
|
||||
set testmodule [file normalize tests/modules/subcommands.so]
|
||||
|
||||
start_server {tags {"modules"}} {
|
||||
r module load $testmodule
|
||||
|
||||
test "Module subcommands via COMMAND" {
|
||||
set reply [r command info subcommands.bitarray]
|
||||
set subcmds [lindex [lindex $reply 0] 8]
|
||||
assert_equal [lsort $subcmds] {{get -2 {} 1 1 1 {} {{flags read begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}} {set -2 {} 1 1 1 {} {{flags write begin_search {type index spec {index 1}} find_keys {type range spec {lastkey 0 keystep 1 limit 0}}}} {}}}
|
||||
}
|
||||
|
||||
test "Module pure-container command fails on arity error" {
|
||||
catch {r subcommands.bitarray} e
|
||||
assert_match {*wrong number of arguments*} $e
|
||||
|
||||
# Subcommands can be called
|
||||
assert_equal [r subcommands.bitarray get k1] {OK}
|
||||
}
|
||||
}
|
@ -712,11 +712,11 @@ start_server {tags {"stream needs:debug"} overrides {appendonly yes aof-use-rdb-
|
||||
start_server {tags {"stream"}} {
|
||||
test {XGROUP HELP should not have unexpected options} {
|
||||
catch {r XGROUP help xxx} e
|
||||
assert_match "*Unknown subcommand or wrong number of arguments*" $e
|
||||
assert_match "*wrong number of arguments*" $e
|
||||
}
|
||||
|
||||
test {XINFO HELP should not have unexpected options} {
|
||||
catch {r XINFO help xxx} e
|
||||
assert_match "*Unknown subcommand or wrong number of arguments*" $e
|
||||
assert_match "*wrong number of arguments*" $e
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user