diff --git a/src/Interpreters/Access/InterpreterGrantQuery.cpp b/src/Interpreters/Access/InterpreterGrantQuery.cpp index 53d9df3e11bb..ea4eb527bf9b 100644 --- a/src/Interpreters/Access/InterpreterGrantQuery.cpp +++ b/src/Interpreters/Access/InterpreterGrantQuery.cpp @@ -39,13 +39,17 @@ namespace ServerSetting namespace { /// Extracts access rights elements which are going to be granted or revoked from a query. + /// elements_to_revoke: applied BEFORE the grant (for REPLACE: revoke ALL, or standalone REVOKE). + /// elements_to_revoke_after_grant: applied AFTER the grant (combined syntax revokes → creates partial revokes). void collectAccessRightsElementsToGrantOrRevoke( const ASTGrantQuery & query, AccessRightsElements & elements_to_grant, - AccessRightsElements & elements_to_revoke) + AccessRightsElements & elements_to_revoke, + AccessRightsElements & elements_to_revoke_after_grant) { elements_to_grant.clear(); elements_to_revoke.clear(); + elements_to_revoke_after_grant.clear(); if (query.is_revoke) { @@ -57,11 +61,15 @@ namespace /// GRANT WITH REPLACE OPTION elements_to_grant = query.access_rights_elements; elements_to_revoke.emplace_back(AccessType::ALL); + /// Explicit revokes from combined syntax are applied after the grant + elements_to_revoke_after_grant = query.access_rights_elements_to_revoke; } else { - /// GRANT + /// GRANT (possibly with embedded REVOKE) elements_to_grant = query.access_rights_elements; + /// Combined syntax revokes are applied after the grant to create partial revokes + elements_to_revoke_after_grant = query.access_rights_elements_to_revoke; } } @@ -135,13 +143,19 @@ namespace const std::vector & grantees_from_query, bool & need_check_grantees_are_allowed, const AccessRightsElements & elements_to_grant, - AccessRightsElements & elements_to_revoke) + AccessRightsElements & elements_to_revoke, + AccessRightsElements & elements_to_revoke_after_grant) { /// Check access rights which are going to be granted. /// To execute the command GRANT the current user needs to have the access granted with GRANT OPTION. current_user_access.checkGrantOption(elements_to_grant); - if (current_user_access.hasGrantOption(elements_to_revoke)) + /// Combine both revoke lists for permission checking. + AccessRightsElements all_elements_to_revoke; + all_elements_to_revoke.insert(all_elements_to_revoke.end(), elements_to_revoke.begin(), elements_to_revoke.end()); + all_elements_to_revoke.insert(all_elements_to_revoke.end(), elements_to_revoke_after_grant.begin(), elements_to_revoke_after_grant.end()); + + if (current_user_access.hasGrantOption(all_elements_to_revoke)) { /// Simple case: the current user has the grant option for all the access rights specified for REVOKE. return; @@ -175,20 +189,20 @@ namespace need_check_grantees_are_allowed = false; /// already checked - if (!elements_to_revoke.empty() && elements_to_revoke[0].is_partial_revoke) - std::for_each(elements_to_revoke.begin(), elements_to_revoke.end(), [&](AccessRightsElement & element) { element.is_partial_revoke = false; }); + if (!all_elements_to_revoke.empty() && all_elements_to_revoke[0].is_partial_revoke) + std::for_each(all_elements_to_revoke.begin(), all_elements_to_revoke.end(), [&](AccessRightsElement & element) { element.is_partial_revoke = false; }); AccessRights access_to_revoke; - access_to_revoke.grant(elements_to_revoke); + access_to_revoke.grant(all_elements_to_revoke); access_to_revoke.makeIntersection(all_granted_access); /// Build more accurate list of elements to revoke, now we use an intersection of the initial list of elements to revoke /// and all the granted access rights to these grantees. - bool grant_option = !elements_to_revoke.empty() && elements_to_revoke[0].grant_option; - elements_to_revoke.clear(); + bool grant_option = !all_elements_to_revoke.empty() && all_elements_to_revoke[0].grant_option; + all_elements_to_revoke.clear(); for (auto & element_to_revoke : access_to_revoke.getElements()) { if (!element_to_revoke.is_partial_revoke && (element_to_revoke.grant_option || !grant_option)) - elements_to_revoke.emplace_back(std::move(element_to_revoke)); + all_elements_to_revoke.emplace_back(std::move(element_to_revoke)); } /// Additional check for REVOKE @@ -207,7 +221,7 @@ namespace return; /// Technically, this check always fails if `containsWithGrantOption` returns `false`. But we still call it to get a nice exception message. - current_user_access.checkGrantOption(elements_to_revoke); + current_user_access.checkGrantOption(all_elements_to_revoke); } /// Checks if the current user has enough roles granted with admin option to grant or revoke specified roles. @@ -278,10 +292,11 @@ namespace /// This function is less accurate than checkGrantOption() because it cannot use any information about /// access rights the grantees currently have (due to those grantees are located on multiple nodes, /// we just don't have the full information about them). - AccessRightsElements getRequiredAccessForExecutingOnCluster(const AccessRightsElements & elements_to_grant, const AccessRightsElements & elements_to_revoke) + AccessRightsElements getRequiredAccessForExecutingOnCluster(const AccessRightsElements & elements_to_grant, const AccessRightsElements & elements_to_revoke, const AccessRightsElements & elements_to_revoke_after_grant) { auto required_access = elements_to_grant; required_access.insert(required_access.end(), elements_to_revoke.begin(), elements_to_revoke.end()); + required_access.insert(required_access.end(), elements_to_revoke_after_grant.begin(), elements_to_revoke_after_grant.end()); std::for_each(required_access.begin(), required_access.end(), [&](AccessRightsElement & element) { element.grant_option = true; }); return required_access; } @@ -320,16 +335,23 @@ namespace T & grantee, const AccessRightsElements & elements_to_grant, const AccessRightsElements & elements_to_revoke, + const AccessRightsElements & elements_to_revoke_after_grant, const std::vector & roles_to_grant, const RolesOrUsersSet & roles_to_revoke, bool admin_option) { + /// Step 1: Pre-grant revoke (for REPLACE: revoke ALL, or standalone REVOKE). if (!elements_to_revoke.empty()) grantee.access.revoke(elements_to_revoke); + /// Step 2: Grant. if (!elements_to_grant.empty()) grantee.access.grant(elements_to_grant); + /// Step 3: Post-grant revoke (for combined GRANT ... REVOKE ... syntax → creates partial revokes). + if (!elements_to_revoke_after_grant.empty()) + grantee.access.revoke(elements_to_revoke_after_grant); + if (!roles_to_revoke.empty()) { if (admin_option) @@ -352,38 +374,44 @@ namespace IAccessEntity & grantee, const AccessRightsElements & elements_to_grant, const AccessRightsElements & elements_to_revoke, + const AccessRightsElements & elements_to_revoke_after_grant, const std::vector & roles_to_grant, const RolesOrUsersSet & roles_to_revoke, bool admin_option) { if (auto * user = typeid_cast(&grantee)) - updateGrantedAccessRightsAndRolesTemplate(*user, elements_to_grant, elements_to_revoke, roles_to_grant, roles_to_revoke, admin_option); + updateGrantedAccessRightsAndRolesTemplate(*user, elements_to_grant, elements_to_revoke, elements_to_revoke_after_grant, roles_to_grant, roles_to_revoke, admin_option); else if (auto * role = typeid_cast(&grantee)) - updateGrantedAccessRightsAndRolesTemplate(*role, elements_to_grant, elements_to_revoke, roles_to_grant, roles_to_revoke, admin_option); + updateGrantedAccessRightsAndRolesTemplate(*role, elements_to_grant, elements_to_revoke, elements_to_revoke_after_grant, roles_to_grant, roles_to_revoke, admin_option); } template void grantCurrentGrantsTemplate( T & grantee, const AccessRights & rights_to_grant, - const AccessRightsElements & elements_to_revoke) + const AccessRightsElements & elements_to_revoke, + const AccessRightsElements & elements_to_revoke_after_grant) { if (!elements_to_revoke.empty()) grantee.access.revoke(elements_to_revoke); grantee.access.makeUnion(rights_to_grant); + + if (!elements_to_revoke_after_grant.empty()) + grantee.access.revoke(elements_to_revoke_after_grant); } /// Grants current user's grants with grant options to specified user. void grantCurrentGrants( IAccessEntity & grantee, const AccessRights & new_rights, - const AccessRightsElements & elements_to_revoke) + const AccessRightsElements & elements_to_revoke, + const AccessRightsElements & elements_to_revoke_after_grant) { if (auto * user = typeid_cast(&grantee)) - grantCurrentGrantsTemplate(*user, new_rights, elements_to_revoke); + grantCurrentGrantsTemplate(*user, new_rights, elements_to_revoke, elements_to_revoke_after_grant); else if (auto * role = typeid_cast(&grantee)) - grantCurrentGrantsTemplate(*role, new_rights, elements_to_revoke); + grantCurrentGrantsTemplate(*role, new_rights, elements_to_revoke, elements_to_revoke_after_grant); } /// Calculates all available rights to grant with current user intersection. @@ -415,13 +443,14 @@ namespace { AccessRightsElements elements_to_grant; AccessRightsElements elements_to_revoke; - collectAccessRightsElementsToGrantOrRevoke(query, elements_to_grant, elements_to_revoke); + AccessRightsElements elements_to_revoke_after_grant; + collectAccessRightsElementsToGrantOrRevoke(query, elements_to_grant, elements_to_revoke, elements_to_revoke_after_grant); std::vector roles_to_grant; RolesOrUsersSet roles_to_revoke; collectRolesToGrantOrRevoke(query, roles_to_grant, roles_to_revoke); - updateGrantedAccessRightsAndRoles(grantee, elements_to_grant, elements_to_revoke, roles_to_grant, roles_to_revoke, query.admin_option); + updateGrantedAccessRightsAndRoles(grantee, elements_to_grant, elements_to_revoke, elements_to_revoke_after_grant, roles_to_grant, roles_to_revoke, query.admin_option); } } @@ -433,11 +462,14 @@ BlockIO InterpreterGrantQuery::execute() query.replaceCurrentUserTag(getContext()->getUserName()); query.access_rights_elements.eraseNotGrantable(); + query.access_rights_elements_to_revoke.eraseNotGrantable(); if (!query.access_rights_elements.sameOptions()) throw Exception(ErrorCodes::LOGICAL_ERROR, "Elements of an ASTGrantQuery are expected to have the same options"); if (!query.access_rights_elements.empty() && query.access_rights_elements[0].is_partial_revoke && !query.is_revoke) throw Exception(ErrorCodes::LOGICAL_ERROR, "A partial revoke should be revoked, not granted"); + if (!query.access_rights_elements_to_revoke.sameOptions()) + throw Exception(ErrorCodes::LOGICAL_ERROR, "Revoke elements of an ASTGrantQuery are expected to have the same options"); auto & access_control = getContext()->getAccessControl(); auto current_user_access = getContext()->getAccess(); @@ -447,7 +479,8 @@ BlockIO InterpreterGrantQuery::execute() /// Collect access rights and roles we're going to grant or revoke. AccessRightsElements elements_to_grant; AccessRightsElements elements_to_revoke; - collectAccessRightsElementsToGrantOrRevoke(query, elements_to_grant, elements_to_revoke); + AccessRightsElements elements_to_revoke_after_grant; + collectAccessRightsElementsToGrantOrRevoke(query, elements_to_grant, elements_to_revoke, elements_to_revoke_after_grant); std::vector roles_to_grant; RolesOrUsersSet roles_to_revoke; @@ -457,7 +490,9 @@ BlockIO InterpreterGrantQuery::execute() String current_database = getContext()->getCurrentDatabase(); elements_to_grant.replaceEmptyDatabase(current_database); elements_to_revoke.replaceEmptyDatabase(current_database); + elements_to_revoke_after_grant.replaceEmptyDatabase(current_database); query.access_rights_elements.replaceEmptyDatabase(current_database); + query.access_rights_elements_to_revoke.replaceEmptyDatabase(current_database); /// Executing on cluster. if (!query.cluster.empty()) @@ -465,7 +500,7 @@ BlockIO InterpreterGrantQuery::execute() if (query.current_grants) throw Exception(ErrorCodes::BAD_ARGUMENTS, "GRANT CURRENT GRANTS can't be executed on cluster."); - auto required_access = getRequiredAccessForExecutingOnCluster(elements_to_grant, elements_to_revoke); + auto required_access = getRequiredAccessForExecutingOnCluster(elements_to_grant, elements_to_revoke, elements_to_revoke_after_grant); checkAdminOptionForExecutingOnCluster(*current_user_access, roles_to_grant, roles_to_revoke); current_user_access->checkGranteesAreAllowed(grantees); DDLQueryOnClusterParams params; @@ -476,7 +511,7 @@ BlockIO InterpreterGrantQuery::execute() /// Check if the current user has corresponding access rights granted with grant option. bool need_check_grantees_are_allowed = true; if (!query.current_grants) - checkGrantOption(access_control, *current_user_access, grantees, need_check_grantees_are_allowed, elements_to_grant, elements_to_revoke); + checkGrantOption(access_control, *current_user_access, grantees, need_check_grantees_are_allowed, elements_to_grant, elements_to_revoke, elements_to_revoke_after_grant); /// Check if the current user has corresponding roles granted with admin option. checkAdminOption(access_control, *current_user_access, grantees, need_check_grantees_are_allowed, roles_to_grant, roles_to_revoke, query.admin_option); @@ -543,9 +578,9 @@ BlockIO InterpreterGrantQuery::execute() current_user_access->checkAccess(AccessType::PROTECTED_ACCESS_MANAGEMENT); auto clone = entity->clone(); if (query.current_grants) - grantCurrentGrants(*clone, new_rights, elements_to_revoke); + grantCurrentGrants(*clone, new_rights, elements_to_revoke, elements_to_revoke_after_grant); else - updateGrantedAccessRightsAndRoles(*clone, elements_to_grant, elements_to_revoke, roles_to_grant, roles_to_revoke, query.admin_option); + updateGrantedAccessRightsAndRoles(*clone, elements_to_grant, elements_to_revoke, elements_to_revoke_after_grant, roles_to_grant, roles_to_revoke, query.admin_option); return clone; }; diff --git a/src/Interpreters/Access/InterpreterShowGrantsQuery.cpp b/src/Interpreters/Access/InterpreterShowGrantsQuery.cpp index 195295ead1f0..55cd10f6facc 100644 --- a/src/Interpreters/Access/InterpreterShowGrantsQuery.cpp +++ b/src/Interpreters/Access/InterpreterShowGrantsQuery.cpp @@ -28,6 +28,60 @@ namespace ErrorCodes namespace { + /// Merges standalone REVOKE ASTGrantQuery nodes (partial revokes) into a preceding + /// GRANT ASTGrantQuery node's access_rights_elements_to_revoke field, producing + /// the combined "GRANT ... REVOKE ... TO ..." syntax. + void mergeRevokeIntoGrant(ASTs & grant_queries) + { + /// Find the last GRANT node for each grant_option value (false / true). + /// Partial revokes that share the same grant_option are merged into it. + std::shared_ptr last_grant_no_option; + std::shared_ptr last_grant_with_option; + + ASTs merged; + merged.reserve(grant_queries.size()); + + for (auto & ast : grant_queries) + { + auto & query = ast->as(); + + if (!query.is_revoke) + { + /// It's a GRANT node — track it as a merge target. + bool go = !query.access_rights_elements.empty() && query.access_rights_elements[0].grant_option; + if (go) + last_grant_with_option = std::static_pointer_cast(ast); + else + last_grant_no_option = std::static_pointer_cast(ast); + merged.push_back(ast); + } + else + { + /// It's a REVOKE node (partial revoke). Try to merge into a preceding GRANT. + bool go = !query.access_rights_elements.empty() && query.access_rights_elements[0].grant_option; + auto & target = go ? last_grant_with_option : last_grant_no_option; + + if (target) + { + /// Merge: move the revoke elements into the GRANT node's revoke list. + for (auto & elem : query.access_rights_elements) + { + elem.is_partial_revoke = false; /// Clear the flag — it's now an explicit revoke in the combined syntax + target->access_rights_elements_to_revoke.emplace_back(std::move(elem)); + } + /// Don't add this REVOKE node to merged — it's absorbed. + } + else + { + /// No preceding GRANT to merge into — keep the standalone REVOKE. + merged.push_back(ast); + } + } + } + + grant_queries = std::move(merged); + } + void getGrantsFromAccess( ASTs & res, const AccessRights & access, @@ -69,6 +123,12 @@ namespace current_query->access_rights_elements.emplace_back(std::move(element)); } + + /// Post-processing: merge partial-revoke REVOKE nodes into preceding GRANT nodes + /// to produce combined "GRANT ... REVOKE ... TO ..." output. + /// The elements are sorted: grants before partial-revokes (for the same full_name), + /// and alphabetically by full_name. So a GRANT ON *.* always precedes REVOKE ON system.*. + mergeRevokeIntoGrant(res); } template diff --git a/src/Parsers/Access/ASTGrantQuery.cpp b/src/Parsers/Access/ASTGrantQuery.cpp index 5ce5fb9a3840..e16a77a16e68 100644 --- a/src/Parsers/Access/ASTGrantQuery.cpp +++ b/src/Parsers/Access/ASTGrantQuery.cpp @@ -51,6 +51,8 @@ void ASTGrantQuery::formatImpl(WriteBuffer & ostr, const FormatSettings & settin throw Exception(ErrorCodes::LOGICAL_ERROR, "Elements of an ASTGrantQuery are expected to have the same options"); if (!access_rights_elements.empty() && access_rights_elements[0].is_partial_revoke && !is_revoke) throw Exception(ErrorCodes::LOGICAL_ERROR, "A partial revoke should be revoked, not granted"); + if (!access_rights_elements_to_revoke.empty() && !access_rights_elements_to_revoke.sameOptions()) + throw Exception(ErrorCodes::LOGICAL_ERROR, "Revoke elements of an ASTGrantQuery are expected to have the same options"); bool grant_option = !access_rights_elements.empty() && access_rights_elements[0].grant_option; formatOnCluster(ostr, settings); @@ -82,6 +84,13 @@ void ASTGrantQuery::formatImpl(WriteBuffer & ostr, const FormatSettings & settin access_rights_elements.formatElementsWithoutOptions(ostr, settings.hilite); } + /// Format the embedded REVOKE clause (combined GRANT ... REVOKE ... TO ... syntax) + if (!access_rights_elements_to_revoke.empty()) + { + ostr << " " << (settings.hilite ? hilite_keyword : "") << "REVOKE" << (settings.hilite ? hilite_none : "") << " "; + access_rights_elements_to_revoke.formatElementsWithoutOptions(ostr, settings.hilite); + } + ostr << (settings.hilite ? IAST::hilite_keyword : "") << (is_revoke ? " FROM " : " TO ") << (settings.hilite ? IAST::hilite_none : ""); grantees->format(ostr, settings); @@ -102,6 +111,7 @@ void ASTGrantQuery::formatImpl(WriteBuffer & ostr, const FormatSettings & settin void ASTGrantQuery::replaceEmptyDatabase(const String & current_database) { access_rights_elements.replaceEmptyDatabase(current_database); + access_rights_elements_to_revoke.replaceEmptyDatabase(current_database); } diff --git a/src/Parsers/Access/ASTGrantQuery.h b/src/Parsers/Access/ASTGrantQuery.h index 3dcd2bfbed52..12ed2b3971e0 100644 --- a/src/Parsers/Access/ASTGrantQuery.h +++ b/src/Parsers/Access/ASTGrantQuery.h @@ -10,7 +10,7 @@ namespace DB class ASTRolesOrUsersSet; -/** GRANT access_type[(column_name [,...])] [,...] ON {db.table|db.*|*.*|table|*} TO {user_name | CURRENT_USER} [,...] [WITH GRANT OPTION] +/** GRANT access_type[(column_name [,...])] [,...] ON {db.table|db.*|*.*|table|*} [REVOKE access_type[(column_name [,...])] [,...] ON {db.table|db.*|*.*|table|*}] TO {user_name | CURRENT_USER} [,...] [WITH GRANT OPTION] * REVOKE access_type[(column_name [,...])] [,...] ON {db.table|db.*|*.*|table|*} FROM {user_name | CURRENT_USER} [,...] | ALL | ALL EXCEPT {user_name | CURRENT_USER} [,...] * * GRANT role [,...] TO {user_name | role_name | CURRENT_USER} [,...] [WITH ADMIN OPTION] @@ -22,6 +22,7 @@ class ASTGrantQuery : public IAST, public ASTQueryWithOnCluster bool attach_mode = false; bool is_revoke = false; AccessRightsElements access_rights_elements; + AccessRightsElements access_rights_elements_to_revoke; /// For combined GRANT ... REVOKE ... TO ... syntax std::shared_ptr roles; bool admin_option = false; bool replace_access = false; diff --git a/src/Parsers/Access/ParserGrantQuery.cpp b/src/Parsers/Access/ParserGrantQuery.cpp index 6782efbf7dd4..76a25f7432b4 100644 --- a/src/Parsers/Access/ParserGrantQuery.cpp +++ b/src/Parsers/Access/ParserGrantQuery.cpp @@ -174,6 +174,14 @@ bool ParserGrantQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) return false; } + /// Parse optional REVOKE clause inside a GRANT statement (combined GRANT ... REVOKE ... TO ... syntax) + AccessRightsElements elements_to_revoke; + if (!is_revoke && ParserKeyword{Keyword::REVOKE}.ignore(pos, expected)) + { + if (!parseAccessRightsElementsWithoutOptions(pos, expected, elements_to_revoke)) + return false; + } + if (cluster.empty()) parseOnCluster(pos, expected, cluster); @@ -216,6 +224,8 @@ bool ParserGrantQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) { for (auto & element : elements) element.grant_option = true; + for (auto & element : elements_to_revoke) + element.grant_option = true; } @@ -239,6 +249,7 @@ bool ParserGrantQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected) query->attach_mode = attach_mode; query->cluster = std::move(cluster); query->access_rights_elements = std::move(elements); + query->access_rights_elements_to_revoke = std::move(elements_to_revoke); query->roles = std::move(roles); query->grantees = std::move(grantees); query->admin_option = admin_option; diff --git a/tests/queries/0_stateless/03637_combined_grant_revoke.reference b/tests/queries/0_stateless/03637_combined_grant_revoke.reference new file mode 100644 index 000000000000..2cf0c8a2b1a6 --- /dev/null +++ b/tests/queries/0_stateless/03637_combined_grant_revoke.reference @@ -0,0 +1,17 @@ +--- parser --- +GRANT SELECT ON *.* REVOKE SELECT ON system.* TO u +GRANT ALL ON *.* REVOKE ALL ON system.*, ALL ON information_schema.* TO u +--- table exclusion --- +1 +3 +OK +--- show grants --- +GRANT SELECT ON test_db.* REVOKE SELECT ON test_db.t2 TO user1 +--- db exclusion --- +OK +GRANT SELECT ON *.* REVOKE SELECT ON test_db.* TO user1 +--- grant option --- +1 +OK +--- equivalence --- +EQUIVALENT diff --git a/tests/queries/0_stateless/03637_combined_grant_revoke.sh b/tests/queries/0_stateless/03637_combined_grant_revoke.sh new file mode 100755 index 000000000000..6308950a012e --- /dev/null +++ b/tests/queries/0_stateless/03637_combined_grant_revoke.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +# Tags: no-parallel + +CURDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=../shell_config.sh +. "$CURDIR"/../shell_config.sh + +set -e + +db=$CLICKHOUSE_DATABASE +user1="user03637_1_${CLICKHOUSE_TEST_UNIQUE_NAME}" +user2="user03637_2_${CLICKHOUSE_TEST_UNIQUE_NAME}" + +function cleanup() +{ + ${CLICKHOUSE_CLIENT} -mq " + DROP USER IF EXISTS $user1, $user2; + DROP TABLE IF EXISTS ${db}.t1; + DROP TABLE IF EXISTS ${db}.t2; + DROP TABLE IF EXISTS ${db}.t3; + " +} +cleanup +trap cleanup EXIT + +${CLICKHOUSE_CLIENT} -mq " + CREATE TABLE ${db}.t1 (x UInt32) ENGINE = MergeTree ORDER BY x; + CREATE TABLE ${db}.t2 (x UInt32) ENGINE = MergeTree ORDER BY x; + CREATE TABLE ${db}.t3 (x UInt32) ENGINE = MergeTree ORDER BY x; + INSERT INTO ${db}.t1 VALUES (1); + INSERT INTO ${db}.t2 VALUES (2); + INSERT INTO ${db}.t3 VALUES (3); + CREATE USER $user1, $user2; +" + +# -- Parser round-trip -- +echo "--- parser ---" +echo "GRANT SELECT ON *.* REVOKE SELECT ON system.* TO u" | $CLICKHOUSE_FORMAT --oneline +echo "GRANT ALL ON *.* REVOKE ALL ON system.*, ALL ON information_schema.* TO u" | $CLICKHOUSE_FORMAT --oneline + +# -- Table-level partial revoke via combined syntax -- +echo "--- table exclusion ---" +${CLICKHOUSE_CLIENT} --query "GRANT SELECT ON ${db}.* REVOKE SELECT ON ${db}.t2 TO $user1" + +${CLICKHOUSE_CLIENT} --user $user1 --query "SELECT * FROM ${db}.t1" +${CLICKHOUSE_CLIENT} --user $user1 --query "SELECT * FROM ${db}.t3" +(( $(${CLICKHOUSE_CLIENT} --user $user1 --query "SELECT * FROM ${db}.t2" 2>&1 | grep -c "Not enough privileges") >= 1 )) && echo "OK" || echo "UNEXPECTED" + +# -- SHOW GRANTS displays combined format -- +echo "--- show grants ---" +${CLICKHOUSE_CLIENT} --query "SHOW GRANTS FOR $user1" | sed "s/$user1/user1/g" | sed "s/${db}/test_db/g" + +# -- Database-level partial revoke via combined syntax -- +echo "--- db exclusion ---" +${CLICKHOUSE_CLIENT} -mq " + REVOKE ALL ON *.* FROM $user1; + GRANT SELECT ON *.* REVOKE SELECT ON ${db}.* TO $user1; +" +(( $(${CLICKHOUSE_CLIENT} --user $user1 --query "SELECT * FROM ${db}.t1" 2>&1 | grep -c "Not enough privileges") >= 1 )) && echo "OK" || echo "UNEXPECTED" + +${CLICKHOUSE_CLIENT} --query "SHOW GRANTS FOR $user1" | sed "s/$user1/user1/g" | sed "s/${db}/test_db/g" + +# -- Combined with GRANT OPTION -- +echo "--- grant option ---" +${CLICKHOUSE_CLIENT} -mq " + REVOKE ALL ON *.* FROM $user1, $user2; + GRANT SELECT ON ${db}.* REVOKE SELECT ON ${db}.t2 TO $user2 WITH GRANT OPTION; +" + +# user2 can re-grant what they have (t1, t3) +${CLICKHOUSE_CLIENT} --user $user2 --query "GRANT SELECT ON ${db}.t1 TO $user1" +${CLICKHOUSE_CLIENT} --user $user1 --query "SELECT * FROM ${db}.t1" + +# user2 cannot grant the revoked table (t2) +(( $(${CLICKHOUSE_CLIENT} --user $user2 --query "GRANT SELECT ON ${db}.t2 TO $user1" 2>&1 | grep -c "Not enough privileges") >= 1 )) && echo "OK" || echo "UNEXPECTED" + +# -- Combined syntax is equivalent to separate GRANT + REVOKE -- +echo "--- equivalence ---" +${CLICKHOUSE_CLIENT} -mq " + REVOKE ALL ON *.* FROM $user1, $user2; + GRANT SELECT ON ${db}.* REVOKE SELECT ON ${db}.t2 TO $user1; + GRANT SELECT ON ${db}.* TO $user2; + REVOKE SELECT ON ${db}.t2 FROM $user2; +" + +grants1=$(${CLICKHOUSE_CLIENT} --query "SHOW GRANTS FOR $user1" | sed "s/$user1/userX/g" | sed "s/${db}/test_db/g") +grants2=$(${CLICKHOUSE_CLIENT} --query "SHOW GRANTS FOR $user2" | sed "s/$user2/userX/g" | sed "s/${db}/test_db/g") + +if [[ "$grants1" == "$grants2" ]]; then + echo "EQUIVALENT" +else + echo "NOT EQUIVALENT" + echo "user1: $grants1" + echo "user2: $grants2" +fi