Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix mysql_user on_new_username IndexError #642

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ec7686d
fix tuple indexerror when no accounts are found
laurent-indermuehle Jun 6, 2024
5477db9
Fix tests for update_password not executed
laurent-indermuehle Jun 6, 2024
fa3c7b0
Add test for case where existing user have different password
laurent-indermuehle Jun 6, 2024
c7218c7
lint to prevent warning about jinja templating in when clause
laurent-indermuehle Jun 6, 2024
3f4af18
Refactor get_existing_authentication to return a list of all row found
laurent-indermuehle Jun 6, 2024
f1f06d9
Refactor host option to be optional
laurent-indermuehle Jun 6, 2024
2532f2a
Add change log fragment
laurent-indermuehle Jun 6, 2024
a7a298d
Add link to the PR in the change log
laurent-indermuehle Jun 6, 2024
0bcbaf8
lint for ansible devel
laurent-indermuehle Jun 6, 2024
b1fa90d
Fix templating type error could not cconvert to bool with ansible devel
laurent-indermuehle Jun 7, 2024
a90e904
Revert changes made for ansible-devel that broke tests for Ansible 2.15
laurent-indermuehle Jun 10, 2024
dfb3ef9
Revert changes made for ansible-devel that broke tests
laurent-indermuehle Jun 10, 2024
bc33aa7
Merge branch 'main' into lie_fix_mysql_user_on_new_username
laurent-indermuehle Jun 11, 2024
bd4f17d
Cut unnecessary set, uniqueness is ensured by the group_by in the query
laurent-indermuehle Jun 19, 2024
67f2e80
Cut auth plugin from returned values when multiple existing auths exists
laurent-indermuehle Jun 24, 2024
0eacc5a
fix convertion of list(dict) to list(tuple)
laurent-indermuehle Jun 24, 2024
b651053
Fix test for empty password on MySQL 8+
laurent-indermuehle Jun 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
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
Loading