From af90ecc891762338504e51b4e66c6730a5ea9aa0 Mon Sep 17 00:00:00 2001
From: Dmitry Rodionov <d.rodionov@picodata.io>
Date: Tue, 12 Sep 2023 14:45:45 +0300
Subject: [PATCH] feat: export box_access_check_space

NO_DOC=picodata internal patch
NO_CHANGELOG=picodata internal patch
NO_TEST=picodata internal patch

Co-authored-by: Yaroslav Dynnikov <yaroslav.dynnikov@gmail.com>
---
 extra/exports                    |  1 +
 src/box/box.cc                   |  9 ++++
 src/box/box.h                    | 15 ++++++
 src/box/space.h                  |  4 ++
 test/app-tap/module_api.test.lua | 85 +++++++++++++++++++++++++++++++-
 5 files changed, 113 insertions(+), 1 deletion(-)

diff --git a/extra/exports b/extra/exports
index c164164f28..c8f814b33a 100644
--- a/extra/exports
+++ b/extra/exports
@@ -12,6 +12,7 @@
 base64_bufsize
 base64_decode
 base64_encode
+box_access_check_space
 box_auth_data_prepare
 box_dd_version_id
 box_decimal_abs
diff --git a/src/box/box.cc b/src/box/box.cc
index 9aea8367ae..1b0541c88f 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -3649,6 +3649,15 @@ box_iproto_override(uint32_t req_type, iproto_handler_t handler,
 	return iproto_override(req_type, handler, destroy, ctx);
 }
 
+API_EXPORT int
+box_access_check_space(uint32_t space_id, uint16_t access)
+{
+	struct space *space = space_cache_find(space_id);
+	if (space == NULL)
+		return -1;
+	return access_check_space(space, 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 1df8996cdc..b52a387fb9 100644
--- a/src/box/box.h
+++ b/src/box/box.h
@@ -700,6 +700,21 @@ box_effective_user_id(void);
 API_EXPORT int
 box_user_id_by_name(const char *name, const char *name_end, uint32_t *uid);
 
+/**
+ * Run access check for the current user against
+ * specified space and access type.
+ * While it is possible to pass bitmask in access
+ * parameter this function is intended to be used
+ * with only one permission at a time.
+ * Most relevant access types are read and write.
+ * \param space_id space id
+ * \param access type of access. See valid options in priv_type enum.
+ * \retval -1 on error (check box_error_last())
+ * \retval 0 on success
+ */
+API_EXPORT int
+box_access_check_space(uint32_t space_id, uint16_t access);
+
 /**
  * Sends a packet with the given header and body over the IPROTO session's
  * socket.
diff --git a/src/box/space.h b/src/box/space.h
index 2fbea44f4b..93b197edac 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -434,6 +434,10 @@ index_name_by_id(struct space *space, uint32_t id);
 /**
  * Check whether or not the current user can be granted
  * the requested access to the space.
+ * @param space Space to run access check against
+ * @param access Requested access
+ * @retval 0 on success when access is granted
+ * @retval -1 on error (check box_error_last())
  */
 int
 access_check_space(struct space *space, user_access_t access);
diff --git a/test/app-tap/module_api.test.lua b/test/app-tap/module_api.test.lua
index 1788357c5b..29e28a765c 100755
--- a/test/app-tap/module_api.test.lua
+++ b/test/app-tap/module_api.test.lua
@@ -663,8 +663,90 @@ local function test_box_iproto_override(test, module)
     module.box_iproto_override_reset(box.iproto.type.PING)
 end
 
+local function test_box_access_check_space(test)
+    test:plan(9)
+
+    local user_name = "box_access_check_space_test_user"
+    box.schema.user.create(user_name, {password = 'foobar'})
+
+    local space_name = "test_box_access_check_space_ffi"
+    box.schema.space.create(space_name)
+
+    ffi = require('ffi')
+    ffi.cdef('int box_access_check_space(uint32_t space_id, uint16_t access);')
+    local function test_access_check(test, access, expected_ret, expected_msg)
+        box.error.clear()
+        local r = ffi.C.box_access_check_space(box.space[space_name].id, access)
+        local e = tostring(box.error.last())
+        test:plan(2)
+        test:is(r, expected_ret, "return value");
+        test:like(e, expected_msg, "error message");
+    end
+
+    box.session.su(user_name, function()
+        test:test("no access - read fails",
+            test_access_check, box.priv.R,
+            -1, "Read access to space .+ is denied for user .+"
+        )
+
+        test:test("no access - write fails",
+            test_access_check, box.priv.W,
+            -1, "Write access to space .+ is denied for user .+"
+        )
+    end)
+
+    box.schema.user.grant(user_name, 'read', 'space', space_name)
+
+    box.session.su(user_name, function()
+        test:test("grant read - read ok",
+            test_access_check, box.priv.R,
+            0, "nil"
+        )
+
+        test:test("grant read - write fails",
+            test_access_check, box.priv.W,
+            -1, "Write access to space .+ is denied for user .+"
+        )
+    end)
+
+    box.schema.user.grant(user_name, 'write', 'space', space_name)
+
+    box.session.su(user_name, function()
+        test:test("grant rw - read ok",
+            test_access_check, box.priv.R,
+            0, "nil"
+        )
+
+        test:test("grant rw - write ok",
+            test_access_check, box.priv.W,
+            0, "nil"
+        )
+
+        -- Execute accesses can't be granted to a space. This case is
+        -- artificial, but access check still fails.
+        test:test("grant rw - execute access check fails",
+            test_access_check, box.priv.X,
+            -1, "Execute access to space .+ is denied for user .+"
+        )
+    end)
+
+    box.schema.user.revoke(user_name, 'read', 'space', space_name)
+
+    box.session.su(user_name, function()
+        test:test("grant write - read fails",
+            test_access_check, box.priv.R,
+            -1, "Read access to space .+ is denied for user .+"
+        )
+
+        test:test("grant write - write ok",
+            test_access_check, box.priv.W,
+            0, "nil"
+        )
+    end)
+end
+
 require('tap').test("module_api", function(test)
-    test:plan(56)
+    test:plan(57)
     local status, module = pcall(require, 'module_api')
     test:is(status, true, "module")
     test:ok(status, "module is loaded")
@@ -701,6 +783,7 @@ require('tap').test("module_api", function(test)
     test:test("box_session_id_matches", test_box_session_id_matches, module)
     test:test("box_iproto_send", test_box_iproto_send, module)
     test:test("box_iproto_override", test_box_iproto_override, module)
+    test:test("box_access_check_space", test_box_access_check_space)
 
     space:drop()
 end)
-- 
GitLab