From be0244e5bc4f8496dbaf53ddfd94f632d3caf8bf Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 12:57:33 +0200 Subject: [PATCH 01/27] Introduce account locking functionality --- plugins/modules/mysql_user.py | 115 ++++++++++++-- .../test_mysql_user/tasks/issue-49.yml | 148 ++++++++++++++++++ .../targets/test_mysql_user/tasks/main.yml | 1 + 3 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 tests/integration/targets/test_mysql_user/tasks/issue-49.yml diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 0c757fa..43e5734 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -116,6 +116,14 @@ options: - 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 + version_added: '1.1.2' notes: - "MySQL server installs with default I(login_user) of C(root) and no password. @@ -139,6 +147,7 @@ author: - Jonathan Mainguy (@Jmainguy) - Benjamin Malynovytch (@bmalynovytch) - Lukasz Tomaszkiewicz (@tomaszkiewicz) +- Jorge Rodriguez (@Jorge-Rodriguez) extends_documentation_fragment: - community.mysql.mysql @@ -188,6 +197,22 @@ EXAMPLES = r''' '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. @@ -386,6 +411,50 @@ def supports_identified_by_password(cursor): return LooseVersion(version_str) < LooseVersion('8') +def validate_account_locking(cursor, account_locking): + cursor.execute("SELECT VERSION()") + result = cursor.fetchone() + version_str = result[0] + version = version_str.split('.') + + locking = {} + + if 'mariadb' in version_str.lower(): + msg = "MariaDB does not support this manner of account locking. Use the MAX_PASSWORD_ERRORS server variable instead." + else: + if int(version[0]) * 1000 + int(version[2]) < 8019: + msg = "MySQL is too old to support this manner of account locking." + else: + msg = None + 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)) + } + return msg, locking + + +def get_account_locking(cursor, user, host): + cursor.execute("SELECT VERSION()") + result = cursor.fetchone() + version_str = result[0] + version = version_str.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 +495,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 +504,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,13 +553,14 @@ 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): # we cannot create users without a proper hostname if host_all: return False + msg, locking = validate_account_locking(cursor, account_locking) if check_mode: - return True + return (True, msg) # Determine what user management method server uses old_user_mgmt = use_old_user_mgmt(cursor) @@ -519,7 +596,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 (True, msg) def is_hash(password): @@ -532,7 +609,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 @@ -714,6 +791,20 @@ def user_mod(cursor, user, host, host_all, password, encrypted, cursor.execute(*query_with_args) changed = True + # Handle Account locking + note, locking = validate_account_locking(cursor, account_locking) + if note: + module.warn(note) + module.warn("Account locking settings are being ignored.") + current_locking = get_account_locking(cursor, user, host) + clear_locking = {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 +1122,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 +1146,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 +1205,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)) @@ -1127,7 +1220,7 @@ def main(): try: changed = user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, - priv, tls_requires, module.check_mode) + priv, tls_requires, account_locking, module.check_mode) if changed: msg = "User added" 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 0000000..584eb71 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml @@ -0,0 +1,148 @@ +--- +- 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 + + - include: assert_no_user.yml user_name={{ user_name_1 }} + + - 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 + + - 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 + + - 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 + + - 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 a744050..62c623d 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 From 9ffdeb0be399e7bd0e763a701606f3acdaf523a6 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 12:58:20 +0200 Subject: [PATCH 02/27] Fix assertions --- .../test_mysql_user/tasks/create_user.yml | 2 +- .../targets/test_mysql_user/tasks/main.yml | 2 +- .../test_mysql_user/tasks/remove_user.yml | 4 +- .../test_mysql_user/tasks/test_privs.yml | 6 +- .../tasks/user_password_update_test.yml | 178 ++++++++++++++++++ 5 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 tests/integration/targets/test_mysql_user/tasks/user_password_update_test.yml 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 790d9bb..9c3459a 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/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index 62c623d..3a82b69 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -80,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}} 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 45a0ad4..ca9f74a 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 4ed75d1..bc5f39c 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/user_password_update_test.yml b/tests/integration/targets/test_mysql_user/tasks/user_password_update_test.yml new file mode 100644 index 0000000..25056e4 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/user_password_update_test.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 From 5646003189b013d041e2438ee3e733038c282347 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 13:36:29 +0200 Subject: [PATCH 03/27] Fix parsing of versions with dashes --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 43e5734..de7f019 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -415,7 +415,7 @@ def validate_account_locking(cursor, account_locking): cursor.execute("SELECT VERSION()") result = cursor.fetchone() version_str = result[0] - version = version_str.split('.') + version = version_str.split('-')[0].split('.') locking = {} From 46d7f4106ebacffc6d128cfff57ac1dacc71d649 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 13:39:36 +0200 Subject: [PATCH 04/27] Make dictionary comprehension 2.6 compatible --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index de7f019..ae7cd4e 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -797,7 +797,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, module.warn(note) module.warn("Account locking settings are being ignored.") current_locking = get_account_locking(cursor, user, host) - clear_locking = {x: y for x, y in locking.items() if y != '0'} + 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: From da9d82ecc66b9de0ec80819082bd6c539a3c8f21 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 13:45:12 +0200 Subject: [PATCH 05/27] Fix version added --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index ae7cd4e..7081548 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -123,7 +123,7 @@ options: - 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 - version_added: '1.1.2' + version_added: '1.1.0' notes: - "MySQL server installs with default I(login_user) of C(root) and no password. From 0532ebd6ca7f836855e90f33e51c5459b87a4769 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 13:56:08 +0200 Subject: [PATCH 06/27] Fix parsing of versions with dashes --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 7081548..5115072 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -438,7 +438,7 @@ def get_account_locking(cursor, user, host): cursor.execute("SELECT VERSION()") result = cursor.fetchone() version_str = result[0] - version = version_str.split('.') + version = version_str.split('-')[0].split('.') locking = {} From 2f26a6681f30cbd83cab959f3126e9701deef5fd Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 14:52:59 +0200 Subject: [PATCH 07/27] Add account locking warnings on user add --- plugins/modules/mysql_user.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 5115072..045467b 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -553,12 +553,16 @@ 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, account_locking, check_mode): + tls_requires, account_locking, check_mode, module): # we cannot create users without a proper hostname if host_all: return False msg, locking = validate_account_locking(cursor, account_locking) + if msg: + module.warn(msg) + module.warn("Account locking settings are being ignored.") + if check_mode: return (True, msg) @@ -1219,8 +1223,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, account_locking, module.check_mode) + plugin, plugin_hash_string, plugin_auth_string, priv, + tls_requires, account_locking, module.check_mode, module) if changed: msg = "User added" From 6d795eab69578502923424ee70beac790beed65d Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 14:53:16 +0200 Subject: [PATCH 08/27] Fix assertions for versions that do not support account locking --- .../targets/test_mysql_user/tasks/issue-49.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-49.yml b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml index 584eb71..d2638e1 100644 --- a/tests/integration/targets/test_mysql_user/tasks/issue-49.yml +++ b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml @@ -109,6 +109,11 @@ - 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 @@ -132,6 +137,11 @@ - 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 From aaf70204cbf0a29e3f98522fc67e5c1b4a32daa9 Mon Sep 17 00:00:00 2001 From: "Jorge Rodriguez (A.K.A. Tiriel)" Date: Tue, 17 Nov 2020 15:16:02 +0200 Subject: [PATCH 09/27] Fix version added Co-authored-by: Andrew Klychkov --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 045467b..cd4feee 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -123,7 +123,7 @@ options: - 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 - version_added: '1.1.0' + version_added: '1.2.0' notes: - "MySQL server installs with default I(login_user) of C(root) and no password. From 9730eeb52c9bbdeb97072dd778dfb8d0ea053d0e Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Tue, 17 Nov 2020 15:30:46 +0200 Subject: [PATCH 10/27] Log account locking warnings only when locking is requested --- plugins/modules/mysql_user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index cd4feee..89662b3 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -559,7 +559,7 @@ def user_add(cursor, user, host, host_all, password, encrypted, return False msg, locking = validate_account_locking(cursor, account_locking) - if msg: + if msg and account_locking: module.warn(msg) module.warn("Account locking settings are being ignored.") @@ -797,7 +797,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, # Handle Account locking note, locking = validate_account_locking(cursor, account_locking) - if note: + if note and account_locking: module.warn(note) module.warn("Account locking settings are being ignored.") current_locking = get_account_locking(cursor, user, host) From c2d5b97b7adb51f28d5337c9c548daef976dc54e Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sat, 21 Nov 2020 09:43:15 +0200 Subject: [PATCH 11/27] Add changelog fragment --- changelogs/fragments/49-mysql_user_login_tracking.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/49-mysql_user_login_tracking.yml 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 0000000..69fa61d --- /dev/null +++ b/changelogs/fragments/49-mysql_user_login_tracking.yml @@ -0,0 +1,2 @@ +minor_changes: +- mysql_user - add support for login attempt tracking and account locking feature (https://github.com/ansible-collections/community.mysql/pull/62). From 800d9a553bc0d289134ee964a720861c8bc04079 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 29 Nov 2020 22:02:26 +0200 Subject: [PATCH 12/27] Update changelog --- changelogs/fragments/49-mysql_user_login_tracking.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/49-mysql_user_login_tracking.yml b/changelogs/fragments/49-mysql_user_login_tracking.yml index 69fa61d..28ed1e6 100644 --- a/changelogs/fragments/49-mysql_user_login_tracking.yml +++ b/changelogs/fragments/49-mysql_user_login_tracking.yml @@ -1,2 +1,2 @@ minor_changes: -- mysql_user - add support for login attempt tracking and account locking feature (https://github.com/ansible-collections/community.mysql/pull/62). +- mysql_user - add the ``account_locking`` option sot support login attempt tracking and account locking feature (https://github.com/ansible-collections/community.mysql/issues/49). From 9a303ac55afa70187cdd46999713a849598c1095 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 29 Nov 2020 22:02:53 +0200 Subject: [PATCH 13/27] Update module documentation --- plugins/modules/mysql_user.py | 24 +++- .../test_mysql_user/tasks/issue-49.yml | 116 ++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 89662b3..cc0059b 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -119,10 +119,23 @@ options: 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)." + - 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: @@ -242,7 +255,14 @@ EXAMPLES = r''' 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 diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-49.yml b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml index d2638e1..486e5c8 100644 --- a/tests/integration/targets/test_mysql_user/tasks/issue-49.yml +++ b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml @@ -40,7 +40,118 @@ 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 changed + 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 changed + 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 changed + 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 changed + 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: @@ -56,6 +167,11 @@ that: - result is changed + - assert: + that: + - result is changed + when: version_string is version('8.0.19', '<') or version_string is version('10', '>=') + - include: assert_user.yml user_name={{ user_name_1 }} - block: From de75697d5f569b6784aa625d5b4bada4d9feb5d5 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 29 Nov 2020 22:03:54 +0200 Subject: [PATCH 14/27] Check `account_locking` values --- plugins/modules/mysql_user.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index cc0059b..6d8631d 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -440,10 +440,12 @@ def validate_account_locking(cursor, account_locking): locking = {} if 'mariadb' in version_str.lower(): - msg = "MariaDB does not support this manner of account locking. Use the MAX_PASSWORD_ERRORS server variable instead." + 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: - msg = "MySQL is too old to support this manner of account locking." + module.warn("MySQL is too old to support this manner of account locking.") + module.warn("Account locking settings are being ignored.") else: msg = None if account_locking is not None: @@ -451,7 +453,11 @@ def validate_account_locking(cursor, account_locking): "FAILED_LOGIN_ATTEMPTS": str(account_locking.get("FAILED_LOGIN_ATTEMPTS", 0)), "PASSWORD_LOCK_TIME": str(account_locking.get("PASSWORD_LOCK_TIME", 0)) } - return msg, locking + 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 not re.match("[-+]?\\d+$", locking["PASSWORD_LOCK_TIME"]) and locking["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): @@ -578,13 +584,10 @@ def user_add(cursor, user, host, host_all, password, encrypted, if host_all: return False - msg, locking = validate_account_locking(cursor, account_locking) - if msg and account_locking: - module.warn(msg) - module.warn("Account locking settings are being ignored.") + locking = validate_account_locking(cursor, account_locking, module) if check_mode: - return (True, msg) + return True # Determine what user management method server uses old_user_mgmt = use_old_user_mgmt(cursor) @@ -816,10 +819,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, changed = True # Handle Account locking - note, locking = validate_account_locking(cursor, account_locking) - if note and account_locking: - module.warn(note) - module.warn("Account locking settings are being ignored.") + 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: From 58d21f6f0ec145d480801fdcf6cccd7381be7037 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 29 Nov 2020 22:05:12 +0200 Subject: [PATCH 15/27] Remove reference to unused `msg` --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 6d8631d..3d8218d 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -623,7 +623,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, msg) + return True def is_hash(password): From 0502359633ab65fa8afc463e52605a9db2e32c0c Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Mon, 30 Nov 2020 08:45:08 +0200 Subject: [PATCH 16/27] Fix docstring --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 3d8218d..3930165 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -119,7 +119,7 @@ options: 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). + - "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 From f41a9ce08b99c6db321be0462f3532ccc48dfcac Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Mon, 30 Nov 2020 10:25:14 +0200 Subject: [PATCH 17/27] Fix key error --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 3930165..e71b658 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -455,7 +455,7 @@ def validate_account_locking(cursor, account_locking): } 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 not re.match("[-+]?\\d+$", locking["PASSWORD_LOCK_TIME"]) and locking["PASSWORD_LOCK_TIME"] != "UNBOUNDED": + if 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 From 64aae9ba08a4d2a575382e8d11b28971815a83db Mon Sep 17 00:00:00 2001 From: "Jorge Rodriguez (A.K.A. Tiriel)" Date: Mon, 30 Nov 2020 11:57:59 +0200 Subject: [PATCH 18/27] Supply empty string to re.match if value is not defined --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index e71b658..2a6f09f 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -455,7 +455,7 @@ def validate_account_locking(cursor, account_locking): } 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 not re.match("[-+]?\\d+$", locking.get("PASSWORD_LOCK_TIME")) and locking.get("PASSWORD_LOCK_TIME") != "UNBOUNDED": + if 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 From bb45038ce7e640adb5c3a97dbce45acf4d388c08 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Mon, 30 Nov 2020 12:29:16 +0200 Subject: [PATCH 19/27] Fix password lock time check --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 2a6f09f..d163079 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -455,7 +455,7 @@ def validate_account_locking(cursor, account_locking): } 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 not re.match("[-+]?\\d+$", locking.get("PASSWORD_LOCK_TIME", "")) and locking.get("PASSWORD_LOCK_TIME") != "UNBOUNDED": + 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 From 8f337edbe9bdae7557c48d54b183d3c7f4af656c Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 7 Feb 2021 10:31:42 +0200 Subject: [PATCH 20/27] Fix function signature --- plugins/modules/mysql_user.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index d163079..a5d729c 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -431,7 +431,7 @@ def supports_identified_by_password(cursor): return LooseVersion(version_str) < LooseVersion('8') -def validate_account_locking(cursor, account_locking): +def validate_account_locking(cursor, account_locking, module): cursor.execute("SELECT VERSION()") result = cursor.fetchone() version_str = result[0] @@ -447,7 +447,6 @@ def validate_account_locking(cursor, account_locking): module.warn("MySQL is too old to support this manner of account locking.") module.warn("Account locking settings are being ignored.") else: - msg = None if account_locking is not None: locking = { "FAILED_LOGIN_ATTEMPTS": str(account_locking.get("FAILED_LOGIN_ATTEMPTS", 0)), From 2fed868087a0ba4d6c051ff66d3087d48792e699 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 7 Feb 2021 10:37:14 +0200 Subject: [PATCH 21/27] Fix PEP8 error --- plugins/modules/mysql_user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index a5d729c..72057ae 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -454,7 +454,9 @@ def validate_account_locking(cursor, account_locking, module): } 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": + 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 From e9fd1db20092c0757ab68be28a4ad29e1a6037e8 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 7 Feb 2021 11:01:56 +0200 Subject: [PATCH 22/27] Fix function call --- plugins/modules/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 72057ae..6923d1c 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -617,7 +617,7 @@ def user_add(cursor, user, host, host_all, password, encrypted, query_with_args = "CREATE USER %s@%s", (user, host) query_with_args_and_tls_requires = query_with_args + (tls_requires,) - cursor.execute(*mogrify(*query_with_args_and_tls_requires)) + cursor.execute(*mogrify(*query_with_args_and_tls_requires, locking)) if new_priv is not None: for db_table, priv in iteritems(new_priv): From 1146f66e09038665854a1d42148a34a6db0d68dd Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 7 Feb 2021 12:25:06 +0200 Subject: [PATCH 23/27] Fix argument tuple --- plugins/modules/mysql_user.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 6923d1c..3fe13ac 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -616,8 +616,8 @@ 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,) - cursor.execute(*mogrify(*query_with_args_and_tls_requires, locking)) + 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: for db_table, priv in iteritems(new_priv): @@ -645,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: @@ -811,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) From d7c931f84f0764c235dbef74e748a027d035255b Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Sun, 7 Feb 2021 13:30:59 +0200 Subject: [PATCH 24/27] Fix test conditionals --- .../targets/test_mysql_user/tasks/issue-49.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-49.yml b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml index 486e5c8..742e39d 100644 --- a/tests/integration/targets/test_mysql_user/tasks/issue-49.yml +++ b/tests/integration/targets/test_mysql_user/tasks/issue-49.yml @@ -81,7 +81,7 @@ - assert: that: - - result is changed + - 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 @@ -103,7 +103,7 @@ - assert: that: - - result is changed + - 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 @@ -125,7 +125,7 @@ - assert: that: - - result is changed + - 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 @@ -147,7 +147,7 @@ - assert: that: - - result is changed + - 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 }} @@ -164,12 +164,13 @@ register: result - assert: - that: - - result is changed + that: + - result is changed + when: version_string is version('8.0.19', '>=') and version_string is version('10', '<') - assert: that: - - result is changed + - 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 }} From 7bcf810afd310f72db9cf50e4cc7c1891ccc2688 Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Thu, 11 Feb 2021 09:18:18 +0200 Subject: [PATCH 25/27] Fix changelog typo --- changelogs/fragments/49-mysql_user_login_tracking.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/49-mysql_user_login_tracking.yml b/changelogs/fragments/49-mysql_user_login_tracking.yml index 28ed1e6..652d7e6 100644 --- a/changelogs/fragments/49-mysql_user_login_tracking.yml +++ b/changelogs/fragments/49-mysql_user_login_tracking.yml @@ -1,2 +1,2 @@ minor_changes: -- mysql_user - add the ``account_locking`` option sot support login attempt tracking and account locking feature (https://github.com/ansible-collections/community.mysql/issues/49). +- 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). From d3d68e44a0ae54e348e6451ccc21c165dd0e5fef Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Thu, 11 Feb 2021 09:23:45 +0200 Subject: [PATCH 26/27] Include the password update tests in the play. --- tests/integration/targets/test_mysql_user/tasks/main.yml | 1 + ...er_password_update_test.yml => test_user_password_update.yml} | 0 2 files changed, 1 insertion(+) rename tests/integration/targets/test_mysql_user/tasks/{user_password_update_test.yml => test_user_password_update.yml} (100%) diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index 3a82b69..e2bab70 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -237,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/user_password_update_test.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_password_update.yml similarity index 100% rename from tests/integration/targets/test_mysql_user/tasks/user_password_update_test.yml rename to tests/integration/targets/test_mysql_user/tasks/test_user_password_update.yml From f43676cb1ffa5d05502107b8bf86ece32a5aacfb Mon Sep 17 00:00:00 2001 From: Jorge-Rodriguez Date: Thu, 11 Feb 2021 18:02:08 +0200 Subject: [PATCH 27/27] Fix command quotes --- .../targets/test_mysql_user/tasks/test_user_password_update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 25056e4..09e8d69 100644 --- 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 @@ -87,7 +87,7 @@ ignore_errors: yes - name: store user2 grants with new password - command: "{{ mysql_command }} -e SHOW GRANTS FOR '{{ user_name_2 }}'@'localhost'\"" + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_2 }}'@'localhost'\"" register: user_password_new when: user_password_new_create is failed