From 1cb0e5c12ee1a6fb54d612dc7f6495af2ace35cf Mon Sep 17 00:00:00 2001 From: Dmitry Rodionov <d.rodionov@picodata.io> Date: Wed, 25 Oct 2023 17:17:12 +0300 Subject: [PATCH] feat: export box_access_check_ddl NO_DOC=picodata internal patch NO_CHANGELOG=picodata internal patch NO_TEST=picodata internal patch --- extra/exports | 1 + src/box/alter.cc | 2 +- src/box/alter.h | 9 + src/box/box.cc | 14 + src/box/box.h | 35 ++ test/app-luatest/module_api_luatest_test.lua | 316 +++++++++++++++++++ 6 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 test/app-luatest/module_api_luatest_test.lua diff --git a/extra/exports b/extra/exports index 3cccb09ac0..adf813b09f 100644 --- a/extra/exports +++ b/extra/exports @@ -12,6 +12,7 @@ base64_bufsize base64_decode base64_encode +box_access_check_ddl box_access_check_space box_auth_data_prepare box_dd_version_id diff --git a/src/box/alter.cc b/src/box/alter.cc index be8ab2f7f5..a6e9b33579 100644 --- a/src/box/alter.cc +++ b/src/box/alter.cc @@ -69,7 +69,7 @@ box_schema_version_bump(void) box_broadcast_schema(); } -static int +int access_check_ddl(const char *name, uint32_t object_id, uint32_t owner_uid, enum schema_object_type type, enum box_privilege_type priv_type) diff --git a/src/box/alter.h b/src/box/alter.h index 592a91fe9d..6f1d862337 100644 --- a/src/box/alter.h +++ b/src/box/alter.h @@ -31,6 +31,7 @@ * SUCH DAMAGE. */ #include "trigger.h" +#include "user_def.h" extern struct trigger alter_space_on_replace_space; extern struct trigger alter_space_on_replace_index; @@ -47,4 +48,12 @@ extern struct trigger on_replace_space_sequence; extern struct trigger on_replace_trigger; extern struct trigger on_replace_func_index; +/** + * Check access for execution of a ddl operation + */ +int +access_check_ddl(const char *name, uint32_t object_id, uint32_t owner_uid, + enum schema_object_type type, + enum box_privilege_type priv_type); + #endif /* INCLUDES_TARANTOOL_BOX_ALTER_H */ diff --git a/src/box/box.cc b/src/box/box.cc index e22e664680..2524e3ac1f 100644 --- a/src/box/box.cc +++ b/src/box/box.cc @@ -97,6 +97,7 @@ #include "small/static.h" #include "sqlLimit.h" #include "tt_sort.h" +#include "alter.h" static char status[64] = "unconfigured"; @@ -3658,6 +3659,19 @@ box_access_check_space(uint32_t space_id, uint16_t access) return access_check_space(space, access); } +API_EXPORT int +box_access_check_ddl( + const char *name, uint32_t object_id, uint32_t owner_uid, + uint32_t object_type, uint16_t access) +{ + return access_check_ddl( + name, + object_id, + owner_uid, + (enum schema_object_type)object_type, + (enum box_privilege_type)access); +} + static inline void box_register_replica(uint32_t id, const struct tt_uuid *uuid) { diff --git a/src/box/box.h b/src/box/box.h index eff8aa3ee3..6f5c04f4fd 100644 --- a/src/box/box.h +++ b/src/box/box.h @@ -715,6 +715,41 @@ box_user_id_by_name(const char *name, const char *name_end, uint32_t *uid); API_EXPORT int box_access_check_space(uint32_t space_id, uint16_t access); +/** + * Run ddl access check for the current user. + * The function checks whole object type permission first: + * i e read arbitrary spaces. Then in case the check didn't succeed + * individual object permissions are checked. + * + * \param name name of the object, used to format the error message + * \param object_id id of the object user tries to access. + * + * Note: In case you need to check create privilege you still need + * to pass object id, additionally you need to ensure that this + * object doesn't exist yet. + * + * \param owner_id uid of the owning user. If you're checking for grant + * privilege this must be the user who grants the privilege (grantor) + * \param object_type type of the object, for values see + * box_schema_object_type enum + * \param access type of an access, for values see box_privilege_type enum + * + * Note: Be careful, some permissions can't be checked directly. + * For example execute can't be checked on a role, because permissions given + * girectly to users are merged with permissions given to roles the + * user has execute access to. + * + * Note: Not all combinations of parameters are valid. Be careful, ENTITY_* + * object types can only be used with grant or revoke. Otherwise this leads + * to undefined behavior. + * + * \retval -1 on error (check box_error_last()) + * \retval 0 on success + */ +API_EXPORT int +box_access_check_ddl(const char *name, uint32_t object_id, uint32_t owner_uid, + uint32_t object_type, uint16_t access); + /** * Sends a packet with the given header and body over the IPROTO session's * socket. diff --git a/test/app-luatest/module_api_luatest_test.lua b/test/app-luatest/module_api_luatest_test.lua new file mode 100644 index 0000000000..80e36ea53a --- /dev/null +++ b/test/app-luatest/module_api_luatest_test.lua @@ -0,0 +1,316 @@ +local t = require('luatest') +local server = require('luatest.server') + +local g = t.group() + +g.before_all(function() + g.server = server:new() + g.server:start() + + g.user_name = "test_box_access_check_ddl_user" + g.another_user_name = "test_box_access_check_ddl_user2" + g.space_name = "test_box_access_check_ddl_space" + g.role_name = "test_box_access_check_ddl_test_role" + g.server:exec(function(user_name, space_name, role_name, another_user_name) + box.schema.user.create(user_name, {password = 'foobar'}) + box.schema.user.create(another_user_name, {password = 'foobar'}) + box.schema.space.create(space_name) + box.schema.role.create(role_name) + local lua_code = [[function(a, b) return a + b end]] + box.schema.func.create('sum', {body = lua_code}) + end, {g.user_name, g.space_name, g.role_name, g.another_user_name}) +end) + +g.after_all(function() + g.server:exec(function(user_name) + box.schema.user.drop(user_name) + end, {g.user_name}) +end) + +g.test_box_access_check_ddl = function() + g.server:exec(function(user_name, space_name, role_name, another_user_name) + local function test_access( + user_name, object_name, object_id, object_type, + access, expected_ret, expected_msg + ) + box.error.clear() + + box.session.su(user_name, function() + local ffi = require('ffi') + local r = ffi.C.box_access_check_ddl( + object_name, + object_id, + 1 --[[admin]], + object_type, + access) + local e = tostring(box.error.last()) + t.assert_equals(r, expected_ret, "Error: " .. e) + if expected_msg ~= nil then + t.assert_str_matches(e, expected_msg) + end + end) + end + + local function expected_msg(priv, obj) + return priv .. " access to " .. obj .. ".+ is denied for user .+" + end + local ffi = require('ffi') + ffi.cdef([[ + int box_access_check_ddl( + const char *name, uint32_t object_id, uint32_t owner_uid, + uint32_t object_type, uint16_t access); + ]]) + + local priv_to_name = {} + -- permissions granted on all entities of type globally + priv_to_name[box.priv.C] = "Create" + priv_to_name[box.priv.D] = "Drop" + -- permissions granted globally or on particular entity + priv_to_name[box.priv.R] = "Read" + priv_to_name[box.priv.W] = "Write" + priv_to_name[box.priv.A] = "Alter" + priv_to_name[box.priv.X] = "Execute" + + -- space CREATE (global permission, without particular entity) + test_access( + user_name, + "space_to_be_created", + 42, + 2, -- BOX_SC_SPACE + box.priv.C, + -1, + expected_msg(priv_to_name[box.priv.C], 'space') + ) + + box.session.su('admin', function() + box.schema.user.grant( + user_name, + string.lower(priv_to_name[box.priv.C]), + "space") + end) + + test_access( + user_name, + "space_to_be_created", + 42, + 2, -- BOX_SC_SPACE + box.priv.C, + 0, + nil + ) + + -- space (particular entity) read write alter drop + for _, priv in ipairs( + {box.priv.R, box.priv.W, box.priv.A, box.priv.D} + ) do + test_access( + user_name, + space_name, + box.space[space_name].id, + 2, -- BOX_SC_SPACE + priv, + -1, + expected_msg(priv_to_name[priv], 'space') + ) + + box.session.su('admin', function() + box.schema.user.grant( + user_name, + string.lower(priv_to_name[priv]), + "space", + box.space[space_name].id) + end) + + test_access( + user_name, + space_name, + box.space[space_name].id, + 2, -- BOX_SC_SPACE + priv, + 0, + nil + ) + end + + -- user - create (global permission, without particular entity) + test_access( + user_name, + "user_to_be_created", + 42, + 4, -- BOX_SC_USER + box.priv.C, + -1, + expected_msg(priv_to_name[box.priv.C], 'user') + ) + + box.session.su('admin', function() + box.schema.user.grant( + user_name, + string.lower(priv_to_name[box.priv.C]), + "user") + end) + + test_access( + user_name, + "user_to_be_created", + 42, + 4, -- BOX_SC_USER + box.priv.C, + 0, + nil + ) + + -- user (particular entity) alter drop + local another_user_id = box.space._user.index.name:select( + {another_user_name} + )[1][1]; + for _, priv in ipairs({box.priv.A, box.priv.D}) do + test_access( + user_name, + another_user_name, + another_user_id, + 4, -- BOX_SC_USER + priv, + -1, + expected_msg(priv_to_name[priv], 'user') + ) + + box.session.su('admin', function() + box.schema.user.grant( + user_name, + string.lower(priv_to_name[priv]), + "user", + another_user_id) + end) + + test_access( + user_name, + another_user_name, + another_user_id, + 4, -- BOX_SC_USER + priv, + 0, + nil + ) + end + + -- role - create (global permission, without particular entity) + test_access( + user_name, + role_name, + 42, -- no entity id + 5, -- BOX_SC_ROLE + box.priv.C, + -1, + expected_msg(priv_to_name[box.priv.C], 'role') + ) + + box.session.su('admin', function() + box.schema.user.grant( + user_name, + string.lower(priv_to_name[box.priv.C]), + "role") + end) + + test_access( + user_name, + role_name, + 0, + 5, -- BOX_SC_ROLE + box.priv.C, + 0, + nil + ) + + -- role - drop (particular entity) + local role_id = box.space._user.index.name:select({role_name})[1][1]; + test_access( + user_name, + role_name, + role_id, + 5, -- BOX_SC_ROLE + box.priv.D, + -1, + expected_msg(priv_to_name[box.priv.D], 'role') + ) + + box.session.su('admin', function() + box.schema.user.grant( + user_name, + string.lower(priv_to_name[box.priv.D]), + "role", + role_id) + end) + + test_access( + user_name, + role_name, + role_id, + 5, -- BOX_SC_ROLE + box.priv.D, + 0, + nil + ) + + -- function - create (global permission, without particular entity) + test_access( + user_name, + role_name, + 0, -- no entity id + 3, -- BOX_SC_FUNCTION + box.priv.C, + -1, + expected_msg(priv_to_name[box.priv.C], 'function') + ) + + box.session.su('admin', function() + box.schema.user.grant( + user_name, + string.lower(priv_to_name[box.priv.C]), + "function") + end) + + test_access( + user_name, + role_name, + 0, + 3, -- BOX_SC_FUNCTION + box.priv.C, + 0, + nil + ) + + -- function - execute, drop (particular entity) + local func_id = box.func.sum.id; + for _, priv in ipairs({box.priv.X, box.priv.D}) do + test_access( + user_name, + 'sum', + func_id, + 3, -- BOX_SC_FUNCTION + priv, + -1, + expected_msg(priv_to_name[priv], "function") + ) + + box.session.su('admin', function() + box.schema.user.grant( + user_name, + string.lower(priv_to_name[priv]), + "function", + "sum") + end) + + test_access( + user_name, + 'sum', + func_id, + 3, -- BOX_SC_FUNCTION + priv, + 0, + nil + ) + end + + end, {g.user_name, g.space_name, g.role_name, g.another_user_name}) +end -- GitLab