mirror of
https://github.com/ansible-collections/community.mysql.git
synced 2025-04-09 20:20:32 -07:00
mysql_user: add "update_password: on_new_username" argument, "password_changed" result field (#365)
* mysql_user: add value 'on_new_username' to argument 'update_password' * mysql_user: return "password_changed" boolean (true if the user got a new password) * mysql_user: optimize queries for existing passwords * mysql_user: add integration tests for update_password argument * mysql_user: add description for "update_password: on_new_username" argument * add changelog fragment * formatting (PEP8) * Update changelogs/fragments/365-mysql_user-add-on_new_username-and-password_changed.yml Co-authored-by: Benjamin MALYNOVYTCH <bmalynovytch@users.noreply.github.com> * Update changelogs/fragments/365-mysql_user-add-on_new_username-and-password_changed.yml Co-authored-by: Benjamin MALYNOVYTCH <bmalynovytch@users.noreply.github.com> * Update plugins/modules/mysql_user.py Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru> * Update changelogs/fragments/365-mysql_user-add-on_new_username-and-password_changed.yml Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru> * Update changelogs/fragments/365-mysql_user-add-on_new_username-and-password_changed.yml Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru> Co-authored-by: Felix Hamme <felix.hamme@ionos.com> Co-authored-by: Benjamin MALYNOVYTCH <bmalynovytch@users.noreply.github.com> Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>
This commit is contained in:
parent
51a38840d9
commit
ed3935abec
6 changed files with 232 additions and 27 deletions
changelogs/fragments
plugins
tests/integration/targets/test_mysql_user/tasks
|
@ -0,0 +1,10 @@
|
|||
minor_changes:
|
||||
- >
|
||||
mysql_user - Add the option ``on_new_username`` to argument ``update_password`` to reuse the password (plugin and
|
||||
authentication_string) when creating a new user if some user with the same name already exists.
|
||||
If the existing user with the same name have varying passwords, the password from the arguments is used like with
|
||||
``update_password: always`` (https://github.com/ansible-collections/community.mysql/pull/365).
|
||||
- >
|
||||
mysql_user - Add the result field ``password_changed`` (boolean). It is true, when the user got a new password.
|
||||
When the user was created with ``update_password: on_new_username`` and an existing password was reused,
|
||||
``password_changed`` is false (https://github.com/ansible-collections/community.mysql/pull/365).
|
|
@ -112,21 +112,49 @@ def get_grants(cursor, user, host):
|
|||
return grants.split(", ")
|
||||
|
||||
|
||||
def get_existing_authentication(cursor, user):
|
||||
# Return the plugin and auth_string if there is exactly one distinct existing plugin and auth_string.
|
||||
cursor.execute("SELECT VERSION()")
|
||||
if 'mariadb' in cursor.fetchone()[0].lower():
|
||||
# before MariaDB 10.2.19 and 10.3.11, "password" and "authentication_string" can differ
|
||||
# when using mysql_native_password
|
||||
cursor.execute("""select plugin, auth from (
|
||||
select plugin, password as auth from mysql.user where user=%(user)s
|
||||
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
|
||||
) x group by plugin, auth limit 2
|
||||
""", {'user': user})
|
||||
else:
|
||||
cursor.execute("""select plugin, authentication_string as auth from mysql.user where user=%(user)s
|
||||
group by plugin, authentication_string limit 2""", {'user': user})
|
||||
rows = cursor.fetchall()
|
||||
if len(rows) == 1:
|
||||
return {'plugin': rows[0][0], 'auth_string': rows[0][1]}
|
||||
return None
|
||||
|
||||
|
||||
def user_add(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string, new_priv,
|
||||
tls_requires, check_mode):
|
||||
tls_requires, check_mode, reuse_existing_password):
|
||||
# we cannot create users without a proper hostname
|
||||
if host_all:
|
||||
return False
|
||||
return {'changed': False, 'password_changed': False}
|
||||
|
||||
if check_mode:
|
||||
return True
|
||||
return {'changed': True, 'password_changed': None}
|
||||
|
||||
# Determine what user management method server uses
|
||||
old_user_mgmt = impl.use_old_user_mgmt(cursor)
|
||||
|
||||
mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires
|
||||
|
||||
used_existing_password = False
|
||||
if reuse_existing_password:
|
||||
existing_auth = get_existing_authentication(cursor, user)
|
||||
if existing_auth:
|
||||
plugin = existing_auth['plugin']
|
||||
plugin_hash_string = existing_auth['auth_string']
|
||||
password = None
|
||||
used_existing_password = True
|
||||
if password and encrypted:
|
||||
if impl.supports_identified_by_password(cursor):
|
||||
query_with_args = "CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password)
|
||||
|
@ -156,7 +184,7 @@ def user_add(cursor, user, host, host_all, password, encrypted,
|
|||
privileges_grant(cursor, user, host, db_table, priv, tls_requires)
|
||||
if tls_requires is not None:
|
||||
privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires)
|
||||
return True
|
||||
return {'changed': True, 'password_changed': not used_existing_password}
|
||||
|
||||
|
||||
def is_hash(password):
|
||||
|
@ -182,6 +210,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
else:
|
||||
hostnames = [host]
|
||||
|
||||
password_changed = False
|
||||
for host in hostnames:
|
||||
# Handle clear text and hashed passwords.
|
||||
if not role:
|
||||
|
@ -226,9 +255,10 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
encrypted_password = cursor.fetchone()[0]
|
||||
|
||||
if current_pass_hash != encrypted_password:
|
||||
password_changed = True
|
||||
msg = "Password updated"
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
|
||||
if old_user_mgmt:
|
||||
cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password))
|
||||
msg = "Password updated (old style)"
|
||||
|
@ -280,6 +310,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)
|
||||
|
||||
cursor.execute(*query_with_args)
|
||||
password_changed = True
|
||||
changed = True
|
||||
|
||||
# Handle privileges
|
||||
|
@ -297,7 +328,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
if user != "root" and "PROXY" not in priv:
|
||||
msg = "Privileges updated"
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
|
||||
privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role)
|
||||
changed = True
|
||||
|
||||
|
@ -308,7 +339,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
if db_table not in curr_priv:
|
||||
msg = "New privileges granted"
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
|
||||
privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role)
|
||||
changed = True
|
||||
|
||||
|
@ -338,7 +369,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
if len(grant_privs) + len(revoke_privs) > 0:
|
||||
msg = "Privileges updated: granted %s, revoked %s" % (grant_privs, revoke_privs)
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
|
||||
if len(revoke_privs) > 0:
|
||||
privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role)
|
||||
if len(grant_privs) > 0:
|
||||
|
@ -353,7 +384,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
if current_requires != tls_requires:
|
||||
msg = "TLS requires updated"
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
|
||||
if not old_user_mgmt:
|
||||
pre_query = "ALTER USER"
|
||||
else:
|
||||
|
@ -369,7 +400,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
cursor.execute(*query_with_args)
|
||||
changed = True
|
||||
|
||||
return (changed, msg)
|
||||
return {'changed': changed, 'msg': msg, 'password_changed': password_changed}
|
||||
|
||||
|
||||
def user_delete(cursor, user, host, host_all, check_mode):
|
||||
|
|
|
@ -911,10 +911,11 @@ class Role():
|
|||
set_default_role_all=set_default_role_all)
|
||||
|
||||
if privs:
|
||||
changed, msg = user_mod(self.cursor, self.name, self.host,
|
||||
None, None, None, None, None, None,
|
||||
privs, append_privs, subtract_privs, None,
|
||||
self.module, role=True, maria_role=self.is_mariadb)
|
||||
result = user_mod(self.cursor, self.name, self.host,
|
||||
None, None, None, None, None, None,
|
||||
privs, append_privs, subtract_privs, None,
|
||||
self.module, role=True, maria_role=self.is_mariadb)
|
||||
changed = result['changed']
|
||||
|
||||
if admin:
|
||||
self.role_impl.set_admin(admin)
|
||||
|
|
|
@ -118,8 +118,12 @@ options:
|
|||
description:
|
||||
- C(always) will update passwords if they differ. This affects I(password) and the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string).
|
||||
- C(on_create) will only set the password or the combination of plugin, plugin_hash_string, plugin_auth_string for newly created users.
|
||||
- "C(on_new_username) works like C(on_create), but it tries to reuse an existing password: If one different user
|
||||
with the same username exists, or multiple different users with the same username and equal C(plugin) and
|
||||
C(authentication_string) attribute, the existing C(plugin) and C(authentication_string) are used for the
|
||||
new user instead of the I(password), I(plugin), I(plugin_hash_string) or I(plugin_auth_string) argument."
|
||||
type: str
|
||||
choices: [ always, on_create ]
|
||||
choices: [ always, on_create, on_new_username ]
|
||||
default: always
|
||||
plugin:
|
||||
description:
|
||||
|
@ -370,7 +374,7 @@ def main():
|
|||
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),
|
||||
update_password=dict(type='str', default='always', choices=['always', 'on_create', 'on_new_username'], no_log=False),
|
||||
sql_log_bin=dict(type='bool', default=True),
|
||||
plugin=dict(default=None, type='str'),
|
||||
plugin_hash_string=dict(default=None, type='str'),
|
||||
|
@ -447,18 +451,22 @@ def main():
|
|||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e))
|
||||
priv = privileges_unpack(priv, mode, ensure_usage=not subtract_privs)
|
||||
|
||||
password_changed = False
|
||||
if state == "present":
|
||||
if user_exists(cursor, user, host, host_all):
|
||||
try:
|
||||
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, subtract_privs, tls_requires, module)
|
||||
result = user_mod(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string,
|
||||
priv, append_privs, subtract_privs, tls_requires, module)
|
||||
|
||||
else:
|
||||
changed, msg = user_mod(cursor, user, host, host_all, None, encrypted,
|
||||
None, None, None,
|
||||
priv, append_privs, subtract_privs, tls_requires, module)
|
||||
result = user_mod(cursor, user, host, host_all, None, encrypted,
|
||||
None, None, None,
|
||||
priv, append_privs, subtract_privs, tls_requires, module)
|
||||
changed = result['changed']
|
||||
msg = result['msg']
|
||||
password_changed = result['password_changed']
|
||||
|
||||
except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
|
||||
module.fail_json(msg=to_native(e))
|
||||
|
@ -468,9 +476,12 @@ def main():
|
|||
try:
|
||||
if subtract_privs:
|
||||
priv = None # avoid granting unwanted privileges
|
||||
changed = user_add(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string,
|
||||
priv, tls_requires, module.check_mode)
|
||||
reuse_existing_password = update_password == 'on_new_username'
|
||||
result = user_add(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string,
|
||||
priv, tls_requires, module.check_mode, reuse_existing_password)
|
||||
changed = result['changed']
|
||||
password_changed = result['password_changed']
|
||||
if changed:
|
||||
msg = "User added"
|
||||
|
||||
|
@ -487,7 +498,7 @@ def main():
|
|||
else:
|
||||
changed = False
|
||||
msg = "User doesn't exist"
|
||||
module.exit_json(changed=changed, user=user, msg=msg)
|
||||
module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
- name: "applying user {{ username }}@{{ host }} with update_password={{ update_password }}"
|
||||
mysql_user:
|
||||
login_user: '{{ mysql_parameters.login_user }}'
|
||||
login_password: '{{ mysql_parameters.login_password }}'
|
||||
login_host: '{{ mysql_parameters.login_host }}'
|
||||
login_port: '{{ mysql_parameters.login_port }}'
|
||||
state: present
|
||||
name: "{{ username }}"
|
||||
host: "{{ host }}"
|
||||
password: "{{ password }}"
|
||||
update_password: "{{ update_password }}"
|
||||
register: result
|
||||
- name: assert a change occurred
|
||||
assert:
|
||||
that:
|
||||
- "result.changed == {{ expect_change }}"
|
||||
- "result.password_changed == {{ expect_password_change }}"
|
||||
- name: query the user
|
||||
command: "{{ mysql_command }} -BNe \"SELECT plugin, authentication_string FROM mysql.user where user='{{ username }}' and host='{{ host }}'\""
|
||||
register: existing_user
|
||||
- name: assert the password is as set to expect_hash
|
||||
assert:
|
||||
that:
|
||||
- "'mysql_native_password\t{{ expect_password_hash }}' in existing_user.stdout_lines"
|
|
@ -0,0 +1,128 @@
|
|||
# Tests scenarios for both plaintext and encrypted user passwords.
|
||||
|
||||
- vars:
|
||||
mysql_parameters:
|
||||
login_user: '{{ mysql_user }}'
|
||||
login_password: '{{ mysql_password }}'
|
||||
login_host: 127.0.0.1
|
||||
login_port: '{{ mysql_primary_port }}'
|
||||
test_password1: kbB9tcx5WOGVGfzV
|
||||
test_password1_hash: '*AF6A7F9D038475C17EE46564F154104877EE5037'
|
||||
test_password2: XBYjpHmjIctMxl1y
|
||||
test_password2_hash: '*9E22D1B35C68BDDF398B8F28AE482E5A865BAC0A'
|
||||
test_password3: tem33JfR5Yx98BB
|
||||
test_password3_hash: '*C7E7C2710702F20336F8D93BC0670C8FB66BDBC7'
|
||||
|
||||
|
||||
block:
|
||||
- include_tasks: assert_user_password.yml
|
||||
vars:
|
||||
username: "{{ item.username }}"
|
||||
host: '127.0.0.1'
|
||||
update_password: "{{ item.update_password }}"
|
||||
password: "{{ test_password1 }}"
|
||||
expect_change: "{{ item.expect_change }}"
|
||||
expect_password_change: "{{ item.expect_change }}"
|
||||
expect_password_hash: "{{ test_password1_hash }}"
|
||||
loop:
|
||||
# all variants set the password when nothing exists
|
||||
- username: test1
|
||||
update_password: always
|
||||
expect_change: true
|
||||
- username: test2
|
||||
update_password: on_create
|
||||
expect_change: true
|
||||
- username: test3
|
||||
update_password: on_new_username
|
||||
expect_change: true
|
||||
|
||||
# assert idempotency
|
||||
- username: test1
|
||||
update_password: always
|
||||
expect_change: false
|
||||
- username: test2
|
||||
update_password: on_create
|
||||
expect_change: false
|
||||
- username: test3
|
||||
update_password: on_new_username
|
||||
expect_change: false
|
||||
|
||||
# same user, new password
|
||||
- include_tasks: assert_user_password.yml
|
||||
vars:
|
||||
username: "{{ item.username }}"
|
||||
host: '127.0.0.1'
|
||||
update_password: "{{ item.update_password }}"
|
||||
password: "{{ test_password2 }}"
|
||||
expect_change: "{{ item.expect_change }}"
|
||||
expect_password_change: "{{ item.expect_change }}"
|
||||
expect_password_hash: "{{ item.expect_password_hash }}"
|
||||
loop:
|
||||
- username: test1
|
||||
update_password: always
|
||||
expect_change: true
|
||||
expect_password_hash: "{{ test_password2_hash }}"
|
||||
- username: test2
|
||||
update_password: on_create
|
||||
expect_change: false
|
||||
expect_password_hash: "{{ test_password1_hash }}"
|
||||
- username: test3
|
||||
update_password: on_new_username
|
||||
expect_change: false
|
||||
expect_password_hash: "{{ test_password1_hash }}"
|
||||
|
||||
# new user, new password
|
||||
- include_tasks: assert_user_password.yml
|
||||
vars:
|
||||
username: "{{ item.username }}"
|
||||
host: '::1'
|
||||
update_password: "{{ item.update_password }}"
|
||||
password: "{{ item.password }}"
|
||||
expect_change: "{{ item.expect_change }}"
|
||||
expect_password_change: "{{ item.expect_password_change }}"
|
||||
expect_password_hash: "{{ item.expect_password_hash }}"
|
||||
loop:
|
||||
- username: test1
|
||||
update_password: always
|
||||
expect_change: true
|
||||
expect_password_change: true
|
||||
password: "{{ test_password1 }}"
|
||||
expect_password_hash: "{{ test_password1_hash }}"
|
||||
- username: test2
|
||||
update_password: on_create
|
||||
expect_change: true
|
||||
expect_password_change: true
|
||||
password: "{{ test_password2 }}"
|
||||
expect_password_hash: "{{ test_password2_hash }}"
|
||||
- username: test3
|
||||
update_password: on_new_username
|
||||
expect_change: true
|
||||
expect_password_change: false
|
||||
password: "{{ test_password2 }}"
|
||||
expect_password_hash: "{{ test_password1_hash }}"
|
||||
|
||||
# prepare for next test: ensure all users have varying passwords
|
||||
- username: test3
|
||||
update_password: always
|
||||
expect_change: true
|
||||
expect_password_change: true
|
||||
password: "{{ test_password2 }}"
|
||||
expect_password_hash: "{{ test_password2_hash }}"
|
||||
|
||||
# another new user, another new password and multiple existing users with varying passwords
|
||||
- include_tasks: assert_user_password.yml
|
||||
vars:
|
||||
username: "{{ item.username }}"
|
||||
host: '2001:db8::1'
|
||||
update_password: "{{ item.update_password }}"
|
||||
password: "{{ test_password3 }}"
|
||||
expect_change: true
|
||||
expect_password_change: true
|
||||
expect_password_hash: "{{ test_password3_hash }}"
|
||||
loop:
|
||||
- username: test1
|
||||
update_password: always
|
||||
- username: test2
|
||||
update_password: on_create
|
||||
- username: test3
|
||||
update_password: on_new_username
|
Loading…
Add table
Reference in a new issue