Skip to content

Commit 034dcf1

Browse files
committed
Add module APIs for custom authentication
1 parent e9b99c7 commit 034dcf1

File tree

13 files changed

+685
-32
lines changed

13 files changed

+685
-32
lines changed

runtest-moduleapi

+1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ $TCLSH tests/test_helper.tcl \
2424
--single unit/moduleapi/blockonkeys \
2525
--single unit/moduleapi/scan \
2626
--single unit/moduleapi/datatype \
27+
--single unit/moduleapi/auth \
2728
"${@}"

src/acl.c

+1
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,7 @@ int ACLAuthenticateUser(client *c, robj *username, robj *password) {
975975
if (ACLCheckUserCredentials(username,password) == C_OK) {
976976
c->authenticated = 1;
977977
c->user = ACLGetUserByName(username->ptr,sdslen(username->ptr));
978+
moduleNotifyUserChanged(c);
978979
return C_OK;
979980
} else {
980981
return C_ERR;

src/module.c

+222
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,21 @@ list *RedisModule_EventListeners; /* Global list of all the active events. */
355355
unsigned long long ModulesInHooks = 0; /* Total number of modules in hooks
356356
callbacks right now. */
357357

358+
/* Data structures related to the redis module users */
359+
360+
/* This callback type is called by moduleNotifyUserChanged() every time
361+
* a user authenticated via the module API is associated with a different
362+
* user or gets disconnected. */
363+
typedef void (*RedisModuleUserChangedFunc) (uint64_t client_id, void *privdata);
364+
365+
/* This is the object returned by RM_CreateModuleUser(). The module API is
366+
* able to create users, set ACLs to such users, and later authenticate
367+
* clients using such newly created users. */
368+
typedef struct RedisModuleUser {
369+
user *user; /* Reference to the real redis user */
370+
} RedisModuleUser;
371+
372+
358373
/* --------------------------------------------------------------------------
359374
* Prototypes
360375
* -------------------------------------------------------------------------- */
@@ -719,6 +734,7 @@ int commandFlagsFromString(char *s) {
719734
else if (!strcasecmp(t,"allow-stale")) flags |= CMD_STALE;
720735
else if (!strcasecmp(t,"no-monitor")) flags |= CMD_SKIP_MONITOR;
721736
else if (!strcasecmp(t,"fast")) flags |= CMD_FAST;
737+
else if (!strcasecmp(t,"no-auth")) flags |= CMD_NO_AUTH;
722738
else if (!strcasecmp(t,"getkeys-api")) flags |= CMD_MODULE_GETKEYS;
723739
else if (!strcasecmp(t,"no-cluster")) flags |= CMD_MODULE_NO_CLUSTER;
724740
else break;
@@ -780,6 +796,9 @@ int commandFlagsFromString(char *s) {
780796
* example, is unable to report the position of the
781797
* keys, programmatically creates key names, or any
782798
* other reason.
799+
* * **"no-auth"**: This command can be run by an un-authenticated client.
800+
* Normally this is used by a command that is used
801+
* to authenticate a client.
783802
*/
784803
int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) {
785804
int flags = strflags ? commandFlagsFromString((char*)strflags) : 0;
@@ -5236,6 +5255,202 @@ int RM_GetTimerInfo(RedisModuleCtx *ctx, RedisModuleTimerID id, uint64_t *remain
52365255
return REDISMODULE_OK;
52375256
}
52385257

5258+
/* --------------------------------------------------------------------------
5259+
* Modules ACL API
5260+
*
5261+
* Implements a hook into the authentication and authorization within Redis.
5262+
* --------------------------------------------------------------------------*/
5263+
5264+
/* This function is called when a client's user has changed and invoked a
5265+
* a modules client changed callback if it was set. This callback should
5266+
* cleanup any state the module was tracking about this client.
5267+
*
5268+
* A client's user can be changed through the AUTH command, module
5269+
* authentication, and when the client is freed. */
5270+
void moduleNotifyUserChanged(client *c) {
5271+
if (c->auth_callback) {
5272+
c->auth_callback(c->id, c->auth_callback_privdata);
5273+
5274+
/* The callback will fire exactly once, even if the user remains
5275+
* the same, it is expected to completely clean up it's state
5276+
* so all references are removed */
5277+
c->auth_callback = NULL;
5278+
c->auth_callback_privdata = NULL;
5279+
c->auth_module = NULL;
5280+
}
5281+
}
5282+
5283+
void revokeClientAuthentication(client *c) {
5284+
/* Fire the client changed handler now in case we are unloading the module
5285+
* and need to cleanup. */
5286+
moduleNotifyUserChanged(c);
5287+
5288+
c->user = DefaultUser;
5289+
c->authenticated = 0;
5290+
freeClientAsync(c);
5291+
}
5292+
5293+
/* Cleanup all clients that have been authenticated with this module. This
5294+
* is called from onUnload() to give the module a chance to cleanup any
5295+
* resources associated with the authentication. */
5296+
static void moduleFreeAuthenticatedClients(RedisModule *module) {
5297+
listIter li;
5298+
listNode *ln;
5299+
listRewind(server.clients,&li);
5300+
while ((ln = listNext(&li)) != NULL) {
5301+
client *c = listNodeValue(ln);
5302+
if (!c->auth_module) continue;
5303+
5304+
RedisModule *auth_module = (RedisModule *) c->auth_module;
5305+
if (auth_module == module) {
5306+
revokeClientAuthentication(c);
5307+
}
5308+
}
5309+
}
5310+
5311+
/* Creates a Redis ACL user that the module can use to authenticate a client.
5312+
* After obtaining the user, the module should set what such user can do
5313+
* using the RM_SetUserACL() function. Once configured, the user
5314+
* can be used in order to authenticate a connection, with the specified
5315+
* ACL rules, using the RedisModule_AuthClientWithUser() function.
5316+
*
5317+
* Note that:
5318+
*
5319+
* * Users created here are not listed by the ACL command.
5320+
* * Users created here are not checked for duplicated name, so it's up to
5321+
* the module calling this function to take care of not creating users
5322+
* with the same name.
5323+
* * The created user can be used to authenticate multiple Redis connections.
5324+
*
5325+
* The caller can later free the user using the function
5326+
* RM_FreeModuleUser(). When this function is called, if there are
5327+
* still clients authenticated with this user, they are disconnected.
5328+
* The function to free the user should only be used when the caller really
5329+
* wants to invalidate the user to define a new one with different
5330+
* capabilities. */
5331+
RedisModuleUser *RM_CreateModuleUser(const char *name) {
5332+
RedisModuleUser *new_user = zmalloc(sizeof(RedisModuleUser));
5333+
new_user->user = ACLCreateUnlinkedUser();
5334+
5335+
/* Free the previous temporarily assigned name to assign the new one */
5336+
sdsfree(new_user->user->name);
5337+
new_user->user->name = sdsnew(name);
5338+
return new_user;
5339+
}
5340+
5341+
/* Frees a given user and disconnects all of the clients that have been
5342+
* authenticated with it. See RM_CreateModuleUser for detailed usage.*/
5343+
int RM_FreeModuleUser(RedisModuleUser *user) {
5344+
ACLFreeUserAndKillClients(user->user);
5345+
zfree(user);
5346+
return REDISMODULE_OK;
5347+
}
5348+
5349+
/* Sets the permissions of a user created through the redis module
5350+
* interface. The syntax is the same as ACL SETUSER, so refer to the
5351+
* documentation in acl.c for more information. See RM_CreateModuleUser
5352+
* for detailed usage.
5353+
*
5354+
* Returns REDISMODULE_OK on success and REDISMODULE_ERR on failure
5355+
* and will set an errno describing why the operation failed. */
5356+
int RM_SetModuleUserACL(RedisModuleUser *user, const char* acl) {
5357+
return ACLSetUser(user->user, acl, -1);
5358+
}
5359+
5360+
/* Authenticate the client associated with the context with
5361+
* the provided user. Returns REDISMODULE_OK on success and
5362+
* REDISMODULE_ERR on error.
5363+
*
5364+
* This authentication can be tracked with the optional callback and private
5365+
* data fields. The callback will be called whenever the user of the client
5366+
* changes. This callback should be used to cleanup any state that is being
5367+
* kept in the module related to the client authentication. It will only be
5368+
* called once, even when the user hasn't changed, in order to allow for a
5369+
* new callback to be specified. If this authentication does not need to be
5370+
* tracked, pass in NULL for the callback and privdata.
5371+
*
5372+
* If client_id is not NULL, it will be filled with the id of the client
5373+
* that was authenticated. This can be used with the
5374+
* RM_DeauthenticateAndCloseClient() API in order to deauthenticate a
5375+
* previously authenticated client if the authentication is no longer valid.
5376+
*
5377+
* For expensive authentication operations, it is recommended to block the
5378+
* client and do the authentication in the background then attach the user
5379+
* to the client in a threadsafe context. */
5380+
static int authenticateClientWithUser(RedisModuleCtx *ctx, user *user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) {
5381+
if (user->flags & USER_FLAG_DISABLED) {
5382+
return REDISMODULE_ERR;
5383+
}
5384+
5385+
/* Freeing the client would result in moduleNotifyUserChanged() to be
5386+
* called later, however since we use revokeClientAuthentication() also
5387+
* in moduleFreeAuthenticatedClients() to implement module unloading, we
5388+
* do this action ASAP: this way if the module is unloaded, when the client
5389+
* is eventually freed we don't rely on the module to still exist. */
5390+
moduleNotifyUserChanged(ctx->client);
5391+
5392+
ctx->client->user = user;
5393+
ctx->client->authenticated = 1;
5394+
5395+
if (callback) {
5396+
ctx->client->auth_callback = callback;
5397+
ctx->client->auth_callback_privdata = privdata;
5398+
ctx->client->auth_module = ctx->module;
5399+
}
5400+
5401+
if (client_id) {
5402+
*client_id = ctx->client->id;
5403+
}
5404+
5405+
return REDISMODULE_OK;
5406+
}
5407+
5408+
5409+
/* Authenticate the current context's user with the provided redis acl user.
5410+
* Returns REDISMODULE_ERR if the user is disabled.
5411+
*
5412+
* See authenticateClientWithUser for information about callback and client_id,
5413+
* and general usage for authentication. */
5414+
int RM_AuthenticateClientWithUser(RedisModuleCtx *ctx, RedisModuleUser *module_user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) {
5415+
return authenticateClientWithUser(ctx, module_user->user, callback, privdata, client_id);
5416+
}
5417+
5418+
/* Authenticate the current context's user with the provided redis acl user.
5419+
* Returns REDISMODULE_ERR if the user is disabled or the user does not exist.
5420+
*
5421+
* See authenticateClientWithUser for information about callback and client_id,
5422+
* and general usage for authentication. */
5423+
int RM_AuthenticateClientWithACLUser(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) {
5424+
user *acl_user = ACLGetUserByName(name, len);
5425+
5426+
if (!acl_user) {
5427+
return REDISMODULE_ERR;
5428+
}
5429+
return authenticateClientWithUser(ctx, acl_user, callback, privdata, client_id);
5430+
}
5431+
5432+
/* Deauthenticate and close the client. The client resources will not be
5433+
* be immediately freed, but will be cleaned up in a background job. This is
5434+
* the recommended way to deauthenicate a client since most clients can't
5435+
* handle users becomming deauthenticated. Returns REDISMODULE_ERR when the
5436+
* client doesn't exist and REDISMODULE_OK when the operation was successful.
5437+
*
5438+
* The client ID can be obtained from the AuthenticateClientWithUser and
5439+
* AuthenticateClientWithACLUser APIs or through other APIs such as
5440+
* server events.
5441+
*
5442+
* This function is not thread safe, and must be executed within the context
5443+
* of a command or thread safe context. */
5444+
int RM_DeauthenticateAndCloseClient(RedisModuleCtx *ctx, uint64_t client_id) {
5445+
UNUSED(ctx);
5446+
client *c = lookupClientByID(client_id);
5447+
if (c == NULL) return REDISMODULE_ERR;
5448+
5449+
/* Revoke also marks client to be closed ASAP */
5450+
revokeClientAuthentication(c);
5451+
return REDISMODULE_OK;
5452+
}
5453+
52395454
/* --------------------------------------------------------------------------
52405455
* Modules Dictionary API
52415456
*
@@ -7078,6 +7293,7 @@ int moduleUnload(sds name) {
70787293
}
70797294
}
70807295

7296+
moduleFreeAuthenticatedClients(module);
70817297
moduleUnregisterCommands(module);
70827298
moduleUnregisterSharedAPI(module);
70837299
moduleUnregisterUsedAPI(module);
@@ -7561,4 +7777,10 @@ void moduleRegisterCoreAPI(void) {
75617777
REGISTER_API(ScanCursorRestart);
75627778
REGISTER_API(Scan);
75637779
REGISTER_API(ScanKey);
7780+
REGISTER_API(CreateModuleUser);
7781+
REGISTER_API(SetModuleUserACL);
7782+
REGISTER_API(FreeModuleUser);
7783+
REGISTER_API(DeauthenticateAndCloseClient);
7784+
REGISTER_API(AuthenticateClientWithACLUser);
7785+
REGISTER_API(AuthenticateClientWithUser);
75647786
}

src/modules/Makefile

+6-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ endif
1313

1414
.SUFFIXES: .c .so .xo .o
1515

16-
all: helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so hellohook.so
16+
all: helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so hellohook.so helloacl.so
1717

1818
.c.xo:
1919
$(CC) -I. $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@
@@ -53,6 +53,11 @@ hellohook.xo: ../redismodule.h
5353
hellohook.so: hellohook.xo
5454
$(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc
5555

56+
helloacl.xo: ../redismodule.h
57+
58+
helloacl.so: helloacl.xo
59+
$(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc
60+
5661
testmodule.xo: ../redismodule.h
5762

5863
testmodule.so: testmodule.xo

0 commit comments

Comments
 (0)