From 1e7badca6c00d422b1dbfa9640d8d29233ec30ad Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 19 May 2020 17:16:19 +0300 Subject: [PATCH 01/37] Add TLS connection parameters --- plugins/modules/database/mysql/mysql_user.py | 78 ++++++++++++++++---- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index d740909df41..1534a20d38f 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -60,6 +60,13 @@ user instead of overwriting existing ones. type: bool default: no + tls_requires: + description: + - Set requirement for secure transport as a dictionary of requirements (see the examples). + - Valid requirements are SSL, X509, SUBJECT, ISSUER, CIPHER + - SUBJECT, ISSUER and CIPHER are complementary, and mutually exclusive with SSL and X509 + - https://mariadb.com/kb/en/securing-connections-for-client-and-server/#requiring-tls + type: dict sql_log_bin: description: - Whether binary logging should be enabled or disabled for the connection. @@ -183,6 +190,20 @@ priv: '*.*:REQUIRESSL' state: present +- name: Modifiy user to require TLS connection with a valid client certificate + mysql_user: + name: bob + tls_requires: + x509: + state: present + +- name: Modifiy user to require TLS connection with a specific client certificate and cipher + mysql_user: + name: bob + tls_requires: + subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' + cipher: 'ECDHE-ECDSA-AES256-SHA384' + - name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials. mysql_user: login_user: root @@ -344,8 +365,27 @@ def user_exists(cursor, user, host, host_all): return count[0] > 0 -def user_add(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, new_priv, check_mode): +def parse_requires(tls_requires): + def fix_quotes(value): + if value: + return '%s' % value.strip('"').strip("'") + + for key in tls_requires.keys(): + if not key.isupper(): + tls_requires[key.upper] = tls_requires[key] + tls_requires.pop(key) + if any([key in ['CIPHER', 'ISSUER', 'SUBJECT'] for key in tls_requires.keys()]): + tls_requires.pop('SSL', None) + tls_requires.pop('X509', None) + + if 'X509' in tls_requires.keys(): + tls_requires.pop('SSL', None) + + return "REQUIRE %s" % ' AND '.join([' '.join(filter(None, (key, fix_quotes(value)))) for key,value in tls_requires.items()]) + + +def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, + plugin_auth_string, new_priv, tls_requires, check_mode): # we cannot create users without a proper hostname if host_all: return False @@ -353,16 +393,17 @@ def user_add(cursor, user, host, host_all, password, encrypted, if check_mode: return True + requires = parse_requires(tls_requires) if password and encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password)) + cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s %s", (user, host, password, requires)) elif password and not encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password)) + cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s %s", (user, host, password, requires)) elif plugin and plugin_hash_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string)) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s %s", (user, host, plugin, plugin_hash_string, requires)) elif plugin and plugin_auth_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s %s", (user, host, plugin, plugin_auth_string, requires)) elif plugin: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s %s", (user, host, plugin, requires)) else: cursor.execute("CREATE USER %s@%s", (user, host)) if new_priv is not None: @@ -379,8 +420,8 @@ def is_hash(password): return ishash -def user_mod(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, new_priv, append_privs, module): +def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, + plugin_auth_string, new_priv, append_privs, tls_requires, module): changed = False msg = "User unchanged" grant_option = False @@ -390,6 +431,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted, else: hostnames = [host] + requires = parse_requires(tls_requires) + for host in hostnames: # Handle clear text and hashed passwords. if bool(password): @@ -443,7 +486,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, msg = "Password updated (old style)" else: try: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s %s", (user, host, encrypted_password, requires)) msg = "Password updated (new style)" except (mysql_driver.Error) as e: # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql @@ -453,6 +496,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, "UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s", ('mysql_native_password', encrypted_password, user, host) ) + cursor.execute("GRANT USAGE on *.* to '%s'@'%s' %s" % (user, host, requires)) cursor.execute("FLUSH PRIVILEGES") msg = "Password forced update" else: @@ -482,11 +526,11 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if update: if plugin_hash_string: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s AS %s %s", (user, host, plugin, plugin_hash_string, requires)) elif plugin_auth_string: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s BY %s %s", (user, host, plugin, plugin_auth_string, requires)) else: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s %s", (user, host, plugin, requires)) changed = True # Handle privileges @@ -847,6 +891,7 @@ def main(): host_all=dict(type="bool", default=False), state=dict(type='str', default='present', choices=['absent', 'present']), priv=dict(type='raw'), + tls_requires=dict(type='dict'), append_privs=dict(type='bool', default=False), check_implicit_admin=dict(type='bool', default=False), update_password=dict(type='str', default='always', choices=['always', 'on_create'], no_log=False), @@ -872,6 +917,7 @@ def main(): host_all = module.params["host_all"] state = module.params["state"] priv = module.params["priv"] + tls_requires = module.params["tls_requires"] check_implicit_admin = module.params['check_implicit_admin'] connect_timeout = module.params['connect_timeout'] config_file = module.params['config_file'] @@ -930,11 +976,11 @@ def main(): if update_password == 'always': changed, msg = user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, module) + priv, append_privs, tls_requires, module) else: changed, msg = user_mod(cursor, user, host, host_all, None, encrypted, plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, module) + priv, append_privs, tls_requires, module) except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: module.fail_json(msg=to_native(e)) @@ -944,7 +990,7 @@ def main(): try: changed = user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, - priv, module.check_mode) + priv, tls_requires, module.check_mode) if changed: msg = "User added" From c5138a9c0f6ca913ffc5e999cb7ae3e8b1eacdde Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 19 May 2020 17:20:24 +0300 Subject: [PATCH 02/37] Add changelog fragment --- changelogs/fragments/369-mysql_user_add_tls_requires | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/369-mysql_user_add_tls_requires diff --git a/changelogs/fragments/369-mysql_user_add_tls_requires b/changelogs/fragments/369-mysql_user_add_tls_requires new file mode 100644 index 00000000000..6b58a5efa31 --- /dev/null +++ b/changelogs/fragments/369-mysql_user_add_tls_requires @@ -0,0 +1,2 @@ +minor_changes: + - mysql_user - add TLS REQUIRES parameters (https://github.com/ansible-collections/community.general/pull/369) From 9b93097dec1e5dae5762b4c771d6e49dd262738a Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Thu, 11 Jun 2020 11:30:04 +0300 Subject: [PATCH 03/37] Add deprecation notice --- changelogs/fragments/369-mysql_user_add_tls_requires | 2 -- .../fragments/369-mysql_user_add_tls_requires.yml | 4 ++++ plugins/modules/database/mysql/mysql_user.py | 11 +++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 changelogs/fragments/369-mysql_user_add_tls_requires create mode 100644 changelogs/fragments/369-mysql_user_add_tls_requires.yml diff --git a/changelogs/fragments/369-mysql_user_add_tls_requires b/changelogs/fragments/369-mysql_user_add_tls_requires deleted file mode 100644 index 6b58a5efa31..00000000000 --- a/changelogs/fragments/369-mysql_user_add_tls_requires +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: - - mysql_user - add TLS REQUIRES parameters (https://github.com/ansible-collections/community.general/pull/369) diff --git a/changelogs/fragments/369-mysql_user_add_tls_requires.yml b/changelogs/fragments/369-mysql_user_add_tls_requires.yml new file mode 100644 index 00000000000..f1c60da445d --- /dev/null +++ b/changelogs/fragments/369-mysql_user_add_tls_requires.yml @@ -0,0 +1,4 @@ +minor_changes: + - mysql_user - add TLS REQUIRES parameters (https://github.com/ansible-collections/community.general/pull/369). +deprecated_features: + - mysql_user - REQUIRESSL is deprecated in favor of `tls_requires`. diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 1534a20d38f..76e6af4a2bd 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -63,9 +63,9 @@ tls_requires: description: - Set requirement for secure transport as a dictionary of requirements (see the examples). - - Valid requirements are SSL, X509, SUBJECT, ISSUER, CIPHER - - SUBJECT, ISSUER and CIPHER are complementary, and mutually exclusive with SSL and X509 - - https://mariadb.com/kb/en/securing-connections-for-client-and-server/#requiring-tls + - Valid requirements are SSL, X509, SUBJECT, ISSUER, CIPHER. + - SUBJECT, ISSUER and CIPHER are complementary, and mutually exclusive with SSL and X509. + - U(https://mariadb.com/kb/en/securing-connections-for-client-and-server/#requiring-tls). type: dict sql_log_bin: description: @@ -289,7 +289,9 @@ from ansible_collections.community.general.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg from ansible.module_utils.six import iteritems from ansible.module_utils._text import to_native +from ansible.utils.display import Display +display = Display() VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION', 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', @@ -381,7 +383,7 @@ def fix_quotes(value): if 'X509' in tls_requires.keys(): tls_requires.pop('SSL', None) - return "REQUIRE %s" % ' AND '.join([' '.join(filter(None, (key, fix_quotes(value)))) for key,value in tls_requires.items()]) + return "REQUIRE %s" % ' AND '.join([' '.join(filter(None, (key, fix_quotes(value)))) for key, value in tls_requires.items()]) def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, @@ -633,6 +635,7 @@ def pick(x): if "WITH GRANT OPTION" in res.group(7): privileges.append('GRANT') if "REQUIRE SSL" in res.group(7): + display.deprecated('Rather than using the REQUIRE SSL privilege, use the require_ssl parameter.') privileges.append('REQUIRESSL') db = res.group(2) output.setdefault(db, []).extend(privileges) From 0b63322923da93dbaf7ac5a2e857eedfd0820e94 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Thu, 11 Jun 2020 13:54:22 +0300 Subject: [PATCH 04/37] Add tests --- .../targets/mysql_user/tasks/main.yml | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 9bc78a64ccf..1dd9e80551e 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -112,6 +112,65 @@ - include: assert_no_user.yml user_name={{user_name_2}} +# ============================================================ +# Create users with TLS requirements and verify requirements are assigned +# +- name: create user with TLS requirements state=present (expect changed=true) + mysql_user: + name: '{{ item[0] }}' + password: '{{ user_password_1 }}' + tls_requires: '{{ item[1] }}' + login_unix_socket: '{{ mysql_socket }}' + with_together: + - [ '{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + - + - SSL: + - X509: + - subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' + cipher: 'ECDHE-ECDSA-AES256-SHA384' + issuer: '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' + +- name: retrieve TLS requiremets for users + command: mysql -L -N -s -e "SELECT JSON_OBJECT('type', ssl_type, 'cipher', ssl_cipher, 'issuer', x509_issuer, 'subject', x509_subject) FROM mysql.user where User='{{ item }}';" + register: result + with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + +- name: assert user1 TLS requirements + assert: + that: + - reqs.type == 'ANY' + - reqs.cipher == 'base64:type252:' + - reqs.issuer == 'base64:type252:' + - reqs.subject == 'base64:type252:' + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout | from_json + +- name: assert user2 TLS requirements + assert: + that: + - reqs.type == 'X509' + - reqs.cipher == 'base64:type252:' + - reqs.issuer == 'base64:type252:' + - reqs.subject == 'base64:type252:' + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_2) | first).stdout | from_json + +- name: assert user3 TLS requirements + assert: + that: + - reqs.type == 'SPECIFIED' + - reqs.cipher == 'base64:type252:RUNESEUtRUNEU0EtQUVTMjU2LVNIQTM4NA==' + - reqs.issuer == 'base64:type252:L0NOPW9yZy9PPU15RG9tLCBJbmMuL0M9VVMvU1Q9T3JlZ29uL0w9UG9ydGxhbmQ=' + - reqs.subject == 'base64:type252:L0NOPWFsaWNlL089TXlEb20sIEluYy4vQz1VUy9TVD1PcmVnb24vTD1Qb3J0bGFuZA==' + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_3) | first).stdout | from_json + +- 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 }} + +- include: remove_user.yml user_name={{user_name_2}} user_password={{ user_password_1 }} + # ============================================================ # Assert user has access to multiple databases # From c0bc4591b06720b82a8480deff1c1b617702cb27 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Thu, 11 Jun 2020 14:03:24 +0300 Subject: [PATCH 05/37] Remove deprecation notice --- plugins/modules/database/mysql/mysql_user.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 76e6af4a2bd..4cc8ca3e5a0 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -289,9 +289,6 @@ from ansible_collections.community.general.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg from ansible.module_utils.six import iteritems from ansible.module_utils._text import to_native -from ansible.utils.display import Display - -display = Display() VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION', 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', @@ -635,7 +632,6 @@ def pick(x): if "WITH GRANT OPTION" in res.group(7): privileges.append('GRANT') if "REQUIRE SSL" in res.group(7): - display.deprecated('Rather than using the REQUIRE SSL privilege, use the require_ssl parameter.') privileges.append('REQUIRESSL') db = res.group(2) output.setdefault(db, []).extend(privileges) From 87729f1441303aca4ea89dea7ea08876fa18e991 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Thu, 11 Jun 2020 14:20:44 +0300 Subject: [PATCH 06/37] Do not attempt parsing unspecified tls_requires --- plugins/modules/database/mysql/mysql_user.py | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 4cc8ca3e5a0..db7b8723a22 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -369,18 +369,20 @@ def fix_quotes(value): if value: return '%s' % value.strip('"').strip("'") - for key in tls_requires.keys(): - if not key.isupper(): - tls_requires[key.upper] = tls_requires[key] - tls_requires.pop(key) - if any([key in ['CIPHER', 'ISSUER', 'SUBJECT'] for key in tls_requires.keys()]): - tls_requires.pop('SSL', None) - tls_requires.pop('X509', None) - - if 'X509' in tls_requires.keys(): - tls_requires.pop('SSL', None) - - return "REQUIRE %s" % ' AND '.join([' '.join(filter(None, (key, fix_quotes(value)))) for key, value in tls_requires.items()]) + if tls_requires: + for key in tls_requires.keys(): + if not key.isupper(): + tls_requires[key.upper] = tls_requires[key] + tls_requires.pop(key) + if any([key in ['CIPHER', 'ISSUER', 'SUBJECT'] for key in tls_requires.keys()]): + tls_requires.pop('SSL', None) + tls_requires.pop('X509', None) + + if 'X509' in tls_requires.keys(): + tls_requires.pop('SSL', None) + + return "REQUIRE %s" % ' AND '.join([' '.join(filter(None, (key, fix_quotes(value)))) for key, value in tls_requires.items()]) + return None def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, From d01b1d260df3fb2396b3086787e7b857c7dd3c05 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Thu, 11 Jun 2020 15:26:35 +0300 Subject: [PATCH 07/37] Format SQL queries to handle optional TLS requires --- plugins/modules/database/mysql/mysql_user.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index db7b8723a22..c760661d788 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -396,15 +396,15 @@ def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_h requires = parse_requires(tls_requires) if password and encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s %s", (user, host, password, requires)) + cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, ' '.join(filter(None,(password, requires))))) elif password and not encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s %s", (user, host, password, requires)) + cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, ' '.join(filter(None,(password, requires))))) elif plugin and plugin_hash_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s %s", (user, host, plugin, plugin_hash_string, requires)) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, ' '.join(filter(None,(plugin_hash_string, requires))))) elif plugin and plugin_auth_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s %s", (user, host, plugin, plugin_auth_string, requires)) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, ' '.join(filter(None,(plugin_auth_string, requires))))) elif plugin: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s %s", (user, host, plugin, requires)) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, ' '.join(filter(None,(plugin, requires))))) else: cursor.execute("CREATE USER %s@%s", (user, host)) if new_priv is not None: From 9ab6e257458ff131be22553ac2b639dce1d9133e Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Fri, 12 Jun 2020 10:05:25 +0300 Subject: [PATCH 08/37] Bug fixes --- plugins/modules/database/mysql/mysql_user.py | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index c760661d788..b25988bae59 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -183,6 +183,7 @@ 'db2.*': 'ALL,GRANT' # Note that REQUIRESSL is a special privilege that should only apply to *.* by itself. +# Setting this privilege in this manner is deprecated. Use 'tls_requires' instead. - name: Modify user to require SSL connections. mysql_user: name: bob @@ -372,7 +373,7 @@ def fix_quotes(value): if tls_requires: for key in tls_requires.keys(): if not key.isupper(): - tls_requires[key.upper] = tls_requires[key] + tls_requires[key.upper()] = tls_requires[key] tls_requires.pop(key) if any([key in ['CIPHER', 'ISSUER', 'SUBJECT'] for key in tls_requires.keys()]): tls_requires.pop('SSL', None) @@ -396,17 +397,17 @@ def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_h requires = parse_requires(tls_requires) if password and encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, ' '.join(filter(None,(password, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, ' '.join(filter(None, (password, requires))))) elif password and not encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, ' '.join(filter(None,(password, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, ' '.join(filter(None, (password, requires))))) elif plugin and plugin_hash_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, ' '.join(filter(None,(plugin_hash_string, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, ' '.join(filter(None, (plugin_hash_string, requires))))) elif plugin and plugin_auth_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, ' '.join(filter(None,(plugin_auth_string, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, ' '.join(filter(None, (plugin_auth_string, requires))))) elif plugin: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, ' '.join(filter(None,(plugin, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, ' '.join(filter(None, (plugin, requires))))) else: - cursor.execute("CREATE USER %s@%s", (user, host)) + cursor.execute("CREATE USER %s@%s", (user, ' '.join(filter(None, (host, requires))))) if new_priv is not None: for db_table, priv in iteritems(new_priv): privileges_grant(cursor, user, host, db_table, priv) @@ -487,7 +488,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h msg = "Password updated (old style)" else: try: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s %s", (user, host, encrypted_password, requires)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, ' '.join(filter(None, (encrypted_password, requires))))) msg = "Password updated (new style)" except (mysql_driver.Error) as e: # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql @@ -497,7 +498,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h "UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s", ('mysql_native_password', encrypted_password, user, host) ) - cursor.execute("GRANT USAGE on *.* to '%s'@'%s' %s" % (user, host, requires)) + cursor.execute("GRANT USAGE on *.* to '%s'@'%s' %s" % (user, ' '.join(filter(None, (host, requires))))) cursor.execute("FLUSH PRIVILEGES") msg = "Password forced update" else: @@ -527,11 +528,11 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h if update: if plugin_hash_string: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s AS %s %s", (user, host, plugin, plugin_hash_string, requires)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, ' '.join(filter(None, (plugin_hash_string, requires))))) elif plugin_auth_string: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s BY %s %s", (user, host, plugin, plugin_auth_string, requires)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, ' '.join(filter(None, (plugin_auth_string, requires))))) else: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s %s", (user, host, plugin, requires)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, ' '.join(filter(None, (plugin, requires))))) changed = True # Handle privileges From fc3baea8f1954bea445913b30609c42ac5e04795 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Fri, 12 Jun 2020 10:31:18 +0300 Subject: [PATCH 09/37] Fix sanity errors --- plugins/modules/database/mysql/mysql_user.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index b25988bae59..8ea1a58f4cd 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -397,17 +397,17 @@ def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_h requires = parse_requires(tls_requires) if password and encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, ' '.join(filter(None, (password, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, ' '.join(filter(None, (password, requires))))) elif password and not encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, ' '.join(filter(None, (password, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, ' '.join(filter(None, (password, requires))))) elif plugin and plugin_hash_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, ' '.join(filter(None, (plugin_hash_string, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, ' '.join(filter(None, (plugin_hash_string, requires))))) elif plugin and plugin_auth_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, ' '.join(filter(None, (plugin_auth_string, requires))))) + cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, ' '.join(filter(None, (plugin_auth_string, requires))))) elif plugin: cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, ' '.join(filter(None, (plugin, requires))))) else: - cursor.execute("CREATE USER %s@%s", (user, ' '.join(filter(None, (host, requires))))) + cursor.execute("CREATE USER %s@%s", (user, ' '.join(filter(None, (host, requires))))) if new_priv is not None: for db_table, priv in iteritems(new_priv): privileges_grant(cursor, user, host, db_table, priv) @@ -488,7 +488,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h msg = "Password updated (old style)" else: try: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, ' '.join(filter(None, (encrypted_password, requires))))) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", + (user, host, ' '.join(filter(None, (encrypted_password, requires))))) msg = "Password updated (new style)" except (mysql_driver.Error) as e: # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql From 9f70d2ff3d9472da2d883b6208dcd7b08d1f5a8d Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Fri, 12 Jun 2020 10:31:52 +0300 Subject: [PATCH 10/37] Remove JSON_OBJECT function from test query as it is not always available --- .../targets/mysql_user/tasks/main.yml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 1dd9e80551e..e64e953d942 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -131,37 +131,37 @@ issuer: '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' - name: retrieve TLS requiremets for users - command: mysql -L -N -s -e "SELECT JSON_OBJECT('type', ssl_type, 'cipher', ssl_cipher, 'issuer', x509_issuer, 'subject', x509_subject) FROM mysql.user where User='{{ item }}';" + command: mysql -L -N -s -e "SELECT ssl_type, ssl_cipher, x509_issuer, x509_subject FROM mysql.user where User='{{ item }}';" register: result with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] - name: assert user1 TLS requirements assert: that: - - reqs.type == 'ANY' - - reqs.cipher == 'base64:type252:' - - reqs.issuer == 'base64:type252:' - - reqs.subject == 'base64:type252:' + - reqs[0] == 'ANY' + - reqs[1] == '0x' + - reqs[2] == '0x' + - reqs[3] == '0x' vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout | from_json + - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split() - name: assert user2 TLS requirements assert: that: - - reqs.type == 'X509' - - reqs.cipher == 'base64:type252:' - - reqs.issuer == 'base64:type252:' - - reqs.subject == 'base64:type252:' + - reqs[0] == 'X509' + - reqs[1] == '0x' + - reqs[2] == '0x' + - reqs[3] == '0x' vars: - reqs: (result.results | selectattr('item', 'eq', user_name_2) | first).stdout | from_json - name: assert user3 TLS requirements assert: that: - - reqs.type == 'SPECIFIED' - - reqs.cipher == 'base64:type252:RUNESEUtRUNEU0EtQUVTMjU2LVNIQTM4NA==' - - reqs.issuer == 'base64:type252:L0NOPW9yZy9PPU15RG9tLCBJbmMuL0M9VVMvU1Q9T3JlZ29uL0w9UG9ydGxhbmQ=' - - reqs.subject == 'base64:type252:L0NOPWFsaWNlL089TXlEb20sIEluYy4vQz1VUy9TVD1PcmVnb24vTD1Qb3J0bGFuZA==' + - reqs[0] == 'SPECIFIED' + - reqs[1] == '0x45434448452D45434453412D4145533235362D534841333834' + - reqs[2] == '0x2F434E3D6F72672F4F3D4D79446F6D2C20496E632E2F433D55532F53543D4F7265676F6E2F4C3D506F72746C616E64' + - reqs[3] == '0x2F434E3D616C6963652F4F3D4D79446F6D2C20496E632E2F433D55532F53543D4F7265676F6E2F4C3D506F72746C616E64' vars: - reqs: (result.results | selectattr('item', 'eq', user_name_3) | first).stdout | from_json From bbdb787fa29d22c7e28f59326becada3fabd8cab Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Fri, 12 Jun 2020 11:05:08 +0300 Subject: [PATCH 11/37] Run grant query with TLS requires only if specified --- plugins/modules/database/mysql/mysql_user.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 8ea1a58f4cd..c8b51641e7d 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -499,7 +499,10 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h "UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s", ('mysql_native_password', encrypted_password, user, host) ) - cursor.execute("GRANT USAGE on *.* to '%s'@'%s' %s" % (user, ' '.join(filter(None, (host, requires))))) + if requires: + cursor.execute("GRANT USAGE on *.* to '%s'@'%s' %s" % (user, host, requires)) + else: + cursor.execute("GRANT USAGE on *.* to '%s'@'%s'" % (user, host)) cursor.execute("FLUSH PRIVILEGES") msg = "Password forced update" else: From 3cc350155e985c46022370c3bc050656f2bdd6e2 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Fri, 12 Jun 2020 11:05:43 +0300 Subject: [PATCH 12/37] Debug test --- .../integration/targets/mysql_user/tasks/main.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index e64e953d942..2d3e9d97d42 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -131,7 +131,8 @@ issuer: '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' - name: retrieve TLS requiremets for users - command: mysql -L -N -s -e "SELECT ssl_type, ssl_cipher, x509_issuer, x509_subject FROM mysql.user where User='{{ item }}';" + command: mysql -e "SELECT * FROM mysql.user where User='{{ item }}'\G" + #command: mysql -L -N -s -e "SELECT ssl_type, ssl_cipher, x509_issuer, x509_subject FROM mysql.user where User='{{ item }}';" register: result with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] @@ -153,7 +154,7 @@ - reqs[2] == '0x' - reqs[3] == '0x' vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_2) | first).stdout | from_json + - reqs: (result.results | selectattr('item', 'eq', user_name_2) | first).stdout.split() - name: assert user3 TLS requirements assert: @@ -163,13 +164,19 @@ - reqs[2] == '0x2F434E3D6F72672F4F3D4D79446F6D2C20496E632E2F433D55532F53543D4F7265676F6E2F4C3D506F72746C616E64' - reqs[3] == '0x2F434E3D616C6963652F4F3D4D79446F6D2C20496E632E2F433D55532F53543D4F7265676F6E2F4C3D506F72746C616E64' vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_3) | first).stdout | from_json + - reqs: (result.results | selectattr('item', 'eq', user_name_3) | first).stdout.split() - 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 }} -- include: remove_user.yml user_name={{user_name_2}} user_password={{ user_password_1 }} +- include: remove_user.yml user_name={{user_name_3}} user_password={{ user_password_1 }} + +- include: assert_no_user.yml user_name={{user_name_1}} + +- include: assert_no_user.yml user_name={{user_name_2}} + +- include: assert_no_user.yml user_name={{user_name_3}} # ============================================================ # Assert user has access to multiple databases From a15fbea22ce1d03eb5dd78581e0812a38e23d1f9 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Mon, 15 Jun 2020 13:47:25 +0300 Subject: [PATCH 13/37] Construct SQL queries properly --- plugins/modules/database/mysql/mysql_user.py | 64 +++++++++++--------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index c8b51641e7d..ec82ec9fb61 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -365,11 +365,7 @@ def user_exists(cursor, user, host, host_all): return count[0] > 0 -def parse_requires(tls_requires): - def fix_quotes(value): - if value: - return '%s' % value.strip('"').strip("'") - +def sanitize_requires(tls_requires): if tls_requires: for key in tls_requires.keys(): if not key.isupper(): @@ -380,12 +376,27 @@ def fix_quotes(value): tls_requires.pop('X509', None) if 'X509' in tls_requires.keys(): - tls_requires.pop('SSL', None) + tls_requires = 'X509' + else: + tls_requires = 'SSL' - return "REQUIRE %s" % ' AND '.join([' '.join(filter(None, (key, fix_quotes(value)))) for key, value in tls_requires.items()]) + return tls_requires +# return " REQUIRE %s" % ' AND '.join([' '.join(filter(None, (key, fix_quotes(value)))) for key, value in tls_requires.items()]) return None +def mogrify_requires(query, params, tls_requires): + if tls_requires: + if isinstance(tls_requires, dict): + k, v = zip(*tls_requires.items()) + requires_query = ' AND '.join(('{} %s'.format(key) for key in k)) + params += v + else: + requires_query = tls_requires + query = ' REQUIRE '.join((query, requires_query)) + return query, params + + def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, new_priv, tls_requires, check_mode): # we cannot create users without a proper hostname @@ -395,19 +406,18 @@ def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_h if check_mode: return True - requires = parse_requires(tls_requires) if password and encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, ' '.join(filter(None, (password, requires))))) + cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password), tls_requires)) elif password and not encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, ' '.join(filter(None, (password, requires))))) + cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password), tls_requires)) elif plugin and plugin_hash_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, ' '.join(filter(None, (plugin_hash_string, requires))))) + cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string), tls_requires)) elif plugin and plugin_auth_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, ' '.join(filter(None, (plugin_auth_string, requires))))) + cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string), tls_requires)) elif plugin: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, ' '.join(filter(None, (plugin, requires))))) + cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin), tls_requires)) else: - cursor.execute("CREATE USER %s@%s", (user, ' '.join(filter(None, (host, requires))))) + cursor.execute(*mogrify_requires("CREATE USER %s@%s", (user, host), tls_requires)) if new_priv is not None: for db_table, priv in iteritems(new_priv): privileges_grant(cursor, user, host, db_table, priv) @@ -433,8 +443,6 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h else: hostnames = [host] - requires = parse_requires(tls_requires) - for host in hostnames: # Handle clear text and hashed passwords. if bool(password): @@ -488,8 +496,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h msg = "Password updated (old style)" else: try: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", - (user, host, ' '.join(filter(None, (encrypted_password, requires))))) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password)) msg = "Password updated (new style)" except (mysql_driver.Error) as e: # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql @@ -499,10 +506,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h "UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s", ('mysql_native_password', encrypted_password, user, host) ) - if requires: - cursor.execute("GRANT USAGE on *.* to '%s'@'%s' %s" % (user, host, requires)) - else: - cursor.execute("GRANT USAGE on *.* to '%s'@'%s'" % (user, host)) + cursor.execute("GRANT USAGE on *.* to '%s'@'%s'", (user, host)) cursor.execute("FLUSH PRIVILEGES") msg = "Password forced update" else: @@ -511,8 +515,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h # Handle plugin authentication if plugin: - cursor.execute("SELECT plugin, authentication_string FROM mysql.user " - "WHERE user = %s AND host = %s", (user, host)) + cursor.execute("SELECT plugin, authentication_string FROM mysql.user WHERE user = %s AND host = %s", (user, host)) current_plugin = cursor.fetchone() update = False @@ -532,13 +535,18 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h if update: if plugin_hash_string: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, ' '.join(filter(None, (plugin_hash_string, requires))))) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string)) elif plugin_auth_string: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, ' '.join(filter(None, (plugin_auth_string, requires))))) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)) else: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, ' '.join(filter(None, (plugin, requires))))) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)) changed = True + # Handle TLS requirements + if tls_requires is not None: + cursor.execute(*mogrify_requires("ALTER USER %s@%s", (user, host), tls_requires)) + changed = True + # Handle privileges if new_priv is not None: curr_priv = privileges_get(cursor, user, host) @@ -923,7 +931,7 @@ def main(): host_all = module.params["host_all"] state = module.params["state"] priv = module.params["priv"] - tls_requires = module.params["tls_requires"] + tls_requires = sanitize_requires(module.params["tls_requires"]) check_implicit_admin = module.params['check_implicit_admin'] connect_timeout = module.params['connect_timeout'] config_file = module.params['config_file'] From 76b0f17da20d8164d765d460d7fe547b4439e810 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 16 Jun 2020 10:51:44 +0300 Subject: [PATCH 14/37] debug test --- tests/integration/targets/mysql_user/tasks/main.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 2d3e9d97d42..725ba74abf7 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -115,6 +115,14 @@ # ============================================================ # Create users with TLS requirements and verify requirements are assigned # +- name: find out the database version + mysql_info: + login_unix_socket: '{{ mysql_socket }}' + filter: version + register: db_ver +- debug: + var: db_var + - name: create user with TLS requirements state=present (expect changed=true) mysql_user: name: '{{ item[0] }}' From 8c9c631371a72507d2f6b5d30a81c67a8b2bc314 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Wed, 17 Jun 2020 14:55:12 +0300 Subject: [PATCH 15/37] Modify code to accomodate requires statements to different db versions --- plugins/modules/database/mysql/mysql_user.py | 97 +++++++++++-- .../targets/mysql_user/tasks/main.yml | 127 ++++++++++++------ 2 files changed, 173 insertions(+), 51 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index ec82ec9fb61..1d231c9424f 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -374,6 +374,7 @@ def sanitize_requires(tls_requires): if any([key in ['CIPHER', 'ISSUER', 'SUBJECT'] for key in tls_requires.keys()]): tls_requires.pop('SSL', None) tls_requires.pop('X509', None) + return tls_requires if 'X509' in tls_requires.keys(): tls_requires = 'X509' @@ -381,7 +382,6 @@ def sanitize_requires(tls_requires): tls_requires = 'SSL' return tls_requires -# return " REQUIRE %s" % ' AND '.join([' '.join(filter(None, (key, fix_quotes(value)))) for key, value in tls_requires.items()]) return None @@ -397,6 +397,33 @@ def mogrify_requires(query, params, tls_requires): return query, params +def do_not_mogrify_requires(query, params, tls_requires): + return query, params + + +def get_tls_requires(cursor, user, host): + if server_suports_requires_create(cursor): + query = "SHOW CREATE USER for '%s'@'%s'" % (user, host) + else: + query = "SHOW GRANTS for '%s'@'%s'" % (user, host) + + cursor.execute(query) + require_line = filter(lambda x: 'REQUIRE' in x, cursor.fetchall()).next() + pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))" + requires = re.search(pattern, require_line).group().strip() + if len(requires.split()) > 1: + import shlex + items = iter(shlex.split(requires)) + requires = dict(zip(items, items)) + return requires + +def get_grant_query(cursor, user, host): + cursor.execute('SHOW GRANTS FOR %s@%s', (user, host)) + grants_line = filter(lambda x: 'ON *.*' in x, cursor.fetchall()).next() + pattern = r"(?<=\bGRANT\b)(.*?)(?=(?:\bON\b))" + grants = re.search(pattern, grants_line).group().strip() + return "GRANT %s ON *.* TO" % grants + def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, new_priv, tls_requires, check_mode): # we cannot create users without a proper hostname @@ -406,21 +433,23 @@ def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_h if check_mode: return True + mogrify = mogrify_requires if server_suports_requires_create(cursor) else do_not_mogrify_requires + if password and encrypted: - cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password), tls_requires)) + cursor.execute(*mogrify("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password), tls_requires)) elif password and not encrypted: - cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password), tls_requires)) + cursor.execute(*mogrify("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password), tls_requires)) elif plugin and plugin_hash_string: - cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string), tls_requires)) + cursor.execute(*mogrify("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string), tls_requires)) elif plugin and plugin_auth_string: - cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string), tls_requires)) + cursor.execute(*mogrify("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string), tls_requires)) elif plugin: - cursor.execute(*mogrify_requires("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin), tls_requires)) + cursor.execute(*mogrify("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin), tls_requires)) else: - cursor.execute(*mogrify_requires("CREATE USER %s@%s", (user, host), tls_requires)) + cursor.execute(*mogrify("CREATE USER %s@%s", (user, host), tls_requires)) if new_priv is not None: for db_table, priv in iteritems(new_priv): - privileges_grant(cursor, user, host, db_table, priv) + privileges_grant(cursor, user, host, db_table, priv, tls_requires) return True @@ -543,8 +572,19 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h changed = True # Handle TLS requirements - if tls_requires is not None: - cursor.execute(*mogrify_requires("ALTER USER %s@%s", (user, host), tls_requires)) + current_requires = get_tls_requires(cursor, user, host) + if current_requires != tls_requires: + if server_suports_requires_create(cursor): + pre_query = "ALTER USER" + else: + pre_query = get_grant_query(cursor, user, host) + + if tls_requires is not None: + query = ' '.join(pre_query, '%s@%s') + cursor.execute(*mogrify_requires(query, (user, host), tls_requires)) + else: + query = ' '.join(pre_query, '%s@%s REQUIRE NONE') + cursor.execute(query, (user, host)) changed = True # Handle privileges @@ -725,19 +765,23 @@ def privileges_revoke(cursor, user, host, db_table, priv, grant_option): cursor.execute(query, (user, host)) -def privileges_grant(cursor, user, host, db_table, priv): +def privileges_grant(cursor, user, host, db_table, priv, tls_requires): # Escape '%' since mysql db.execute uses a format string and the # specification of db and table often use a % (SQL wildcard) db_table = db_table.replace('%', '%%') priv_string = ",".join([p for p in priv if p not in ('GRANT', 'REQUIRESSL')]) query = ["GRANT %s ON %s" % (priv_string, db_table)] query.append("TO %s@%s") - if 'REQUIRESSL' in priv: + params = (user, host) + if tls_requires and not server_suports_requires_create(cursor): + query, params = mogrify_requires(' '.join(query), params, tls_requires) + query = [query] + if 'REQUIRESSL' in priv and not tls_requires: query.append("REQUIRE SSL") if 'GRANT' in priv: query.append("WITH GRANT OPTION") query = ' '.join(query) - cursor.execute(query, (user, host)) + cursor.execute(query, params) def convert_priv_dict_to_str(priv): @@ -754,6 +798,33 @@ def convert_priv_dict_to_str(priv): return '/'.join(priv_list) +# TLS requires on user create statement is supported since MySQL 5.7 and MariaDB 10.2 +def server_suports_requires_create(cursor): + """Check if the server supports REQUIRES on the CREATE USER statement or doesn't. + + Args: + cursor (cursor): DB driver cursor object. + + Returns: True if supports, False otherwise. + """ + cursor.execute("SELECT VERSION()") + version_str = cursor.fetchone()[0] + version = version_str.split('.') + + if 'mariadb' in version_str.lower(): + # MariaDB 10.2 and later + if int(version[0]) * 1000 + int(version[1]) >= 10002: + return True + else: + return False + else: + # MySQL 5.6 and later + if int(version[0]) * 1000 + int(version[1]) >= 5007: + return True + else: + return False + + # Alter user is supported since MySQL 5.6 and MariaDB 10.2.0 def server_supports_alter_user(cursor): """Check if the server supports ALTER USER statement or doesn't. diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 725ba74abf7..f4d88714b82 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -119,10 +119,8 @@ mysql_info: login_unix_socket: '{{ mysql_socket }}' filter: version - register: db_ver -- debug: - var: db_var - + register: db_version + - name: create user with TLS requirements state=present (expect changed=true) mysql_user: name: '{{ item[0] }}' @@ -138,41 +136,94 @@ cipher: 'ECDHE-ECDSA-AES256-SHA384' issuer: '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' -- name: retrieve TLS requiremets for users - command: mysql -e "SELECT * FROM mysql.user where User='{{ item }}'\G" - #command: mysql -L -N -s -e "SELECT ssl_type, ssl_cipher, x509_issuer, x509_subject FROM mysql.user where User='{{ item }}';" - register: result - with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] +- block: + - name: retrieve TLS requiremets for users in old database version + command: mysql -L -N -s -e "SHOW GRANTS for '{{ item }}'@'localhost'" + register: result + with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + + - name: assert user1 TLS requirements + assert: + that: 'SSL' in reqs + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() + + - name: assert user2 TLS requirements + assert: + that: 'X509' in reqs + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() + + - name: assert user3 TLS requirements + assert: + that: + - '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'SUBJECT')) + - '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'ISSUER')) + - 'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('in', 'CIPHER')) + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace("' ", "':").split(":") + when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 + +- block: + - name: retrieve TLS requiremets for users in new database version + command: mysql -L -N -s -e "SHOW CREATE USER '{{ item }}'@'localhost'" + register: result + with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + + - name: assert user1 TLS requirements + assert: + that: 'SSL' in reqs + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() + + - name: assert user2 TLS requirements + assert: + that: 'X509' in reqs + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() + + - name: assert user3 TLS requirements + assert: + that: + - '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'SUBJECT')) + - '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'ISSUER')) + - 'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('in', 'CIPHER')) + vars: + - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace("' ", "':").split(":") + when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 + +- name: modify user with TLS requirements state=present (expect changed=true) + mysql_user: + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + tls_requires: + - X509: + login_unix_socket: '{{ mysql_socket }}' + +- block: + - name: retrieve TLS requiremets for users in old database version + command: mysql -L -N -s -e "SHOW GRANTS for '{{ user_name_1 }}'@'localhost'" + register: result + + - name: assert user1 TLS requirements + assert: + that: 'X509' in reqs + vars: + - reqs: result.stdout.split('REQUIRE')[1].split('\n')[0].strip() + when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 + +- block: + - name: retrieve TLS requiremets for users in new database version + command: mysql -L -N -s -e "SHOW CREATE USER '{{ user_name_1 }}'@'localhost'" + register: result + + - name: assert user1 TLS requirements + assert: + that: 'X509' in reqs + vars: + - reqs: result.stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() + when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 -- name: assert user1 TLS requirements - assert: - that: - - reqs[0] == 'ANY' - - reqs[1] == '0x' - - reqs[2] == '0x' - - reqs[3] == '0x' - vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split() - -- name: assert user2 TLS requirements - assert: - that: - - reqs[0] == 'X509' - - reqs[1] == '0x' - - reqs[2] == '0x' - - reqs[3] == '0x' - vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_2) | first).stdout.split() - -- name: assert user3 TLS requirements - assert: - that: - - reqs[0] == 'SPECIFIED' - - reqs[1] == '0x45434448452D45434453412D4145533235362D534841333834' - - reqs[2] == '0x2F434E3D6F72672F4F3D4D79446F6D2C20496E632E2F433D55532F53543D4F7265676F6E2F4C3D506F72746C616E64' - - reqs[3] == '0x2F434E3D616C6963652F4F3D4D79446F6D2C20496E632E2F433D55532F53543D4F7265676F6E2F4C3D506F72746C616E64' - vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_3) | first).stdout.split() - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} From af34ea67521e8eba218f231be557ba36412c0cf9 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Wed, 17 Jun 2020 15:00:24 +0300 Subject: [PATCH 16/37] Fix quotations --- .../targets/mysql_user/tasks/main.yml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index f4d88714b82..ded6ae543b9 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -144,22 +144,22 @@ - name: assert user1 TLS requirements assert: - that: 'SSL' in reqs + that: "'SSL' in reqs" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() - name: assert user2 TLS requirements assert: - that: 'X509' in reqs + that: "'X509' in reqs" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() - name: assert user3 TLS requirements assert: that: - - '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'SUBJECT')) - - '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'ISSUER')) - - 'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('in', 'CIPHER')) + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'SUBJECT'))" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'ISSUER'))" + - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('in', 'CIPHER'))" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace("' ", "':").split(":") when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 @@ -172,22 +172,22 @@ - name: assert user1 TLS requirements assert: - that: 'SSL' in reqs + that: "'SSL' in reqs" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() - name: assert user2 TLS requirements assert: - that: 'X509' in reqs + that: "'X509' in reqs" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() - name: assert user3 TLS requirements assert: that: - - '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'SUBJECT')) - - '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'ISSUER')) - - 'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('in', 'CIPHER')) + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'SUBJECT'))" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'ISSUER'))" + - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('in', 'CIPHER'))" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace("' ", "':").split(":") when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 @@ -207,7 +207,7 @@ - name: assert user1 TLS requirements assert: - that: 'X509' in reqs + that: "'X509' in reqs" vars: - reqs: result.stdout.split('REQUIRE')[1].split('\n')[0].strip() when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 @@ -219,7 +219,7 @@ - name: assert user1 TLS requirements assert: - that: 'X509' in reqs + that: "'X509' in reqs" vars: - reqs: result.stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 From 14332393c6f9c4599e59d2cdd9bfe356d30383bc Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Wed, 17 Jun 2020 15:01:24 +0300 Subject: [PATCH 17/37] Fix PEP errors --- plugins/modules/database/mysql/mysql_user.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 1d231c9424f..6594315aa0b 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -417,6 +417,7 @@ def get_tls_requires(cursor, user, host): requires = dict(zip(items, items)) return requires + def get_grant_query(cursor, user, host): cursor.execute('SHOW GRANTS FOR %s@%s', (user, host)) grants_line = filter(lambda x: 'ON *.*' in x, cursor.fetchall()).next() @@ -424,6 +425,7 @@ def get_grant_query(cursor, user, host): grants = re.search(pattern, grants_line).group().strip() return "GRANT %s ON *.* TO" % grants + def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, new_priv, tls_requires, check_mode): # we cannot create users without a proper hostname From e852758aae674e7c312846c28f631b72f4d48ead Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Wed, 17 Jun 2020 15:21:50 +0300 Subject: [PATCH 18/37] Don't query requires for blank users --- plugins/modules/database/mysql/mysql_user.py | 29 ++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 6594315aa0b..532ba663661 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -402,20 +402,21 @@ def do_not_mogrify_requires(query, params, tls_requires): def get_tls_requires(cursor, user, host): - if server_suports_requires_create(cursor): - query = "SHOW CREATE USER for '%s'@'%s'" % (user, host) - else: - query = "SHOW GRANTS for '%s'@'%s'" % (user, host) - - cursor.execute(query) - require_line = filter(lambda x: 'REQUIRE' in x, cursor.fetchall()).next() - pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))" - requires = re.search(pattern, require_line).group().strip() - if len(requires.split()) > 1: - import shlex - items = iter(shlex.split(requires)) - requires = dict(zip(items, items)) - return requires + if user: + if server_suports_requires_create(cursor): + query = "SHOW CREATE USER for '%s'@'%s'" % (user, host) + else: + query = "SHOW GRANTS for '%s'@'%s'" % (user, host) + + cursor.execute(query) + require_line = filter(lambda x: 'REQUIRE' in x, cursor.fetchall()).next() + pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))" + requires = re.search(pattern, require_line).group().strip() + if len(requires.split()) > 1: + import shlex + items = iter(shlex.split(requires)) + requires = dict(zip(items, items)) + return requires def get_grant_query(cursor, user, host): From 0b6297575f3f378a34cfc3b893f691f415510e90 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Wed, 17 Jun 2020 15:23:33 +0300 Subject: [PATCH 19/37] Use old style formatting --- plugins/modules/database/mysql/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 532ba663661..170aa643e59 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -389,7 +389,7 @@ def mogrify_requires(query, params, tls_requires): if tls_requires: if isinstance(tls_requires, dict): k, v = zip(*tls_requires.items()) - requires_query = ' AND '.join(('{} %s'.format(key) for key in k)) + requires_query = ' AND '.join(('%s %%s' % key for key in k)) params += v else: requires_query = tls_requires From 98e4f4ac204abb7d7d8e49a0753c4ff525c56def Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Wed, 17 Jun 2020 16:02:01 +0300 Subject: [PATCH 20/37] Fix filter handling --- plugins/modules/database/mysql/mysql_user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 170aa643e59..f4dcddea775 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -409,7 +409,8 @@ def get_tls_requires(cursor, user, host): query = "SHOW GRANTS for '%s'@'%s'" % (user, host) cursor.execute(query) - require_line = filter(lambda x: 'REQUIRE' in x, cursor.fetchall()).next() + require_list = list(filter(lambda x: 'REQUIRE' in x, cursor.fetchall())) + require_line = require_list[0] if require_list else '' pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))" requires = re.search(pattern, require_line).group().strip() if len(requires.split()) > 1: From 2957ffde6e27c9249a4fd096f83190529062c06d Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Wed, 17 Jun 2020 16:27:55 +0300 Subject: [PATCH 21/37] Handle match objects properly --- plugins/modules/database/mysql/mysql_user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index f4dcddea775..35f9b1a2955 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -412,12 +412,13 @@ def get_tls_requires(cursor, user, host): require_list = list(filter(lambda x: 'REQUIRE' in x, cursor.fetchall())) require_line = require_list[0] if require_list else '' pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))" - requires = re.search(pattern, require_line).group().strip() + requires_match = re.search(pattern, require_line) + requires = requires_match.group().strip() if requires_match else '' if len(requires.split()) > 1: import shlex items = iter(shlex.split(requires)) requires = dict(zip(items, items)) - return requires + return requires or None def get_grant_query(cursor, user, host): From e9a3ae159e32b8f65327f69333fa22e86f7bfbd2 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Thu, 18 Jun 2020 12:23:47 +0300 Subject: [PATCH 22/37] Handle existing grants when managing TLS requires in old db versions --- plugins/modules/database/mysql/mysql_user.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 35f9b1a2955..61d539a303b 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -421,12 +421,12 @@ def get_tls_requires(cursor, user, host): return requires or None -def get_grant_query(cursor, user, host): +def get_grants(cursor, user, host): cursor.execute('SHOW GRANTS FOR %s@%s', (user, host)) - grants_line = filter(lambda x: 'ON *.*' in x, cursor.fetchall()).next() + grants_line = list(filter(lambda x: 'ON *.*' in x[0], cursor.fetchall()))[0] pattern = r"(?<=\bGRANT\b)(.*?)(?=(?:\bON\b))" - grants = re.search(pattern, grants_line).group().strip() - return "GRANT %s ON *.* TO" % grants + grants = re.search(pattern, grants_line[0]).group().strip() + return grants.split(', ') def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, @@ -455,6 +455,8 @@ def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_h if new_priv is not None: for db_table, priv in iteritems(new_priv): privileges_grant(cursor, user, host, db_table, priv, tls_requires) + if tls_requires is not None: + privileges_grant(cursor, user, host, '*.*', get_grants(cursor, user, host), tls_requires) return True @@ -582,10 +584,10 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h if server_suports_requires_create(cursor): pre_query = "ALTER USER" else: - pre_query = get_grant_query(cursor, user, host) + pre_query = 'GRANT %s ON *.* TO' % ','.join(get_grants(cursor, user, host)) if tls_requires is not None: - query = ' '.join(pre_query, '%s@%s') + query = ' '.join((pre_query, '%s@%s')) cursor.execute(*mogrify_requires(query, (user, host), tls_requires)) else: query = ' '.join(pre_query, '%s@%s REQUIRE NONE') From e9269db7d8c70cf418b3f90a9605b143b9c2db11 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Thu, 18 Jun 2020 12:24:31 +0300 Subject: [PATCH 23/37] Fix typo --- plugins/modules/database/mysql/mysql_user.py | 4 ++-- .../integration/targets/mysql_user/tasks/main.yml | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 61d539a303b..fac2441ca04 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -191,14 +191,14 @@ priv: '*.*:REQUIRESSL' state: present -- name: Modifiy user to require TLS connection with a valid client certificate +- name: Modify user to require TLS connection with a valid client certificate mysql_user: name: bob tls_requires: x509: state: present -- name: Modifiy user to require TLS connection with a specific client certificate and cipher +- name: Modify user to require TLS connection with a specific client certificate and cipher mysql_user: name: bob tls_requires: diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index ded6ae543b9..4f726929d9e 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -144,13 +144,13 @@ - name: assert user1 TLS requirements assert: - that: "'SSL' in reqs" + that: "'SSL' in {{reqs}}" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() - name: assert user2 TLS requirements assert: - that: "'X509' in reqs" + that: "'X509' in {{reqs}}" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() @@ -172,13 +172,16 @@ - name: assert user1 TLS requirements assert: - that: "'SSL' in reqs" + that: + - "'SSL' in {{reqs}}" + fail_msg: "{{ reqs }}" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() - name: assert user2 TLS requirements assert: - that: "'X509' in reqs" + that: + - "'X509' in {{reqs}}" vars: - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() @@ -207,7 +210,7 @@ - name: assert user1 TLS requirements assert: - that: "'X509' in reqs" + that: "'X509' in {{reqs}}" vars: - reqs: result.stdout.split('REQUIRE')[1].split('\n')[0].strip() when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 @@ -219,7 +222,7 @@ - name: assert user1 TLS requirements assert: - that: "'X509' in reqs" + that: "'X509' in {{reqs}}" vars: - reqs: result.stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 From 104a0d119ac72bcd0bdace1d5715b816cd2801b8 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Mon, 22 Jun 2020 10:16:25 +0300 Subject: [PATCH 24/37] Change select test --- .../targets/mysql_user/tasks/main.yml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 4f726929d9e..873223e2dfa 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -146,22 +146,22 @@ assert: that: "'SSL' in {{reqs}}" vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() + - reqs: (result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() - name: assert user2 TLS requirements assert: that: "'X509' in {{reqs}}" vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() + - reqs: (result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() - name: assert user3 TLS requirements assert: that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'SUBJECT'))" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'ISSUER'))" - - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('in', 'CIPHER'))" + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT'))" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER'))" + - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER'))" vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace("' ", "':").split(":") + - reqs: (result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace("' ", "':").split(":") when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 - block: @@ -176,23 +176,23 @@ - "'SSL' in {{reqs}}" fail_msg: "{{ reqs }}" vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() + - reqs: (result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() - name: assert user2 TLS requirements assert: that: - "'X509' in {{reqs}}" vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() + - reqs: (result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() - name: assert user3 TLS requirements assert: that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'SUBJECT'))" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('in', 'ISSUER'))" - - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('in', 'CIPHER'))" + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT'))" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER'))" + - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER'))" vars: - - reqs: (result.results | selectattr('item', 'eq', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace("' ", "':").split(":") + - reqs: (result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace("' ", "':").split(":") when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 - name: modify user with TLS requirements state=present (expect changed=true) From 6d03c69cbca71333b3c5f943deb4be1956477109 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Mon, 22 Jun 2020 10:28:45 +0300 Subject: [PATCH 25/37] Fix show create user statement --- plugins/modules/database/mysql/mysql_user.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index fac2441ca04..a54d01b0b31 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -67,6 +67,7 @@ - SUBJECT, ISSUER and CIPHER are complementary, and mutually exclusive with SSL and X509. - U(https://mariadb.com/kb/en/securing-connections-for-client-and-server/#requiring-tls). type: dict + version_added: 1.0.0 sql_log_bin: description: - Whether binary logging should be enabled or disabled for the connection. @@ -404,7 +405,7 @@ def do_not_mogrify_requires(query, params, tls_requires): def get_tls_requires(cursor, user, host): if user: if server_suports_requires_create(cursor): - query = "SHOW CREATE USER for '%s'@'%s'" % (user, host) + query = "SHOW CREATE USER '%s'@'%s'" % (user, host) else: query = "SHOW GRANTS for '%s'@'%s'" % (user, host) @@ -581,6 +582,9 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h # Handle TLS requirements current_requires = get_tls_requires(cursor, user, host) if current_requires != tls_requires: + msg = "TLS requires updated" + if module.check_mode: + return (True, msg) if server_suports_requires_create(cursor): pre_query = "ALTER USER" else: From e17ad26cdb7ce11b9accdb88e7c710df66d2f89b Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Mon, 22 Jun 2020 11:10:29 +0300 Subject: [PATCH 26/37] Fix variable references --- tests/integration/targets/mysql_user/tasks/main.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 873223e2dfa..a5e7c427e23 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -157,9 +157,9 @@ - name: assert user3 TLS requirements assert: that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT'))" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER'))" - - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER'))" + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'SUBJECT')}}" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'ISSUER')}}" + - "'ECDHE-ECDSA-AES256-SHA384' in {{reqs | select('contains', 'CIPHER')}}" vars: - reqs: (result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace("' ", "':").split(":") when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 @@ -174,7 +174,6 @@ assert: that: - "'SSL' in {{reqs}}" - fail_msg: "{{ reqs }}" vars: - reqs: (result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() @@ -188,9 +187,9 @@ - name: assert user3 TLS requirements assert: that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT'))" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER'))" - - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER'))" + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'SUBJECT')}}" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'ISSUER')}}" + - "'ECDHE-ECDSA-AES256-SHA384' in {{reqs | select('contains', 'CIPHER')}}" vars: - reqs: (result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace("' ", "':").split(":") when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 From c4695387ef3acd73f12c1f58cc3dc2480b4f699b Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Mon, 22 Jun 2020 12:42:43 +0300 Subject: [PATCH 27/37] Fix assertion variable definitions --- .../targets/mysql_user/tasks/main.yml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index a5e7c427e23..e208f9de732 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -146,22 +146,22 @@ assert: that: "'SSL' in {{reqs}}" vars: - - reqs: (result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" - name: assert user2 TLS requirements assert: that: "'X509' in {{reqs}}" vars: - - reqs: (result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip() + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" - name: assert user3 TLS requirements assert: that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'SUBJECT')}}" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'ISSUER')}}" - - "'ECDHE-ECDSA-AES256-SHA384' in {{reqs | select('contains', 'CIPHER')}}" + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'SUBJECT') | first}}" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'ISSUER') | first}}" + - "'ECDHE-ECDSA-AES256-SHA384' in {{reqs | select('contains', 'CIPHER') | first}}" vars: - - reqs: (result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace("' ", "':").split(":") + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace(\"' \", \"':\").split(\":\")}}" when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 - block: @@ -175,23 +175,23 @@ that: - "'SSL' in {{reqs}}" vars: - - reqs: (result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" - name: assert user2 TLS requirements assert: that: - "'X509' in {{reqs}}" vars: - - reqs: (result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" - name: assert user3 TLS requirements assert: that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'SUBJECT')}}" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'ISSUER')}}" - - "'ECDHE-ECDSA-AES256-SHA384' in {{reqs | select('contains', 'CIPHER')}}" + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'SUBJECT') | first}}" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'ISSUER')| first}}" + - "'ECDHE-ECDSA-AES256-SHA384' in {{reqs | select('contains', 'CIPHER') | first}}" vars: - - reqs: (result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace("' ", "':").split(":") + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace(\"' \", \"':\").split(\":\")}}" when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 - name: modify user with TLS requirements state=present (expect changed=true) @@ -211,7 +211,7 @@ assert: that: "'X509' in {{reqs}}" vars: - - reqs: result.stdout.split('REQUIRE')[1].split('\n')[0].strip() + - reqs: "{{result.stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 - block: @@ -223,7 +223,7 @@ assert: that: "'X509' in {{reqs}}" vars: - - reqs: result.stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip() + - reqs: "{{result.stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 From efc7aa50b4bc5ceb86e0af8fe027a769f0780792 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Mon, 22 Jun 2020 20:23:38 +0300 Subject: [PATCH 28/37] Fix test variables --- .../targets/mysql_user/tasks/main.yml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index e208f9de732..c41112d0c01 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -144,22 +144,22 @@ - name: assert user1 TLS requirements assert: - that: "'SSL' in {{reqs}}" + that: "'SSL' in reqs" vars: - reqs: "{{(result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" - name: assert user2 TLS requirements assert: - that: "'X509' in {{reqs}}" + that: "'X509' in reqs" vars: - reqs: "{{(result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" - name: assert user3 TLS requirements assert: that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'SUBJECT') | first}}" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'ISSUER') | first}}" - - "'ECDHE-ECDSA-AES256-SHA384' in {{reqs | select('contains', 'CIPHER') | first}}" + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT') | first)" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)" + - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)" vars: - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace(\"' \", \"':\").split(\":\")}}" when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 @@ -173,23 +173,23 @@ - name: assert user1 TLS requirements assert: that: - - "'SSL' in {{reqs}}" + - "'SSL' in reqs" vars: - reqs: "{{(result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" - name: assert user2 TLS requirements assert: that: - - "'X509' in {{reqs}}" + - "'X509' in reqs" vars: - reqs: "{{(result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" - name: assert user3 TLS requirements assert: that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'SUBJECT') | first}}" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in {{reqs | select('contains', 'ISSUER')| first}}" - - "'ECDHE-ECDSA-AES256-SHA384' in {{reqs | select('contains', 'CIPHER') | first}}" + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT') | first)" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)" + - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)" vars: - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace(\"' \", \"':\").split(\":\")}}" when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 @@ -209,7 +209,7 @@ - name: assert user1 TLS requirements assert: - that: "'X509' in {{reqs}}" + that: "'X509' in reqs" vars: - reqs: "{{result.stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 @@ -221,7 +221,7 @@ - name: assert user1 TLS requirements assert: - that: "'X509' in {{reqs}}" + that: "'X509' in reqs" vars: - reqs: "{{result.stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 From ce7e4cc2686de5e8772624f58120061dfb4f8ea3 Mon Sep 17 00:00:00 2001 From: "Jorge Rodriguez (A.K.A. Tiriel)" Date: Tue, 23 Jun 2020 02:04:53 +0300 Subject: [PATCH 29/37] Fix tls_requires parameter --- tests/integration/targets/mysql_user/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index c41112d0c01..5c395d32086 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -199,7 +199,7 @@ name: '{{ user_name_1 }}' password: '{{ user_password_1 }}' tls_requires: - - X509: + X509: login_unix_socket: '{{ mysql_socket }}' - block: From 247d4dc5fb9081a75c51e4566c85c499e8326a86 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 23 Jun 2020 09:11:07 +0300 Subject: [PATCH 30/37] Don't modify dictionary while iterating it --- plugins/modules/database/mysql/mysql_user.py | 23 ++++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index a54d01b0b31..9a71c677e6d 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -367,22 +367,21 @@ def user_exists(cursor, user, host, host_all): def sanitize_requires(tls_requires): + sanitized_requires = {} if tls_requires: for key in tls_requires.keys(): - if not key.isupper(): - tls_requires[key.upper()] = tls_requires[key] - tls_requires.pop(key) - if any([key in ['CIPHER', 'ISSUER', 'SUBJECT'] for key in tls_requires.keys()]): - tls_requires.pop('SSL', None) - tls_requires.pop('X509', None) - return tls_requires - - if 'X509' in tls_requires.keys(): - tls_requires = 'X509' + sanitized_requires[key.upper()] = tls_requires[key] + if any([key in ['CIPHER', 'ISSUER', 'SUBJECT'] for key in sanitized_requires.keys()]): + sanitized_requires.pop('SSL', None) + sanitized_requires.pop('X509', None) + return sanitized_requires + + if 'X509' in sanitized_requires.keys(): + sanitized_requires = 'X509' else: - tls_requires = 'SSL' + sanitized_requires = 'SSL' - return tls_requires + return sanitized_requires return None From 95199dd6236b096f829007c7d5cb2feecb993310 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 23 Jun 2020 09:14:35 +0300 Subject: [PATCH 31/37] Add tls_requires to all privileges_grant calls --- plugins/modules/database/mysql/mysql_user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 9a71c677e6d..37710e9f3bc 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -622,7 +622,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h msg = "New privileges granted" if module.check_mode: return (True, msg) - privileges_grant(cursor, user, host, db_table, priv) + privileges_grant(cursor, user, host, db_table, priv, tls_requires) changed = True # If the db.table specification exists in both the user's current privileges @@ -636,7 +636,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h return (True, msg) if not append_privs: privileges_revoke(cursor, user, host, db_table, curr_priv[db_table], grant_option) - privileges_grant(cursor, user, host, db_table, new_priv[db_table]) + privileges_grant(cursor, user, host, db_table, new_priv[db_table], tls_requires) changed = True return (changed, msg) From ef43b48849d47c22e8af6bdeea99dcb150ad7fff Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 23 Jun 2020 12:56:09 +0300 Subject: [PATCH 32/37] Refactor tests to reduce duplication --- .../targets/mysql_user/tasks/main.yml | 70 +++++++------------ 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 5c395d32086..6aaee68efc7 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -142,26 +142,9 @@ register: result with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] - - name: assert user1 TLS requirements - assert: - that: "'SSL' in reqs" - vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" - - - name: assert user2 TLS requirements - assert: - that: "'X509' in reqs" - vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" - - - name: assert user3 TLS requirements - assert: - that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT') | first)" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)" - - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)" - vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('\n')[0].replace(\"' \", \"':\").split(\":\")}}" + - name: set old database separator + set_fact: + separator: '\n' when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 - block: @@ -170,19 +153,25 @@ register: result with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + - name: set new database separator + set_fact: + separator: 'PASSWORD' + when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 + +- block: - name: assert user1 TLS requirements assert: that: - "'SSL' in reqs" vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - name: assert user2 TLS requirements assert: that: - "'X509' in reqs" vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - name: assert user3 TLS requirements assert: @@ -191,8 +180,9 @@ - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)" - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)" vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split('PASSWORD')[0].replace(\"' \", \"':\").split(\":\")}}" - when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 + - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\")}}" + # CentOS 6 uses an older version of jinja that does not provide the selectattr filter. + when: ansible_distribution is not 'CentOS' or ansible_distribution_major_version is not '6' - name: modify user with TLS requirements state=present (expect changed=true) mysql_user: @@ -202,30 +192,22 @@ X509: login_unix_socket: '{{ mysql_socket }}' -- block: - - name: retrieve TLS requiremets for users in old database version - command: mysql -L -N -s -e "SHOW GRANTS for '{{ user_name_1 }}'@'localhost'" - register: result - - - name: assert user1 TLS requirements - assert: - that: "'X509' in reqs" - vars: - - reqs: "{{result.stdout.split('REQUIRE')[1].split('\n')[0].strip()}}" +- name: retrieve TLS requiremets for users in old database version + command: mysql -L -N -s -e "SHOW GRANTS for '{{ user_name_1 }}'@'localhost'" + register: result when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 -- block: - - name: retrieve TLS requiremets for users in new database version - command: mysql -L -N -s -e "SHOW CREATE USER '{{ user_name_1 }}'@'localhost'" - register: result - - - name: assert user1 TLS requirements - assert: - that: "'X509' in reqs" - vars: - - reqs: "{{result.stdout.split('REQUIRE')[1].split('PASSWORD')[0].strip()}}" +- name: retrieve TLS requiremets for users in new database version + command: mysql -L -N -s -e "SHOW CREATE USER '{{ user_name_1 }}'@'localhost'" + register: result when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 +- name: assert user1 TLS requirements + assert: + that: "'X509' in reqs" + vars: + - reqs: "{{result.stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" + - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} From 4cbd9e06ec055c8aaa0bb2f6c56b80c88e3e1ca4 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 23 Jun 2020 13:44:15 +0300 Subject: [PATCH 33/37] Fix conditional clause --- tests/integration/targets/mysql_user/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 6aaee68efc7..2c20ec0ac7d 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -182,7 +182,7 @@ vars: - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\")}}" # CentOS 6 uses an older version of jinja that does not provide the selectattr filter. - when: ansible_distribution is not 'CentOS' or ansible_distribution_major_version is not '6' + when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6' - name: modify user with TLS requirements state=present (expect changed=true) mysql_user: From eaeaee1057b5c95a8e3edad7cfd158462b78eaae Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 23 Jun 2020 15:37:09 +0300 Subject: [PATCH 34/37] Improve changelog message --- changelogs/fragments/369-mysql_user_add_tls_requires.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/369-mysql_user_add_tls_requires.yml b/changelogs/fragments/369-mysql_user_add_tls_requires.yml index f1c60da445d..6a479ec4e0e 100644 --- a/changelogs/fragments/369-mysql_user_add_tls_requires.yml +++ b/changelogs/fragments/369-mysql_user_add_tls_requires.yml @@ -1,4 +1,4 @@ minor_changes: - mysql_user - add TLS REQUIRES parameters (https://github.com/ansible-collections/community.general/pull/369). deprecated_features: - - mysql_user - REQUIRESSL is deprecated in favor of `tls_requires`. + - mysql_user - using ``REQUIRESSL`` in ``priv`` is deprecated in favor of ``tls_requires`` (https://github.com/ansible-collections/community.general/pull/369). From 0fcf8270ae0b476dcd87bfece5688a662e4a8e3c Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 23 Jun 2020 16:50:48 +0300 Subject: [PATCH 35/37] Properly set results variable --- .../targets/mysql_user/tasks/main.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/integration/targets/mysql_user/tasks/main.yml b/tests/integration/targets/mysql_user/tasks/main.yml index 2c20ec0ac7d..b3be1c3d90b 100644 --- a/tests/integration/targets/mysql_user/tasks/main.yml +++ b/tests/integration/targets/mysql_user/tasks/main.yml @@ -139,7 +139,7 @@ - block: - name: retrieve TLS requiremets for users in old database version command: mysql -L -N -s -e "SHOW GRANTS for '{{ item }}'@'localhost'" - register: result + register: old_result with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] - name: set old database separator @@ -150,7 +150,7 @@ - block: - name: retrieve TLS requiremets for users in new database version command: mysql -L -N -s -e "SHOW CREATE USER '{{ item }}'@'localhost'" - register: result + register: new_result with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] - name: set new database separator @@ -164,14 +164,14 @@ that: - "'SSL' in reqs" vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" + - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - name: assert user2 TLS requirements assert: that: - "'X509' in reqs" vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" + - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - name: assert user3 TLS requirements assert: @@ -180,7 +180,7 @@ - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)" - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)" vars: - - reqs: "{{(result.results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\")}}" + - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\")}}" # CentOS 6 uses an older version of jinja that does not provide the selectattr filter. when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6' @@ -194,19 +194,19 @@ - name: retrieve TLS requiremets for users in old database version command: mysql -L -N -s -e "SHOW GRANTS for '{{ user_name_1 }}'@'localhost'" - register: result + register: old_result when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 - name: retrieve TLS requiremets for users in new database version command: mysql -L -N -s -e "SHOW CREATE USER '{{ user_name_1 }}'@'localhost'" - register: result + register: new_result when: db_version.version.major >= 5 and db_version.version.major < 10 and db_version.version.minor > 6 or db_version.version.major == 10 and db_version.version.minor >= 2 - name: assert user1 TLS requirements assert: that: "'X509' in reqs" vars: - - reqs: "{{result.stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" + - reqs: "{{(old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} From b72d2a1e52e685de177b8ed8aa5075ac5f4b3b42 Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Tue, 23 Jun 2020 19:29:02 +0300 Subject: [PATCH 36/37] trigger shippable From f582451f67ca729bfb6cdfe5e5baa59fea47055c Mon Sep 17 00:00:00 2001 From: Jorge Rodriguez Date: Wed, 24 Jun 2020 10:31:35 +0300 Subject: [PATCH 37/37] Handle TLS requires after privileges --- plugins/modules/database/mysql/mysql_user.py | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/modules/database/mysql/mysql_user.py b/plugins/modules/database/mysql/mysql_user.py index 37710e9f3bc..45ca5b1f61f 100644 --- a/plugins/modules/database/mysql/mysql_user.py +++ b/plugins/modules/database/mysql/mysql_user.py @@ -578,25 +578,6 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)) changed = True - # Handle TLS requirements - current_requires = get_tls_requires(cursor, user, host) - if current_requires != tls_requires: - msg = "TLS requires updated" - if module.check_mode: - return (True, msg) - if server_suports_requires_create(cursor): - pre_query = "ALTER USER" - else: - pre_query = 'GRANT %s ON *.* TO' % ','.join(get_grants(cursor, user, host)) - - if tls_requires is not None: - query = ' '.join((pre_query, '%s@%s')) - cursor.execute(*mogrify_requires(query, (user, host), tls_requires)) - else: - query = ' '.join(pre_query, '%s@%s REQUIRE NONE') - cursor.execute(query, (user, host)) - changed = True - # Handle privileges if new_priv is not None: curr_priv = privileges_get(cursor, user, host) @@ -639,6 +620,25 @@ def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_h privileges_grant(cursor, user, host, db_table, new_priv[db_table], tls_requires) changed = True + # Handle TLS requirements + current_requires = get_tls_requires(cursor, user, host) + if current_requires != tls_requires: + msg = "TLS requires updated" + if module.check_mode: + return (True, msg) + if server_suports_requires_create(cursor): + pre_query = "ALTER USER" + else: + pre_query = 'GRANT %s ON *.* TO' % ','.join(get_grants(cursor, user, host)) + + if tls_requires is not None: + query = ' '.join((pre_query, '%s@%s')) + cursor.execute(*mogrify_requires(query, (user, host), tls_requires)) + else: + query = ' '.join(pre_query, '%s@%s REQUIRE NONE') + cursor.execute(query, (user, host)) + changed = True + return (changed, msg)