Adding ACL support for modules (#9309)

This commit introduced a new flag to the RM_Call:
'C' - Check if the command can be executed according to the ACLs associated with it.

Also, three new API's added to check if a command, key, or channel can be executed or accessed
by a user, according to the ACLs associated with it.
- RM_ACLCheckCommandPerm
- RM_ACLCheckKeyPerm
- RM_ACLCheckChannelPerm

The user for these API's is a RedisModuleUser object, that for a Module user returned by the RM_CreateModuleUser API, or for a general ACL user can be retrieved by these two new API's:
- RM_GetCurrentUserName - Retrieve the user name of the client connection behind the current context.
- RM_GetModuleUserFromUserName - Get a RedisModuleUser from a user name

As a result of getting a RedisModuleUser from name, it can now also access the general ACL users (not just ones created by the module).
This mean the already existing API RM_SetModuleUserACL(), can be used to change the ACL rules for such users.
This commit is contained in:
YaacovHazan 2021-09-23 08:52:56 +03:00 committed by GitHub
parent 14d6abd8e9
commit a56d4533b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 477 additions and 75 deletions

View File

@ -40,4 +40,5 @@ $TCLSH tests/test_helper.tcl \
--single unit/moduleapi/list \
--single unit/moduleapi/stream \
--single unit/moduleapi/datatype2 \
--single unit/moduleapi/aclcheck \
"${@}"

148
src/acl.c
View File

@ -384,7 +384,7 @@ int ACLGetCommandBitCoordinates(uint64_t id, uint64_t *word, uint64_t *bit) {
*
* If the bit overflows the user internal representation, zero is returned
* in order to disallow the execution of the command in such edge case. */
int ACLGetUserCommandBit(user *u, unsigned long id) {
int ACLGetUserCommandBit(const user *u, unsigned long id) {
uint64_t word, bit;
if (ACLGetCommandBitCoordinates(id,&word,&bit) == C_ERR) return 0;
return (u->allowed_commands[word] & bit) != 0;
@ -1126,7 +1126,7 @@ int ACLAuthenticateUser(client *c, robj *username, robj *password) {
moduleNotifyUserChanged(c);
return C_OK;
} else {
addACLLogEntry(c,ACL_DENIED_AUTH,0,username->ptr);
addACLLogEntry(c,ACL_DENIED_AUTH,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,0,username->ptr,NULL);
return C_ERR;
}
}
@ -1179,31 +1179,56 @@ user *ACLGetUserByName(const char *name, size_t namelen) {
return myuser;
}
/* Check if the command is ready to be executed in the client 'c', already
* referenced by c->cmd, and can be executed by this client according to the
* ACLs associated to the client user c->user.
/* Check if the key can be accessed by the client according to
* the ACLs associated with the specified user.
*
* If the user can access the key, ACL_OK is returned, otherwise
* ACL_DENIED_KEY is returned. */
int ACLCheckKey(const user *u, const char *key, int keylen) {
/* If there is no associated user, the connection can run anything. */
if (u == NULL) return ACL_OK;
/* The user can run any keys */
if (u->flags & USER_FLAG_ALLKEYS) return ACL_OK;
listIter li;
listNode *ln;
listRewind(u->patterns,&li);
/* Test this key against every pattern. */
while((ln = listNext(&li))) {
sds pattern = listNodeValue(ln);
size_t plen = sdslen(pattern);
if (stringmatchlen(pattern,plen,key,keylen,0))
return ACL_OK;
}
return ACL_DENIED_KEY;
}
/* Check if the command is ready to be executed according to the
* ACLs associated with the specified user.
*
* If the user can execute the command ACL_OK is returned, otherwise
* ACL_DENIED_CMD or ACL_DENIED_KEY is returned: the first in case the
* command cannot be executed because the user is not allowed to run such
* command, the second if the command is denied because the user is trying
* to access keys that are not among the specified patterns. */
int ACLCheckCommandPerm(client *c, int *keyidxptr) {
user *u = c->user;
uint64_t id = c->cmd->id;
int ACLCheckCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, int argc, int *keyidxptr) {
int ret;
uint64_t id = cmd->id;
/* If there is no associated user, the connection can run anything. */
if (u == NULL) return ACL_OK;
/* Check if the user can execute this command or if the command
* doesn't need to be authenticated (hello, auth). */
if (!(u->flags & USER_FLAG_ALLCOMMANDS) && !(c->cmd->flags & CMD_NO_AUTH))
if (!(u->flags & USER_FLAG_ALLCOMMANDS) && !(cmd->flags & CMD_NO_AUTH))
{
/* If the bit is not set we have to check further, in case the
* command is allowed just with that specific subcommand. */
if (ACLGetUserCommandBit(u,id) == 0) {
/* Check if the subcommand matches. */
if (c->argc < 2 ||
if (argc < 2 ||
u->allowed_subcommands == NULL ||
u->allowed_subcommands[id] == NULL)
{
@ -1214,7 +1239,7 @@ int ACLCheckCommandPerm(client *c, int *keyidxptr) {
while (1) {
if (u->allowed_subcommands[id][subid] == NULL)
return ACL_DENIED_CMD;
if (!strcasecmp(c->argv[1]->ptr,
if (!strcasecmp(argv[1]->ptr,
u->allowed_subcommands[id][subid]))
break; /* Subcommand match found. Stop here. */
subid++;
@ -1224,34 +1249,19 @@ int ACLCheckCommandPerm(client *c, int *keyidxptr) {
/* Check if the user can execute commands explicitly touching the keys
* mentioned in the command arguments. */
if (!(c->user->flags & USER_FLAG_ALLKEYS) &&
(c->cmd->getkeys_proc || c->cmd->key_specs_num))
if (!(u->flags & USER_FLAG_ALLKEYS) &&
(cmd->getkeys_proc || cmd->key_specs_num))
{
getKeysResult result = GETKEYS_RESULT_INIT;
int numkeys = getKeysFromCommand(c->cmd,c->argv,c->argc,&result);
int numkeys = getKeysFromCommand(cmd,argv,argc,&result);
int *keyidx = result.keys;
for (int j = 0; j < numkeys; j++) {
listIter li;
listNode *ln;
listRewind(u->patterns,&li);
/* Test this key against every pattern. */
int match = 0;
while((ln = listNext(&li))) {
sds pattern = listNodeValue(ln);
size_t plen = sdslen(pattern);
int idx = keyidx[j];
if (stringmatchlen(pattern,plen,c->argv[idx]->ptr,
sdslen(c->argv[idx]->ptr),0))
{
match = 1;
break;
}
}
if (!match) {
int idx = keyidx[j];
ret = ACLCheckKey(u, argv[idx]->ptr, sdslen(argv[idx]->ptr));
if (ret != ACL_OK) {
if (keyidxptr) *keyidxptr = keyidx[j];
getKeysFreeResult(&result);
return ACL_DENIED_KEY;
return ret;
}
}
getKeysFreeResult(&result);
@ -1341,9 +1351,8 @@ void ACLKillPubsubClientsIfNeeded(user *u, list *upcoming) {
}
}
/* Check if the pub/sub channels of the command, that's ready to be executed in
* the client 'c', can be executed by this client according to the ACLs channels
* associated to the client user c->user.
/* Check if the pub/sub channels of the command, that's ready to be executed
* according to the ACLs channels associated with the specified user.
*
* idx and count are the index and count of channel arguments from the
* command. The literal argument controls whether the user's ACL channels are
@ -1351,17 +1360,15 @@ void ACLKillPubsubClientsIfNeeded(user *u, list *upcoming) {
*
* If the user can execute the command ACL_OK is returned, otherwise
* ACL_DENIED_CHANNEL. */
int ACLCheckPubsubPerm(client *c, int idx, int count, int literal, int *idxptr) {
user *u = c->user;
int ACLCheckPubsubPerm(const user *u, robj **argv, int idx, int count, int literal, int *idxptr) {
/* If there is no associated user, the connection can run anything. */
if (u == NULL) return ACL_OK;
/* Check if the user can access the channels mentioned in the command's
* arguments. */
if (!(c->user->flags & USER_FLAG_ALLCHANNELS)) {
if (!(u->flags & USER_FLAG_ALLCHANNELS)) {
for (int j = idx; j < idx+count; j++) {
if (ACLCheckPubsubChannelPerm(c->argv[j]->ptr,u->channels,literal)
if (ACLCheckPubsubChannelPerm(argv[j]->ptr,u->channels,literal)
!= ACL_OK) {
if (idxptr) *idxptr = j;
return ACL_DENIED_CHANNEL;
@ -1378,19 +1385,23 @@ int ACLCheckPubsubPerm(client *c, int idx, int count, int literal, int *idxptr)
/* Check whether the command is ready to be executed by ACLCheckCommandPerm.
* If check passes, then check whether pub/sub channels of the command is
* ready to be executed by ACLCheckPubsubPerm */
int ACLCheckAllPerm(client *c, int *idxptr) {
int acl_retval = ACLCheckCommandPerm(c,idxptr);
int ACLCheckAllUserCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr) {
int acl_retval = ACLCheckCommandPerm(u,cmd,argv,argc,idxptr);
if (acl_retval != ACL_OK)
return acl_retval;
if (c->cmd->proc == publishCommand)
acl_retval = ACLCheckPubsubPerm(c,1,1,0,idxptr);
else if (c->cmd->proc == subscribeCommand)
acl_retval = ACLCheckPubsubPerm(c,1,c->argc-1,0,idxptr);
else if (c->cmd->proc == psubscribeCommand)
acl_retval = ACLCheckPubsubPerm(c,1,c->argc-1,1,idxptr);
if (cmd->proc == publishCommand)
acl_retval = ACLCheckPubsubPerm(u,argv,1,1,0,idxptr);
else if (cmd->proc == subscribeCommand)
acl_retval = ACLCheckPubsubPerm(u,argv,1,argc-1,0,idxptr);
else if (cmd->proc == psubscribeCommand)
acl_retval = ACLCheckPubsubPerm(u,argv,1,argc-1,1,idxptr);
return acl_retval;
}
int ACLCheckAllPerm(client *c, int *idxptr) {
return ACLCheckAllUserCommandPerm(c->user, c->cmd, c->argv, c->argc, idxptr);
}
/* =============================================================================
* ACL loading / saving functions
* ==========================================================================*/
@ -1757,9 +1768,6 @@ void ACLLoadUsersAtStartup(void) {
* ACL log
* ==========================================================================*/
#define ACL_LOG_CTX_TOPLEVEL 0
#define ACL_LOG_CTX_LUA 1
#define ACL_LOG_CTX_MULTI 2
#define ACL_LOG_GROUPING_MAX_TIME_DELTA 60000
/* This structure defines an entry inside the ACL log. */
@ -1804,37 +1812,36 @@ void ACLFreeLogEntry(void *leptr) {
*
* The argpos argument is used when the reason is ACL_DENIED_KEY or
* ACL_DENIED_CHANNEL, since it allows the function to log the key or channel
* name that caused the problem. Similarly the username is only passed when we
* failed to authenticate the user with AUTH or HELLO, for the ACL_DENIED_AUTH
* reason. Otherwise it will just be NULL.
* name that caused the problem.
*
* The last 2 arguments are a manual override to be used, instead of any of the automatic
* ones which depend on the client and reason arguments (use NULL for default).
*/
void addACLLogEntry(client *c, int reason, int argpos, sds username) {
void addACLLogEntry(client *c, int reason, int context, int argpos, sds username, sds object) {
/* Create a new entry. */
struct ACLLogEntry *le = zmalloc(sizeof(*le));
le->count = 1;
le->reason = reason;
le->username = sdsdup(reason == ACL_DENIED_AUTH ? username : c->user->name);
le->username = sdsdup(username ? username : c->user->name);
le->ctime = mstime();
switch(reason) {
case ACL_DENIED_CMD: le->object = sdsnew(c->cmd->name); break;
case ACL_DENIED_KEY: le->object = sdsdup(c->argv[argpos]->ptr); break;
case ACL_DENIED_CHANNEL: le->object = sdsdup(c->argv[argpos]->ptr); break;
case ACL_DENIED_AUTH: le->object = sdsdup(c->argv[0]->ptr); break;
default: le->object = sdsempty();
if (object) {
le->object = sdsnew(object);
} else {
switch(reason) {
case ACL_DENIED_CMD: le->object = sdsnew(c->cmd->name); break;
case ACL_DENIED_KEY: le->object = sdsdup(c->argv[argpos]->ptr); break;
case ACL_DENIED_CHANNEL: le->object = sdsdup(c->argv[argpos]->ptr); break;
case ACL_DENIED_AUTH: le->object = sdsdup(c->argv[0]->ptr); break;
default: le->object = sdsempty();
}
}
client *realclient = c;
if (realclient->flags & CLIENT_LUA) realclient = server.lua_caller;
le->cinfo = catClientInfoString(sdsempty(),realclient);
if (c->flags & CLIENT_MULTI) {
le->context = ACL_LOG_CTX_MULTI;
} else if (c->flags & CLIENT_LUA) {
le->context = ACL_LOG_CTX_LUA;
} else {
le->context = ACL_LOG_CTX_TOPLEVEL;
}
le->context = context;
/* Try to match this entry with past ones, to see if we can just
* update an existing entry instead of creating a new one. */
@ -2184,6 +2191,7 @@ void aclCommand(client *c) {
case ACL_LOG_CTX_TOPLEVEL: ctxstr="toplevel"; break;
case ACL_LOG_CTX_MULTI: ctxstr="multi"; break;
case ACL_LOG_CTX_LUA: ctxstr="lua"; break;
case ACL_LOG_CTX_MODULE: ctxstr="module"; break;
default: ctxstr="unknown";
}
addReplyBulkCString(c,ctxstr);

View File

@ -343,6 +343,7 @@ typedef struct RedisModuleServerInfoData {
#define REDISMODULE_ARGV_NO_REPLICAS (1<<2)
#define REDISMODULE_ARGV_RESP_3 (1<<3)
#define REDISMODULE_ARGV_RESP_AUTO (1<<4)
#define REDISMODULE_ARGV_CHECK_ACL (1<<5)
/* Determine whether Redis should signalModifiedKey implicitly.
* In case 'ctx' has no 'module' member (and therefore no module->options),
@ -373,6 +374,7 @@ unsigned long long ModulesInHooks = 0; /* Total number of modules in hooks
* clients using such newly created users. */
typedef struct RedisModuleUser {
user *user; /* Reference to the real redis user */
int free_user; /* Indicates that user should also be freed when this object is freed */
} RedisModuleUser;
/* This is a structure used to export some meta-information such as dbid to the module. */
@ -4605,6 +4607,7 @@ RedisModuleString *RM_CreateStringFromCallReply(RedisModuleCallReply *reply) {
* "R" -> REDISMODULE_ARGV_NO_REPLICAS
* "3" -> REDISMODULE_ARGV_RESP_3
* "0" -> REDISMODULE_ARGV_RESP_AUTO
* "C" -> REDISMODULE_ARGV_CHECK_ACL
*
* On error (format specifier error) NULL is returned and nothing is
* allocated. On success the argument vector is returned. */
@ -4667,6 +4670,8 @@ robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int
if (flags) (*flags) |= REDISMODULE_ARGV_RESP_3;
} else if (*p == '0') {
if (flags) (*flags) |= REDISMODULE_ARGV_RESP_AUTO;
} else if (*p == 'C') {
if (flags) (*flags) |= REDISMODULE_ARGV_CHECK_ACL;
} else {
goto fmterr;
}
@ -4704,6 +4709,7 @@ fmterr:
* * `0` -- Return the reply in auto mode, i.e. the reply format will be the
* same as the client attached to the given RedisModuleCtx. This will
* probably used when you want to pass the reply directly to the client.
* * `C` -- Check if command can be executed according to ACL rules.
* * **...**: The actual arguments to the Redis command.
*
* On success a RedisModuleCallReply object is returned, otherwise
@ -4716,6 +4722,8 @@ fmterr:
* * EROFS: operation in Cluster instance when a write command is sent
* in a readonly state.
* * ENETDOWN: operation in Cluster instance when cluster is down.
* * ENOTSUP: No ACL user for the specified module context
* * EACCES: Command cannot be executed, according to ACL rules
*
* Example code fragment:
*
@ -4754,6 +4762,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
* recursive call to this module.) */
c = createClient(NULL);
}
c->user = NULL; /* Root user. */
c->flags = CLIENT_MODULE;
@ -4797,6 +4806,25 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
goto cleanup;
}
/* Check if the user can run this command according to the current
* ACLs. */
if (flags & REDISMODULE_ARGV_CHECK_ACL) {
int acl_errpos;
int acl_retval;
if (ctx->client->user == NULL) {
errno = ENOTSUP;
goto cleanup;
}
acl_retval = ACLCheckAllUserCommandPerm(ctx->client->user,c->cmd,c->argv,c->argc,&acl_errpos);
if (acl_retval != ACL_OK) {
sds object = (acl_retval == ACL_DENIED_CMD) ? c->cmd->name : c->argv[acl_errpos]->ptr;
addACLLogEntry(ctx->client, acl_retval, ACL_LOG_CTX_MODULE, -1, ctx->client->user->name, object);
errno = EACCES;
goto cleanup;
}
}
/* If this is a Redis Cluster node, we need to make sure the module is not
* trying to access non-local keys, with the exception of commands
* received from our master. */
@ -7197,6 +7225,7 @@ static void moduleFreeAuthenticatedClients(RedisModule *module) {
RedisModuleUser *RM_CreateModuleUser(const char *name) {
RedisModuleUser *new_user = zmalloc(sizeof(RedisModuleUser));
new_user->user = ACLCreateUnlinkedUser();
new_user->free_user = 1;
/* Free the previous temporarily assigned name to assign the new one */
sdsfree(new_user->user->name);
@ -7207,7 +7236,8 @@ RedisModuleUser *RM_CreateModuleUser(const char *name) {
/* 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);
if (user->free_user)
ACLFreeUserAndKillClients(user->user);
zfree(user);
return REDISMODULE_OK;
}
@ -7223,6 +7253,98 @@ int RM_SetModuleUserACL(RedisModuleUser *user, const char* acl) {
return ACLSetUser(user->user, acl, -1);
}
/* Retrieve the user name of the client connection behind the current context.
* The user name can be used later, in order to get a RedisModuleUser.
* See more information in RM_GetModuleUserFromUserName.
*
* The returned string must be released with RedisModule_FreeString() or by
* enabling automatic memory management. */
RedisModuleString *RM_GetCurrentUserName(RedisModuleCtx *ctx) {
return RM_CreateString(ctx,ctx->client->user->name,sdslen(ctx->client->user->name));
}
/* A RedisModuleUser can be used to check if command, key or channel can be executed or
* accessed according to the ACLs rules associated with that user.
* When a Module wants to do ACL checks on a general ACL user (not created by RM_CreateModuleUser),
* it can get the RedisModuleUser from this API, based on the user name retrieved by RM_GetCurrentUserName.
*
* Since a general ACL user can be deleted at any time, this RedisModuleUser should be used only in the context
* where this function was called. In order to do ACL checks out of that context, the Module can store the user name,
* and call this API at any other context.
*
* Returns NULL if the user is disabled or the user does not exist.
* The caller should later free the user using the function RM_FreeModuleUser().*/
RedisModuleUser *RM_GetModuleUserFromUserName(RedisModuleString *name) {
/* First, verfify that the user exist */
user *acl_user = ACLGetUserByName(name->ptr, sdslen(name->ptr));
if (acl_user == NULL) {
return NULL;
}
RedisModuleUser *new_user = zmalloc(sizeof(RedisModuleUser));
new_user->user = acl_user;
new_user->free_user = 0;
return new_user;
}
/* Checks if the command can be executed by the user, according to the ACLs associated with it.
*
* On success a REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned and errno is set to the following values:
*
* * ENOENT: Specified command does not exist.
* * EACCES: Command cannot be executed, according to ACL rules
*/
int RM_ACLCheckCommandPermissions(RedisModuleUser *user, RedisModuleString **argv, int argc) {
int keyidxptr;
struct redisCommand *cmd;
/* Find command */
if ((cmd = lookupCommand(argv[0]->ptr)) == NULL) {
errno = ENOENT;
return REDISMODULE_ERR;
}
if (ACLCheckAllUserCommandPerm(user->user, cmd, argv, argc, &keyidxptr) != ACL_OK) {
errno = EACCES;
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
/* Check if the key can be accessed by the user, according to the ACLs associated with it.
*
* If the user can access the key, REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned. */
int RM_ACLCheckKeyPermissions(RedisModuleUser *user, RedisModuleString *key) {
if (ACLCheckKey(user->user, key->ptr, sdslen(key->ptr)) != ACL_OK)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
/* Check if the pubsub channel can be accessed by the user, according to the ACLs associated with it.
* Glob-style pattern matching is employed, unless the literal flag is
* set.
*
* If the user can access the pubsub channel, REDISMODULE_OK is returned, otherwise
* REDISMODULE_ERR is returned. */
int RM_ACLCheckChannelPermissions(RedisModuleUser *user, RedisModuleString *ch, int literal) {
if (ACLCheckPubsubChannelPerm(ch->ptr, user->user->channels, literal) != ACL_OK)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
/* Adds a new entry in the ACL log.
* Returns REDISMODULE_OK on success and REDISMODULE_ERR on error.
*
* For more information about ACL log, please refer to https://redis.io/commands/acl-log */
void RM_ACLAddLogEntry(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString *object) {
addACLLogEntry(ctx->client, 0, ACL_LOG_CTX_MODULE, -1, user->user->name, object->ptr);
}
/* Authenticate the client associated with the context with
* the provided user. Returns REDISMODULE_OK on success and
* REDISMODULE_ERR on error.
@ -10316,6 +10438,12 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API(ScanKey);
REGISTER_API(CreateModuleUser);
REGISTER_API(SetModuleUserACL);
REGISTER_API(GetCurrentUserName);
REGISTER_API(GetModuleUserFromUserName);
REGISTER_API(ACLCheckCommandPermissions);
REGISTER_API(ACLCheckKeyPermissions);
REGISTER_API(ACLCheckChannelPermissions);
REGISTER_API(ACLAddLogEntry);
REGISTER_API(FreeModuleUser);
REGISTER_API(DeauthenticateAndCloseClient);
REGISTER_API(AuthenticateClientWithACLUser);

View File

@ -228,7 +228,7 @@ void execCommand(client *c) {
reason = "no permission";
break;
}
addACLLogEntry(c,acl_retval,acl_errpos,NULL);
addACLLogEntry(c,acl_retval,ACL_LOG_CTX_MULTI,acl_errpos,NULL,NULL);
addReplyErrorFormat(c,
"-NOPERM ACLs rules changed between the moment the "
"transaction was accumulated and the EXEC call. "

View File

@ -886,6 +886,12 @@ REDISMODULE_API size_t (*RedisModule_MallocSize)(void* ptr) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleUser * (*RedisModule_CreateModuleUser)(const char *name) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_FreeModuleUser)(RedisModuleUser *user) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_SetModuleUserACL)(RedisModuleUser *user, const char* acl) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleString * (*RedisModule_GetCurrentUserName)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API RedisModuleUser * (*RedisModule_GetModuleUserFromUserName)(RedisModuleString *name) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ACLCheckCommandPermissions)(RedisModuleUser *user, RedisModuleString **argv, int argc) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ACLCheckKeyPermissions)(RedisModuleUser *user, RedisModuleString *key) 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) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_AuthenticateClientWithACLUser)(RedisModuleCtx *ctx, const char *name, size_t len, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_AuthenticateClientWithUser)(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleUserChangedFunc callback, void *privdata, uint64_t *client_id) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_DeauthenticateAndCloseClient)(RedisModuleCtx *ctx, uint64_t client_id) REDISMODULE_ATTR;
@ -1195,6 +1201,12 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
REDISMODULE_GET_API(CreateModuleUser);
REDISMODULE_GET_API(FreeModuleUser);
REDISMODULE_GET_API(SetModuleUserACL);
REDISMODULE_GET_API(GetCurrentUserName);
REDISMODULE_GET_API(GetModuleUserFromUserName);
REDISMODULE_GET_API(ACLCheckCommandPermissions);
REDISMODULE_GET_API(ACLCheckKeyPermissions);
REDISMODULE_GET_API(ACLCheckChannelPermissions);
REDISMODULE_GET_API(ACLAddLogEntry);
REDISMODULE_GET_API(DeauthenticateAndCloseClient);
REDISMODULE_GET_API(AuthenticateClientWithACLUser);
REDISMODULE_GET_API(AuthenticateClientWithUser);

View File

@ -760,7 +760,7 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
int acl_errpos;
int acl_retval = ACLCheckAllPerm(c,&acl_errpos);
if (acl_retval != ACL_OK) {
addACLLogEntry(c,acl_retval,acl_errpos,NULL);
addACLLogEntry(c,acl_retval,ACL_LOG_CTX_LUA,acl_errpos,NULL,NULL);
switch (acl_retval) {
case ACL_DENIED_CMD:
luaPushError(lua, "The user executing the script can't run this "

View File

@ -4559,7 +4559,7 @@ int processCommand(client *c) {
int acl_errpos;
int acl_retval = ACLCheckAllPerm(c,&acl_errpos);
if (acl_retval != ACL_OK) {
addACLLogEntry(c,acl_retval,acl_errpos,NULL);
addACLLogEntry(c,acl_retval,(c->flags & CLIENT_MULTI) ? ACL_LOG_CTX_MULTI : ACL_LOG_CTX_TOPLEVEL,acl_errpos,NULL,NULL);
switch (acl_retval) {
case ACL_DENIED_CMD:
rejectCommandFormat(c,

View File

@ -2274,11 +2274,21 @@ void ACLInit(void);
#define ACL_DENIED_KEY 2
#define ACL_DENIED_AUTH 3 /* Only used for ACL LOG entries. */
#define ACL_DENIED_CHANNEL 4 /* Only used for pub/sub commands */
/* Context values for addACLLogEntry(). */
#define ACL_LOG_CTX_TOPLEVEL 0
#define ACL_LOG_CTX_LUA 1
#define ACL_LOG_CTX_MULTI 2
#define ACL_LOG_CTX_MODULE 3
int ACLCheckUserCredentials(robj *username, robj *password);
int ACLAuthenticateUser(client *c, robj *username, robj *password);
unsigned long ACLGetCommandID(const char *cmdname);
void ACLClearCommandID(void);
user *ACLGetUserByName(const char *name, size_t namelen);
int ACLCheckKey(const user *u, const char *key, int keylen);
int ACLCheckPubsubChannelPerm(sds channel, list *allowed, int literal);
int ACLCheckAllUserCommandPerm(const user *u, struct redisCommand *cmd, robj **argv, int argc, int *idxptr);
int ACLCheckAllPerm(client *c, int *idxptr);
int ACLSetUser(user *u, const char *op, ssize_t oplen);
sds ACLDefaultUserFirstPassword(void);
@ -2291,7 +2301,7 @@ void ACLLoadUsersAtStartup(void);
void addReplyCommandCategories(client *c, struct redisCommand *cmd);
user *ACLCreateUnlinkedUser();
void ACLFreeUserAndKillClients(user *u);
void addACLLogEntry(client *c, int reason, int keypos, sds username);
void addACLLogEntry(client *c, int reason, int context, int argpos, sds username, sds object);
void ACLUpdateDefaultUserPassword(sds password);
/* Sorted sets data type */

View File

@ -41,6 +41,7 @@ TEST_MODULES = \
hash.so \
zset.so \
stream.so \
aclcheck.so \
list.so

176
tests/modules/aclcheck.c Normal file
View File

@ -0,0 +1,176 @@
#define REDISMODULE_EXPERIMENTAL_API
#include "redismodule.h"
#include <errno.h>
#include <assert.h>
/* A wrap for SET command with ACL check on the key. */
int set_aclcheck_key(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc < 3) {
return RedisModule_WrongArity(ctx);
}
/* Check that the key can be accessed */
RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
int ret = RedisModule_ACLCheckKeyPermissions(user, argv[1]);
if (ret != 0) {
RedisModule_ReplyWithError(ctx, "DENIED KEY");
RedisModule_FreeModuleUser(user);
RedisModule_FreeString(ctx, user_name);
return REDISMODULE_OK;
}
RedisModuleCallReply *rep = RedisModule_Call(ctx, "SET", "v", argv + 1, argc - 1);
if (!rep) {
RedisModule_ReplyWithError(ctx, "NULL reply returned");
} else {
RedisModule_ReplyWithCallReply(ctx, rep);
RedisModule_FreeCallReply(rep);
}
RedisModule_FreeModuleUser(user);
RedisModule_FreeString(ctx, user_name);
return REDISMODULE_OK;
}
/* A wrap for PUBLISH command with ACL check on the channel. */
int publish_aclcheck_channel(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 3) {
return RedisModule_WrongArity(ctx);
}
/* Check that the pubsub channel can be accessed */
RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
int ret = RedisModule_ACLCheckChannelPermissions(user, argv[1], 1);
if (ret != 0) {
RedisModule_ReplyWithError(ctx, "DENIED CHANNEL");
RedisModule_FreeModuleUser(user);
RedisModule_FreeString(ctx, user_name);
return REDISMODULE_OK;
}
RedisModuleCallReply *rep = RedisModule_Call(ctx, "PUBLISH", "v", argv + 1, argc - 1);
if (!rep) {
RedisModule_ReplyWithError(ctx, "NULL reply returned");
} else {
RedisModule_ReplyWithCallReply(ctx, rep);
RedisModule_FreeCallReply(rep);
}
RedisModule_FreeModuleUser(user);
RedisModule_FreeString(ctx, user_name);
return REDISMODULE_OK;
}
/* A wrap for RM_Call that check first that the command can be executed */
int rm_call_aclcheck_cmd(RedisModuleCtx *ctx, RedisModuleUser *user, RedisModuleString **argv, int argc) {
if (argc < 2) {
return RedisModule_WrongArity(ctx);
}
/* Check that the command can be executed */
int ret = RedisModule_ACLCheckCommandPermissions(user, argv + 1, argc - 1);
if (ret != 0) {
RedisModule_ReplyWithError(ctx, "DENIED CMD");
/* Add entry to ACL log */
RedisModule_ACLAddLogEntry(ctx, user, argv[1]);
return REDISMODULE_OK;
}
const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "v", argv + 2, argc - 2);
if(!rep){
RedisModule_ReplyWithError(ctx, "NULL reply returned");
}else{
RedisModule_ReplyWithCallReply(ctx, rep);
RedisModule_FreeCallReply(rep);
}
return REDISMODULE_OK;
}
int rm_call_aclcheck_cmd_default_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
RedisModuleString *user_name = RedisModule_GetCurrentUserName(ctx);
RedisModuleUser *user = RedisModule_GetModuleUserFromUserName(user_name);
int res = rm_call_aclcheck_cmd(ctx, user, argv, argc);
RedisModule_FreeModuleUser(user);
RedisModule_FreeString(ctx, user_name);
return res;
}
int rm_call_aclcheck_cmd_module_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
/* Create a user and authenticate */
RedisModuleUser *user = RedisModule_CreateModuleUser("testuser1");
RedisModule_SetModuleUserACL(user, "allcommands");
RedisModule_SetModuleUserACL(user, "allkeys");
RedisModule_SetModuleUserACL(user, "on");
RedisModule_AuthenticateClientWithUser(ctx, user, NULL, NULL, NULL);
int res = rm_call_aclcheck_cmd(ctx, user, argv, argc);
/* authenticated back to "default" user (so once we free testuser1 we will not disconnected */
RedisModule_AuthenticateClientWithACLUser(ctx, "default", 7, NULL, NULL, NULL);
RedisModule_FreeModuleUser(user);
return res;
}
/* A wrap for RM_Call that pass the 'C' flag to do ACL check on the command. */
int rm_call_aclcheck(RedisModuleCtx *ctx, RedisModuleString **argv, int argc){
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
if(argc < 2){
return RedisModule_WrongArity(ctx);
}
const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);
RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, "vC", argv + 2, argc - 2);
if(!rep) {
char err[100];
switch (errno) {
case EACCES:
RedisModule_ReplyWithError(ctx, "ERR NOPERM");
break;
default:
snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno);
RedisModule_ReplyWithError(ctx, err);
break;
}
} else {
RedisModule_ReplyWithCallReply(ctx, rep);
RedisModule_FreeCallReply(rep);
}
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
if (RedisModule_Init(ctx,"aclcheck",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.set.check.key", set_aclcheck_key,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.publish.check.channel", publish_aclcheck_channel,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call.check.cmd", rm_call_aclcheck_cmd_default_user,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call.check.cmd.module.user", rm_call_aclcheck_cmd_module_user,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"aclcheck.rm_call", rm_call_aclcheck,"",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}

View File

@ -0,0 +1,66 @@
set testmodule [file normalize tests/modules/aclcheck.so]
start_server {tags {"modules acl"}} {
r module load $testmodule
test {test module check acl for command perm} {
# by default all commands allowed
assert_equal [r aclcheck.rm_call.check.cmd set x 5] OK
# block SET command for user
r acl setuser default -set
catch {r aclcheck.rm_call.check.cmd set x 5} e
assert_match {*DENIED CMD*} $e
# verify that new log entry added
set entry [lindex [r ACL LOG] 0]
assert {[dict get $entry username] eq {default}}
assert {[dict get $entry context] eq {module}}
assert {[dict get $entry object] eq {set}}
}
test {test module check acl for key perm} {
# give permission for SET and block all keys but x
r acl setuser default +set resetkeys ~x
assert_equal [r aclcheck.set.check.key x 5] OK
catch {r aclcheck.set.check.key y 5} e
set e
} {*DENIED KEY*}
test {test module check acl for module user} {
# the module user has access to all keys
assert_equal [r aclcheck.rm_call.check.cmd.module.user set y 5] OK
}
test {test module check acl for channel perm} {
# block all channels but ch1
r acl setuser default resetchannels &ch1
assert_equal [r aclcheck.publish.check.channel ch1 msg] 0
catch {r aclcheck.publish.check.channel ch2 msg} e
set e
} {*DENIED CHANNEL*}
test {test module check acl in rm_call} {
# rm call check for key permission (x can be accessed)
assert_equal [r aclcheck.rm_call set x 5] OK
# rm call check for key permission (y can't be accessed)
catch {r aclcheck.rm_call set y 5} e
assert_match {*NOPERM*} $e
# verify that new log entry added
set entry [lindex [r ACL LOG] 0]
assert {[dict get $entry username] eq {default}}
assert {[dict get $entry context] eq {module}}
assert {[dict get $entry object] eq {y}}
# rm call check for command permission
r acl setuser default -set
catch {r aclcheck.rm_call set x 5} e
assert_match {*NOPERM*} $e
# verify that new log entry added
set entry [lindex [r ACL LOG] 0]
assert {[dict get $entry username] eq {default}}
assert {[dict get $entry context] eq {module}}
assert {[dict get $entry object] eq {set}}
}
}