diff options
author | yoav-steinberg <yoav@monfort.co.il> | 2022-02-07 08:04:01 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-07 08:04:01 +0200 |
commit | 9dfeda58ed8425a75a0a81939e03fee8f422be3b (patch) | |
tree | 4bf46f3651712bf2c9125138cec40d5ab0b87211 /src/script_lua.c | |
parent | c5e3d135ae640027d388a4d98bc16cf0b5dbf789 (diff) | |
download | redis-9dfeda58ed8425a75a0a81939e03fee8f422be3b.tar.gz |
acl check api for functions and eval (#10220)
Changes:
1. Adds the `redis.acl_check_cmd()` api to lua scripts. It can be used to check if the
current user has permissions to execute a given command. The new function receives
the command to check as an argument exactly like `redis.call()` receives the command
to execute as an argument.
2. In the PR I unified the code used to convert lua arguments to redis argv arguments from
both the new `redis.acl_check_cmd()` API and the `redis.[p]call()` API. This cleans up
potential duplicate code.
3. While doing the refactoring in 2 I noticed there's an optimization to reduce allocation calls
when parsing lua arguments into an `argv` array in the `redis.[p]call()` implementation.
These optimizations were introduced years ago in 48c49c485155ba9e4a7851fd1644c171674c6f0f
and 4f686555ce962e6632235d824512ea8fdeda003c. It is unclear why this was added.
The original commit message claims a 4% performance increase which I couldn't recreate
and might not be worth it even if it did recreate. This PR removes that optimization.
Following are details of the benchmark I did that couldn't reveal any performance
improvements due to this optimization:
```
benchmark 1: src/redis-benchmark -P 500 -n 10000000 eval 'return redis.call("ping")' 0
benchmark 2: src/redis-benchmark -P 500 -r 1000 -n 1000000 eval 'return redis.call("mset","k1__rand_int__","v1__rand_int__","k2__rand_int__","v2__rand_int__","k3__rand_int__","v3__rand_int__","k4__rand_int__","v4__rand_int__")' 0
benchmark 3: src/redis-benchmark -P 500 -r 1000 -n 100000 eval "for i=1,100,1 do redis.call('set','kk'..i,'vv'..__rand_int__) end return redis.call('get','kk5')" 0
benchmark 4: src/redis-benchmark -P 500 -r 1000 -n 1000000 eval 'return redis.call("mset","k1__rand_int__","v1__rand_int__","k2__rand_int__","v2__rand_int__","k3__rand_int__","v3__rand_int__","k4__rand_int__","v4__rand_int__xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")'
```
I ran the benchmark on this branch with and without commit 68b71680a4d3bb8f0509e06578a9f15d05b92a47
Results in requests per second:
cmd | without optimization | without optimization 2nd run | with original optimization | with original optimization 2nd run
-- | -- | -- | -- | --
1 | 461233.34 | 477395.31 | 471098.16 | 469946.91
2 | 34774.14 | 35469.8 | 35149.38 | 34464.93
3 | 6390.59 | 6281.41 | 6146.28 | 6464.12
4 | 28005.71 | | 27965.77 |
As you can see, different use cases showed identical or negligible performance differences.
So finally I decided to chuck the original optimization and simplify the code.
Diffstat (limited to 'src/script_lua.c')
-rw-r--r-- | src/script_lua.c | 193 |
1 files changed, 98 insertions, 95 deletions
diff --git a/src/script_lua.c b/src/script_lua.c index d7332cf86..2873159d4 100644 --- a/src/script_lua.c +++ b/src/script_lua.c @@ -655,55 +655,19 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu * Lua redis.* functions implementations. * ------------------------------------------------------------------------- */ -#define LUA_CMD_OBJCACHE_SIZE 32 -#define LUA_CMD_OBJCACHE_MAX_LEN 64 -static int luaRedisGenericCommand(lua_State *lua, int raise_error) { - int j, argc = lua_gettop(lua); - scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME); - if (!rctx) { - luaPushError(lua, "redis.call/pcall can only be called inside a script invocation"); - return luaRaiseError(lua); - } - sds err = NULL; - client* c = rctx->c; - sds reply; - - /* Cached across calls. */ - static robj **argv = NULL; - static int argv_size = 0; - static robj *cached_objects[LUA_CMD_OBJCACHE_SIZE]; - static size_t cached_objects_len[LUA_CMD_OBJCACHE_SIZE]; - static int inuse = 0; /* Recursive calls detection. */ - - /* By using Lua debug hooks it is possible to trigger a recursive call - * to luaRedisGenericCommand(), which normally should never happen. - * To make this function reentrant is futile and makes it slower, but - * we should at least detect such a misuse, and abort. */ - if (inuse) { - char *recursion_warning = - "luaRedisGenericCommand() recursive call detected. " - "Are you doing funny stuff with Lua debug hooks?"; - serverLog(LL_WARNING,"%s",recursion_warning); - luaPushError(lua,recursion_warning); - return 1; - } - inuse++; - +static robj **luaArgsToRedisArgv(lua_State *lua, int *argc) { + int j; /* Require at least one argument */ - if (argc == 0) { - luaPushError(lua, - "Please specify at least one argument for redis.call()"); - inuse--; - return raise_error ? luaRaiseError(lua) : 1; + *argc = lua_gettop(lua); + if (*argc == 0) { + luaPushError(lua, "Please specify at least one argument for this redis lib call"); + return NULL; } /* Build the arguments vector */ - if (argv_size < argc) { - argv = zrealloc(argv,sizeof(robj*)*argc); - argv_size = argc; - } + robj **argv = zcalloc(sizeof(robj*) * *argc); - for (j = 0; j < argc; j++) { + for (j = 0; j < *argc; j++) { char *obj_s; size_t obj_len; char dbuf[64]; @@ -720,38 +684,62 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { if (obj_s == NULL) break; /* Not a string. */ } - /* Try to use a cached object. */ - if (j < LUA_CMD_OBJCACHE_SIZE && cached_objects[j] && - cached_objects_len[j] >= obj_len) - { - sds s = cached_objects[j]->ptr; - argv[j] = cached_objects[j]; - cached_objects[j] = NULL; - memcpy(s,obj_s,obj_len+1); - sdssetlen(s, obj_len); - } else { - argv[j] = createStringObject(obj_s, obj_len); - } + argv[j] = createStringObject(obj_s, obj_len); } + /* Pop all arguments from the stack, we do not need them anymore + * and this way we guaranty we will have room on the stack for the result. */ + lua_pop(lua, *argc); + /* Check if one of the arguments passed by the Lua script * is not a string or an integer (lua_isstring() return true for * integers as well). */ - if (j != argc) { + if (j != *argc) { j--; while (j >= 0) { decrRefCount(argv[j]); j--; } - luaPushError(lua, - "Lua redis() command arguments must be strings or integers"); - inuse--; + zfree(argv); + luaPushError(lua, "Lua redis lib command arguments must be strings or integers"); + return NULL; + } + + return argv; +} + +static int luaRedisGenericCommand(lua_State *lua, int raise_error) { + int j; + scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME); + if (!rctx) { + luaPushError(lua, "redis.call/pcall can only be called inside a script invocation"); + return luaRaiseError(lua); + } + sds err = NULL; + client* c = rctx->c; + sds reply; + + int argc; + robj **argv = luaArgsToRedisArgv(lua, &argc); + if (argv == NULL) { return raise_error ? luaRaiseError(lua) : 1; } - /* Pop all arguments from the stack, we do not need them anymore - * and this way we guaranty we will have room on the stack for the result. */ - lua_pop(lua, argc); + static int inuse = 0; /* Recursive calls detection. */ + + /* By using Lua debug hooks it is possible to trigger a recursive call + * to luaRedisGenericCommand(), which normally should never happen. + * To make this function reentrant is futile and makes it slower, but + * we should at least detect such a misuse, and abort. */ + if (inuse) { + char *recursion_warning = + "luaRedisGenericCommand() recursive call detected. " + "Are you doing funny stuff with Lua debug hooks?"; + serverLog(LL_WARNING,"%s",recursion_warning); + luaPushError(lua,recursion_warning); + return 1; + } + inuse++; /* Log the command if debugging is active. */ if (ldbIsEnabled()) { @@ -769,7 +757,6 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { ldbLog(cmdlog); } - scriptCall(rctx, argv, argc, &err); if (err) { luaPushError(lua, err); @@ -810,45 +797,16 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) { cleanup: /* Clean up. Command code may have changed argv/argc so we use the * argv/argc of the client instead of the local variables. */ - for (j = 0; j < c->argc; j++) { - robj *o = c->argv[j]; - - /* Try to cache the object in the cached_objects array. - * The object must be small, SDS-encoded, and with refcount = 1 - * (we must be the only owner) for us to cache it. */ - if (j < LUA_CMD_OBJCACHE_SIZE && - o->refcount == 1 && - (o->encoding == OBJ_ENCODING_RAW || - o->encoding == OBJ_ENCODING_EMBSTR) && - sdslen(o->ptr) <= LUA_CMD_OBJCACHE_MAX_LEN) - { - sds s = o->ptr; - if (cached_objects[j]) decrRefCount(cached_objects[j]); - cached_objects[j] = o; - cached_objects_len[j] = sdsalloc(s); - } else { - decrRefCount(o); - } - } - - if (c->argv != argv) { - zfree(c->argv); - argv = NULL; - argv_size = 0; - } - + freeClientArgv(c); c->user = NULL; - c->argv = NULL; - c->argc = 0; + inuse--; if (raise_error) { /* If we are here we should have an error in the stack, in the * form of a table with an "err" field. Extract the string to * return the plain error. */ - inuse--; return luaRaiseError(lua); } - inuse--; return 1; } @@ -939,6 +897,46 @@ static int luaRedisSetReplCommand(lua_State *lua) { return 0; } +/* redis.acl_check_cmd() + * + * Checks ACL permissions for given command for the current user. */ +static int luaRedisAclCheckCmdPermissionsCommand(lua_State *lua) { + scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME); + if (!rctx) { + lua_pushstring(lua, "redis.acl_check_cmd can only be called inside a script invocation"); + return lua_error(lua); + } + int raise_error = 0; + + int argc; + robj **argv = luaArgsToRedisArgv(lua, &argc); + + /* Require at least one argument */ + if (argv == NULL) return lua_error(lua); + + /* Find command */ + struct redisCommand *cmd; + if ((cmd = lookupCommand(argv, argc)) == NULL) { + lua_pushstring(lua, "Invalid command passed to redis.acl_check_cmd()"); + raise_error = 1; + } else { + int keyidxptr; + if (ACLCheckAllUserCommandPerm(rctx->original_client->user, cmd, argv, argc, &keyidxptr) != ACL_OK) { + lua_pushboolean(lua, 0); + } else { + lua_pushboolean(lua, 1); + } + } + + while (argc--) decrRefCount(argv[argc]); + zfree(argv); + if (raise_error) + return lua_error(lua); + else + return 1; +} + + /* redis.log() */ static int luaLogCommand(lua_State *lua) { int j, argc = lua_gettop(lua); @@ -1251,8 +1249,13 @@ void luaRegisterRedisAPI(lua_State* lua) { lua_pushstring(lua,"REPL_ALL"); lua_pushnumber(lua,PROPAGATE_AOF|PROPAGATE_REPL); + lua_settable(lua,-3); + /* redis.acl_check_cmd */ + lua_pushstring(lua,"acl_check_cmd"); + lua_pushcfunction(lua,luaRedisAclCheckCmdPermissionsCommand); lua_settable(lua,-3); + /* Finally set the table as 'redis' global var. */ lua_setglobal(lua,REDIS_API_NAME); |