diff --git a/docs/en/sql-reference/statements/check-grant.md b/docs/en/sql-reference/statements/check-grant.md
new file mode 100644
index 00000000000..a1971eb59d1
--- /dev/null
+++ b/docs/en/sql-reference/statements/check-grant.md
@@ -0,0 +1,43 @@
+---
+slug: /en/sql-reference/statements/check-grant
+sidebar_position: 56
+sidebar_label: CHECK GRANT
+title: "CHECK GRANT Statement"
+---
+
+The `CHECK GRANT` query is used to check whether the current user/role has been granted a specific privilege, and whether the corresponding table/column exists in the memory.
+
+## Syntax
+
+The basic syntax of the query is as follows:
+
+```sql
+CHECK GRANT privilege[(column_name [,...])] [,...] ON {db.table|db.*|*.*|table|*}
+```
+
+- `privilege` — Type of privilege.
+
+## Examples
+
+If the user used to be granted the privilege, or the role (which is granted with the privilege), and the db.table(column) exists on this node, the response`check_grant` will be `1`. Otherwise, the response `check_grant` will be `0`.
+
+If `table_1.col1` exists and current user is granted by privilege `SELECT`/`SELECT(con)` or role(with privilege), the response is `1`.
+```sql
+CHECK GRANT SELECT(col1) ON table_1;
+```
+
+```text
+┌─CHECK_GRANT─┐
+│ 1 │
+└─────────────┘
+```
+If `table_2.col2` doesn't exists, or current user is not granted by privilege `SELECT`/`SELECT(con)` or role(with privilege), the response is `0`.
+```sql
+CHECK GRANT SELECT(col2) ON table_2;
+```
+
+```text
+┌─CHECK_GRANT─┐
+│ 0 │
+└─────────────┘
+```
diff --git a/docs/zh/sql-reference/statements/check-grant.mdx b/docs/zh/sql-reference/statements/check-grant.mdx
new file mode 100644
index 00000000000..60c95699a5e
--- /dev/null
+++ b/docs/zh/sql-reference/statements/check-grant.mdx
@@ -0,0 +1,10 @@
+---
+slug: /zh/sql-reference/statements/check-grant
+sidebar_position: 56
+sidebar_label: CHECK
+title: "CHECK GRANT Statement"
+---
+
+import Content from '@site/docs/en/sql-reference/statements/check-grant.md';
+
+
diff --git a/src/Interpreters/Access/InterpreterCheckGrantQuery.cpp b/src/Interpreters/Access/InterpreterCheckGrantQuery.cpp
new file mode 100644
index 00000000000..7a01380e343
--- /dev/null
+++ b/src/Interpreters/Access/InterpreterCheckGrantQuery.cpp
@@ -0,0 +1,114 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "Databases/IDatabase.h"
+#include "Storages/IStorage.h"
+
+namespace DB
+{
+
+namespace
+{
+ /// Extracts access rights elements which are going to check grant
+ void collectAccessRightsElementsToGrantOrRevoke(
+ const ASTCheckGrantQuery & query,
+ AccessRightsElements & elements_to_check_grant)
+ {
+ elements_to_check_grant.clear();
+ /// GRANT
+ elements_to_check_grant = query.access_rights_elements;
+ }
+}
+
+
+BlockIO InterpreterCheckGrantQuery::execute()
+{
+ auto & query = query_ptr->as();
+ query.access_rights_elements.eraseNonGrantable();
+
+ auto current_user_access = getContext()->getAccess();
+
+ /// Collect access rights elements which will be checked.
+ AccessRightsElements elements_to_check_grant;
+ collectAccessRightsElementsToGrantOrRevoke(query, elements_to_check_grant);
+
+
+ /// Replacing empty database with the default. This step must be done before replication to avoid privilege escalation.
+ String current_database = getContext()->getCurrentDatabase();
+ elements_to_check_grant.replaceEmptyDatabase(current_database);
+ query.access_rights_elements.replaceEmptyDatabase(current_database);
+ auto *logger = &::Poco::Logger::get("CheckGrantQuery");
+
+ /// Check If Table/Columns exist.
+ for (const auto & elem : elements_to_check_grant)
+ {
+ try
+ {
+ DatabasePtr database;
+ database = DatabaseCatalog::instance().getDatabase(elem.database);
+ if (!database->isTableExist(elem.table, getContext()))
+ {
+ /// Table not found.
+ return executeQuery("SELECT 0 AS CHECK_GRANT", getContext(), QueryFlags{.internal = true}).second;
+ }
+ auto table = database->getTable(elem.table, getContext());
+
+ auto column_name_with_sizes = table->getColumnSizes();
+ for (const auto & elem_col : elem.columns)
+ {
+ bool founded = false;
+ for (const auto & col_in_table : column_name_with_sizes)
+ {
+ if (col_in_table.first == elem_col)
+ {
+ founded = true;
+ break;
+ }
+ }
+ if (!founded)
+ {
+ /// Column not found.
+ return executeQuery("SELECT 0 AS CHECK_GRANT", getContext(), QueryFlags{.internal = true}).second;
+ }
+ }
+ }
+ catch (...)
+ {
+ tryLogCurrentException(logger);
+ return executeQuery("SELECT 0 AS CHECK_GRANT", getContext(), QueryFlags{.internal = true}).second;
+ }
+ }
+ bool user_is_granted = current_user_access->isGranted(elements_to_check_grant);
+ if (!user_is_granted)
+ {
+ return executeQuery("SELECT 0 AS CHECK_GRANT", getContext(), QueryFlags{.internal = true}).second;
+ }
+
+ return executeQuery("SELECT 1 AS CHECK_GRANT", getContext(), QueryFlags{.internal = true}).second;
+}
+
+void registerInterpreterCheckGrantQuery(InterpreterFactory & factory)
+{
+ auto create_fn = [] (const InterpreterFactory::Arguments & args)
+ {
+ return std::make_unique(args.query, args.context);
+ };
+ factory.registerInterpreter("InterpreterCheckGrantQuery", create_fn);
+}
+
+}
diff --git a/src/Interpreters/Access/InterpreterCheckGrantQuery.h b/src/Interpreters/Access/InterpreterCheckGrantQuery.h
new file mode 100644
index 00000000000..e79d6925c93
--- /dev/null
+++ b/src/Interpreters/Access/InterpreterCheckGrantQuery.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include
+#include
+#include
+
+
+namespace DB
+{
+
+class ASTCheckGrantQuery;
+struct User;
+struct Role;
+
+class InterpreterCheckGrantQuery : public IInterpreter, WithMutableContext
+{
+public:
+ InterpreterCheckGrantQuery(const ASTPtr & query_ptr_, ContextMutablePtr context_) : WithMutableContext(context_), query_ptr(query_ptr_) {}
+
+ BlockIO execute() override;
+
+private:
+ ASTPtr query_ptr;
+};
+
+}
diff --git a/src/Interpreters/InterpreterFactory.cpp b/src/Interpreters/InterpreterFactory.cpp
index 12b3b510098..cae43d98560 100644
--- a/src/Interpreters/InterpreterFactory.cpp
+++ b/src/Interpreters/InterpreterFactory.cpp
@@ -40,6 +40,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -298,6 +299,10 @@ InterpreterFactory::InterpreterPtr InterpreterFactory::get(ASTPtr & query, Conte
{
interpreter_name = "InterpreterShowGrantsQuery";
}
+ else if (query->as())
+ {
+ interpreter_name = "InterpreterCheckGrantQuery";
+ }
else if (query->as())
{
interpreter_name = "InterpreterShowAccessEntitiesQuery";
diff --git a/src/Interpreters/registerInterpreters.cpp b/src/Interpreters/registerInterpreters.cpp
index 481d0597a85..7d8ed17a11f 100644
--- a/src/Interpreters/registerInterpreters.cpp
+++ b/src/Interpreters/registerInterpreters.cpp
@@ -45,6 +45,7 @@ void registerInterpreterDropNamedCollectionQuery(InterpreterFactory & factory);
void registerInterpreterGrantQuery(InterpreterFactory & factory);
void registerInterpreterShowCreateAccessEntityQuery(InterpreterFactory & factory);
void registerInterpreterShowGrantsQuery(InterpreterFactory & factory);
+void registerInterpreterCheckGrantQuery(InterpreterFactory & factory);
void registerInterpreterShowAccessEntitiesQuery(InterpreterFactory & factory);
void registerInterpreterShowAccessQuery(InterpreterFactory & factory);
void registerInterpreterShowPrivilegesQuery(InterpreterFactory & factory);
@@ -104,6 +105,7 @@ void registerInterpreters()
registerInterpreterGrantQuery(factory);
registerInterpreterShowCreateAccessEntityQuery(factory);
registerInterpreterShowGrantsQuery(factory);
+ registerInterpreterCheckGrantQuery(factory);
registerInterpreterShowAccessEntitiesQuery(factory);
registerInterpreterShowAccessQuery(factory);
registerInterpreterShowPrivilegesQuery(factory);
diff --git a/src/Parsers/Access/ASTCheckGrantQuery.cpp b/src/Parsers/Access/ASTCheckGrantQuery.cpp
new file mode 100644
index 00000000000..0f23fcce48a
--- /dev/null
+++ b/src/Parsers/Access/ASTCheckGrantQuery.cpp
@@ -0,0 +1,126 @@
+#include
+#include
+#include
+#include
+
+
+namespace DB
+{
+
+namespace
+{
+ void formatColumnNames(const Strings & columns, const IAST::FormatSettings & settings)
+ {
+ settings.ostr << "(";
+ bool need_comma = false;
+ for (const auto & column : columns)
+ {
+ if (std::exchange(need_comma, true))
+ settings.ostr << ", ";
+ settings.ostr << backQuoteIfNeed(column);
+ }
+ settings.ostr << ")";
+ }
+
+
+ void formatONClause(const AccessRightsElement & element, const IAST::FormatSettings & settings)
+ {
+ settings.ostr << (settings.hilite ? IAST::hilite_keyword : "") << "ON " << (settings.hilite ? IAST::hilite_none : "");
+ if (element.isGlobalWithParameter())
+ {
+ if (element.any_parameter)
+ settings.ostr << "*";
+ else
+ settings.ostr << backQuoteIfNeed(element.parameter);
+ }
+ else if (element.any_database)
+ {
+ settings.ostr << "*.*";
+ }
+ else
+ {
+ if (!element.database.empty())
+ settings.ostr << backQuoteIfNeed(element.database) << ".";
+ if (element.any_table)
+ settings.ostr << "*";
+ else
+ settings.ostr << backQuoteIfNeed(element.table);
+ }
+ }
+
+
+ void formatElementsWithoutOptions(const AccessRightsElements & elements, const IAST::FormatSettings & settings)
+ {
+ bool no_output = true;
+ for (size_t i = 0; i != elements.size(); ++i)
+ {
+ const auto & element = elements[i];
+ auto keywords = element.access_flags.toKeywords();
+ if (keywords.empty() || (!element.any_column && element.columns.empty()))
+ continue;
+
+ for (const auto & keyword : keywords)
+ {
+ if (!std::exchange(no_output, false))
+ settings.ostr << ", ";
+
+ settings.ostr << (settings.hilite ? IAST::hilite_keyword : "") << keyword << (settings.hilite ? IAST::hilite_none : "");
+ if (!element.any_column)
+ formatColumnNames(element.columns, settings);
+ }
+
+ bool next_element_on_same_db_and_table = false;
+ if (i != elements.size() - 1)
+ {
+ const auto & next_element = elements[i + 1];
+ if (element.sameDatabaseAndTableAndParameter(next_element))
+ {
+ next_element_on_same_db_and_table = true;
+ }
+ }
+
+ if (!next_element_on_same_db_and_table)
+ {
+ settings.ostr << " ";
+ formatONClause(element, settings);
+ }
+ }
+
+ if (no_output)
+ settings.ostr << (settings.hilite ? IAST::hilite_keyword : "") << "USAGE ON " << (settings.hilite ? IAST::hilite_none : "") << "*.*";
+ }
+
+}
+
+
+String ASTCheckGrantQuery::getID(char) const
+{
+ return "CheckGrantQuery";
+}
+
+
+ASTPtr ASTCheckGrantQuery::clone() const
+{
+ auto res = std::make_shared(*this);
+
+ return res;
+}
+
+
+void ASTCheckGrantQuery::formatImpl(const FormatSettings & settings, FormatState &, FormatStateStacked) const
+{
+ settings.ostr << (settings.hilite ? IAST::hilite_keyword : "") << "CHECK GRANT"
+ << (settings.hilite ? IAST::hilite_none : "");
+
+ settings.ostr << " ";
+
+ formatElementsWithoutOptions(access_rights_elements, settings);
+}
+
+
+void ASTCheckGrantQuery::replaceEmptyDatabase(const String & current_database)
+{
+ access_rights_elements.replaceEmptyDatabase(current_database);
+}
+
+}
diff --git a/src/Parsers/Access/ASTCheckGrantQuery.h b/src/Parsers/Access/ASTCheckGrantQuery.h
new file mode 100644
index 00000000000..567ffdb289a
--- /dev/null
+++ b/src/Parsers/Access/ASTCheckGrantQuery.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+#include
+#include
+
+
+namespace DB
+{
+class ASTRolesOrUsersSet;
+
+
+/** Parses queries like
+ * CHECK GRANT access_type[(column_name [,...])] [,...] ON {db.table|db.*|*.*|table|*}
+ */
+class ASTCheckGrantQuery : public IAST
+{
+public:
+ AccessRightsElements access_rights_elements;
+
+ String getID(char) const override;
+ ASTPtr clone() const override;
+ void formatImpl(const FormatSettings & settings, FormatState &, FormatStateStacked) const override;
+ void replaceEmptyDatabase(const String & current_database);
+ QueryKind getQueryKind() const override { return QueryKind::Check; }
+};
+}
diff --git a/src/Parsers/Access/ParserCheckGrantQuery.cpp b/src/Parsers/Access/ParserCheckGrantQuery.cpp
new file mode 100644
index 00000000000..c7433b7d4df
--- /dev/null
+++ b/src/Parsers/Access/ParserCheckGrantQuery.cpp
@@ -0,0 +1,231 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+namespace DB
+{
+namespace ErrorCodes
+{
+ extern const int INVALID_GRANT;
+}
+
+namespace
+{
+ bool parseAccessFlags(IParser::Pos & pos, Expected & expected, AccessFlags & access_flags)
+ {
+ static constexpr auto is_one_of_access_type_words = [](IParser::Pos & pos_)
+ {
+ if (pos_->type != TokenType::BareWord)
+ return false;
+ std::string_view word{pos_->begin, pos_->size()};
+ return !(boost::iequals(word, toStringView(Keyword::ON)) || boost::iequals(word, toStringView(Keyword::TO)) || boost::iequals(word, toStringView(Keyword::FROM)));
+ };
+
+ expected.add(pos, "access type");
+
+ return IParserBase::wrapParseImpl(pos, [&]
+ {
+ if (!is_one_of_access_type_words(pos))
+ return false;
+
+ String str;
+ do
+ {
+ if (!str.empty())
+ str += " ";
+ str += std::string_view(pos->begin, pos->size());
+ ++pos;
+ }
+ while (is_one_of_access_type_words(pos));
+
+ try
+ {
+ access_flags = AccessFlags{str};
+ }
+ catch (...)
+ {
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+
+ bool parseColumnNames(IParser::Pos & pos, Expected & expected, Strings & columns)
+ {
+ return IParserBase::wrapParseImpl(pos, [&]
+ {
+ if (!ParserToken{TokenType::OpeningRoundBracket}.ignore(pos, expected))
+ return false;
+
+ ASTPtr ast;
+ if (!ParserList{std::make_unique(), std::make_unique(TokenType::Comma), false}.parse(pos, ast, expected))
+ return false;
+
+ Strings res_columns;
+ for (const auto & child : ast->children)
+ res_columns.emplace_back(getIdentifierName(child));
+
+ if (!ParserToken{TokenType::ClosingRoundBracket}.ignore(pos, expected))
+ return false;
+
+ columns = std::move(res_columns);
+ return true;
+ });
+ }
+
+ bool parseAccessFlagsWithColumns(IParser::Pos & pos, Expected & expected,
+ std::vector> & access_and_columns)
+ {
+ std::vector> res;
+
+ auto parse_access_and_columns = [&]
+ {
+ AccessFlags access_flags;
+ if (!parseAccessFlags(pos, expected, access_flags))
+ return false;
+
+ Strings columns;
+ parseColumnNames(pos, expected, columns);
+ res.emplace_back(access_flags, std::move(columns));
+ return true;
+ };
+
+ if (!ParserList::parseUtil(pos, expected, parse_access_and_columns, false))
+ return false;
+
+ access_and_columns = std::move(res);
+ return true;
+ }
+
+
+ bool parseElementsWithoutOptions(IParser::Pos & pos, Expected & expected, AccessRightsElements & elements)
+ {
+ return IParserBase::wrapParseImpl(pos, [&]
+ {
+ AccessRightsElements res_elements;
+
+ auto parse_around_on = [&]
+ {
+ std::vector> access_and_columns;
+ if (!parseAccessFlagsWithColumns(pos, expected, access_and_columns))
+ return false;
+
+ String database_name, table_name, parameter;
+ bool any_database = false, any_table = false, any_parameter = false;
+
+ size_t is_global_with_parameter = 0;
+ for (const auto & elem : access_and_columns)
+ {
+ if (elem.first.isGlobalWithParameter())
+ ++is_global_with_parameter;
+ }
+
+ if (!ParserKeyword{Keyword::ON}.ignore(pos, expected))
+ return false;
+
+ if (is_global_with_parameter && is_global_with_parameter == access_and_columns.size())
+ {
+ ASTPtr parameter_ast;
+ if (ParserToken{TokenType::Asterisk}.ignore(pos, expected))
+ {
+ any_parameter = true;
+ }
+ else if (ParserIdentifier{}.parse(pos, parameter_ast, expected))
+ {
+ any_parameter = false;
+ parameter = getIdentifierName(parameter_ast);
+ }
+ else
+ return false;
+
+ any_database = any_table = true;
+ }
+ else if (!parseDatabaseAndTableNameOrAsterisks(pos, expected, database_name, any_database, table_name, any_table))
+ {
+ return false;
+ }
+
+ for (auto & [access_flags, columns] : access_and_columns)
+ {
+ AccessRightsElement element;
+ element.access_flags = access_flags;
+ element.any_column = columns.empty();
+ element.columns = std::move(columns);
+ element.any_database = any_database;
+ element.database = database_name;
+ element.any_table = any_table;
+ element.any_parameter = any_parameter;
+ element.table = table_name;
+ element.parameter = parameter;
+ res_elements.emplace_back(std::move(element));
+ }
+
+ return true;
+ };
+
+ if (!ParserList::parseUtil(pos, expected, parse_around_on, false))
+ return false;
+
+ elements = std::move(res_elements);
+ return true;
+ });
+ }
+
+ void throwIfNotGrantable(AccessRightsElements & elements)
+ {
+ std::erase_if(elements, [](AccessRightsElement & element)
+ {
+ if (element.empty())
+ return true;
+ auto old_flags = element.access_flags;
+ element.eraseNonGrantable();
+ if (!element.empty())
+ return false;
+
+ if (!element.any_column)
+ throw Exception(ErrorCodes::INVALID_GRANT, "{} cannot check grant on the column level", old_flags.toString());
+ else if (!element.any_table)
+ throw Exception(ErrorCodes::INVALID_GRANT, "{} cannot check grant on the table level", old_flags.toString());
+ else if (!element.any_database)
+ throw Exception(ErrorCodes::INVALID_GRANT, "{} cannot check grant on the database level", old_flags.toString());
+ else if (!element.any_parameter)
+ throw Exception(ErrorCodes::INVALID_GRANT, "{} cannot check grant on the global with parameter level", old_flags.toString());
+ else
+ throw Exception(ErrorCodes::INVALID_GRANT, "{} cannot check grant", old_flags.toString());
+ });
+ }
+}
+
+
+bool ParserCheckGrantQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
+{
+ if (!ParserKeyword{Keyword::CHECK_GRANT}.ignore(pos, expected))
+ return false;
+
+
+ AccessRightsElements elements;
+
+ if (!parseElementsWithoutOptions(pos, expected, elements))
+ return false;
+
+ throwIfNotGrantable(elements);
+
+ auto query = std::make_shared();
+ node = query;
+
+ query->access_rights_elements = std::move(elements);
+
+ return true;
+}
+}
diff --git a/src/Parsers/Access/ParserCheckGrantQuery.h b/src/Parsers/Access/ParserCheckGrantQuery.h
new file mode 100644
index 00000000000..d58ea6002a8
--- /dev/null
+++ b/src/Parsers/Access/ParserCheckGrantQuery.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include
+
+
+namespace DB
+{
+/** Parses queries like
+ * CHECK GRANT access_type[(column_name [,...])] [,...] ON {db.table|db.*|*.*|table|*}
+ */
+class ParserCheckGrantQuery : public IParserBase
+{
+protected:
+ const char * getName() const override { return "CHECK GRANT"; }
+ bool parseImpl(Pos & pos, ASTPtr & node, Expected & expected) override;
+};
+}
diff --git a/src/Parsers/CommonParsers.h b/src/Parsers/CommonParsers.h
index 46e08cf3f7e..67ea46bc0d1 100644
--- a/src/Parsers/CommonParsers.h
+++ b/src/Parsers/CommonParsers.h
@@ -78,6 +78,7 @@ namespace DB
MR_MACROS(CHARACTER, "CHARACTER") \
MR_MACROS(CHECK_ALL_TABLES, "CHECK ALL TABLES") \
MR_MACROS(CHECK_TABLE, "CHECK TABLE") \
+ MR_MACROS(CHECK_GRANT, "CHECK GRANT")\
MR_MACROS(CHECK, "CHECK") \
MR_MACROS(CLEANUP, "CLEANUP") \
MR_MACROS(CLEAR_COLUMN, "CLEAR COLUMN") \
diff --git a/src/Parsers/ParserQuery.cpp b/src/Parsers/ParserQuery.cpp
index 22ddc25019f..e939cdd07ba 100644
--- a/src/Parsers/ParserQuery.cpp
+++ b/src/Parsers/ParserQuery.cpp
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include
#include
@@ -56,6 +57,7 @@ bool ParserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
ParserDropAccessEntityQuery drop_access_entity_p;
ParserMoveAccessEntityQuery move_access_entity_p;
ParserGrantQuery grant_p;
+ ParserCheckGrantQuery check_grant_p;
ParserSetRoleQuery set_role_p;
ParserExternalDDLQuery external_ddl_p;
ParserTransactionControl transaction_control_p;
@@ -82,6 +84,7 @@ bool ParserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
|| drop_access_entity_p.parse(pos, node, expected)
|| move_access_entity_p.parse(pos, node, expected)
|| grant_p.parse(pos, node, expected)
+ || check_grant_p.parse(pos, node, expected)
|| external_ddl_p.parse(pos, node, expected)
|| transaction_control_p.parse(pos, node, expected)
|| delete_p.parse(pos, node, expected);
diff --git a/tests/queries/0_stateless/03234_check_grant.reference b/tests/queries/0_stateless/03234_check_grant.reference
new file mode 100644
index 00000000000..5565ed6787f
--- /dev/null
+++ b/tests/queries/0_stateless/03234_check_grant.reference
@@ -0,0 +1,4 @@
+0
+1
+0
+1
diff --git a/tests/queries/0_stateless/03234_check_grant.sh b/tests/queries/0_stateless/03234_check_grant.sh
new file mode 100755
index 00000000000..29b33e64f2e
--- /dev/null
+++ b/tests/queries/0_stateless/03234_check_grant.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+# Tags: no-parallel
+CURDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+# shellcheck source=../shell_config.sh
+. "$CURDIR"/../shell_config.sh
+
+${CLICKHOUSE_CLIENT} --query "DROP USER IF EXISTS user_03234; DROP TABLE IF EXISTS tb;CREATE USER user_03234; GRANT SELECT ON tb TO user_03234;"
+
+# Has been granted but not table not exists
+${CLICKHOUSE_CURL} -sS "${CLICKHOUSE_URL}&user=user_03234" --data-binary "CHECK GRANT SELECT ON tb"
+
+${CLICKHOUSE_CLIENT} --query "CREATE TABLE tb (\`content\` UInt64) ENGINE = MergeTree ORDER BY content; INSERT INTO tb VALUES (1);"
+# Has been granted and table exists
+${CLICKHOUSE_CURL} -sS "${CLICKHOUSE_URL}&user=user_03234" --data-binary "CHECK GRANT SELECT ON tb"
+
+${CLICKHOUSE_CLIENT} --query "REVOKE SELECT ON tb FROM user_03234;"
+# Has not been granted but table exists
+${CLICKHOUSE_CURL} -sS "${CLICKHOUSE_URL}&user=user_03234" --data-binary "CHECK GRANT SELECT ON tb"
+
+# Role
+${CLICKHOUSE_CLIENT} --query "DROP ROLE IF EXISTS role_03234;CREATE ROLE role_03234;GRANT SELECT ON tb TO role_03234;GRANT role_03234 TO user_03234"
+${CLICKHOUSE_CURL} -sS "${CLICKHOUSE_URL}&user=user_03234" --data-binary "SET ROLE role_03234"
+${CLICKHOUSE_CURL} -sS "${CLICKHOUSE_URL}&user=user_03234" --data-binary "CHECK GRANT SELECT ON tb"