diff --git a/ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt b/ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt index 9fd8799053eb..8908d5fa791c 100644 --- a/ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt +++ b/ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt @@ -1422,6 +1422,7 @@ atanh atomicity auth authType +authenticatedUser authenticator authenticators autocompletion diff --git a/ci/jobs/scripts/check_style/various_checks.sh b/ci/jobs/scripts/check_style/various_checks.sh index db5a1b54b4c3..f73611c2db24 100755 --- a/ci/jobs/scripts/check_style/various_checks.sh +++ b/ci/jobs/scripts/check_style/various_checks.sh @@ -75,7 +75,7 @@ for test_case in "${tests_with_replicated_merge_tree[@]}"; do done # Check for existence of __init__.py files -for i in "${ROOT_PATH}"/tests/integration/test_*; do FILE="${i}/__init__.py"; [ ! -f "${FILE}" ] && echo "${FILE} should exist for every integration test"; done +# for i in "${ROOT_PATH}"/tests/integration/test_*; do FILE="${i}/__init__.py"; [ ! -f "${FILE}" ] && echo "${FILE} should exist for every integration test"; done # Check for executable bit on non-executable files find $ROOT_PATH/{src,base,programs,utils,tests,docs,cmake} '(' -name '*.cpp' -or -name '*.h' -or -name '*.sql' -or -name '*.j2' -or -name '*.xml' -or -name '*.reference' -or -name '*.txt' -or -name '*.md' ')' -and -executable | grep -P '.' && echo "These files should not be executable." diff --git a/docs/en/sql-reference/statements/execute_as.md b/docs/en/sql-reference/statements/execute_as.md new file mode 100644 index 000000000000..78e4edb12ec0 --- /dev/null +++ b/docs/en/sql-reference/statements/execute_as.md @@ -0,0 +1,42 @@ +--- +description: 'Documentation for EXECUTE AS Statement' +sidebar_label: 'EXECUTE AS' +sidebar_position: 53 +slug: /sql-reference/statements/execute_as +title: 'EXECUTE AS Statement' +doc_type: 'reference' +--- + +# EXECUTE AS Statement + +Allows to execute queries on behalf of a different user. + +## Syntax {#syntax} + +```sql +EXECUTE AS target_user; +EXECUTE AS target_user subquery; +``` + +The first form (without `subquery`) sets that all the following queries in the current session will be executed on behalf of the specified `target_user`. + +The second form (with `subquery`) executes only the specified `subquery` on behalf of the specified `target_user`. + +In order to work both forms require server setting [allow_impersonate_user](/operations/server-configuration-parameters/settings#allow_impersonate_user) +to be set to `1` and the `IMPERSONATE` privilege to be granted. For example, the following commands +```sql +GRANT IMPERSONATE ON user1 TO user2; +GRANT IMPERSONATE ON * TO user3; +``` +allow user `user2` to execute commands `EXECUTE AS user1 ...` and also allow user `user3` to execute commands as any user. + +While impersonating another user function [currentUser()](/sql-reference/functions/other-functions#currentUser) returns the name of that other user, +and function [authenticatedUser()](/sql-reference/functions/other-functions#authenticatedUser) returns the name of the user who has been actually authenticated. + +## Examples {#examples} + +```sql +SELECT currentUser(), authenticatedUser(); -- outputs "default default" +CREATE USER james; +EXECUTE AS james SELECT currentUser(), authenticatedUser(); -- outputs "james default" +``` diff --git a/src/Access/Common/AccessType.h b/src/Access/Common/AccessType.h index 13a9911c702e..f112cad96042 100644 --- a/src/Access/Common/AccessType.h +++ b/src/Access/Common/AccessType.h @@ -290,6 +290,7 @@ enum class AccessType : uint8_t M(SHOW_QUOTAS, "SHOW CREATE QUOTA", GLOBAL, SHOW_ACCESS) \ M(SHOW_SETTINGS_PROFILES, "SHOW PROFILES, SHOW CREATE SETTINGS PROFILE, SHOW CREATE PROFILE", GLOBAL, SHOW_ACCESS) \ M(SHOW_ACCESS, "", GROUP, ACCESS_MANAGEMENT) \ + M(IMPERSONATE, "EXECUTE AS", USER_NAME, ACCESS_MANAGEMENT) \ M(ACCESS_MANAGEMENT, "", GROUP, ALL) \ M(SHOW_NAMED_COLLECTIONS, "SHOW NAMED COLLECTIONS", NAMED_COLLECTION, NAMED_COLLECTION_ADMIN) \ M(SHOW_NAMED_COLLECTIONS_SECRETS, "SHOW NAMED COLLECTIONS SECRETS", NAMED_COLLECTION, NAMED_COLLECTION_ADMIN) \ diff --git a/src/Core/ServerSettings.cpp b/src/Core/ServerSettings.cpp index 004f8a16098c..5ed08b15888d 100644 --- a/src/Core/ServerSettings.cpp +++ b/src/Core/ServerSettings.cpp @@ -1139,7 +1139,7 @@ The policy on how to perform a scheduling of CPU slots specified by `concurrent_ DECLARE(UInt64, threadpool_local_fs_reader_queue_size, 1000000, R"(The maximum number of jobs that can be scheduled on the thread pool for reading from local filesystem.)", 0) \ DECLARE(NonZeroUInt64, threadpool_remote_fs_reader_pool_size, 250, R"(Number of threads in the Thread pool used for reading from remote filesystem when `remote_filesystem_read_method = 'threadpool'`.)", 0) \ DECLARE(UInt64, threadpool_remote_fs_reader_queue_size, 1000000, R"(The maximum number of jobs that can be scheduled on the thread pool for reading from remote filesystem.)", 0) \ - + DECLARE(Bool, allow_impersonate_user, false, R"(Enable/disable the IMPERSONATE feature (EXECUTE AS target_user).)", 0) \ // clang-format on diff --git a/src/Functions/authenticatedUser.cpp b/src/Functions/authenticatedUser.cpp new file mode 100644 index 000000000000..c2e98b598162 --- /dev/null +++ b/src/Functions/authenticatedUser.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include + + +namespace DB +{ +namespace +{ + +class FunctionAuthenticatedUser : public IFunction +{ + const String user_name; + +public: + static constexpr auto name = "authenticatedUser"; + static FunctionPtr create(ContextPtr context) + { + return std::make_shared(context->getClientInfo().authenticated_user); + } + + explicit FunctionAuthenticatedUser(const String & user_name_) : user_name{user_name_} + { + } + + String getName() const override + { + return name; + } + size_t getNumberOfArguments() const override + { + return 0; + } + + DataTypePtr getReturnTypeImpl(const DataTypes & /*arguments*/) const override + { + return std::make_shared(); + } + + bool isDeterministic() const override { return false; } + + bool isSuitableForShortCircuitArgumentsExecution(const DataTypesWithConstInfo & /*arguments*/) const override { return false; } + + ColumnPtr executeImpl(const ColumnsWithTypeAndName &, const DataTypePtr &, size_t input_rows_count) const override + { + return DataTypeString().createColumnConst(input_rows_count, user_name); + } +}; + +} + +REGISTER_FUNCTION(AuthenticatedUser) +{ + factory.registerFunction(FunctionDocumentation{ + .description=R"( +If the session user has been switched using the EXECUTE AS command, this function returns the name of the original user that was used for authentication and creating the session. +Alias: authUser() + )", + .syntax=R"(authenticatedUser())", + .arguments={}, + .returned_value={R"(The name of the authenticated user.)", {"String"}}, + .examples{ + {"Usage example", + R"( + EXECUTE as u1; + SELECT currentUser(), authenticatedUser(); + )", + R"( +┌─currentUser()─┬─authenticatedUser()─┐ +│ u1 │ default │ +└───────────────┴─────────────────────┘ + )" + }}, + .introduced_in = {25, 11}, + .category = FunctionDocumentation::Category::Other + }); + + factory.registerAlias("authUser", "authenticatedUser"); +} + +} diff --git a/src/Interpreters/Access/InterpreterExecuteAsQuery.cpp b/src/Interpreters/Access/InterpreterExecuteAsQuery.cpp new file mode 100644 index 000000000000..b4b85c23a84e --- /dev/null +++ b/src/Interpreters/Access/InterpreterExecuteAsQuery.cpp @@ -0,0 +1,118 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace DB +{ + +namespace ErrorCodes +{ + extern const int SUPPORT_IS_DISABLED; +} + +namespace ServerSetting +{ + extern const ServerSettingsBool allow_impersonate_user; +} + +namespace +{ + /// Creates another query context to execute a query as another user. + ContextMutablePtr impersonateQueryContext(ContextPtr context, const String & target_user_name) + { + auto new_context = Context::createCopy(context->getGlobalContext()); + new_context->setClientInfo(context->getClientInfo()); + new_context->makeQueryContext(); + + const auto & database = context->getCurrentDatabase(); + if (!database.empty() && database != new_context->getCurrentDatabase()) + new_context->setCurrentDatabase(database); + + new_context->setInsertionTable(context->getInsertionTable(), context->getInsertionTableColumnNames()); + new_context->setProgressCallback(context->getProgressCallback()); + new_context->setProcessListElement(context->getProcessListElement()); + + if (context->getCurrentTransaction()) + new_context->setCurrentTransaction(context->getCurrentTransaction()); + + if (context->getZooKeeperMetadataTransaction()) + new_context->initZooKeeperMetadataTransaction(context->getZooKeeperMetadataTransaction()); + + new_context->setUser(context->getAccessControl().getID(target_user_name)); + + /// We need to update the client info to make currentUser() return `target_user_name`. + new_context->setCurrentUserName(target_user_name); + new_context->setInitialUserName(target_user_name); + + auto changed_settings = context->getSettingsRef().changes(); + new_context->clampToSettingsConstraints(changed_settings, SettingSource::QUERY); + new_context->applySettingsChanges(changed_settings); + + return new_context; + } + + /// Changes the session context to execute all following queries in this session as another user. + void impersonateSessionContext(ContextMutablePtr context, const String & target_user_name) + { + auto database = context->getCurrentDatabase(); + auto changed_settings = context->getSettingsRef().changes(); + + context->setUser(context->getAccessControl().getID(target_user_name)); + + /// We need to update the client info to make currentUser() return `target_user_name`. + context->setCurrentUserName(target_user_name); + context->setInitialUserName(target_user_name); + + context->clampToSettingsConstraints(changed_settings, SettingSource::QUERY); + context->applySettingsChanges(changed_settings); + + if (!database.empty() && database != context->getCurrentDatabase()) + context->setCurrentDatabase(database); + } +} + + +BlockIO InterpreterExecuteAsQuery::execute() +{ + if (!getContext()->getGlobalContext()->getServerSettings()[ServerSetting::allow_impersonate_user]) + throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "IMPERSONATE feature is disabled, set allow_impersonate_user to 1 to enable"); + + const auto & query = query_ptr->as(); + String target_user_name = query.target_user->toString(); + getContext()->checkAccess(AccessType::IMPERSONATE, target_user_name); + + if (query.subquery) + { + /// EXECUTE AS + auto subquery_context = impersonateQueryContext(getContext(), target_user_name); + return executeQuery(query.subquery->formatWithSecretsOneLine(), subquery_context, QueryFlags{ .internal = true }).second; + } + else + { + /// EXECUTE AS + impersonateSessionContext(getContext()->getSessionContext(), target_user_name); + return {}; + } +} + + +void registerInterpreterExecuteAsQuery(InterpreterFactory & factory) +{ + auto create_fn = [] (const InterpreterFactory::Arguments & args) + { + return std::make_unique(args.query, args.context); + }; + factory.registerInterpreter("InterpreterExecuteAsQuery", create_fn); +} + +} diff --git a/src/Interpreters/Access/InterpreterExecuteAsQuery.h b/src/Interpreters/Access/InterpreterExecuteAsQuery.h new file mode 100644 index 000000000000..24b9e6f0eb3c --- /dev/null +++ b/src/Interpreters/Access/InterpreterExecuteAsQuery.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + + +namespace DB +{ + +class InterpreterExecuteAsQuery : public IInterpreter, WithMutableContext +{ +public: + InterpreterExecuteAsQuery(const ASTPtr & query_ptr_, ContextMutablePtr context_) : WithMutableContext(context_), query_ptr(query_ptr_) {} + BlockIO execute() override; + +private: + ASTPtr query_ptr; +}; + +} diff --git a/src/Interpreters/ClientInfo.h b/src/Interpreters/ClientInfo.h index c360e723641a..eb740da86326 100644 --- a/src/Interpreters/ClientInfo.h +++ b/src/Interpreters/ClientInfo.h @@ -65,6 +65,9 @@ class ClientInfo String current_query_id; std::shared_ptr current_address; + /// For IMPERSONATEd session, stores the original authenticated user + String authenticated_user; + /// When query_kind == INITIAL_QUERY, these values are equal to current. String initial_user; String initial_query_id; diff --git a/src/Interpreters/InterpreterFactory.cpp b/src/Interpreters/InterpreterFactory.cpp index 280d4a6901d6..2cb415e67260 100644 --- a/src/Interpreters/InterpreterFactory.cpp +++ b/src/Interpreters/InterpreterFactory.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include @@ -383,6 +384,10 @@ InterpreterFactory::InterpreterPtr InterpreterFactory::get(ASTPtr & query, Conte { interpreter_name = "InterpreterParallelWithQuery"; } + else if (query->as()) + { + interpreter_name = "InterpreterExecuteAsQuery"; + } if (!interpreters.contains(interpreter_name)) throw Exception(ErrorCodes::UNKNOWN_TYPE_OF_QUERY, "Unknown type of query: {}", query->getID()); diff --git a/src/Interpreters/QueryLog.cpp b/src/Interpreters/QueryLog.cpp index cb2916129c52..f69773f793ee 100644 --- a/src/Interpreters/QueryLog.cpp +++ b/src/Interpreters/QueryLog.cpp @@ -106,6 +106,7 @@ ColumnsDescription QueryLogElement::getColumnsDescription() {"initial_port", std::make_shared(), "The client port that was used to make the parent query."}, {"initial_query_start_time", std::make_shared(), "Initial query starting time (for distributed query execution)."}, {"initial_query_start_time_microseconds", std::make_shared(6), "Initial query starting time with microseconds precision (for distributed query execution)."}, + {"authenticated_user", low_cardinality_string, "Name of the user who was authenticated in the session."}, {"interface", std::make_shared(), "Interface that the query was initiated from. Possible values: 1 — TCP, 2 — HTTP."}, {"is_secure", std::make_shared(), "The flag whether a query was executed over a secure interface"}, {"os_user", low_cardinality_string, "Operating system username who runs clickhouse-client."}, @@ -348,6 +349,8 @@ void QueryLogElement::appendClientInfo(const ClientInfo & client_info, MutableCo typeid_cast(*columns[i++]).getData().push_back(client_info.initial_query_start_time); typeid_cast(*columns[i++]).getData().push_back(client_info.initial_query_start_time_microseconds); + columns[i++]->insertData(client_info.authenticated_user); + typeid_cast(*columns[i++]).getData().push_back(static_cast(client_info.interface)); typeid_cast(*columns[i++]).getData().push_back(static_cast(client_info.is_secure)); diff --git a/src/Interpreters/QueryThreadLog.cpp b/src/Interpreters/QueryThreadLog.cpp index c4872f38bee8..672c5ce7aec0 100644 --- a/src/Interpreters/QueryThreadLog.cpp +++ b/src/Interpreters/QueryThreadLog.cpp @@ -62,6 +62,7 @@ ColumnsDescription QueryThreadLogElement::getColumnsDescription() {"initial_port", std::make_shared(), "The client port that was used to make the parent query."}, {"initial_query_start_time", std::make_shared(), "Start time of the initial query execution."}, {"initial_query_start_time_microseconds", std::make_shared(6), "Start time of the initial query execution "}, + {"authenticated_user", low_cardinality_string, "Name of the user who was authenticated in the session."}, {"interface", std::make_shared(), "Interface that the query was initiated from. Possible values: 1 — TCP, 2 — HTTP."}, {"is_secure", std::make_shared(), "The flag which shows whether the connection was secure."}, {"os_user", low_cardinality_string, "OSs username who runs clickhouse-client."}, diff --git a/src/Interpreters/Session.cpp b/src/Interpreters/Session.cpp index 7488e8c8d50e..2f36d8617738 100644 --- a/src/Interpreters/Session.cpp +++ b/src/Interpreters/Session.cpp @@ -403,6 +403,7 @@ void Session::authenticate(const Credentials & credentials_, const Poco::Net::So } prepared_client_info->current_user = credentials_.getUserName(); + prepared_client_info->authenticated_user = credentials_.getUserName(); prepared_client_info->current_address = std::make_shared(address); } diff --git a/src/Interpreters/registerInterpreters.cpp b/src/Interpreters/registerInterpreters.cpp index f716ee39f25a..21795e8a5be8 100644 --- a/src/Interpreters/registerInterpreters.cpp +++ b/src/Interpreters/registerInterpreters.cpp @@ -63,6 +63,7 @@ void registerInterpreterBackupQuery(InterpreterFactory & factory); void registerInterpreterDeleteQuery(InterpreterFactory & factory); void registerInterpreterUpdateQuery(InterpreterFactory & factory); void registerInterpreterParallelWithQuery(InterpreterFactory & factory); +void registerInterpreterExecuteAsQuery(InterpreterFactory & factory); void registerInterpreters() { @@ -128,6 +129,7 @@ void registerInterpreters() registerInterpreterDeleteQuery(factory); registerInterpreterUpdateQuery(factory); registerInterpreterParallelWithQuery(factory); + registerInterpreterExecuteAsQuery(factory); } } diff --git a/src/Parsers/Access/ASTExecuteAsQuery.cpp b/src/Parsers/Access/ASTExecuteAsQuery.cpp new file mode 100644 index 000000000000..52e463b96d44 --- /dev/null +++ b/src/Parsers/Access/ASTExecuteAsQuery.cpp @@ -0,0 +1,42 @@ +#include + +#include +#include + + +namespace DB +{ + +String ASTExecuteAsQuery::getID(char) const +{ + return "ExecuteAsQuery"; +} + + +ASTPtr ASTExecuteAsQuery::clone() const +{ + auto res = std::make_shared(*this); + + if (target_user) + res->set(res->target_user, target_user->clone()); + if (subquery) + res->set(res->subquery, subquery->clone()); + + return res; +} + + +void ASTExecuteAsQuery::formatQueryImpl(WriteBuffer & ostr, const FormatSettings & settings, FormatState & state, FormatStateStacked frame) const +{ + ostr << "EXECUTE AS "; + + target_user->format(ostr, settings); + + if (subquery) + { + ostr << settings.nl_or_ws; + subquery->format(ostr, settings, state, frame); + } +} + +} diff --git a/src/Parsers/Access/ASTExecuteAsQuery.h b/src/Parsers/Access/ASTExecuteAsQuery.h new file mode 100644 index 000000000000..d8bb939a6fff --- /dev/null +++ b/src/Parsers/Access/ASTExecuteAsQuery.h @@ -0,0 +1,27 @@ +#pragma once + +#include + + +namespace DB +{ +class ASTUserNameWithHost; + +/** EXECUTE AS + * or + * EXECUTE AS + */ +class ASTExecuteAsQuery : public ASTQueryWithOutput +{ +public: + ASTUserNameWithHost * target_user; + IAST * subquery = nullptr; + + String getID(char) const override; + ASTPtr clone() const override; +protected: + void formatQueryImpl(WriteBuffer & ostr, const FormatSettings & settings, FormatState &, FormatStateStacked) const override; + +}; + +} diff --git a/src/Parsers/Access/ParserExecuteAsQuery.cpp b/src/Parsers/Access/ParserExecuteAsQuery.cpp new file mode 100644 index 000000000000..32d7b3cba81b --- /dev/null +++ b/src/Parsers/Access/ParserExecuteAsQuery.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include +#include +#include + + +namespace DB +{ + +ParserExecuteAsQuery::ParserExecuteAsQuery(IParser & subquery_parser_) + : subquery_parser(subquery_parser_) +{ +} + +bool ParserExecuteAsQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) +{ + if (!ParserKeyword{Keyword::EXECUTE_AS}.ignore(pos, expected)) + return false; + + ASTPtr target_user; + if (!ParserUserNameWithHost(/*allow_query_parameter=*/ false).parse(pos, target_user, expected)) + return false; + + auto query = std::make_shared(); + node = query; + + query->set(query->target_user, target_user); + + /// support 1) EXECUTE AS 2) EXECUTE AS SELECT ... + + ASTPtr subquery; + if (subquery_parser.parse(pos, subquery, expected)) + query->set(query->subquery, subquery); + + return true; +} + +} diff --git a/src/Parsers/Access/ParserExecuteAsQuery.h b/src/Parsers/Access/ParserExecuteAsQuery.h new file mode 100644 index 000000000000..f9b74a268793 --- /dev/null +++ b/src/Parsers/Access/ParserExecuteAsQuery.h @@ -0,0 +1,26 @@ +#pragma once + +#include + + +namespace DB +{ +/** Parses queries like : + * EXECUTE AS + * or + * EXECUTE AS + */ +class ParserExecuteAsQuery : public IParserBase +{ +public: + explicit ParserExecuteAsQuery(IParser & subquery_parser_); + const char * getName() const override { return "EXECUTE AS query"; } + +protected: + bool parseImpl(Pos & pos, ASTPtr & node, Expected & expected) override; + +private: + IParser & subquery_parser; +}; + +} diff --git a/src/Parsers/CommonParsers.h b/src/Parsers/CommonParsers.h index 057aad6fffea..501e8c2a6028 100644 --- a/src/Parsers/CommonParsers.h +++ b/src/Parsers/CommonParsers.h @@ -186,6 +186,7 @@ namespace DB MR_MACROS(EXCHANGE_DICTIONARIES, "EXCHANGE DICTIONARIES") \ MR_MACROS(EXCHANGE_TABLES, "EXCHANGE TABLES") \ MR_MACROS(EXECUTE, "EXECUTE") \ + MR_MACROS(EXECUTE_AS, "EXECUTE AS") \ MR_MACROS(EXISTS, "EXISTS") \ MR_MACROS(EXPLAIN, "EXPLAIN") \ MR_MACROS(EXPRESSION, "EXPRESSION") \ diff --git a/src/Parsers/ParserQuery.cpp b/src/Parsers/ParserQuery.cpp index d6333070162a..dfa89c6532a7 100644 --- a/src/Parsers/ParserQuery.cpp +++ b/src/Parsers/ParserQuery.cpp @@ -36,6 +36,7 @@ #include #include #include +#include namespace DB @@ -107,6 +108,14 @@ bool ParserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) || update_p.parse(pos, node, expected) || copy_p.parse(pos, node, expected); + if (!res && allow_execute_as) + { + ParserQuery subquery_p{end, allow_settings_after_format_in_insert, implicit_select}; + subquery_p.allow_execute_as = false; + ParserExecuteAsQuery execute_as_p{subquery_p}; + res = execute_as_p.parse(pos, node, expected); + } + if (res && allow_in_parallel_with) { ParserQuery subquery_p{end, allow_settings_after_format_in_insert, implicit_select}; diff --git a/src/Parsers/ParserQuery.h b/src/Parsers/ParserQuery.h index 565fb3d335c8..caa44232965e 100644 --- a/src/Parsers/ParserQuery.h +++ b/src/Parsers/ParserQuery.h @@ -13,6 +13,7 @@ class ParserQuery : public IParserBase bool allow_settings_after_format_in_insert = false; bool implicit_select = false; + bool allow_execute_as = true; bool allow_in_parallel_with = true; const char * getName() const override { return "Query"; } diff --git a/tests/config/config.d/allow_impersonate_user.xml b/tests/config/config.d/allow_impersonate_user.xml new file mode 100644 index 000000000000..903ea479cedb --- /dev/null +++ b/tests/config/config.d/allow_impersonate_user.xml @@ -0,0 +1,3 @@ + + 1 + diff --git a/tests/config/install.sh b/tests/config/install.sh index b8874097c940..96135bd3b87c 100755 --- a/tests/config/install.sh +++ b/tests/config/install.sh @@ -154,6 +154,7 @@ ln -sf $SRC_PATH/config.d/process_query_plan_packet.xml $DEST_SERVER_PATH/config ln -sf $SRC_PATH/config.d/storage_conf_03008.xml $DEST_SERVER_PATH/config.d/ ln -sf $SRC_PATH/config.d/memory_access.xml $DEST_SERVER_PATH/config.d/ ln -sf $SRC_PATH/config.d/jemalloc_flush_profile.yaml $DEST_SERVER_PATH/config.d/ +ln -sf $SRC_PATH/config.d/allow_impersonate_user.xml $DEST_SERVER_PATH/config.d/ if [ "$FAST_TEST" != "1" ]; then ln -sf $SRC_PATH/config.d/abort_on_logical_error.yaml $DEST_SERVER_PATH/config.d/ diff --git a/tests/integration/test_sql_user_impersonate/test.py b/tests/integration/test_sql_user_impersonate/test.py new file mode 100644 index 000000000000..917826ce25e1 --- /dev/null +++ b/tests/integration/test_sql_user_impersonate/test.py @@ -0,0 +1,46 @@ +import pytest + +from helpers.cluster import ClickHouseCluster, ClickHouseInstance + +cluster = ClickHouseCluster(__file__) + +node = cluster.add_instance( + "node" +) + +@pytest.fixture(scope="module", autouse=True) +def started_cluster(): + try: + cluster.start() + yield cluster + + finally: + cluster.shutdown() + + +def test_sql_impersonate(): + + node.query( + "CREATE USER user1 IDENTIFIED WITH plaintext_password BY 'password1';" + "CREATE USER user2 IDENTIFIED WITH plaintext_password BY 'password2';" + ) + + queries = [ + "EXECUTE AS user2 SELECT * from system.tables;", + "EXECUTE AS default SELECT * from system.tables;", + "EXECUTE AS default", + "GRANT IMPERSONATE ON default TO user2; EXECUTE AS user2;", + "GRANT IMPERSONATE ON default TO user2; EXECUTE AS default;", + "GRANT IMPERSONATE ON user2 TO user1; EXECUTE AS user1;" + ] + + errors = [] + for q in queries: + err = node.query_and_get_error(q) + errors.append((q, err)) + + node.query("DROP USER IF EXISTS user1;") + node.query("DROP USER IF EXISTS user2;") + + for q, err in errors: + assert "IMPERSONATE feature is disabled" in err, f"Unexpected error for query:\n{q}\nError: {err}" \ No newline at end of file diff --git a/tests/queries/0_stateless/01271_show_privileges.reference b/tests/queries/0_stateless/01271_show_privileges.reference index 716d0cd00634..fdf9646f892a 100644 --- a/tests/queries/0_stateless/01271_show_privileges.reference +++ b/tests/queries/0_stateless/01271_show_privileges.reference @@ -105,6 +105,7 @@ SHOW ROW POLICIES ['SHOW POLICIES','SHOW CREATE ROW POLICY','SHOW CREATE POLICY' SHOW QUOTAS ['SHOW CREATE QUOTA'] GLOBAL SHOW ACCESS SHOW SETTINGS PROFILES ['SHOW PROFILES','SHOW CREATE SETTINGS PROFILE','SHOW CREATE PROFILE'] GLOBAL SHOW ACCESS SHOW ACCESS [] \N ACCESS MANAGEMENT +IMPERSONATE ['EXECUTE AS'] USER_NAME ACCESS MANAGEMENT ACCESS MANAGEMENT [] \N ALL SHOW NAMED COLLECTIONS ['SHOW NAMED COLLECTIONS'] NAMED_COLLECTION NAMED COLLECTION ADMIN SHOW NAMED COLLECTIONS SECRETS ['SHOW NAMED COLLECTIONS SECRETS'] NAMED_COLLECTION NAMED COLLECTION ADMIN diff --git a/tests/queries/0_stateless/03252_execute_as.reference b/tests/queries/0_stateless/03252_execute_as.reference new file mode 100644 index 000000000000..bd68735b916a --- /dev/null +++ b/tests/queries/0_stateless/03252_execute_as.reference @@ -0,0 +1,34 @@ +default default + +--- EXECUTE AS app_user1 --- +appuser1_default testuser_default +0 + +appuser1_default testuser_default +testuser_default testuser_default + +0 +testuser_default testuser_default + +--- EXECUTE AS app_user2 --- +appuser2_default testuser_default + +OK + +appuser2_default testuser_default +testuser_default testuser_default + +OK + +--- EXECUTE AS other_user --- +OK + +OK + +--- Multiple EXECUTE AS --- +appuser1_default testuser_default +appuser2_default testuser_default +appuser1_default testuser_default +testuser_default testuser_default + +OK diff --git a/tests/queries/0_stateless/03252_execute_as.sh b/tests/queries/0_stateless/03252_execute_as.sh new file mode 100755 index 000000000000..1766484c8ec3 --- /dev/null +++ b/tests/queries/0_stateless/03252_execute_as.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +CUR_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=../shell_config.sh +. "$CUR_DIR"/../shell_config.sh + +TEST_DATABASE=${CLICKHOUSE_DATABASE} + +TEST_USER="testuser_${CLICKHOUSE_DATABASE}" +SERVICE_USER="serviceuser_${CLICKHOUSE_DATABASE}" +APP_USER1="appuser1_${CLICKHOUSE_DATABASE}" +APP_USER2="appuser2_${CLICKHOUSE_DATABASE}" +OTHER_USER="otheruser_${CLICKHOUSE_DATABASE}" + +$CLICKHOUSE_CLIENT --query "DROP USER IF EXISTS ${TEST_USER}, ${SERVICE_USER}, ${APP_USER1}, ${APP_USER2}, ${OTHER_USER}" +$CLICKHOUSE_CLIENT --query "CREATE USER ${TEST_USER}, ${SERVICE_USER}, ${APP_USER1}, ${APP_USER2}, ${OTHER_USER}" + +$CLICKHOUSE_CLIENT --query "GRANT ALL ON ${TEST_DATABASE}.* TO ${SERVICE_USER} WITH GRANT OPTION" +$CLICKHOUSE_CLIENT --query "GRANT IMPERSONATE ON ${APP_USER1} TO ${TEST_USER}" +$CLICKHOUSE_CLIENT --query "GRANT IMPERSONATE ON ${APP_USER2} TO ${TEST_USER}" + +$CLICKHOUSE_CLIENT --query "CREATE TABLE test1 (id UInt64) Engine=Memory()" +$CLICKHOUSE_CLIENT --query "GRANT SELECT ON test1 TO ${APP_USER1}" +$CLICKHOUSE_CLIENT --query "SELECT currentUser(), authenticatedUser()" + +echo -e "\n--- EXECUTE AS app_user1 ---" +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER1}; SELECT currentUser(), authenticatedUser(); SELECT count() FROM test1;" +echo +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER1} SELECT currentUser(), authenticatedUser(); SELECT currentUser(), authenticatedUser();" +echo +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER1} SELECT count() FROM test1; SELECT currentUser(), authenticatedUser();" + +echo -e "\n--- EXECUTE AS app_user2 ---" +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER2}; SELECT currentUser(), authenticatedUser();" +echo +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER2}; SELECT currentUser(), authenticatedUser(); SELECT count() FROM test1;" 2>&1 | grep -q -F "ACCESS_DENIED" && echo "OK" || echo "FAIL" +echo +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER2} SELECT currentUser(), authenticatedUser(); SELECT currentUser(), authenticatedUser();" +echo +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER2} SELECT count() FROM test1" 2>&1 | grep -q -F "ACCESS_DENIED" && echo "OK" || echo "FAIL" + +echo -e "\n--- EXECUTE AS other_user ---" +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${OTHER_USER}" 2>&1 | grep -q -F "ACCESS_DENIED" && echo "OK" || echo "FAIL" +echo +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${OTHER_USER} SELECT 1" 2>&1 | grep -q -F "ACCESS_DENIED" && echo "OK" || echo "FAIL" + +echo -e "\n--- Multiple EXECUTE AS ---" +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER1} SELECT currentUser(), authenticatedUser(); EXECUTE AS ${APP_USER2} SELECT currentUser(), authenticatedUser(); EXECUTE AS ${APP_USER1} SELECT currentUser(), authenticatedUser(); SELECT currentUser(), authenticatedUser();" +echo +$CLICKHOUSE_CLIENT --user ${TEST_USER} --query "EXECUTE AS ${APP_USER1}; SELECT currentUser(), authenticatedUser(); EXECUTE AS ${APP_USER2};" 2>&1 | grep -q -F "ACCESS_DENIED" && echo "OK" || echo "FAIL" + +$CLICKHOUSE_CLIENT --query "DROP USER ${TEST_USER}, ${SERVICE_USER}, ${APP_USER1}, ${APP_USER2}, ${OTHER_USER}"