diff options
author | Madelyn Olson <matolson@amazon.com> | 2019-02-26 01:23:11 +0000 |
---|---|---|
committer | Madelyn Olson <matolson@amazon.com> | 2019-12-17 06:59:59 +0000 |
commit | 034dcf185cfaa844154e422d26a9ec7afa314189 (patch) | |
tree | ee8a6afeea783d2b3d6fe6e59e33b9ecfa102cc8 | |
parent | e9b99c78dfd338ecf3c5b7b69318c10ce9dead03 (diff) | |
download | redis-034dcf185cfaa844154e422d26a9ec7afa314189.tar.gz |
Add module APIs for custom authentication
-rwxr-xr-x | runtest-moduleapi | 1 | ||||
-rw-r--r-- | src/acl.c | 1 | ||||
-rw-r--r-- | src/module.c | 222 | ||||
-rw-r--r-- | src/modules/Makefile | 7 | ||||
-rw-r--r-- | src/modules/helloacl.c | 195 | ||||
-rw-r--r-- | src/networking.c | 6 | ||||
-rw-r--r-- | src/redismodule.h | 17 | ||||
-rw-r--r-- | src/sentinel.c | 4 | ||||
-rw-r--r-- | src/server.c | 11 | ||||
-rw-r--r-- | src/server.h | 61 | ||||
-rw-r--r-- | tests/modules/Makefile | 3 | ||||
-rw-r--r-- | tests/modules/auth.c | 118 | ||||
-rw-r--r-- | tests/unit/moduleapi/auth.tcl | 71 |
13 files changed, 685 insertions, 32 deletions
diff --git a/runtest-moduleapi b/runtest-moduleapi index 32d5c2b8d..f6cc0a258 100755 --- a/runtest-moduleapi +++ b/runtest-moduleapi @@ -24,4 +24,5 @@ $TCLSH tests/test_helper.tcl \ --single unit/moduleapi/blockonkeys \ --single unit/moduleapi/scan \ --single unit/moduleapi/datatype \ +--single unit/moduleapi/auth \ "${@}" @@ -975,6 +975,7 @@ int ACLAuthenticateUser(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; } else { return C_ERR; diff --git a/src/module.c b/src/module.c index 5d9a39387..f6cc35112 100644 --- a/src/module.c +++ b/src/module.c @@ -355,6 +355,21 @@ list *RedisModule_EventListeners; /* Global list of all the active events. */ unsigned long long ModulesInHooks = 0; /* Total number of modules in hooks callbacks right now. */ +/* Data structures related to the redis module users */ + +/* This callback type is called by moduleNotifyUserChanged() every time + * a user authenticated via the module API is associated with a different + * user or gets disconnected. */ +typedef void (*RedisModuleUserChangedFunc) (uint64_t client_id, void *privdata); + +/* This is the object returned by RM_CreateModuleUser(). The module API is + * able to create users, set ACLs to such users, and later authenticate + * clients using such newly created users. */ +typedef struct RedisModuleUser { + user *user; /* Reference to the real redis user */ +} RedisModuleUser; + + /* -------------------------------------------------------------------------- * Prototypes * -------------------------------------------------------------------------- */ @@ -719,6 +734,7 @@ int commandFlagsFromString(char *s) { else if (!strcasecmp(t,"allow-stale")) flags |= CMD_STALE; else if (!strcasecmp(t,"no-monitor")) flags |= CMD_SKIP_MONITOR; else if (!strcasecmp(t,"fast")) flags |= CMD_FAST; + else if (!strcasecmp(t,"no-auth")) flags |= CMD_NO_AUTH; else if (!strcasecmp(t,"getkeys-api")) flags |= CMD_MODULE_GETKEYS; else if (!strcasecmp(t,"no-cluster")) flags |= CMD_MODULE_NO_CLUSTER; else break; @@ -780,6 +796,9 @@ int commandFlagsFromString(char *s) { * example, is unable to report the position of the * keys, programmatically creates key names, or any * other reason. + * * **"no-auth"**: This command can be run by an un-authenticated client. + * Normally this is used by a command that is used + * to authenticate a client. */ int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) { int flags = strflags ? commandFlagsFromString((char*)strflags) : 0; @@ -5237,6 +5256,202 @@ int RM_GetTimerInfo(RedisModuleCtx *ctx, RedisModuleTimerID id, uint64_t *remain } /* -------------------------------------------------------------------------- + * Modules ACL API + * + * Implements a hook into the authentication and authorization within Redis. + * --------------------------------------------------------------------------*/ + +/* This function is called when a client's user has changed and invoked a + * a modules client changed callback if it was set. This callback should + * cleanup any state the module was tracking about this client. + * + * A client's user can be changed through the AUTH command, module + * authentication, and when the client is freed. */ +void moduleNotifyUserChanged(client *c) { + if (c->auth_callback) { + c->auth_callback(c->id, c->auth_callback_privdata); + + /* The callback will fire exactly once, even if the user remains + * the same, it is expected to completely clean up it's state + * so all references are removed */ + c->auth_callback = NULL; + c->auth_callback_privdata = NULL; + c->auth_module = NULL; + } +} + +void revokeClientAuthentication(client *c) { + /* Fire the client changed handler now in case we are unloading the module + * and need to cleanup. */ + moduleNotifyUserChanged(c); + + c->user = DefaultUser; + c->authenticated = 0; + freeClientAsync(c); +} + +/* Cleanup all clients that have been authenticated with this module. This + * is called from onUnload() to give the module a chance to cleanup any + * resources associated with the authentication. */ +static void moduleFreeAuthenticatedClients(RedisModule *module) { + listIter li; + listNode *ln; + listRewind(server.clients,&li); + while ((ln = listNext(&li)) != NULL) { + client *c = listNodeValue(ln); + if (!c->auth_module) continue; + + RedisModule *auth_module = (RedisModule *) c->auth_module; + if (auth_module == module) { + revokeClientAuthentication(c); + } + } +} + +/* Creates a Redis ACL user that the module can use to authenticate a client. + * After obtaining the user, the module should set what such user can do + * using the RM_SetUserACL() function. Once configured, the user + * can be used in order to authenticate a connection, with the specified + * ACL rules, using the RedisModule_AuthClientWithUser() function. + * + * Note that: + * + * * Users created here are not listed by the ACL command. + * * Users created here are not checked for duplicated name, so it's up to + * the module calling this function to take care of not creating users + * with the same name. + * * The created user can be used to authenticate multiple Redis connections. + * + * The caller can later free the user using the function + * RM_FreeModuleUser(). When this function is called, if there are + * still clients authenticated with this user, they are disconnected. + * The function to free the user should only be used when the caller really + * wants to invalidate the user to define a new one with different + * capabilities. */ +RedisModuleUser *RM_CreateModuleUser(const char *name) { + RedisModuleUser *new_user = zmalloc(sizeof(RedisModuleUser)); + new_user->user = ACLCreateUnlinkedUser(); + + /* Free the previous temporarily assigned name to assign the new one */ + sdsfree(new_user->user->name); + new_user->user->name = sdsnew(name); + return new_user; +} + +/* Frees a given user and disconnects all of the clients that have been + * authenticated with it. See RM_CreateModuleUser for detailed usage.*/ +int RM_FreeModuleUser(RedisModuleUser *user) { + ACLFreeUserAndKillClients(user->user); + zfree(user); + return REDISMODULE_OK; +} + +/* Sets the permissions of a user created through the redis module + * interface. The syntax is the same as ACL SETUSER, so refer to the + * documentation in acl.c for more information. See RM_CreateModuleUser + * for detailed usage. + * + * Returns REDISMODULE_OK on success and REDISMODULE_ERR on failure + * and will set an errno describing why the operation failed. */ +int RM_SetModuleUserACL(RedisModuleUser *user, const char* acl) { + return ACLSetUser(user->user, acl, -1); +} + +/* Authenticate the client associated with the context with + * the provided user. Returns REDISMODULE_OK on success and + * REDISMODULE_ERR on error. + * + * This authentication can be tracked with the optional callback and private + * data fields. The callback will be called whenever the user of the client + * changes. This callback should be used to cleanup any state that is being + * kept in the module related to the client authentication. It will only be + * called once, even when the user hasn't changed, in order to allow for a + * new callback to be specified. If this authentication does not need to be + * tracked, pass in NULL for the callback and privdata. + * + * If client_id is not NULL, it will be filled with the id of the client + * that was authenticated. This can be used with the + * RM_DeauthenticateAndCloseClient() API in order to deauthenticate a + * previously authenticated client if the authentication is no longer valid. + * + * For expensive authentication operations, it is recommended to block the + * client and do the authentication in the background then attach the user + * to the client in a threadsafe context. */ +static int authenticateClientWithUser(RedisModuleCtx *ctx, user *user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) { + if (user->flags & USER_FLAG_DISABLED) { + return REDISMODULE_ERR; + } + + /* Freeing the client would result in moduleNotifyUserChanged() to be + * called later, however since we use revokeClientAuthentication() also + * in moduleFreeAuthenticatedClients() to implement module unloading, we + * do this action ASAP: this way if the module is unloaded, when the client + * is eventually freed we don't rely on the module to still exist. */ + moduleNotifyUserChanged(ctx->client); + + ctx->client->user = user; + ctx->client->authenticated = 1; + + if (callback) { + ctx->client->auth_callback = callback; + ctx->client->auth_callback_privdata = privdata; + ctx->client->auth_module = ctx->module; + } + + if (client_id) { + *client_id = ctx->client->id; + } + + return REDISMODULE_OK; +} + + +/* Authenticate the current context's user with the provided redis acl user. + * Returns REDISMODULE_ERR if the user is disabled. + * + * See authenticateClientWithUser for information about callback and client_id, + * and general usage for authentication. */ +int RM_AuthenticateClientWithUser(RedisModuleCtx *ctx, RedisModuleUser *module_user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) { + return authenticateClientWithUser(ctx, module_user->user, callback, privdata, client_id); +} + +/* Authenticate the current context's user with the provided redis acl user. + * Returns REDISMODULE_ERR if the user is disabled or the user does not exist. + * + * See authenticateClientWithUser for information about callback and client_id, + * and general usage for authentication. */ +int RM_AuthenticateClientWithACLUser(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) { + user *acl_user = ACLGetUserByName(name, len); + + if (!acl_user) { + return REDISMODULE_ERR; + } + return authenticateClientWithUser(ctx, acl_user, callback, privdata, client_id); +} + +/* Deauthenticate and close the client. The client resources will not be + * be immediately freed, but will be cleaned up in a background job. This is + * the recommended way to deauthenicate a client since most clients can't + * handle users becomming deauthenticated. Returns REDISMODULE_ERR when the + * client doesn't exist and REDISMODULE_OK when the operation was successful. + * + * The client ID can be obtained from the AuthenticateClientWithUser and + * AuthenticateClientWithACLUser APIs or through other APIs such as + * server events. + * + * This function is not thread safe, and must be executed within the context + * of a command or thread safe context. */ +int RM_DeauthenticateAndCloseClient(RedisModuleCtx *ctx, uint64_t client_id) { + UNUSED(ctx); + client *c = lookupClientByID(client_id); + if (c == NULL) return REDISMODULE_ERR; + + /* Revoke also marks client to be closed ASAP */ + revokeClientAuthentication(c); + return REDISMODULE_OK; +} + +/* -------------------------------------------------------------------------- * Modules Dictionary API * * Implements a sorted dictionary (actually backed by a radix tree) with @@ -7078,6 +7293,7 @@ int moduleUnload(sds name) { } } + moduleFreeAuthenticatedClients(module); moduleUnregisterCommands(module); moduleUnregisterSharedAPI(module); moduleUnregisterUsedAPI(module); @@ -7561,4 +7777,10 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(ScanCursorRestart); REGISTER_API(Scan); REGISTER_API(ScanKey); + REGISTER_API(CreateModuleUser); + REGISTER_API(SetModuleUserACL); + REGISTER_API(FreeModuleUser); + REGISTER_API(DeauthenticateAndCloseClient); + REGISTER_API(AuthenticateClientWithACLUser); + REGISTER_API(AuthenticateClientWithUser); } diff --git a/src/modules/Makefile b/src/modules/Makefile index 9fb84cff8..5e012d6f1 100644 --- a/src/modules/Makefile +++ b/src/modules/Makefile @@ -13,7 +13,7 @@ endif .SUFFIXES: .c .so .xo .o -all: helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so hellohook.so +all: helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so hellohook.so helloacl.so .c.xo: $(CC) -I. $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@ @@ -53,6 +53,11 @@ hellohook.xo: ../redismodule.h hellohook.so: hellohook.xo $(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc +helloacl.xo: ../redismodule.h + +helloacl.so: helloacl.xo + $(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc + testmodule.xo: ../redismodule.h testmodule.so: testmodule.xo diff --git a/src/modules/helloacl.c b/src/modules/helloacl.c new file mode 100644 index 000000000..a6cc68a04 --- /dev/null +++ b/src/modules/helloacl.c @@ -0,0 +1,195 @@ +/* ACL API example - An example of performing custom password authentication + * + * ----------------------------------------------------------------------------- + * + * Copyright 2019 Amazon.com, Inc. or its affiliates. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#define REDISMODULE_EXPERIMENTAL_API +#include "../redismodule.h" +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> +#include <string.h> +#include <strings.h> +#include <pthread.h> +#include <unistd.h> + +// A simple global user +static RedisModuleUser *global; +static uint64_t global_auth_client_id = 0; + +/* HELLOACL.REVOKE + * Synchronously revoke access from a user. */ +int RevokeCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if (global_auth_client_id) { + RedisModule_DisconnectClient(ctx, global_auth_client_id); + return RedisModule_ReplyWithSimpleString(ctx, "OK"); + } else { + return RedisModule_ReplyWithError(ctx, "Global user currently not used"); + } +} + +/* HELLOACL.RESET + * Synchronously delete and re-create a module user. */ +int ResetCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + RedisModule_FreeModuleUser(global); + global = RedisModule_CreateModuleUser("global"); + RedisModule_SetModuleUserACL(global, "allcommands"); + RedisModule_SetModuleUserACL(global, "allkeys"); + RedisModule_SetModuleUserACL(global, "on"); + + return RedisModule_ReplyWithSimpleString(ctx, "OK"); +} + +/* Callback handler for user changes, use this to notify a module of + * changes to users authenticated by the module */ +void HelloACL_UserChanged(uint64_t client_id, void *privdata) { + REDISMODULE_NOT_USED(privdata); + REDISMODULE_NOT_USED(client_id); + global_auth_client_id = 0; +} + +/* HELLOACL.AUTHGLOBAL + * Synchronously assigns a module user to the current context. */ +int AuthGlobalCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if (global_auth_client_id) { + return RedisModule_ReplyWithError(ctx, "Global user currently used"); + } + + RedisModule_AuthenticateClientWithUser(ctx, global, HelloACL_UserChanged, NULL, &global_auth_client_id); + + return RedisModule_ReplyWithSimpleString(ctx, "OK"); +} + +#define TIMEOUT_TIME 1000 + +/* Reply callback for auth command HELLOACL.AUTHASYNC */ +int HelloACL_Reply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + size_t length; + + RedisModuleString *user_string = RedisModule_GetBlockedClientPrivateData(ctx); + const char *name = RedisModule_StringPtrLen(user_string, &length); + + if (RedisModule_AuthenticateClientWithACLUser(ctx, name, length, NULL, NULL, NULL) == + REDISMODULE_ERR) { + return RedisModule_ReplyWithError(ctx, "Invalid Username or password"); + } + return RedisModule_ReplyWithSimpleString(ctx, "OK"); +} + +/* Timeout callback for auth command HELLOACL.AUTHASYNC */ +int HelloACL_Timeout(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + return RedisModule_ReplyWithSimpleString(ctx, "Request timedout"); +} + +/* Private data frees data for HELLOACL.AUTHASYNC command. */ +void HelloACL_FreeData(RedisModuleCtx *ctx, void *privdata) { + REDISMODULE_NOT_USED(ctx); + RedisModule_FreeString(NULL, privdata); +} + +/* Background authentication can happen here. */ +void *HelloACL_ThreadMain(void *args) { + void **targs = args; + RedisModuleBlockedClient *bc = targs[0]; + RedisModuleString *user = targs[1]; + RedisModule_Free(targs); + + RedisModule_UnblockClient(bc,user); + return NULL; +} + +/* HELLOACL.AUTHASYNC + * Asynchronously assigns an ACL user to the current context. */ +int AuthAsyncCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 2) return RedisModule_WrongArity(ctx); + + pthread_t tid; + RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx, HelloACL_Reply, HelloACL_Timeout, HelloACL_FreeData, TIMEOUT_TIME); + + + void **targs = RedisModule_Alloc(sizeof(void*)*2); + targs[0] = bc; + targs[1] = RedisModule_CreateStringFromString(NULL, argv[1]); + + if (pthread_create(&tid, NULL, HelloACL_ThreadMain, targs) != 0) { + RedisModule_AbortBlock(bc); + return RedisModule_ReplyWithError(ctx, "-ERR Can't start thread"); + } + + 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) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if (RedisModule_Init(ctx,"helloacl",1,REDISMODULE_APIVER_1) + == REDISMODULE_ERR) return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"helloacl.reset", + ResetCommand_RedisCommand,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"helloacl.revoke", + RevokeCommand_RedisCommand,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"helloacl.authglobal", + AuthGlobalCommand_RedisCommand,"no-auth",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"helloacl.authasync", + AuthAsyncCommand_RedisCommand,"no-auth",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + global = RedisModule_CreateModuleUser("global"); + RedisModule_SetModuleUserACL(global, "allcommands"); + RedisModule_SetModuleUserACL(global, "allkeys"); + RedisModule_SetModuleUserACL(global, "on"); + + global_auth_client_id = 0; + + return REDISMODULE_OK; +} diff --git a/src/networking.c b/src/networking.c index 901ce0a7b..37f8fa9b7 100644 --- a/src/networking.c +++ b/src/networking.c @@ -154,6 +154,9 @@ client *createClient(connection *conn) { c->peerid = NULL; c->client_list_node = NULL; c->client_tracking_redirection = 0; + c->auth_callback = NULL; + c->auth_callback_privdata = NULL; + c->auth_module = NULL; listSetFreeMethod(c->pubsub_patterns,decrRefCountVoid); listSetMatchMethod(c->pubsub_patterns,listMatchObjects); if (conn) linkClient(c); @@ -1051,6 +1054,9 @@ void freeClient(client *c) { c); } + /* Notify module system that this client auth status changed. */ + moduleNotifyUserChanged(c); + /* If it is our master that's beging disconnected we should make sure * to cache the state to try a partial resynchronization later. * diff --git a/src/redismodule.h b/src/redismodule.h index 02f267273..212988be9 100644 --- a/src/redismodule.h +++ b/src/redismodule.h @@ -395,6 +395,8 @@ typedef struct RedisModuleCommandFilter RedisModuleCommandFilter; typedef struct RedisModuleInfoCtx RedisModuleInfoCtx; typedef struct RedisModuleServerInfoData RedisModuleServerInfoData; typedef struct RedisModuleScanCursor RedisModuleScanCursor; +typedef struct RedisModuleUser RedisModuleUser; +typedef struct RedisModuleAuthCtx RedisModuleAuthCtx; typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc); typedef void (*RedisModuleDisconnectFunc)(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc); @@ -414,6 +416,7 @@ typedef void (*RedisModuleForkDoneHandler) (int exitcode, int bysignal, void *us typedef void (*RedisModuleInfoFunc)(RedisModuleInfoCtx *ctx, int for_crash_report); typedef void (*RedisModuleScanCB)(RedisModuleCtx *ctx, RedisModuleString *keyname, RedisModuleKey *key, void *privdata); typedef void (*RedisModuleScanKeyCB)(RedisModuleKey *key, RedisModuleString *field, RedisModuleString *value, void *privdata); +typedef void (*RedisModuleUserChangedFunc) (uint64_t client_id, void *privdata); #define REDISMODULE_TYPE_METHOD_VERSION 2 typedef struct RedisModuleTypeMethods { @@ -613,7 +616,7 @@ void REDISMODULE_API_FUNC(RedisModule_ScanCursorRestart)(RedisModuleScanCursor * void REDISMODULE_API_FUNC(RedisModule_ScanCursorDestroy)(RedisModuleScanCursor *cursor); int REDISMODULE_API_FUNC(RedisModule_Scan)(RedisModuleCtx *ctx, RedisModuleScanCursor *cursor, RedisModuleScanCB fn, void *privdata); int REDISMODULE_API_FUNC(RedisModule_ScanKey)(RedisModuleKey *key, RedisModuleScanCursor *cursor, RedisModuleScanKeyCB fn, void *privdata); - +#define REDISMODULE_EXPERIMENTAL_API /* Experimental APIs */ #ifdef REDISMODULE_EXPERIMENTAL_API #define REDISMODULE_EXPERIMENTAL_API_VERSION 3 @@ -660,6 +663,12 @@ int REDISMODULE_API_FUNC(RedisModule_ExitFromChild)(int retcode); int REDISMODULE_API_FUNC(RedisModule_KillForkChild)(int child_pid); float REDISMODULE_API_FUNC(RedisModule_GetUsedMemoryRatio)(); size_t REDISMODULE_API_FUNC(RedisModule_MallocSize)(void* ptr); +RedisModuleUser *REDISMODULE_API_FUNC(RedisModule_CreateModuleUser)(const char *name); +void REDISMODULE_API_FUNC(RedisModule_FreeModuleUser)(RedisModuleUser *user); +int REDISMODULE_API_FUNC(RedisModule_SetModuleUserACL)(RedisModuleUser *user, const char* acl); +int REDISMODULE_API_FUNC(RedisModule_AuthenticateClientWithACLUser)(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id); +int REDISMODULE_API_FUNC(RedisModule_AuthenticateClientWithUser)(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id); +void REDISMODULE_API_FUNC(RedisModule_DeauthenticateAndCloseClient)(RedisModuleCtx *ctx, uint64_t client_id); #endif #define RedisModule_IsAOFClient(id) ((id) == UINT64_MAX) @@ -891,6 +900,12 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int REDISMODULE_GET_API(KillForkChild); REDISMODULE_GET_API(GetUsedMemoryRatio); REDISMODULE_GET_API(MallocSize); + REDISMODULE_GET_API(CreateModuleUser); + REDISMODULE_GET_API(FreeModuleUser); + REDISMODULE_GET_API(SetModuleUserACL); + REDISMODULE_GET_API(DeauthenticateAndCloseClient); + REDISMODULE_GET_API(AuthenticateClientWithACLUser); + REDISMODULE_GET_API(AuthenticateClientWithUser); #endif if (RedisModule_IsModuleNameBusy && RedisModule_IsModuleNameBusy(name)) return REDISMODULE_ERR; diff --git a/src/sentinel.c b/src/sentinel.c index 42c4d2467..10117252d 100644 --- a/src/sentinel.c +++ b/src/sentinel.c @@ -461,8 +461,8 @@ struct redisCommand sentinelcmds[] = { {"role",sentinelRoleCommand,1,"ok-loading",0,NULL,0,0,0,0,0}, {"client",clientCommand,-2,"read-only no-script",0,NULL,0,0,0,0,0}, {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}, - {"auth",authCommand,2,"no-script ok-loading ok-stale fast",0,NULL,0,0,0,0,0}, - {"hello",helloCommand,-2,"no-script fast",0,NULL,0,0,0,0,0} + {"auth",authCommand,2,"no-auth no-script ok-loading ok-stale fast",0,NULL,0,0,0,0,0}, + {"hello",helloCommand,-2,"no-auth no-script fast",0,NULL,0,0,0,0,0} }; /* This function overwrites a few normal Redis config default with Sentinel diff --git a/src/server.c b/src/server.c index bcf7eee63..e38a17034 100644 --- a/src/server.c +++ b/src/server.c @@ -629,7 +629,7 @@ struct redisCommand redisCommandTable[] = { 0,NULL,0,0,0,0,0,0}, {"auth",authCommand,-2, - "no-script ok-loading ok-stale fast no-monitor no-slowlog @connection", + "no-auth no-script ok-loading ok-stale fast no-monitor no-slowlog @connection", 0,NULL,0,0,0,0,0,0}, /* We don't allow PING during loading since in Redis PING is used as @@ -824,7 +824,7 @@ struct redisCommand redisCommandTable[] = { 0,NULL,0,0,0,0,0,0}, {"hello",helloCommand,-2, - "no-script fast no-monitor no-slowlog @connection", + "no-auth no-script fast no-monitor no-slowlog @connection", 0,NULL,0,0,0,0,0,0}, /* EVAL can modify the dataset, however it is not flagged as a write @@ -2925,6 +2925,8 @@ int populateCommandTableParseFlags(struct redisCommand *c, char *strflags) { c->flags |= CMD_ASKING; } else if (!strcasecmp(flag,"fast")) { c->flags |= CMD_FAST | CMD_CATEGORY_FAST; + } else if (!strcasecmp(flag,"no-auth")) { + c->flags |= CMD_NO_AUTH; } else { /* Parse ACL categories here if the flag name starts with @. */ uint64_t catflag; @@ -3345,8 +3347,9 @@ int processCommand(client *c) { DefaultUser->flags & USER_FLAG_DISABLED) && !c->authenticated; if (auth_required) { - /* AUTH and HELLO are valid even in non authenticated state. */ - if (c->cmd->proc != authCommand && c->cmd->proc != helloCommand) { + /* AUTH and HELLO and no auth modules are valid even in + * non-authenticated state. */ + if (!(c->cmd->flags & CMD_NO_AUTH)) { flagTransaction(c); addReply(c,shared.noautherr); return C_OK; diff --git a/src/server.h b/src/server.h index f2c93241c..9c25a7ef5 100644 --- a/src/server.h +++ b/src/server.h @@ -166,33 +166,34 @@ typedef long long ustime_t; /* microsecond time type. */ #define CMD_SKIP_SLOWLOG (1ULL<<12) /* "no-slowlog" flag */ #define CMD_ASKING (1ULL<<13) /* "cluster-asking" flag */ #define CMD_FAST (1ULL<<14) /* "fast" flag */ +#define CMD_NO_AUTH (1ULL<<15) /* "no-auth" flag */ /* Command flags used by the module system. */ -#define CMD_MODULE_GETKEYS (1ULL<<15) /* Use the modules getkeys interface. */ -#define CMD_MODULE_NO_CLUSTER (1ULL<<16) /* Deny on Redis Cluster. */ +#define CMD_MODULE_GETKEYS (1ULL<<16) /* Use the modules getkeys interface. */ +#define CMD_MODULE_NO_CLUSTER (1ULL<<17) /* Deny on Redis Cluster. */ /* Command flags that describe ACLs categories. */ -#define CMD_CATEGORY_KEYSPACE (1ULL<<17) -#define CMD_CATEGORY_READ (1ULL<<18) -#define CMD_CATEGORY_WRITE (1ULL<<19) -#define CMD_CATEGORY_SET (1ULL<<20) -#define CMD_CATEGORY_SORTEDSET (1ULL<<21) -#define CMD_CATEGORY_LIST (1ULL<<22) -#define CMD_CATEGORY_HASH (1ULL<<23) -#define CMD_CATEGORY_STRING (1ULL<<24) -#define CMD_CATEGORY_BITMAP (1ULL<<25) -#define CMD_CATEGORY_HYPERLOGLOG (1ULL<<26) -#define CMD_CATEGORY_GEO (1ULL<<27) -#define CMD_CATEGORY_STREAM (1ULL<<28) -#define CMD_CATEGORY_PUBSUB (1ULL<<29) -#define CMD_CATEGORY_ADMIN (1ULL<<30) -#define CMD_CATEGORY_FAST (1ULL<<31) -#define CMD_CATEGORY_SLOW (1ULL<<32) -#define CMD_CATEGORY_BLOCKING (1ULL<<33) -#define CMD_CATEGORY_DANGEROUS (1ULL<<34) -#define CMD_CATEGORY_CONNECTION (1ULL<<35) -#define CMD_CATEGORY_TRANSACTION (1ULL<<36) -#define CMD_CATEGORY_SCRIPTING (1ULL<<37) +#define CMD_CATEGORY_KEYSPACE (1ULL<<18) +#define CMD_CATEGORY_READ (1ULL<<19) +#define CMD_CATEGORY_WRITE (1ULL<<20) +#define CMD_CATEGORY_SET (1ULL<<21) +#define CMD_CATEGORY_SORTEDSET (1ULL<<22) +#define CMD_CATEGORY_LIST (1ULL<<23) +#define CMD_CATEGORY_HASH (1ULL<<24) +#define CMD_CATEGORY_STRING (1ULL<<25) +#define CMD_CATEGORY_BITMAP (1ULL<<26) +#define CMD_CATEGORY_HYPERLOGLOG (1ULL<<27) +#define CMD_CATEGORY_GEO (1ULL<<28) +#define CMD_CATEGORY_STREAM (1ULL<<29) +#define CMD_CATEGORY_PUBSUB (1ULL<<30) +#define CMD_CATEGORY_ADMIN (1ULL<<31) +#define CMD_CATEGORY_FAST (1ULL<<32) +#define CMD_CATEGORY_SLOW (1ULL<<33) +#define CMD_CATEGORY_BLOCKING (1ULL<<34) +#define CMD_CATEGORY_DANGEROUS (1ULL<<35) +#define CMD_CATEGORY_CONNECTION (1ULL<<36) +#define CMD_CATEGORY_TRANSACTION (1ULL<<37) +#define CMD_CATEGORY_SCRIPTING (1ULL<<38) /* AOF states */ #define AOF_OFF 0 /* AOF is off */ @@ -480,6 +481,10 @@ typedef void (*moduleTypeDigestFunc)(struct RedisModuleDigest *digest, void *val typedef size_t (*moduleTypeMemUsageFunc)(const void *value); typedef void (*moduleTypeFreeFunc)(void *value); +/* TODO */ +typedef void (*RedisModuleUserChangedFunc) (uint64_t client_id, void *privdata); + + /* The module type, which is referenced in each value of a given type, defines * the methods and links to the module exporting the type. */ typedef struct RedisModuleType { @@ -798,6 +803,13 @@ typedef struct client { list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */ sds peerid; /* Cached peer ID. */ listNode *client_list_node; /* list node in client list */ + RedisModuleUserChangedFunc auth_callback; /* Callback to execute when the + * authentication changes */ + void *auth_callback_privdata; /* Private data that is passed when the auth + * callback is executed */ + void *auth_module; /* The module that owns the callback, which is used + * to disconnect the client if the module is + * unloaded to allow for cleanup. */ /* If this client is in tracking mode and this field is non zero, * invalidation messages for keys fetched by this client will be send to @@ -1516,6 +1528,7 @@ void processModuleLoadingProgressEvent(int is_aof); int moduleTryServeClientBlockedOnKey(client *c, robj *key); void moduleUnblockClient(client *c); int moduleClientIsBlockedOnKeys(client *c); +void moduleNotifyUserChanged(client *c); /* Utils */ long long ustime(void); @@ -1809,6 +1822,8 @@ int ACLLoadConfiguredUsers(void); sds ACLDescribeUser(user *u); void ACLLoadUsersAtStartup(void); void addReplyCommandCategories(client *c, struct redisCommand *cmd); +user *ACLCreateUnlinkedUser(); +void ACLFreeUserAndKillClients(user *u); /* Sorted sets data type */ diff --git a/tests/modules/Makefile b/tests/modules/Makefile index f33f9e80e..363231a87 100644 --- a/tests/modules/Makefile +++ b/tests/modules/Makefile @@ -21,7 +21,8 @@ TEST_MODULES = \ hooks.so \ blockonkeys.so \ scan.so \ - datatype.so + datatype.so \ + auth.so .PHONY: all diff --git a/tests/modules/auth.c b/tests/modules/auth.c new file mode 100644 index 000000000..ad4366e47 --- /dev/null +++ b/tests/modules/auth.c @@ -0,0 +1,118 @@ +/* ACL API example - An example of performing custom password authentication + * + * ----------------------------------------------------------------------------- + * + * Copyright 2019 Amazon.com, Inc. or its affiliates. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#define REDISMODULE_EXPERIMENTAL_API +#include "redismodule.h" + +// A simple global user +static RedisModuleUser *global; +static long long client_change_delta = 0; + +void UserChangedCallback(uint64_t client_id, void *privdata) { + REDISMODULE_NOT_USED(privdata); + REDISMODULE_NOT_USED(client_id); + client_change_delta++; +} + +int Auth_CreateModuleUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + + if (global) { + RedisModule_FreeModuleUser(global); + } + + global = RedisModule_CreateModuleUser("global"); + RedisModule_SetModuleUserACL(global, "allcommands"); + RedisModule_SetModuleUserACL(global, "allkeys"); + RedisModule_SetModuleUserACL(global, "on"); + + return RedisModule_ReplyWithSimpleString(ctx, "OK"); +} + +int Auth_AuthModuleUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + uint64_t client_id; + RedisModule_AuthenticateClientWithUser(ctx, global, UserChangedCallback, NULL, &client_id); + + return RedisModule_ReplyWithLongLong(ctx, (uint64_t) client_id); +} + +int Auth_AuthRealUser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc != 2) return RedisModule_WrongArity(ctx); + + size_t length; + uint64_t client_id; + + RedisModuleString *user_string = argv[1]; + const char *name = RedisModule_StringPtrLen(user_string, &length); + + if (RedisModule_AuthenticateClientWithACLUser(ctx, name, length, + UserChangedCallback, NULL, &client_id) == REDISMODULE_ERR) { + return RedisModule_ReplyWithError(ctx, "Invalid user"); + } + + return RedisModule_ReplyWithLongLong(ctx, (uint64_t) client_id); +} + +int Auth_ChangeCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + long long result = client_change_delta; + client_change_delta = 0; + return RedisModule_ReplyWithLongLong(ctx, result); +} + +/* This function must be present on each Redis module. It is used in order to + * register the commands into the Redis server. */ +int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + + if (RedisModule_Init(ctx,"testacl",1,REDISMODULE_APIVER_1) + == REDISMODULE_ERR) return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"auth.authrealuser", + Auth_AuthRealUser,"no-auth",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"auth.createmoduleuser", + Auth_CreateModuleUser,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"auth.authmoduleuser", + Auth_AuthModuleUser,"no-auth",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + if (RedisModule_CreateCommand(ctx,"auth.changecount", + Auth_ChangeCount,"",0,0,0) == REDISMODULE_ERR) + return REDISMODULE_ERR; + + client_change_delta = 0; + + return REDISMODULE_OK; +} diff --git a/tests/unit/moduleapi/auth.tcl b/tests/unit/moduleapi/auth.tcl new file mode 100644 index 000000000..04a90a496 --- /dev/null +++ b/tests/unit/moduleapi/auth.tcl @@ -0,0 +1,71 @@ +set testmodule [file normalize tests/modules/auth.so] + +start_server {tags {"modules"}} { + r module load $testmodule + + test {Modules can create a user that can be authenticated} { + # Make sure we start authenticated with default user + r auth default "" + assert_equal [r acl whoami] "default" + r auth.createmoduleuser + + set id [r auth.authmoduleuser] + assert_equal [r client id] $id + + # Verify returned id is the same as our current id and + # we are authenticated with the specified user + assert_equal [r acl whoami] "global" + } + + test {De-authenticating clients is tracked and kills clients} { + assert_equal [r auth.changecount] 0 + r auth.createmoduleuser + + # Catch the I/O exception that was thrown when Redis + # disconnected with us. + catch { [r ping] } e + assert_match {*I/O*} $e + + # Check that a user change was registered + assert_equal [r auth.changecount] 1 + } + + test {Modules cant authenticate with ACLs users that dont exist} { + catch { [r auth.authrealuser auth-module-test-fake] } e + assert_match {*Invalid user*} $e + } + + test {Modules can authenticate with ACL users} { + assert_equal [r acl whoami] "default" + + # Create user to auth into + r acl setuser auth-module-test on allkeys allcommands + + set id [r auth.authrealuser auth-module-test] + + # Verify returned id is the same as our current id and + # we are authenticated with the specified user + assert_equal [r client id] $id + assert_equal [r acl whoami] "auth-module-test" + } + + test {Client callback is called on user switch} { + assert_equal [r auth.changecount] 0 + + # Auth again and validate change count + r auth.authrealuser auth-module-test + assert_equal [r auth.changecount] 1 + + # Re-auth with the default user + r auth default "" + assert_equal [r auth.changecount] 1 + assert_equal [r acl whoami] "default" + + # Re-auth with the default user again, to + # verify the callback isn't fired again + r auth default "" + assert_equal [r auth.changecount] 0 + assert_equal [r acl whoami] "default" + } + +}
\ No newline at end of file |