mirror of https://mirror.osredm.com/root/redis.git
Add internal connection and command mechanism (#13740)
# PR: Add Mechanism for Internal Commands and Connections in Redis This PR introduces a mechanism to handle **internal commands and connections** in Redis. It includes enhancements for command registration, internal authentication, and observability. ## Key Features 1. **Internal Command Flag**: - Introduced a new **module command registration flag**: `internal`. - Commands marked with `internal` can only be executed by **internal connections**, AOF loading flows, and master-replica connections. - For any other connection, these commands will appear as non-existent. 2. **Support for internal authentication added to `AUTH`**: - Used by depicting the special username `internal connection` with the right internal password, i.e.,: `AUTH "internal connection" <internal_secret>`. - No user-defined ACL username can have this name, since spaces are not aloud in the ACL parser. - Allows connections to authenticate as **internal connections**. - Authenticated internal connections can execute internal commands successfully. 4. **Module API for Internal Secret**: - Added the `RedisModule_GetInternalSecret()` API, that exposes the internal secret that should be used as the password for the new `AUTH "internal connection" <password>` command. - This API enables the modules to authenticate against other shards as local connections. ## Notes on Behavior - **ACL validation**: - Commands dispatched by internal connections bypass ACL validation, to give the caller full access regardless of the user with which it is connected. - **Command Visibility**: - Internal commands **do not appear** in `COMMAND <subcommand>` and `MONITOR` for non-internal connections. - Internal commands **are logged** in the slow log, latency report and commands' statistics to maintain observability. - **`RM_Call()` Updates**: - **Non-internal connections**: - Cannot execute internal commands when the command is sent with the `C` flag (otherwise can). - Internal connections bypass ACL validations (i.e., run as the unrestricted user). - **Internal commands' success**: - Internal commands succeed upon being sent from either an internal connection (i.e., authenticated via the new `AUTH "internal connection" <internal_secret>` API), an AOF loading process, or from a master via the replication link. Any other connections that attempt to execute an internal command fail with the `unknown command` error message raised. - **`CLIENT LIST` flags**: - Added the `I` flag, to indicate that the connection is internal. - **Lua Scripts**: - Prevented internal commands from being executed via Lua scripts. --------- Co-authored-by: Meir Shpilraien <meir@redis.com>
This commit is contained in:
parent
09f8a2f374
commit
04589f90d7
|
@ -56,4 +56,5 @@ $TCLSH tests/test_helper.tcl \
|
|||
--single unit/moduleapi/moduleauth \
|
||||
--single unit/moduleapi/rdbloadsave \
|
||||
--single unit/moduleapi/crash \
|
||||
--single unit/moduleapi/internalsecret \
|
||||
"${@}"
|
||||
|
|
41
src/acl.c
41
src/acl.c
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
#include "server.h"
|
||||
#include "cluster.h"
|
||||
#include "sha256.h"
|
||||
#include <fcntl.h>
|
||||
#include <ctype.h>
|
||||
|
@ -3194,6 +3195,38 @@ void addReplyCommandCategories(client *c, struct redisCommand *cmd) {
|
|||
setDeferredSetLen(c, flaglen, flagcount);
|
||||
}
|
||||
|
||||
/* When successful, initiates an internal connection, that is able to execute
|
||||
* internal commands (see CMD_INTERNAL). */
|
||||
static void internalAuth(client *c) {
|
||||
if (server.cluster == NULL) {
|
||||
addReplyError(c, "Cannot authenticate as an internal connection on non-cluster instances");
|
||||
return;
|
||||
}
|
||||
|
||||
sds password = c->argv[2]->ptr;
|
||||
|
||||
/* Get internal secret. */
|
||||
size_t len = -1;
|
||||
const char *internal_secret = clusterGetSecret(&len);
|
||||
if (sdslen(password) != len) {
|
||||
addReplyError(c, "-WRONGPASS invalid internal password");
|
||||
return;
|
||||
}
|
||||
if (!time_independent_strcmp((char *)internal_secret, (char *)password, len)) {
|
||||
c->flags |= CLIENT_INTERNAL;
|
||||
/* No further authentication is needed. */
|
||||
c->authenticated = 1;
|
||||
/* Set the user to the unrestricted user, if it is not already set (default). */
|
||||
if (c->user != NULL) {
|
||||
c->user = NULL;
|
||||
moduleNotifyUserChanged(c);
|
||||
}
|
||||
addReply(c, shared.ok);
|
||||
} else {
|
||||
addReplyError(c, "-WRONGPASS invalid internal password");
|
||||
}
|
||||
}
|
||||
|
||||
/* AUTH <password>
|
||||
* AUTH <username> <password> (Redis >= 6.0 form)
|
||||
*
|
||||
|
@ -3227,6 +3260,14 @@ void authCommand(client *c) {
|
|||
username = c->argv[1];
|
||||
password = c->argv[2];
|
||||
redactClientCommandArgument(c, 2);
|
||||
|
||||
/* Handle internal authentication commands.
|
||||
* Note: No user-defined ACL user can have this username (no spaces
|
||||
* allowed), thus no conflicts with ACL possible. */
|
||||
if (!strcmp(username->ptr, "internal connection")) {
|
||||
internalAuth(c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
robj *err = NULL;
|
||||
|
|
13
src/debug.c
13
src/debug.c
|
@ -494,6 +494,8 @@ void debugCommand(client *c) {
|
|||
" Enable or disable the main dict and expire dict resizing.",
|
||||
"SCRIPT <LIST|<sha>>",
|
||||
" Output SHA and content of all scripts or of a specific script with its SHA.",
|
||||
"MARK-INTERNAL-CLIENT [UNMARK]",
|
||||
" Promote the current connection to an internal connection.",
|
||||
NULL
|
||||
};
|
||||
addExtendedReplyHelp(c, help, clusterDebugCommandExtendedHelp());
|
||||
|
@ -1074,6 +1076,17 @@ NULL
|
|||
return;
|
||||
}
|
||||
addReply(c,shared.ok);
|
||||
} else if(!strcasecmp(c->argv[1]->ptr,"mark-internal-client") && c->argc < 4) {
|
||||
if (c->argc == 2) {
|
||||
c->flags |= CLIENT_INTERNAL;
|
||||
addReply(c, shared.ok);
|
||||
} else if (c->argc == 3 && !strcasecmp(c->argv[2]->ptr, "unmark")) {
|
||||
c->flags &= ~CLIENT_INTERNAL;
|
||||
addReply(c, shared.ok);
|
||||
} else {
|
||||
addReplySubcommandSyntaxError(c);
|
||||
return;
|
||||
}
|
||||
} else if(!handleDebugClusterCommand(c)) {
|
||||
addReplySubcommandSyntaxError(c);
|
||||
return;
|
||||
|
|
39
src/module.c
39
src/module.c
|
@ -1151,6 +1151,7 @@ int64_t commandFlagsFromString(char *s) {
|
|||
else if (!strcasecmp(t,"no-cluster")) flags |= CMD_MODULE_NO_CLUSTER;
|
||||
else if (!strcasecmp(t,"no-mandatory-keys")) flags |= CMD_NO_MANDATORY_KEYS;
|
||||
else if (!strcasecmp(t,"allow-busy")) flags |= CMD_ALLOW_BUSY;
|
||||
else if (!strcasecmp(t, "internal")) flags |= (CMD_INTERNAL|CMD_NOSCRIPT); /* We also disallow internal commands in scripts. */
|
||||
else break;
|
||||
}
|
||||
sdsfreesplitres(tokens,count);
|
||||
|
@ -1235,6 +1236,9 @@ RedisModuleCommand *moduleCreateCommandProxy(struct RedisModule *module, sds dec
|
|||
* RM_Yield.
|
||||
* * **"getchannels-api"**: The command implements the interface to return
|
||||
* the arguments that are channels.
|
||||
* * **"internal"**: Internal command, one that should not be exposed to the user connections.
|
||||
* For example, module commands that are called by the modules,
|
||||
* commands that do not perform ACL validations (relying on earlier checks)
|
||||
*
|
||||
* The last three parameters specify which arguments of the new command are
|
||||
* Redis keys. See https://redis.io/commands/command for more information.
|
||||
|
@ -6295,6 +6299,8 @@ fmterr:
|
|||
* dependent activity, such as ACL checks within scripts will proceed as
|
||||
* expected.
|
||||
* Otherwise, the command will run as the Redis unrestricted user.
|
||||
* Upon sending a command from an internal connection, this flag is
|
||||
* ignored and the command will run as the Redis unrestricted user.
|
||||
* * `S` -- Run the command in a script mode, this means that it will raise
|
||||
* an error if a command which are not allowed inside a script
|
||||
* (flagged with the `deny-script` flag) is invoked (like SHUTDOWN).
|
||||
|
@ -6403,8 +6409,12 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
|||
}
|
||||
if (ctx->module) ctx->module->in_call++;
|
||||
|
||||
/* Attach the user of the context or client.
|
||||
* Internal connections always run with the unrestricted user. */
|
||||
user *user = NULL;
|
||||
if (flags & REDISMODULE_ARGV_RUN_AS_USER) {
|
||||
if ((flags & REDISMODULE_ARGV_RUN_AS_USER) &&
|
||||
!(ctx->client->flags & CLIENT_INTERNAL))
|
||||
{
|
||||
user = ctx->user ? ctx->user->user : ctx->client->user;
|
||||
if (!user) {
|
||||
errno = ENOTSUP;
|
||||
|
@ -6435,6 +6445,17 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
|||
* if necessary.
|
||||
*/
|
||||
c->cmd = c->lastcmd = c->realcmd = lookupCommand(c->argv,c->argc);
|
||||
|
||||
/* We nullify the command if it is not supposed to be seen by the client,
|
||||
* such that it will be rejected like an unknown command. */
|
||||
if (c->cmd &&
|
||||
(c->cmd->flags & CMD_INTERNAL) &&
|
||||
(flags & REDISMODULE_ARGV_RUN_AS_USER) &&
|
||||
!((ctx->client->flags & CLIENT_INTERNAL) || mustObeyClient(ctx->client)))
|
||||
{
|
||||
c->cmd = c->lastcmd = c->realcmd = NULL;
|
||||
}
|
||||
|
||||
sds err;
|
||||
if (!commandCheckExistence(c, error_as_call_replies? &err : NULL)) {
|
||||
errno = ENOENT;
|
||||
|
@ -6555,7 +6576,9 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
|||
*
|
||||
* If RM_SetContextUser has set a user, that user is used, otherwise
|
||||
* use the attached client's user. If there is no attached client user and no manually
|
||||
* set user, an error will be returned */
|
||||
* set user, an error will be returned.
|
||||
* An internal command should only succeed for an internal connection, AOF,
|
||||
* and master commands. */
|
||||
if (flags & REDISMODULE_ARGV_RUN_AS_USER) {
|
||||
int acl_errpos;
|
||||
int acl_retval;
|
||||
|
@ -13389,6 +13412,17 @@ int RM_RdbSave(RedisModuleCtx *ctx, RedisModuleRdbStream *stream, int flags) {
|
|||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
/* Returns the internal secret of the cluster.
|
||||
* Should be used to authenticate as an internal connection to a node in the
|
||||
* cluster, and by that gain the permissions to execute internal commands.
|
||||
*/
|
||||
const char* RM_GetInternalSecret(RedisModuleCtx *ctx, size_t *len) {
|
||||
UNUSED(ctx);
|
||||
serverAssert(len != NULL);
|
||||
const char *secret = clusterGetSecret(len);
|
||||
return secret;
|
||||
}
|
||||
|
||||
/* Redis MODULE command.
|
||||
*
|
||||
* MODULE LIST
|
||||
|
@ -14330,4 +14364,5 @@ void moduleRegisterCoreAPI(void) {
|
|||
REGISTER_API(RdbStreamFree);
|
||||
REGISTER_API(RdbLoad);
|
||||
REGISTER_API(RdbSave);
|
||||
REGISTER_API(GetInternalSecret);
|
||||
}
|
||||
|
|
|
@ -3099,6 +3099,7 @@ sds catClientInfoString(sds s, client *client) {
|
|||
if (client->flags & CLIENT_NO_EVICT) *p++ = 'e';
|
||||
if (client->flags & CLIENT_NO_TOUCH) *p++ = 'T';
|
||||
if (client->flags & CLIENT_REPL_RDB_CHANNEL) *p++ = 'C';
|
||||
if (client->flags & CLIENT_INTERNAL) *p++ = 'I';
|
||||
if (p == flags) *p++ = 'N';
|
||||
*p++ = '\0';
|
||||
|
||||
|
|
|
@ -1327,6 +1327,7 @@ REDISMODULE_API RedisModuleRdbStream *(*RedisModule_RdbStreamCreateFromFile)(con
|
|||
REDISMODULE_API void (*RedisModule_RdbStreamFree)(RedisModuleRdbStream *stream) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_RdbLoad)(RedisModuleCtx *ctx, RedisModuleRdbStream *stream, int flags) REDISMODULE_ATTR;
|
||||
REDISMODULE_API int (*RedisModule_RdbSave)(RedisModuleCtx *ctx, RedisModuleRdbStream *stream, int flags) REDISMODULE_ATTR;
|
||||
REDISMODULE_API const char * (*RedisModule_GetInternalSecret)(RedisModuleCtx *ctx, size_t *len) REDISMODULE_ATTR;
|
||||
|
||||
#define RedisModule_IsAOFClient(id) ((id) == UINT64_MAX)
|
||||
|
||||
|
@ -1698,6 +1699,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
|||
REDISMODULE_GET_API(RdbStreamFree);
|
||||
REDISMODULE_GET_API(RdbLoad);
|
||||
REDISMODULE_GET_API(RdbSave);
|
||||
REDISMODULE_GET_API(GetInternalSecret);
|
||||
|
||||
if (RedisModule_IsModuleNameBusy && RedisModule_IsModuleNameBusy(name)) return REDISMODULE_ERR;
|
||||
RedisModule_SetModuleAttribs(ctx,name,ver,apiver);
|
||||
|
|
|
@ -660,6 +660,10 @@ void replicationFeedMonitors(client *c, list *monitors, int dictid, robj **argv,
|
|||
listRewind(monitors,&li);
|
||||
while((ln = listNext(&li))) {
|
||||
client *monitor = ln->value;
|
||||
/* Do not show internal commands to non-internal clients. */
|
||||
if (c->realcmd && (c->realcmd->flags & CMD_INTERNAL) && !(monitor->flags & CLIENT_INTERNAL)) {
|
||||
continue;
|
||||
}
|
||||
addReply(monitor,cmdobj);
|
||||
updateClientMemUsageAndBucket(monitor);
|
||||
}
|
||||
|
|
80
src/server.c
80
src/server.c
|
@ -3562,6 +3562,11 @@ int incrCommandStatsOnError(struct redisCommand *cmd, int flags) {
|
|||
return res;
|
||||
}
|
||||
|
||||
/* Returns true if the command is not internal, or the connection is internal. */
|
||||
static bool commandVisibleForClient(client *c, struct redisCommand *cmd) {
|
||||
return (!(cmd->flags & CMD_INTERNAL)) || (c->flags & CLIENT_INTERNAL);
|
||||
}
|
||||
|
||||
/* Call() is the core of Redis execution of a command.
|
||||
*
|
||||
* The following flags can be passed:
|
||||
|
@ -3713,7 +3718,8 @@ void call(client *c, int flags) {
|
|||
* Other exceptions is a client which is unblocked and retrying to process the command
|
||||
* or we are currently in the process of loading AOF. */
|
||||
if (update_command_stats && !reprocessing_command &&
|
||||
!(c->cmd->flags & (CMD_SKIP_MONITOR|CMD_ADMIN))) {
|
||||
!(c->cmd->flags & (CMD_SKIP_MONITOR|CMD_ADMIN)))
|
||||
{
|
||||
robj **argv = c->original_argv ? c->original_argv : c->argv;
|
||||
int argc = c->original_argv ? c->original_argc : c->argc;
|
||||
replicationFeedMonitors(c,server.monitors,c->db->id,argv,argc);
|
||||
|
@ -3992,6 +3998,7 @@ int processCommand(client *c) {
|
|||
* we do not have to repeat the same checks */
|
||||
if (!client_reprocessing_command) {
|
||||
struct redisCommand *cmd = c->iolookedcmd ? c->iolookedcmd : lookupCommand(c->argv, c->argc);
|
||||
|
||||
if (!cmd) {
|
||||
/* Handle possible security attacks. */
|
||||
if (!strcasecmp(c->argv[0]->ptr,"host:") || !strcasecmp(c->argv[0]->ptr,"post")) {
|
||||
|
@ -3999,6 +4006,13 @@ int processCommand(client *c) {
|
|||
return C_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
/* Internal commands seem unexistent to non-internal connections.
|
||||
* masters and AOF loads are implicitly internal. */
|
||||
if (cmd && (cmd->flags & CMD_INTERNAL) && !((c->flags & CLIENT_INTERNAL) || mustObeyClient(c))) {
|
||||
cmd = NULL;
|
||||
}
|
||||
|
||||
c->cmd = c->lastcmd = c->realcmd = cmd;
|
||||
sds err;
|
||||
if (!commandCheckExistence(c, &err)) {
|
||||
|
@ -5019,7 +5033,7 @@ void addReplyCommandKeySpecs(client *c, struct redisCommand *cmd) {
|
|||
|
||||
/* Reply with an array of sub-command using the provided reply callback. */
|
||||
void addReplyCommandSubCommands(client *c, struct redisCommand *cmd, void (*reply_function)(client*, struct redisCommand*), int use_map) {
|
||||
if (!cmd->subcommands_dict) {
|
||||
if (!cmd->subcommands_dict || !commandVisibleForClient(c, cmd)) {
|
||||
addReplySetLen(c, 0);
|
||||
return;
|
||||
}
|
||||
|
@ -5041,7 +5055,7 @@ void addReplyCommandSubCommands(client *c, struct redisCommand *cmd, void (*repl
|
|||
|
||||
/* Output the representation of a Redis command. Used by the COMMAND command and COMMAND INFO. */
|
||||
void addReplyCommandInfo(client *c, struct redisCommand *cmd) {
|
||||
if (!cmd) {
|
||||
if (!cmd || !commandVisibleForClient(c, cmd)) {
|
||||
addReplyNull(c);
|
||||
} else {
|
||||
int firstkey = 0, lastkey = 0, keystep = 0;
|
||||
|
@ -5145,7 +5159,7 @@ void getKeysSubcommandImpl(client *c, int with_flags) {
|
|||
getKeysResult result = GETKEYS_RESULT_INIT;
|
||||
int j;
|
||||
|
||||
if (!cmd) {
|
||||
if (!cmd || !commandVisibleForClient(c, cmd)) {
|
||||
addReplyError(c,"Invalid command specified");
|
||||
return;
|
||||
} else if (!doesCommandHaveKeys(cmd)) {
|
||||
|
@ -5189,22 +5203,39 @@ void getKeysSubcommand(client *c) {
|
|||
getKeysSubcommandImpl(c, 0);
|
||||
}
|
||||
|
||||
/* COMMAND (no args) */
|
||||
void commandCommand(client *c) {
|
||||
void genericCommandCommand(client *c, int count_only) {
|
||||
dictIterator *di;
|
||||
dictEntry *de;
|
||||
void *len = NULL;
|
||||
int count = 0;
|
||||
|
||||
if (!count_only)
|
||||
len = addReplyDeferredLen(c);
|
||||
|
||||
addReplyArrayLen(c, dictSize(server.commands));
|
||||
di = dictGetIterator(server.commands);
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
addReplyCommandInfo(c, dictGetVal(de));
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
if (!commandVisibleForClient(c, cmd))
|
||||
continue;
|
||||
if (!count_only)
|
||||
addReplyCommandInfo(c, dictGetVal(de));
|
||||
count++;
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
if (count_only)
|
||||
addReplyLongLong(c, count);
|
||||
else
|
||||
setDeferredArrayLen(c, len, count);
|
||||
}
|
||||
|
||||
/* COMMAND (no args) */
|
||||
void commandCommand(client *c) {
|
||||
genericCommandCommand(c, 0);
|
||||
}
|
||||
|
||||
/* COMMAND COUNT */
|
||||
void commandCountCommand(client *c) {
|
||||
addReplyLongLong(c, dictSize(server.commands));
|
||||
genericCommandCommand(c, 1);
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
|
@ -5258,7 +5289,7 @@ void commandListWithFilter(client *c, dict *commands, commandListFilter filter,
|
|||
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
if (!shouldFilterFromCommandList(cmd,&filter)) {
|
||||
if (commandVisibleForClient(c, cmd) && !shouldFilterFromCommandList(cmd,&filter)) {
|
||||
addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
|
||||
(*numcmds)++;
|
||||
}
|
||||
|
@ -5277,8 +5308,10 @@ void commandListWithoutFilter(client *c, dict *commands, int *numcmds) {
|
|||
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
|
||||
(*numcmds)++;
|
||||
if (commandVisibleForClient(c, cmd)) {
|
||||
addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
|
||||
(*numcmds)++;
|
||||
}
|
||||
|
||||
if (cmd->subcommands_dict) {
|
||||
commandListWithoutFilter(c, cmd->subcommands_dict, numcmds);
|
||||
|
@ -5334,14 +5367,7 @@ void commandInfoCommand(client *c) {
|
|||
int i;
|
||||
|
||||
if (c->argc == 2) {
|
||||
dictIterator *di;
|
||||
dictEntry *de;
|
||||
addReplyArrayLen(c, dictSize(server.commands));
|
||||
di = dictGetIterator(server.commands);
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
addReplyCommandInfo(c, dictGetVal(de));
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
genericCommandCommand(c, 0);
|
||||
} else {
|
||||
addReplyArrayLen(c, c->argc-2);
|
||||
for (i = 2; i < c->argc; i++) {
|
||||
|
@ -5353,25 +5379,29 @@ void commandInfoCommand(client *c) {
|
|||
/* COMMAND DOCS [command-name [command-name ...]] */
|
||||
void commandDocsCommand(client *c) {
|
||||
int i;
|
||||
int numcmds = 0;
|
||||
if (c->argc == 2) {
|
||||
/* Reply with an array of all commands */
|
||||
dictIterator *di;
|
||||
dictEntry *de;
|
||||
addReplyMapLen(c, dictSize(server.commands));
|
||||
void *replylen = addReplyDeferredLen(c);
|
||||
di = dictGetIterator(server.commands);
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
struct redisCommand *cmd = dictGetVal(de);
|
||||
addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
|
||||
addReplyCommandDocs(c, cmd);
|
||||
if (commandVisibleForClient(c, cmd)) {
|
||||
addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
|
||||
addReplyCommandDocs(c, cmd);
|
||||
numcmds++;
|
||||
}
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
setDeferredMapLen(c,replylen,numcmds);
|
||||
} else {
|
||||
/* Reply with an array of the requested commands (if we find them) */
|
||||
int numcmds = 0;
|
||||
void *replylen = addReplyDeferredLen(c);
|
||||
for (i = 2; i < c->argc; i++) {
|
||||
struct redisCommand *cmd = lookupCommandBySds(c->argv[i]->ptr);
|
||||
if (!cmd)
|
||||
if (!cmd || !commandVisibleForClient(c, cmd))
|
||||
continue;
|
||||
addReplyBulkCBuffer(c, cmd->fullname, sdslen(cmd->fullname));
|
||||
addReplyCommandDocs(c, cmd);
|
||||
|
|
|
@ -225,6 +225,7 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
|
|||
#define CMD_ALLOW_BUSY ((1ULL<<26))
|
||||
#define CMD_MODULE_GETCHANNELS (1ULL<<27) /* Use the modules getchannels interface. */
|
||||
#define CMD_TOUCHES_ARBITRARY_KEYS (1ULL<<28)
|
||||
#define CMD_INTERNAL (1ULL<<29) /* Internal command. */
|
||||
|
||||
/* Command flags that describe ACLs categories. */
|
||||
#define ACL_CATEGORY_KEYSPACE (1ULL<<0)
|
||||
|
@ -396,6 +397,7 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
|
|||
#define CLIENT_MODULE_PREVENT_REPL_PROP (1ULL<<49) /* Module client do not want to propagate to replica */
|
||||
#define CLIENT_REPROCESSING_COMMAND (1ULL<<50) /* The client is re-processing the command. */
|
||||
#define CLIENT_REPL_RDB_CHANNEL (1ULL<<51) /* Client which is used for rdb delivery as part of rdb channel replication */
|
||||
#define CLIENT_INTERNAL (1ULL<<52) /* Internal client connection */
|
||||
|
||||
/* Any flag that does not let optimize FLUSH SYNC to run it in bg as blocking client ASYNC */
|
||||
#define CLIENT_AVOID_BLOCKING_ASYNC_FLUSH (CLIENT_DENY_BLOCKING|CLIENT_MULTI|CLIENT_LUA_DEBUG|CLIENT_LUA_DEBUG_SYNC|CLIENT_MODULE)
|
||||
|
@ -2429,6 +2431,9 @@ typedef int redisGetKeysProc(struct redisCommand *cmd, robj **argv, int argc, ge
|
|||
* CMD_TOUCHES_ARBITRARY_KEYS: The command may touch (and cause lazy-expire)
|
||||
* arbitrary key (i.e not provided in argv)
|
||||
*
|
||||
* CMD_INTERNAL: The command may perform operations without performing
|
||||
* validations such as ACL.
|
||||
*
|
||||
* The following additional flags are only used in order to put commands
|
||||
* in a specific ACL category. Commands can have multiple ACL categories.
|
||||
* See redis.conf for the exact meaning of each.
|
||||
|
|
|
@ -63,7 +63,8 @@ TEST_MODULES = \
|
|||
postnotifications.so \
|
||||
moduleauthtwo.so \
|
||||
rdbloadsave.so \
|
||||
crash.so
|
||||
crash.so \
|
||||
internalsecret.so
|
||||
|
||||
.PHONY: all
|
||||
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
#include "redismodule.h"
|
||||
#include <errno.h>
|
||||
|
||||
int InternalAuth_GetInternalSecret(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
/* NOTE: The internal secret SHOULD NOT be exposed by any module. This is
|
||||
done for testing purposes only. */
|
||||
size_t len;
|
||||
const char *secret = RedisModule_GetInternalSecret(ctx, &len);
|
||||
if(secret) {
|
||||
RedisModule_ReplyWithStringBuffer(ctx, secret, len);
|
||||
} else {
|
||||
RedisModule_ReplyWithError(ctx, "ERR no internal secret available");
|
||||
}
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
int InternalAuth_InternalCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
RM_CALL_REGULAR = 0,
|
||||
RM_CALL_WITHUSER = 1,
|
||||
RM_CALL_WITHDETACHEDCLIENT = 2,
|
||||
RM_CALL_REPLICATED = 3
|
||||
} RMCallMode;
|
||||
|
||||
int call_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc, RMCallMode mode) {
|
||||
if(argc < 2){
|
||||
return RedisModule_WrongArity(ctx);
|
||||
}
|
||||
RedisModuleCallReply *rep = NULL;
|
||||
RedisModuleCtx *detached_ctx = NULL;
|
||||
const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
|
||||
|
||||
switch (mode) {
|
||||
case RM_CALL_REGULAR:
|
||||
// Regular call, with the unrestricted user.
|
||||
rep = RedisModule_Call(ctx, cmd, "vE", argv + 2, argc - 2);
|
||||
break;
|
||||
case RM_CALL_WITHUSER:
|
||||
// Simply call the command with the current client.
|
||||
rep = RedisModule_Call(ctx, cmd, "vCE", argv + 2, argc - 2);
|
||||
break;
|
||||
case RM_CALL_WITHDETACHEDCLIENT:
|
||||
// Use a context created with the thread-safe-context API
|
||||
detached_ctx = RedisModule_GetThreadSafeContext(NULL);
|
||||
if(!detached_ctx){
|
||||
RedisModule_ReplyWithError(ctx, "ERR failed to create detached context");
|
||||
return REDISMODULE_ERR;
|
||||
}
|
||||
// Dispatch the command with the detached context
|
||||
rep = RedisModule_Call(detached_ctx, cmd, "vCE", argv + 2, argc - 2);
|
||||
break;
|
||||
case RM_CALL_REPLICATED:
|
||||
rep = RedisModule_Call(ctx, cmd, "vE", argv + 2, argc - 2);
|
||||
}
|
||||
|
||||
if(!rep) {
|
||||
char err[100];
|
||||
switch (errno) {
|
||||
case EACCES:
|
||||
RedisModule_ReplyWithError(ctx, "ERR NOPERM");
|
||||
break;
|
||||
case ENOENT:
|
||||
RedisModule_ReplyWithError(ctx, "ERR unknown command");
|
||||
break;
|
||||
default:
|
||||
snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno);
|
||||
RedisModule_ReplyWithError(ctx, err);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
RedisModule_ReplyWithCallReply(ctx, rep);
|
||||
RedisModule_FreeCallReply(rep);
|
||||
if (mode == RM_CALL_REPLICATED)
|
||||
RedisModule_ReplicateVerbatim(ctx);
|
||||
}
|
||||
|
||||
if (mode == RM_CALL_WITHDETACHEDCLIENT) {
|
||||
RedisModule_FreeThreadSafeContext(detached_ctx);
|
||||
}
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
||||
|
||||
int internal_rmcall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
return call_rm_call(ctx, argv, argc, RM_CALL_REGULAR);
|
||||
}
|
||||
|
||||
int noninternal_rmcall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
return call_rm_call(ctx, argv, argc, RM_CALL_REGULAR);
|
||||
}
|
||||
|
||||
int noninternal_rmcall_withuser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
return call_rm_call(ctx, argv, argc, RM_CALL_WITHUSER);
|
||||
}
|
||||
|
||||
int noninternal_rmcall_detachedcontext_withuser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
return call_rm_call(ctx, argv, argc, RM_CALL_WITHDETACHEDCLIENT);
|
||||
}
|
||||
|
||||
int internal_rmcall_replicated(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
return call_rm_call(ctx, argv, argc, RM_CALL_REPLICATED);
|
||||
}
|
||||
|
||||
/* This function must be present on each Redis module. It is used in order to
|
||||
* register the commands into the Redis server. */
|
||||
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||
REDISMODULE_NOT_USED(argv);
|
||||
REDISMODULE_NOT_USED(argc);
|
||||
|
||||
if (RedisModule_Init(ctx,"testinternalsecret",1,REDISMODULE_APIVER_1)
|
||||
== REDISMODULE_ERR) return REDISMODULE_ERR;
|
||||
|
||||
/* WARNING: A module should NEVER expose the internal secret - this is for
|
||||
* testing purposes only. */
|
||||
if (RedisModule_CreateCommand(ctx,"internalauth.getinternalsecret",
|
||||
InternalAuth_GetInternalSecret,"",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"internalauth.internalcommand",
|
||||
InternalAuth_InternalCommand,"internal",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"internalauth.internal_rmcall",
|
||||
internal_rmcall,"write internal",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall",
|
||||
noninternal_rmcall,"write",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall_withuser",
|
||||
noninternal_rmcall_withuser,"write",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall_detachedcontext_withuser",
|
||||
noninternal_rmcall_detachedcontext_withuser,"write",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
if (RedisModule_CreateCommand(ctx,"internalauth.internal_rmcall_replicated",
|
||||
internal_rmcall_replicated,"write internal",0,0,0) == REDISMODULE_ERR)
|
||||
return REDISMODULE_ERR;
|
||||
|
||||
return REDISMODULE_OK;
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
tags {modules} {
|
||||
set testmodule [file normalize tests/modules/internalsecret.so]
|
||||
|
||||
set modules [list loadmodule $testmodule]
|
||||
start_cluster 1 0 [list config_lines $modules] {
|
||||
set r [srv 0 client]
|
||||
|
||||
test {Internal command without internal connection fails as an unknown command} {
|
||||
assert_error {*unknown command*with args beginning with:*} {r internalauth.internalcommand}
|
||||
}
|
||||
|
||||
test {Wrong internalsecret fails authentication} {
|
||||
assert_error {*WRONGPASS invalid internal password*} {r auth "internal connection" 123}
|
||||
}
|
||||
|
||||
test {Internal connection basic flow} {
|
||||
# A non-internal connection cannot execute internal commands, and they
|
||||
# seem non-existent to it.
|
||||
assert_error {*unknown command*} {r internalauth.internalcommand}
|
||||
|
||||
# Authenticate as an internal connection
|
||||
assert_equal {OK} [r debug mark-internal-client]
|
||||
|
||||
# Now, internal commands are available.
|
||||
assert_equal {OK} [r internalauth.internalcommand]
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
r module load $testmodule
|
||||
|
||||
test {Internal secret is not available in non-cluster mode} {
|
||||
# On non-cluster mode, the internal secret does not exist, nor is the
|
||||
# internal auth command available
|
||||
assert_error {*unknown command*} {r internalauth.internalcommand}
|
||||
assert_error {*ERR no internal secret available*} {r internalauth.getinternalsecret}
|
||||
assert_error {*Cannot authenticate as an internal connection on non-cluster instances*} {r auth "internal connection" somepassword}
|
||||
}
|
||||
|
||||
test {marking and un-marking a connection as internal via a debug command} {
|
||||
# After marking the connection to an internal one via a debug command,
|
||||
# internal commands succeed.
|
||||
r debug mark-internal-client
|
||||
assert_equal {OK} [r internalauth.internalcommand]
|
||||
|
||||
# After unmarking the connection, internal commands fail.
|
||||
r debug mark-internal-client unmark
|
||||
assert_error {*unknown command*} {r internalauth.internalcommand}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
r module load $testmodule
|
||||
|
||||
test {Test `COMMAND *` commands with\without internal connections} {
|
||||
# ------------------ Non-internal connection ------------------
|
||||
# `COMMAND DOCS <cmd>` returns empty response.
|
||||
assert_equal {} [r command docs internalauth.internalcommand]
|
||||
|
||||
# `COMMAND INFO <cmd>` should reply with null for the internal command
|
||||
assert_equal {{}} [r command info internalauth.internalcommand]
|
||||
|
||||
# `COMMAND GETKEYS/GETKEYSANDFLAGS <cmd> <args>` returns an invalid command error
|
||||
assert_error {*Invalid command specified*} {r command getkeys internalauth.internalcommand}
|
||||
assert_error {*Invalid command specified*} {r command getkeysandflags internalauth.internalcommand}
|
||||
|
||||
# -------------------- Internal connection --------------------
|
||||
# Non-empty response for non-internal connections.
|
||||
assert_equal {OK} [r debug mark-internal-client]
|
||||
|
||||
# `COMMAND DOCS <cmd>` returns a correct response.
|
||||
assert_match {*internalauth.internalcommand*} [r command docs internalauth.internalcommand]
|
||||
|
||||
# `COMMAND INFO <cmd>` should reply with a full response for the internal command
|
||||
assert_match {*internalauth.internalcommand*} [r command info internalauth.internalcommand]
|
||||
|
||||
# `COMMAND GETKEYS/GETKEYSANDFLAGS <cmd> <args>` returns a key error (not related to the internal connection)
|
||||
assert_error {*ERR The command has no key arguments*} {r command getkeys internalauth.internalcommand}
|
||||
assert_error {*ERR The command has no key arguments*} {r command getkeysandflags internalauth.internalcommand}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
r module load $testmodule
|
||||
|
||||
test {No authentication needed for internal connections} {
|
||||
# Authenticate with a user that does not have permissions to any command
|
||||
r acl setuser David on >123 &* ~* -@all +auth +internalauth.getinternalsecret +debug +internalauth.internalcommand
|
||||
assert_equal {OK} [r auth David 123]
|
||||
|
||||
assert_equal {OK} [r debug mark-internal-client]
|
||||
# Execute a command for which David does not have permission
|
||||
assert_equal {OK} [r internalauth.internalcommand]
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
r module load $testmodule
|
||||
|
||||
test {RM_Call of internal commands without user-flag succeeds only for all connections} {
|
||||
# Fail before authenticating as an internal connection.
|
||||
assert_equal {OK} [r internalauth.noninternal_rmcall internalauth.internalcommand]
|
||||
}
|
||||
|
||||
test {Internal commands via RM_Call succeeds for non-internal connections depending on the user flag} {
|
||||
# A non-internal connection that calls rm_call of an internal command
|
||||
assert_equal {OK} [r internalauth.noninternal_rmcall internalauth.internalcommand]
|
||||
|
||||
# A non-internal connection that calls rm_call of an internal command
|
||||
# with a user flag should fail.
|
||||
assert_error {*unknown command*} {r internalauth.noninternal_rmcall_withuser internalauth.internalcommand}
|
||||
}
|
||||
|
||||
test {Internal connections override the user flag} {
|
||||
# Authenticate as an internal connection
|
||||
assert_equal {OK} [r debug mark-internal-client]
|
||||
|
||||
assert_equal {OK} [r internalauth.noninternal_rmcall internalauth.internalcommand]
|
||||
assert_equal {OK} [r internalauth.noninternal_rmcall_withuser internalauth.internalcommand]
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
r module load $testmodule
|
||||
|
||||
test {RM_Call with the user-flag after setting thread-safe-context from an internal connection should fail} {
|
||||
# Authenticate as an internal connection
|
||||
assert_equal {OK} [r debug mark-internal-client]
|
||||
|
||||
# New threadSafeContexts do not inherit the internal flag.
|
||||
assert_error {*unknown command*} {r internalauth.noninternal_rmcall_detachedcontext_withuser internalauth.internalcommand}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
r module load $testmodule
|
||||
|
||||
r config set appendonly yes
|
||||
r config set appendfsync always
|
||||
waitForBgrewriteaof r
|
||||
|
||||
test {AOF executes internal commands successfully} {
|
||||
# Authenticate as an internal connection
|
||||
assert_equal {OK} [r debug mark-internal-client]
|
||||
|
||||
# Call an internal writing command
|
||||
assert_equal {OK} [r internalauth.internal_rmcall_replicated set x 5]
|
||||
|
||||
# Reload the server from the AOF
|
||||
r debug loadaof
|
||||
|
||||
# Check if the internal command was executed successfully
|
||||
assert_equal {5} [r get x]
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
r module load $testmodule
|
||||
|
||||
test {Internal commands are not allowed from scripts} {
|
||||
# Internal commands are not allowed from scripts
|
||||
assert_error {*not allowed from script*} {r eval {redis.call('internalauth.internalcommand')} 0}
|
||||
|
||||
# Even after authenticating as an internal connection
|
||||
assert_equal {OK} [r debug mark-internal-client]
|
||||
assert_error {*not allowed from script*} {r eval {redis.call('internalauth.internalcommand')} 0}
|
||||
}
|
||||
}
|
||||
|
||||
start_cluster 1 1 [list config_lines $modules] {
|
||||
set master [srv 0 client]
|
||||
set slave [srv -1 client]
|
||||
|
||||
test {Setup master} {
|
||||
# Authenticate as an internal connection
|
||||
set reply [$master internalauth.getinternalsecret]
|
||||
assert_equal {OK} [$master auth "internal connection" $reply]
|
||||
}
|
||||
|
||||
test {Slaves successfully execute internal commands from the replication link} {
|
||||
assert {[s -1 role] eq {slave}}
|
||||
wait_for_condition 1000 50 {
|
||||
[s -1 master_link_status] eq {up}
|
||||
} else {
|
||||
fail "Master link status is not up"
|
||||
}
|
||||
|
||||
# Execute internal command in master, that will set `x` to `5`.
|
||||
assert_equal {OK} [$master internalauth.internal_rmcall_replicated set x 5]
|
||||
|
||||
# Wait for replica to have the key
|
||||
$slave readonly
|
||||
wait_for_condition 1000 50 {
|
||||
[$slave exists x] eq "1"
|
||||
} else {
|
||||
fail "Test key was not replicated"
|
||||
}
|
||||
|
||||
# See that the slave has the same value for `x`.
|
||||
assert_equal {5} [$slave get x]
|
||||
}
|
||||
}
|
||||
|
||||
start_server {} {
|
||||
r module load $testmodule
|
||||
|
||||
test {Internal commands are not reported in the monitor output for non-internal connections when unsuccessful} {
|
||||
set rd [redis_deferring_client]
|
||||
$rd monitor
|
||||
$rd read ; # Discard the OK
|
||||
assert_error {*unknown command*} {r internalauth.internalcommand}
|
||||
|
||||
# Assert that the monitor output does not contain the internal command
|
||||
r ping
|
||||
assert_match {*ping*} [$rd read]
|
||||
$rd close
|
||||
}
|
||||
|
||||
test {Internal commands are not reported in the monitor output for non-internal connections when successful} {
|
||||
# Authenticate as an internal connection
|
||||
assert_equal {OK} [r debug mark-internal-client]
|
||||
|
||||
set rd [redis_deferring_client]
|
||||
$rd monitor
|
||||
$rd read ; # Discard the OK
|
||||
assert_equal {OK} [r internalauth.internalcommand]
|
||||
|
||||
# Assert that the monitor output does not contain the internal command
|
||||
r ping
|
||||
assert_match {*ping*} [$rd read]
|
||||
$rd close
|
||||
}
|
||||
|
||||
test {Internal commands are reported in the monitor output for internal connections} {
|
||||
set rd [redis_deferring_client]
|
||||
$rd debug mark-internal-client
|
||||
assert_equal {OK} [$rd read]
|
||||
$rd monitor
|
||||
$rd read ; # Discard the OK
|
||||
assert_equal {OK} [r internalauth.internalcommand]
|
||||
|
||||
# Assert that the monitor output contains the internal command
|
||||
assert_match {*internalauth.internalcommand*} [$rd read]
|
||||
$rd close
|
||||
}
|
||||
|
||||
test {Internal commands are reported in the slowlog} {
|
||||
# Set up slowlog to log all commands
|
||||
r config set slowlog-log-slower-than 0
|
||||
|
||||
# Execute an internal command
|
||||
r slowlog reset
|
||||
r internalauth.internalcommand
|
||||
|
||||
# The slow-log should contain the internal command
|
||||
set log [r slowlog get 1]
|
||||
assert_match {*internalauth.internalcommand*} $log
|
||||
}
|
||||
|
||||
test {Internal commands are reported in the latency report} {
|
||||
# The latency report should contain the internal command
|
||||
set report [r latency histogram internalauth.internalcommand]
|
||||
assert_match {*internalauth.internalcommand*} $report
|
||||
}
|
||||
|
||||
test {Internal commands are reported in the command stats report} {
|
||||
# The INFO report should contain the internal command for both the internal
|
||||
# and non-internal connections.
|
||||
set report [r info commandstats]
|
||||
assert_match {*internalauth.internalcommand*} $report
|
||||
|
||||
set report [r info latencystats]
|
||||
assert_match {*internalauth.internalcommand*} $report
|
||||
|
||||
# Un-mark the connection as internal
|
||||
r debug mark-internal-client unmark
|
||||
assert_error {*unknown command*} {r internalauth.internalcommand}
|
||||
|
||||
# We still expect to see the internal command in the report
|
||||
set report [r info commandstats]
|
||||
assert_match {*internalauth.internalcommand*} $report
|
||||
|
||||
set report [r info latencystats]
|
||||
assert_match {*internalauth.internalcommand*} $report
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue