Unverified Commit f8a5a4f7 authored by KarthikSubbarao's avatar KarthikSubbarao Committed by GitHub
Browse files

Custom authentication for Modules (#11659)



This change adds new module callbacks that can override the default password based authentication associated with ACLs. With this, Modules can register auth callbacks through which they can implement their own Authentication logic. When `AUTH` and `HELLO AUTH ...` commands are used, Module based authentication is attempted and then normal password based authentication is attempted if needed.
The new Module APIs added in this PR are - `RM_RegisterCustomAuthCallback` and `RM_BlockClientOnAuth` and `RedisModule_ACLAddLogEntryByUserName `.

Module based authentication will be attempted for all Redis users (created through the ACL SETUSER cmd or through Module APIs) even if the Redis user does not exist at the time of the command. This gives a chance for the Module to create the RedisModule user and then authenticate via the RedisModule API - from the custom auth callback.

For the AUTH command, we will support both variations - `AUTH <username> <password>` and `AUTH <password>`. In case of the `AUTH <password>` variation, the custom auth callbacks are triggered with “default” as the username and password as what is provided.


### RedisModule_RegisterCustomAuthCallback
```
void RM_RegisterCustomAuthCallback(RedisModuleCtx *ctx, RedisModuleCustomAuthCallback cb) {
```
This API registers a callback to execute to prior to normal password based authentication. Multiple callbacks can be registered across different modules. These callbacks are responsible for either handling the authentication, each authenticating the user or explicitly denying, or deferring it to other authentication mechanisms. Callbacks are triggered in the order they were registered. When a Module is unloaded, all the auth callbacks registered by it are unregistered. The callbacks are attempted, in the order of most recently registered callbacks, when the AUTH/HELLO (with AUTH field is provided) commands are called. The callbacks will be called with a module context along with a username and a password, and are expected to take one of the following actions:

 (1) Authenticate - Use the RM_Authenticate* API successfully and return `REDISMODULE_AUTH_HANDLED`. This will immediately end the auth chain as successful and add the OK reply.
(2) Block a client on authentication - Use the `RM_BlockClientOnAuth` API and return `REDISMODULE_AUTH_HANDLED`. Here, the client will be blocked until the `RM_UnblockClient `API is used which will trigger the auth reply callback (provided earlier through the `RM_BlockClientOnAuth`). In this reply callback, the Module should authenticate, deny or skip handling authentication.
(3) Deny Authentication - Return `REDISMODULE_AUTH_HANDLED` without authenticating or blocking the client. Optionally, `err` can be set to a custom error message. This will immediately end the auth chain as unsuccessful and add the ERR reply.
(4) Skip handling Authentication - Return `REDISMODULE_AUTH_NOT_HANDLED` without blocking the client. This will allow the engine to attempt the next custom auth callback.

If none of the callbacks authenticate or deny auth, then password based auth is attempted and will authenticate or add failure logs and reply to the clients accordingly.

### RedisModule_BlockClientOnAuth
```
RedisModuleBlockedClient *RM_BlockClientOnAuth(RedisModuleCtx *ctx, RedisModuleCustomAuthCallback reply_callback,
                                               void (*free_privdata)(RedisModuleCtx*,void*))
```
This API can only be used from a Module from the custom auth callback. If a client is not in the middle of custom module based authentication, ERROR is returned. Otherwise, the client is blocked and the `RedisModule_BlockedClient` is returned similar to the `RedisModule_BlockClient` API.

### RedisModule_ACLAddLogEntryByUserName
```
int RM_ACLAddLogEntryByUserName(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *object, RedisModuleACLLogEntryReason reason)
```
Adds a new entry in the ACL log with the `username` RedisModuleString provided. This simplifies the Module usage because now, developers do not need to create a Module User just to add an error ACL Log entry. Aside from accepting username (RedisModuleString) instead of a RedisModuleUser, it is the same as the existing `RedisModule_ACLAddLogEntry` API.


### Breaking changes
- HELLO command - Clients can now only set the client name and RESP protocol from the `HELLO` command if they are authenticated. Also, we now finish command arg validation first and return early with a ERR reply if any arg is invalid. This is to avoid mutating the client name / RESP from a command that would have failed on invalid arguments.

### Notable behaviors
- Module unblocking - Now, we will not allow Modules to block the client from inside the context of a reply callback (triggered from the Module unblock flow `moduleHandleBlockedClients`).

---------
Co-authored-by: default avatarMadelyn Olson <34459052+madolson@users.noreply.github.com>
parent 58285a6e
...@@ -52,4 +52,5 @@ $TCLSH tests/test_helper.tcl \ ...@@ -52,4 +52,5 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/publish \ --single unit/moduleapi/publish \
--single unit/moduleapi/usercall \ --single unit/moduleapi/usercall \
--single unit/moduleapi/postnotifications \ --single unit/moduleapi/postnotifications \
--single unit/moduleapi/moduleauth \
"${@}" "${@}"
...@@ -1406,24 +1406,50 @@ int ACLCheckUserCredentials(robj *username, robj *password) { ...@@ -1406,24 +1406,50 @@ int ACLCheckUserCredentials(robj *username, robj *password) {
return C_ERR; 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 /* This is like ACLCheckUserCredentials(), however if the user/pass
* are correct, the connection is put in authenticated state and the * are correct, the connection is put in authenticated state and the
* connection user reference is populated. * connection user reference is populated.
* *
* The return value is C_OK or C_ERR with the same meaning as * The return value is AUTH_OK on success (valid username / password pair) & AUTH_ERR otherwise. */
* ACLCheckUserCredentials(). */ int checkPasswordBasedAuth(client *c, robj *username, robj *password) {
int ACLAuthenticateUser(client *c, robj *username, robj *password) {
if (ACLCheckUserCredentials(username,password) == C_OK) { if (ACLCheckUserCredentials(username,password) == C_OK) {
c->authenticated = 1; c->authenticated = 1;
c->user = ACLGetUserByName(username->ptr,sdslen(username->ptr)); c->user = ACLGetUserByName(username->ptr,sdslen(username->ptr));
moduleNotifyUserChanged(c); moduleNotifyUserChanged(c);
return C_OK; return AUTH_OK;
} else { } else {
addACLLogEntry(c,ACL_DENIED_AUTH,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,0,username->ptr,NULL); 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 /* 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 * 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 * should have an assigned ID (that is used to index the bitmap). This function
...@@ -3046,11 +3072,14 @@ void authCommand(client *c) { ...@@ -3046,11 +3072,14 @@ void authCommand(client *c) {
redactClientCommandArgument(c, 2); redactClientCommandArgument(c, 2);
} }
if (ACLAuthenticateUser(c,username,password) == C_OK) { robj *err = NULL;
addReply(c,shared.ok); int result = ACLAuthenticateUser(c, username, password, &err);
} else { if (result == AUTH_OK) {
addReplyError(c,"-WRONGPASS invalid username-password pair or user is disabled."); 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 /* Set the password for the "default" ACL user. This implements supports for
......
This diff is collapsed.
...@@ -207,6 +207,8 @@ client *createClient(connection *conn) { ...@@ -207,6 +207,8 @@ client *createClient(connection *conn) {
c->client_tracking_prefixes = NULL; c->client_tracking_prefixes = NULL;
c->last_memory_usage = 0; c->last_memory_usage = 0;
c->last_memory_type = CLIENT_TYPE_NORMAL; c->last_memory_type = CLIENT_TYPE_NORMAL;
c->module_blocked_client = NULL;
c->module_auth_ctx = NULL;
c->auth_callback = NULL; c->auth_callback = NULL;
c->auth_callback_privdata = NULL; c->auth_callback_privdata = NULL;
c->auth_module = NULL; c->auth_module = NULL;
...@@ -1534,6 +1536,9 @@ void freeClient(client *c) { ...@@ -1534,6 +1536,9 @@ void freeClient(client *c) {
/* Notify module system that this client auth status changed. */ /* Notify module system that this client auth status changed. */
moduleNotifyUserChanged(c); 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 /* 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 * from the queue. Note that we need to do this here, because later
* we may call replicationCacheMaster() and the client should already * we may call replicationCacheMaster() and the client should already
...@@ -2809,27 +2814,39 @@ sds getAllClientsInfoString(int type) { ...@@ -2809,27 +2814,39 @@ sds getAllClientsInfoString(int type) {
return o; return o;
} }
/* Returns C_OK if the name has been set or C_ERR if the name is invalid. */ /* Returns C_OK if the name is valid. Returns C_ERR & sets `err` (when provided) otherwise. */
int clientSetName(client *c, robj *name) { 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; int len = (name != NULL) ? sdslen(name->ptr) : 0;
/* We allow setting the client name to an empty string. */
/* Setting the client name to an empty string actually removes if (len == 0)
* the current name. */
if (len == 0) {
if (c->name) decrRefCount(c->name);
c->name = NULL;
return C_OK; return C_OK;
}
/* Otherwise check if the charset is ok. We need to do this otherwise /* Otherwise check if the charset is ok. We need to do this otherwise
* CLIENT LIST format will break. You should always be able to * CLIENT LIST format will break. You should always be able to
* split by space to get the different fields. */ * split by space to get the different fields. */
char *p = name->ptr; char *p = name->ptr;
for (int j = 0; j < len; j++) { for (int j = 0; j < len; j++) {
if (p[j] < '!' || p[j] > '~') { /* ASCII is assumed. */ if (p[j] < '!' || p[j] > '~') { /* ASCII is assumed. */
if (err) *err = err_msg;
return C_ERR; 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); if (c->name) decrRefCount(c->name);
c->name = name; c->name = name;
incrRefCount(name); incrRefCount(name);
...@@ -2846,11 +2863,10 @@ int clientSetName(client *c, robj *name) { ...@@ -2846,11 +2863,10 @@ int clientSetName(client *c, robj *name) {
* *
* This function is also used to implement the HELLO SETNAME option. */ * This function is also used to implement the HELLO SETNAME option. */
int clientSetNameOrReply(client *c, robj *name) { 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) { if (result == C_ERR) {
addReplyError(c, addReplyError(c, err);
"Client names cannot contain spaces, "
"newlines or special characters.");
} }
return result; return result;
} }
...@@ -3434,19 +3450,25 @@ void helloCommand(client *c) { ...@@ -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++) { for (int j = next_arg; j < c->argc; j++) {
int moreargs = (c->argc-1) - j; int moreargs = (c->argc-1) - j;
const char *opt = c->argv[j]->ptr; const char *opt = c->argv[j]->ptr;
if (!strcasecmp(opt,"AUTH") && moreargs >= 2) { if (!strcasecmp(opt,"AUTH") && moreargs >= 2) {
redactClientCommandArgument(c, j+1); redactClientCommandArgument(c, j+1);
redactClientCommandArgument(c, j+2); redactClientCommandArgument(c, j+2);
if (ACLAuthenticateUser(c, c->argv[j+1], c->argv[j+2]) == C_ERR) { username = c->argv[j+1];
addReplyError(c,"-WRONGPASS invalid username-password pair or user is disabled."); password = c->argv[j+2];
return;
}
j += 2; j += 2;
} else if (!strcasecmp(opt,"SETNAME") && moreargs) { } 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++; j++;
} else { } else {
addReplyErrorFormat(c,"Syntax error in HELLO option '%s'",opt); addReplyErrorFormat(c,"Syntax error in HELLO option '%s'",opt);
...@@ -3454,6 +3476,20 @@ void helloCommand(client *c) { ...@@ -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. */ /* At this point we need to be authenticated to continue. */
if (!c->authenticated) { if (!c->authenticated) {
addReplyError(c,"-NOAUTH HELLO must be called with the client already " addReplyError(c,"-NOAUTH HELLO must be called with the client already "
...@@ -3463,6 +3499,9 @@ void helloCommand(client *c) { ...@@ -3463,6 +3499,9 @@ void helloCommand(client *c) {
return; return;
} }
/* Now that we're authenticated, set the client name. */
if (clientname) clientSetName(c, clientname, NULL);
/* Let's switch to the specified RESP mode. */ /* Let's switch to the specified RESP mode. */
if (ver) c->resp = ver; if (ver) c->resp = ver;
addReplyMapLen(c,6 + !server.sentinel_mode); addReplyMapLen(c,6 + !server.sentinel_mode);
......
...@@ -34,6 +34,10 @@ typedef long long ustime_t; ...@@ -34,6 +34,10 @@ typedef long long ustime_t;
#define REDISMODULE_OK 0 #define REDISMODULE_OK 0
#define REDISMODULE_ERR 1 #define REDISMODULE_ERR 1
/* Module Based Authentication status return values. */
#define REDISMODULE_AUTH_HANDLED 0
#define REDISMODULE_AUTH_NOT_HANDLED 1
/* API versions. */ /* API versions. */
#define REDISMODULE_APIVER_1 1 #define REDISMODULE_APIVER_1 1
...@@ -912,6 +916,7 @@ typedef int (*RedisModuleConfigSetNumericFunc)(const char *name, long long val, ...@@ -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 (*RedisModuleConfigSetBoolFunc)(const char *name, int val, void *privdata, RedisModuleString **err);
typedef int (*RedisModuleConfigSetEnumFunc)(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 (*RedisModuleConfigApplyFunc)(RedisModuleCtx *ctx, void *privdata, RedisModuleString **err);
typedef int (*RedisModuleAuthCallback)(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err);
typedef struct RedisModuleTypeMethods { typedef struct RedisModuleTypeMethods {
uint64_t version; uint64_t version;
...@@ -1164,6 +1169,7 @@ REDISMODULE_API RedisModuleString * (*RedisModule_DictPrev)(RedisModuleCtx *ctx, ...@@ -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_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_DictCompare)(RedisModuleDictIter *di, const char *op, RedisModuleString *key) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_RegisterInfoFunc)(RedisModuleCtx *ctx, RedisModuleInfoFunc cb) 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_InfoAddSection)(RedisModuleInfoCtx *ctx, const char *name) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_InfoBeginDictField)(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; REDISMODULE_API int (*RedisModule_InfoEndDictField)(RedisModuleInfoCtx *ctx) REDISMODULE_ATTR;
...@@ -1201,6 +1207,7 @@ REDISMODULE_API int (*RedisModule_GetServerVersion)() REDISMODULE_ATTR; ...@@ -1201,6 +1207,7 @@ REDISMODULE_API int (*RedisModule_GetServerVersion)() REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_GetTypeMethodVersion)() 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 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_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_UnblockClient)(RedisModuleBlockedClient *bc, void *privdata) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_IsBlockedReplyRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_IsBlockedReplyRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_IsBlockedTimeoutRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR; REDISMODULE_API int (*RedisModule_IsBlockedTimeoutRequest)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
...@@ -1264,6 +1271,7 @@ REDISMODULE_API int (*RedisModule_ACLCheckCommandPermissions)(RedisModuleUser *u ...@@ -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_ACLCheckKeyPermissions)(RedisModuleUser *user, RedisModuleString *key, int flags) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ACLCheckChannelPermissions)(RedisModuleUser *user, RedisModuleString *ch, int literal) 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_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_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_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; 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 ...@@ -1506,6 +1514,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(DictCompare); REDISMODULE_GET_API(DictCompare);
REDISMODULE_GET_API(DictCompareC); REDISMODULE_GET_API(DictCompareC);
REDISMODULE_GET_API(RegisterInfoFunc); REDISMODULE_GET_API(RegisterInfoFunc);
REDISMODULE_GET_API(RegisterAuthCallback);
REDISMODULE_GET_API(InfoAddSection); REDISMODULE_GET_API(InfoAddSection);
REDISMODULE_GET_API(InfoBeginDictField); REDISMODULE_GET_API(InfoBeginDictField);
REDISMODULE_GET_API(InfoEndDictField); REDISMODULE_GET_API(InfoEndDictField);
...@@ -1554,6 +1563,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int ...@@ -1554,6 +1563,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(ThreadSafeContextTryLock); REDISMODULE_GET_API(ThreadSafeContextTryLock);
REDISMODULE_GET_API(ThreadSafeContextUnlock); REDISMODULE_GET_API(ThreadSafeContextUnlock);
REDISMODULE_GET_API(BlockClient); REDISMODULE_GET_API(BlockClient);
REDISMODULE_GET_API(BlockClientOnAuth);
REDISMODULE_GET_API(UnblockClient); REDISMODULE_GET_API(UnblockClient);
REDISMODULE_GET_API(IsBlockedReplyRequest); REDISMODULE_GET_API(IsBlockedReplyRequest);
REDISMODULE_GET_API(IsBlockedTimeoutRequest); REDISMODULE_GET_API(IsBlockedTimeoutRequest);
...@@ -1611,6 +1621,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int ...@@ -1611,6 +1621,7 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(ACLCheckKeyPermissions); REDISMODULE_GET_API(ACLCheckKeyPermissions);
REDISMODULE_GET_API(ACLCheckChannelPermissions); REDISMODULE_GET_API(ACLCheckChannelPermissions);
REDISMODULE_GET_API(ACLAddLogEntry); REDISMODULE_GET_API(ACLAddLogEntry);
REDISMODULE_GET_API(ACLAddLogEntryByUserName);
REDISMODULE_GET_API(DeauthenticateAndCloseClient); REDISMODULE_GET_API(DeauthenticateAndCloseClient);
REDISMODULE_GET_API(AuthenticateClientWithACLUser); REDISMODULE_GET_API(AuthenticateClientWithACLUser);
REDISMODULE_GET_API(AuthenticateClientWithUser); REDISMODULE_GET_API(AuthenticateClientWithUser);
......
...@@ -392,6 +392,8 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT]; ...@@ -392,6 +392,8 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
scripts even when in OOM */ scripts even when in OOM */
#define CLIENT_NO_TOUCH (1ULL<<45) /* This client will not touch LFU/LRU stats. */ #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_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) /* Client block type (btype field in client structure)
* if CLIENT_BLOCKED flag is set. */ * if CLIENT_BLOCKED flag is set. */
...@@ -740,6 +742,7 @@ typedef void (*moduleTypeFreeFunc2)(struct RedisModuleKeyOptCtx *ctx, void *valu ...@@ -740,6 +742,7 @@ typedef void (*moduleTypeFreeFunc2)(struct RedisModuleKeyOptCtx *ctx, void *valu
typedef size_t (*moduleTypeFreeEffortFunc2)(struct RedisModuleKeyOptCtx *ctx, const void *value); typedef size_t (*moduleTypeFreeEffortFunc2)(struct RedisModuleKeyOptCtx *ctx, const void *value);
typedef void (*moduleTypeUnlinkFunc2)(struct RedisModuleKeyOptCtx *ctx, void *value); typedef void (*moduleTypeUnlinkFunc2)(struct RedisModuleKeyOptCtx *ctx, void *value);
typedef void *(*moduleTypeCopyFunc2)(struct RedisModuleKeyOptCtx *ctx, const 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 /* The module type, which is referenced in each value of a given type, defines
...@@ -856,6 +859,9 @@ struct RedisModuleDigest { ...@@ -856,6 +859,9 @@ struct RedisModuleDigest {
memset(mdvar.x,0,sizeof(mdvar.x)); \ memset(mdvar.x,0,sizeof(mdvar.x)); \
} while(0) } 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 /* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object * internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */ * is set to one of this fields for this object. */
...@@ -1200,6 +1206,12 @@ typedef struct client { ...@@ -1200,6 +1206,12 @@ typedef struct client {
listNode *client_list_node; /* list node in client list */ listNode *client_list_node; /* list node in client list */
listNode *postponed_list_node; /* list node within the postponed list */ listNode *postponed_list_node; /* list node within the postponed list */
listNode *pending_read_list_node; /* list node in clients pending read 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 RedisModuleUserChangedFunc auth_callback; /* Module callback to execute
* when the authenticated user * when the authenticated user
* changes. */ * changes. */
...@@ -2467,7 +2479,7 @@ void moduleInitModulesSystem(void); ...@@ -2467,7 +2479,7 @@ void moduleInitModulesSystem(void);
void moduleInitModulesSystemLast(void); void moduleInitModulesSystemLast(void);
void modulesCron(void); void modulesCron(void);
int moduleLoad(const char *path, void **argv, int argc, int is_loadex); 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); void moduleLoadFromQueue(void);
int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result);
int moduleGetCommandChannelsViaAPI(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); ...@@ -2598,7 +2610,7 @@ char *getClientPeerId(client *client);
char *getClientSockName(client *client); char *getClientSockName(client *client);
sds catClientInfoString(sds s, client *client); sds catClientInfoString(sds s, client *client);
sds getAllClientsInfoString(int type); 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 rewriteClientCommandVector(client *c, int argc, ...);
void rewriteClientCommandArgument(client *c, int i, robj *newval); void rewriteClientCommandArgument(client *c, int i, robj *newval);
void replaceClientCommandVector(client *c, int argc, robj **argv); void replaceClientCommandVector(client *c, int argc, robj **argv);
...@@ -2895,8 +2907,18 @@ void ACLInit(void); ...@@ -2895,8 +2907,18 @@ void ACLInit(void);
#define ACL_WRITE_PERMISSION (1<<1) #define ACL_WRITE_PERMISSION (1<<1)
#define ACL_ALL_PERMISSION (ACL_READ_PERMISSION|ACL_WRITE_PERMISSION) #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 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); unsigned long ACLGetCommandID(sds cmdname);
void ACLClearCommandID(void); void ACLClearCommandID(void);
user *ACLGetUserByName(const char *name, size_t namelen); user *ACLGetUserByName(const char *name, size_t namelen);
......
...@@ -60,7 +60,8 @@ TEST_MODULES = \ ...@@ -60,7 +60,8 @@ TEST_MODULES = \
moduleconfigstwo.so \ moduleconfigstwo.so \
publish.so \ publish.so \
usercall.so \ usercall.so \
postnotifications.so postnotifications.so \
moduleauthtwo.so
.PHONY: all .PHONY: all
......
#include "redismodule.h" #include "redismodule.h"
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#define UNUSED(V) ((void) V) #define UNUSED(V) ((void) V)
// A simple global user // A simple global user
...@@ -72,6 +76,146 @@ int Auth_ChangeCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { ...@@ -72,6 +76,146 @@ int Auth_ChangeCount(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
return RedisModule_ReplyWithLongLong(ctx, result); 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 /* This function must be present on each Redis module. It is used in order to
* register the commands into the Redis server. */ * register the commands into the Redis server. */
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
...@@ -101,6 +245,14 @@ 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) Auth_RedactedAPI,"",0,0,0) == REDISMODULE_ERR)
return 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; return REDISMODULE_OK;
} }
......
#include "redismodule.h"
#include <string.h>
/* This is a second sample module to validate that module authentication callbacks can be registered
* from multiple modules. */
/* Non Blocking Module Auth callback / implementation. */
int auth_cb(RedisModuleCtx *ctx, RedisModuleString *username, RedisModuleString *password, RedisModuleString **err) {
const char *user = RedisModule_StringPtrLen(username, NULL);
const char *pwd = RedisModule_StringPtrLen(password, NULL);
if (!strcmp(user,"foo") && !strcmp(pwd,"allow_two")) {
RedisModule_AuthenticateClientWithACLUser(ctx, "foo", 3, NULL, NULL, NULL);
return REDISMODULE_AUTH_HANDLED;
}
else if (!strcmp(user,"foo") && !strcmp(pwd,"deny_two")) {
RedisModuleString *log = RedisModule_CreateString(ctx, "Module Auth", 11);
RedisModule_ACLAddLogEntryByUserName(ctx, username, log, REDISMODULE_ACL_LOG_AUTH);
RedisModule_FreeString(ctx, log);
const char *err_msg = "Auth denied by Misc Module.";
*err = RedisModule_CreateString(ctx, err_msg, strlen(err_msg));
return REDISMODULE_AUTH_HANDLED;
}
return REDISMODULE_AUTH_NOT_HANDLED;
}
int test_rm_register_auth_cb(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_RegisterAuthCallback(ctx, auth_cb);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
if (RedisModule_Init(ctx,"moduleauthtwo",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"testmoduletwo.rm_register_auth_cb", test_rm_register_auth_cb,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
\ No newline at end of file
...@@ -793,6 +793,31 @@ start_server {tags {"acl external:skip"}} { ...@@ -793,6 +793,31 @@ start_server {tags {"acl external:skip"}} {
r AUTH default "" 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} { test {ACL HELP should not have unexpected options} {
catch {r ACL help xxx} e catch {r ACL help xxx} e
assert_match "*wrong number of arguments for 'acl|help' command" $e assert_match "*wrong number of arguments for 'acl|help' command" $e
......
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]
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment