commit - db27af79da9866f0e65965671e4750f9c0cd7f38
commit + 38c6b0d38254d5ebde82cd469366955e808798b0
blob - /dev/null
blob + dab95a2ad74ab188c7901b2882f690bca61d0d64 (mode 644)
--- /dev/null
+++ changelogs/unreleased/gh-10310-runtime-grant-lua-call-via-config.md
+## feature/config
+
+* Now users can specify the `lua_call` option to allow calling Lua functions
+ even when the database is in read-only mode or has an outdated schema version
+ (gh-10310).
blob - e2223e194e795aa9b1cff7b4358dfbd374c5751d
blob + 82d5a253eff53279319a5bd97d4a393310ed0c60
--- src/box/CMakeLists.txt
+++ src/box/CMakeLists.txt
lua_source(lua_sources lua/mkversion.lua mkversion_lua)
# {{{ config
-lua_source(lua_sources lua/config/applier/app.lua config_applier_app_lua)
-lua_source(lua_sources lua/config/applier/box_cfg.lua config_applier_box_cfg_lua)
-lua_source(lua_sources lua/config/applier/compat.lua config_applier_compat_lua)
-lua_source(lua_sources lua/config/applier/console.lua config_applier_console_lua)
-lua_source(lua_sources lua/config/applier/credentials.lua config_applier_credentials_lua)
-lua_source(lua_sources lua/config/applier/fiber.lua config_applier_fiber_lua)
-lua_source(lua_sources lua/config/applier/mkdir.lua config_applier_mkdir_lua)
-lua_source(lua_sources lua/config/applier/roles.lua config_applier_roles_lua)
-lua_source(lua_sources lua/config/applier/sharding.lua config_applier_sharding_lua)
-lua_source(lua_sources lua/config/cluster_config.lua config_cluster_config_lua)
-lua_source(lua_sources lua/config/configdata.lua config_configdata_lua)
-lua_source(lua_sources lua/config/init.lua config_init_lua)
-lua_source(lua_sources lua/config/instance_config.lua config_instance_config_lua)
-lua_source(lua_sources lua/config/source/env.lua config_source_env_lua)
-lua_source(lua_sources lua/config/source/file.lua config_source_file_lua)
-lua_source(lua_sources lua/config/utils/aboard.lua config_utils_aboard_lua)
-lua_source(lua_sources lua/config/utils/expression.lua config_utils_expression_lua)
-lua_source(lua_sources lua/config/utils/file.lua config_utils_file_lua)
-lua_source(lua_sources lua/config/utils/log.lua config_utils_log_lua)
-lua_source(lua_sources lua/config/utils/odict.lua config_utils_odict_lua)
-lua_source(lua_sources lua/config/utils/schema.lua config_utils_schema_lua)
-lua_source(lua_sources lua/config/utils/snapshot.lua config_utils_snapshot_lua)
-lua_source(lua_sources lua/config/utils/tabulate.lua config_utils_tabulate_lua)
+lua_source(lua_sources lua/config/applier/app.lua config_applier_app_lua)
+lua_source(lua_sources lua/config/applier/box_cfg.lua config_applier_box_cfg_lua)
+lua_source(lua_sources lua/config/applier/runtime_priv.lua config_applier_runtime_priv_lua)
+lua_source(lua_sources lua/config/applier/compat.lua config_applier_compat_lua)
+lua_source(lua_sources lua/config/applier/console.lua config_applier_console_lua)
+lua_source(lua_sources lua/config/applier/credentials.lua config_applier_credentials_lua)
+lua_source(lua_sources lua/config/applier/fiber.lua config_applier_fiber_lua)
+lua_source(lua_sources lua/config/applier/mkdir.lua config_applier_mkdir_lua)
+lua_source(lua_sources lua/config/applier/roles.lua config_applier_roles_lua)
+lua_source(lua_sources lua/config/applier/sharding.lua config_applier_sharding_lua)
+lua_source(lua_sources lua/config/cluster_config.lua config_cluster_config_lua)
+lua_source(lua_sources lua/config/configdata.lua config_configdata_lua)
+lua_source(lua_sources lua/config/init.lua config_init_lua)
+lua_source(lua_sources lua/config/instance_config.lua config_instance_config_lua)
+lua_source(lua_sources lua/config/source/env.lua config_source_env_lua)
+lua_source(lua_sources lua/config/source/file.lua config_source_file_lua)
+lua_source(lua_sources lua/config/utils/aboard.lua config_utils_aboard_lua)
+lua_source(lua_sources lua/config/utils/expression.lua config_utils_expression_lua)
+lua_source(lua_sources lua/config/utils/file.lua config_utils_file_lua)
+lua_source(lua_sources lua/config/utils/log.lua config_utils_log_lua)
+lua_source(lua_sources lua/config/utils/odict.lua config_utils_odict_lua)
+lua_source(lua_sources lua/config/utils/schema.lua config_utils_schema_lua)
+lua_source(lua_sources lua/config/utils/snapshot.lua config_utils_snapshot_lua)
+lua_source(lua_sources lua/config/utils/tabulate.lua config_utils_tabulate_lua)
if (ENABLE_CONFIG_EXTRAS)
lua_source(lua_sources ${CONFIG_EXTRAS_DIR}/source/etcd.lua config_source_etcd_lua)
blob - /dev/null
blob + a44fab989226a924e123fb524788beedcfc1967e (mode 644)
--- /dev/null
+++ src/box/lua/config/applier/runtime_priv.lua
+-- Extract and add functions from a user or a role definition to
+-- the `{[func_name] = true, <...>}` mapping `res`.
+--
+-- The source is `lua_call` declarations.
+local function add_funcs(res, user_or_role_def)
+ local privileges = user_or_role_def.privileges or {}
+
+ for _, privilege in ipairs(privileges) do
+ local permissions = privilege.permissions or {}
+ local has_execute = false
+ for _, perm in ipairs(permissions) do
+ if perm == 'execute' then
+ has_execute = true
+ break
+ end
+ end
+ if has_execute and privilege.lua_call ~= nil then
+ for _, func_name in ipairs(privilege.lua_call) do
+ res[func_name] = true
+ end
+ end
+ end
+end
+
+-- Extract and add roles for the given user to the
+-- `{[role_name] = true, <...>}` mapping `res`, including
+-- transitively assigned (when a role is assigned to a role).
+local function add_roles(res, user_or_role_def, ctx)
+ for _, role_name in ipairs(user_or_role_def.roles or {}) do
+ -- Detect a recursion.
+ if ctx.visited[role_name] then
+ error(('Recursion detected: credentials.roles.%s depends on ' ..
+ 'itself'):format(role_name), 0)
+ end
+
+ -- Add the role into the result.
+ res[role_name] = true
+
+ -- Add the nested roles.
+ --
+ -- Ignore unknown roles. For example, there is a
+ -- built-in role 'super' that doesn't have to be
+ -- configured.
+ local role_def = ctx.roles[role_name]
+ if role_def ~= nil then
+ ctx.visited[role_name] = true
+ add_roles(res, role_def, ctx)
+ ctx.visited[role_name] = nil
+ end
+ end
+end
+
+-- Extract all the user's functions listed in the `lua_call`
+-- directives in the user definition or its roles assigned
+-- directly or transitively over the other roles.
+local function extract_funcs(user_name, ctx)
+ local user_def = ctx.users[user_name]
+
+ local roles = {}
+ ctx.visited = {}
+ add_roles(roles, user_def, ctx)
+ ctx.visited = nil
+
+ -- Collect a full set of functions for the given user.
+ --
+ -- {
+ -- [func_name] = true,
+ -- <...>,
+ -- }
+ local funcs = {}
+ add_funcs(funcs, user_def)
+ for role_name, _ in pairs(roles) do
+ local role_def = ctx.roles[role_name]
+ if role_def ~= nil then
+ add_funcs(funcs, role_def)
+ end
+ end
+
+ return funcs
+end
+
+local function apply(config_module)
+ -- Prepare a context with the configuration information to
+ -- transform.
+ local configdata = config_module._configdata
+ local ctx = {
+ roles = configdata:get('credentials.roles') or {},
+ users = configdata:get('credentials.users') or {},
+ }
+
+ -- Collect a mapping from users to their granted functions.
+ --
+ -- {
+ -- [user_name] = {
+ -- [func_name] = true,
+ -- <...>,
+ -- },
+ -- <...>
+ -- }
+ local res = {}
+ for user_name, _ in pairs(ctx.users) do
+ local funcs = extract_funcs(user_name, ctx)
+ if next(funcs) ~= nil then
+ res[user_name] = funcs
+ end
+ end
+
+ -- Reset the runtime privileges and grant all the configured
+ -- ones.
+ box.internal.lua_call_runtime_priv_reset()
+ for user_name, funcs in pairs(res) do
+ for func_name, _ in pairs(funcs) do
+ if func_name == 'all' then
+ box.internal.lua_call_runtime_priv_grant(user_name, '')
+ else
+ box.internal.lua_call_runtime_priv_grant(user_name, func_name)
+ end
+ end
+ end
+end
+
+return {
+ name = 'runtime_priv',
+ apply = apply,
+}
blob - 26a2fe4aceadfb66fb1c4a9bcabf6cfd5bd7303a
blob + f356aa943ab9d5d72b51a0c60dd8141d779497c0
--- src/box/lua/config/init.lua
+++ src/box/lua/config/init.lua
self:_register_applier(require('internal.config.applier.compat'))
self:_register_applier(require('internal.config.applier.mkdir'))
self:_register_applier(require('internal.config.applier.console'))
+ self:_register_applier(require('internal.config.applier.runtime_priv'))
self:_register_applier(require('internal.config.applier.box_cfg'))
self:_register_applier(require('internal.config.applier.credentials'))
self:_register_applier(require('internal.config.applier.fiber'))
blob - 5d607b4071a05d426fd6470060ec462e180bcc5c
blob + 386d8db4fa268f1bc9ad99f09304f8abcf122b6f
--- src/box/lua/init.c
+++ src/box/lua/init.c
/* {{{ config */
config_applier_app_lua[],
config_applier_box_cfg_lua[],
+ config_applier_runtime_priv_lua[],
config_applier_compat_lua[],
config_applier_console_lua[],
config_applier_credentials_lua[],
"internal.config.applier.console",
config_applier_console_lua,
+ "config/applier/runtime_priv",
+ "internal.config.applier.runtime_priv",
+ config_applier_runtime_priv_lua,
+
"config/applier/credentials",
"internal.config.applier.credentials",
config_applier_credentials_lua,
blob - /dev/null
blob + 8e300555c2bd868937ef92633b8c7191edce36ef (mode 644)
--- /dev/null
+++ test/config-luatest/runtime_priv_test.lua
+local t = require('luatest')
+local cbuilder = require('luatest.cbuilder')
+local cluster = require('test.config-luatest.cluster')
+
+local g = t.group()
+
+g.before_all(cluster.init)
+g.after_each(cluster.drop)
+g.after_all(cluster.clean)
+
+-- {{{ Testing helpers
+
+local function access_error_msg(user, func)
+ local templ = 'Execute access to function \'%s\' is denied for user \'%s\''
+ return string.format(templ, func, user)
+end
+
+local function define_access_error_msg_function(server)
+ server:exec(function(access_error_msg)
+ rawset(_G, 'access_error_msg', loadstring(access_error_msg))
+ end, {string.dump(access_error_msg)})
+end
+
+local function define_stub_function(server, func_name)
+ server:exec(function(func_name)
+ rawset(_G, func_name, function()
+ return true
+ end)
+ end, {func_name})
+end
+
+local function define_get_connection_function(server)
+ server:exec(function()
+ local netbox = require('net.box')
+ local config = require('config')
+
+ rawset(_G, 'get_connection', function()
+ local uri = config:instance_uri().uri
+ return netbox.connect(uri, {user = 'alice', password = 'ALICE'})
+ end)
+ end)
+end
+
+local function new_cluster_in_ro()
+ local config = cbuilder:new()
+ :add_instance('i-001', {})
+ -- Create a user and set the password while we're in RW.
+ :set_global_option('credentials.users.alice', {
+ password = 'ALICE',
+ })
+ :config()
+ local cluster = cluster.new(g, config)
+ cluster:start()
+
+ define_access_error_msg_function(cluster['i-001'])
+ define_stub_function(cluster['i-001'], 'foo')
+ define_stub_function(cluster['i-001'], 'bar')
+ define_stub_function(cluster['i-001'], 'baz')
+ define_get_connection_function(cluster['i-001'])
+
+ -- We can only bootstrap the initial database in the RW mode.
+ -- Let's reload to RO after it.
+ local config = cbuilder:new(config)
+ :set_global_option('database.mode', 'ro')
+ :config()
+ cluster:reload(config)
+
+ -- Verify that the instance is in the RO mode.
+ cluster['i-001']:exec(function()
+ t.assert_equals(box.info.ro, true)
+ end)
+
+ return config, cluster
+end
+
+local function lua_call_priv(funcs)
+ return {
+ permissions = {'execute'},
+ lua_call = funcs,
+ }
+end
+
+local function inane_lua_call_priv(funcs)
+ return {
+ permissions = {'read'},
+ lua_call = funcs,
+ }
+end
+
+-- }}} Testing helpers
+
+g.test_direct_lua_call_in_ro_mode = function()
+ local config, cluster = new_cluster_in_ro()
+
+ -- Add new function privileges.
+ local config = cbuilder:new(config)
+ :set_global_option('credentials.roles.test', {
+ privileges = {lua_call_priv({'bar'})},
+ })
+ :set_global_option('credentials.roles.deps', {
+ privileges = {lua_call_priv({'box.info'})},
+ roles = {'test'},
+ })
+ :set_global_option('credentials.users.alice', {
+ privileges = {lua_call_priv({'foo'})},
+ roles = {'deps'},
+ password = 'ALICE',
+ })
+ :config()
+ cluster:reload(config)
+
+ -- Verify that the new privileges are granted.
+ cluster['i-001']:exec(function()
+ local conn = _G.get_connection()
+
+ -- Granted by user's privileges.
+ t.assert(conn:call('foo'))
+
+ -- Granted by a directly assigned role.
+ t.assert(conn:call('box.info'))
+
+ -- Granted by an indirectly assigned role.
+ t.assert(conn:call('bar'))
+
+ -- Any other function is forbidden.
+ local exp_err = _G.access_error_msg('alice', 'baz')
+ t.assert_error_msg_equals(exp_err, function()
+ conn:call('baz')
+ end)
+ end)
+end
+
+g.test_universe_lua_call_in_ro_mode = function()
+ local config, cluster = new_cluster_in_ro()
+
+ -- Add new function privileges.
+ local config = cbuilder:new(config)
+ :set_global_option('credentials.roles.test', {
+ privileges = {lua_call_priv({'all', 'box.info'})},
+ })
+ :set_global_option('credentials.roles.no_exec', {
+ privileges = {inane_lua_call_priv({'loadstring'})},
+ })
+ :set_global_option('credentials.users.alice', {
+ privileges = {lua_call_priv({'box.info'})},
+ roles = {'test', 'exec'},
+ password = 'ALICE',
+ })
+ :config()
+ cluster:reload(config)
+
+ cluster['i-001']:exec(function()
+ local conn = _G.get_connection()
+
+ -- Verify that user `alice` able to call any global lua function.
+ t.assert(conn:call('foo'))
+
+ -- Verify that user `alice` able to call granted built-in function.
+ t.assert(conn:call('box.info'))
+
+ -- User `alice` is unable to use the loadstring function because the
+ -- `no_exec` role hasn't `execute` permissions.
+ local exp_err = _G.access_error_msg('alice', 'loadstring')
+ t.assert_error_msg_equals(exp_err, function()
+ conn:call('loadstring')
+ end)
+ end)
+end
+
+g.test_no_user_privileges_lua_call_ro_mode = function()
+ -- This test case checks the scenario where the user 'alice' has no direct
+ -- privileges, but inherits them from a role `test` that grants the lua_call
+ -- privileges.
+ local config, cluster = new_cluster_in_ro()
+
+ -- Add new function privileges.
+ local config = cbuilder:new(config)
+ :set_global_option('credentials.roles.test', {
+ privileges = {lua_call_priv({'box.info', 'foo'})},
+ })
+ :set_global_option('credentials.users.alice', {
+ roles = {'test'},
+ password = 'ALICE',
+ })
+ :config()
+ cluster:reload(config)
+
+ cluster['i-001']:exec(function()
+ local conn = _G.get_connection()
+
+ -- Verify that user `alice` able to call `foo` function.
+ t.assert(conn:call('foo'))
+
+ -- Verify that user `alice` able to call `box.info` built-in function.
+ t.assert(conn:call('box.info'))
+
+ -- Any other function is forbidden.
+ local exp_err = _G.access_error_msg('alice', 'baz')
+ t.assert_error_msg_equals(exp_err, function()
+ conn:call('baz')
+ end)
+ end)
+end