From 52737129415b4e0761e8a0c64ec6a00cd0f400e1 Mon Sep 17 00:00:00 2001 From: n-cc Date: Tue, 9 Jan 2024 16:23:17 -0600 Subject: [PATCH 1/6] add support for mysql user attributes --- plugins/module_utils/user.py | 177 ++++++++++++++++++++++++---------- plugins/modules/mysql_user.py | 28 +++++- 2 files changed, 149 insertions(+), 56 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index dbc1c9b8..b2de38df 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -10,6 +10,7 @@ # Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) import string +import json import re from ansible.module_utils.six import iteritems @@ -151,14 +152,20 @@ def get_existing_authentication(cursor, user, host): def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, new_priv, - tls_requires, check_mode, reuse_existing_password): + attributes, tls_requires, reuse_existing_password, module): # we cannot create users without a proper hostname if host_all: return {'changed': False, 'password_changed': False} - if check_mode: + if module.check_mode: return {'changed': True, 'password_changed': None} + # If attributes are set, perform a sanity check to ensure server supports user attributes before creating user + if attributes and not get_attribute_support(cursor): + module.fail_json(msg="user attributes were specified but the mysql server does not support user attributes") + + final_attributes = {} + # Determine what user management method server uses old_user_mgmt = impl.use_old_user_mgmt(cursor) @@ -203,9 +210,13 @@ def user_add(cursor, user, host, host_all, password, encrypted, 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 attributes is not None: + cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes))) + final_attributes = attributes_get(cursor, user, host) if tls_requires is not None: privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires) - return {'changed': True, 'password_changed': not used_existing_password} + + return {'changed': True, 'password_changed': not used_existing_password, 'attributes': final_attributes} def is_hash(password): @@ -218,7 +229,7 @@ def is_hash(password): def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, new_priv, - append_privs, subtract_privs, tls_requires, module, role=False, maria_role=False): + append_privs, subtract_privs, attributes, tls_requires, module, role=False, maria_role=False): changed = False msg = "User unchanged" grant_option = False @@ -278,27 +289,26 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if current_pass_hash != encrypted_password: password_changed = True msg = "Password updated" - if module.check_mode: - return {'changed': True, 'msg': msg, 'password_changed': password_changed} - if old_user_mgmt: - cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password)) - msg = "Password updated (old style)" - else: - try: - 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 - # Replacing empty root password with new authentication mechanisms fails with error 1396 - if e.args[0] == 1396: - cursor.execute( - "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("FLUSH PRIVILEGES") - msg = "Password forced update" - else: - raise e + if not module.check_mode: + if old_user_mgmt: + cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password)) + msg = "Password updated (old style)" + else: + try: + 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 + # Replacing empty root password with new authentication mechanisms fails with error 1396 + if e.args[0] == 1396: + cursor.execute( + "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("FLUSH PRIVILEGES") + msg = "Password forced update" + else: + raise e changed = True # Handle plugin authentication @@ -352,9 +362,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if db_table not in new_priv: if user != "root" and "PROXY" not in priv: msg = "Privileges updated" - if module.check_mode: - return {'changed': True, 'msg': msg, 'password_changed': password_changed} - privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role) + if not module.check_mode: + privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role) changed = True # If the user doesn't currently have any privileges on a db.table, then @@ -363,9 +372,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted, for db_table, priv in iteritems(new_priv): if db_table not in curr_priv: msg = "New privileges granted" - if module.check_mode: - return {'changed': True, 'msg': msg, 'password_changed': password_changed} - privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role) + if not module.check_mode: + privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role) changed = True # If the db.table specification exists in both the user's current privileges @@ -404,17 +412,42 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if len(grant_privs) + len(revoke_privs) > 0: msg = "Privileges updated: granted %s, revoked %s" % (grant_privs, revoke_privs) - if module.check_mode: - return {'changed': True, 'msg': msg, 'password_changed': password_changed} - if len(revoke_privs) > 0: - privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role) - if len(grant_privs) > 0: - privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role) + if not module.check_mode: + if len(revoke_privs) > 0: + privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role) + if len(grant_privs) > 0: + privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role) # after privilege manipulation, compare privileges from before and now after_priv = privileges_get(cursor, user, host, maria_role) changed = changed or (curr_priv != after_priv) + # Handle attributes + attribute_support = get_attribute_support(cursor) + + if attributes: + if not attribute_support: + module.fail_json(msg="user attributes were specified but the mysql server does not support user attributes") + else: + current_attributes = attributes_get(cursor, user, host) + attributes_to_change = {} + + for key, value in attributes.items(): + if key not in current_attributes or current_attributes[key] != value: + # The mysql null value (None in python) is used to delete an attribute; we use False to represent None in the attributes parameter + attributes_to_change[key] = None if not value else value + + if attributes_to_change: + msg = "Attributes updated: %s" % (", ".join(["%s: %s" % (key, value) for key, value in attributes_to_change.items()])) + if not module.check_mode: + cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes_to_change))) + changed = True + + if attribute_support: + final_attributes = attributes_get(cursor, user, host) + else: + final_attributes = {} + if role: continue @@ -422,24 +455,23 @@ def user_mod(cursor, user, host, host_all, password, encrypted, current_requires = get_tls_requires(cursor, user, host) if current_requires != tls_requires: msg = "TLS requires updated" - if module.check_mode: - return {'changed': True, 'msg': msg, 'password_changed': password_changed} - if not old_user_mgmt: - pre_query = "ALTER USER" - else: - pre_query = "GRANT %s ON *.* TO" % ",".join(get_grants(cursor, user, host)) + if not module.check_mode: + if not old_user_mgmt: + 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")) - query_with_args = mogrify_requires(query, (user, host), tls_requires) - else: - query = " ".join((pre_query, "%s@%s REQUIRE NONE")) - query_with_args = query, (user, host) + if tls_requires is not None: + query = " ".join((pre_query, "%s@%s")) + query_with_args = mogrify_requires(query, (user, host), tls_requires) + else: + query = " ".join((pre_query, "%s@%s REQUIRE NONE")) + query_with_args = query, (user, host) - cursor.execute(*query_with_args) + cursor.execute(*query_with_args) changed = True - return {'changed': changed, 'msg': msg, 'password_changed': password_changed} + return {'changed': changed, 'msg': msg, 'password_changed': password_changed, 'attributes': final_attributes} def user_delete(cursor, user, host, host_all, check_mode): @@ -924,6 +956,49 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode): return True +def get_attribute_support(cursor): + """Checks if the MySQL server supports user attributes. + + Args: + cursor (cursor): DB driver cursor object. + Returns: + True if attributes are supported, False if they are not. + """ + + try: + # information_schema.tables does not hold the tables within information_schema itself + cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES LIMIT 0") + cursor.fetchone() + except mysql_driver.OperationalError: + return False + + return True + +def attributes_get(cursor, user, host): + """Get attributes for a given user. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User host name. + + Returns: + None if the user does not exist, otherwise a dict of attributes set on the user + """ + cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = %s AND host = %s", (user, host)) + + r = cursor.fetchone() + + if r: + attributes = r[0] + # convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format + if attributes: + return json.loads(attributes) + else: + return {} + + return None + def get_impl(cursor): global impl cursor.execute("SELECT VERSION()") diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 3e914e6a..1d4aad3a 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -155,7 +155,6 @@ - Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead. type: dict version_added: '3.6.0' - column_case_sensitive: description: - The default is C(false). @@ -165,6 +164,13 @@ fields names in privileges. type: bool version_added: '3.8.0' + attributes: + description: + - Create, update, or delete user attributes (arbitrary "key: value" comments) for the user. + - MySQL server must support the INFORMATION_SCHEMA.USER_ATTRIBUTES table. Provided since MySQL 8.0. + - To delete an existing attribute, set its value to False. + type: dict + version_added: '3.9.0' notes: - "MySQL server installs with default I(login_user) of C(root) and no password. @@ -257,6 +263,13 @@ FUNCTION my_db.my_function: EXECUTE state: present +- name: Modify user attributes, creating the attribute 'foo' and removing the attribute 'bar' + community.mysql.mysql_user: + name: bob + attributes: + foo: "foo" + bar: False + - name: Modify user to require TLS connection with a valid client certificate community.mysql.mysql_user: name: bob @@ -405,6 +418,7 @@ def main(): tls_requires=dict(type='dict'), append_privs=dict(type='bool', default=False), subtract_privs=dict(type='bool', default=False), + attributes=dict(type='dict'), check_implicit_admin=dict(type='bool', default=False), update_password=dict(type='str', default='always', choices=['always', 'on_create', 'on_new_username'], no_log=False), sql_log_bin=dict(type='bool', default=True), @@ -437,6 +451,7 @@ def main(): append_privs = module.boolean(module.params["append_privs"]) subtract_privs = module.boolean(module.params['subtract_privs']) update_password = module.params['update_password'] + attributes = module.params['attributes'] ssl_cert = module.params["client_cert"] ssl_key = module.params["client_key"] ssl_ca = module.params["ca_cert"] @@ -500,21 +515,23 @@ def main(): priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs) password_changed = False + final_attributes = {} if state == "present": if user_exists(cursor, user, host, host_all): try: if update_password == "always": result = user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, subtract_privs, tls_requires, module) + priv, append_privs, subtract_privs, attributes, tls_requires, module) else: result = user_mod(cursor, user, host, host_all, None, encrypted, None, None, None, - priv, append_privs, subtract_privs, tls_requires, module) + priv, append_privs, subtract_privs, attributes, tls_requires, module) changed = result['changed'] msg = result['msg'] password_changed = result['password_changed'] + final_attributes = result['attributes'] except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: module.fail_json(msg=to_native(e)) @@ -527,9 +544,10 @@ def main(): reuse_existing_password = update_password == 'on_new_username' result = user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, - priv, tls_requires, module.check_mode, reuse_existing_password) + priv, attributes, tls_requires, reuse_existing_password, module) changed = result['changed'] password_changed = result['password_changed'] + final_attributes = result['attributes'] if changed: msg = "User added" @@ -546,7 +564,7 @@ def main(): else: changed = False msg = "User doesn't exist" - module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed) + module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed, attributes=final_attributes) if __name__ == '__main__': From 7012a277de943c195e731729c8a4b09900926b96 Mon Sep 17 00:00:00 2001 From: n-cc Date: Wed, 10 Jan 2024 15:56:01 -0600 Subject: [PATCH 2/6] fix CI --- plugins/module_utils/user.py | 10 +++++++--- plugins/modules/mysql_role.py | 2 +- plugins/modules/mysql_user.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index b2de38df..87f2572e 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -155,10 +155,10 @@ def user_add(cursor, user, host, host_all, password, encrypted, attributes, tls_requires, reuse_existing_password, module): # we cannot create users without a proper hostname if host_all: - return {'changed': False, 'password_changed': False} + return {'changed': False, 'password_changed': False, 'attributes': {}} if module.check_mode: - return {'changed': True, 'password_changed': None} + return {'changed': True, 'password_changed': None, 'attributes': {}} # If attributes are set, perform a sanity check to ensure server supports user attributes before creating user if attributes and not get_attribute_support(cursor): @@ -417,6 +417,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted, privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role) if len(grant_privs) > 0: privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role) + else: + changed = True # after privilege manipulation, compare privileges from before and now after_priv = privileges_get(cursor, user, host, maria_role) @@ -969,11 +971,12 @@ def get_attribute_support(cursor): # information_schema.tables does not hold the tables within information_schema itself cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES LIMIT 0") cursor.fetchone() - except mysql_driver.OperationalError: + except mysql_driver.Error: return False return True + def attributes_get(cursor, user, host): """Get attributes for a given user. @@ -999,6 +1002,7 @@ def attributes_get(cursor, user, host): return None + def get_impl(cursor): global impl cursor.execute("SELECT VERSION()") diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py index e8920933..57137913 100644 --- a/plugins/modules/mysql_role.py +++ b/plugins/modules/mysql_role.py @@ -931,7 +931,7 @@ def update(self, users, privs, check_mode=False, if privs: result = user_mod(self.cursor, self.name, self.host, None, None, None, None, None, None, - privs, append_privs, subtract_privs, None, + privs, append_privs, subtract_privs, None, None, self.module, role=True, maria_role=self.is_mariadb) changed = result['changed'] diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 1d4aad3a..bf83b3c3 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -166,7 +166,7 @@ version_added: '3.8.0' attributes: description: - - Create, update, or delete user attributes (arbitrary "key: value" comments) for the user. + - "Create, update, or delete user attributes (arbitrary 'key: value' comments) for the user." - MySQL server must support the INFORMATION_SCHEMA.USER_ATTRIBUTES table. Provided since MySQL 8.0. - To delete an existing attribute, set its value to False. type: dict From 9a8b53dd5a4b6dde4b9fa27dc08ff34107e1a5e5 Mon Sep 17 00:00:00 2001 From: n-cc Date: Thu, 11 Jan 2024 12:54:18 -0600 Subject: [PATCH 3/6] write integration tests --- plugins/module_utils/user.py | 39 +-- .../targets/test_mysql_user/tasks/main.yml | 3 + .../tasks/test_user_attributes.yml | 244 ++++++++++++++++++ 3 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 87f2572e..8bd25336 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -153,18 +153,16 @@ def get_existing_authentication(cursor, user, host): def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, new_priv, attributes, tls_requires, reuse_existing_password, module): - # we cannot create users without a proper hostname - if host_all: - return {'changed': False, 'password_changed': False, 'attributes': {}} - - if module.check_mode: - return {'changed': True, 'password_changed': None, 'attributes': {}} - # If attributes are set, perform a sanity check to ensure server supports user attributes before creating user if attributes and not get_attribute_support(cursor): module.fail_json(msg="user attributes were specified but the mysql server does not support user attributes") - final_attributes = {} + # we cannot create users without a proper hostname + if host_all: + return {'changed': False, 'password_changed': False, 'attributes': attributes} + + if module.check_mode: + return {'changed': True, 'password_changed': None, 'attributes': attributes} # Determine what user management method server uses old_user_mgmt = impl.use_old_user_mgmt(cursor) @@ -210,12 +208,15 @@ def user_add(cursor, user, host, host_all, password, encrypted, 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 attributes is not None: - cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes))) - final_attributes = attributes_get(cursor, user, host) if tls_requires is not None: privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires) + final_attributes = {} + + if attributes: + cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes))) + final_attributes = attributes_get(cursor, user, host) + return {'changed': True, 'password_changed': not used_existing_password, 'attributes': final_attributes} @@ -426,6 +427,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, # Handle attributes attribute_support = get_attribute_support(cursor) + final_attributes = {} if attributes: if not attribute_support: @@ -441,14 +443,21 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if attributes_to_change: msg = "Attributes updated: %s" % (", ".join(["%s: %s" % (key, value) for key, value in attributes_to_change.items()])) + + # Calculate final attributes by re-running attributes_get when not in check mode, and merge dictionaries when in check mode if not module.check_mode: cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes_to_change))) + final_attributes = attributes_get(cursor, user, host) + else: + # Final if statements excludes items whose values are None in attributes_to_change, i.e. attributes that will be deleted + final_attributes = {k: v for d in (current_attributes, attributes_to_change) for k, v in d.items() if k not in attributes_to_change or + attributes_to_change[k]} changed = True - - if attribute_support: - final_attributes = attributes_get(cursor, user, host) + else: + final_attributes = current_attributes else: - final_attributes = {} + if attribute_support: + final_attributes = attributes_get(cursor, user, host) if role: continue diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index f4247e40..f5e07484 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -267,6 +267,9 @@ tags: - issue_465 + # Tests for user attributes + - include_tasks: test_user_attributes.yml + # Tests for the TLS requires dictionary - include_tasks: test_tls_requirements.yml diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml new file mode 100644 index 00000000..ec4a8f79 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml @@ -0,0 +1,244 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - when: db_engine == 'mariadb' + block: + + - name: Attributes | Attempt to create user with attributes with mariadb in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + attributes: + key1: "value1" + ignore_errors: yes + register: result + check_mode: yes + + - name: Attributes | Assert that creating user with attributes fails with mariadb + assert: + that: + - result is failed + + - name: Attributes | Attempt to create user with attributes with mariadb + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + attributes: + key1: "value1" + ignore_errors: yes + register: result + + - name: Attributes | Assert that creating user with attributes fails with mariadb + assert: + that: + - result is failed + + - when: db_engine == 'mysql' + block: + + # Create user with attributes + - name: Attributes | Test creating a user with attributes in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + attributes: + key1: "value1" + register: result + check_mode: yes + + - name: Attributes | Assert that user would have been created with attributes + assert: + that: + - result is changed + - result.attributes.key1 == "value1" + + - name: Attributes | Test creating a user with attributes + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + attributes: + key1: "value1" + register: result + + - name: Attributes | Assert that user was created with attributes + assert: + that: + - result is changed + - result.attributes.key1 == "value1" + + # Append attributes on an existing user + - name: Attributes | Test appending attributes to an existing user in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: "value2" + register: result + check_mode: yes + + - name: Attributes | Assert that attribute would have been appended and existing attribute stays + assert: + that: + - result is changed + - result.attributes.key1 == "value1" + - result.attributes.key2 == "value2" + + - name: Attributes | Test appending attributes to an existing user + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: "value2" + register: result + + - name: Attributes | Assert that new attribute was appended and existing attribute stays + assert: + that: + - result is changed + - result.attributes.key1 == "value1" + - result.attributes.key2 == "value2" + + # Test updating existing attributes + - name: Attributes | Test updating attributes on an existing user in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: "new_value2" + check_mode: yes + register: result + + - name: Attributes | Assert that attribute would have been updated + assert: + that: + - result is changed + - result.attributes.key2 == "new_value2" + + - name: Attributes | Test updating attributes on an existing user + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: "new_value2" + register: result + + - name: Attributes | Assert that attribute was updated + assert: + that: + - result is changed + - result.attributes.key2 == "new_value2" + + # Test deleting attributes + - name: Attributes | Test deleting attributes on an existing user in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: False + register: result + check_mode: yes + + - name: Attributes | Assert that attribute would have been deleted + assert: + that: + - result is changed + - "'key2' not in result.attributes" + + - name: Attributes | Test deleting attributes on an existing user + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: False + register: result + + - name: Attributes | Assert that attribute was deleted + assert: + that: + - result is changed + - "'key2' not in result.attributes" + + # Test attribute idempotency + - name: Attributes | Test attribute idempotency by trying to change an already correct attribute in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key1: "value1" + register: result + check_mode: yes + + - name: Attributes | Assert that attribute would not have been updated + assert: + that: + - result is not changed + - result.attributes.key1 == "value1" + + - name: Attributes | Test attribute idempotency by trying to change an already correct attribute + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key1: "value1" + register: result + + - name: Attributes | Assert that attribute was not updated + assert: + that: + - result is not changed + - result.attributes.key1 == "value1" + + - name: Attributes | Test attribute idempotency by not specifying attribute parameter in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + register: result + check_mode: yes + + - name: Attributes | Assert that attribute is returned in check mode + assert: + that: + - result is not changed + - result.attributes.key1 == "value1" + + - name: Attributes | Test attribute idempotency by not specifying attribute parameter + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + register: result + + - name: Attributes | Assert that attribute is returned + assert: + that: + - result is not changed + - result.attributes.key1 == "value1" + + # Cleanup + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" From c35bfb983b9c64c98d717c2dd8385e19ad83f154 Mon Sep 17 00:00:00 2001 From: n-cc Date: Fri, 12 Jan 2024 15:35:42 -0600 Subject: [PATCH 4/6] requested changes pt. 1 --- CHANGELOG.rst | 12 + plugins/module_utils/user.py | 9 +- plugins/modules/mysql_user.py | 4 +- .../tasks/test_user_attributes.yml | 231 ++++++++++++++---- 4 files changed, 203 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f6c6cb8d..e61ffdc5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,18 @@ Community MySQL Collection Release Notes This changelog describes changes after version 2.0.0. +v3.9.0 +====== + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Minor Changes +------------- + +- mysql_user - add user attribute support via the ``attributes`` parameter and return value (https://github.com/ansible-collections/community.mysql/pull/604). + v3.8.0 ====== diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 8bd25336..59a3ef4d 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -155,7 +155,7 @@ def user_add(cursor, user, host, host_all, password, encrypted, attributes, tls_requires, reuse_existing_password, module): # If attributes are set, perform a sanity check to ensure server supports user attributes before creating user if attributes and not get_attribute_support(cursor): - module.fail_json(msg="user attributes were specified but the mysql server does not support user attributes") + module.fail_json(msg="user attributes were specified but the server does not support user attributes") # we cannot create users without a proper hostname if host_all: @@ -431,15 +431,14 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if attributes: if not attribute_support: - module.fail_json(msg="user attributes were specified but the mysql server does not support user attributes") + module.fail_json(msg="user attributes were specified but the server does not support user attributes") else: current_attributes = attributes_get(cursor, user, host) attributes_to_change = {} for key, value in attributes.items(): if key not in current_attributes or current_attributes[key] != value: - # The mysql null value (None in python) is used to delete an attribute; we use False to represent None in the attributes parameter - attributes_to_change[key] = None if not value else value + attributes_to_change[key] = value if attributes_to_change: msg = "Attributes updated: %s" % (", ".join(["%s: %s" % (key, value) for key, value in attributes_to_change.items()])) @@ -451,7 +450,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, else: # Final if statements excludes items whose values are None in attributes_to_change, i.e. attributes that will be deleted final_attributes = {k: v for d in (current_attributes, attributes_to_change) for k, v in d.items() if k not in attributes_to_change or - attributes_to_change[k]} + attributes_to_change[k] is not None} changed = True else: final_attributes = current_attributes diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index bf83b3c3..b9cbc8ba 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -168,7 +168,7 @@ description: - "Create, update, or delete user attributes (arbitrary 'key: value' comments) for the user." - MySQL server must support the INFORMATION_SCHEMA.USER_ATTRIBUTES table. Provided since MySQL 8.0. - - To delete an existing attribute, set its value to False. + - To delete an existing attribute, set its value to null. type: dict version_added: '3.9.0' @@ -268,7 +268,7 @@ name: bob attributes: foo: "foo" - bar: False + bar: null - name: Modify user to require TLS connection with a valid client certificate community.mysql.mysql_user: diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml index ec4a8f79..1932a621 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml @@ -11,6 +11,11 @@ - when: db_engine == 'mariadb' block: + # ============================================================ + # Fail creating a user with mariadb + # + + # Check mode - name: Attributes | Attempt to create user with attributes with mariadb in check mode mysql_user: <<: *mysql_params @@ -20,14 +25,23 @@ attributes: key1: "value1" ignore_errors: yes - register: result + register: result_module check_mode: yes - - name: Attributes | Assert that creating user with attributes fails with mariadb + - name: Attributes | Run query to verify user creation with attributes fails with mariadb in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT user FROM mysql.user WHERE user = "{{ user_name_2 }}" AND host = "%"' + ignore_errors: yes + register: result_query + + - name: Attributes | Assert that creating user with attributes fails with mariadb in check mode assert: that: - - result is failed + - result_module is failed + - not result_query.query_result[0] + # Real mode - name: Attributes | Attempt to create user with attributes with mariadb mysql_user: <<: *mysql_params @@ -37,17 +51,28 @@ attributes: key1: "value1" ignore_errors: yes - register: result + register: result_module + + - name: Attributes | Run query to verify user creation with attributes fails with mariadb + mysql_query: + <<: *mysql_params + query: 'SELECT user FROM mysql.user WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query - name: Attributes | Assert that creating user with attributes fails with mariadb assert: that: - - result is failed + - result_module is failed + - not result_query.query_result[0] - when: db_engine == 'mysql' block: + # ============================================================ # Create user with attributes + # + + # Check mode - name: Attributes | Test creating a user with attributes in check mode mysql_user: <<: *mysql_params @@ -56,15 +81,23 @@ password: '{{ user_password_2 }}' attributes: key1: "value1" - register: result + register: result_module check_mode: yes + - name: Attributes | Run query to verify user creation did not take place in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT user FROM mysql.user WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + - name: Attributes | Assert that user would have been created with attributes assert: that: - - result is changed - - result.attributes.key1 == "value1" + - result_module is changed + - result_module.attributes.key1 == "value1" + - not result_query.query_result[0] + # Real mode - name: Attributes | Test creating a user with attributes mysql_user: <<: *mysql_params @@ -73,15 +106,26 @@ password: '{{ user_password_2 }}' attributes: key1: "value1" - register: result + register: result_module + + - name: Attributes | Run query to verify created user attributes + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query - name: Attributes | Assert that user was created with attributes assert: that: - - result is changed - - result.attributes.key1 == "value1" + - result_module is changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + # ============================================================ # Append attributes on an existing user + # + + # Check mode - name: Attributes | Test appending attributes to an existing user in check mode mysql_user: <<: *mysql_params @@ -89,16 +133,24 @@ host: '%' attributes: key2: "value2" - register: result + register: result_module check_mode: yes + - name: Attributes | Run query to check appended attributes in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + - name: Attributes | Assert that attribute would have been appended and existing attribute stays assert: that: - - result is changed - - result.attributes.key1 == "value1" - - result.attributes.key2 == "value2" + - result_module is changed + - result_module.attributes.key1 == "value1" + - result_module.attributes.key2 == "value2" + - "'key2' not in result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml" + # Real mode - name: Attributes | Test appending attributes to an existing user mysql_user: <<: *mysql_params @@ -106,16 +158,28 @@ host: '%' attributes: key2: "value2" - register: result + register: result_module + + - name: Attributes | Run query to check appended attributes + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query - name: Attributes | Assert that new attribute was appended and existing attribute stays assert: that: - - result is changed - - result.attributes.key1 == "value1" - - result.attributes.key2 == "value2" + - result_module is changed + - result_module.attributes.key1 == "value1" + - result_module.attributes.key2 == "value2" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "value2" + # ============================================================ # Test updating existing attributes + # + + # Check mode - name: Attributes | Test updating attributes on an existing user in check mode mysql_user: <<: *mysql_params @@ -124,14 +188,22 @@ attributes: key2: "new_value2" check_mode: yes - register: result + register: result_module + + - name: Attributes | Run query to verify updated attribute in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query - name: Attributes | Assert that attribute would have been updated assert: that: - - result is changed - - result.attributes.key2 == "new_value2" + - result_module is changed + - result_module.attributes.key2 == "new_value2" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "value2" + # Real mode - name: Attributes | Test updating attributes on an existing user mysql_user: <<: *mysql_params @@ -139,47 +211,77 @@ host: '%' attributes: key2: "new_value2" - register: result + register: result_module + + - name: Attributes | Run query to verify updated attribute + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query - name: Attributes | Assert that attribute was updated assert: that: - - result is changed - - result.attributes.key2 == "new_value2" + - result_module is changed + - result_module.attributes.key2 == "new_value2" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "new_value2" + # ============================================================ # Test deleting attributes + # + + # Check mode - name: Attributes | Test deleting attributes on an existing user in check mode mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' attributes: - key2: False - register: result + key2: null + register: result_module check_mode: yes + - name: Attributes | Run query to verify deleted attribute in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + - name: Attributes | Assert that attribute would have been deleted assert: that: - - result is changed - - "'key2' not in result.attributes" + - result_module is changed + - "'key2' not in result_module.attributes" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "new_value2" + # Real mode - name: Attributes | Test deleting attributes on an existing user mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' attributes: - key2: False - register: result + key2: null + register: result_module + + - name: Attributes | Run query to verify deleted attribute + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query - name: Attributes | Assert that attribute was deleted assert: that: - - result is changed - - "'key2' not in result.attributes" + - result_module is changed + - "'key2' not in result_module.attributes" + - "'key2' not in result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml" + + # ============================================================ + # Test attribute idempotency when specifying attributes + # - # Test attribute idempotency + # Check mode - name: Attributes | Test attribute idempotency by trying to change an already correct attribute in check mode mysql_user: <<: *mysql_params @@ -187,15 +289,23 @@ host: '%' attributes: key1: "value1" - register: result + register: result_module check_mode: yes + - name: Attributes | Run query to verify idempotency of already correct attribute in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + - name: Attributes | Assert that attribute would not have been updated assert: that: - - result is not changed - - result.attributes.key1 == "value1" + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + # Real mode - name: Attributes | Test attribute idempotency by trying to change an already correct attribute mysql_user: <<: *mysql_params @@ -203,42 +313,71 @@ host: '%' attributes: key1: "value1" - register: result + register: result_module + + - name: Attributes | Run query to verify idempotency of already correct attribute + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query - name: Attributes | Assert that attribute was not updated assert: that: - - result is not changed - - result.attributes.key1 == "value1" + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + + # ============================================================ + # Test attribute idempotency when not specifying attribute parameter + # + # Check mode - name: Attributes | Test attribute idempotency by not specifying attribute parameter in check mode mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' - register: result + register: result_module check_mode: yes + - name: Attributes | Run query to verify idempotency when not specifying attribute parameter in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + - name: Attributes | Assert that attribute is returned in check mode assert: that: - - result is not changed - - result.attributes.key1 == "value1" + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + # Real mode - name: Attributes | Test attribute idempotency by not specifying attribute parameter mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' - register: result + register: result_module + + - name: Attributes | Run query to verify idempotency when not specifying attribute parameter + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query - name: Attributes | Assert that attribute is returned assert: that: - - result is not changed - - result.attributes.key1 == "value1" + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + # ============================================================ # Cleanup + # - include_tasks: utils/remove_user.yml vars: user_name: "{{ user_name_2 }}" From 55e896e252ecdffb7ab229dbc3273d0155a04996 Mon Sep 17 00:00:00 2001 From: n-cc Date: Wed, 17 Jan 2024 10:07:06 -0600 Subject: [PATCH 5/6] requested changes pt. 2 --- plugins/module_utils/user.py | 25 +-- plugins/modules/mysql_user.py | 2 +- .../tasks/test_user_attributes.yml | 169 ++++++++++++++---- 3 files changed, 144 insertions(+), 52 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 59a3ef4d..1e5a275b 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -211,7 +211,7 @@ def user_add(cursor, user, host, host_all, password, encrypted, if tls_requires is not None: privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires) - final_attributes = {} + final_attributes = None if attributes: cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes))) @@ -434,6 +434,10 @@ def user_mod(cursor, user, host, host_all, password, encrypted, module.fail_json(msg="user attributes were specified but the server does not support user attributes") else: current_attributes = attributes_get(cursor, user, host) + + if current_attributes is None: + current_attributes = {} + attributes_to_change = {} for key, value in attributes.items(): @@ -451,6 +455,9 @@ def user_mod(cursor, user, host, host_all, password, encrypted, # Final if statements excludes items whose values are None in attributes_to_change, i.e. attributes that will be deleted final_attributes = {k: v for d in (current_attributes, attributes_to_change) for k, v in d.items() if k not in attributes_to_change or attributes_to_change[k] is not None} + + # Convert empty dict to None per return value requirements + final_attributes = final_attributes if final_attributes else None changed = True else: final_attributes = current_attributes @@ -974,7 +981,6 @@ def get_attribute_support(cursor): Returns: True if attributes are supported, False if they are not. """ - try: # information_schema.tables does not hold the tables within information_schema itself cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES LIMIT 0") @@ -994,21 +1000,16 @@ def attributes_get(cursor, user, host): host (str): User host name. Returns: - None if the user does not exist, otherwise a dict of attributes set on the user + None if the user does not exist or the user has no attributes set, otherwise a dict of attributes set on the user """ cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = %s AND host = %s", (user, host)) r = cursor.fetchone() + # convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format + j = json.loads(r[0]) if r and r[0] else None - if r: - attributes = r[0] - # convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format - if attributes: - return json.loads(attributes) - else: - return {} - - return None + # if the attributes dict is empty, return None instead + return j if j else None def get_impl(cursor): diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index b9cbc8ba..c6a02fc1 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -515,7 +515,7 @@ def main(): priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs) password_changed = False - final_attributes = {} + final_attributes = None if state == "present": if user_exists(cursor, user, host, host_all): try: diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml index 1932a621..b5cec100 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml @@ -68,6 +68,60 @@ - when: db_engine == 'mysql' block: + # ============================================================ + # Create user with no attributes (test attributes return type) + # + + # Check mode + - name: Attributes | Test creating a user with no attributes in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + register: result_module + check_mode: yes + + - name: Attributes | Run query to verify user creation with no attributes did not take place in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT user FROM mysql.user WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that user would have been created without attributes + assert: + that: + - result_module is changed + - result_module.attributes is none + - not result_query.query_result[0] + + # Real mode + - name: Attributes | Test creating a user with no attributes + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + register: result_module + + - name: Attributes | Run query to verify created user without attributes + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that user was created without attributes + assert: + that: + - result_module is changed + - result_module.attributes is none + - result_query.query_result[0][0]['ATTRIBUTE'] is none + + # Clean up user to allow it to be recreated with attributes + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" + # ============================================================ # Create user with attributes # @@ -227,78 +281,76 @@ - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "new_value2" # ============================================================ - # Test deleting attributes + # Test attribute idempotency when specifying attributes # # Check mode - - name: Attributes | Test deleting attributes on an existing user in check mode + - name: Attributes | Test attribute idempotency by trying to change an already correct attribute in check mode mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' attributes: - key2: null + key1: "value1" register: result_module check_mode: yes - - name: Attributes | Run query to verify deleted attribute in check mode + - name: Attributes | Run query to verify idempotency of already correct attribute in check mode mysql_query: <<: *mysql_params query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' register: result_query - - name: Attributes | Assert that attribute would have been deleted + - name: Attributes | Assert that attribute would not have been updated assert: that: - - result_module is changed - - "'key2' not in result_module.attributes" - - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "new_value2" + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" # Real mode - - name: Attributes | Test deleting attributes on an existing user + - name: Attributes | Test attribute idempotency by trying to change an already correct attribute mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' attributes: - key2: null + key1: "value1" register: result_module - - name: Attributes | Run query to verify deleted attribute + - name: Attributes | Run query to verify idempotency of already correct attribute mysql_query: <<: *mysql_params query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' register: result_query - - name: Attributes | Assert that attribute was deleted + - name: Attributes | Assert that attribute was not updated assert: that: - - result_module is changed - - "'key2' not in result_module.attributes" - - "'key2' not in result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml" + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" # ============================================================ - # Test attribute idempotency when specifying attributes + # Test attribute idempotency when not specifying attribute parameter # # Check mode - - name: Attributes | Test attribute idempotency by trying to change an already correct attribute in check mode + - name: Attributes | Test attribute idempotency by not specifying attribute parameter in check mode mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' - attributes: - key1: "value1" register: result_module check_mode: yes - - name: Attributes | Run query to verify idempotency of already correct attribute in check mode + - name: Attributes | Run query to verify idempotency when not specifying attribute parameter in check mode mysql_query: <<: *mysql_params query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' register: result_query - - name: Attributes | Assert that attribute would not have been updated + - name: Attributes | Assert that attribute is returned in check mode assert: that: - result_module is not changed @@ -306,22 +358,20 @@ - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" # Real mode - - name: Attributes | Test attribute idempotency by trying to change an already correct attribute + - name: Attributes | Test attribute idempotency by not specifying attribute parameter mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' - attributes: - key1: "value1" register: result_module - - name: Attributes | Run query to verify idempotency of already correct attribute + - name: Attributes | Run query to verify idempotency when not specifying attribute parameter mysql_query: <<: *mysql_params query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' register: result_query - - name: Attributes | Assert that attribute was not updated + - name: Attributes | Assert that attribute is returned assert: that: - result_module is not changed @@ -329,51 +379,92 @@ - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" # ============================================================ - # Test attribute idempotency when not specifying attribute parameter + # Test deleting attributes # # Check mode - - name: Attributes | Test attribute idempotency by not specifying attribute parameter in check mode + - name: Attributes | Test deleting attributes on an existing user in check mode mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' + attributes: + key2: null register: result_module check_mode: yes - - name: Attributes | Run query to verify idempotency when not specifying attribute parameter in check mode + - name: Attributes | Run query to verify deleted attribute in check mode mysql_query: <<: *mysql_params query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' register: result_query - - name: Attributes | Assert that attribute is returned in check mode + - name: Attributes | Assert that attribute would have been deleted assert: that: - - result_module is not changed - - result_module.attributes.key1 == "value1" - - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + - result_module is changed + - "'key2' not in result_module.attributes" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "new_value2" # Real mode - - name: Attributes | Test attribute idempotency by not specifying attribute parameter + - name: Attributes | Test deleting attributes on an existing user mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' host: '%' + attributes: + key2: null register: result_module - - name: Attributes | Run query to verify idempotency when not specifying attribute parameter + - name: Attributes | Run query to verify deleted attribute mysql_query: <<: *mysql_params query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' register: result_query - - name: Attributes | Assert that attribute is returned + - name: Attributes | Assert that attribute was deleted assert: that: - - result_module is not changed - - result_module.attributes.key1 == "value1" - - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + - result_module is changed + - "'key2' not in result_module.attributes" + - "'key2' not in result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml" + + # ============================================================ + # Test attribute return value when no attributes exist + # + + # Check mode + - name: Attributes | Test attributes return value when no attributes exist in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key1: null + register: result_module + check_mode: yes + + - name: Attributes | Assert attributes return value when no attributes exist in check mode + assert: + that: + - result_module is changed + - result_module.attributes is none + + # Real mode + - name: Attributes | Test attributes return value when no attributes exist + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key1: null + register: result_module + + - name: Attributes | Assert attributes return value when no attributes exist + assert: + that: + - result_module is changed + - result_module.attributes is none # ============================================================ # Cleanup From 208719f625e328c68275decfc2c6c2fa28030767 Mon Sep 17 00:00:00 2001 From: n-cc Date: Thu, 18 Jan 2024 13:50:20 -0600 Subject: [PATCH 6/6] fix changelog fragment --- CHANGELOG.rst | 12 ------------ changelogs/fragments/604-user-attributes.yaml | 2 ++ 2 files changed, 2 insertions(+), 12 deletions(-) create mode 100644 changelogs/fragments/604-user-attributes.yaml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e61ffdc5..f6c6cb8d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,18 +6,6 @@ Community MySQL Collection Release Notes This changelog describes changes after version 2.0.0. -v3.9.0 -====== - -This is the minor release of the ``community.mysql`` collection. -This changelog contains all changes to the modules and plugins in this -collection that have been made after the previous release. - -Minor Changes -------------- - -- mysql_user - add user attribute support via the ``attributes`` parameter and return value (https://github.com/ansible-collections/community.mysql/pull/604). - v3.8.0 ====== diff --git a/changelogs/fragments/604-user-attributes.yaml b/changelogs/fragments/604-user-attributes.yaml new file mode 100644 index 00000000..260201d1 --- /dev/null +++ b/changelogs/fragments/604-user-attributes.yaml @@ -0,0 +1,2 @@ +minor_changes: + - "mysql_user - add user attribute support via the ``attributes`` parameter and return value (https://github.com/ansible-collections/community.mysql/pull/604)."