Commit Diff


commit - db27af79da9866f0e65965671e4750f9c0cd7f38
commit + 38c6b0d38254d5ebde82cd469366955e808798b0
blob - /dev/null
blob + dab95a2ad74ab188c7901b2882f690bca61d0d64 (mode 644)
--- /dev/null
+++ changelogs/unreleased/gh-10310-runtime-grant-lua-call-via-config.md
@@ -0,0 +1,5 @@
+## 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
@@ -44,29 +44,30 @@ lua_source(lua_sources lua/iproto.lua iproto_lua)
 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
@@ -0,0 +1,125 @@
+-- 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
@@ -146,6 +146,7 @@ function methods._initialize(self)
     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
@@ -151,6 +151,7 @@ extern char session_lua[],
 	/* {{{ 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[],
@@ -426,6 +427,10 @@ static const char *lua_sources[] = {
 	"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
@@ -0,0 +1,203 @@
+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