Skip to content

Commit 33e8754

Browse files
Fix mysql_user on_new_username IndexError (#642)
* 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+
1 parent 1922e71 commit 33e8754

File tree

8 files changed

+139
-72
lines changed

8 files changed

+139
-72
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
3+
bugfixes:
4+
5+
- 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).
6+
- 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).

plugins/module_utils/user.py

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,12 @@ def get_grants(cursor, user, host):
9595
return grants.split(", ")
9696

9797

98-
def get_existing_authentication(cursor, user, host):
99-
# Return the plugin and auth_string if there is exactly one distinct existing plugin and auth_string.
98+
def get_existing_authentication(cursor, user, host=None):
99+
""" Return a list of dict containing the plugin and auth_string for the
100+
specified username.
101+
If hostname is provided, return only the information about this particular
102+
account.
103+
"""
100104
cursor.execute("SELECT VERSION()")
101105
srv_type = cursor.fetchone()
102106
# Mysql_info use a DictCursor so we must convert back to a list
@@ -107,37 +111,50 @@ def get_existing_authentication(cursor, user, host):
107111
if 'mariadb' in srv_type[0].lower():
108112
# before MariaDB 10.2.19 and 10.3.11, "password" and "authentication_string" can differ
109113
# when using mysql_native_password
110-
cursor.execute("""select plugin, auth from (
111-
select plugin, password as auth from mysql.user where user=%(user)s
112-
and host=%(host)s
113-
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
114-
and host=%(host)s) x group by plugin, auth limit 2
115-
""", {'user': user, 'host': host})
114+
if host:
115+
cursor.execute("""select plugin, auth from (
116+
select plugin, password as auth from mysql.user where user=%(user)s
117+
and host=%(host)s
118+
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
119+
and host=%(host)s) x group by plugin, auth
120+
""", {'user': user, 'host': host})
121+
else:
122+
cursor.execute("""select plugin, auth from (
123+
select plugin, password as auth from mysql.user where user=%(user)s
124+
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
125+
) x group by plugin, auth
126+
""", {'user': user})
116127
else:
117-
cursor.execute("""select plugin, authentication_string as auth
118-
from mysql.user where user=%(user)s and host=%(host)s
119-
group by plugin, authentication_string limit 2""", {'user': user, 'host': host})
128+
if host:
129+
cursor.execute("""select plugin, authentication_string as auth
130+
from mysql.user where user=%(user)s and host=%(host)s
131+
group by plugin, authentication_string""", {'user': user, 'host': host})
132+
else:
133+
cursor.execute("""select plugin, authentication_string as auth
134+
from mysql.user where user=%(user)s
135+
group by plugin, authentication_string""", {'user': user})
136+
120137
rows = cursor.fetchall()
121138

122-
# Mysql_info use a DictCursor so we must convert back to a list
123-
# otherwise we get KeyError 0
124-
if isinstance(rows, dict):
125-
rows = list(rows.values())
139+
if len(rows) == 0:
140+
return []
126141

127-
# 'plugin_auth_string' contains the hash string. Must be removed in c.mysql 4.0
128-
# See https://github.com/ansible-collections/community.mysql/pull/629
129-
if isinstance(rows[0], tuple):
130-
return {'plugin': rows[0][0],
131-
'plugin_auth_string': rows[0][1],
132-
'plugin_hash_string': rows[0][1]}
142+
# Mysql_info use a DictCursor so we must convert list(dict)
143+
# to list(tuple) otherwise we get KeyError 0
144+
if isinstance(rows[0], dict):
145+
rows = [tuple(row.values()) for row in rows]
146+
147+
existing_auth_list = []
133148

134149
# 'plugin_auth_string' contains the hash string. Must be removed in c.mysql 4.0
135150
# See https://github.com/ansible-collections/community.mysql/pull/629
136-
if isinstance(rows[0], dict):
137-
return {'plugin': rows[0].get('plugin'),
138-
'plugin_auth_string': rows[0].get('auth'),
139-
'plugin_hash_string': rows[0].get('auth')}
140-
return None
151+
for r in rows:
152+
existing_auth_list.append({
153+
'plugin': r[0],
154+
'plugin_auth_string': r[1],
155+
'plugin_hash_string': r[1]})
156+
157+
return existing_auth_list
141158

142159

143160
def user_add(cursor, user, host, host_all, password, encrypted,
@@ -161,14 +178,24 @@ def user_add(cursor, user, host, host_all, password, encrypted,
161178

162179
mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires
163180

181+
# This is for update_password: on_new_username
164182
used_existing_password = False
165183
if reuse_existing_password:
166-
existing_auth = get_existing_authentication(cursor, user, host)
184+
existing_auth = get_existing_authentication(cursor, user)
167185
if existing_auth:
168-
plugin = existing_auth['plugin']
169-
plugin_hash_string = existing_auth['plugin_hash_string']
170-
password = None
171-
used_existing_password = True
186+
if len(existing_auth) != 1:
187+
module.warn("An account with the username %s has a different "
188+
"password than the others existing accounts. Thus "
189+
"on_new_username can't decide which password to "
190+
"reuse so it will use your provided password "
191+
"instead. If no password is provided, the account "
192+
"will have an empty password!" % user)
193+
used_existing_password = False
194+
else:
195+
plugin_hash_string = existing_auth[0]['plugin_hash_string']
196+
password = None
197+
used_existing_password = True
198+
plugin = existing_auth[0]['plugin'] # What if plugin differ?
172199
if password and encrypted:
173200
if impl.supports_identified_by_password(cursor):
174201
query_with_args = "CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password)

plugins/modules/mysql_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ def __get_users_info(self):
639639

640640
authentications = get_existing_authentication(self.cursor, user, host)
641641
if authentications:
642-
output_dict.update(authentications)
642+
output_dict.update(authentications[0])
643643

644644
# TODO password_option
645645
# TODO lock_option

tests/integration/targets/test_mysql_user/tasks/main.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,7 @@
295295
- name: Mysql_user - test column case sensitive
296296
ansible.builtin.import_tasks:
297297
file: test_column_case_sensitive.yml
298+
299+
- name: Mysql_user - test update_password
300+
ansible.builtin.import_tasks:
301+
file: test_update_password.yml

tests/integration/targets/test_mysql_user/tasks/test_update_password.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,29 @@
127127
update_password: on_create
128128
- username: test3
129129
update_password: on_new_username
130+
131+
# another new user, another new password and multiple existing users with
132+
# varying passwords without providing a password
133+
- name: update_password | Create account with on_new_username while omit password
134+
community.mysql.mysql_user:
135+
login_user: '{{ mysql_parameters.login_user }}'
136+
login_password: '{{ mysql_parameters.login_password }}'
137+
login_host: '{{ mysql_parameters.login_host }}'
138+
login_port: '{{ mysql_parameters.login_port }}'
139+
state: present
140+
name: test3
141+
host: '10.10.10.10'
142+
update_password: on_new_username
143+
144+
- name: update_password | Assert create account with on_new_username while omit password produce empty auth string
145+
ansible.builtin.command: >-
146+
{{ mysql_command }} -BNe "SELECT user, host, plugin, authentication_string
147+
FROM mysql.user where user='test3' and host='10.10.10.10'"
148+
register: test3_info
149+
changed_when: false
150+
failed_when:
151+
# MariaDB default plugin is mysql_native_password
152+
- "'test3\t10.10.10.10\tmysql_native_password\t' != test3_info.stdout"
153+
154+
# MySQL 8+ default plugin is caching_sha2_password
155+
- "'test3\t10.10.10.10\tcaching_sha2_password\t' != test3_info.stdout"
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
- name: Utils | Assert user password | Apply update_password to {{ username }}
3-
mysql_user:
3+
community.mysql.mysql_user:
44
login_user: '{{ mysql_parameters.login_user }}'
55
login_password: '{{ mysql_parameters.login_password }}'
66
login_host: '{{ mysql_parameters.login_host }}'
@@ -13,16 +13,17 @@
1313
register: result
1414

1515
- name: Utils | Assert user password | Assert a change occurred
16-
assert:
16+
ansible.builtin.assert:
1717
that:
18-
- "result.changed | bool == {{ expect_change }} | bool"
19-
- "result.password_changed == {{ expect_password_change }}"
18+
- result.changed | bool == expect_change | bool
19+
- result.password_changed == expect_password_change
2020

21-
- name: Utils | Assert user password | Query user {{ username }}
22-
command: "{{ mysql_command }} -BNe \"SELECT plugin, authentication_string FROM mysql.user where user='{{ username }}' and host='{{ host }}'\""
21+
- name: Utils | Assert user password | Assert expect_hash is in user stdout for {{ username }}
22+
ansible.builtin.command: >-
23+
{{ mysql_command }} -BNe "SELECT plugin, authentication_string
24+
FROM mysql.user where user='{{ username }}' and host='{{ host }}'"
2325
register: existing_user
24-
25-
- name: Utils | Assert user password | Assert expect_hash is in user stdout
26-
assert:
27-
that:
28-
- "'mysql_native_password\t{{ expect_password_hash }}' in existing_user.stdout_lines"
26+
changed_when: false
27+
failed_when: pattern not in existing_user.stdout_lines
28+
vars:
29+
pattern: "mysql_native_password\t{{ expect_password_hash }}"

tests/integration/targets/test_mysql_variables/tasks/issue-28.yml

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
---
22
- name: set fact tls_enabled
3-
command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\""
3+
ansible.builtin.command:
4+
cmd: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\""
45
register: result
5-
- set_fact:
6+
7+
- name: Set tls_enabled fact
8+
ansible.builtin.set_fact:
69
tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}"
710

811
- vars:
@@ -16,21 +19,21 @@
1619

1720
# ============================================================
1821
- name: get server certificate
19-
copy:
22+
ansible.builtin.copy:
2023
content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}"
2124
dest: /tmp/cert.pem
2225
delegate_to: localhost
2326

2427
- name: Drop mysql user if exists
25-
mysql_user:
28+
community.mysql.mysql_user:
2629
<<: *mysql_params
2730
name: '{{ user_name_1 }}'
2831
host_all: true
2932
state: absent
30-
ignore_errors: yes
33+
ignore_errors: true
3134

3235
- name: create user with ssl requirement
33-
mysql_user:
36+
community.mysql.mysql_user:
3437
<<: *mysql_params
3538
name: "{{ user_name_1 }}"
3639
host: '%'
@@ -40,30 +43,32 @@
4043
SSL:
4144

4245
- name: attempt connection with newly created user (expect failure)
43-
mysql_variables:
46+
community.mysql.mysql_variables:
4447
variable: '{{ set_name }}'
4548
login_user: '{{ user_name_1 }}'
4649
login_password: '{{ user_password_1 }}'
4750
login_host: '{{ mysql_host }}'
4851
login_port: '{{ mysql_primary_port }}'
4952
ca_cert: /tmp/cert.pem
5053
register: result
51-
ignore_errors: yes
54+
ignore_errors: true
5255

53-
- assert:
56+
- name: Assert that result is failed for pymysql
57+
ansible.builtin.assert:
5458
that:
5559
- result is failed
5660
when:
5761
- connector_name == 'pymysql'
5862

59-
- assert:
63+
- name: Assert that result is success for mysqlclient
64+
ansible.builtin.assert:
6065
that:
6166
- result is succeeded
6267
when:
6368
- connector_name != 'pymysql'
6469

6570
- name: attempt connection with newly created user ignoring hostname
66-
mysql_variables:
71+
community.mysql.mysql_variables:
6772
variable: '{{ set_name }}'
6873
login_user: '{{ user_name_1 }}'
6974
login_password: '{{ user_password_1 }}'
@@ -72,14 +77,12 @@
7277
ca_cert: /tmp/cert.pem
7378
check_hostname: no
7479
register: result
75-
ignore_errors: yes
76-
77-
- assert:
78-
that:
79-
- result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg
80+
ignore_errors: true
81+
failed_when:
82+
- result is failed or 'pymysql >= 0.7.11 is required' not in result.msg
8083

8184
- name: Drop mysql user
82-
mysql_user:
85+
community.mysql.mysql_user:
8386
<<: *mysql_params
8487
name: '{{ user_name_1 }}'
8588
host_all: true

tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
# Verify mysql_variable successfully updates a variable (issue:4568)
4848
#
4949
- set_fact:
50-
set_name: 'delay_key_write'
51-
set_value: 'ON'
50+
set_name: 'delay_key_write'
51+
set_value: 'ON'
5252

5353
- name: set mysql variable
5454
mysql_variables:
@@ -74,8 +74,8 @@
7474
# Verify mysql_variable successfully updates a variable using single quotes
7575
#
7676
- set_fact:
77-
set_name: 'wait_timeout'
78-
set_value: '300'
77+
set_name: 'wait_timeout'
78+
set_value: '300'
7979

8080
- name: set mysql variable to a temp value
8181
mysql_variables:
@@ -105,8 +105,8 @@
105105
# Verify mysql_variable successfully updates a variable using double quotes
106106
#
107107
- set_fact:
108-
set_name: "wait_timeout"
109-
set_value: "400"
108+
set_name: "wait_timeout"
109+
set_value: "400"
110110

111111
- name: set mysql variable to a temp value
112112
mysql_variables:
@@ -132,8 +132,8 @@
132132
# Verify mysql_variable successfully updates a variable using no quotes
133133
#
134134
- set_fact:
135-
set_name: wait_timeout
136-
set_value: 500
135+
set_name: wait_timeout
136+
set_value: 500
137137

138138
- name: set mysql variable to a temp value
139139
mysql_variables:
@@ -251,8 +251,8 @@
251251
# Verify mysql_variable works with the login_user and login_password parameters
252252
#
253253
- set_fact:
254-
set_name: wait_timeout
255-
set_value: 77
254+
set_name: wait_timeout
255+
set_value: 77
256256

257257
- name: query mysql_variable using login_user and password_password
258258
mysql_variables:
@@ -291,8 +291,8 @@
291291
# Verify mysql_variable fails with an incorrect login_password parameter
292292
#
293293
- set_fact:
294-
set_name: connect_timeout
295-
set_value: 10
294+
set_name: connect_timeout
295+
set_value: 10
296296

297297
- name: query mysql_variable using incorrect login_password
298298
mysql_variables:

0 commit comments

Comments
 (0)