diff --git a/changelogs/unreleased/gh-8803-exec-priv.md b/changelogs/unreleased/gh-8803-exec-priv.md new file mode 100644 index 0000000000000000000000000000000000000000..30a4a7370844542c94de89b92ad8528b2cd34191 --- /dev/null +++ b/changelogs/unreleased/gh-8803-exec-priv.md @@ -0,0 +1,8 @@ +## feature/box + +* Introduced the new `lua_eval` and `lua_call` object types for + `box.schema.user.grant`. Granting the `'execute'` privilege on `lua_eval` + allows the user to execute an arbitrary Lua expression with the + `IPROTO_EVAL` request. Granting the `'execute'` privilege on `lua_call` + allows the user to execute any global user-defined Lua function with + the `IPROTO_CALL` request (gh-8803). diff --git a/src/box/call.c b/src/box/call.c index 65312e2406c21cf20dbe29daa8cc457dfa595300..ed1e85903f8ebe7579e505d2a07d3f4360a2e369 100644 --- a/src/box/call.c +++ b/src/box/call.c @@ -31,6 +31,7 @@ #include "box/call.h" #include "lua/call.h" +#include "../lua/init.h" /* tarantool_lua_is_builtin_global */ #include "schema.h" #include "session.h" #include "func.h" @@ -151,6 +152,45 @@ box_run_on_call(enum iproto_type type, const char *expr, int expr_len, trigger_run(&box_on_call, &ctx); } +/** Checks if the current user may execute any global Lua function. */ +static int +access_check_call(const char *name, uint32_t name_len) +{ + struct credentials *cr = effective_user(); + user_access_t access = PRIV_X | PRIV_U; + access &= ~cr->universal_access; + if (access == 0) + return 0; + access &= ~universe.access_lua_call[cr->auth_token].effective; + if (access == 0 && !tarantool_lua_is_builtin_global(name, name_len)) + return 0; + struct user *user = user_find(cr->uid); + if (user != NULL) + diag_set(AccessDeniedError, priv_name(PRIV_X), + schema_object_name(SC_FUNCTION), + tt_cstr(name, name_len), user->def->name); + return -1; +} + +/** Checks if the current user may execute an arbitrary Lua expression. */ +static int +access_check_eval(void) +{ + struct credentials *cr = effective_user(); + user_access_t access = PRIV_X | PRIV_U; + access &= ~cr->universal_access; + if (access == 0) + return 0; + access &= ~universe.access_lua_eval[cr->auth_token].effective; + if (access == 0) + return 0; + struct user *user = user_find(cr->uid); + if (user != NULL) + diag_set(AccessDeniedError, priv_name(PRIV_X), + schema_object_name(SC_UNIVERSE), "", user->def->name); + return -1; +} + int box_process_call(struct call_request *request, struct port *port) { @@ -172,9 +212,7 @@ box_process_call(struct call_request *request, struct port *port) if (func_call_no_access_check(func, &args, port) != 0) return -1; } else { - if (access_check_universe_object(PRIV_X | PRIV_U, - SC_FUNCTION, - tt_cstr(name, name_len)) != 0) + if (access_check_call(name, name_len) != 0) return -1; box_run_on_call(IPROTO_CALL, name, name_len, request->args); if (box_lua_call(name, name_len, &args, port) != 0) @@ -188,7 +226,7 @@ box_process_eval(struct call_request *request, struct port *port) { rmean_collect(rmean_box, IPROTO_EVAL, 1); /* Check permissions */ - if (access_check_universe(PRIV_X) != 0) + if (access_check_eval() != 0) return -1; struct port args; port_msgpack_create(&args, request->args, diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index 8950935fbef680a3dc64c2302f1c5106aaef088f..7d99f15f678bc0a88ebead9282672e9c6d28d3e2 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -2873,6 +2873,8 @@ end -- allowed combination of privilege bits for object local priv_object_combo = { ["universe"] = box.priv.ALL, + ["lua_call"] = bit.bor(box.priv.X, box.priv.U), + ["lua_eval"] = bit.bor(box.priv.X, box.priv.U), -- sic: we allow to grant 'execute' on space. This is a legacy -- bug, please fix it in 2.0 ["space"] = bit.bxor(box.priv.ALL, box.priv.S, @@ -2946,12 +2948,23 @@ local function privilege_name(privilege) return table.concat(names, ",") end +-- Set of object types that have a single global instance. +local singleton_object_types = { + ['universe'] = true, + ['lua_call'] = true, + ['lua_eval'] = true, +} + +local function is_singleton_object_type(object_type) + return singleton_object_types[object_type] +end + local function object_resolve(object_type, object_name) if object_name ~= nil and type(object_name) ~= 'string' and type(object_name) ~= 'number' then box.error(box.error.ILLEGAL_PARAMS, "wrong object name type") end - if object_type == 'universe' then + if is_singleton_object_type(object_type) then return 0 end if object_type == 'space' then @@ -3015,7 +3028,7 @@ local function object_resolve(object_type, object_name) end local function object_name(object_type, object_id) - if object_type == 'universe' or object_id == '' then + if is_singleton_object_type(object_type) or object_id == '' then return "" end local space @@ -3350,7 +3363,7 @@ local function grant(uid, name, privilege, object_type, privilege == 'execute' then box.error(box.error.ROLE_GRANTED, name, object_name) else - if object_type ~= 'universe' then + if not is_singleton_object_type(object_type) then object_name = string.format(" '%s'", object_name) end box.error(box.error.PRIV_GRANTED, name, privilege, diff --git a/src/box/schema_def.c b/src/box/schema_def.c index cbfad479ac1c22767e14952cb9e869e86199ca4e..b635422076dd2d6d40d14bd9e4c60d4454849940 100644 --- a/src/box/schema_def.c +++ b/src/box/schema_def.c @@ -38,6 +38,8 @@ const char *sql_storage_engine_strs[] = { static const char *object_type_strs[] = { /* [SC_UKNNOWN] = */ "unknown", /* [SC_UNIVERSE] = */ "universe", + /* [SC_LUA_CALL] = */ "lua_call", + /* [SC_LUA_EVAL] = */ "lua_eval", /* [SC_SPACE] = */ "space", /* [SC_FUNCTION] = */ "function", /* [SC_USER] = */ "user", diff --git a/src/box/schema_def.h b/src/box/schema_def.h index 60d2340015f3e4c6f93242492a51bb34403924b0..34d8571223b25c071ad262aa46c0ec479741fe31 100644 --- a/src/box/schema_def.h +++ b/src/box/schema_def.h @@ -303,6 +303,8 @@ enum { enum schema_object_type { SC_UNKNOWN = 0, SC_UNIVERSE, + SC_LUA_CALL, + SC_LUA_EVAL, SC_SPACE, SC_FUNCTION, SC_USER, diff --git a/src/box/session.c b/src/box/session.c index b9bf8e3ee00e9e691f90928ff709b201abf88833..fb1be868056faad06b80660381ed8c9a4e9e94aa 100644 --- a/src/box/session.c +++ b/src/box/session.c @@ -455,9 +455,7 @@ access_check_session(struct user *user) } int -access_check_universe_object(user_access_t access, - enum schema_object_type object_type, - const char *object_name) +access_check_universe(user_access_t access) { struct credentials *credentials = effective_user(); access |= PRIV_U; @@ -473,7 +471,7 @@ access_check_universe_object(user_access_t access, if (user != NULL) { diag_set(AccessDeniedError, priv_name(denied_access), - schema_object_name(object_type), object_name, + schema_object_name(SC_UNIVERSE), "", user->def->name); } else { /* @@ -488,12 +486,6 @@ access_check_universe_object(user_access_t access, return 0; } -int -access_check_universe(user_access_t access) -{ - return access_check_universe_object(access, SC_UNIVERSE, ""); -} - /** * If set, raise an error on any attempt to use box.session.push. */ diff --git a/src/box/session.h b/src/box/session.h index 6e02e62afc32d9e52bf9187501ae5672ef7a85b6..a0575b850afd76083f741e1b822dd5dda4c124ba 100644 --- a/src/box/session.h +++ b/src/box/session.h @@ -393,16 +393,6 @@ access_check_session(struct user *user); int access_check_universe(user_access_t access); -/** - * Same as access_check_universe(), but in case the current user - * doesn't have universal access, set AccessDeniedError for the - * given object type and name. - */ -int -access_check_universe_object(user_access_t access, - enum schema_object_type object_type, - const char *object_name); - /** * This function is called by public API wrappers around session push. * It logs a deprecation warning. If session push is disabled, it also diff --git a/src/box/user.cc b/src/box/user.cc index 5510f902171e9ab1fbbcd6f1f1abbf117672da6c..7231481cd7e71a9263070d67d6d9dfa22e6b451d 100644 --- a/src/box/user.cc +++ b/src/box/user.cc @@ -216,6 +216,10 @@ access_find(const struct priv_def *priv) switch (priv->object_type) { case SC_UNIVERSE: return universe.access; + case SC_LUA_CALL: + return universe.access_lua_call; + case SC_LUA_EVAL: + return universe.access_lua_eval; case SC_SPACE: { if (priv->is_entity_access) diff --git a/src/box/user.h b/src/box/user.h index efbc39b92876edbfe24a08365810e5b9ff72b8b6..5a24cf79ee47b21d7863e32a20a70fe684463da9 100644 --- a/src/box/user.h +++ b/src/box/user.h @@ -43,6 +43,10 @@ extern "C" { struct universe { /** Global privileges this user has on the universe. */ struct access access[BOX_USER_MAX]; + /** Privileges to execute any global Lua function with IPROTO_CALL. */ + struct access access_lua_call[BOX_USER_MAX]; + /** Privileges to execute any Lua expression with IPROTO_EVAL. */ + struct access access_lua_eval[BOX_USER_MAX]; }; /** A single instance of the universe. */ diff --git a/src/lua/init.c b/src/lua/init.c index 1d741b90a92b371b874e24f1fedad89f645d4c1f..186b81df74d54455063b4ec0140f57dea092fc3e 100644 --- a/src/lua/init.c +++ b/src/lua/init.c @@ -45,6 +45,7 @@ #include <fiber.h> #include "version.h" +#include "assoc.h" #include "coio.h" #include "core/backtrace.h" #include "core/tt_static.h" @@ -445,6 +446,62 @@ static const char *lua_modules_preload[] = { NULL }; +/** + * Names of all global built-in objects. Note that this is a set, not a map, + * i.e. only keys are used while values are NULL. + * + * We consider a global key (from _G) to be built-in if it exists after + * initializing all Tarantool modules but before executing the user script. + */ +static struct mh_strnptr_t *builtin_globals = NULL; + +static void +builtin_globals_init(struct lua_State *L) +{ + struct mh_strnptr_t *h = mh_strnptr_new(); + lua_pushnil(L); + while (lua_next(L, LUA_GLOBALSINDEX) != 0) { + lua_pop(L, 1); /* pop the value, leave the key */ + if (lua_type(L, -1) == LUA_TSTRING) { + size_t len; + const char *s = xstrdup(lua_tolstring(L, -1, &len)); + uint32_t hash = mh_strn_hash(s, len); + struct mh_strnptr_node_t n = {s, len, hash, NULL}; + struct mh_strnptr_node_t prev; + struct mh_strnptr_node_t *prev_ptr = &prev; + mh_strnptr_put(h, &n, &prev_ptr, NULL); + assert(prev_ptr == NULL); + } + } + builtin_globals = h; +} + +static void +builtin_globals_free(void) +{ + struct mh_strnptr_t *h = builtin_globals; + if (h != NULL) { + builtin_globals = NULL; + mh_int_t i; + mh_foreach(h, i) + free((void *)mh_strnptr_node(h, i)->str); + mh_strnptr_delete(h); + } +} + +bool +tarantool_lua_is_builtin_global(const char *name, uint32_t name_len) +{ + /* Extract the top-level namespace prefix. */ + uint32_t len = 0; + for (const char *s = name; len < name_len; s++, len++) { + if (*s == ' ' || *s == '.' || *s == ':' || *s == '[') + break; + } + struct mh_strnptr_t *h = builtin_globals; + return h != NULL && mh_strnptr_find_str(h, name, len) != mh_end(h); +} + /* * {{{ box Lua library: common functions */ @@ -970,6 +1027,7 @@ tarantool_lua_init(const char *tarantool_bin, const char *script, int argc, int tarantool_lua_postinit(struct lua_State *L) { + builtin_globals_init(L); /* * loaders.initializing = nil * @@ -1282,6 +1340,7 @@ tarantool_lua_run_script(char *path, struct instance_state *instance, void tarantool_lua_free() { + builtin_globals_free(); builtin_modcache_free(); tarantool_lua_utf8_free(); /* diff --git a/src/lua/init.h b/src/lua/init.h index 28553c732f2c35f3ecaaae66e025fb0e98550ff5..9fdda1c0afe95b14bcd44dba85dc2ad72e010cbd 100644 --- a/src/lua/init.h +++ b/src/lua/init.h @@ -57,6 +57,10 @@ struct instance_state { #define O_EXECUTE 0x8 #define O_HELP_ENV_LIST 0x10 +/** Returns true if the name refers to a built-in global Lua object. */ +bool +tarantool_lua_is_builtin_global(const char *name, uint32_t name_len); + /** * Create tarantool_L and initialize built-in Lua modules. */ diff --git a/test/box-luatest/gh_8803_exec_priv_test.lua b/test/box-luatest/gh_8803_exec_priv_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..3103a6c677aab281d4b89a49edbb42ad7a10b680 --- /dev/null +++ b/test/box-luatest/gh_8803_exec_priv_test.lua @@ -0,0 +1,172 @@ +local net = require('net.box') +local server = require('luatest.server') +local t = require('luatest') + +local g_common = t.group('gh_8803_exec_priv.common', t.helpers.matrix({ + obj_type = {'lua_call', 'lua_eval'}, +})) + +g_common.before_all(function(cg) + cg.server = server:new() + cg.server:start() +end) + +g_common.after_all(function(cg) + cg.server:drop() +end) + +g_common.before_each(function(cg) + cg.server:exec(function() + box.session.su('admin', box.schema.user.create, 'test') + end) +end) + +g_common.after_each(function(cg) + cg.server:exec(function() + box.session.su('admin', box.schema.user.drop, 'test') + end) +end) + +-- Checks the error raised on grant of unsupported privileges. +g_common.test_unsupported_privs = function(cg) + cg.server:exec(function(obj_type) + local unsupported_privs = { + 'read', 'write', 'session', 'create', 'drop', 'alter', + 'reference', 'trigger', 'insert', 'udpate', 'delete', + } + for _, priv in ipairs(unsupported_privs) do + t.assert_error_msg_equals( + string.format("Unsupported %s privilege '%s'", obj_type, priv), + box.schema.user.grant, 'test', priv, obj_type) + end + end, {cg.params.obj_type}) +end + +-- Checks that global execute access may be granted only by admin. +g_common.test_grant = function(cg) + cg.server:exec(function(obj_type) + box.session.su('admin', box.schema.user.grant, 'test', 'super') + t.assert_error_msg_equals( + string.format("Grant access to %s '' is denied for user 'test'", + obj_type), + box.session.su, 'test', box.schema.user.grant, 'test', 'execute', + obj_type) + end, {cg.params.obj_type}) +end + +local g = t.group('gh_8803_exec_priv') + +g.before_all(function(cg) + cg.server = server:new() + cg.server:start() + cg.grant = function(...) + cg.server:exec(function(...) + box.session.su('admin', box.schema.user.grant, ...) + end, {...}) + end + cg.revoke = function(...) + cg.server:exec(function(...) + box.session.su('admin', box.schema.user.revoke, ...) + end, {...}) + end + cg.server:exec(function() + rawset(_G, 'lua_func', function() return true end) + end) +end) + +g.after_all(function(cg) + cg.server:drop() +end) + +g.before_each(function(cg) + cg.server:exec(function() + box.session.su('admin', box.schema.user.create, 'test', + {password = 'secret'}) + end) + cg.conn = net.connect(cg.server.net_box_uri, { + user = 'test', password = 'secret', + }) +end) + +g.after_each(function(cg) + cg.conn:close() + cg.server:exec(function() + box.session.su('admin', box.schema.user.drop, 'test') + end) +end) + +-- Checks that execute privilege granted on lua_eval grants access to +-- evaluating an arbitrary Lua expression. +g.test_lua_eval = function(cg) + local c = cg.conn + local expr = 'return true' + local errmsg = "Execute access to universe '' is denied for user 'test'" + t.assert_error_msg_equals(errmsg, c.eval, c, expr) + cg.grant('test', 'execute', 'lua_eval') + t.assert(pcall(c.eval, c, expr)) + cg.revoke('test', 'usage', 'universe') + t.assert_error_msg_equals(errmsg, c.eval, c, expr) + cg.grant('test', 'usage', 'lua_eval') + t.assert(pcall(c.eval, c, expr)) +end + +-- Checks that execute privilege granted on lua_call grants access to +-- any global user-defined Lua function. +g.test_lua_call = function(cg) + local c = cg.conn + local errmsg = "Execute access to function 'lua_func' is denied " .. + "for user 'test'" + t.assert_error_msg_equals(errmsg, c.call, c, 'lua_func') + cg.grant('test', 'execute', 'lua_call') + t.assert(pcall(c.call, c, 'lua_func')) + cg.revoke('test', 'usage', 'universe') + t.assert_error_msg_equals(errmsg, c.call, c, 'lua_func') + cg.grant('test', 'usage', 'lua_call') + t.assert(pcall(c.call, c, 'lua_func')) +end + +g.before_test('test_lua_call_func', function(cg) + cg.server:exec(function() + box.schema.func.create('c_func', {language = 'C'}) + box.schema.func.create('lua_func', {language = 'LUA'}) + box.schema.func.create('stored_lua_func', { + language = 'LUA', + body = [[function() return true end]], + }) + end) +end) + +g.after_test('test_lua_call_func', function(cg) + cg.server:exec(function() + box.schema.func.drop('c_func') + box.schema.func.drop('lua_func') + box.schema.func.drop('stored_lua_func') + end) +end) + +-- Checks that execute privilege granted on lua_call does not grant access to +-- Lua functions from _func. +g.test_lua_call_func = function(cg) + local c = cg.conn + local errfmt = "Execute access to function '%s' is denied for user 'test'" + local func_list = {'c_func', 'lua_func', 'stored_lua_func'} + cg.grant('test', 'execute', 'lua_call') + for _, func in ipairs(func_list) do + t.assert_error_msg_equals(errfmt:format(func), c.call, c, func) + end +end + +-- Checks that execute privilege granted on lua_call does not grant access +-- to built-in Lua functions. +g.test_lua_call_builtin = function(cg) + local c = cg.conn + local errfmt = "Execute access to function '%s' is denied for user 'test'" + local func_list = { + 'load', 'loadstring', 'loadfile', 'rawset', 'pcall', + 'box.cfg', 'box["cfg"]', 'box.session.su', 'box:error', + } + cg.grant('test', 'execute', 'lua_call') + for _, func in ipairs(func_list) do + t.assert_error_msg_equals(errfmt:format(func), c.call, c, func) + end +end