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