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:
Vitaly Baranov 2020-07-02 03:09:57 +03:00
parent c39eb8f71b
commit 03b36c262e
9 changed files with 504 additions and 140 deletions

View File

@ -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;
}

View File

@ -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); }

View File

@ -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>{}; });
}
}

View File

@ -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;
};

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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())

View File

@ -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")

View File

@ -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")