diff --git a/runtest-moduleapi b/runtest-moduleapi index 4b4f72d10..b5e90bfe8 100755 --- a/runtest-moduleapi +++ b/runtest-moduleapi @@ -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 \ "${@}" diff --git a/src/acl.c b/src/acl.c index e5dcf56b9..e225b2a39 100644 --- a/src/acl.c +++ b/src/acl.c @@ -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 diff --git a/src/module.c b/src/module.c index 418c5bd32..6ce57f69c 100644 --- a/src/module.c +++ b/src/module.c @@ -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); } diff --git a/src/networking.c b/src/networking.c index 954de341e..e8f8f93d2 100644 --- a/src/networking.c +++ b/src/networking.c @@ -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); diff --git a/src/redismodule.h b/src/redismodule.h index 342340af4..92b7d83de 100644 --- a/src/redismodule.h +++ b/src/redismodule.h @@ -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); diff --git a/src/server.h b/src/server.h index 3b267cf2e..be9905677 100644 --- a/src/server.h +++ b/src/server.h @@ -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); 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 +#include +#include + #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 + +/* 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] + } +}