mysql_user: fixed encrypted option for MySQL 8.0 and test coverage

The purpose of this change was originally to expand test coverage to
unblock #76, but an issue was detected with the encrypted parameter on
MySQL 8.0 in the process of writing the tests. Additionally,
user_password_update_test.yml had been disabled at some point, so I
opted to replace it with two new files that will focus on the password
and plugin auth paths.
This commit is contained in:
Steve Teahan 2020-12-29 08:29:35 -05:00
parent 9eb007969c
commit 4100f4ba0e
7 changed files with 545 additions and 184 deletions

View file

@ -0,0 +1,2 @@
bugfixes:
- mysql_user - fixed creating user with encrypted password in MySQL 8.0 (https://github.com/ansible-collections/community.mysql/pull/79).

View file

@ -10,6 +10,8 @@
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import (absolute_import, division, print_function)
from distutils.version import LooseVersion
from functools import reduce
__metaclass__ = type
@ -133,3 +135,9 @@ def mysql_common_argument_spec():
ca_cert=dict(type='path', aliases=['ssl_ca']),
check_hostname=dict(type='bool', default=None),
)
def get_server_version(cursor):
cursor.execute("SELECT VERSION()")
version_str = cursor.fetchone()[0]
return LooseVersion(version_str)

View file

@ -8,7 +8,6 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_user
@ -299,10 +298,13 @@ RETURN = '''#'''
import re
import string
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.database import SQLParseError
from ansible_collections.community.mysql.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec, get_server_version
)
from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_native
@ -475,7 +477,19 @@ def user_add(cursor, user, host, host_all, password, encrypted,
mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires
if password and encrypted:
cursor.execute(*mogrify("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password), tls_requires))
server_version = get_server_version(cursor)
# The IDENTIFIED BY PASSWORD syntax was dropped in MySQL 8.0, but appears to be supported by MariaDB 10.x.
if server_version < LooseVersion("8") or server_version > LooseVersion("10"):
cursor.execute(*mogrify("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password), tls_requires))
else:
cursor.execute(
*mogrify(
"CREATE USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, password),
tls_requires
)
)
elif password and not encrypted:
if old_user_mgmt:
cursor.execute(*mogrify("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password), tls_requires))

View file

@ -233,10 +233,14 @@
- include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }}
# ============================================================
# Update user password for a user.
# Assert the user password is updated and old password can no longer be used.
# Test plaintext and encrypted password scenarios.
#
#- include: user_password_update_test.yml
- include: test_user_password.yml
# ============================================================
# Test plugin authentication scenarios.
#
- include: test_user_plugin_auth.yml
# ============================================================
# Assert create user with SELECT privileges, attempt to create database and update privileges to create database

View file

@ -0,0 +1,269 @@
# Tests scenarios for both plaintext and encrypted user passwords.
- vars:
mysql_parameters: &mysql_params
login_user: '{{ mysql_user }}'
login_password: '{{ mysql_password }}'
login_host: 127.0.0.1
login_port: '{{ mysql_primary_port }}'
test_user_name: 'test_user_password'
initial_password: 'a5C8SN*DBa0%a75sGz'
initial_password_encrypted: '*0A12D4DF68C2A50716111674E565CA3D7D68B048'
new_password: 'NkN&qECv33vuQzf3bJg'
new_password_encrypted: '*B6559186FAD0953589F54383AD8EE9E9172296DA'
test_default_priv_type: 'SELECT'
test_default_priv: '*.*:{{ test_default_priv_type }}'
block:
# ============================================================
# Test setting plaintext password and changing it.
#
- name: Create user with initial password
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
password: '{{ initial_password }}'
priv: '{{ test_default_priv }}'
state: present
register: result
- name: Assert that a change occurred because the user was added
assert:
that:
- "result.changed == true"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Get the MySQL version using the newly created used creds
mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ initial_password }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
ignore_errors: true
- name: Assert that mysql_info was successful
assert:
that:
- "result.failed == false"
- name: Run mysql_user again without any changes
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
password: '{{ initial_password }}'
priv: '{{ test_default_priv }}'
state: present
register: result
- name: Assert that there weren't any changes because username/password didn't change
assert:
that:
- "result.changed == false"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Update the user password
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
password: '{{ new_password }}'
state: present
register: result
- name: Assert that a change occurred because the password was updated
assert:
that:
- "result.changed == true"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Get the MySQL version data using the original password (should fail)
mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ initial_password }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
ignore_errors: true
- name: Assert that the mysql_info module failed because we used the old password
assert:
that:
- "result.failed == true"
- name: Get the MySQL version data using the new password (should work)
mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ new_password }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
ignore_errors: true
- name: Assert that the mysql_info module succeeded because we used the new password
assert:
that:
- "result.failed == false"
# Cleanup
- include: remove_user.yml user_name={{ test_user_name }} user_password={{ new_password }}
# ============================================================
# Test setting a plaintext password and then the same password encrypted to ensure there isn't a change detected.
#
- name: Create user with initial password
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
password: '{{ initial_password }}'
priv: '{{ test_default_priv }}'
state: present
register: result
- name: Assert that a change occurred because the user was added
assert:
that:
- "result.changed == true"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Pass in the same password as before, but in the encrypted form (no change expected)
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
password: '{{ initial_password_encrypted }}'
encrypted: yes
priv: '{{ test_default_priv }}'
state: present
register: result
- name: Assert that there weren't any changes because username/password didn't change
assert:
that:
- "result.changed == false"
# Cleanup
- include: remove_user.yml user_name={{ test_user_name }} user_password={{ new_password }}
# ============================================================
# Test setting an encrypted password and then the same password in plaintext to ensure there isn't a change.
#
- name: Create user with initial password
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
password: '{{ initial_password_encrypted }}'
encrypted: yes
priv: '{{ test_default_priv }}'
state: present
register: result
- name: Assert that a change occurred because the user was added
assert:
that:
- "result.changed == true"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Get the MySQL version data using the new creds
mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ initial_password }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
ignore_errors: true
- name: Assert that the mysql_info module succeeded because we used the new password
assert:
that:
- "result.failed == false"
- name: Pass in the same password as before, but in the encrypted form (no change expected)
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
password: '{{ initial_password }}'
state: present
register: result
- name: Assert that there weren't any changes because username/password didn't change
assert:
that:
- "result.changed == false"
# Cleanup
- include: remove_user.yml user_name={{ test_user_name }} user_password={{ new_password }}
# ============================================================
# Test setting an empty password.
#
- name: Create user with empty password
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
priv: '{{ test_default_priv }}'
state: present
register: result
- name: Assert that a change occurred because the user was added
assert:
that:
- "result.changed == true"
- name: Get the MySQL version using an empty password for the newly created user
mysql_info:
login_user: '{{ test_user_name }}'
login_password: ''
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
ignore_errors: true
- name: Assert that mysql_info was successful
assert:
that:
- "result.failed == false"
- name: Get the MySQL version using an non-empty password (should fail)
mysql_info:
login_user: '{{ test_user_name }}'
login_password: 'some_password'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
ignore_errors: true
- name: Assert that mysql_info failed
assert:
that:
- "result.failed == true"
- name: Update the user without changing the password
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
priv: '{{ test_default_priv }}'
state: present
register: result
- name: Assert that the user wasn't changed because the password is still empty
assert:
that:
- "result.changed == false"
# Cleanup
- include: remove_user.yml user_name={{ test_user_name }} user_password=''

View file

@ -0,0 +1,242 @@
# Test user plugin auth scenarios.
- vars:
mysql_parameters: &mysql_params
login_user: '{{ mysql_user }}'
login_password: '{{ mysql_password }}'
login_host: 127.0.0.1
login_port: '{{ mysql_primary_port }}'
test_user_name: 'test_user_plugin_auth'
test_plugin_type: 'mysql_native_password'
test_plugin_hash: '*0CB5B86F23FDC24DB19A29B8854EB860CBC47793'
test_plugin_auth_string: 'Fdt8fd^34ds'
test_default_priv_type: 'SELECT'
test_default_priv: '*.*:{{ test_default_priv_type }}'
block:
# ============================================================
# Test plugin auth initially setting a hash and then switching to a plaintext auth string.
#
- name: Create user with plugin auth (with hash string)
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
plugin_hash_string: '{{ test_plugin_hash }}'
priv: '{{ test_default_priv }}'
register: result
- name: Check that the module made a change
assert:
that:
- "result.changed == true"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Get the MySQL version using the newly created creds
mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
- name: Assert that mysql_info was successful
assert:
that:
- "result.failed == false"
- name: Update the user with the same hash (no change expected)
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
plugin_hash_string: '{{ test_plugin_hash }}'
register: result
- name: Check that the module doesn't make a change when the same hash is passed in
assert:
that:
- "result.changed == false"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Change the user using the same plugin, but switch to the same auth string in plaintext form
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
plugin_auth_string: '{{ test_plugin_auth_string }}'
register: result
# Expecting a change is currently by design (see comment in source).
- name: Check that the module did not change the password
assert:
that:
- "result.changed == true"
- name: Getting the MySQL info should still work
mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
- name: Assert that mysql_info was successful
assert:
that:
- "result.failed == false"
# Cleanup
- include: remove_user.yml user_name={{ test_user_name }} user_password={{ test_plugin_auth_string }}
# ============================================================
# Test plugin auth initially setting a plaintext auth string and then switching to a plaintext auth string.
#
- name: Create user with plugin auth (with auth string)
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
plugin_auth_string: '{{ test_plugin_auth_string }}'
priv: '{{ test_default_priv }}'
register: result
- name: Check that the module made a change
assert:
that:
- "result.changed == true"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Get the MySQL version using the newly created creds
mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
- name: Assert that mysql_info was successful
assert:
that:
- "result.failed == false"
- name: Update the user with the same auth string
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
plugin_auth_string: '{{ test_plugin_auth_string }}'
register: result
# This is the current expected behavior because there isn't a reliable way to hash the password in the mysql_user
# module in order to be able to compare this password with the stored hash. See the source for more info.
- name: The module should detect a change even though the password is the same
assert:
that:
- "result.changed == true"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Change the user using the same plugin, but switch to the same auth string in hash form
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
plugin_hash_string: '{{ test_plugin_hash }}'
register: result
- name: Check that the module did not change the password
assert:
that:
- "result.changed == false"
- name: Get the MySQL version using the newly created creds
mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
- name: Assert that mysql_info was successful
assert:
that:
- "result.failed == false"
# Cleanup
- include: remove_user.yml user_name={{ test_user_name }} user_password={{ test_plugin_auth_string }}
# ============================================================
# Test plugin auth with an empty auth string.
#
- name: Create user with plugin auth (empty auth string)
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
priv: '{{ test_default_priv }}'
register: result
- name: Check that the module made a change
assert:
that:
- "result.changed == true"
- include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }}
- name: Get the MySQL version using an empty password for the newly created user
mysql_info:
login_user: '{{ test_user_name }}'
login_password: ''
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
ignore_errors: true
- name: Assert that mysql_info was successful
assert:
that:
- "result.failed == false"
- name: Get the MySQL version using an non-empty password (should fail)
mysql_info:
login_user: '{{ test_user_name }}'
login_password: 'some_password'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter: version
register: result
ignore_errors: true
- name: Assert that mysql_info failed
assert:
that:
- "result.failed == true"
- name: Update the user without changing the auth mechanism
mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
state: present
register: result
- name: Assert that the user wasn't changed because the auth string is still empty
assert:
that:
- "result.changed == false"
# Cleanup
- include: remove_user.yml user_name={{ test_user_name }} user_password={{ test_plugin_auth_string }}

View file

@ -1,178 +0,0 @@
# test code update password for the mysql_user module
# (c) 2014, Wayne Rosario <wrosario@ansible.com>
# 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 <http://www.gnu.org/licenses/>.
- 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.changed == true"
- 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