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"