mirror of
https://codeberg.org/redict/redict.git
synced 2025-01-22 16:18:28 -05:00
Add RM_SetContextUser to support acl validation in RM_Call (and scripts) (#10966)
Adds a number of user management/ACL validaiton/command execution functions to improve a Redis module's ability to enforce ACLs correctly and easily. * RM_SetContextUser - sets a RedisModuleUser on the context, which RM_Call will use to both validate ACLs (if requested and set) as well as assign to the client so that scripts executed via RM_Call will have proper ACL validation. * RM_SetModuleUserACLString - Enables one to pass an entire ACL string, not just a single OP and have it applied to the user * RM_GetModuleUserACLString - returns a stringified version of the user's ACL (same format as dump and list). Contains an optimization to cache the stringified version until the underlying ACL is modified. * Slightly re-purpose the "C" flag to RM_Call from just being about ACL check before calling the command, to actually running the command with the right user, so that it also affects commands inside EVAL scripts. see #11231
This commit is contained in:
parent
6d21560190
commit
6e993a5dfa
@ -50,4 +50,5 @@ $TCLSH tests/test_helper.tcl \
|
|||||||
--single unit/moduleapi/eventloop \
|
--single unit/moduleapi/eventloop \
|
||||||
--single unit/moduleapi/timer \
|
--single unit/moduleapi/timer \
|
||||||
--single unit/moduleapi/publish \
|
--single unit/moduleapi/publish \
|
||||||
|
--single unit/moduleapi/usercall \
|
||||||
"${@}"
|
"${@}"
|
||||||
|
155
src/acl.c
155
src/acl.c
@ -386,6 +386,7 @@ user *ACLCreateUser(const char *name, size_t namelen) {
|
|||||||
u->flags = USER_FLAG_DISABLED;
|
u->flags = USER_FLAG_DISABLED;
|
||||||
u->flags |= USER_FLAG_SANITIZE_PAYLOAD;
|
u->flags |= USER_FLAG_SANITIZE_PAYLOAD;
|
||||||
u->passwords = listCreate();
|
u->passwords = listCreate();
|
||||||
|
u->acl_string = NULL;
|
||||||
listSetMatchMethod(u->passwords,ACLListMatchSds);
|
listSetMatchMethod(u->passwords,ACLListMatchSds);
|
||||||
listSetFreeMethod(u->passwords,ACLListFreeSds);
|
listSetFreeMethod(u->passwords,ACLListFreeSds);
|
||||||
listSetDupMethod(u->passwords,ACLListDupSds);
|
listSetDupMethod(u->passwords,ACLListDupSds);
|
||||||
@ -423,6 +424,10 @@ user *ACLCreateUnlinkedUser(void) {
|
|||||||
* will not remove the user from the Users global radix tree. */
|
* will not remove the user from the Users global radix tree. */
|
||||||
void ACLFreeUser(user *u) {
|
void ACLFreeUser(user *u) {
|
||||||
sdsfree(u->name);
|
sdsfree(u->name);
|
||||||
|
if (u->acl_string) {
|
||||||
|
decrRefCount(u->acl_string);
|
||||||
|
u->acl_string = NULL;
|
||||||
|
}
|
||||||
listRelease(u->passwords);
|
listRelease(u->passwords);
|
||||||
listRelease(u->selectors);
|
listRelease(u->selectors);
|
||||||
zfree(u);
|
zfree(u);
|
||||||
@ -467,6 +472,14 @@ void ACLCopyUser(user *dst, user *src) {
|
|||||||
dst->passwords = listDup(src->passwords);
|
dst->passwords = listDup(src->passwords);
|
||||||
dst->selectors = listDup(src->selectors);
|
dst->selectors = listDup(src->selectors);
|
||||||
dst->flags = src->flags;
|
dst->flags = src->flags;
|
||||||
|
if (dst->acl_string) {
|
||||||
|
decrRefCount(dst->acl_string);
|
||||||
|
}
|
||||||
|
dst->acl_string = src->acl_string;
|
||||||
|
if (dst->acl_string) {
|
||||||
|
/* if src is NULL, we set it to NULL, if not, need to increment reference count */
|
||||||
|
incrRefCount(dst->acl_string);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Free all the users registered in the radix tree 'users' and free the
|
/* Free all the users registered in the radix tree 'users' and free the
|
||||||
@ -803,7 +816,12 @@ sds ACLDescribeSelector(aclSelector *selector) {
|
|||||||
* the ACLDescribeSelectorCommandRules() function. This is the function we call
|
* the ACLDescribeSelectorCommandRules() function. This is the function we call
|
||||||
* when we want to rewrite the configuration files describing ACLs and
|
* when we want to rewrite the configuration files describing ACLs and
|
||||||
* in order to show users with ACL LIST. */
|
* in order to show users with ACL LIST. */
|
||||||
sds ACLDescribeUser(user *u) {
|
robj *ACLDescribeUser(user *u) {
|
||||||
|
if (u->acl_string) {
|
||||||
|
incrRefCount(u->acl_string);
|
||||||
|
return u->acl_string;
|
||||||
|
}
|
||||||
|
|
||||||
sds res = sdsempty();
|
sds res = sdsempty();
|
||||||
|
|
||||||
/* Flags. */
|
/* Flags. */
|
||||||
@ -837,7 +855,12 @@ sds ACLDescribeUser(user *u) {
|
|||||||
}
|
}
|
||||||
sdsfree(default_perm);
|
sdsfree(default_perm);
|
||||||
}
|
}
|
||||||
return res;
|
|
||||||
|
u->acl_string = createObject(OBJ_STRING, res);
|
||||||
|
/* because we are returning it, have to increase count */
|
||||||
|
incrRefCount(u->acl_string);
|
||||||
|
|
||||||
|
return u->acl_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get a command from the original command table, that is not affected
|
/* Get a command from the original command table, that is not affected
|
||||||
@ -1211,6 +1234,12 @@ int ACLSetSelector(aclSelector *selector, const char* op, size_t oplen) {
|
|||||||
* ECHILD: Attempt to allow a specific first argument of a subcommand
|
* ECHILD: Attempt to allow a specific first argument of a subcommand
|
||||||
*/
|
*/
|
||||||
int ACLSetUser(user *u, const char *op, ssize_t oplen) {
|
int ACLSetUser(user *u, const char *op, ssize_t oplen) {
|
||||||
|
/* as we are changing the ACL, the old generated string is now invalid */
|
||||||
|
if (u->acl_string) {
|
||||||
|
decrRefCount(u->acl_string);
|
||||||
|
u->acl_string = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
if (oplen == -1) oplen = strlen(op);
|
if (oplen == -1) oplen = strlen(op);
|
||||||
if (oplen == 0) return C_OK; /* Empty string is a no-operation. */
|
if (oplen == 0) return C_OK; /* Empty string is a no-operation. */
|
||||||
if (!strcasecmp(op,"on")) {
|
if (!strcasecmp(op,"on")) {
|
||||||
@ -1940,6 +1969,68 @@ sds *ACLMergeSelectorArguments(sds *argv, int argc, int *merged_argc, int *inval
|
|||||||
return acl_args;
|
return acl_args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* takes an acl string already split on spaces and adds it to the given user
|
||||||
|
* if the user object is NULL, will create a user with the given username
|
||||||
|
*
|
||||||
|
* Returns an error as an sds string if the ACL string is not parsable
|
||||||
|
*/
|
||||||
|
sds ACLStringSetUser(user *u, sds username, sds *argv, int argc) {
|
||||||
|
serverAssert(u != NULL || username != NULL);
|
||||||
|
|
||||||
|
sds error = NULL;
|
||||||
|
|
||||||
|
int merged_argc = 0, invalid_idx = 0;
|
||||||
|
sds *acl_args = ACLMergeSelectorArguments(argv, argc, &merged_argc, &invalid_idx);
|
||||||
|
|
||||||
|
if (!acl_args) {
|
||||||
|
return sdscatfmt(sdsempty(),
|
||||||
|
"Unmatched parenthesis in acl selector starting "
|
||||||
|
"at '%s'.", (char *) argv[invalid_idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a temporary user to validate and stage all changes against
|
||||||
|
* before applying to an existing user or creating a new user. If all
|
||||||
|
* arguments are valid the user parameters will all be applied together.
|
||||||
|
* If there are any errors then none of the changes will be applied. */
|
||||||
|
user *tempu = ACLCreateUnlinkedUser();
|
||||||
|
if (u) {
|
||||||
|
ACLCopyUser(tempu, u);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < merged_argc; j++) {
|
||||||
|
if (ACLSetUser(tempu,acl_args[j],(ssize_t) sdslen(acl_args[j])) != C_OK) {
|
||||||
|
const char *errmsg = ACLSetUserStringError();
|
||||||
|
error = sdscatfmt(sdsempty(),
|
||||||
|
"Error in ACL SETUSER modifier '%s': %s",
|
||||||
|
(char*)acl_args[j], errmsg);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Existing pub/sub clients authenticated with the user may need to be
|
||||||
|
* disconnected if (some of) their channel permissions were revoked. */
|
||||||
|
if (u) {
|
||||||
|
ACLKillPubsubClientsIfNeeded(tempu, u);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overwrite the user with the temporary user we modified above. */
|
||||||
|
if (!u) {
|
||||||
|
u = ACLCreateUser(username,sdslen(username));
|
||||||
|
}
|
||||||
|
serverAssert(u != NULL);
|
||||||
|
|
||||||
|
ACLCopyUser(u, tempu);
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
ACLFreeUser(tempu);
|
||||||
|
for (int i = 0; i < merged_argc; i++) {
|
||||||
|
sdsfree(acl_args[i]);
|
||||||
|
}
|
||||||
|
zfree(acl_args);
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
/* Given an argument vector describing a user in the form:
|
/* Given an argument vector describing a user in the form:
|
||||||
*
|
*
|
||||||
* user <username> ... ACL rules and flags ...
|
* user <username> ... ACL rules and flags ...
|
||||||
@ -2255,9 +2346,9 @@ int ACLSaveToFile(const char *filename) {
|
|||||||
sds user = sdsnew("user ");
|
sds user = sdsnew("user ");
|
||||||
user = sdscatsds(user,u->name);
|
user = sdscatsds(user,u->name);
|
||||||
user = sdscatlen(user," ",1);
|
user = sdscatlen(user," ",1);
|
||||||
sds descr = ACLDescribeUser(u);
|
robj *descr = ACLDescribeUser(u);
|
||||||
user = sdscatsds(user,descr);
|
user = sdscatsds(user,descr->ptr);
|
||||||
sdsfree(descr);
|
decrRefCount(descr);
|
||||||
acl = sdscatsds(acl,user);
|
acl = sdscatsds(acl,user);
|
||||||
acl = sdscatlen(acl,"\n",1);
|
acl = sdscatlen(acl,"\n",1);
|
||||||
sdsfree(user);
|
sdsfree(user);
|
||||||
@ -2590,50 +2681,18 @@ void aclCommand(client *c) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int merged_argc = 0, invalid_idx = 0;
|
user *u = ACLGetUserByName(username,sdslen(username));
|
||||||
|
|
||||||
sds *temp_argv = zmalloc(c->argc * sizeof(sds));
|
sds *temp_argv = zmalloc(c->argc * sizeof(sds));
|
||||||
for (int i = 3; i < c->argc; i++) temp_argv[i-3] = c->argv[i]->ptr;
|
for (int i = 3; i < c->argc; i++) temp_argv[i-3] = c->argv[i]->ptr;
|
||||||
sds *acl_args = ACLMergeSelectorArguments(temp_argv, c->argc - 3, &merged_argc, &invalid_idx);
|
|
||||||
|
sds error = ACLStringSetUser(u, username, temp_argv, c->argc - 3);
|
||||||
zfree(temp_argv);
|
zfree(temp_argv);
|
||||||
|
if (error == NULL) {
|
||||||
if (!acl_args) {
|
addReply(c,shared.ok);
|
||||||
addReplyErrorFormat(c,
|
} else {
|
||||||
"Unmatched parenthesis in acl selector starting "
|
addReplyErrorSdsSafe(c, error);
|
||||||
"at '%s'.", (char *) c->argv[invalid_idx]->ptr);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create a temporary user to validate and stage all changes against
|
|
||||||
* before applying to an existing user or creating a new user. If all
|
|
||||||
* arguments are valid the user parameters will all be applied together.
|
|
||||||
* If there are any errors then none of the changes will be applied. */
|
|
||||||
user *tempu = ACLCreateUnlinkedUser();
|
|
||||||
user *u = ACLGetUserByName(username,sdslen(username));
|
|
||||||
if (u) ACLCopyUser(tempu, u);
|
|
||||||
|
|
||||||
for (int j = 0; j < merged_argc; j++) {
|
|
||||||
if (ACLSetUser(tempu,acl_args[j],sdslen(acl_args[j])) != C_OK) {
|
|
||||||
const char *errmsg = ACLSetUserStringError();
|
|
||||||
addReplyErrorFormat(c,
|
|
||||||
"Error in ACL SETUSER modifier '%s': %s",
|
|
||||||
(char*)acl_args[j], errmsg);
|
|
||||||
goto setuser_cleanup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Existing pub/sub clients authenticated with the user may need to be
|
|
||||||
* disconnected if (some of) their channel permissions were revoked. */
|
|
||||||
if (u) ACLKillPubsubClientsIfNeeded(tempu, u);
|
|
||||||
|
|
||||||
/* Overwrite the user with the temporary user we modified above. */
|
|
||||||
if (!u) u = ACLCreateUser(username,sdslen(username));
|
|
||||||
serverAssert(u != NULL);
|
|
||||||
ACLCopyUser(u, tempu);
|
|
||||||
addReply(c,shared.ok);
|
|
||||||
setuser_cleanup:
|
|
||||||
ACLFreeUser(tempu);
|
|
||||||
for (int i = 0; i < merged_argc; i++) sdsfree(acl_args[i]);
|
|
||||||
zfree(acl_args);
|
|
||||||
return;
|
return;
|
||||||
} else if (!strcasecmp(sub,"deluser") && c->argc >= 3) {
|
} else if (!strcasecmp(sub,"deluser") && c->argc >= 3) {
|
||||||
int deleted = 0;
|
int deleted = 0;
|
||||||
@ -2720,9 +2779,9 @@ setuser_cleanup:
|
|||||||
sds config = sdsnew("user ");
|
sds config = sdsnew("user ");
|
||||||
config = sdscatsds(config,u->name);
|
config = sdscatsds(config,u->name);
|
||||||
config = sdscatlen(config," ",1);
|
config = sdscatlen(config," ",1);
|
||||||
sds descr = ACLDescribeUser(u);
|
robj *descr = ACLDescribeUser(u);
|
||||||
config = sdscatsds(config,descr);
|
config = sdscatsds(config,descr->ptr);
|
||||||
sdsfree(descr);
|
decrRefCount(descr);
|
||||||
addReplyBulkSds(c,config);
|
addReplyBulkSds(c,config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1418,9 +1418,9 @@ void rewriteConfigUserOption(struct rewriteConfigState *state) {
|
|||||||
sds line = sdsnew("user ");
|
sds line = sdsnew("user ");
|
||||||
line = sdscatsds(line,u->name);
|
line = sdscatsds(line,u->name);
|
||||||
line = sdscatlen(line," ",1);
|
line = sdscatlen(line," ",1);
|
||||||
sds descr = ACLDescribeUser(u);
|
robj *descr = ACLDescribeUser(u);
|
||||||
line = sdscatsds(line,descr);
|
line = sdscatsds(line,descr->ptr);
|
||||||
sdsfree(descr);
|
decrRefCount(descr);
|
||||||
rewriteConfigRewriteLine(state,"user",line,1);
|
rewriteConfigRewriteLine(state,"user",line,1);
|
||||||
}
|
}
|
||||||
raxStop(&ri);
|
raxStop(&ri);
|
||||||
|
117
src/module.c
117
src/module.c
@ -137,6 +137,7 @@ typedef struct RedisModulePoolAllocBlock {
|
|||||||
* but only the fields needed in a given context. */
|
* but only the fields needed in a given context. */
|
||||||
|
|
||||||
struct RedisModuleBlockedClient;
|
struct RedisModuleBlockedClient;
|
||||||
|
struct RedisModuleUser;
|
||||||
|
|
||||||
struct RedisModuleCtx {
|
struct RedisModuleCtx {
|
||||||
void *getapifuncptr; /* NOTE: Must be the first field. */
|
void *getapifuncptr; /* NOTE: Must be the first field. */
|
||||||
@ -161,6 +162,9 @@ struct RedisModuleCtx {
|
|||||||
|
|
||||||
struct RedisModulePoolAllocBlock *pa_head;
|
struct RedisModulePoolAllocBlock *pa_head;
|
||||||
long long next_yield_time;
|
long long next_yield_time;
|
||||||
|
|
||||||
|
const struct RedisModuleUser *user; /* RedisModuleUser commands executed via
|
||||||
|
RM_Call should be executed as, if set */
|
||||||
};
|
};
|
||||||
typedef struct RedisModuleCtx RedisModuleCtx;
|
typedef struct RedisModuleCtx RedisModuleCtx;
|
||||||
|
|
||||||
@ -352,7 +356,7 @@ typedef struct RedisModuleServerInfoData {
|
|||||||
#define REDISMODULE_ARGV_NO_REPLICAS (1<<2)
|
#define REDISMODULE_ARGV_NO_REPLICAS (1<<2)
|
||||||
#define REDISMODULE_ARGV_RESP_3 (1<<3)
|
#define REDISMODULE_ARGV_RESP_3 (1<<3)
|
||||||
#define REDISMODULE_ARGV_RESP_AUTO (1<<4)
|
#define REDISMODULE_ARGV_RESP_AUTO (1<<4)
|
||||||
#define REDISMODULE_ARGV_CHECK_ACL (1<<5)
|
#define REDISMODULE_ARGV_RUN_AS_USER (1<<5)
|
||||||
#define REDISMODULE_ARGV_SCRIPT_MODE (1<<6)
|
#define REDISMODULE_ARGV_SCRIPT_MODE (1<<6)
|
||||||
#define REDISMODULE_ARGV_NO_WRITES (1<<7)
|
#define REDISMODULE_ARGV_NO_WRITES (1<<7)
|
||||||
#define REDISMODULE_ARGV_CALL_REPLIES_AS_ERRORS (1<<8)
|
#define REDISMODULE_ARGV_CALL_REPLIES_AS_ERRORS (1<<8)
|
||||||
@ -566,7 +570,7 @@ void *RM_PoolAlloc(RedisModuleCtx *ctx, size_t bytes) {
|
|||||||
* Helpers for modules API implementation
|
* Helpers for modules API implementation
|
||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
client *moduleAllocTempClient() {
|
client *moduleAllocTempClient(user *user) {
|
||||||
client *c = NULL;
|
client *c = NULL;
|
||||||
|
|
||||||
if (moduleTempClientCount > 0) {
|
if (moduleTempClientCount > 0) {
|
||||||
@ -576,8 +580,10 @@ client *moduleAllocTempClient() {
|
|||||||
} else {
|
} else {
|
||||||
c = createClient(NULL);
|
c = createClient(NULL);
|
||||||
c->flags |= CLIENT_MODULE;
|
c->flags |= CLIENT_MODULE;
|
||||||
c->user = NULL; /* Root user */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c->user = user;
|
||||||
|
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -761,7 +767,7 @@ void moduleCreateContext(RedisModuleCtx *out_ctx, RedisModule *module, int ctx_f
|
|||||||
out_ctx->module = module;
|
out_ctx->module = module;
|
||||||
out_ctx->flags = ctx_flags;
|
out_ctx->flags = ctx_flags;
|
||||||
if (ctx_flags & REDISMODULE_CTX_TEMP_CLIENT)
|
if (ctx_flags & REDISMODULE_CTX_TEMP_CLIENT)
|
||||||
out_ctx->client = moduleAllocTempClient();
|
out_ctx->client = moduleAllocTempClient(NULL);
|
||||||
else if (ctx_flags & REDISMODULE_CTX_NEW_CLIENT)
|
else if (ctx_flags & REDISMODULE_CTX_NEW_CLIENT)
|
||||||
out_ctx->client = createClient(NULL);
|
out_ctx->client = createClient(NULL);
|
||||||
|
|
||||||
@ -5619,6 +5625,11 @@ RedisModuleString *RM_CreateStringFromCallReply(RedisModuleCallReply *reply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modifies the user that RM_Call will use (e.g. for ACL checks) */
|
||||||
|
void RM_SetContextUser(RedisModuleCtx *ctx, const RedisModuleUser *user) {
|
||||||
|
ctx->user = user;
|
||||||
|
}
|
||||||
|
|
||||||
/* Returns an array of robj pointers, by parsing the format specifier "fmt" as described for
|
/* Returns an array of robj pointers, by parsing the format specifier "fmt" as described for
|
||||||
* the RM_Call(), RM_Replicate() and other module APIs. Populates *argcp with the number of
|
* the RM_Call(), RM_Replicate() and other module APIs. Populates *argcp with the number of
|
||||||
* items and *argvlenp with the length of the allocated argv.
|
* items and *argvlenp with the length of the allocated argv.
|
||||||
@ -5631,7 +5642,7 @@ RedisModuleString *RM_CreateStringFromCallReply(RedisModuleCallReply *reply) {
|
|||||||
* "R" -> REDISMODULE_ARGV_NO_REPLICAS
|
* "R" -> REDISMODULE_ARGV_NO_REPLICAS
|
||||||
* "3" -> REDISMODULE_ARGV_RESP_3
|
* "3" -> REDISMODULE_ARGV_RESP_3
|
||||||
* "0" -> REDISMODULE_ARGV_RESP_AUTO
|
* "0" -> REDISMODULE_ARGV_RESP_AUTO
|
||||||
* "C" -> REDISMODULE_ARGV_CHECK_ACL
|
* "C" -> REDISMODULE_ARGV_RUN_AS_USER
|
||||||
*
|
*
|
||||||
* On error (format specifier error) NULL is returned and nothing is
|
* On error (format specifier error) NULL is returned and nothing is
|
||||||
* allocated. On success the argument vector is returned. */
|
* allocated. On success the argument vector is returned. */
|
||||||
@ -5695,7 +5706,7 @@ robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int
|
|||||||
} else if (*p == '0') {
|
} else if (*p == '0') {
|
||||||
if (flags) (*flags) |= REDISMODULE_ARGV_RESP_AUTO;
|
if (flags) (*flags) |= REDISMODULE_ARGV_RESP_AUTO;
|
||||||
} else if (*p == 'C') {
|
} else if (*p == 'C') {
|
||||||
if (flags) (*flags) |= REDISMODULE_ARGV_CHECK_ACL;
|
if (flags) (*flags) |= REDISMODULE_ARGV_RUN_AS_USER;
|
||||||
} else if (*p == 'S') {
|
} else if (*p == 'S') {
|
||||||
if (flags) (*flags) |= REDISMODULE_ARGV_SCRIPT_MODE;
|
if (flags) (*flags) |= REDISMODULE_ARGV_SCRIPT_MODE;
|
||||||
} else if (*p == 'W') {
|
} else if (*p == 'W') {
|
||||||
@ -5744,7 +5755,17 @@ fmterr:
|
|||||||
* * `0` -- Return the reply in auto mode, i.e. the reply format will be the
|
* * `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
|
* same as the client attached to the given RedisModuleCtx. This will
|
||||||
* probably used when you want to pass the reply directly to the client.
|
* probably used when you want to pass the reply directly to the client.
|
||||||
* * `C` -- Check if command can be executed according to ACL rules.
|
* * `C` -- Run a command as the user attached to the context.
|
||||||
|
* User is either attached automatically via the client that directly
|
||||||
|
* issued the command and created the context or via RM_SetContextUser.
|
||||||
|
* If the context is not directly created by an issued command (such as a
|
||||||
|
* background context and no user was set on it via RM_SetContextUser,
|
||||||
|
* RM_Call will fail.
|
||||||
|
* Checks if the command can be executed according to ACL rules and causes
|
||||||
|
* the command to run as the determined user, so that any future user
|
||||||
|
* dependent activity, such as ACL checks within scripts will proceed as
|
||||||
|
* expected.
|
||||||
|
* Otherwise, the command will run as the Redis unrestricted user.
|
||||||
* * `S` -- Run the command in a script mode, this means that it will raise
|
* * `S` -- Run the command in a script mode, this means that it will raise
|
||||||
* an error if a command which are not allowed inside a script
|
* an error if a command which are not allowed inside a script
|
||||||
* (flagged with the `deny-script` flag) is invoked (like SHUTDOWN).
|
* (flagged with the `deny-script` flag) is invoked (like SHUTDOWN).
|
||||||
@ -5779,7 +5800,7 @@ fmterr:
|
|||||||
* * ESPIPE: Command not allowed on script mode
|
* * ESPIPE: Command not allowed on script mode
|
||||||
*
|
*
|
||||||
* Example code fragment:
|
* Example code fragment:
|
||||||
*
|
*
|
||||||
* reply = RedisModule_Call(ctx,"INCRBY","sc",argv[1],"10");
|
* reply = RedisModule_Call(ctx,"INCRBY","sc",argv[1],"10");
|
||||||
* if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_INTEGER) {
|
* if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_INTEGER) {
|
||||||
* long long myval = RedisModule_CallReplyInteger(reply);
|
* long long myval = RedisModule_CallReplyInteger(reply);
|
||||||
@ -5805,7 +5826,20 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
|||||||
error_as_call_replies = flags & REDISMODULE_ARGV_CALL_REPLIES_AS_ERRORS;
|
error_as_call_replies = flags & REDISMODULE_ARGV_CALL_REPLIES_AS_ERRORS;
|
||||||
va_end(ap);
|
va_end(ap);
|
||||||
|
|
||||||
c = moduleAllocTempClient();
|
user *user = NULL;
|
||||||
|
if (flags & REDISMODULE_ARGV_RUN_AS_USER) {
|
||||||
|
user = ctx->user ? ctx->user->user : ctx->client->user;
|
||||||
|
if (!user) {
|
||||||
|
errno = ENOTSUP;
|
||||||
|
if (error_as_call_replies) {
|
||||||
|
sds msg = sdsnew("cannot run as user, no user directly attached to context or context's client");
|
||||||
|
reply = callReplyCreateError(msg, ctx);
|
||||||
|
}
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c = moduleAllocTempClient(user);
|
||||||
|
|
||||||
/* We do not want to allow block, the module do not expect it */
|
/* We do not want to allow block, the module do not expect it */
|
||||||
c->flags |= CLIENT_DENY_BLOCKING;
|
c->flags |= CLIENT_DENY_BLOCKING;
|
||||||
@ -5953,20 +5987,16 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Check if the user can run this command according to the current
|
/* Check if the user can run this command according to the current
|
||||||
* ACLs. */
|
* ACLs.
|
||||||
if (flags & REDISMODULE_ARGV_CHECK_ACL) {
|
*
|
||||||
|
* If RM_SetContextUser has set a user, that user is used, otherwise
|
||||||
|
* use the attached client's user. If there is no attached client user and no manually
|
||||||
|
* set user, an error will be returned */
|
||||||
|
if (flags & REDISMODULE_ARGV_RUN_AS_USER) {
|
||||||
int acl_errpos;
|
int acl_errpos;
|
||||||
int acl_retval;
|
int acl_retval;
|
||||||
|
|
||||||
if (ctx->client->user == NULL) {
|
acl_retval = ACLCheckAllUserCommandPerm(user,c->cmd,c->argv,c->argc,&acl_errpos);
|
||||||
errno = ENOTSUP;
|
|
||||||
if (error_as_call_replies) {
|
|
||||||
sds msg = sdsnew("acl verification failed, context is not attached to a client.");
|
|
||||||
reply = callReplyCreateError(msg, ctx);
|
|
||||||
}
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
acl_retval = ACLCheckAllUserCommandPerm(ctx->client->user,c->cmd,c->argv,c->argc,&acl_errpos);
|
|
||||||
if (acl_retval != ACL_OK) {
|
if (acl_retval != ACL_OK) {
|
||||||
sds object = (acl_retval == ACL_DENIED_CMD) ? sdsdup(c->cmd->fullname) : sdsdup(c->argv[acl_errpos]->ptr);
|
sds object = (acl_retval == ACL_DENIED_CMD) ? sdsdup(c->cmd->fullname) : sdsdup(c->argv[acl_errpos]->ptr);
|
||||||
addACLLogEntry(ctx->client, acl_retval, ACL_LOG_CTX_MODULE, -1, ctx->client->user->name, object);
|
addACLLogEntry(ctx->client, acl_retval, ACL_LOG_CTX_MODULE, -1, ctx->client->user->name, object);
|
||||||
@ -7202,8 +7232,8 @@ RedisModuleBlockedClient *moduleBlockClient(RedisModuleCtx *ctx, RedisModuleCmdF
|
|||||||
bc->disconnect_callback = NULL; /* Set by RM_SetDisconnectCallback() */
|
bc->disconnect_callback = NULL; /* Set by RM_SetDisconnectCallback() */
|
||||||
bc->free_privdata = free_privdata;
|
bc->free_privdata = free_privdata;
|
||||||
bc->privdata = privdata;
|
bc->privdata = privdata;
|
||||||
bc->reply_client = moduleAllocTempClient();
|
bc->reply_client = moduleAllocTempClient(NULL);
|
||||||
bc->thread_safe_ctx_client = moduleAllocTempClient();
|
bc->thread_safe_ctx_client = moduleAllocTempClient(NULL);
|
||||||
if (bc->client)
|
if (bc->client)
|
||||||
bc->reply_client->resp = bc->client->resp;
|
bc->reply_client->resp = bc->client->resp;
|
||||||
bc->dbid = c->db->id;
|
bc->dbid = c->db->id;
|
||||||
@ -8692,6 +8722,46 @@ int RM_SetModuleUserACL(RedisModuleUser *user, const char* acl) {
|
|||||||
return ACLSetUser(user->user, acl, -1);
|
return ACLSetUser(user->user, acl, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sets the permission of a user with a complete ACL string, such as one
|
||||||
|
* would use on the redis ACL SETUSER command line API. This differs from
|
||||||
|
* RM_SetModuleUserACL, which only takes single ACL operations at a time.
|
||||||
|
*
|
||||||
|
* Returns REDISMODULE_OK on success and REDISMODULE_ERR on failure
|
||||||
|
* if a RedisModuleString is provided in error, a string describing the error
|
||||||
|
* will be returned */
|
||||||
|
int RM_SetModuleUserACLString(RedisModuleCtx *ctx, RedisModuleUser *user, const char *acl, RedisModuleString **error) {
|
||||||
|
serverAssert(user != NULL);
|
||||||
|
|
||||||
|
int argc;
|
||||||
|
sds *argv = sdssplitargs(acl, &argc);
|
||||||
|
|
||||||
|
sds err = ACLStringSetUser(user->user, NULL, argv, argc);
|
||||||
|
|
||||||
|
sdsfreesplitres(argv, argc);
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
if (error) {
|
||||||
|
*error = createObject(OBJ_STRING, err);
|
||||||
|
if (ctx != NULL) autoMemoryAdd(ctx, REDISMODULE_AM_STRING, *error);
|
||||||
|
} else {
|
||||||
|
sdsfree(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return REDISMODULE_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return REDISMODULE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the ACL string for a given user
|
||||||
|
* Returns a RedisModuleString
|
||||||
|
*/
|
||||||
|
RedisModuleString *RM_GetModuleUserACLString(RedisModuleUser *user) {
|
||||||
|
serverAssert(user != NULL);
|
||||||
|
|
||||||
|
return ACLDescribeUser(user->user);
|
||||||
|
}
|
||||||
|
|
||||||
/* Retrieve the user name of the client connection behind the current context.
|
/* 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.
|
* The user name can be used later, in order to get a RedisModuleUser.
|
||||||
* See more information in RM_GetModuleUserFromUserName.
|
* See more information in RM_GetModuleUserFromUserName.
|
||||||
@ -12736,7 +12806,10 @@ void moduleRegisterCoreAPI(void) {
|
|||||||
REGISTER_API(Scan);
|
REGISTER_API(Scan);
|
||||||
REGISTER_API(ScanKey);
|
REGISTER_API(ScanKey);
|
||||||
REGISTER_API(CreateModuleUser);
|
REGISTER_API(CreateModuleUser);
|
||||||
|
REGISTER_API(SetContextUser);
|
||||||
REGISTER_API(SetModuleUserACL);
|
REGISTER_API(SetModuleUserACL);
|
||||||
|
REGISTER_API(SetModuleUserACLString);
|
||||||
|
REGISTER_API(GetModuleUserACLString);
|
||||||
REGISTER_API(GetCurrentUserName);
|
REGISTER_API(GetCurrentUserName);
|
||||||
REGISTER_API(GetModuleUserFromUserName);
|
REGISTER_API(GetModuleUserFromUserName);
|
||||||
REGISTER_API(ACLCheckCommandPermissions);
|
REGISTER_API(ACLCheckCommandPermissions);
|
||||||
|
@ -601,6 +601,13 @@ void addReplyErrorSds(client *c, sds err) {
|
|||||||
addReplyErrorSdsEx(c, err, 0);
|
addReplyErrorSdsEx(c, err, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* See addReplyErrorLength for expectations from the input string. */
|
||||||
|
/* As a side effect the SDS string is freed. */
|
||||||
|
void addReplyErrorSdsSafe(client *c, sds err) {
|
||||||
|
err = sdsmapchars(err, "\r\n", " ", 2);
|
||||||
|
addReplyErrorSdsEx(c, err, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/* Internal function used by addReplyErrorFormat and addReplyErrorFormatEx.
|
/* Internal function used by addReplyErrorFormat and addReplyErrorFormatEx.
|
||||||
* Refer to afterErrorReply for more information about the flags. */
|
* Refer to afterErrorReply for more information about the flags. */
|
||||||
static void addReplyErrorFormatInternal(client *c, int flags, const char *fmt, va_list ap) {
|
static void addReplyErrorFormatInternal(client *c, int flags, const char *fmt, va_list ap) {
|
||||||
|
@ -1198,7 +1198,10 @@ REDISMODULE_API size_t (*RedisModule_MallocSizeString)(RedisModuleString* str) R
|
|||||||
REDISMODULE_API size_t (*RedisModule_MallocSizeDict)(RedisModuleDict* dict) REDISMODULE_ATTR;
|
REDISMODULE_API size_t (*RedisModule_MallocSizeDict)(RedisModuleDict* dict) REDISMODULE_ATTR;
|
||||||
REDISMODULE_API RedisModuleUser * (*RedisModule_CreateModuleUser)(const char *name) REDISMODULE_ATTR;
|
REDISMODULE_API RedisModuleUser * (*RedisModule_CreateModuleUser)(const char *name) REDISMODULE_ATTR;
|
||||||
REDISMODULE_API void (*RedisModule_FreeModuleUser)(RedisModuleUser *user) REDISMODULE_ATTR;
|
REDISMODULE_API void (*RedisModule_FreeModuleUser)(RedisModuleUser *user) REDISMODULE_ATTR;
|
||||||
|
REDISMODULE_API void (*RedisModule_SetContextUser)(RedisModuleCtx *ctx, const RedisModuleUser *user) REDISMODULE_ATTR;
|
||||||
REDISMODULE_API int (*RedisModule_SetModuleUserACL)(RedisModuleUser *user, const char* acl) REDISMODULE_ATTR;
|
REDISMODULE_API int (*RedisModule_SetModuleUserACL)(RedisModuleUser *user, const char* acl) REDISMODULE_ATTR;
|
||||||
|
REDISMODULE_API int (*RedisModule_SetModuleUserACLString)(RedisModuleCtx * ctx, RedisModuleUser *user, const char* acl, RedisModuleString **error) REDISMODULE_ATTR;
|
||||||
|
REDISMODULE_API RedisModuleString * (*RedisModule_GetModuleUserACLString)(RedisModuleUser *user) REDISMODULE_ATTR;
|
||||||
REDISMODULE_API RedisModuleString * (*RedisModule_GetCurrentUserName)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
|
REDISMODULE_API RedisModuleString * (*RedisModule_GetCurrentUserName)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
|
||||||
REDISMODULE_API RedisModuleUser * (*RedisModule_GetModuleUserFromUserName)(RedisModuleString *name) 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_ACLCheckCommandPermissions)(RedisModuleUser *user, RedisModuleString **argv, int argc) REDISMODULE_ATTR;
|
||||||
@ -1538,7 +1541,10 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int
|
|||||||
REDISMODULE_GET_API(MallocSizeDict);
|
REDISMODULE_GET_API(MallocSizeDict);
|
||||||
REDISMODULE_GET_API(CreateModuleUser);
|
REDISMODULE_GET_API(CreateModuleUser);
|
||||||
REDISMODULE_GET_API(FreeModuleUser);
|
REDISMODULE_GET_API(FreeModuleUser);
|
||||||
|
REDISMODULE_GET_API(SetContextUser);
|
||||||
REDISMODULE_GET_API(SetModuleUserACL);
|
REDISMODULE_GET_API(SetModuleUserACL);
|
||||||
|
REDISMODULE_GET_API(SetModuleUserACLString);
|
||||||
|
REDISMODULE_GET_API(GetModuleUserACLString);
|
||||||
REDISMODULE_GET_API(GetCurrentUserName);
|
REDISMODULE_GET_API(GetCurrentUserName);
|
||||||
REDISMODULE_GET_API(GetModuleUserFromUserName);
|
REDISMODULE_GET_API(GetModuleUserFromUserName);
|
||||||
REDISMODULE_GET_API(ACLCheckCommandPermissions);
|
REDISMODULE_GET_API(ACLCheckCommandPermissions);
|
||||||
|
@ -1043,6 +1043,7 @@ typedef struct {
|
|||||||
list *selectors; /* A list of selectors this user validates commands
|
list *selectors; /* A list of selectors this user validates commands
|
||||||
against. This list will always contain at least
|
against. This list will always contain at least
|
||||||
one selector for backwards compatibility. */
|
one selector for backwards compatibility. */
|
||||||
|
robj *acl_string; /* cached string represent of ACLs */
|
||||||
} user;
|
} user;
|
||||||
|
|
||||||
/* With multiplexing we need to take per-client state.
|
/* With multiplexing we need to take per-client state.
|
||||||
@ -2463,6 +2464,7 @@ void addReplyOrErrorObject(client *c, robj *reply);
|
|||||||
void afterErrorReply(client *c, const char *s, size_t len, int flags);
|
void afterErrorReply(client *c, const char *s, size_t len, int flags);
|
||||||
void addReplyErrorSdsEx(client *c, sds err, int flags);
|
void addReplyErrorSdsEx(client *c, sds err, int flags);
|
||||||
void addReplyErrorSds(client *c, sds err);
|
void addReplyErrorSds(client *c, sds err);
|
||||||
|
void addReplyErrorSdsSafe(client *c, sds err);
|
||||||
void addReplyError(client *c, const char *err);
|
void addReplyError(client *c, const char *err);
|
||||||
void addReplyErrorArity(client *c);
|
void addReplyErrorArity(client *c);
|
||||||
void addReplyErrorExpireTime(client *c);
|
void addReplyErrorExpireTime(client *c);
|
||||||
@ -2783,11 +2785,12 @@ int ACLCheckAllUserCommandPerm(user *u, struct redisCommand *cmd, robj **argv, i
|
|||||||
int ACLUserCheckCmdWithUnrestrictedKeyAccess(user *u, struct redisCommand *cmd, robj **argv, int argc, int flags);
|
int ACLUserCheckCmdWithUnrestrictedKeyAccess(user *u, struct redisCommand *cmd, robj **argv, int argc, int flags);
|
||||||
int ACLCheckAllPerm(client *c, int *idxptr);
|
int ACLCheckAllPerm(client *c, int *idxptr);
|
||||||
int ACLSetUser(user *u, const char *op, ssize_t oplen);
|
int ACLSetUser(user *u, const char *op, ssize_t oplen);
|
||||||
|
sds ACLStringSetUser(user *u, sds username, sds *argv, int argc);
|
||||||
uint64_t ACLGetCommandCategoryFlagByName(const char *name);
|
uint64_t ACLGetCommandCategoryFlagByName(const char *name);
|
||||||
int ACLAppendUserForLoading(sds *argv, int argc, int *argc_err);
|
int ACLAppendUserForLoading(sds *argv, int argc, int *argc_err);
|
||||||
const char *ACLSetUserStringError(void);
|
const char *ACLSetUserStringError(void);
|
||||||
int ACLLoadConfiguredUsers(void);
|
int ACLLoadConfiguredUsers(void);
|
||||||
sds ACLDescribeUser(user *u);
|
robj *ACLDescribeUser(user *u);
|
||||||
void ACLLoadUsersAtStartup(void);
|
void ACLLoadUsersAtStartup(void);
|
||||||
void addReplyCommandCategories(client *c, struct redisCommand *cmd);
|
void addReplyCommandCategories(client *c, struct redisCommand *cmd);
|
||||||
user *ACLCreateUnlinkedUser();
|
user *ACLCreateUnlinkedUser();
|
||||||
|
@ -58,7 +58,8 @@ TEST_MODULES = \
|
|||||||
eventloop.so \
|
eventloop.so \
|
||||||
moduleconfigs.so \
|
moduleconfigs.so \
|
||||||
moduleconfigstwo.so \
|
moduleconfigstwo.so \
|
||||||
publish.so
|
publish.so \
|
||||||
|
usercall.so
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
|
|
||||||
|
129
tests/modules/usercall.c
Normal file
129
tests/modules/usercall.c
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
#include "redismodule.h"
|
||||||
|
|
||||||
|
RedisModuleUser *user = NULL;
|
||||||
|
|
||||||
|
int call_without_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||||
|
if (argc < 2) {
|
||||||
|
return RedisModule_WrongArity(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *cmd = RedisModule_StringPtrLen(argv[1], NULL);
|
||||||
|
|
||||||
|
RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, "Ev", argv + 2, argc - 2);
|
||||||
|
if (!rep) {
|
||||||
|
RedisModule_ReplyWithError(ctx, "NULL reply returned");
|
||||||
|
} else {
|
||||||
|
RedisModule_ReplyWithCallReply(ctx, rep);
|
||||||
|
RedisModule_FreeCallReply(rep);
|
||||||
|
}
|
||||||
|
return REDISMODULE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int call_with_user_flag(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||||
|
if (argc < 3) {
|
||||||
|
return RedisModule_WrongArity(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisModule_SetContextUser(ctx, user);
|
||||||
|
|
||||||
|
/* Append Ev to the provided flags. */
|
||||||
|
RedisModuleString *flags = RedisModule_CreateStringFromString(ctx, argv[1]);
|
||||||
|
RedisModule_StringAppendBuffer(ctx, flags, "Ev", 2);
|
||||||
|
|
||||||
|
const char* flg = RedisModule_StringPtrLen(flags, NULL);
|
||||||
|
const char* cmd = RedisModule_StringPtrLen(argv[2], NULL);
|
||||||
|
|
||||||
|
RedisModuleCallReply* rep = RedisModule_Call(ctx, cmd, flg, argv + 3, argc - 3);
|
||||||
|
if (!rep) {
|
||||||
|
RedisModule_ReplyWithError(ctx, "NULL reply returned");
|
||||||
|
} else {
|
||||||
|
RedisModule_ReplyWithCallReply(ctx, rep);
|
||||||
|
RedisModule_FreeCallReply(rep);
|
||||||
|
}
|
||||||
|
RedisModule_FreeString(ctx, flags);
|
||||||
|
|
||||||
|
return REDISMODULE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int add_to_acl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||||
|
if (argc != 2) {
|
||||||
|
return RedisModule_WrongArity(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t acl_len;
|
||||||
|
const char *acl = RedisModule_StringPtrLen(argv[1], &acl_len);
|
||||||
|
|
||||||
|
RedisModuleString *error;
|
||||||
|
int ret = RedisModule_SetModuleUserACLString(ctx, user, acl, &error);
|
||||||
|
if (ret) {
|
||||||
|
size_t len;
|
||||||
|
const char * e = RedisModule_StringPtrLen(error, &len);
|
||||||
|
RedisModule_ReplyWithError(ctx, e);
|
||||||
|
return REDISMODULE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisModule_ReplyWithSimpleString(ctx, "OK");
|
||||||
|
|
||||||
|
return REDISMODULE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_acl(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||||
|
REDISMODULE_NOT_USED(argv);
|
||||||
|
|
||||||
|
if (argc != 1) {
|
||||||
|
return RedisModule_WrongArity(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisModule_Assert(user != NULL);
|
||||||
|
|
||||||
|
RedisModuleString *acl = RedisModule_GetModuleUserACLString(user);
|
||||||
|
|
||||||
|
RedisModule_ReplyWithString(ctx, acl);
|
||||||
|
|
||||||
|
RedisModule_FreeString(NULL, acl);
|
||||||
|
|
||||||
|
return REDISMODULE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int reset_user(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
|
||||||
|
REDISMODULE_NOT_USED(argv);
|
||||||
|
|
||||||
|
if (argc != 1) {
|
||||||
|
return RedisModule_WrongArity(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user != NULL) {
|
||||||
|
RedisModule_FreeModuleUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
user = RedisModule_CreateModuleUser("module_user");
|
||||||
|
|
||||||
|
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,"usercall",1,REDISMODULE_APIVER_1)== REDISMODULE_ERR)
|
||||||
|
return REDISMODULE_ERR;
|
||||||
|
|
||||||
|
if (RedisModule_CreateCommand(ctx,"usercall.call_without_user", call_without_user,"write",0,0,0) == REDISMODULE_ERR)
|
||||||
|
return REDISMODULE_ERR;
|
||||||
|
|
||||||
|
if (RedisModule_CreateCommand(ctx,"usercall.call_with_user_flag", call_with_user_flag,"write",0,0,0) == REDISMODULE_ERR)
|
||||||
|
return REDISMODULE_ERR;
|
||||||
|
|
||||||
|
if (RedisModule_CreateCommand(ctx, "usercall.add_to_acl", add_to_acl, "write",0,0,0) == REDISMODULE_ERR)
|
||||||
|
return REDISMODULE_ERR;
|
||||||
|
|
||||||
|
if (RedisModule_CreateCommand(ctx,"usercall.reset_user", reset_user,"write",0,0,0) == REDISMODULE_ERR)
|
||||||
|
return REDISMODULE_ERR;
|
||||||
|
|
||||||
|
if (RedisModule_CreateCommand(ctx,"usercall.get_acl", get_acl,"write",0,0,0) == REDISMODULE_ERR)
|
||||||
|
return REDISMODULE_ERR;
|
||||||
|
|
||||||
|
return REDISMODULE_OK;
|
||||||
|
}
|
95
tests/unit/moduleapi/usercall.tcl
Normal file
95
tests/unit/moduleapi/usercall.tcl
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
set testmodule [file normalize tests/modules/usercall.so]
|
||||||
|
|
||||||
|
set test_script_set "#!lua
|
||||||
|
redis.call('set','x',1)
|
||||||
|
return 1"
|
||||||
|
|
||||||
|
set test_script_get "#!lua
|
||||||
|
redis.call('get','x')
|
||||||
|
return 1"
|
||||||
|
|
||||||
|
start_server {tags {"modules usercall"}} {
|
||||||
|
r module load $testmodule
|
||||||
|
|
||||||
|
# baseline test that module isn't doing anything weird
|
||||||
|
test {test module check regular redis command without user/acl} {
|
||||||
|
assert_equal [r usercall.reset_user] OK
|
||||||
|
assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
|
||||||
|
assert_equal [r usercall.call_without_user set x 5] OK
|
||||||
|
assert_equal [r usercall.reset_user] OK
|
||||||
|
}
|
||||||
|
|
||||||
|
# call with user with acl set on it, but without testing the acl
|
||||||
|
test {test module check regular redis command with user} {
|
||||||
|
assert_equal [r set x 5] OK
|
||||||
|
|
||||||
|
assert_equal [r usercall.reset_user] OK
|
||||||
|
assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
|
||||||
|
# off and sanitize-payload because module user / default value
|
||||||
|
assert_equal [r usercall.get_acl] "off sanitize-payload ~* &* +@all -set"
|
||||||
|
|
||||||
|
# doesn't fail for regular commands as just testing acl here
|
||||||
|
assert_equal [r usercall.call_with_user_flag {} set x 10] OK
|
||||||
|
|
||||||
|
assert_equal [r get x] 10
|
||||||
|
assert_equal [r usercall.reset_user] OK
|
||||||
|
}
|
||||||
|
|
||||||
|
# call with user with acl set on it, but with testing the acl in rm_call (for cmd itself)
|
||||||
|
test {test module check regular redis command with user and acl} {
|
||||||
|
assert_equal [r set x 5] OK
|
||||||
|
|
||||||
|
assert_equal [r usercall.reset_user] OK
|
||||||
|
assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
|
||||||
|
# off and sanitize-payload because module user / default value
|
||||||
|
assert_equal [r usercall.get_acl] "off sanitize-payload ~* &* +@all -set"
|
||||||
|
|
||||||
|
# fails here as testing acl in rm call
|
||||||
|
catch {r usercall.call_with_user_flag C set x 10} e
|
||||||
|
assert_match {*ERR acl verification failed*} $e
|
||||||
|
|
||||||
|
assert_equal [r usercall.call_with_user_flag C get x] 5
|
||||||
|
|
||||||
|
assert_equal [r usercall.reset_user] OK
|
||||||
|
}
|
||||||
|
|
||||||
|
# baseline script test, call without user on script
|
||||||
|
test {test module check eval script without user} {
|
||||||
|
set sha_set [r script load $test_script_set]
|
||||||
|
set sha_get [r script load $test_script_get]
|
||||||
|
|
||||||
|
assert_equal [r usercall.call_without_user evalsha $sha_set 0] 1
|
||||||
|
assert_equal [r usercall.call_without_user evalsha $sha_get 0] 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# baseline script test, call without user on script
|
||||||
|
test {test module check eval script with user being set, but not acl testing} {
|
||||||
|
set sha_set [r script load $test_script_set]
|
||||||
|
set sha_get [r script load $test_script_get]
|
||||||
|
|
||||||
|
assert_equal [r usercall.reset_user] OK
|
||||||
|
assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
|
||||||
|
# off and sanitize-payload because module user / default value
|
||||||
|
assert_equal [r usercall.get_acl] "off sanitize-payload ~* &* +@all -set"
|
||||||
|
|
||||||
|
# passes as not checking ACL
|
||||||
|
assert_equal [r usercall.call_with_user_flag {} evalsha $sha_set 0] 1
|
||||||
|
assert_equal [r usercall.call_with_user_flag {} evalsha $sha_get 0] 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# call with user on script (without rm_call acl check) to ensure user carries through to script execution
|
||||||
|
# we already tested the check in rm_call above, here we are checking the script itself will enforce ACL
|
||||||
|
test {test module check eval script with user and acl} {
|
||||||
|
set sha_set [r script load $test_script_set]
|
||||||
|
set sha_get [r script load $test_script_get]
|
||||||
|
|
||||||
|
assert_equal [r usercall.reset_user] OK
|
||||||
|
assert_equal [r usercall.add_to_acl "~* &* +@all -set"] OK
|
||||||
|
|
||||||
|
# fails here in script, as rm_call will permit the eval call
|
||||||
|
catch {r usercall.call_with_user_flag C evalsha $sha_set 0} e
|
||||||
|
assert_match {*ERR The user executing the script can't run this command or subcommand script*} $e
|
||||||
|
|
||||||
|
assert_equal [r usercall.call_with_user_flag C evalsha $sha_get 0] 1
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user