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:
Raz Monsonego 2025-02-05 11:48:08 +02:00 committed by GitHub
parent 09f8a2f374
commit 04589f90d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 602 additions and 28 deletions

View File

@ -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 \
"${@}"

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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';

View File

@ -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);

View File

@ -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);
}

View File

@ -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);

View File

@ -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.

View File

@ -63,7 +63,8 @@ TEST_MODULES = \
postnotifications.so \
moduleauthtwo.so \
rdbloadsave.so \
crash.so
crash.so \
internalsecret.so
.PHONY: all

View File

@ -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;
}

View File

@ -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
}
}
}