mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-22 07:31:57 +00:00
Improve REVOKE command: now it requires only grant/admin option for only
access which will be revoked. REVOKE ALL FROM user1 now revokes all granted roles.
This commit is contained in:
parent
c39eb8f71b
commit
03b36c262e
@ -426,12 +426,17 @@ public:
|
||||
|
||||
friend bool operator!=(const Node & left, const Node & right) { return !(left == right); }
|
||||
|
||||
void merge(const Node & other, const Helper & helper)
|
||||
void makeUnion(const Node & other, const Helper & helper)
|
||||
{
|
||||
mergeAccessRec(other);
|
||||
makeUnionRec(other);
|
||||
calculateFinalAccessRec(helper);
|
||||
}
|
||||
|
||||
void makeIntersection(const Node & other, const Helper & helper)
|
||||
{
|
||||
makeIntersectionRec(other);
|
||||
calculateFinalAccessRec(helper);
|
||||
}
|
||||
|
||||
ProtoElements getElements() const
|
||||
{
|
||||
@ -723,12 +728,12 @@ private:
|
||||
max_access = final_access | max_access_among_children;
|
||||
}
|
||||
|
||||
void mergeAccessRec(const Node & rhs)
|
||||
void makeUnionRec(const Node & rhs)
|
||||
{
|
||||
if (rhs.children)
|
||||
{
|
||||
for (const auto & [rhs_childname, rhs_child] : *rhs.children)
|
||||
getChild(rhs_childname).mergeAccessRec(rhs_child);
|
||||
getChild(rhs_childname).makeUnionRec(rhs_child);
|
||||
}
|
||||
access |= rhs.access;
|
||||
if (children)
|
||||
@ -740,6 +745,24 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void makeIntersectionRec(const Node & rhs)
|
||||
{
|
||||
if (rhs.children)
|
||||
{
|
||||
for (const auto & [rhs_childname, rhs_child] : *rhs.children)
|
||||
getChild(rhs_childname).makeIntersectionRec(rhs_child);
|
||||
}
|
||||
access &= rhs.access;
|
||||
if (children)
|
||||
{
|
||||
for (auto & [lhs_childname, lhs_child] : *children)
|
||||
{
|
||||
if (!rhs.tryGetChild(lhs_childname))
|
||||
lhs_child.access &= rhs.access;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -989,7 +1012,7 @@ bool operator ==(const AccessRights & left, const AccessRights & right)
|
||||
}
|
||||
|
||||
|
||||
void AccessRights::merge(const AccessRights & other)
|
||||
void AccessRights::makeUnion(const AccessRights & other)
|
||||
{
|
||||
auto helper = [](std::unique_ptr<Node> & root_node, const std::unique_ptr<Node> & other_root_node)
|
||||
{
|
||||
@ -1001,7 +1024,29 @@ void AccessRights::merge(const AccessRights & other)
|
||||
}
|
||||
if (other_root_node)
|
||||
{
|
||||
root_node->merge(*other_root_node, Helper::instance());
|
||||
root_node->makeUnion(*other_root_node, Helper::instance());
|
||||
if (!root_node->access && !root_node->children)
|
||||
root_node = nullptr;
|
||||
}
|
||||
};
|
||||
helper(root, other.root);
|
||||
helper(root_with_grant_option, other.root_with_grant_option);
|
||||
}
|
||||
|
||||
|
||||
void AccessRights::makeIntersection(const AccessRights & other)
|
||||
{
|
||||
auto helper = [](std::unique_ptr<Node> & root_node, const std::unique_ptr<Node> & other_root_node)
|
||||
{
|
||||
if (!root_node)
|
||||
{
|
||||
if (other_root_node)
|
||||
root_node = std::make_unique<Node>(*other_root_node);
|
||||
return;
|
||||
}
|
||||
if (other_root_node)
|
||||
{
|
||||
root_node->makeIntersection(*other_root_node, Helper::instance());
|
||||
if (!root_node->access && !root_node->children)
|
||||
root_node = nullptr;
|
||||
}
|
||||
|
@ -93,7 +93,9 @@ public:
|
||||
|
||||
/// Merges two sets of access rights together.
|
||||
/// It's used to combine access rights from multiple roles.
|
||||
void merge(const AccessRights & other);
|
||||
void makeUnion(const AccessRights & other);
|
||||
|
||||
void makeIntersection(const AccessRights & other);
|
||||
|
||||
friend bool operator ==(const AccessRights & left, const AccessRights & right);
|
||||
friend bool operator !=(const AccessRights & left, const AccessRights & right) { return !(left == right); }
|
||||
|
@ -30,6 +30,73 @@ namespace ErrorCodes
|
||||
extern const int UNKNOWN_USER;
|
||||
}
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
std::shared_ptr<AccessRights> mixAccessRightsFromUserAndRoles(const User & user, const EnabledRolesInfo & roles_info)
|
||||
{
|
||||
auto res = std::make_shared<AccessRights>(user.access);
|
||||
res->makeUnion(roles_info.access);
|
||||
return res;
|
||||
}
|
||||
|
||||
std::shared_ptr<AccessRights> applyParamsToAccessRights(const AccessRights & access, const ContextAccessParams & params)
|
||||
{
|
||||
auto res = std::make_shared<AccessRights>(access);
|
||||
|
||||
static const AccessFlags table_ddl = AccessType::CREATE_DATABASE | AccessType::CREATE_TABLE | AccessType::CREATE_VIEW
|
||||
| AccessType::ALTER_TABLE | AccessType::ALTER_VIEW | AccessType::DROP_DATABASE | AccessType::DROP_TABLE | AccessType::DROP_VIEW
|
||||
| AccessType::TRUNCATE;
|
||||
|
||||
static const AccessFlags dictionary_ddl = AccessType::CREATE_DICTIONARY | AccessType::DROP_DICTIONARY;
|
||||
static const AccessFlags table_and_dictionary_ddl = table_ddl | dictionary_ddl;
|
||||
static const AccessFlags write_table_access = AccessType::INSERT | AccessType::OPTIMIZE;
|
||||
static const AccessFlags write_dcl_access = AccessType::ACCESS_MANAGEMENT - AccessType::SHOW_ACCESS;
|
||||
|
||||
if (params.readonly)
|
||||
res->revoke(write_table_access | table_and_dictionary_ddl | write_dcl_access | AccessType::SYSTEM | AccessType::KILL_QUERY);
|
||||
|
||||
if (params.readonly == 1)
|
||||
{
|
||||
/// Table functions are forbidden in readonly mode.
|
||||
/// For example, for readonly = 2 - allowed.
|
||||
res->revoke(AccessType::CREATE_TEMPORARY_TABLE);
|
||||
}
|
||||
|
||||
if (!params.allow_ddl)
|
||||
res->revoke(table_and_dictionary_ddl);
|
||||
|
||||
if (!params.allow_introspection)
|
||||
res->revoke(AccessType::INTROSPECTION);
|
||||
|
||||
/// Anyone has access to the "system" database.
|
||||
res->grant(AccessType::SELECT, DatabaseCatalog::SYSTEM_DATABASE);
|
||||
|
||||
if (params.readonly != 1)
|
||||
{
|
||||
/// User has access to temporary or external table if such table was resolved in session or query context
|
||||
res->grant(AccessFlags::allTableFlags() | AccessFlags::allColumnFlags(), DatabaseCatalog::TEMPORARY_DATABASE);
|
||||
}
|
||||
|
||||
if (params.readonly)
|
||||
{
|
||||
/// No grant option in readonly mode.
|
||||
res->revokeGrantOption(AccessType::ALL);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
std::array<UUID, 1> to_array(const UUID & id)
|
||||
{
|
||||
std::array<UUID, 1> ids;
|
||||
ids[0] = id;
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ContextAccess::ContextAccess(const AccessControlManager & manager_, const Params & params_)
|
||||
: manager(&manager_)
|
||||
, params(params_)
|
||||
@ -54,6 +121,10 @@ void ContextAccess::setUser(const UserPtr & user_) const
|
||||
/// User has been dropped.
|
||||
auto nothing_granted = std::make_shared<AccessRights>();
|
||||
access = nothing_granted;
|
||||
access_without_readonly = nothing_granted;
|
||||
access_with_allow_ddl = nothing_granted;
|
||||
access_with_allow_introspection = nothing_granted;
|
||||
access_from_user_and_roles = nothing_granted;
|
||||
subscription_for_user_change = {};
|
||||
subscription_for_roles_changes = {};
|
||||
enabled_roles = nullptr;
|
||||
@ -108,56 +179,18 @@ void ContextAccess::setRolesInfo(const std::shared_ptr<const EnabledRolesInfo> &
|
||||
enabled_row_policies = manager->getEnabledRowPolicies(*params.user_id, roles_info->enabled_roles);
|
||||
enabled_quota = manager->getEnabledQuota(*params.user_id, user_name, roles_info->enabled_roles, params.address, params.quota_key);
|
||||
enabled_settings = manager->getEnabledSettings(*params.user_id, user->settings, roles_info->enabled_roles, roles_info->settings_from_enabled_roles);
|
||||
setFinalAccess();
|
||||
calculateAccessRights();
|
||||
}
|
||||
|
||||
|
||||
void ContextAccess::setFinalAccess() const
|
||||
void ContextAccess::calculateAccessRights() const
|
||||
{
|
||||
auto final_access = std::make_shared<AccessRights>();
|
||||
*final_access = user->access;
|
||||
if (roles_info)
|
||||
final_access->merge(roles_info->access);
|
||||
access_from_user_and_roles = mixAccessRightsFromUserAndRoles(*user, *roles_info);
|
||||
access = applyParamsToAccessRights(*access_from_user_and_roles, params);
|
||||
|
||||
static const AccessFlags table_ddl = AccessType::CREATE_DATABASE | AccessType::CREATE_TABLE | AccessType::CREATE_VIEW
|
||||
| AccessType::ALTER_TABLE | AccessType::ALTER_VIEW | AccessType::DROP_DATABASE | AccessType::DROP_TABLE | AccessType::DROP_VIEW
|
||||
| AccessType::TRUNCATE;
|
||||
|
||||
static const AccessFlags dictionary_ddl = AccessType::CREATE_DICTIONARY | AccessType::DROP_DICTIONARY;
|
||||
static const AccessFlags table_and_dictionary_ddl = table_ddl | dictionary_ddl;
|
||||
static const AccessFlags write_table_access = AccessType::INSERT | AccessType::OPTIMIZE;
|
||||
static const AccessFlags write_dcl_access = AccessType::ACCESS_MANAGEMENT - AccessType::SHOW_ACCESS;
|
||||
|
||||
if (params.readonly)
|
||||
final_access->revoke(write_table_access | table_and_dictionary_ddl | write_dcl_access | AccessType::SYSTEM | AccessType::KILL_QUERY);
|
||||
|
||||
if (params.readonly == 1)
|
||||
{
|
||||
/// Table functions are forbidden in readonly mode.
|
||||
/// For example, for readonly = 2 - allowed.
|
||||
final_access->revoke(AccessType::CREATE_TEMPORARY_TABLE);
|
||||
}
|
||||
|
||||
if (!params.allow_ddl)
|
||||
final_access->revoke(table_and_dictionary_ddl);
|
||||
|
||||
if (!params.allow_introspection)
|
||||
final_access->revoke(AccessType::INTROSPECTION);
|
||||
|
||||
/// Anyone has access to the "system" database.
|
||||
final_access->grant(AccessType::SELECT, DatabaseCatalog::SYSTEM_DATABASE);
|
||||
|
||||
if (params.readonly != 1)
|
||||
{
|
||||
/// User has access to temporary or external table if such table was resolved in session or query context
|
||||
final_access->grant(AccessFlags::allTableFlags() | AccessFlags::allColumnFlags(), DatabaseCatalog::TEMPORARY_DATABASE);
|
||||
}
|
||||
|
||||
if (params.readonly)
|
||||
{
|
||||
/// No grant option in readonly mode.
|
||||
final_access->revokeGrantOption(AccessType::ALL);
|
||||
}
|
||||
access_without_readonly = nullptr;
|
||||
access_with_allow_ddl = nullptr;
|
||||
access_with_allow_introspection = nullptr;
|
||||
|
||||
if (trace_log)
|
||||
{
|
||||
@ -168,10 +201,8 @@ void ContextAccess::setFinalAccess() const
|
||||
boost::algorithm::join(roles_info->getEnabledRolesNames(), ", "));
|
||||
}
|
||||
LOG_TRACE(trace_log, "Settings: readonly={}, allow_ddl={}, allow_introspection_functions={}", params.readonly, params.allow_ddl, params.allow_introspection);
|
||||
LOG_TRACE(trace_log, "List of all grants: {}", final_access->toString());
|
||||
LOG_TRACE(trace_log, "List of all grants: {}", access->toString());
|
||||
}
|
||||
|
||||
access = final_access;
|
||||
}
|
||||
|
||||
|
||||
@ -361,9 +392,7 @@ void ContextAccess::checkAccessImpl2(const AccessFlags & flags, const Args &...
|
||||
std::lock_guard lock{mutex};
|
||||
|
||||
if (!user)
|
||||
{
|
||||
show_error("User has been dropped", ErrorCodes::UNKNOWN_USER);
|
||||
}
|
||||
|
||||
if (grant_option && access->isGranted(flags, args...))
|
||||
{
|
||||
@ -381,7 +410,7 @@ void ContextAccess::checkAccessImpl2(const AccessFlags & flags, const Args &...
|
||||
{
|
||||
Params changed_params = params;
|
||||
changed_params.readonly = 0;
|
||||
access_without_readonly = manager->getContextAccess(changed_params);
|
||||
access_without_readonly = applyParamsToAccessRights(*access_from_user_and_roles, changed_params);
|
||||
}
|
||||
|
||||
if (access_without_readonly->isGranted(flags, args...))
|
||||
@ -402,7 +431,7 @@ void ContextAccess::checkAccessImpl2(const AccessFlags & flags, const Args &...
|
||||
{
|
||||
Params changed_params = params;
|
||||
changed_params.allow_ddl = true;
|
||||
access_with_allow_ddl = manager->getContextAccess(changed_params);
|
||||
access_with_allow_ddl = applyParamsToAccessRights(*access_from_user_and_roles, changed_params);
|
||||
}
|
||||
|
||||
if (access_with_allow_ddl->isGranted(flags, args...))
|
||||
@ -417,7 +446,7 @@ void ContextAccess::checkAccessImpl2(const AccessFlags & flags, const Args &...
|
||||
{
|
||||
Params changed_params = params;
|
||||
changed_params.allow_introspection = true;
|
||||
access_with_allow_introspection = manager->getContextAccess(changed_params);
|
||||
access_with_allow_introspection = applyParamsToAccessRights(*access_from_user_and_roles, changed_params);
|
||||
}
|
||||
|
||||
if (access_with_allow_introspection->isGranted(flags, args...))
|
||||
@ -483,25 +512,65 @@ void ContextAccess::checkGrantOption(const AccessRightsElement & element) const
|
||||
void ContextAccess::checkGrantOption(const AccessRightsElements & elements) const { checkAccessImpl<true>(elements); }
|
||||
|
||||
|
||||
void ContextAccess::checkAdminOption(const UUID & role_id) const
|
||||
template <typename Container, typename GetNameFunction>
|
||||
void ContextAccess::checkAdminOptionImpl(const Container & role_ids, const GetNameFunction & get_name_function) const
|
||||
{
|
||||
if (isGranted(AccessType::ROLE_ADMIN))
|
||||
return;
|
||||
|
||||
auto info = getRolesInfo();
|
||||
if (info && info->enabled_roles_with_admin_option.count(role_id))
|
||||
if (!info)
|
||||
{
|
||||
if (!user)
|
||||
throw Exception(user_name + ": User has been dropped", ErrorCodes::UNKNOWN_USER);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user)
|
||||
throw Exception(user_name + ": User has been dropped", ErrorCodes::UNKNOWN_USER);
|
||||
size_t i = 0;
|
||||
for (auto it = std::begin(role_ids); it != std::end(role_ids); ++it, ++i)
|
||||
{
|
||||
const UUID & role_id = *it;
|
||||
if (info->enabled_roles_with_admin_option.count(role_id))
|
||||
continue;
|
||||
|
||||
std::optional<String> role_name = manager->readName(role_id);
|
||||
if (!role_name)
|
||||
role_name = "ID {" + toString(role_id) + "}";
|
||||
throw Exception(
|
||||
getUserName() + ": Not enough privileges. To execute this query it's necessary to have the grant " + backQuoteIfNeed(*role_name)
|
||||
+ " WITH ADMIN OPTION ",
|
||||
ErrorCodes::ACCESS_DENIED);
|
||||
auto role_name = get_name_function(role_id, i);
|
||||
if (!role_name)
|
||||
role_name = "ID {" + toString(role_id) + "}";
|
||||
String msg = "To execute this query it's necessary to have the role " + backQuoteIfNeed(*role_name) + " granted with ADMIN option";
|
||||
if (info->enabled_roles.count(role_id))
|
||||
msg = "Role " + backQuote(*role_name) + " is granted, but without ADMIN option. " + msg;
|
||||
throw Exception(getUserName() + ": Not enough privileges. " + msg, ErrorCodes::ACCESS_DENIED);
|
||||
}
|
||||
}
|
||||
|
||||
void ContextAccess::checkAdminOption(const UUID & role_id) const
|
||||
{
|
||||
checkAdminOptionImpl(to_array(role_id), [this](const UUID & id, size_t) { return manager->tryReadName(id); });
|
||||
}
|
||||
|
||||
void ContextAccess::checkAdminOption(const UUID & role_id, const String & role_name) const
|
||||
{
|
||||
checkAdminOptionImpl(to_array(role_id), [&role_name](const UUID &, size_t) { return std::optional<String>{role_name}; });
|
||||
}
|
||||
|
||||
void ContextAccess::checkAdminOption(const UUID & role_id, const std::unordered_map<UUID, String> & names_of_roles) const
|
||||
{
|
||||
checkAdminOptionImpl(to_array(role_id), [&names_of_roles](const UUID & id, size_t) { auto it = names_of_roles.find(id); return (it != names_of_roles.end()) ? it->second : std::optional<String>{}; });
|
||||
}
|
||||
|
||||
void ContextAccess::checkAdminOption(const std::vector<UUID> & role_ids) const
|
||||
{
|
||||
checkAdminOptionImpl(role_ids, [this](const UUID & id, size_t) { return manager->tryReadName(id); });
|
||||
}
|
||||
|
||||
void ContextAccess::checkAdminOption(const std::vector<UUID> & role_ids, const Strings & names_of_roles) const
|
||||
{
|
||||
checkAdminOptionImpl(role_ids, [&names_of_roles](const UUID &, size_t i) { return std::optional<String>{names_of_roles[i]}; });
|
||||
}
|
||||
|
||||
void ContextAccess::checkAdminOption(const std::vector<UUID> & role_ids, const std::unordered_map<UUID, String> & names_of_roles) const
|
||||
{
|
||||
checkAdminOptionImpl(role_ids, [&names_of_roles](const UUID & id, size_t) { auto it = names_of_roles.find(id); return (it != names_of_roles.end()) ? it->second : std::optional<String>{}; });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -135,6 +135,11 @@ public:
|
||||
|
||||
/// Checks if a specified role is granted with admin option, and throws an exception if not.
|
||||
void checkAdminOption(const UUID & role_id) const;
|
||||
void checkAdminOption(const UUID & role_id, const String & role_name) const;
|
||||
void checkAdminOption(const UUID & role_id, const std::unordered_map<UUID, String> & names_of_roles) const;
|
||||
void checkAdminOption(const std::vector<UUID> & role_ids) const;
|
||||
void checkAdminOption(const std::vector<UUID> & role_ids, const Strings & names_of_roles) const;
|
||||
void checkAdminOption(const std::vector<UUID> & role_ids, const std::unordered_map<UUID, String> & names_of_roles) const;
|
||||
|
||||
/// Makes an instance of ContextAccess which provides full access to everything
|
||||
/// without any limitations. This is used for the global context.
|
||||
@ -148,7 +153,7 @@ private:
|
||||
void setUser(const UserPtr & user_) const;
|
||||
void setRolesInfo(const std::shared_ptr<const EnabledRolesInfo> & roles_info_) const;
|
||||
void setSettingsAndConstraints() const;
|
||||
void setFinalAccess() const;
|
||||
void calculateAccessRights() const;
|
||||
|
||||
template <bool grant_option>
|
||||
bool isGrantedImpl(const AccessFlags & flags) const;
|
||||
@ -180,6 +185,9 @@ private:
|
||||
template <bool grant_option, typename... Args>
|
||||
void checkAccessImpl2(const AccessFlags & flags, const Args &... args) const;
|
||||
|
||||
template <typename Container, typename GetNameFunction>
|
||||
void checkAdminOptionImpl(const Container & role_ids, const GetNameFunction & get_name_function) const;
|
||||
|
||||
const AccessControlManager * manager = nullptr;
|
||||
const Params params;
|
||||
mutable Poco::Logger * trace_log = nullptr;
|
||||
@ -193,9 +201,10 @@ private:
|
||||
mutable std::shared_ptr<const EnabledRowPolicies> enabled_row_policies;
|
||||
mutable std::shared_ptr<const EnabledQuota> enabled_quota;
|
||||
mutable std::shared_ptr<const EnabledSettings> enabled_settings;
|
||||
mutable std::shared_ptr<const ContextAccess> access_without_readonly;
|
||||
mutable std::shared_ptr<const ContextAccess> access_with_allow_ddl;
|
||||
mutable std::shared_ptr<const ContextAccess> access_with_allow_introspection;
|
||||
mutable std::shared_ptr<const AccessRights> access_without_readonly;
|
||||
mutable std::shared_ptr<const AccessRights> access_with_allow_ddl;
|
||||
mutable std::shared_ptr<const AccessRights> access_with_allow_introspection;
|
||||
mutable std::shared_ptr<const AccessRights> access_from_user_and_roles;
|
||||
mutable std::mutex mutex;
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,7 @@ namespace
|
||||
roles_info.enabled_roles_with_admin_option.emplace(role_id);
|
||||
|
||||
roles_info.names_of_roles[role_id] = role->getName();
|
||||
roles_info.access.merge(role->access);
|
||||
roles_info.access.makeUnion(role->access);
|
||||
roles_info.settings_from_enabled_roles.merge(role->settings);
|
||||
|
||||
for (const auto & granted_role : role->granted_roles.roles)
|
||||
|
@ -9,58 +9,197 @@
|
||||
#include <Access/User.h>
|
||||
#include <Access/Role.h>
|
||||
#include <boost/range/algorithm/copy.hpp>
|
||||
#include <boost/range/algorithm/set_algorithm.hpp>
|
||||
|
||||
|
||||
namespace DB
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename T>
|
||||
void updateFromQueryImpl(T & grantee, const ASTGrantQuery & query, const std::vector<UUID> & roles_from_query)
|
||||
using Kind = ASTGrantQuery::Kind;
|
||||
|
||||
void doGrantAccess(
|
||||
AccessRights & current_access,
|
||||
const AccessRightsElements & access_to_grant,
|
||||
bool with_grant_option)
|
||||
{
|
||||
if (with_grant_option)
|
||||
current_access.grantWithGrantOption(access_to_grant);
|
||||
else
|
||||
current_access.grant(access_to_grant);
|
||||
}
|
||||
|
||||
|
||||
AccessRightsElements getFilteredAccessRightsElementsToRevoke(
|
||||
const AccessRights & current_access, const AccessRightsElements & access_to_revoke, bool grant_option)
|
||||
{
|
||||
AccessRights intersection;
|
||||
if (grant_option)
|
||||
intersection.grantWithGrantOption(access_to_revoke);
|
||||
else
|
||||
intersection.grant(access_to_revoke);
|
||||
intersection.makeIntersection(current_access);
|
||||
|
||||
AccessRightsElements res;
|
||||
for (auto & element : intersection.getElements())
|
||||
{
|
||||
if ((element.kind == Kind::GRANT) && (element.grant_option || !grant_option))
|
||||
res.emplace_back(std::move(element));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void doRevokeAccess(
|
||||
AccessRights & current_access,
|
||||
const AccessRightsElements & access_to_revoke,
|
||||
bool grant_option,
|
||||
const std::shared_ptr<const ContextAccess> & context)
|
||||
{
|
||||
if (context && !context->hasGrantOption(access_to_revoke))
|
||||
context->checkGrantOption(getFilteredAccessRightsElementsToRevoke(current_access, access_to_revoke, grant_option));
|
||||
|
||||
if (grant_option)
|
||||
current_access.revokeGrantOption(access_to_revoke);
|
||||
else
|
||||
current_access.revoke(access_to_revoke);
|
||||
}
|
||||
|
||||
|
||||
void doGrantRoles(GrantedRoles & granted_roles,
|
||||
const RolesOrUsersSet & roles_to_grant,
|
||||
bool with_admin_option)
|
||||
{
|
||||
auto ids = roles_to_grant.getMatchingIDs();
|
||||
|
||||
if (with_admin_option)
|
||||
granted_roles.grantWithAdminOption(ids);
|
||||
else
|
||||
granted_roles.grant(ids);
|
||||
}
|
||||
|
||||
|
||||
std::vector<UUID>
|
||||
getFilteredListOfRolesToRevoke(const GrantedRoles & granted_roles, const RolesOrUsersSet & roles_to_revoke, bool admin_option)
|
||||
{
|
||||
std::vector<UUID> ids;
|
||||
if (roles_to_revoke.all)
|
||||
{
|
||||
boost::range::set_difference(
|
||||
admin_option ? granted_roles.roles_with_admin_option : granted_roles.roles,
|
||||
roles_to_revoke.except_ids,
|
||||
std::back_inserter(ids));
|
||||
}
|
||||
else
|
||||
{
|
||||
boost::range::set_intersection(
|
||||
admin_option ? granted_roles.roles_with_admin_option : granted_roles.roles,
|
||||
roles_to_revoke.getMatchingIDs(),
|
||||
std::back_inserter(ids));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
void doRevokeRoles(GrantedRoles & granted_roles,
|
||||
RolesOrUsersSet * default_roles,
|
||||
const RolesOrUsersSet & roles_to_revoke,
|
||||
bool admin_option,
|
||||
const std::unordered_map<UUID, String> & names_of_roles,
|
||||
const std::shared_ptr<const ContextAccess> & context)
|
||||
{
|
||||
auto ids = getFilteredListOfRolesToRevoke(granted_roles, roles_to_revoke, admin_option);
|
||||
|
||||
if (context)
|
||||
context->checkAdminOption(ids, names_of_roles);
|
||||
|
||||
if (admin_option)
|
||||
granted_roles.revokeAdminOption(ids);
|
||||
else
|
||||
{
|
||||
granted_roles.revoke(ids);
|
||||
if (default_roles)
|
||||
{
|
||||
for (const UUID & id : ids)
|
||||
default_roles->ids.erase(id);
|
||||
for (const UUID & id : ids)
|
||||
default_roles->except_ids.erase(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
template <typename T>
|
||||
void collectRoleNamesTemplate(
|
||||
std::unordered_map<UUID, String> & names_of_roles,
|
||||
const T & grantee,
|
||||
const ASTGrantQuery & query,
|
||||
const RolesOrUsersSet & roles_from_query,
|
||||
const AccessControlManager & access_control)
|
||||
{
|
||||
for (const auto & id : getFilteredListOfRolesToRevoke(grantee.granted_roles, roles_from_query, query.admin_option))
|
||||
{
|
||||
auto name = access_control.tryReadName(id);
|
||||
if (name)
|
||||
names_of_roles.emplace(id, std::move(*name));
|
||||
}
|
||||
}
|
||||
|
||||
void collectRoleNames(
|
||||
std::unordered_map<UUID, String> & names_of_roles,
|
||||
const IAccessEntity & grantee,
|
||||
const ASTGrantQuery & query,
|
||||
const RolesOrUsersSet & roles_from_query,
|
||||
const AccessControlManager & access_control)
|
||||
{
|
||||
if (const auto * user = typeid_cast<const User *>(&grantee))
|
||||
collectRoleNamesTemplate(names_of_roles, *user, query, roles_from_query, access_control);
|
||||
else if (const auto * role = typeid_cast<const Role *>(&grantee))
|
||||
collectRoleNamesTemplate(names_of_roles, *role, query, roles_from_query, access_control);
|
||||
}
|
||||
|
||||
|
||||
template <typename T>
|
||||
void updateFromQueryTemplate(
|
||||
T & grantee,
|
||||
const ASTGrantQuery & query,
|
||||
const RolesOrUsersSet & roles_from_query,
|
||||
const std::unordered_map<UUID, String> & names_of_roles,
|
||||
const std::shared_ptr<const ContextAccess> & context)
|
||||
{
|
||||
using Kind = ASTGrantQuery::Kind;
|
||||
if (!query.access_rights_elements.empty())
|
||||
{
|
||||
if (query.kind == Kind::GRANT)
|
||||
{
|
||||
if (query.grant_option)
|
||||
grantee.access.grantWithGrantOption(query.access_rights_elements);
|
||||
else
|
||||
grantee.access.grant(query.access_rights_elements);
|
||||
}
|
||||
doGrantAccess(grantee.access, query.access_rights_elements, query.grant_option);
|
||||
else
|
||||
{
|
||||
if (query.grant_option)
|
||||
grantee.access.revokeGrantOption(query.access_rights_elements);
|
||||
else
|
||||
grantee.access.revoke(query.access_rights_elements);
|
||||
}
|
||||
doRevokeAccess(grantee.access, query.access_rights_elements, query.grant_option, context);
|
||||
}
|
||||
|
||||
if (!roles_from_query.empty())
|
||||
{
|
||||
if (query.kind == Kind::GRANT)
|
||||
{
|
||||
if (query.admin_option)
|
||||
grantee.granted_roles.grantWithAdminOption(roles_from_query);
|
||||
else
|
||||
grantee.granted_roles.grant(roles_from_query);
|
||||
}
|
||||
doGrantRoles(grantee.granted_roles, roles_from_query, query.admin_option);
|
||||
else
|
||||
{
|
||||
if (query.admin_option)
|
||||
grantee.granted_roles.revokeAdminOption(roles_from_query);
|
||||
else
|
||||
grantee.granted_roles.revoke(roles_from_query);
|
||||
|
||||
RolesOrUsersSet * grantee_default_roles = nullptr;
|
||||
if constexpr (std::is_same_v<T, User>)
|
||||
{
|
||||
for (const UUID & role_from_query : roles_from_query)
|
||||
grantee.default_roles.ids.erase(role_from_query);
|
||||
}
|
||||
grantee_default_roles = &grantee.default_roles;
|
||||
doRevokeRoles(grantee.granted_roles, grantee_default_roles, roles_from_query, query.admin_option, names_of_roles, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateFromQueryImpl(
|
||||
IAccessEntity & grantee,
|
||||
const ASTGrantQuery & query,
|
||||
const RolesOrUsersSet & roles_from_query,
|
||||
const std::unordered_map<UUID, String> & names_or_roles,
|
||||
const std::shared_ptr<const ContextAccess> & context)
|
||||
{
|
||||
if (auto * user = typeid_cast<User *>(&grantee))
|
||||
updateFromQueryTemplate(*user, query, roles_from_query, names_or_roles, context);
|
||||
else if (auto * role = typeid_cast<Role *>(&grantee))
|
||||
updateFromQueryTemplate(*role, query, roles_from_query, names_or_roles, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -68,40 +207,45 @@ BlockIO InterpreterGrantQuery::execute()
|
||||
{
|
||||
auto & query = query_ptr->as<ASTGrantQuery &>();
|
||||
query.replaceCurrentUserTagWithName(context.getUserName());
|
||||
auto access = context.getAccess();
|
||||
auto & access_control = context.getAccessControlManager();
|
||||
|
||||
std::vector<UUID> roles_from_query;
|
||||
if (query.roles)
|
||||
{
|
||||
roles_from_query = RolesOrUsersSet{*query.roles, access_control}.getMatchingIDs(access_control);
|
||||
for (const UUID & role_from_query : roles_from_query)
|
||||
access->checkAdminOption(role_from_query);
|
||||
}
|
||||
|
||||
if (!query.cluster.empty())
|
||||
return executeDDLQueryOnCluster(query_ptr, context, query.access_rights_elements, true);
|
||||
|
||||
auto access = context.getAccess();
|
||||
auto & access_control = context.getAccessControlManager();
|
||||
query.replaceEmptyDatabaseWithCurrent(context.getCurrentDatabase());
|
||||
access->checkGrantOption(query.access_rights_elements);
|
||||
|
||||
RolesOrUsersSet roles_from_query;
|
||||
if (query.roles)
|
||||
roles_from_query = RolesOrUsersSet{*query.roles, access_control};
|
||||
|
||||
std::vector<UUID> to_roles = RolesOrUsersSet{*query.to_roles, access_control, context.getUserID()}.getMatchingIDs(access_control);
|
||||
|
||||
std::unordered_map<UUID, String> names_of_roles;
|
||||
if (!roles_from_query.empty() && (query.kind == Kind::REVOKE))
|
||||
{
|
||||
for (const auto & id : to_roles)
|
||||
{
|
||||
auto entity = access_control.tryRead(id);
|
||||
if (entity)
|
||||
collectRoleNames(names_of_roles, *entity, query, roles_from_query, access_control);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.kind == Kind::GRANT) /// For Kind::REVOKE the grant/admin option is checked inside updateFromQueryImpl().
|
||||
{
|
||||
if (!query.access_rights_elements.empty())
|
||||
access->checkGrantOption(query.access_rights_elements);
|
||||
|
||||
if (!roles_from_query.empty())
|
||||
access->checkAdminOption(roles_from_query.getMatchingIDs());
|
||||
}
|
||||
|
||||
auto update_func = [&](const AccessEntityPtr & entity) -> AccessEntityPtr
|
||||
{
|
||||
auto clone = entity->clone();
|
||||
if (auto user = typeid_cast<std::shared_ptr<User>>(clone))
|
||||
{
|
||||
updateFromQueryImpl(*user, query, roles_from_query);
|
||||
return user;
|
||||
}
|
||||
else if (auto role = typeid_cast<std::shared_ptr<Role>>(clone))
|
||||
{
|
||||
updateFromQueryImpl(*role, query, roles_from_query);
|
||||
return role;
|
||||
}
|
||||
else
|
||||
return entity;
|
||||
updateFromQueryImpl(*clone, query, roles_from_query, names_of_roles, access);
|
||||
return clone;
|
||||
};
|
||||
|
||||
access_control.update(to_roles, update_func);
|
||||
@ -112,19 +256,19 @@ BlockIO InterpreterGrantQuery::execute()
|
||||
|
||||
void InterpreterGrantQuery::updateUserFromQuery(User & user, const ASTGrantQuery & query)
|
||||
{
|
||||
std::vector<UUID> roles_from_query;
|
||||
RolesOrUsersSet roles_from_query;
|
||||
if (query.roles)
|
||||
roles_from_query = RolesOrUsersSet{*query.roles}.getMatchingIDs();
|
||||
updateFromQueryImpl(user, query, roles_from_query);
|
||||
roles_from_query = RolesOrUsersSet{*query.roles};
|
||||
updateFromQueryImpl(user, query, roles_from_query, {}, nullptr);
|
||||
}
|
||||
|
||||
|
||||
void InterpreterGrantQuery::updateRoleFromQuery(Role & role, const ASTGrantQuery & query)
|
||||
{
|
||||
std::vector<UUID> roles_from_query;
|
||||
RolesOrUsersSet roles_from_query;
|
||||
if (query.roles)
|
||||
roles_from_query = RolesOrUsersSet{*query.roles}.getMatchingIDs();
|
||||
updateFromQueryImpl(role, query, roles_from_query);
|
||||
roles_from_query = RolesOrUsersSet{*query.roles};
|
||||
updateFromQueryImpl(role, query, roles_from_query, {}, nullptr);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ namespace ErrorCodes
|
||||
|
||||
namespace
|
||||
{
|
||||
using Kind = ASTGrantQuery::Kind;
|
||||
|
||||
bool parseAccessFlags(IParser::Pos & pos, Expected & expected, AccessFlags & access_flags)
|
||||
{
|
||||
static constexpr auto is_one_of_access_type_words = [](IParser::Pos & pos_)
|
||||
@ -154,13 +156,16 @@ namespace
|
||||
}
|
||||
|
||||
|
||||
bool parseRoles(IParser::Pos & pos, Expected & expected, bool id_mode, std::shared_ptr<ASTRolesOrUsersSet> & roles)
|
||||
bool parseRoles(IParser::Pos & pos, Expected & expected, Kind kind, bool id_mode, std::shared_ptr<ASTRolesOrUsersSet> & roles)
|
||||
{
|
||||
return IParserBase::wrapParseImpl(pos, [&]
|
||||
{
|
||||
ASTPtr ast;
|
||||
ParserRolesOrUsersSet roles_p;
|
||||
roles_p.allowRoleNames().useIDMode(id_mode);
|
||||
if (kind == Kind::REVOKE)
|
||||
roles_p.allowAll();
|
||||
|
||||
ASTPtr ast;
|
||||
if (!roles_p.parse(pos, ast, expected))
|
||||
return false;
|
||||
|
||||
@ -174,7 +179,6 @@ namespace
|
||||
{
|
||||
return IParserBase::wrapParseImpl(pos, [&]
|
||||
{
|
||||
using Kind = ASTGrantQuery::Kind;
|
||||
if (kind == Kind::GRANT)
|
||||
{
|
||||
if (!ParserKeyword{"TO"}.ignore(pos, expected))
|
||||
@ -217,7 +221,6 @@ bool ParserGrantQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
|
||||
attach = true;
|
||||
}
|
||||
|
||||
using Kind = ASTGrantQuery::Kind;
|
||||
Kind kind;
|
||||
if (ParserKeyword{"GRANT"}.ignore(pos, expected))
|
||||
kind = Kind::GRANT;
|
||||
@ -242,7 +245,7 @@ bool ParserGrantQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
|
||||
|
||||
AccessRightsElements elements;
|
||||
std::shared_ptr<ASTRolesOrUsersSet> roles;
|
||||
if (!parseAccessRightsElements(pos, expected, elements) && !parseRoles(pos, expected, attach, roles))
|
||||
if (!parseAccessRightsElements(pos, expected, elements) && !parseRoles(pos, expected, kind, attach, roles))
|
||||
return false;
|
||||
|
||||
if (cluster.empty())
|
||||
|
@ -64,6 +64,56 @@ def test_grant_option():
|
||||
instance.query('REVOKE SELECT ON test.table FROM A, B')
|
||||
|
||||
|
||||
def test_revoke_requires_grant_option():
|
||||
instance.query("CREATE USER A")
|
||||
instance.query("CREATE USER B")
|
||||
|
||||
instance.query("GRANT SELECT ON test.table TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
|
||||
expected_error = "Not enough privileges"
|
||||
assert expected_error in instance.query_and_get_error("REVOKE SELECT ON test.table FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
|
||||
instance.query("GRANT SELECT ON test.table TO A")
|
||||
expected_error = "privileges have been granted, but without grant option"
|
||||
assert expected_error in instance.query_and_get_error("REVOKE SELECT ON test.table FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
|
||||
instance.query("GRANT SELECT ON test.table TO A WITH GRANT OPTION")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
instance.query("REVOKE SELECT ON test.table FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
instance.query("GRANT SELECT ON test.table TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
instance.query("REVOKE SELECT ON test.* FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
instance.query("GRANT SELECT ON test.table TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
instance.query("REVOKE ALL ON test.* FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
instance.query("GRANT SELECT ON test.table TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
instance.query("REVOKE ALL ON *.* FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
instance.query("REVOKE GRANT OPTION FOR ALL ON *.* FROM A")
|
||||
instance.query("GRANT SELECT ON test.table TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
expected_error = "privileges have been granted, but without grant option"
|
||||
assert expected_error in instance.query_and_get_error("REVOKE SELECT ON test.table FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
|
||||
instance.query("GRANT SELECT ON test.* TO A WITH GRANT OPTION")
|
||||
instance.query("GRANT SELECT ON test.table TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT SELECT ON test.table TO B\n"
|
||||
instance.query("REVOKE SELECT ON test.table FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
|
||||
def test_introspection():
|
||||
instance.query("CREATE USER A")
|
||||
instance.query("CREATE USER B")
|
||||
|
@ -97,6 +97,48 @@ def test_admin_option():
|
||||
assert instance.query("SELECT * FROM test_table", user='B') == "1\t5\n2\t10\n"
|
||||
|
||||
|
||||
def test_revoke_requires_admin_option():
|
||||
instance.query("CREATE USER A, B")
|
||||
instance.query("CREATE ROLE R1, R2")
|
||||
|
||||
instance.query("GRANT R1 TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT R1 TO B\n"
|
||||
|
||||
expected_error = "necessary to have the role R1 granted"
|
||||
assert expected_error in instance.query_and_get_error("REVOKE R1 FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT R1 TO B\n"
|
||||
|
||||
instance.query("GRANT R1 TO A")
|
||||
expected_error = "granted, but without ADMIN option"
|
||||
assert expected_error in instance.query_and_get_error("REVOKE R1 FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT R1 TO B\n"
|
||||
|
||||
instance.query("GRANT R1 TO A WITH ADMIN OPTION")
|
||||
instance.query("REVOKE R1 FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
instance.query("GRANT R1 TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT R1 TO B\n"
|
||||
instance.query("REVOKE ALL FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
instance.query("GRANT R1, R2 TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT R1, R2 TO B\n"
|
||||
expected_error = "necessary to have the role R2 granted"
|
||||
assert expected_error in instance.query_and_get_error("REVOKE ALL FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT R1, R2 TO B\n"
|
||||
instance.query("REVOKE ALL EXCEPT R2 FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT R2 TO B\n"
|
||||
instance.query("GRANT R2 TO A WITH ADMIN OPTION")
|
||||
instance.query("REVOKE ALL FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
instance.query("GRANT R1, R2 TO B")
|
||||
assert instance.query("SHOW GRANTS FOR B") == "GRANT R1, R2 TO B\n"
|
||||
instance.query("REVOKE ALL FROM B", user='A')
|
||||
assert instance.query("SHOW GRANTS FOR B") == ""
|
||||
|
||||
|
||||
def test_introspection():
|
||||
instance.query("CREATE USER A")
|
||||
instance.query("CREATE USER B")
|
||||
|
Loading…
Reference in New Issue
Block a user