diff --git a/changelogs/fragments/lie_fix_mysql_user_on_new_username.yml b/changelogs/fragments/lie_fix_mysql_user_on_new_username.yml new file mode 100644 index 000000000..7f13738b0 --- /dev/null +++ b/changelogs/fragments/lie_fix_mysql_user_on_new_username.yml @@ -0,0 +1,6 @@ +--- + +bugfixes: + + - mysql_user - Fixed an IndexError in the update_password functionality introduced in PR https://github.com/ansible-collections/community.mysql/pull/580 and released in community.mysql 3.8.0. If you used this functionality, please avoid versions 3.8.0 to 3.9.0 (https://github.com/ansible-collections/community.mysql/pull/642). + - mysql_user - Added a warning to update_password's on_new_username option if multiple accounts with the same username but different passwords exist (https://github.com/ansible-collections/community.mysql/pull/642). diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 80da47e73..bd7169184 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -95,8 +95,12 @@ def get_grants(cursor, user, host): return grants.split(", ") -def get_existing_authentication(cursor, user, host): - # Return the plugin and auth_string if there is exactly one distinct existing plugin and auth_string. +def get_existing_authentication(cursor, user, host=None): + """ Return a list of dict containing the plugin and auth_string for the + specified username. + If hostname is provided, return only the information about this particular + account. + """ cursor.execute("SELECT VERSION()") srv_type = cursor.fetchone() # Mysql_info use a DictCursor so we must convert back to a list @@ -107,37 +111,50 @@ def get_existing_authentication(cursor, user, host): if 'mariadb' in srv_type[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 - and host=%(host)s - union select plugin, authentication_string as auth from mysql.user where user=%(user)s - and host=%(host)s) x group by plugin, auth limit 2 - """, {'user': user, 'host': host}) + if host: + cursor.execute("""select plugin, auth from ( + select plugin, password as auth from mysql.user where user=%(user)s + and host=%(host)s + union select plugin, authentication_string as auth from mysql.user where user=%(user)s + and host=%(host)s) x group by plugin, auth + """, {'user': user, 'host': host}) + else: + 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 + """, {'user': user}) else: - cursor.execute("""select plugin, authentication_string as auth - from mysql.user where user=%(user)s and host=%(host)s - group by plugin, authentication_string limit 2""", {'user': user, 'host': host}) + if host: + cursor.execute("""select plugin, authentication_string as auth + from mysql.user where user=%(user)s and host=%(host)s + group by plugin, authentication_string""", {'user': user, 'host': host}) + else: + cursor.execute("""select plugin, authentication_string as auth + from mysql.user where user=%(user)s + group by plugin, authentication_string""", {'user': user}) + rows = cursor.fetchall() - # Mysql_info use a DictCursor so we must convert back to a list - # otherwise we get KeyError 0 - if isinstance(rows, dict): - rows = list(rows.values()) + if len(rows) == 0: + return [] - # 'plugin_auth_string' contains the hash string. Must be removed in c.mysql 4.0 - # See https://github.com/ansible-collections/community.mysql/pull/629 - if isinstance(rows[0], tuple): - return {'plugin': rows[0][0], - 'plugin_auth_string': rows[0][1], - 'plugin_hash_string': rows[0][1]} + # Mysql_info use a DictCursor so we must convert list(dict) + # to list(tuple) otherwise we get KeyError 0 + if isinstance(rows[0], dict): + rows = [tuple(row.values()) for row in rows] + + existing_auth_list = [] # 'plugin_auth_string' contains the hash string. Must be removed in c.mysql 4.0 # See https://github.com/ansible-collections/community.mysql/pull/629 - if isinstance(rows[0], dict): - return {'plugin': rows[0].get('plugin'), - 'plugin_auth_string': rows[0].get('auth'), - 'plugin_hash_string': rows[0].get('auth')} - return None + for r in rows: + existing_auth_list.append({ + 'plugin': r[0], + 'plugin_auth_string': r[1], + 'plugin_hash_string': r[1]}) + + return existing_auth_list def user_add(cursor, user, host, host_all, password, encrypted, @@ -161,14 +178,24 @@ def user_add(cursor, user, host, host_all, password, encrypted, mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires + # This is for update_password: on_new_username used_existing_password = False if reuse_existing_password: - existing_auth = get_existing_authentication(cursor, user, host) + existing_auth = get_existing_authentication(cursor, user) if existing_auth: - plugin = existing_auth['plugin'] - plugin_hash_string = existing_auth['plugin_hash_string'] - password = None - used_existing_password = True + if len(existing_auth) != 1: + module.warn("An account with the username %s has a different " + "password than the others existing accounts. Thus " + "on_new_username can't decide which password to " + "reuse so it will use your provided password " + "instead. If no password is provided, the account " + "will have an empty password!" % user) + used_existing_password = False + else: + plugin_hash_string = existing_auth[0]['plugin_hash_string'] + password = None + used_existing_password = True + plugin = existing_auth[0]['plugin'] # What if plugin differ? 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) diff --git a/plugins/modules/mysql_info.py b/plugins/modules/mysql_info.py index c119b8df0..acdad2f94 100644 --- a/plugins/modules/mysql_info.py +++ b/plugins/modules/mysql_info.py @@ -633,7 +633,7 @@ def __get_users_info(self): authentications = get_existing_authentication(self.cursor, user, host) if authentications: - output_dict.update(authentications) + output_dict.update(authentications[0]) # TODO password_option # TODO lock_option diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index 8ec0798c5..e77c44367 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -295,3 +295,7 @@ - name: Mysql_user - test column case sensitive ansible.builtin.import_tasks: file: test_column_case_sensitive.yml + + - name: Mysql_user - test update_password + ansible.builtin.import_tasks: + file: test_update_password.yml diff --git a/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml b/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml index 428c1ef78..adaa7c728 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml @@ -127,3 +127,29 @@ update_password: on_create - username: test3 update_password: on_new_username + + # another new user, another new password and multiple existing users with + # varying passwords without providing a password + - name: update_password | Create account with on_new_username while omit password + community.mysql.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: test3 + host: '10.10.10.10' + update_password: on_new_username + + - name: update_password | Assert create account with on_new_username while omit password produce empty auth string + ansible.builtin.command: >- + {{ mysql_command }} -BNe "SELECT user, host, plugin, authentication_string + FROM mysql.user where user='test3' and host='10.10.10.10'" + register: test3_info + changed_when: false + failed_when: + # MariaDB default plugin is mysql_native_password + - "'test3\t10.10.10.10\tmysql_native_password\t' != test3_info.stdout" + + # MySQL 8+ default plugin is caching_sha2_password + - "'test3\t10.10.10.10\tcaching_sha2_password\t' != test3_info.stdout" diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml index d95e53b8c..e6bd69535 100644 --- a/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml +++ b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml @@ -1,6 +1,6 @@ --- - name: Utils | Assert user password | Apply update_password to {{ username }} - mysql_user: + community.mysql.mysql_user: login_user: '{{ mysql_parameters.login_user }}' login_password: '{{ mysql_parameters.login_password }}' login_host: '{{ mysql_parameters.login_host }}' @@ -13,16 +13,17 @@ register: result - name: Utils | Assert user password | Assert a change occurred - assert: + ansible.builtin.assert: that: - - "result.changed | bool == {{ expect_change }} | bool" - - "result.password_changed == {{ expect_password_change }}" + - result.changed | bool == expect_change | bool + - result.password_changed == expect_password_change -- name: Utils | Assert user password | Query user {{ username }} - command: "{{ mysql_command }} -BNe \"SELECT plugin, authentication_string FROM mysql.user where user='{{ username }}' and host='{{ host }}'\"" +- name: Utils | Assert user password | Assert expect_hash is in user stdout for {{ username }} + ansible.builtin.command: >- + {{ mysql_command }} -BNe "SELECT plugin, authentication_string + FROM mysql.user where user='{{ username }}' and host='{{ host }}'" register: existing_user - -- name: Utils | Assert user password | Assert expect_hash is in user stdout - assert: - that: - - "'mysql_native_password\t{{ expect_password_hash }}' in existing_user.stdout_lines" + changed_when: false + failed_when: pattern not in existing_user.stdout_lines + vars: + pattern: "mysql_native_password\t{{ expect_password_hash }}" diff --git a/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml b/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml index 10a915415..89d3d260a 100644 --- a/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml @@ -1,8 +1,11 @@ --- - name: set fact tls_enabled - command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" + ansible.builtin.command: + cmd: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" register: result -- set_fact: + +- name: Set tls_enabled fact + ansible.builtin.set_fact: tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" - vars: @@ -16,21 +19,21 @@ # ============================================================ - name: get server certificate - copy: + ansible.builtin.copy: content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null = 0.7.11 is required' in result.msg + ignore_errors: true + failed_when: + - result is failed or 'pymysql >= 0.7.11 is required' not in result.msg - name: Drop mysql user - mysql_user: + community.mysql.mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' host_all: true diff --git a/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml b/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml index 2d2318e2d..4a7fd0002 100644 --- a/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml +++ b/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml @@ -47,8 +47,8 @@ # Verify mysql_variable successfully updates a variable (issue:4568) # - set_fact: - set_name: 'delay_key_write' - set_value: 'ON' + set_name: 'delay_key_write' + set_value: 'ON' - name: set mysql variable mysql_variables: @@ -74,8 +74,8 @@ # Verify mysql_variable successfully updates a variable using single quotes # - set_fact: - set_name: 'wait_timeout' - set_value: '300' + set_name: 'wait_timeout' + set_value: '300' - name: set mysql variable to a temp value mysql_variables: @@ -105,8 +105,8 @@ # Verify mysql_variable successfully updates a variable using double quotes # - set_fact: - set_name: "wait_timeout" - set_value: "400" + set_name: "wait_timeout" + set_value: "400" - name: set mysql variable to a temp value mysql_variables: @@ -132,8 +132,8 @@ # Verify mysql_variable successfully updates a variable using no quotes # - set_fact: - set_name: wait_timeout - set_value: 500 + set_name: wait_timeout + set_value: 500 - name: set mysql variable to a temp value mysql_variables: @@ -251,8 +251,8 @@ # Verify mysql_variable works with the login_user and login_password parameters # - set_fact: - set_name: wait_timeout - set_value: 77 + set_name: wait_timeout + set_value: 77 - name: query mysql_variable using login_user and password_password mysql_variables: @@ -291,8 +291,8 @@ # Verify mysql_variable fails with an incorrect login_password parameter # - set_fact: - set_name: connect_timeout - set_value: 10 + set_name: connect_timeout + set_value: 10 - name: query mysql_variable using incorrect login_password mysql_variables: