diff --git a/changelogs/fragments/49-mysql_user_login_tracking.yml b/changelogs/fragments/49-mysql_user_login_tracking.yml new file mode 100644 index 00000000..652d7e64 --- /dev/null +++ b/changelogs/fragments/49-mysql_user_login_tracking.yml @@ -0,0 +1,2 @@ +minor_changes: +- mysql_user - add the ``account_locking`` options to support login attempt tracking and account locking feature (https://github.com/ansible-collections/community.mysql/issues/49). diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 0c757fa8..3fe13ac4 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -116,6 +116,27 @@ - Used when I(state=present), ignored otherwise. type: dict version_added: '0.1.0' + account_locking: + description: + - Configure user accounts such that too many consecutive login failures cause temporary account locking. Provided since MySQL 8.0.19. + - "Available options are C(FAILED_LOGIN_ATTEMPTS: num), C(PASSWORD_LOCK_TIME: num | UNBOUNDED)." + - Used when I(state=present) and target server is MySQL >= 8.0.19, ignored otherwise. + - U(https://dev.mysql.com/doc/refman/8.0/en/password-management.html#failed-login-tracking). + type: dict + suboptions: + FAILED_LOGIN_ATTEMPTS: + description: + - Number of failed login attempts before the user account is locked. + - Permitted values are in the range from 0 to 32767. + - A value of 0 disables the option. + type: int + PASSWORD_LOCK_TIME: + description: + - Number of days the account stays locked after the FAILED_LOGIN_ATTEMPTS threshold is exceeded. + - Permitted values are in the range from 0 to 32767, or the string ``UNBOUNDED`` + - A value of 0 disables the option. + - A value of ``UNBOUNDED`` permanently locks the account until it's administratively unlocked. + version_added: '1.2.0' notes: - "MySQL server installs with default I(login_user) of C(root) and no password. @@ -139,6 +160,7 @@ - Jonathan Mainguy (@Jmainguy) - Benjamin Malynovytch (@bmalynovytch) - Lukasz Tomaszkiewicz (@tomaszkiewicz) +- Jorge Rodriguez (@Jorge-Rodriguez) extends_documentation_fragment: - community.mysql.mysql @@ -188,6 +210,22 @@ 'db1.*': 'ALL,GRANT' 'db2.*': 'ALL,GRANT' +- name: Create user with password and locking such that the account locks after three failed attempts + community.mysql.mysql_user: + name: bob + password: 12345 + account_locking: + FAILED_LOGIN_ATTEMPTS: 3 + PASSWORD_LOCK_TIME: UNBOUNDED + +- name: Create user with password and locking such that the account locks for 5 days after three failed attempts + community.mysql.mysql_user: + name: bob + password: 12345 + account_locking: + FAILED_LOGIN_ATTEMPTS: 3 + PASSWORD_LOCK_TIME: 5 + # Note that REQUIRESSL is a special privilege that should only apply to *.* by itself. # Setting this privilege in this manner is supported for backwards compatibility only. # Use 'tls_requires' instead. @@ -217,7 +255,14 @@ name: bob tls_requires: -- name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials +- name: Create user with enabled loging tracking. + community.mysql.mysql_user: + name: bob + account_locking: + PASSWORD_LOCK_TIME: 2 + FAILED_LOGIN_ATTEMPTS: 5 + +- name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials. community.mysql.mysql_user: login_user: root login_password: 123456 @@ -386,6 +431,57 @@ def supports_identified_by_password(cursor): return LooseVersion(version_str) < LooseVersion('8') +def validate_account_locking(cursor, account_locking, module): + cursor.execute("SELECT VERSION()") + result = cursor.fetchone() + version_str = result[0] + version = version_str.split('-')[0].split('.') + + locking = {} + + if 'mariadb' in version_str.lower(): + module.warn("MariaDB does not support this manner of account locking. Use the MAX_PASSWORD_ERRORS server variable instead.") + module.warn("Account locking settings are being ignored.") + else: + if int(version[0]) * 1000 + int(version[2]) < 8019: + module.warn("MySQL is too old to support this manner of account locking.") + module.warn("Account locking settings are being ignored.") + else: + if account_locking is not None: + locking = { + "FAILED_LOGIN_ATTEMPTS": str(account_locking.get("FAILED_LOGIN_ATTEMPTS", 0)), + "PASSWORD_LOCK_TIME": str(account_locking.get("PASSWORD_LOCK_TIME", 0)) + } + if any([int(value) < 0 or int(value) > 32767 for value in locking.values() if re.match("[-+]?\\d+$", value)]): + module.fail_json(msg="Account locking values are out of the valid range (0-32767)") + if ("PASSWORD_LOCK_TIME" in locking.keys() + and not re.match("[-+]?\\d+$", locking.get("PASSWORD_LOCK_TIME")) + and locking.get("PASSWORD_LOCK_TIME") != "UNBOUNDED"): + module.fail_json(msg="PASSWORD_LOCK_TIME must be an integer between 0 and 32767 or 'UNBOUNDED'") + return locking + + +def get_account_locking(cursor, user, host): + cursor.execute("SELECT VERSION()") + result = cursor.fetchone() + version_str = result[0] + version = version_str.split('-')[0].split('.') + + locking = {} + + if 'mariadb' in version_str.lower() or int(version[0]) * 1000 + int(version[2]) < 8019: + return locking + + cursor.execute("SHOW CREATE USER %s@%s", (user, host)) + result = cursor.fetchone() + + for setting in ('FAILED_LOGIN_ATTEMPTS', 'PASSWORD_LOCK_TIME'): + match = re.search("%s (\\d+|UNBOUNDED)" % setting, result[0]) + if match: + locking[setting] = match.groups()[0] + return locking + + def get_mode(cursor): cursor.execute('SELECT @@GLOBAL.sql_mode') result = cursor.fetchone() @@ -426,7 +522,7 @@ def sanitize_requires(tls_requires): return None -def mogrify_requires(query, params, tls_requires): +def mogrify_requires(query, params, tls_requires, account_locking): if tls_requires: if isinstance(tls_requires, dict): k, v = zip(*tls_requires.items()) @@ -435,10 +531,17 @@ def mogrify_requires(query, params, tls_requires): else: requires_query = tls_requires query = " REQUIRE ".join((query, requires_query)) + return mogrify_account_locking(query, params, account_locking) + + +def do_not_mogrify_requires(query, params, tls_requires, account_locking): return query, params -def do_not_mogrify_requires(query, params, tls_requires): +def mogrify_account_locking(query, params, account_locking): + if account_locking: + for k, v in account_locking.items(): + query = ' '.join((query, k, str(v))) return query, params @@ -477,11 +580,13 @@ def get_grants(cursor, user, host): 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, account_locking, check_mode, module): # we cannot create users without a proper hostname if host_all: return False + locking = validate_account_locking(cursor, account_locking, module) + if check_mode: return True @@ -511,7 +616,7 @@ def user_add(cursor, user, host, host_all, password, encrypted, else: query_with_args = "CREATE USER %s@%s", (user, host) - query_with_args_and_tls_requires = query_with_args + (tls_requires,) + query_with_args_and_tls_requires = query_with_args + (tls_requires, locking) cursor.execute(*mogrify(*query_with_args_and_tls_requires)) if new_priv is not None: @@ -532,7 +637,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): + append_privs, tls_requires, account_locking, module): changed = False msg = "User unchanged" grant_option = False @@ -540,6 +645,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted, # Determine what user management method server uses old_user_mgmt = use_old_user_mgmt(cursor) + locking = validate_account_locking(cursor, account_locking, module) + if host_all: hostnames = user_get_hostnames(cursor, user) else: @@ -706,7 +813,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if tls_requires is not None: query = " ".join((pre_query, "%s@%s")) - query_with_args = mogrify_requires(query, (user, host), tls_requires) + query_with_args = mogrify_requires(query, (user, host), tls_requires, locking) else: query = " ".join((pre_query, "%s@%s REQUIRE NONE")) query_with_args = query, (user, host) @@ -714,6 +821,17 @@ def user_mod(cursor, user, host, host_all, password, encrypted, cursor.execute(*query_with_args) changed = True + # Handle Account locking + locking = validate_account_locking(cursor, account_locking, module) + current_locking = get_account_locking(cursor, user, host) + clear_locking = dict((x, y) for x, y in locking.items() if y != '0') + if current_locking != clear_locking: + msg = "Account locking updated" + if module.check_mode: + return (True, msg) + cursor.execute(*mogrify_account_locking("ALTER USER %s@%s", (user, host), locking)) + changed = True + return (changed, msg) @@ -1031,6 +1149,7 @@ def main(): state=dict(type='str', default='present', choices=['absent', 'present']), priv=dict(type='raw'), tls_requires=dict(type='dict'), + account_locking=dict(type='dict', default={}), append_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), @@ -1054,6 +1173,7 @@ def main(): state = module.params["state"] priv = module.params["priv"] tls_requires = sanitize_requires(module.params["tls_requires"]) + account_locking = module.params['account_locking'] check_implicit_admin = module.params["check_implicit_admin"] connect_timeout = module.params["connect_timeout"] config_file = module.params["config_file"] @@ -1112,12 +1232,12 @@ def main(): 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, tls_requires, module) + plugin, plugin_hash_string, plugin_auth_string, priv, + append_privs, tls_requires, account_locking, 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) + plugin, plugin_hash_string, plugin_auth_string, priv, + append_privs, tls_requires, account_locking, module) except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: module.fail_json(msg=to_native(e)) @@ -1126,8 +1246,8 @@ def main(): module.fail_json(msg="host_all parameter cannot be used when adding a user") try: changed = user_add(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, - priv, tls_requires, module.check_mode) + plugin, plugin_hash_string, plugin_auth_string, priv, + tls_requires, account_locking, module.check_mode, module) if changed: msg = "User added" diff --git a/tests/integration/targets/test_mysql_user/tasks/create_user.yml b/tests/integration/targets/test_mysql_user/tasks/create_user.yml index 790d9bb2..9c3459ac 100644 --- a/tests/integration/targets/test_mysql_user/tasks/create_user.yml +++ b/tests/integration/targets/test_mysql_user/tasks/create_user.yml @@ -37,4 +37,4 @@ - name: assert output message mysql user was created assert: that: - - "result.changed == true" + - result is changed diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-49.yml b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml new file mode 100644 index 00000000..742e39d9 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml @@ -0,0 +1,275 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + + block: + + # ============================================================ + - name: find out the database version + mysql_info: + <<: *mysql_params + filter: version + register: db_version + + - set_fact: + version_string: "{{[db_version.version.major, db_version.version.minor, db_version.version.release] | join('.')}}" + + - name: Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + state: absent + ignore_errors: yes + + - name: Create user with account locking in test mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 3 + FAILED_LOGIN_ATTEMPTS: 3 + check_mode: True + register: result + + - assert: + that: + - result is changed + + - name: Create user with account locking with password lock time below range + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: -1 + FAILED_LOGIN_ATTEMPTS: 3 + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + - result.msg == "Account locking values are out of the valid range (0-32767)" + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - assert: + that: + - result is changed + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + + - name: Create user with account locking with password lock time above range + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 32768 + FAILED_LOGIN_ATTEMPTS: 3 + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + - result.msg == "Account locking values are out of the valid range (0-32767)" + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - assert: + that: + - result is succeeded + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + + - name: Create user with account locking with failed login attempts below range + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 2 + FAILED_LOGIN_ATTEMPTS: -1 + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + - result.msg == "Account locking values are out of the valid range (0-32767)" + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - assert: + that: + - result is succeeded + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + + - name: Create user with account locking with failed login attempts above range + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 2 + FAILED_LOGIN_ATTEMPTS: 32768 + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + - result.msg == "Account locking values are out of the valid range (0-32767)" + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - assert: + that: + - result is succeeded + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + + - name: Create user with account locking with invalid password lock time + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: INVALID + FAILED_LOGIN_ATTEMPTS: 3 + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + - result.msg == "PASSWORD_LOCK_TIME must be an integer between 0 and 32767 or 'UNBOUNDED'" + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - assert: + that: + - result is succeeded + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + + - include: assert_no_user.yml user_name={{ user_name_1 }} + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - name: Create user with account locking + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 3 + FAILED_LOGIN_ATTEMPTS: 3 + register: result + + - assert: + that: + - result is changed + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - assert: + that: + - result is succeeded + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + + - include: assert_user.yml user_name={{ user_name_1 }} + + - block: + - name: retrieve create request + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: result + + - assert: + that: + - "{{ 'PASSWORD_LOCK_TIME 3' in result.stdout }}" + - "{{ 'FAILED_LOGIN_ATTEMPTS 3' in result.stdout }}" + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - name: Create existing user with account locking in test mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 3 + FAILED_LOGIN_ATTEMPTS: 3 + check_mode: True + register: result + + - assert: + that: result is not changed + + - name: Create existing user with account locking + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 3 + FAILED_LOGIN_ATTEMPTS: 3 + register: result + + - assert: + that: result is not changed + + - name: Update existing user with account locking in test mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 3 + FAILED_LOGIN_ATTEMPTS: 5 + check_mode: True + register: result + + - assert: + that: result is changed + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - assert: + that: result is not changed + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + + - block: + - name: retrieve create request + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: result + - assert: + that: + - "{{ 'PASSWORD_LOCK_TIME 3' in result.stdout }}" + - "{{ 'FAILED_LOGIN_ATTEMPTS 3' in result.stdout }}" + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - name: Update existing user with account locking + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + account_locking: + PASSWORD_LOCK_TIME: 2 + FAILED_LOGIN_ATTEMPTS: 5 + register: result + + - assert: + that: result is changed + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - assert: + that: result is not changed + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + + - block: + - name: retrieve create request + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: result + - assert: + that: + - "{{ 'PASSWORD_LOCK_TIME 2' in result.stdout }}" + - "{{ 'FAILED_LOGIN_ATTEMPTS 5' in result.stdout }}" + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') + + - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} + + - include: assert_no_user.yml user_name={{user_name_1}} diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index a744050a..e2bab700 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -36,6 +36,7 @@ login_port: '{{ mysql_primary_port }}' block: + - include: issue-49.yml - include: issue-28.yml @@ -79,7 +80,7 @@ - name: assert output message mysql user was removed assert: that: - - "result.changed == true" + - result is changed - include: assert_no_user.yml user_name={{user_name_1}} @@ -236,6 +237,7 @@ # Test plaintext and encrypted password scenarios. # - include: test_user_password.yml + - include: test_user_password_update.yml # ============================================================ # Test plugin authentication scenarios. diff --git a/tests/integration/targets/test_mysql_user/tasks/remove_user.yml b/tests/integration/targets/test_mysql_user/tasks/remove_user.yml index 45a0ad4c..ca9f74a7 100644 --- a/tests/integration/targets/test_mysql_user/tasks/remove_user.yml +++ b/tests/integration/targets/test_mysql_user/tasks/remove_user.yml @@ -37,7 +37,7 @@ - name: assert output message mysql user was removed assert: that: - - "result.changed == true" + - result is changed # ============================================================ - name: create blank mysql user to be removed later @@ -58,7 +58,7 @@ - name: assert changed is true for removing all blank users assert: that: - - "result.changed == true" + - result is changed - name: remove blank mysql user with hosts=all (expect ok) mysql_user: diff --git a/tests/integration/targets/test_mysql_user/tasks/test_privs.yml b/tests/integration/targets/test_mysql_user/tasks/test_privs.yml index 4ed75d17..bc5f39cf 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_privs.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_privs.yml @@ -51,7 +51,7 @@ - name: assert output message for current privileges assert: that: - - "result.changed == true" + - result is changed - name: run command to show privileges for user (expect privileges in stdout) command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{user_name_2}}'@'localhost'\"" @@ -101,7 +101,7 @@ - name: Assert that priv changed assert: that: - - "result.changed == true" + - result is changed - name: Add privs to a specific table (expect ok) mysql_user: @@ -162,7 +162,7 @@ - name: Assert that priv changed assert: that: - - "result.changed == true" + - result is changed - name: Test idempotency (expect ok) mysql_user: diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_password_update.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_password_update.yml new file mode 100644 index 00000000..09e8d69b --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_password_update.yml @@ -0,0 +1,178 @@ +# test code update password for the mysql_user module +# (c) 2014, Wayne Rosario + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 dof the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + + block: + + # ============================================================ + # Update user password for a user. + # Assert the user password is updated and old password can no longer be used. + # + - name: create user1 state=present with a password + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + priv: '*.*:ALL' + state: present + + - name: create user2 state=present with a password + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + priv: '*.*:ALL' + state: present + + - name: store user2 grants with old password (mysql 5.7.6 and newer) + command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ user_name_2 }}'@'localhost'\"" + register: user_password_old_create + ignore_errors: yes + + - name: store user2 grants with old password (mysql 5.7.5 and older) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_2 }}'@'localhost'\"" + register: user_password_old + when: user_password_old_create is failed + + - name: update user2 state=present with same password (expect changed=false) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + priv: '*.*:ALL' + state: present + register: result + + - name: assert output user2 was not updated + assert: + that: + - "result.changed == false" + + - include: assert_user.yml user_name={{user_name_2}} priv='ALL PRIVILEGES' + + - name: update user2 state=present with a new password (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_1 }}' + state: present + register: result + + - include: assert_user.yml user_name={{user_name_2}} priv='ALL PRIVILEGES' + + - name: store user2 grants with old password (mysql 5.7.6 and newer) + command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ user_name_2 }}'@'localhost'\"" + register: user_password_new_create + ignore_errors: yes + + - name: store user2 grants with new password + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_2 }}'@'localhost'\"" + register: user_password_new + when: user_password_new_create is failed + + - name: assert output message password was update for user2 (mysql 5.7.6 and newer) + assert: + that: + - "user_password_old_create.stdout != user_password_new_create.stdout" + when: user_password_new_create is not failed + + - name: assert output message password was update for user2 (mysql 5.7.5 and older) + assert: + that: + - "user_password_old.stdout != user_password_new.stdout" + when: user_password_new_create is failed + + - name: create database using user2 and old password + mysql_db: + login_user: '{{ user_name_2 }}' + login_password: '{{ user_password_2 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: present + ignore_errors: true + register: result + + - debug: var=result.msg + - name: assert output message that database not create with old password + assert: + that: + - "result.failed == true" + + - name: create database using user2 and new password + mysql_db: + login_user: '{{ user_name_2 }}' + login_password: '{{ user_password_1 }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: present + register: result + + - name: assert output message that database is created with new password + assert: + that: + - result is changed + + - name: remove database + mysql_db: + <<: *mysql_params + name: '{{ db_name }}' + state: absent + login_unix_socket: '{{ mysql_socket }}' + + - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} + + - include: remove_user.yml user_name={{user_name_2}} user_password={{ user_password_1 }} + + - name: Create user with Fdt8fd^34ds using hash. (expect changed=true) + mysql_user: + <<: *mysql_params + name: jmainguy + password: '*0cb5b86f23fdc24db19a29b8854eb860cbc47793' + encrypted: yes + register: encrypt_result + + - name: Check that the module made a change + assert: + that: + - "encrypt_result.changed == True" + + - name: See if the password needs to be updated. (expect changed=false) + mysql_user: + <<: *mysql_params + name: jmainguy + password: 'Fdt8fd^34ds' + register: plain_result + + - name: Check that the module did not change the password + assert: + that: + - "plain_result.changed == False" + + - name: Remove user (cleanup) + mysql_user: + <<: *mysql_params + name: jmainguy + state: absent