redis/tests/modules/internalsecret.c
Raz Monsonego 04589f90d7
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>
2025-02-05 11:48:08 +02:00

154 lines
5.6 KiB
C

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