summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorKarthikSubbarao <104098378+KarthikSubbarao@users.noreply.github.com>2023-03-15 15:18:42 -0700
committerGitHub <noreply@github.com>2023-03-15 15:18:42 -0700
commitf8a5a4f70ccada85943af90f6f2db3250ee50b27 (patch)
tree3ba462b1c1be69a4a04ba72c760d301f03db2cfc /tests
parent58285a6e9258412d0c99c8d0300620d43b209204 (diff)
downloadredis-f8a5a4f70ccada85943af90f6f2db3250ee50b27.tar.gz
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>
Diffstat (limited to 'tests')
-rw-r--r--tests/modules/Makefile3
-rw-r--r--tests/modules/auth.c152
-rw-r--r--tests/modules/moduleauthtwo.c43
-rw-r--r--tests/unit/acl.tcl25
-rw-r--r--tests/unit/moduleapi/moduleauth.tcl405
5 files changed, 627 insertions, 1 deletions
diff --git a/tests/modules/Makefile b/tests/modules/Makefile
index 5c73ed0e3..a1f5b074b 100644
--- a/tests/modules/Makefile
+++ b/tests/modules/Makefile
@@ -60,7 +60,8 @@ TEST_MODULES = \
moduleconfigstwo.so \
publish.so \
usercall.so \
- postnotifications.so
+ postnotifications.so \
+ moduleauthtwo.so
.PHONY: all
diff --git a/tests/modules/auth.c b/tests/modules/auth.c
index 612320dbc..9ef0626cb 100644
--- a/tests/modules/auth.c
+++ b/tests/modules/auth.c
@@ -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;
}
diff --git a/tests/modules/moduleauthtwo.c b/tests/modules/moduleauthtwo.c
new file mode 100644
index 000000000..0a4f56b65
--- /dev/null
+++ b/tests/modules/moduleauthtwo.c
@@ -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;
+} \ No newline at end of file
diff --git a/tests/unit/acl.tcl b/tests/unit/acl.tcl
index 13eea86de..555fb5a34 100644
--- a/tests/unit/acl.tcl
+++ b/tests/unit/acl.tcl
@@ -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
diff --git a/tests/unit/moduleapi/moduleauth.tcl b/tests/unit/moduleapi/moduleauth.tcl
new file mode 100644
index 000000000..82f42f5d1
--- /dev/null
+++ b/tests/unit/moduleapi/moduleauth.tcl
@@ -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]
+ }
+}