Skip to content

Commit

Permalink
Fix mysql_user on_new_username IndexError (#642)
Browse files Browse the repository at this point in the history
* fix tuple indexerror when no accounts are found

* Fix tests for update_password not executed

* Add test for case where existing user have different password

* lint to prevent warning about jinja templating in when clause

* Refactor get_existing_authentication to return a list of all row found

Previously we were returning only the first row found. We need to be
able to see if there is a difference in the existing passwords.

* Refactor host option to be optional

This make it possible to use the same method from mysql_user to help
update_password retrieve existing password for all account with the same
username independently of their hostname. And from mysql_info to get
the password of a specif user using WHERE user = '' AND host = ''

* Add change log fragment

* Add link to the PR in the change log

* lint for ansible devel

* Fix templating type error could not cconvert to bool with ansible devel

* Revert changes made for ansible-devel that broke tests for Ansible 2.15

* Revert changes made for ansible-devel that broke tests

* Cut unnecessary set, uniqueness is ensured by the group_by in the query

* Cut auth plugin from returned values when multiple existing auths exists

Discussed here:
https://github.com/ansible-collections/community.mysql/pull/642/files#r1649720519

* fix convertion of list(dict) to list(tuple)

* Fix test for empty password on MySQL 8+
  • Loading branch information
laurent-indermuehle authored Jun 27, 2024
1 parent 1922e71 commit 33e8754
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 72 deletions.
6 changes: 6 additions & 0 deletions changelogs/fragments/lie_fix_mysql_user_on_new_username.yml
Original file line number Diff line number Diff line change
@@ -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).
89 changes: 58 additions & 31 deletions plugins/module_utils/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion plugins/modules/mysql_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,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
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/targets/test_mysql_user/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -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 }}'
Expand All @@ -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 }}"
37 changes: 20 additions & 17 deletions tests/integration/targets/test_mysql_variables/tasks/issue-28.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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 </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}"
dest: /tmp/cert.pem
delegate_to: localhost

- name: Drop mysql user if exists
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ user_name_1 }}'
host_all: true
state: absent
ignore_errors: yes
ignore_errors: true

- name: create user with ssl requirement
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ user_name_1 }}"
host: '%'
Expand All @@ -40,30 +43,32 @@
SSL:

- name: attempt connection with newly created user (expect failure)
mysql_variables:
community.mysql.mysql_variables:
variable: '{{ set_name }}'
login_user: '{{ user_name_1 }}'
login_password: '{{ user_password_1 }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
ca_cert: /tmp/cert.pem
register: result
ignore_errors: yes
ignore_errors: true

- assert:
- name: Assert that result is failed for pymysql
ansible.builtin.assert:
that:
- result is failed
when:
- connector_name == 'pymysql'

- assert:
- name: Assert that result is success for mysqlclient
ansible.builtin.assert:
that:
- result is succeeded
when:
- connector_name != 'pymysql'

- name: attempt connection with newly created user ignoring hostname
mysql_variables:
community.mysql.mysql_variables:
variable: '{{ set_name }}'
login_user: '{{ user_name_1 }}'
login_password: '{{ user_password_1 }}'
Expand All @@ -72,14 +77,12 @@
ca_cert: /tmp/cert.pem
check_hostname: no
register: result
ignore_errors: yes

- assert:
that:
- result is succeeded or 'pymysql >= 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 33e8754

Please sign in to comment.