mirror of
https://codeberg.org/redict/redict.git
synced 2025-01-21 23:58:51 -05:00
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:
parent
14d6abd8e9
commit
a56d4533b7
@ -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
148
src/acl.c
@ -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);
|
||||
|
130
src/module.c
130
src/module.c
@ -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);
|
||||
|
@ -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. "
|
||||
|
@ -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);
|
||||
|
@ -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 "
|
||||
|
@ -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,
|
||||
|
12
src/server.h
12
src/server.h
@ -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 */
|
||||
|
@ -41,6 +41,7 @@ TEST_MODULES = \
|
||||
hash.so \
|
||||
zset.so \
|
||||
stream.so \
|
||||
aclcheck.so \
|
||||
list.so
|
||||
|
||||
|
||||
|
176
tests/modules/aclcheck.c
Normal file
176
tests/modules/aclcheck.c
Normal 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;
|
||||
}
|
66
tests/unit/moduleapi/aclcheck.tcl
Normal file
66
tests/unit/moduleapi/aclcheck.tcl
Normal 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}}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user