Custom authentication for Modules (#11659)

This change adds new module callbacks that can override the default password based authentication associated with ACLs. With this, Modules can register auth callbacks through which they can implement their own Authentication logic. When `AUTH` and `HELLO AUTH ...` commands are used, Module based authentication is attempted and then normal password based authentication is attempted if needed.
The new Module APIs added in this PR are - `RM_RegisterCustomAuthCallback` and `RM_BlockClientOnAuth` and `RedisModule_ACLAddLogEntryByUserName `.

Module based authentication will be attempted for all Redis users (created through the ACL SETUSER cmd or through Module APIs) even if the Redis user does not exist at the time of the command. This gives a chance for the Module to create the RedisModule user and then authenticate via the RedisModule API - from the custom auth callback.

For the AUTH command, we will support both variations - `AUTH <username> <password>` and `AUTH <password>`. In case of the `AUTH <password>` variation, the custom auth callbacks are triggered with “default” as the username and password as what is provided.


### RedisModule_RegisterCustomAuthCallback
```
void RM_RegisterCustomAuthCallback(RedisModuleCtx *ctx, RedisModuleCustomAuthCallback cb) {
```
This API registers a callback to execute to prior to normal password based authentication. Multiple callbacks can be registered across different modules. These callbacks are responsible for either handling the authentication, each authenticating the user or explicitly denying, or deferring it to other authentication mechanisms. Callbacks are triggered in the order they were registered. When a Module is unloaded, all the auth callbacks registered by it are unregistered. The callbacks are attempted, in the order of most recently registered callbacks, when the AUTH/HELLO (with AUTH field is provided) commands are called. The callbacks will be called with a module context along with a username and a password, and are expected to take one of the following actions:

 (1) Authenticate - Use the RM_Authenticate* API successfully and return `REDISMODULE_AUTH_HANDLED`. This will immediately end the auth chain as successful and add the OK reply.
(2) Block a client on authentication - Use the `RM_BlockClientOnAuth` API and return `REDISMODULE_AUTH_HANDLED`. Here, the client will be blocked until the `RM_UnblockClient `API is used which will trigger the auth reply callback (provided earlier through the `RM_BlockClientOnAuth`). In this reply callback, the Module should authenticate, deny or skip handling authentication.
(3) Deny Authentication - Return `REDISMODULE_AUTH_HANDLED` without authenticating or blocking the client. Optionally, `err` can be set to a custom error message. This will immediately end the auth chain as unsuccessful and add the ERR reply.
(4) Skip handling Authentication - Return `REDISMODULE_AUTH_NOT_HANDLED` without blocking the client. This will allow the engine to attempt the next custom auth callback.

If none of the callbacks authenticate or deny auth, then password based auth is attempted and will authenticate or add failure logs and reply to the clients accordingly.

### RedisModule_BlockClientOnAuth
```
RedisModuleBlockedClient *RM_BlockClientOnAuth(RedisModuleCtx *ctx, RedisModuleCustomAuthCallback reply_callback,
                                               void (*free_privdata)(RedisModuleCtx*,void*))
```
This API can only be used from a Module from the custom auth callback. If a client is not in the middle of custom module based authentication, ERROR is returned. Otherwise, the client is blocked and the `RedisModule_BlockedClient` is returned similar to the `RedisModule_BlockClient` API.

### RedisModule_ACLAddLogEntryByUserName
```
int RM_ACLAddLogEntryByUserName(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *object, RedisModuleACLLogEntryReason reason)
```
Adds a new entry in the ACL log with the `username` RedisModuleString provided. This simplifies the Module usage because now, developers do not need to create a Module User just to add an error ACL Log entry. Aside from accepting username (RedisModuleString) instead of a RedisModuleUser, it is the same as the existing `RedisModule_ACLAddLogEntry` API.


### Breaking changes
- HELLO command - Clients can now only set the client name and RESP protocol from the `HELLO` command if they are authenticated. Also, we now finish command arg validation first and return early with a ERR reply if any arg is invalid. This is to avoid mutating the client name / RESP from a command that would have failed on invalid arguments.

### Notable behaviors
- Module unblocking - Now, we will not allow Modules to block the client from inside the context of a reply callback (triggered from the Module unblock flow `moduleHandleBlockedClients`).

---------

Co-authored-by: Madelyn Olson <34459052+madolson@users.noreply.github.com>
This commit is contained in:
KarthikSubbarao 2023-03-15 15:18:42 -07:00 committed by GitHub
parent 58285a6e92
commit f8a5a4f70c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1057 additions and 103 deletions

View File

@ -52,4 +52,5 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/publish \
--single unit/moduleapi/usercall \
--single unit/moduleapi/postnotifications \
--single unit/moduleapi/moduleauth \
"${@}"

View File

@ -1406,24 +1406,50 @@ int ACLCheckUserCredentials(robj *username, robj *password) {
return C_ERR;
}
/* If `err` is provided, this is added as an error reply to the client.
* Otherwise, the standard Auth error is added as a reply. */
void addAuthErrReply(client *c, robj *err) {
if (clientHasPendingReplies(c)) return;
if (!err) {
addReplyError(c, "-WRONGPASS invalid username-password pair or user is disabled.");
return;
}
addReplyError(c, err->ptr);
}
/* This is like ACLCheckUserCredentials(), however if the user/pass
* are correct, the connection is put in authenticated state and the
* connection user reference is populated.
*
* The return value is C_OK or C_ERR with the same meaning as
* ACLCheckUserCredentials(). */
int ACLAuthenticateUser(client *c, robj *username, robj *password) {
* The return value is AUTH_OK on success (valid username / password pair) & AUTH_ERR otherwise. */
int checkPasswordBasedAuth(client *c, robj *username, robj *password) {
if (ACLCheckUserCredentials(username,password) == C_OK) {
c->authenticated = 1;
c->user = ACLGetUserByName(username->ptr,sdslen(username->ptr));
moduleNotifyUserChanged(c);
return C_OK;
return AUTH_OK;
} else {
addACLLogEntry(c,ACL_DENIED_AUTH,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,0,username->ptr,NULL);
return C_ERR;
return AUTH_ERR;
}
}
/* Attempt authenticating the user - first through module based authentication,
* and then, if needed, with normal password based authentication.
* Returns one of the following codes:
* AUTH_OK - Indicates that authentication succeeded.
* AUTH_ERR - Indicates that authentication failed.
* AUTH_BLOCKED - Indicates module authentication is in progress through a blocking implementation.
*/
int ACLAuthenticateUser(client *c, robj *username, robj *password, robj **err) {
int result = checkModuleAuthentication(c, username, password, err);
/* If authentication was not handled by any Module, attempt normal password based auth. */
if (result == AUTH_NOT_HANDLED) {
result = checkPasswordBasedAuth(c, username, password);
}
return result;
}
/* For ACL purposes, every user has a bitmap with the commands that such
* user is allowed to execute. In order to populate the bitmap, every command
* should have an assigned ID (that is used to index the bitmap). This function
@ -3046,11 +3072,14 @@ void authCommand(client *c) {
redactClientCommandArgument(c, 2);
}
if (ACLAuthenticateUser(c,username,password) == C_OK) {
addReply(c,shared.ok);
} else {
addReplyError(c,"-WRONGPASS invalid username-password pair or user is disabled.");
robj *err = NULL;
int result = ACLAuthenticateUser(c, username, password, &err);
if (result == AUTH_OK) {
addReply(c, shared.ok);
} else if (result == AUTH_ERR) {
addAuthErrReply(c, err);
}
if (err) decrRefCount(err);
}
/* Set the password for the "default" ACL user. This implements supports for

View File

@ -229,6 +229,7 @@ struct RedisModuleKey {
* a Redis module. */
struct RedisModuleBlockedClient;
typedef int (*RedisModuleCmdFunc) (RedisModuleCtx *ctx, void **argv, int argc);
typedef int (*RedisModuleAuthCallback)(RedisModuleCtx *ctx, void *username, void *password, RedisModuleString **err);
typedef void (*RedisModuleDisconnectFunc) (RedisModuleCtx *ctx, struct RedisModuleBlockedClient *bc);
/* This struct holds the information about a command registered by a module.*/
@ -249,6 +250,12 @@ typedef struct RedisModuleCommand RedisModuleCommand;
* only the type, proto and protolen are filled. */
typedef struct CallReply RedisModuleCallReply;
/* Structure to hold the module auth callback & the Module implementing it. */
typedef struct RedisModuleAuthCtx {
struct RedisModule *module;
RedisModuleAuthCallback auth_cb;
} RedisModuleAuthCtx;
/* Structure representing a blocked client. We get a pointer to such
* an object when blocking from modules. */
typedef struct RedisModuleBlockedClient {
@ -256,6 +263,8 @@ typedef struct RedisModuleBlockedClient {
was destroyed during the life of this object. */
RedisModule *module; /* Module blocking the client. */
RedisModuleCmdFunc reply_callback; /* Reply callback on normal completion.*/
RedisModuleAuthCallback auth_reply_cb; /* Reply callback on completing blocking
module authentication. */
RedisModuleCmdFunc timeout_callback; /* Reply callback on timeout. */
RedisModuleDisconnectFunc disconnect_callback; /* Called on disconnection.*/
void (*free_privdata)(RedisModuleCtx*,void*);/* privdata cleanup callback.*/
@ -274,6 +283,11 @@ typedef struct RedisModuleBlockedClient {
Used for measuring latency of blocking cmds */
} RedisModuleBlockedClient;
/* This is a list of Module Auth Contexts. Each time a Module registers a callback, a new ctx is
* added to this list. Multiple modules can register auth callbacks and the same Module can have
* multiple auth callbacks. */
static list *moduleAuthCallbacks;
static pthread_mutex_t moduleUnblockedClientsMutex = PTHREAD_MUTEX_INITIALIZER;
static list *moduleUnblockedClients;
@ -3521,7 +3535,7 @@ int RM_SetClientNameById(uint64_t id, RedisModuleString *name) {
errno = ENOENT;
return REDISMODULE_ERR;
}
if (clientSetName(client, name) == C_ERR) {
if (clientSetName(client, name, NULL) == C_ERR) {
errno = EINVAL;
return REDISMODULE_ERR;
}
@ -7369,6 +7383,7 @@ void unblockClientFromModule(client *c) {
*
*/
RedisModuleBlockedClient *moduleBlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback,
RedisModuleAuthCallback auth_reply_callback,
RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*),
long long timeout_ms, RedisModuleString **keys, int numkeys, void *privdata,
int flags) {
@ -7388,6 +7403,7 @@ RedisModuleBlockedClient *moduleBlockClient(RedisModuleCtx *ctx, RedisModuleCmdF
bc->client = (islua || ismulti) ? NULL : c;
bc->module = ctx->module;
bc->reply_callback = reply_callback;
bc->auth_reply_cb = auth_reply_callback;
bc->timeout_callback = timeout_callback;
bc->disconnect_callback = NULL; /* Set by RM_SetDisconnectCallback() */
bc->free_privdata = free_privdata;
@ -7408,6 +7424,13 @@ RedisModuleBlockedClient *moduleBlockClient(RedisModuleCtx *ctx, RedisModuleCmdF
addReplyError(c, islua ?
"Blocking module command called from Lua script" :
"Blocking module command called from transaction");
} else if (ctx->flags & REDISMODULE_CTX_BLOCKED_REPLY) {
c->bstate.module_blocked_handle = NULL;
addReplyError(c, "Blocking module command called from a Reply callback context");
}
else if (!auth_reply_callback && clientHasModuleAuthInProgress(c)) {
c->bstate.module_blocked_handle = NULL;
addReplyError(c, "Clients undergoing module based authentication can only be blocked on auth");
} else {
if (keys) {
blockForKeys(c,BLOCKED_MODULE,keys,numkeys,timeout,flags&REDISMODULE_BLOCK_UNBLOCK_DELETED);
@ -7418,6 +7441,185 @@ RedisModuleBlockedClient *moduleBlockClient(RedisModuleCtx *ctx, RedisModuleCmdF
return bc;
}
/* This API registers a callback to execute in addition to normal password based authentication.
* Multiple callbacks can be registered across different modules. When a Module is unloaded, all the
* auth callbacks registered by it are unregistered.
* The callbacks are attempted (in the order of most recently registered first) when the AUTH/HELLO
* (with AUTH field provided) commands are called.
* The callbacks will be called with a module context along with a username and a password, and are
* expected to take one of the following actions:
* (1) Authenticate - Use the RM_AuthenticateClient* API and return REDISMODULE_AUTH_HANDLED.
* This will immediately end the auth chain as successful and add the OK reply.
* (2) Deny Authentication - Return REDISMODULE_AUTH_HANDLED without authenticating or blocking the
* client. Optionally, `err` can be set to a custom error message and `err` will be automatically
* freed by the server.
* This will immediately end the auth chain as unsuccessful and add the ERR reply.
* (3) Block a client on authentication - Use the RM_BlockClientOnAuth API and return
* REDISMODULE_AUTH_HANDLED. Here, the client will be blocked until the RM_UnblockClient API is used
* which will trigger the auth reply callback (provided through the RM_BlockClientOnAuth).
* In this reply callback, the Module should authenticate, deny or skip handling authentication.
* (4) Skip handling Authentication - Return REDISMODULE_AUTH_NOT_HANDLED without blocking the
* client. This will allow the engine to attempt the next module auth callback.
* If none of the callbacks authenticate or deny auth, then password based auth is attempted and
* will authenticate or add failure logs and reply to the clients accordingly.
*
* Note: If a client is disconnected while it was in the middle of blocking module auth, that
* occurrence of the AUTH or HELLO command will not be tracked in the INFO command stats.
*
* The following is an example of how non-blocking module based authentication can be used:
*
* int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
* const char *user = RedisModule_StringPtrLen(username, NULL);
* const char *pwd = RedisModule_StringPtrLen(password, NULL);
* if (!strcmp(user,"foo") && !strcmp(pwd,"valid_password")) {
* RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL);
* return REDISMODULE_AUTH_HANDLED;
* }
*
* else if (!strcmp(user,"foo") && !strcmp(pwd,"wrong_password")) {
* RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
* RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
* RedisModule_FreeString(ctx, log);
* const char *err_msg = "Auth denied by Misc Module.";
* *err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
* return REDISMODULE_AUTH_HANDLED;
* }
* return REDISMODULE_AUTH_NOT_HANDLED;
* }
*
* int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
* if (RedisModule_Init(ctx,"authmodule",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
* return REDISMODULE_ERR;
* RedisModule_RegisterAuthCallback(ctx, auth_cb);
* return REDISMODULE_OK;
* }
*/
void RM_RegisterAuthCallback(RedisModuleCtx *ctx, RedisModuleAuthCallback cb) {
RedisModuleAuthCtx *auth_ctx = zmalloc(sizeof(RedisModuleAuthCtx));
auth_ctx->module = ctx->module;
auth_ctx->auth_cb = cb;
listAddNodeHead(moduleAuthCallbacks, auth_ctx);
}
/* Helper function to invoke the free private data callback of a Module blocked client. */
void moduleInvokeFreePrivDataCallback(client *c, RedisModuleBlockedClient *bc) {
if (bc->privdata && bc->free_privdata) {
RedisModuleCtx ctx;
int ctx_flags = c == NULL ? REDISMODULE_CTX_BLOCKED_DISCONNECTED : REDISMODULE_CTX_NONE;
moduleCreateContext(&ctx, bc->module, ctx_flags);
ctx.blocked_privdata = bc->privdata;
ctx.client = bc->client;
bc->free_privdata(&ctx,bc->privdata);
moduleFreeContext(&ctx);
}
}
/* Unregisters all the module auth callbacks that have been registered by this Module. */
void moduleUnregisterAuthCBs(RedisModule *module) {
listIter li;
listNode *ln;
listRewind(moduleAuthCallbacks, &li);
while ((ln = listNext(&li))) {
RedisModuleAuthCtx *ctx = listNodeValue(ln);
if (ctx->module == module) {
listDelNode(moduleAuthCallbacks, ln);
zfree(ctx);
}
}
}
/* Search for & attempt next module auth callback after skipping the ones already attempted.
* Returns the result of the module auth callback. */
int attemptNextAuthCb(client *c, robj *username, robj *password, robj **err) {
int handle_next_callback = c->module_auth_ctx == NULL;
RedisModuleAuthCtx *cur_auth_ctx = NULL;
listNode *ln;
listIter li;
listRewind(moduleAuthCallbacks, &li);
int result = REDISMODULE_AUTH_NOT_HANDLED;
while((ln = listNext(&li))) {
cur_auth_ctx = listNodeValue(ln);
/* Skip over the previously attempted auth contexts. */
if (!handle_next_callback) {
handle_next_callback = cur_auth_ctx == c->module_auth_ctx;
continue;
}
/* Remove the module auth complete flag before we attempt the next cb. */
c->flags &= ~CLIENT_MODULE_AUTH_HAS_RESULT;
RedisModuleCtx ctx;
moduleCreateContext(&ctx, cur_auth_ctx->module, REDISMODULE_CTX_NONE);
ctx.client = c;
*err = NULL;
c->module_auth_ctx = cur_auth_ctx;
result = cur_auth_ctx->auth_cb(&ctx, username, password, err);
moduleFreeContext(&ctx);
if (result == REDISMODULE_AUTH_HANDLED) break;
/* If Auth was not handled (allowed/denied/blocked) by the Module, try the next auth cb. */
}
return result;
}
/* Helper function to handle a reprocessed unblocked auth client.
* Returns REDISMODULE_AUTH_NOT_HANDLED if the client was not reprocessed after a blocking module
* auth operation.
* Otherwise, we attempt the auth reply callback & the free priv data callback, update fields and
* return the result of the reply callback. */
int attemptBlockedAuthReplyCallback(client *c, robj *username, robj *password, robj **err) {
int result = REDISMODULE_AUTH_NOT_HANDLED;
if (!c->module_blocked_client) return result;
RedisModuleBlockedClient *bc = (RedisModuleBlockedClient *) c->module_blocked_client;
bc->client = c;
if (bc->auth_reply_cb) {
RedisModuleCtx ctx;
moduleCreateContext(&ctx, bc->module, REDISMODULE_CTX_BLOCKED_REPLY);
ctx.blocked_privdata = bc->privdata;
ctx.blocked_ready_key = NULL;
ctx.client = bc->client;
ctx.blocked_client = bc;
result = bc->auth_reply_cb(&ctx, username, password, err);
moduleFreeContext(&ctx);
}
moduleInvokeFreePrivDataCallback(c, bc);
c->module_blocked_client = NULL;
c->lastcmd->microseconds += bc->background_duration;
bc->module->blocked_clients--;
zfree(bc);
return result;
}
/* Helper function to attempt Module based authentication through module auth callbacks.
* Here, the Module is expected to authenticate the client using the RedisModule APIs and to add ACL
* logs in case of errors.
* Returns one of the following codes:
* AUTH_OK - Indicates that a module handled and authenticated the client.
* AUTH_ERR - Indicates that a module handled and denied authentication for this client.
* AUTH_NOT_HANDLED - Indicates that authentication was not handled by any Module and that
* normal password based authentication can be attempted next.
* AUTH_BLOCKED - Indicates module authentication is in progress through a blocking implementation.
* In this case, authentication is handled here again after the client is unblocked / reprocessed. */
int checkModuleAuthentication(client *c, robj *username, robj *password, robj **err) {
if (!listLength(moduleAuthCallbacks)) return AUTH_NOT_HANDLED;
int result = attemptBlockedAuthReplyCallback(c, username, password, err);
if (result == REDISMODULE_AUTH_NOT_HANDLED) {
result = attemptNextAuthCb(c, username, password, err);
}
if (c->flags & CLIENT_BLOCKED) {
/* Modules are expected to return REDISMODULE_AUTH_HANDLED when blocking clients. */
serverAssert(result == REDISMODULE_AUTH_HANDLED);
return AUTH_BLOCKED;
}
c->module_auth_ctx = NULL;
if (result == REDISMODULE_AUTH_NOT_HANDLED) {
c->flags &= ~CLIENT_MODULE_AUTH_HAS_RESULT;
return AUTH_NOT_HANDLED;
}
if (c->flags & CLIENT_MODULE_AUTH_HAS_RESULT) {
c->flags &= ~CLIENT_MODULE_AUTH_HAS_RESULT;
if (c->authenticated) return AUTH_OK;
}
return AUTH_ERR;
}
/* This function is called from module.c in order to check if a module
* blocked for BLOCKED_MODULE and subtype 'on keys' (bc->blocked_on_keys true)
* can really be unblocked, since the module was able to serve the client.
@ -7488,7 +7690,24 @@ int moduleTryServeClientBlockedOnKey(client *c, robj *key) {
RedisModuleBlockedClient *RM_BlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback,
RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*),
long long timeout_ms) {
return moduleBlockClient(ctx,reply_callback,timeout_callback,free_privdata,timeout_ms, NULL,0,NULL,0);
return moduleBlockClient(ctx,reply_callback,NULL,timeout_callback,free_privdata,timeout_ms, NULL,0,NULL,0);
}
/* Block the current client for module authentication in the background. If module auth is not in
* progress on the client, the API returns NULL. Otherwise, the client is blocked and the RM_BlockedClient
* is returned similar to the RM_BlockClient API.
* Note: Only use this API from the context of a module auth callback. */
RedisModuleBlockedClient *RM_BlockClientOnAuth(RedisModuleCtx *ctx, RedisModuleAuthCallback reply_callback,
void (*free_privdata)(RedisModuleCtx*,void*)) {
if (!clientHasModuleAuthInProgress(ctx->client)) {
addReplyError(ctx->client, "Module blocking client on auth when not currently undergoing module authentication");
return NULL;
}
RedisModuleBlockedClient *bc = moduleBlockClient(ctx,NULL,reply_callback,NULL,free_privdata,0, NULL,0,NULL,0);
if (ctx->client->flags & CLIENT_BLOCKED) {
ctx->client->flags |= CLIENT_PENDING_COMMAND;
}
return bc;
}
/* This call is similar to RedisModule_BlockClient(), however in this case we
@ -7552,7 +7771,7 @@ RedisModuleBlockedClient *RM_BlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc
RedisModuleBlockedClient *RM_BlockClientOnKeys(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback,
RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*),
long long timeout_ms, RedisModuleString **keys, int numkeys, void *privdata) {
return moduleBlockClient(ctx,reply_callback,timeout_callback,free_privdata,timeout_ms, keys,numkeys,privdata,0);
return moduleBlockClient(ctx,reply_callback,NULL,timeout_callback,free_privdata,timeout_ms, keys,numkeys,privdata,0);
}
/* Same as RedisModule_BlockClientOnKeys, but can take REDISMODULE_BLOCK_* flags
@ -7568,7 +7787,7 @@ RedisModuleBlockedClient *RM_BlockClientOnKeysWithFlags(RedisModuleCtx *ctx, Red
RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*),
long long timeout_ms, RedisModuleString **keys, int numkeys, void *privdata,
int flags) {
return moduleBlockClient(ctx,reply_callback,timeout_callback,free_privdata,timeout_ms, keys,numkeys,privdata,flags);
return moduleBlockClient(ctx,reply_callback,NULL,timeout_callback,free_privdata,timeout_ms, keys,numkeys,privdata,flags);
}
/* This function is used in order to potentially unblock a client blocked
@ -7643,6 +7862,7 @@ int RM_UnblockClient(RedisModuleBlockedClient *bc, void *privdata) {
int RM_AbortBlock(RedisModuleBlockedClient *bc) {
bc->reply_callback = NULL;
bc->disconnect_callback = NULL;
bc->auth_reply_cb = NULL;
return RM_UnblockClient(bc,NULL);
}
@ -7709,16 +7929,13 @@ void moduleHandleBlockedClients(void) {
reply_us = elapsedUs(replyTimer);
moduleFreeContext(&ctx);
}
/* Free privdata if any. */
if (bc->privdata && bc->free_privdata) {
RedisModuleCtx ctx;
int ctx_flags = c == NULL ? REDISMODULE_CTX_BLOCKED_DISCONNECTED : REDISMODULE_CTX_NONE;
moduleCreateContext(&ctx, bc->module, ctx_flags);
ctx.blocked_privdata = bc->privdata;
ctx.client = bc->client;
bc->free_privdata(&ctx,bc->privdata);
moduleFreeContext(&ctx);
/* Hold onto the blocked client if module auth is in progress. The reply callback is invoked
* when the client is reprocessed. */
if (c && clientHasModuleAuthInProgress(c)) {
c->module_blocked_client = bc;
} else {
/* Free privdata if any. */
moduleInvokeFreePrivDataCallback(c, bc);
}
/* It is possible that this blocked client object accumulated
@ -7733,7 +7950,7 @@ void moduleHandleBlockedClients(void) {
* This needs to be out of the reply callback above given that a
* module might not define any callback and still do blocking ops.
*/
if (c && !bc->blocked_on_keys) {
if (c && !clientHasModuleAuthInProgress(c) && !bc->blocked_on_keys) {
updateStatsOnUnblock(c, bc->background_duration, reply_us, server.stat_total_error_replies != prev_error_replies);
}
@ -7746,7 +7963,7 @@ void moduleHandleBlockedClients(void) {
/* Put the client in the list of clients that need to write
* if there are pending replies here. This is needed since
* during a non blocking command the client may receive output. */
if (clientHasPendingReplies(c) &&
if (!clientHasModuleAuthInProgress(c) && clientHasPendingReplies(c) &&
!(c->flags & CLIENT_PENDING_WRITE))
{
c->flags |= CLIENT_PENDING_WRITE;
@ -7757,8 +7974,10 @@ void moduleHandleBlockedClients(void) {
/* Free 'bc' only after unblocking the client, since it is
* referenced in the client blocking context, and must be valid
* when calling unblockClient(). */
bc->module->blocked_clients--;
zfree(bc);
if (!(c && clientHasModuleAuthInProgress(c))) {
bc->module->blocked_clients--;
zfree(bc);
}
/* Lock again before to iterate the loop. */
pthread_mutex_lock(&moduleUnblockedClientsMutex);
@ -9135,24 +9354,41 @@ int RM_ACLCheckChannelPermissions(RedisModuleUser *user, RedisModuleString *ch,
return REDISMODULE_OK;
}
/* Adds a new entry in the ACL log.
* Returns REDISMODULE_OK on success and REDISMODULE_ERR on error.
*
* For more information about ACL log, please refer to https://redis.io/commands/acl-log */
int RM_ACLAddLogEntry(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString *object, RedisModuleACLLogEntryReason reason) {
int acl_reason;
/* Helper function to map a RedisModuleACLLogEntryReason to ACL Log entry reason. */
int moduleGetACLLogEntryReason(RedisModuleACLLogEntryReason reason) {
int acl_reason = 0;
switch (reason) {
case REDISMODULE_ACL_LOG_AUTH: acl_reason = ACL_DENIED_AUTH; break;
case REDISMODULE_ACL_LOG_KEY: acl_reason = ACL_DENIED_KEY; break;
case REDISMODULE_ACL_LOG_CHANNEL: acl_reason = ACL_DENIED_CHANNEL; break;
case REDISMODULE_ACL_LOG_CMD: acl_reason = ACL_DENIED_CMD; break;
default: return REDISMODULE_ERR;
default: break;
}
return acl_reason;
}
/* Adds a new entry in the ACL log.
* Returns REDISMODULE_OK on success and REDISMODULE_ERR on error.
*
* For more information about ACL log, please refer to https://redis.io/commands/acl-log */
int RM_ACLAddLogEntry(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString *object, RedisModuleACLLogEntryReason reason) {
int acl_reason = moduleGetACLLogEntryReason(reason);
if (!acl_reason) return REDISMODULE_ERR;
addACLLogEntry(ctx->client, acl_reason, ACL_LOG_CTX_MODULE, -1, user->user->name, sdsdup(object->ptr));
return REDISMODULE_OK;
}
/* Adds a new entry in the ACL log with the `username` RedisModuleString provided.
* Returns REDISMODULE_OK on success and REDISMODULE_ERR on error.
*
* For more information about ACL log, please refer to https://redis.io/commands/acl-log */
int RM_ACLAddLogEntryByUserName(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *object, RedisModuleACLLogEntryReason reason) {
int acl_reason = moduleGetACLLogEntryReason(reason);
if (!acl_reason) return REDISMODULE_ERR;
addACLLogEntry(ctx->client, acl_reason, ACL_LOG_CTX_MODULE, -1, username->ptr, sdsdup(object->ptr));
return REDISMODULE_OK;
}
/* Authenticate the client associated with the context with
* the provided user. Returns REDISMODULE_OK on success and
* REDISMODULE_ERR on error.
@ -9188,6 +9424,10 @@ static int authenticateClientWithUser(RedisModuleCtx *ctx, user *user, RedisModu
ctx->client->user = user;
ctx->client->authenticated = 1;
if (clientHasModuleAuthInProgress(ctx->client)) {
ctx->client->flags |= CLIENT_MODULE_AUTH_HAS_RESULT;
}
if (callback) {
ctx->client->auth_callback = callback;
ctx->client->auth_callback_privdata = privdata;
@ -11278,6 +11518,7 @@ void moduleInitModulesSystem(void) {
server.loadmodule_queue = listCreate();
server.module_configs_queue = dictCreate(&sdsKeyValueHashDictType);
modules = dictCreate(&modulesDictType);
moduleAuthCallbacks = listCreate();
/* Set up the keyspace notification subscriber list and static client */
moduleKeyspaceSubscribers = listCreate();
@ -11582,6 +11823,7 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa
moduleUnregisterSharedAPI(ctx.module);
moduleUnregisterUsedAPI(ctx.module);
moduleRemoveConfigs(ctx.module);
moduleUnregisterAuthCBs(ctx.module);
moduleFreeModuleStructure(ctx.module);
}
moduleFreeContext(&ctx);
@ -11604,16 +11846,21 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa
serverLog(LL_NOTICE,"Module '%s' loaded from %s",ctx.module->name,path);
int post_load_err = 0;
if (listLength(ctx.module->module_configs) && !ctx.module->configs_initialized) {
serverLogRaw(LL_WARNING, "Module Configurations were not set, likely a missing LoadConfigs call. Unloading the module.");
moduleUnload(ctx.module->name);
moduleFreeContext(&ctx);
return C_ERR;
post_load_err = 1;
}
if (is_loadex && dictSize(server.module_configs_queue)) {
serverLogRaw(LL_WARNING, "Loadex configurations were not applied, likely due to invalid arguments. Unloading the module.");
moduleUnload(ctx.module->name);
post_load_err = 1;
}
if (post_load_err) {
/* Unregister module auth callbacks (if any exist) that this Module registered onload. */
moduleUnregisterAuthCBs(ctx.module);
moduleUnload(ctx.module->name, NULL);
moduleFreeContext(&ctx);
return C_ERR;
}
@ -11628,32 +11875,29 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa
}
/* Unload the module registered with the specified name. On success
* C_OK is returned, otherwise C_ERR is returned and errno is set
* to the following values depending on the type of error:
*
* * ENONET: No such module having the specified name.
* * EBUSY: The module exports a new data type and can only be reloaded.
* * EPERM: The module exports APIs which are used by other module.
* * EAGAIN: The module has blocked clients.
* * EINPROGRESS: The module holds timer not fired.
* * ECANCELED: Unload module error. */
int moduleUnload(sds name) {
* C_OK is returned, otherwise C_ERR is returned and errmsg is set
* with an appropriate message. */
int moduleUnload(sds name, const char **errmsg) {
struct RedisModule *module = dictFetchValue(modules,name);
if (module == NULL) {
errno = ENOENT;
*errmsg = "no such module with that name";
return C_ERR;
} else if (listLength(module->types)) {
errno = EBUSY;
*errmsg = "the module exports one or more module-side data "
"types, can't unload";
return C_ERR;
} else if (listLength(module->usedby)) {
errno = EPERM;
*errmsg = "the module exports APIs used by other modules. "
"Please unload them first and try again";
return C_ERR;
} else if (module->blocked_clients) {
errno = EAGAIN;
*errmsg = "the module has blocked clients. "
"Please wait for them to be unblocked and try again";
return C_ERR;
} else if (moduleHoldsTimer(module)) {
errno = EINPROGRESS;
*errmsg = "the module holds timer that is not fired. "
"Please stop the timer or wait until it fires.";
return C_ERR;
}
@ -11678,6 +11922,7 @@ int moduleUnload(sds name) {
moduleUnregisterSharedAPI(module);
moduleUnregisterUsedAPI(module);
moduleUnregisterFilters(module);
moduleUnregisterAuthCBs(module);
moduleRemoveConfigs(module);
/* Remove any notification subscribers this module might have */
@ -12301,35 +12546,13 @@ NULL
}
} else if (!strcasecmp(subcmd,"unload") && c->argc == 3) {
if (moduleUnload(c->argv[2]->ptr) == C_OK)
const char *errmsg = NULL;
if (moduleUnload(c->argv[2]->ptr, &errmsg) == C_OK)
addReply(c,shared.ok);
else {
char *errmsg;
switch(errno) {
case ENOENT:
errmsg = "no such module with that name";
break;
case EBUSY:
errmsg = "the module exports one or more module-side data "
"types, can't unload";
break;
case EPERM:
errmsg = "the module exports APIs used by other modules. "
"Please unload them first and try again";
break;
case EAGAIN:
errmsg = "the module has blocked clients. "
"Please wait them unblocked and try again";
break;
case EINPROGRESS:
errmsg = "the module holds timer that is not fired. "
"Please stop the timer or wait until it fires.";
break;
default:
errmsg = "operation not possible.";
break;
}
addReplyErrorFormat(c,"Error unloading module: %s",errmsg);
if (errmsg == NULL) errmsg = "operation not possible.";
addReplyErrorFormat(c, "Error unloading module: %s", errmsg);
serverLog(LL_WARNING, "Error unloading module %s: %s", (sds) c->argv[2]->ptr, errmsg);
}
} else if (!strcasecmp(subcmd,"list") && c->argc == 2) {
addReplyLoadedModules(c);
@ -12978,6 +13201,7 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(GetKeyNameFromDigest);
REGISTER_API(GetDbIdFromDigest);
REGISTER_API(BlockClient);
REGISTER_API(BlockClientOnAuth);
REGISTER_API(UnblockClient);
REGISTER_API(IsBlockedReplyRequest);
REGISTER_API(IsBlockedTimeoutRequest);
@ -13104,6 +13328,7 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(ACLCheckKeyPermissions);
REGISTER_API(ACLCheckChannelPermissions);
REGISTER_API(ACLAddLogEntry);
REGISTER_API(ACLAddLogEntryByUserName);
REGISTER_API(FreeModuleUser);
REGISTER_API(DeauthenticateAndCloseClient);
REGISTER_API(AuthenticateClientWithACLUser);
@ -13134,4 +13359,5 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(RegisterStringConfig);
REGISTER_API(RegisterEnumConfig);
REGISTER_API(LoadConfigs);
REGISTER_API(RegisterAuthCallback);
}

View File

@ -207,6 +207,8 @@ client *createClient(connection *conn) {
c->client_tracking_prefixes = NULL;
c->last_memory_usage = 0;
c->last_memory_type = CLIENT_TYPE_NORMAL;
c->module_blocked_client = NULL;
c->module_auth_ctx = NULL;
c->auth_callback = NULL;
c->auth_callback_privdata = NULL;
c->auth_module = NULL;
@ -1534,6 +1536,9 @@ void freeClient(client *c) {
/* Notify module system that this client auth status changed. */
moduleNotifyUserChanged(c);
/* Free the RedisModuleBlockedClient held onto for reprocessing if not already freed. */
zfree(c->module_blocked_client);
/* If this client was scheduled for async freeing we need to remove it
* from the queue. Note that we need to do this here, because later
* we may call replicationCacheMaster() and the client should already
@ -2809,27 +2814,39 @@ sds getAllClientsInfoString(int type) {
return o;
}
/* Returns C_OK if the name has been set or C_ERR if the name is invalid. */
int clientSetName(client *c, robj *name) {
/* Returns C_OK if the name is valid. Returns C_ERR & sets `err` (when provided) otherwise. */
int validateClientName(robj *name, const char **err) {
const char *err_msg = "Client names cannot contain spaces, newlines or special characters.";
int len = (name != NULL) ? sdslen(name->ptr) : 0;
/* Setting the client name to an empty string actually removes
* the current name. */
if (len == 0) {
if (c->name) decrRefCount(c->name);
c->name = NULL;
/* We allow setting the client name to an empty string. */
if (len == 0)
return C_OK;
}
/* Otherwise check if the charset is ok. We need to do this otherwise
* CLIENT LIST format will break. You should always be able to
* split by space to get the different fields. */
char *p = name->ptr;
for (int j = 0; j < len; j++) {
if (p[j] < '!' || p[j] > '~') { /* ASCII is assumed. */
if (err) *err = err_msg;
return C_ERR;
}
}
return C_OK;
}
/* Returns C_OK if the name has been set or C_ERR if the name is invalid. */
int clientSetName(client *c, robj *name, const char **err) {
if (validateClientName(name, err) == C_ERR) {
return C_ERR;
}
int len = (name != NULL) ? sdslen(name->ptr) : 0;
/* Setting the client name to an empty string actually removes
* the current name. */
if (len == 0) {
if (c->name) decrRefCount(c->name);
c->name = NULL;
return C_OK;
}
if (c->name) decrRefCount(c->name);
c->name = name;
incrRefCount(name);
@ -2846,11 +2863,10 @@ int clientSetName(client *c, robj *name) {
*
* This function is also used to implement the HELLO SETNAME option. */
int clientSetNameOrReply(client *c, robj *name) {
int result = clientSetName(c, name);
const char *err = NULL;
int result = clientSetName(c, name, &err);
if (result == C_ERR) {
addReplyError(c,
"Client names cannot contain spaces, "
"newlines or special characters.");
addReplyError(c, err);
}
return result;
}
@ -3434,19 +3450,25 @@ void helloCommand(client *c) {
}
}
robj *username = NULL;
robj *password = NULL;
robj *clientname = NULL;
for (int j = next_arg; j < c->argc; j++) {
int moreargs = (c->argc-1) - j;
const char *opt = c->argv[j]->ptr;
if (!strcasecmp(opt,"AUTH") && moreargs >= 2) {
redactClientCommandArgument(c, j+1);
redactClientCommandArgument(c, j+2);
if (ACLAuthenticateUser(c, c->argv[j+1], c->argv[j+2]) == C_ERR) {
addReplyError(c,"-WRONGPASS invalid username-password pair or user is disabled.");
return;
}
username = c->argv[j+1];
password = c->argv[j+2];
j += 2;
} else if (!strcasecmp(opt,"SETNAME") && moreargs) {
if (clientSetNameOrReply(c, c->argv[j+1]) == C_ERR) return;
clientname = c->argv[j+1];
const char *err = NULL;
if (validateClientName(clientname, &err) == C_ERR) {
addReplyError(c, err);
return;
}
j++;
} else {
addReplyErrorFormat(c,"Syntax error in HELLO option '%s'",opt);
@ -3454,6 +3476,20 @@ void helloCommand(client *c) {
}
}
if (username && password) {
robj *err = NULL;
int auth_result = ACLAuthenticateUser(c, username, password, &err);
if (auth_result == AUTH_ERR) {
addAuthErrReply(c, err);
}
if (err) decrRefCount(err);
/* In case of auth errors, return early since we already replied with an ERR.
* In case of blocking module auth, we reply to the client/setname later upon unblocking. */
if (auth_result == AUTH_ERR || auth_result == AUTH_BLOCKED) {
return;
}
}
/* At this point we need to be authenticated to continue. */
if (!c->authenticated) {
addReplyError(c,"-NOAUTH HELLO must be called with the client already "
@ -3463,6 +3499,9 @@ void helloCommand(client *c) {
return;
}
/* Now that we're authenticated, set the client name. */
if (clientname) clientSetName(c, clientname, NULL);
/* Let's switch to the specified RESP mode. */
if (ver) c->resp = ver;
addReplyMapLen(c,6 + !server.sentinel_mode);

View File

@ -34,6 +34,10 @@ typedef long long ustime_t;
#define REDISMODULE_OK 0
#define REDISMODULE_ERR 1
/* Module Based Authentication status return values. */
#define REDISMODULE_AUTH_HANDLED 0
#define REDISMODULE_AUTH_NOT_HANDLED 1
/* API versions. */
#define REDISMODULE_APIVER_1 1
@ -912,6 +916,7 @@ typedef int (*RedisModuleConfigSetNumericFunc)(const char *name, long long val,
typedef int (*RedisModuleConfigSetBoolFunc)(const char *name, int val, void *privdata, RedisModuleString **err);
typedef int (*RedisModuleConfigSetEnumFunc)(const char *name, int val, void *privdata, RedisModuleString **err);
typedef int (*RedisModuleConfigApplyFunc)(RedisModuleCtx *ctx, void *privdata, RedisModuleString **err);
typedef int (*RedisModuleAuthCallback)(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err);
typedef struct RedisModuleTypeMethods {
uint64_t version;
@ -1164,6 +1169,7 @@ REDISMODULE_API RedisModuleString * (*RedisModule_DictPrev)(RedisModuleCtx *ctx,
REDISMODULE_API int (*RedisModule_DictCompareC)(RedisModuleDictIter *di, const char *op, void *key, size_t keylen) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_DictCompare)(RedisModuleDictIter *di, const char *op, RedisModuleString *key) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_RegisterInfoFunc)(RedisModuleCtx *ctx, RedisModuleInfoFunc cb) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_RegisterAuthCallback)(RedisModuleCtx *ctx, RedisModuleAuthCallback cb) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_InfoAddSection)(RedisModuleInfoCtx *ctx, const char *name) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_InfoBeginDictField)(RedisModuleInfoCtx *ctx, const char *name) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_InfoEndDictField)(RedisModuleInfoCtx *ctx) REDISMODULE_ATTR;
@ -1201,6 +1207,7 @@ REDISMODULE_API int (*RedisModule_GetServerVersion)() REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_GetTypeMethodVersion)() REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_Yield)(RedisModuleCtx *ctx, int flags, const char *busy_reply) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleBlockedClient * (*RedisModule_BlockClient)(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(RedisModuleCtx*,void*), long long timeout_ms) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleBlockedClient * (*RedisModule_BlockClientOnAuth)(RedisModuleCtx *ctx, RedisModuleAuthCallback reply_callback, void (*free_privdata)(RedisModuleCtx*,void*)) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_UnblockClient)(RedisModuleBlockedClient *bc, void *privdata) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_IsBlockedReplyRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_IsBlockedTimeoutRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
@ -1264,6 +1271,7 @@ REDISMODULE_API int (*RedisModule_ACLCheckCommandPermissions)(RedisModuleUser *u
REDISMODULE_API int (*RedisModule_ACLCheckKeyPermissions)(RedisModuleUser *user, RedisModuleString *key, int flags) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ACLCheckChannelPermissions)(RedisModuleUser *user, RedisModuleString *ch, int literal) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_ACLAddLogEntry)(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString *object, RedisModuleACLLogEntryReason reason) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_ACLAddLogEntryByUserName)(RedisModuleCtx *ctx, RedisModuleString *user, RedisModuleString *object, RedisModuleACLLogEntryReason reason) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_AuthenticateClientWithACLUser)(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_AuthenticateClientWithUser)(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_DeauthenticateAndCloseClient)(RedisModuleCtx *ctx, uint64_t client_id) REDISMODULE_ATTR;
@ -1506,6 +1514,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(DictCompare);
REDISMODULE_GET_API(DictCompareC);
REDISMODULE_GET_API(RegisterInfoFunc);
REDISMODULE_GET_API(RegisterAuthCallback);
REDISMODULE_GET_API(InfoAddSection);
REDISMODULE_GET_API(InfoBeginDictField);
REDISMODULE_GET_API(InfoEndDictField);
@ -1554,6 +1563,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(ThreadSafeContextTryLock);
REDISMODULE_GET_API(ThreadSafeContextUnlock);
REDISMODULE_GET_API(BlockClient);
REDISMODULE_GET_API(BlockClientOnAuth);
REDISMODULE_GET_API(UnblockClient);
REDISMODULE_GET_API(IsBlockedReplyRequest);
REDISMODULE_GET_API(IsBlockedTimeoutRequest);
@ -1611,6 +1621,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(ACLCheckKeyPermissions);
REDISMODULE_GET_API(ACLCheckChannelPermissions);
REDISMODULE_GET_API(ACLAddLogEntry);
REDISMODULE_GET_API(ACLAddLogEntryByUserName);
REDISMODULE_GET_API(DeauthenticateAndCloseClient);
REDISMODULE_GET_API(AuthenticateClientWithACLUser);
REDISMODULE_GET_API(AuthenticateClientWithUser);

View File

@ -392,6 +392,8 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
scripts even when in OOM */
#define CLIENT_NO_TOUCH (1ULL<<45) /* This client will not touch LFU/LRU stats. */
#define CLIENT_PUSHING (1ULL<<46) /* This client is pushing notifications. */
#define CLIENT_MODULE_AUTH_HAS_RESULT (1ULL<<47) /* Indicates a client in the middle of module based
auth had been authenticated from the Module. */
/* Client block type (btype field in client structure)
* if CLIENT_BLOCKED flag is set. */
@ -740,6 +742,7 @@ typedef void (*moduleTypeFreeFunc2)(struct RedisModuleKeyOptCtx *ctx, void *valu
typedef size_t (*moduleTypeFreeEffortFunc2)(struct RedisModuleKeyOptCtx *ctx, const void *value);
typedef void (*moduleTypeUnlinkFunc2)(struct RedisModuleKeyOptCtx *ctx, void *value);
typedef void *(*moduleTypeCopyFunc2)(struct RedisModuleKeyOptCtx *ctx, const void *value);
typedef int (*moduleTypeAuthCallback)(struct RedisModuleCtx *ctx, void *username, void *password, const char **err);
/* The module type, which is referenced in each value of a given type, defines
@ -856,6 +859,9 @@ struct RedisModuleDigest {
memset(mdvar.x,0,sizeof(mdvar.x)); \
} while(0)
/* Macro to check if the client is in the middle of module based authentication. */
#define clientHasModuleAuthInProgress(c) ((c)->module_auth_ctx != NULL)
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
@ -1200,6 +1206,12 @@ typedef struct client {
listNode *client_list_node; /* list node in client list */
listNode *postponed_list_node; /* list node within the postponed list */
listNode *pending_read_list_node; /* list node in clients pending read list */
void *module_blocked_client; /* Pointer to the RedisModuleBlockedClient associated with this
* client. This is set in case of module authentication before the
* unblocked client is reprocessed to handle reply callbacks. */
void *module_auth_ctx; /* Ongoing / attempted module based auth callback's ctx.
* This is only tracked within the context of the command attempting
* authentication. If not NULL, it means module auth is in progress. */
RedisModuleUserChangedFunc auth_callback; /* Module callback to execute
* when the authenticated user
* changes. */
@ -2467,7 +2479,7 @@ void moduleInitModulesSystem(void);
void moduleInitModulesSystemLast(void);
void modulesCron(void);
int moduleLoad(const char *path, void **argv, int argc, int is_loadex);
int moduleUnload(sds name);
int moduleUnload(sds name, const char **errmsg);
void moduleLoadFromQueue(void);
int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int moduleGetCommandChannelsViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
@ -2598,7 +2610,7 @@ char *getClientPeerId(client *client);
char *getClientSockName(client *client);
sds catClientInfoString(sds s, client *client);
sds getAllClientsInfoString(int type);
int clientSetName(client *c, robj *name);
int clientSetName(client *c, robj *name, const char **err);
void rewriteClientCommandVector(client *c, int argc, ...);
void rewriteClientCommandArgument(client *c, int i, robj *newval);
void replaceClientCommandVector(client *c, int argc, robj **argv);
@ -2895,8 +2907,18 @@ void ACLInit(void);
#define ACL_WRITE_PERMISSION (1<<1)
#define ACL_ALL_PERMISSION (ACL_READ_PERMISSION|ACL_WRITE_PERMISSION)
/* Return codes for Authentication functions to indicate the result. */
typedef enum {
AUTH_OK = 0,
AUTH_ERR,
AUTH_NOT_HANDLED,
AUTH_BLOCKED
} AuthResult;
int ACLCheckUserCredentials(robj *username, robj *password);
int ACLAuthenticateUser(client *c, robj *username, robj *password);
int ACLAuthenticateUser(client *c, robj *username, robj *password, robj **err);
int checkModuleAuthentication(client *c, robj *username, robj *password, robj **err);
void addAuthErrReply(client *c, robj *err);
unsigned long ACLGetCommandID(sds cmdname);
void ACLClearCommandID(void);
user *ACLGetUserByName(const char *name, size_t namelen);

View File

@ -60,7 +60,8 @@ TEST_MODULES = \
moduleconfigstwo.so \
publish.so \
usercall.so \
postnotifications.so
postnotifications.so \
moduleauthtwo.so
.PHONY: all

View File

@ -1,5 +1,9 @@
#include "redismodule.h"
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#define UNUSED(V) ((void) V)
// A simple global user
@ -72,6 +76,146 @@ int Auth_ChangeCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
return RedisModule_ReplyWithLongLong(ctx, result);
}
/* The Module functionality below validates that module authentication callbacks can be registered
* to support both non-blocking and blocking module based authentication. */
/* Non Blocking Module Auth callback / implementation. */
int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
const char *user = RedisModule_StringPtrLen(username, NULL);
const char *pwd = RedisModule_StringPtrLen(password, NULL);
if (!strcmp(user,"foo") && !strcmp(pwd,"allow")) {
RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL);
return REDISMODULE_AUTH_HANDLED;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"deny")) {
RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
RedisModule_FreeString(ctx, log);
const char *err_msg = "Auth denied by Misc Module.";
*err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
return REDISMODULE_AUTH_HANDLED;
}
return REDISMODULE_AUTH_NOT_HANDLED;
}
int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_RegisterAuthCallback(ctx, auth_cb);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
/*
* The thread entry point that actually executes the blocking part of the AUTH command.
* This function sleeps for 0.5 seconds and then unblocks the client which will later call
* `AuthBlock_Reply`.
* `arg` is expected to contain the RedisModuleBlockedClient, username, and password.
*/
void *AuthBlock_ThreadMain(void *arg) {
usleep(500000);
void **targ = arg;
RedisModuleBlockedClient *bc = targ[0];
int result = 2;
const char *user = RedisModule_StringPtrLen(targ[1], NULL);
const char *pwd = RedisModule_StringPtrLen(targ[2], NULL);
if (!strcmp(user,"foo") && !strcmp(pwd,"block_allow")) {
result = 1;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"block_deny")) {
result = 0;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"block_abort")) {
RedisModule_BlockedClientMeasureTimeEnd(bc);
RedisModule_AbortBlock(bc);
goto cleanup;
}
/* Provide the result to the blocking reply cb. */
void **replyarg = RedisModule_Alloc(sizeof(void*));
replyarg[0] = (void *) (uintptr_t) result;
RedisModule_BlockedClientMeasureTimeEnd(bc);
RedisModule_UnblockClient(bc, replyarg);
cleanup:
/* Free the username and password and thread / arg data. */
RedisModule_FreeString(NULL, targ[1]);
RedisModule_FreeString(NULL, targ[2]);
RedisModule_Free(targ);
return NULL;
}
/*
* Reply callback for a blocking AUTH command. This is called when the client is unblocked.
*/
int AuthBlock_Reply(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
REDISMODULE_NOT_USED(password);
void **targ = RedisModule_GetBlockedClientPrivateData(ctx);
int result = (uintptr_t) targ[0];
size_t userlen = 0;
const char *user = RedisModule_StringPtrLen(username, &userlen);
/* Handle the success case by authenticating. */
if (result == 1) {
RedisModule_AuthenticateClientWithACLUser(ctx, user, userlen, NULL, NULL, NULL);
return REDISMODULE_AUTH_HANDLED;
}
/* Handle the Error case by denying auth */
else if (result == 0) {
RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
RedisModule_FreeString(ctx, log);
const char *err_msg = "Auth denied by Misc Module.";
*err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
return REDISMODULE_AUTH_HANDLED;
}
/* "Skip" Authentication */
return REDISMODULE_AUTH_NOT_HANDLED;
}
/* Private data freeing callback for Module Auth. */
void AuthBlock_FreeData(RedisModuleCtx *ctx, void *privdata) {
REDISMODULE_NOT_USED(ctx);
RedisModule_Free(privdata);
}
/* Callback triggered when the engine attempts module auth
* Return code here is one of the following: Auth succeeded, Auth denied,
* Auth not handled, Auth blocked.
* The Module can have auth succeed / denied here itself, but this is an example
* of blocking module auth.
*/
int blocking_auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
REDISMODULE_NOT_USED(username);
REDISMODULE_NOT_USED(password);
REDISMODULE_NOT_USED(err);
/* Block the client from the Module. */
RedisModuleBlockedClient *bc = RedisModule_BlockClientOnAuth(ctx, AuthBlock_Reply, AuthBlock_FreeData);
int ctx_flags = RedisModule_GetContextFlags(ctx);
if (ctx_flags & REDISMODULE_CTX_FLAGS_MULTI || ctx_flags & REDISMODULE_CTX_FLAGS_LUA) {
/* Clean up by using RedisModule_UnblockClient since we attempted blocking the client. */
RedisModule_UnblockClient(bc, NULL);
return REDISMODULE_AUTH_HANDLED;
}
RedisModule_BlockedClientMeasureTimeStart(bc);
pthread_t tid;
/* Allocate memory for information needed. */
void **targ = RedisModule_Alloc(sizeof(void*)*3);
targ[0] = bc;
targ[1] = RedisModule_CreateStringFromString(NULL, username);
targ[2] = RedisModule_CreateStringFromString(NULL, password);
/* Create bg thread and pass the blockedclient, username and password to it. */
if (pthread_create(&tid, NULL, AuthBlock_ThreadMain, targ) != 0) {
RedisModule_AbortBlock(bc);
}
return REDISMODULE_AUTH_HANDLED;
}
int test_rm_register_blocking_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_RegisterAuthCallback(ctx, blocking_auth_cb);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
/* 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) {
@ -101,6 +245,14 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
Auth_RedactedAPI,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"testmoduleone.rm_register_auth_cb",
test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"testmoduleone.rm_register_blocking_auth_cb",
test_rm_register_blocking_auth_cb,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}

View File

@ -0,0 +1,43 @@
#include "redismodule.h"
#include <string.h>
/* This is a second sample module to validate that module authentication callbacks can be registered
* from multiple modules. */
/* Non Blocking Module Auth callback / implementation. */
int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
const char *user = RedisModule_StringPtrLen(username, NULL);
const char *pwd = RedisModule_StringPtrLen(password, NULL);
if (!strcmp(user,"foo") && !strcmp(pwd,"allow_two")) {
RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL);
return REDISMODULE_AUTH_HANDLED;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"deny_two")) {
RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
RedisModule_FreeString(ctx, log);
const char *err_msg = "Auth denied by Misc Module.";
*err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
return REDISMODULE_AUTH_HANDLED;
}
return REDISMODULE_AUTH_NOT_HANDLED;
}
int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_RegisterAuthCallback(ctx, auth_cb);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
if (RedisModule_Init(ctx,"moduleauthtwo",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"testmoduletwo.rm_register_auth_cb", test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}

View File

@ -793,6 +793,31 @@ start_server {tags {"acl external:skip"}} {
r AUTH default ""
}
test {When an authentication chain is used in the HELLO cmd, the last auth cmd has precedence} {
r ACL setuser secure-user1 >supass on +@all
r ACL setuser secure-user2 >supass on +@all
r HELLO 2 AUTH secure-user pass AUTH secure-user2 supass AUTH secure-user1 supass
assert {[r ACL whoami] eq {secure-user1}}
catch {r HELLO 2 AUTH secure-user supass AUTH secure-user2 supass AUTH secure-user pass} e
assert_match "WRONGPASS invalid username-password pair or user is disabled." $e
assert {[r ACL whoami] eq {secure-user1}}
}
test {When a setname chain is used in the HELLO cmd, the last setname cmd has precedence} {
r HELLO 2 setname client1 setname client2 setname client3 setname client4
assert {[r client getname] eq {client4}}
catch {r HELLO 2 setname client5 setname client6 setname "client name"} e
assert_match "ERR Client names cannot contain spaces, newlines or special characters." $e
assert {[r client getname] eq {client4}}
}
test {When authentication fails in the HELLO cmd, the client setname should not be applied} {
r client setname client0
catch {r HELLO 2 AUTH user pass setname client1} e
assert_match "WRONGPASS invalid username-password pair or user is disabled." $e
assert {[r client getname] eq {client0}}
}
test {ACL HELP should not have unexpected options} {
catch {r ACL help xxx} e
assert_match "*wrong number of arguments for 'acl|help' command" $e

View File

@ -0,0 +1,405 @@
set testmodule [file normalize tests/modules/auth.so]
set testmoduletwo [file normalize tests/modules/moduleauthtwo.so]
set miscmodule [file normalize tests/modules/misc.so]
proc cmdstat {cmd} {
return [cmdrstat $cmd r]
}
start_server {tags {"modules"}} {
r module load $testmodule
r module load $testmoduletwo
set hello2_response [r HELLO 2]
set hello3_response [r HELLO 3]
test {test registering module auth callbacks} {
assert_equal {OK} [r testmoduleone.rm_register_blocking_auth_cb]
assert_equal {OK} [r testmoduletwo.rm_register_auth_cb]
assert_equal {OK} [r testmoduleone.rm_register_auth_cb]
}
test {test module AUTH for non existing / disabled users} {
r config resetstat
# Validate that an error is thrown for non existing users.
assert_error {*WRONGPASS*} {r AUTH foo pwd}
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
# Validate that an error is thrown for disabled users.
r acl setuser foo >pwd off ~* &* +@all
assert_error {*WRONGPASS*} {r AUTH foo pwd}
assert_match {*calls=2,*,rejected_calls=0,failed_calls=2} [cmdstat auth]
}
test {test non blocking module AUTH} {
r config resetstat
# Test for a fixed password user
r acl setuser foo >pwd on ~* &* +@all
assert_equal {OK} [r AUTH foo allow]
assert_error {*Auth denied by Misc Module*} {r AUTH foo deny}
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
assert_error {*WRONGPASS*} {r AUTH foo nomatch}
assert_match {*calls=3,*,rejected_calls=0,failed_calls=2} [cmdstat auth]
assert_equal {OK} [r AUTH foo pwd]
# Test for No Pass user
r acl setuser foo on ~* &* +@all nopass
assert_equal {OK} [r AUTH foo allow]
assert_error {*Auth denied by Misc Module*} {r AUTH foo deny}
assert_match {*calls=6,*,rejected_calls=0,failed_calls=3} [cmdstat auth]
assert_equal {OK} [r AUTH foo nomatch]
# Validate that the Module added an ACL Log entry.
set entry [lindex [r ACL LOG] 0]
assert {[dict get $entry username] eq {foo}}
assert {[dict get $entry context] eq {module}}
assert {[dict get $entry reason] eq {auth}}
assert {[dict get $entry object] eq {Module Auth}}
assert_match {*cmd=auth*} [dict get $entry client-info]
r ACL LOG RESET
}
test {test non blocking module HELLO AUTH} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
# Validate proto 2 and 3 in case of success
assert_equal $hello2_response [r HELLO 2 AUTH foo pwd]
assert_equal $hello2_response [r HELLO 2 AUTH foo allow]
assert_equal $hello3_response [r HELLO 3 AUTH foo pwd]
assert_equal $hello3_response [r HELLO 3 AUTH foo allow]
# Validate denying AUTH for the HELLO cmd
assert_error {*Auth denied by Misc Module*} {r HELLO 2 AUTH foo deny}
assert_match {*calls=5,*,rejected_calls=0,failed_calls=1} [cmdstat hello]
assert_error {*WRONGPASS*} {r HELLO 2 AUTH foo nomatch}
assert_match {*calls=6,*,rejected_calls=0,failed_calls=2} [cmdstat hello]
assert_error {*Auth denied by Misc Module*} {r HELLO 3 AUTH foo deny}
assert_match {*calls=7,*,rejected_calls=0,failed_calls=3} [cmdstat hello]
assert_error {*WRONGPASS*} {r HELLO 3 AUTH foo nomatch}
assert_match {*calls=8,*,rejected_calls=0,failed_calls=4} [cmdstat hello]
# Validate that the Module added an ACL Log entry.
set entry [lindex [r ACL LOG] 1]
assert {[dict get $entry username] eq {foo}}
assert {[dict get $entry context] eq {module}}
assert {[dict get $entry reason] eq {auth}}
assert {[dict get $entry object] eq {Module Auth}}
assert_match {*cmd=hello*} [dict get $entry client-info]
r ACL LOG RESET
}
test {test non blocking module HELLO AUTH SETNAME} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
# Validate clientname is set on success
assert_equal $hello2_response [r HELLO 2 AUTH foo pwd setname client1]
assert {[r client getname] eq {client1}}
assert_equal $hello2_response [r HELLO 2 AUTH foo allow setname client2]
assert {[r client getname] eq {client2}}
# Validate clientname is not updated on failure
r client setname client0
assert_error {*Auth denied by Misc Module*} {r HELLO 2 AUTH foo deny setname client1}
assert {[r client getname] eq {client0}}
assert_match {*calls=3,*,rejected_calls=0,failed_calls=1} [cmdstat hello]
assert_error {*WRONGPASS*} {r HELLO 2 AUTH foo nomatch setname client2}
assert {[r client getname] eq {client0}}
assert_match {*calls=4,*,rejected_calls=0,failed_calls=2} [cmdstat hello]
}
test {test blocking module AUTH} {
r config resetstat
# Test for a fixed password user
r acl setuser foo >pwd on ~* &* +@all
assert_equal {OK} [r AUTH foo block_allow]
assert_error {*Auth denied by Misc Module*} {r AUTH foo block_deny}
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
assert_error {*WRONGPASS*} {r AUTH foo nomatch}
assert_match {*calls=3,*,rejected_calls=0,failed_calls=2} [cmdstat auth]
assert_equal {OK} [r AUTH foo pwd]
# Test for No Pass user
r acl setuser foo on ~* &* +@all nopass
assert_equal {OK} [r AUTH foo block_allow]
assert_error {*Auth denied by Misc Module*} {r AUTH foo block_deny}
assert_match {*calls=6,*,rejected_calls=0,failed_calls=3} [cmdstat auth]
assert_equal {OK} [r AUTH foo nomatch]
# Validate that every Blocking AUTH command took at least 500000 usec.
set stats [cmdstat auth]
regexp "usec_per_call=(\[0-9]{1,})\.*," $stats all usec_per_call
assert {$usec_per_call >= 500000}
# Validate that the Module added an ACL Log entry.
set entry [lindex [r ACL LOG] 0]
assert {[dict get $entry username] eq {foo}}
assert {[dict get $entry context] eq {module}}
assert {[dict get $entry reason] eq {auth}}
assert {[dict get $entry object] eq {Module Auth}}
assert_match {*cmd=auth*} [dict get $entry client-info]
r ACL LOG RESET
}
test {test blocking module HELLO AUTH} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
# validate proto 2 and 3 in case of success
assert_equal $hello2_response [r HELLO 2 AUTH foo pwd]
assert_equal $hello2_response [r HELLO 2 AUTH foo block_allow]
assert_equal $hello3_response [r HELLO 3 AUTH foo pwd]
assert_equal $hello3_response [r HELLO 3 AUTH foo block_allow]
# validate denying AUTH for the HELLO cmd
assert_error {*Auth denied by Misc Module*} {r HELLO 2 AUTH foo block_deny}
assert_match {*calls=5,*,rejected_calls=0,failed_calls=1} [cmdstat hello]
assert_error {*WRONGPASS*} {r HELLO 2 AUTH foo nomatch}
assert_match {*calls=6,*,rejected_calls=0,failed_calls=2} [cmdstat hello]
assert_error {*Auth denied by Misc Module*} {r HELLO 3 AUTH foo block_deny}
assert_match {*calls=7,*,rejected_calls=0,failed_calls=3} [cmdstat hello]
assert_error {*WRONGPASS*} {r HELLO 3 AUTH foo nomatch}
assert_match {*calls=8,*,rejected_calls=0,failed_calls=4} [cmdstat hello]
# Validate that every HELLO AUTH command took at least 500000 usec.
set stats [cmdstat hello]
regexp "usec_per_call=(\[0-9]{1,})\.*," $stats all usec_per_call
assert {$usec_per_call >= 500000}
# Validate that the Module added an ACL Log entry.
set entry [lindex [r ACL LOG] 1]
assert {[dict get $entry username] eq {foo}}
assert {[dict get $entry context] eq {module}}
assert {[dict get $entry reason] eq {auth}}
assert {[dict get $entry object] eq {Module Auth}}
assert_match {*cmd=hello*} [dict get $entry client-info]
r ACL LOG RESET
}
test {test blocking module HELLO AUTH SETNAME} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
# Validate clientname is set on success
assert_equal $hello2_response [r HELLO 2 AUTH foo pwd setname client1]
assert {[r client getname] eq {client1}}
assert_equal $hello2_response [r HELLO 2 AUTH foo block_allow setname client2]
assert {[r client getname] eq {client2}}
# Validate clientname is not updated on failure
r client setname client0
assert_error {*Auth denied by Misc Module*} {r HELLO 2 AUTH foo block_deny setname client1}
assert {[r client getname] eq {client0}}
assert_match {*calls=3,*,rejected_calls=0,failed_calls=1} [cmdstat hello]
assert_error {*WRONGPASS*} {r HELLO 2 AUTH foo nomatch setname client2}
assert {[r client getname] eq {client0}}
assert_match {*calls=4,*,rejected_calls=0,failed_calls=2} [cmdstat hello]
# Validate that every HELLO AUTH SETNAME command took at least 500000 usec.
set stats [cmdstat hello]
regexp "usec_per_call=(\[0-9]{1,})\.*," $stats all usec_per_call
assert {$usec_per_call >= 500000}
}
test {test AUTH after registering multiple module auth callbacks} {
r config resetstat
# Register two more callbacks from the same module.
assert_equal {OK} [r testmoduleone.rm_register_blocking_auth_cb]
assert_equal {OK} [r testmoduleone.rm_register_auth_cb]
# Register another module auth callback from the second module.
assert_equal {OK} [r testmoduletwo.rm_register_auth_cb]
r acl setuser foo >pwd on ~* &* +@all
# Case 1 - Non Blocking Success
assert_equal {OK} [r AUTH foo allow]
# Case 2 - Non Blocking Deny
assert_error {*Auth denied by Misc Module*} {r AUTH foo deny}
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
r config resetstat
# Case 3 - Blocking Success
assert_equal {OK} [r AUTH foo block_allow]
# Case 4 - Blocking Deny
assert_error {*Auth denied by Misc Module*} {r AUTH foo block_deny}
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
# Validate that every Blocking AUTH command took at least 500000 usec.
set stats [cmdstat auth]
regexp "usec_per_call=(\[0-9]{1,})\.*," $stats all usec_per_call
assert {$usec_per_call >= 500000}
r config resetstat
# Case 5 - Non Blocking Success via the second module.
assert_equal {OK} [r AUTH foo allow_two]
# Case 6 - Non Blocking Deny via the second module.
assert_error {*Auth denied by Misc Module*} {r AUTH foo deny_two}
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
r config resetstat
# Case 7 - All four auth callbacks "Skip" by not explicitly allowing or denying.
assert_error {*WRONGPASS*} {r AUTH foo nomatch}
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
assert_equal {OK} [r AUTH foo pwd]
# Because we had to attempt all 4 callbacks, validate that the AUTH command took at least
# 1000000 usec (each blocking callback takes 500000 usec).
set stats [cmdstat auth]
regexp "usec_per_call=(\[0-9]{1,})\.*," $stats all usec_per_call
assert {$usec_per_call >= 1000000}
}
test {module auth during blocking module auth} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
set rd [redis_deferring_client]
set rd_two [redis_deferring_client]
# Attempt blocking module auth. While this ongoing, attempt non blocking module auth from
# moduleone/moduletwo and start another blocking module auth from another deferring client.
$rd AUTH foo block_allow
wait_for_blocked_clients_count 1
assert_equal {OK} [r AUTH foo allow]
assert_equal {OK} [r AUTH foo allow_two]
# Validate that the non blocking module auth cmds finished before any blocking module auth.
set info_clients [r info clients]
assert_match "*blocked_clients:1*" $info_clients
$rd_two AUTH foo block_allow
# Validate that all of the AUTH commands succeeded.
wait_for_blocked_clients_count 0 500 10
$rd flush
assert_equal [$rd read] "OK"
$rd_two flush
assert_equal [$rd_two read] "OK"
assert_match {*calls=4,*,rejected_calls=0,failed_calls=0} [cmdstat auth]
}
test {module auth inside MULTI EXEC} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
# Validate that non blocking module auth inside MULTI succeeds.
r multi
r AUTH foo allow
assert_equal {OK} [r exec]
# Validate that blocking module auth inside MULTI throws an err.
r multi
r AUTH foo block_allow
assert_error {*ERR Blocking module command called from transaction*} {r exec}
assert_match {*calls=2,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
}
test {Disabling Redis User during blocking module auth} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
set rd [redis_deferring_client]
# Attempt blocking module auth and disable the Redis user while module auth is in progress.
$rd AUTH foo pwd
wait_for_blocked_clients_count 1
r acl setuser foo >pwd off ~* &* +@all
# Validate that module auth failed.
wait_for_blocked_clients_count 0 500 10
$rd flush
assert_error {*WRONGPASS*} { $rd read }
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
}
test {Killing a client in the middle of blocking module auth} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
set rd [redis_deferring_client]
$rd client id
set cid [$rd read]
# Attempt blocking module auth command on client `cid` and kill the client while module auth
# is in progress.
$rd AUTH foo pwd
wait_for_blocked_clients_count 1
r client kill id $cid
# Validate that the blocked client count goes to 0 and no AUTH command is tracked.
wait_for_blocked_clients_count 0 500 10
$rd flush
assert_error {*I/O error reading reply*} { $rd read }
assert_match {} [cmdstat auth]
}
test {test RM_AbortBlock Module API during blocking module auth} {
r config resetstat
r acl setuser foo >pwd on ~* &* +@all
# Attempt module auth. With the "block_abort" as the password, the "testacl.so" module
# blocks the client and uses the RM_AbortBlock API. This should result in module auth
# failing and the client being unblocked with the default AUTH err message.
assert_error {*WRONGPASS*} {r AUTH foo block_abort}
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdstat auth]
}
test {test RM_RegisterAuthCallback Module API during blocking module auth} {
r config resetstat
r acl setuser foo >defaultpwd on ~* &* +@all
set rd [redis_deferring_client]
# Start the module auth attempt with the standard Redis auth password for the user. This
# will result in all module auth cbs attempted and then standard Redis auth will be tried.
$rd AUTH foo defaultpwd
wait_for_blocked_clients_count 1
# Validate that we allow modules to register module auth cbs while module auth is already
# in progress.
assert_equal {OK} [r testmoduleone.rm_register_blocking_auth_cb]
assert_equal {OK} [r testmoduletwo.rm_register_auth_cb]
# Validate that blocking module auth succeeds.
wait_for_blocked_clients_count 0 500 10
$rd flush
assert_equal [$rd read] "OK"
set stats [cmdstat auth]
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} $stats
# Validate that even the new blocking module auth cb which was registered in the middle of
# blocking module auth is attempted - making it take twice the duration (2x 500000 us).
regexp "usec_per_call=(\[0-9]{1,})\.*," $stats all usec_per_call
assert {$usec_per_call >= 1000000}
}
test {Module unload during blocking module auth} {
r config resetstat
r module load $miscmodule
set rd [redis_deferring_client]
r acl setuser foo >pwd on ~* &* +@all
# Start a blocking module auth attempt.
$rd AUTH foo block_allow
wait_for_blocked_clients_count 1
# moduleone and moduletwo have module auth cbs registered. Because blocking module auth is
# ongoing, they cannot be unloaded.
catch {r module unload testacl} e
assert_match {*the module has blocked clients*} $e
# The moduleauthtwo module can be unregistered because no client is blocked on it.
assert_equal "OK" [r module unload moduleauthtwo]
# The misc module does not have module auth cbs registered, so it can be unloaded even when
# blocking module auth is ongoing.
assert_equal "OK" [r module unload misc]
# Validate that blocking module auth succeeds.
wait_for_blocked_clients_count 0 500 10
$rd flush
assert_equal [$rd read] "OK"
assert_match {*calls=1,*,rejected_calls=0,failed_calls=0} [cmdstat auth]
# Validate that unloading the moduleauthtwo module does not unregister module auth cbs of
# of the testacl module. Module based auth should succeed.
assert_equal {OK} [r AUTH foo allow]
# Validate that the testacl module can be unloaded since blocking module auth is done.
r module unload testacl
# Validate that since all module auth cbs are unregistered, module auth attempts fail.
assert_error {*WRONGPASS*} {r AUTH foo block_allow}
assert_error {*WRONGPASS*} {r AUTH foo allow_two}
assert_error {*WRONGPASS*} {r AUTH foo allow}
assert_match {*calls=5,*,rejected_calls=0,failed_calls=3} [cmdstat auth]
}
}