diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 8fe0629..658b365 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -169,7 +169,7 @@ def is_hash(password): def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, new_priv, - append_privs, tls_requires, module, role=False, maria_role=False): + append_privs, subtract_privs, tls_requires, module, role=False, maria_role=False): changed = False msg = "User unchanged" grant_option = False @@ -288,47 +288,58 @@ def user_mod(cursor, user, host, host_all, password, encrypted, # If the user has privileges on a db.table that doesn't appear at all in # the new specification, then revoke all privileges on it. - for db_table, priv in iteritems(curr_priv): - # If the user has the GRANT OPTION on a db.table, revoke it first. - if "GRANT" in priv: - grant_option = True - if db_table not in new_priv: - if user != "root" and "PROXY" not in priv and not append_privs: - msg = "Privileges updated" - if module.check_mode: - return (True, msg) - privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role) - changed = True + if not append_privs and not subtract_privs: + for db_table, priv in iteritems(curr_priv): + # If the user has the GRANT OPTION on a db.table, revoke it first. + if "GRANT" in priv: + grant_option = True + if db_table not in new_priv: + if user != "root" and "PROXY" not in priv: + msg = "Privileges updated" + if module.check_mode: + return (True, msg) + privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role) + changed = True # If the user doesn't currently have any privileges on a db.table, then # we can perform a straight grant operation. - for db_table, priv in iteritems(new_priv): - if db_table not in curr_priv: - msg = "New privileges granted" - if module.check_mode: - return (True, msg) - privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role) - changed = True + if not subtract_privs: + for db_table, priv in iteritems(new_priv): + if db_table not in curr_priv: + msg = "New privileges granted" + if module.check_mode: + return (True, msg) + privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role) + changed = True # If the db.table specification exists in both the user's current privileges # and in the new privileges, then we need to see if there's a difference. db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys()) for db_table in db_table_intersect: - # If appending privileges, only the set difference between new privileges and current privileges matter. - # The symmetric difference isn't relevant for append because existing privileges will not be revoked. + grant_privs = [] + revoke_privs = [] if append_privs: - priv_diff = set(new_priv[db_table]) - set(curr_priv[db_table]) + # When appending privileges, only missing privileges need to be granted. Nothing is revoked. + grant_privs = list(set(new_priv[db_table]) - set(curr_priv[db_table])) + elif subtract_privs: + # When subtracting privileges, revoke only the intersection of requested and current privileges. + # No privileges are granted. + revoke_privs = list(set(new_priv[db_table]) & set(curr_priv[db_table])) else: - priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) + # When replacing (neither append_privs nor subtract_privs), grant all missing privileges + # and revoke existing privileges that were not requested. + grant_privs = list(set(new_priv[db_table]) - set(curr_priv[db_table])) + revoke_privs = list(set(curr_priv[db_table]) - set(new_priv[db_table])) - if len(priv_diff) > 0: + if len(grant_privs) + len(revoke_privs) > 0: msg = "Privileges updated" if module.check_mode: return (True, msg) - if not append_privs: - privileges_revoke(cursor, user, host, db_table, curr_priv[db_table], grant_option, maria_role) - privileges_grant(cursor, user, host, db_table, new_priv[db_table], tls_requires, maria_role) + if len(revoke_privs) > 0: + privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role) + if len(grant_privs) > 0: + privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role) changed = True if role: diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py index 7641b07..ef022e1 100644 --- a/plugins/modules/mysql_role.py +++ b/plugins/modules/mysql_role.py @@ -51,7 +51,14 @@ options: append_privs: description: - Append the privileges defined by the I(priv) option to the existing ones - for this role instead of overwriting them. + for this role instead of overwriting them. Mutually exclusive with I(subtract_privs). + type: bool + default: no + + subtract_privs: + description: + - Revoke the privileges defined by the I(priv) option and keep other existing privileges. + Mutually exclusive with I(append_privs). type: bool default: no @@ -233,6 +240,14 @@ EXAMPLES = r''' name: business members: - marketing + +- name: Ensure the role foo does not have the DELETE privilege + community.mysql.mysql_role: + state: present + name: foo + subtract_privs: yes + priv: + 'db1.*': DELETE ''' RETURN = '''#''' @@ -821,9 +836,9 @@ class Role(): return True def update(self, users, privs, check_mode=False, - append_privs=False, append_members=False, - detach_members=False, admin=False, - set_default_role_all=True): + append_privs=False, subtract_privs=False, + append_members=False, detach_members=False, + admin=False, set_default_role_all=True): """Update a role. Update a role if needed. @@ -837,6 +852,8 @@ class Role(): check_mode (bool): If True, just checks and does nothing. append_privs (bool): If True, adds new privileges passed through privs not touching current privileges. + subtract_privs (bool): If True, revoke the privileges passed through privs + not touching other existing privileges. append_members (bool): If True, adds new members passed through users not touching current members. detach_members (bool): If True, removes members passed through users from a role. @@ -861,7 +878,7 @@ class Role(): if privs: changed, msg = user_mod(self.cursor, self.name, self.host, None, None, None, None, None, None, - privs, append_privs, None, + privs, append_privs, subtract_privs, None, self.module, role=True, maria_role=self.is_mariadb) if admin: @@ -931,6 +948,7 @@ def main(): admin=dict(type='str'), priv=dict(type='raw'), append_privs=dict(type='bool', default=False), + subtract_privs=dict(type='bool', default=False), members=dict(type='list', elements='str'), append_members=dict(type='bool', default=False), detach_members=dict(type='bool', default=False), @@ -945,6 +963,7 @@ def main(): ('admin', 'members'), ('admin', 'append_members'), ('admin', 'detach_members'), + ('append_privs', 'subtract_privs'), ), ) @@ -958,6 +977,7 @@ def main(): connect_timeout = module.params['connect_timeout'] config_file = module.params['config_file'] append_privs = module.params['append_privs'] + subtract_privs = module.boolean(module.params['subtract_privs']) members = module.params['members'] append_members = module.params['append_members'] detach_members = module.params['detach_members'] @@ -1047,7 +1067,7 @@ def main(): set_default_role_all) else: - changed = role.update(members, priv, module.check_mode, append_privs, + changed = role.update(members, priv, module.check_mode, append_privs, subtract_privs, append_members, detach_members, admin, set_default_role_all) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 326c1fe..b5d08a2 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -63,7 +63,13 @@ options: append_privs: description: - Append the privileges defined by priv to the existing ones for this - user instead of overwriting existing ones. + user instead of overwriting existing ones. Mutually exclusive with I(subtract_privs). + type: bool + default: no + subtract_privs: + description: + - Revoke the privileges defined by the I(priv) option and keep other existing privileges. + Mutually exclusive with I(append_privs). type: bool default: no tls_requires: @@ -306,6 +312,13 @@ EXAMPLES = r''' MAX_QUERIES_PER_HOUR: 10 MAX_CONNECTIONS_PER_HOUR: 5 +- name: Ensure bob does not have the DELETE privilege + community.mysql.mysql_user: + name: bob + subtract_privs: yes + priv: + 'db1.*': DELETE + # Example .my.cnf file for setting the root password # [client] # user=root @@ -352,6 +365,7 @@ def main(): priv=dict(type='raw'), tls_requires=dict(type='dict'), append_privs=dict(type='bool', default=False), + subtract_privs=dict(type='bool', default=False), check_implicit_admin=dict(type='bool', default=False), update_password=dict(type='str', default='always', choices=['always', 'on_create'], no_log=False), sql_log_bin=dict(type='bool', default=True), @@ -364,6 +378,7 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, + mutually_exclusive=(('append_privs', 'subtract_privs'),) ) login_user = module.params["login_user"] login_password = module.params["login_password"] @@ -379,6 +394,7 @@ def main(): connect_timeout = module.params["connect_timeout"] config_file = module.params["config_file"] append_privs = module.boolean(module.params["append_privs"]) + subtract_privs = module.boolean(module.params['subtract_privs']) update_password = module.params['update_password'] ssl_cert = module.params["client_cert"] ssl_key = module.params["client_key"] @@ -435,11 +451,11 @@ def main(): if update_password == "always": changed, msg = user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, tls_requires, module) + priv, append_privs, subtract_privs, tls_requires, module) else: changed, msg = user_mod(cursor, user, host, host_all, None, encrypted, plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, tls_requires, module) + priv, append_privs, subtract_privs, tls_requires, module) except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: module.fail_json(msg=to_native(e))