summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSalvatore Sanfilippo <antirez@gmail.com>2019-12-17 09:58:26 +0100
committerGitHub <noreply@github.com>2019-12-17 09:58:26 +0100
commitb7c78b7651c5458ccf5d95ef5857ec427b927a27 (patch)
treea2dc46e4dfcbf8adeb309bba7b0e05e4a0170a07
parent4348d25fc45706ffd611d251d961eb3f1bf1ebde (diff)
parent7f04a15311b1dfe25f8f63c7a6dabce5a12d55ed (diff)
downloadredis-b7c78b7651c5458ccf5d95ef5857ec427b927a27.tar.gz
Merge pull request #5916 from madolson/dev-unstable-acl-module-pr
Add module APIs for custom authentication
-rwxr-xr-xruntest-moduleapi1
-rw-r--r--src/acl.c1
-rw-r--r--src/module.c220
-rw-r--r--src/modules/Makefile7
-rw-r--r--src/modules/helloacl.c191
-rw-r--r--src/networking.c6
-rw-r--r--src/redismodule.h15
-rw-r--r--src/sentinel.c4
-rw-r--r--src/server.c11
-rw-r--r--src/server.h64
-rw-r--r--tests/modules/Makefile3
-rw-r--r--tests/modules/auth.c90
-rw-r--r--tests/unit/moduleapi/auth.tcl71
13 files changed, 652 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 \
"${@}"
diff --git a/src/acl.c b/src/acl.c
index 4c43add14..db742c649 100644
--- a/src/acl.c
+++ b/src/acl.c
@@ -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 a4e7cbe2c..1fda29625 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;
@@ -5247,6 +5266,200 @@ 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 invokes the
+ * client's user 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 a 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 the state
+ * so all references are cleared here. */
+ c->auth_callback = NULL;
+ c->auth_callback_privdata = NULL;
+ c->auth_module = NULL;
+ }
+}
+
+void revokeClientAuthentication(client *c) {
+ /* 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(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 clients it has authenticated. */
+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 and 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;
+ }
+
+ 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, 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, 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 is returned from the RM_AuthenticateClientWithUser and
+ * RM_AuthenticateClientWithACLUser APIs, but can be obtained through
+ * the CLIENT api or through 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
@@ -7088,6 +7301,7 @@ int moduleUnload(sds name) {
}
}
+ moduleFreeAuthenticatedClients(module);
moduleUnregisterCommands(module);
moduleUnregisterSharedAPI(module);
moduleUnregisterUsedAPI(module);
@@ -7571,4 +7785,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..6766c0a58
--- /dev/null
+++ b/src/modules/helloacl.c
@@ -0,0 +1,191 @@
+/* ACL API example - An example for performing custom synchronous and
+ * asynchronous 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 <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_DeauthenticateAndCloseClient(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..61899f139 100644
--- a/src/redismodule.h
+++ b/src/redismodule.h
@@ -395,6 +395,7 @@ typedef struct RedisModuleCommandFilter RedisModuleCommandFilter;
typedef struct RedisModuleInfoCtx RedisModuleInfoCtx;
typedef struct RedisModuleServerInfoData RedisModuleServerInfoData;
typedef struct RedisModuleScanCursor RedisModuleScanCursor;
+typedef struct RedisModuleUser RedisModuleUser;
typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
typedef void (*RedisModuleDisconnectFunc)(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc);
@@ -414,6 +415,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 +615,6 @@ 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);
-
/* Experimental APIs */
#ifdef REDISMODULE_EXPERIMENTAL_API
#define REDISMODULE_EXPERIMENTAL_API_VERSION 3
@@ -660,6 +661,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 +898,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 b5e51002b..7a78c884f 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,11 @@ typedef void (*moduleTypeDigestFunc)(struct RedisModuleDigest *digest, void *val
typedef size_t (*moduleTypeMemUsageFunc)(const void *value);
typedef void (*moduleTypeFreeFunc)(void *value);
+/* A callback that is called when the client authentication changes. This
+ * needs to be exposed since you can't cast a function pointer to (void *) */
+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 +804,15 @@ 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; /* Module callback to execute
+ * when the authenticated user
+ * changes. */
+ void *auth_callback_privdata; /* Private data that is passed when the auth
+ * changed callback is executed. Opaque for
+ * Redis Core. */
+ void *auth_module; /* The module that owns the callback, which is used
+ * to disconnect the client if the module is
+ * unloaded for cleanup. Opaque for Redis Core.*/
/* 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
@@ -1518,6 +1533,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);
@@ -1811,6 +1827,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..b13c10385
--- /dev/null
+++ b/tests/modules/auth.c
@@ -0,0 +1,90 @@
+#define REDISMODULE_EXPERIMENTAL_API
+#include "redismodule.h"
+
+// A simple global user
+static RedisModuleUser *global = NULL;
+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) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(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) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(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) {
+ REDISMODULE_NOT_USED(argv);
+ REDISMODULE_NOT_USED(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;
+
+ 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