From 1380c20b20cd1d16cebc8e0844efa96e86d35761 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Mon, 3 Mar 2025 16:09:15 +0200 Subject: [PATCH 01/33] function to check if a user is locked already Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 307ef6e6..44ac96f0 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -52,6 +52,22 @@ def user_exists(cursor, user, host, host_all): return count[0] > 0 +def user_is_locked(cursor, user, host, host_all): + if host_all: + cursor.execute("SHOW CREATE USER %s", (user,)) + else: + cursor.execute("SHOW CREATE USER %s@%s", (user, host)) + + # Unless I am very much mistaken there should only be 1 answer to this query ever. + result = cursor.fetchone() + + for res in result.values(): + if res.endswith('ACCOUNT LOCK'): + return True + + return False + + def sanitize_requires(tls_requires): sanitized_requires = {} if tls_requires: From 6747ef32e94f8fbad0944a7d95c2817d77269103 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Mon, 3 Mar 2025 16:36:04 +0200 Subject: [PATCH 02/33] Add the location and logic of where I think user locking would happen. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 44ac96f0..050e766a 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -266,6 +266,9 @@ def user_add(cursor, user, host, host_all, password, encrypted, cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes))) final_attributes = attributes_get(cursor, user, host) +# if locked: +# cursor.execute("ALTER USER %s@%s ACCOUNT LOCK") + return {'changed': True, 'password_changed': not used_existing_password, 'attributes': final_attributes} @@ -575,6 +578,13 @@ def user_mod(cursor, user, host, host_all, password, encrypted, cursor.execute(*query_with_args) changed = True +# if user_is_locked(cursor, user, host, False) != locked: +# if locked: +# cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) +# else: +# cursor.execute("ALTER USER %s@%s ACCOUNT UNLOCK", (user, host)) +# changed = True + return {'changed': changed, 'msg': msg, 'password_changed': password_changed, 'attributes': final_attributes} From 5079dc4ab7aadf869a98717969e0b988cb978084 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Mon, 3 Mar 2025 16:39:26 +0200 Subject: [PATCH 03/33] Fix missing parameters for execute() Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 050e766a..665e1136 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -267,7 +267,7 @@ def user_add(cursor, user, host, host_all, password, encrypted, final_attributes = attributes_get(cursor, user, host) # if locked: -# cursor.execute("ALTER USER %s@%s ACCOUNT LOCK") +# cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) return {'changed': True, 'password_changed': not used_existing_password, 'attributes': final_attributes} From 583407fbe2f9773665381cefc5c6c4610dcd16ea Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 4 Mar 2025 00:25:20 +0200 Subject: [PATCH 04/33] Add the locked attribute Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 29 +++++++++++++++-------------- plugins/modules/mysql_user.py | 8 +++++--- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 665e1136..ac18d8f4 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -61,9 +61,8 @@ def user_is_locked(cursor, user, host, host_all): # Unless I am very much mistaken there should only be 1 answer to this query ever. result = cursor.fetchone() - for res in result.values(): - if res.endswith('ACCOUNT LOCK'): - return True + if result[0].endswith('ACCOUNT LOCK'): + return True return False @@ -176,7 +175,7 @@ def get_existing_authentication(cursor, user, host=None): def user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, salt, new_priv, attributes, tls_requires, reuse_existing_password, module, - password_expire, password_expire_interval): + password_expire, password_expire_interval, locked=False): # 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 server does not support user attributes") @@ -266,8 +265,8 @@ def user_add(cursor, user, host, host_all, password, encrypted, cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes))) final_attributes = attributes_get(cursor, user, host) -# if locked: -# cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) + if locked: + cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) return {'changed': True, 'password_changed': not used_existing_password, 'attributes': final_attributes} @@ -283,7 +282,7 @@ def is_hash(password): def user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, salt, new_priv, append_privs, subtract_privs, attributes, tls_requires, module, - password_expire, password_expire_interval, role=False, maria_role=False): + password_expire, password_expire_interval, locked=False, role=False, maria_role=False): changed = False msg = "User unchanged" grant_option = False @@ -555,6 +554,15 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if attribute_support: final_attributes = attributes_get(cursor, user, host) + if user_is_locked(cursor, user, host, False) != locked: + if locked: + cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) + msg = 'User locked' + else: + cursor.execute("ALTER USER %s@%s ACCOUNT UNLOCK", (user, host)) + msg = 'User unlocked' + changed = True + if role: continue @@ -578,13 +586,6 @@ def user_mod(cursor, user, host, host_all, password, encrypted, cursor.execute(*query_with_args) changed = True -# if user_is_locked(cursor, user, host, False) != locked: -# if locked: -# cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) -# else: -# cursor.execute("ALTER USER %s@%s ACCOUNT UNLOCK", (user, host)) -# changed = True - return {'changed': changed, 'msg': msg, 'password_changed': password_changed, 'attributes': final_attributes} diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 499f2a04..17d83151 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -470,6 +470,7 @@ def main(): column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True password_expire=dict(type='str', choices=['now', 'never', 'default', 'interval'], no_log=True), password_expire_interval=dict(type='int', required_if=[('password_expire', 'interval', True)], no_log=True), + locked=dict(type='bool', default='no'), ) module = AnsibleModule( argument_spec=argument_spec, @@ -510,6 +511,7 @@ def main(): column_case_sensitive = module.params["column_case_sensitive"] password_expire = module.params["password_expire"] password_expire_interval = module.params["password_expire_interval"] + locked = module.boolean(module.params['locked']) if priv and not isinstance(priv, (str, dict)): module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv)) @@ -577,13 +579,13 @@ def main(): result = user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, salt, priv, append_privs, subtract_privs, attributes, tls_requires, module, - password_expire, password_expire_interval) + password_expire, password_expire_interval, locked) else: result = user_mod(cursor, user, host, host_all, None, encrypted, None, None, None, None, priv, append_privs, subtract_privs, attributes, tls_requires, module, - password_expire, password_expire_interval) + password_expire, password_expire_interval, locked) changed = result['changed'] msg = result['msg'] password_changed = result['password_changed'] @@ -601,7 +603,7 @@ def main(): result = user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, salt, priv, attributes, tls_requires, reuse_existing_password, module, - password_expire, password_expire_interval) + password_expire, password_expire_interval, locked) changed = result['changed'] password_changed = result['password_changed'] final_attributes = result['attributes'] From 0f590dc48ed13fe415281562db04b8a9aea0664f Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 5 Mar 2025 14:57:11 +0200 Subject: [PATCH 05/33] Initial user locking integration tests Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../targets/test_mysql_user/tasks/main.yml | 4 + .../tasks/test_user_locking.yml | 140 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index 9244570f..72128866 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -305,3 +305,7 @@ - name: Mysql_user - test update_password ansible.builtin.import_tasks: file: test_update_password.yml + + - name: Mysql_user - test user_locking + ansible.builtin.import_tasks: + file: test_user_locking.yml diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml new file mode 100644 index 00000000..0c18f02a --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -0,0 +1,140 @@ +--- + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # ========================= Prepare ======================================= + - name: Mysql_user Lock user | Create a test database + community.mysql.mysql_db: + <<: *mysql_params + name: mysql_lock_user_test + state: present + + # ========================== Tests ======================================== + + - name: Mysql_user Lock user | Create test user locked + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + host: '%' + password: 'msandbox' + locked: yes + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | Assert that test user is locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'%' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | Unlock test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + host: '%' + locked: no + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | Assert that test user is not locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'%' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | Remove test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + host: '%' + state: absent + + - name: Mysql_user Lock user | Create test user unlocked + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + host: '%' + password: 'msandbox' + locked: no + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | Assert that test user is not locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'%' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | Lock test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + host: '%' + locked: yes + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | Assert that test user is locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'%' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | Remove test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + host: '%' + state: absent + + - name: Mysql_user Lock user | Create test user default lock action + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + host: '%' + password: 'msandbox' + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | Assert that test user is not locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'%' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | Remove test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + host: '%' + state: absent + + # ========================= Teardown ====================================== + + - name: Mysql_user Lock user | Delete test database + community.mysql.mysql_db: + <<: *mysql_params + name: mysql_lock_user_test + state: absent From 7358c1e7a789e0eb5cb1eed303714d1bdea49ed9 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 5 Mar 2025 15:09:40 +0200 Subject: [PATCH 06/33] Add attribute documentation Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/modules/mysql_user.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 17d83151..0f1472a6 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -189,6 +189,14 @@ fields names in privileges. type: bool version_added: '3.8.0' + + locked: + description: + - Lock account to prevent connections using it, this is primarily used for creating a user that will act as a DEFINER on stored procedures. + - The default is C(false) + type: bool + version_added: 'TBA' + attributes: description: - "Create, update, or delete user attributes (arbitrary 'key: value' comments) for the user." From 19ff2d757a7f39115f47d2fc9501b24a8c883b4a Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 5 Mar 2025 15:23:35 +0200 Subject: [PATCH 07/33] More descriptive names in the integration tests Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../tasks/test_user_locking.yml | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index 0c18f02a..00ae7714 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -18,7 +18,7 @@ # ========================== Tests ======================================== - - name: Mysql_user Lock user | Create test user locked + - name: Mysql_user Lock user | create locked | Create test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user @@ -28,7 +28,7 @@ priv: 'mysql_lock_user_test.*': 'SELECT' - - name: Mysql_user Lock user | Assert that test user is locked + - name: Mysql_user Lock user | create locked | Assert that test user is locked community.mysql.mysql_query: <<: *mysql_params query: @@ -37,7 +37,7 @@ failed_when: - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') - - name: Mysql_user Lock user | Unlock test user + - name: Mysql_user Lock user | create locked | Unlock test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user @@ -46,7 +46,7 @@ priv: 'mysql_lock_user_test.*': 'SELECT' - - name: Mysql_user Lock user | Assert that test user is not locked + - name: Mysql_user Lock user | create locked | Assert that test user is not locked community.mysql.mysql_query: <<: *mysql_params query: @@ -55,14 +55,14 @@ failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') - - name: Mysql_user Lock user | Remove test user + - name: Mysql_user Lock user | create locked | Remove test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user host: '%' state: absent - - name: Mysql_user Lock user | Create test user unlocked + - name: Mysql_user Lock user | create unlocked | Create test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user @@ -72,7 +72,7 @@ priv: 'mysql_lock_user_test.*': 'SELECT' - - name: Mysql_user Lock user | Assert that test user is not locked + - name: Mysql_user Lock user | create unlocked | Assert that test user is not locked community.mysql.mysql_query: <<: *mysql_params query: @@ -81,7 +81,7 @@ failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') - - name: Mysql_user Lock user | Lock test user + - name: Mysql_user Lock user | create unlocked | Lock test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user @@ -90,7 +90,7 @@ priv: 'mysql_lock_user_test.*': 'SELECT' - - name: Mysql_user Lock user | Assert that test user is locked + - name: Mysql_user Lock user | create unlocked | Assert that test user is locked community.mysql.mysql_query: <<: *mysql_params query: @@ -99,14 +99,14 @@ failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') - - name: Mysql_user Lock user | Remove test user + - name: Mysql_user Lock user | create unlocked | Remove test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user host: '%' state: absent - - name: Mysql_user Lock user | Create test user default lock action + - name: Mysql_user Lock user | create default | Create test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user @@ -115,7 +115,7 @@ priv: 'mysql_lock_user_test.*': 'SELECT' - - name: Mysql_user Lock user | Assert that test user is not locked + - name: Mysql_user Lock user | create default | Assert that test user is not locked community.mysql.mysql_query: <<: *mysql_params query: @@ -124,7 +124,7 @@ failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') - - name: Mysql_user Lock user | Remove test user + - name: Mysql_user Lock user | create default | Remove test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user From 17e398f5a27932cf05a9befc50a94b3480b5c8d5 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 5 Mar 2025 15:25:15 +0200 Subject: [PATCH 08/33] Fixes for sanity checks Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/modules/mysql_user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 0f1472a6..eac72f27 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -193,9 +193,9 @@ locked: description: - Lock account to prevent connections using it, this is primarily used for creating a user that will act as a DEFINER on stored procedures. - - The default is C(false) + - The C(default) is C(false) type: bool - version_added: 'TBA' + version_added: '3.13.0' attributes: description: From d839871d1ee510bb9a25b9fb9ff60f8ead5d9619 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 5 Mar 2025 15:51:20 +0200 Subject: [PATCH 09/33] - Changes requested/suggested by @Andersson007 - Example usage - Changelog fragment Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- changelogs/fragments/702-user_locking.yaml | 2 ++ plugins/modules/mysql_user.py | 11 +++++++++-- .../test_mysql_user/tasks/test_user_locking.yml | 16 ++++------------ 3 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 changelogs/fragments/702-user_locking.yaml diff --git a/changelogs/fragments/702-user_locking.yaml b/changelogs/fragments/702-user_locking.yaml new file mode 100644 index 00000000..13787930 --- /dev/null +++ b/changelogs/fragments/702-user_locking.yaml @@ -0,0 +1,2 @@ +minor_changes: +- mysql_user - add ``locked`` option to lock/unlock users, this is mainly used to have users that will act as definers on stored procedures. diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index eac72f27..228f763b 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -193,7 +193,7 @@ locked: description: - Lock account to prevent connections using it, this is primarily used for creating a user that will act as a DEFINER on stored procedures. - - The C(default) is C(false) + default: false type: bool version_added: '3.13.0' @@ -408,6 +408,13 @@ priv: 'db1.*': DELETE +- name: Create locked user to act as a definer on procedures + community.mysql.mysql_user: + name: readonly_procedures_locked + locked: true + priv: + db1.*: SELECT + # Example .my.cnf file for setting the root password # [client] # user=root @@ -478,7 +485,7 @@ def main(): column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True password_expire=dict(type='str', choices=['now', 'never', 'default', 'interval'], no_log=True), password_expire_interval=dict(type='int', required_if=[('password_expire', 'interval', True)], no_log=True), - locked=dict(type='bool', default='no'), + locked=dict(type='bool', default='false'), ) module = AnsibleModule( argument_spec=argument_spec, diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index 00ae7714..5d77f4d6 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -22,9 +22,8 @@ community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user - host: '%' password: 'msandbox' - locked: yes + locked: true priv: 'mysql_lock_user_test.*': 'SELECT' @@ -41,8 +40,7 @@ community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user - host: '%' - locked: no + locked: false priv: 'mysql_lock_user_test.*': 'SELECT' @@ -59,16 +57,14 @@ community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user - host: '%' state: absent - name: Mysql_user Lock user | create unlocked | Create test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user - host: '%' password: 'msandbox' - locked: no + locked: false priv: 'mysql_lock_user_test.*': 'SELECT' @@ -85,8 +81,7 @@ community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user - host: '%' - locked: yes + locked: true priv: 'mysql_lock_user_test.*': 'SELECT' @@ -103,14 +98,12 @@ community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user - host: '%' state: absent - name: Mysql_user Lock user | create default | Create test user community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user - host: '%' password: 'msandbox' priv: 'mysql_lock_user_test.*': 'SELECT' @@ -128,7 +121,6 @@ community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user - host: '%' state: absent # ========================= Teardown ====================================== From fbeeff5ca7222521f57a5ad3993df0f0e47ccfde Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 5 Mar 2025 16:06:32 +0200 Subject: [PATCH 10/33] Fix user_is_locked and remove host_all option. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index ac18d8f4..0d112d9f 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -52,16 +52,14 @@ def user_exists(cursor, user, host, host_all): return count[0] > 0 -def user_is_locked(cursor, user, host, host_all): - if host_all: - cursor.execute("SHOW CREATE USER %s", (user,)) - else: - cursor.execute("SHOW CREATE USER %s@%s", (user, host)) +def user_is_locked(cursor, user, host): + cursor.execute("SHOW CREATE USER %s@%s", (user, host)) - # Unless I am very much mistaken there should only be 1 answer to this query ever. + # Per discussions on irc:libera.chat:#maria the query may return up to 2 rows but "ACCOUNT LOCK" should always be in the first row. result = cursor.fetchone() - if result[0].endswith('ACCOUNT LOCK'): + # ACCOUNT LOCK does not have to be the last option in the CREATE USER query. + if result[0].find('ACCOUNT LOCK') > 0: return True return False @@ -554,7 +552,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if attribute_support: final_attributes = attributes_get(cursor, user, host) - if user_is_locked(cursor, user, host, False) != locked: + if user_is_locked(cursor, user, host) != locked: if locked: cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) msg = 'User locked' From 9209a123c6fe35113e36201b57c9feeb1c5ebf6d Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 5 Mar 2025 17:57:36 +0200 Subject: [PATCH 11/33] Fix host of user (was % should have been localhost after deleting `host:` earlier) Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../test_mysql_user/tasks/test_user_locking.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index 5d77f4d6..6c3976fc 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -31,7 +31,7 @@ community.mysql.mysql_query: <<: *mysql_params query: - - SHOW CREATE USER 'mysql_locked_user'@'%' + - SHOW CREATE USER 'mysql_locked_user'@'localhost' register: locked_user_creation failed_when: - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') @@ -48,7 +48,7 @@ community.mysql.mysql_query: <<: *mysql_params query: - - SHOW CREATE USER 'mysql_locked_user'@'%' + - SHOW CREATE USER 'mysql_locked_user'@'localhost' register: locked_user_creation failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') @@ -72,7 +72,7 @@ community.mysql.mysql_query: <<: *mysql_params query: - - SHOW CREATE USER 'mysql_locked_user'@'%' + - SHOW CREATE USER 'mysql_locked_user'@'localhost' register: locked_user_creation failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') @@ -89,7 +89,7 @@ community.mysql.mysql_query: <<: *mysql_params query: - - SHOW CREATE USER 'mysql_locked_user'@'%' + - SHOW CREATE USER 'mysql_locked_user'@'localhost' register: locked_user_creation failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') @@ -112,7 +112,7 @@ community.mysql.mysql_query: <<: *mysql_params query: - - SHOW CREATE USER 'mysql_locked_user'@'%' + - SHOW CREATE USER 'mysql_locked_user'@'localhost' register: locked_user_creation failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') From c7097b9fcf69783e4767b446fca0ae145e6d995b Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 5 Mar 2025 19:05:50 +0200 Subject: [PATCH 12/33] Fix condition Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../targets/test_mysql_user/tasks/test_user_locking.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index 6c3976fc..6462e6bc 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -92,7 +92,7 @@ - SHOW CREATE USER 'mysql_locked_user'@'localhost' register: locked_user_creation failed_when: - - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') - name: Mysql_user Lock user | create unlocked | Remove test user community.mysql.mysql_user: From e95c3dab6bea2f5863191cc4f8e0f06aa5880379 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Thu, 6 Mar 2025 10:30:02 +0200 Subject: [PATCH 13/33] Switch locked to named instead of positional. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/modules/mysql_user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 228f763b..b0699ab3 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -594,13 +594,13 @@ def main(): result = user_mod(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, salt, priv, append_privs, subtract_privs, attributes, tls_requires, module, - password_expire, password_expire_interval, locked) + password_expire, password_expire_interval, locked=locked) else: result = user_mod(cursor, user, host, host_all, None, encrypted, None, None, None, None, priv, append_privs, subtract_privs, attributes, tls_requires, module, - password_expire, password_expire_interval, locked) + password_expire, password_expire_interval, locked=locked) changed = result['changed'] msg = result['msg'] password_changed = result['password_changed'] @@ -618,7 +618,7 @@ def main(): result = user_add(cursor, user, host, host_all, password, encrypted, plugin, plugin_hash_string, plugin_auth_string, salt, priv, attributes, tls_requires, reuse_existing_password, module, - password_expire, password_expire_interval, locked) + password_expire, password_expire_interval, locked=locked) changed = result['changed'] password_changed = result['password_changed'] final_attributes = result['attributes'] From 3984380acb4f3a2aeab9f54d10b281a1d057bbd2 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Thu, 6 Mar 2025 12:07:00 +0200 Subject: [PATCH 14/33] Add check_mode support. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 0d112d9f..c35b945c 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -553,12 +553,13 @@ def user_mod(cursor, user, host, host_all, password, encrypted, final_attributes = attributes_get(cursor, user, host) if user_is_locked(cursor, user, host) != locked: - if locked: - cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) - msg = 'User locked' - else: - cursor.execute("ALTER USER %s@%s ACCOUNT UNLOCK", (user, host)) - msg = 'User unlocked' + if not module.check_mode: + if locked: + cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) + msg = 'User locked' + else: + cursor.execute("ALTER USER %s@%s ACCOUNT UNLOCK", (user, host)) + msg = 'User unlocked' changed = True if role: From 85b16cc82bc47666f5336d8aca0e6a27cd042abe Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Thu, 6 Mar 2025 12:07:16 +0200 Subject: [PATCH 15/33] Change documentation per request of @Andersson007 Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/modules/mysql_user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index b0699ab3..932d01ed 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -192,7 +192,8 @@ locked: description: - - Lock account to prevent connections using it, this is primarily used for creating a user that will act as a DEFINER on stored procedures. + - Lock account to prevent connections using it. + - This is primarily used for creating a user that will act as a DEFINER on stored procedures. default: false type: bool version_added: '3.13.0' From 7fd6b640fe4cb172cbb4bd138a84a385490db13c Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Thu, 6 Mar 2025 12:13:39 +0200 Subject: [PATCH 16/33] Add check_mode: true test cases Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../tasks/test_user_locking.yml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index 6462e6bc..e0fea7ec 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -36,6 +36,24 @@ failed_when: - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') + - name: Mysql_user Lock user | create locked | Unlock test user check_mode: true + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: false + check_mode: true + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create locked | Assert that test user is locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') + - name: Mysql_user Lock user | create locked | Unlock test user community.mysql.mysql_user: <<: *mysql_params @@ -77,6 +95,24 @@ failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + - name: Mysql_user Lock user | create unlocked | Lock test user check_mode: true + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: true + check_mode: true + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create unlocked | Assert that test user is not locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + - name: Mysql_user Lock user | create unlocked | Lock test user community.mysql.mysql_user: <<: *mysql_params From 6b8e7211538c2fb0844a4b96a587b81ee7bbee54 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Thu, 6 Mar 2025 12:17:27 +0200 Subject: [PATCH 17/33] Fix names that included `check_mode: true` Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../targets/test_mysql_user/tasks/test_user_locking.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index e0fea7ec..f0f63eee 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -36,7 +36,7 @@ failed_when: - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') - - name: Mysql_user Lock user | create locked | Unlock test user check_mode: true + - name: 'Mysql_user Lock user | create locked | Unlock test user check_mode: true' community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user @@ -95,7 +95,7 @@ failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') - - name: Mysql_user Lock user | create unlocked | Lock test user check_mode: true + - name: 'Mysql_user Lock user | create unlocked | Lock test user check_mode: true' community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user From 3cceba40ebea9fb324d66984db1fabf913c0a33b Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Thu, 6 Mar 2025 16:14:34 +0200 Subject: [PATCH 18/33] Add idempotence checks Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../tasks/test_user_locking.yml | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index f0f63eee..3268c46c 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -36,6 +36,17 @@ failed_when: - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') + - name: 'Mysql_user Lock user | create locked | Idempotence check' + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: true + check_mode: true + priv: + 'mysql_lock_user_test.*': 'SELECT' + register: idempotence_check + failed_when: idempotence_check is changed + - name: 'Mysql_user Lock user | create locked | Unlock test user check_mode: true' community.mysql.mysql_user: <<: *mysql_params @@ -95,7 +106,18 @@ failed_when: - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') - - name: 'Mysql_user Lock user | create unlocked | Lock test user check_mode: true' + - name: 'Mysql_user Lock user | create unlocked | Idempotence check' + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: false + check_mode: true + priv: + 'mysql_lock_user_test.*': 'SELECT' + register: idempotence_check + failed_when: idempotence_check is changed + + - name: 'Mysql_user Lock user | create unlocked | Lock test user check_mode: true' community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user From 3a8b4a16a12866de6677d0028fe7bebfea3cf5f9 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Thu, 6 Mar 2025 23:30:37 +0200 Subject: [PATCH 19/33] Switch calls to user_mod with sequences of None positional arguments to full named arguments Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/modules/mysql_role.py | 11 ++++++----- plugins/modules/mysql_user.py | 10 ++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py index c88392b7..382445c1 100644 --- a/plugins/modules/mysql_role.py +++ b/plugins/modules/mysql_role.py @@ -930,11 +930,12 @@ def update(self, users, privs, check_mode=False, set_default_role_all=set_default_role_all) if privs: - result = user_mod(self.cursor, self.name, self.host, - None, None, None, None, None, None, None, - privs, append_privs, subtract_privs, None, None, - self.module, None, None, role=True, - maria_role=self.is_mariadb) + result = user_mod(cursor=self.cursor, user=self.name, host=self.host, + host_all=None, password=None, encrypted=None, plugin=None, + plugin_auth_string=None, plugin_hash_string=None, salt=None, + new_priv=privs, append_privs=append_privs, subtract_privs=subtract_privs, + attributes=None, tls_requires=None, module=self.module, password_expire=None, + password_expire_interval=None, role=True, maria_role=self.is_mariadb) changed = result['changed'] if admin: diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 932d01ed..075add9e 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -598,10 +598,12 @@ def main(): password_expire, password_expire_interval, locked=locked) else: - result = user_mod(cursor, user, host, host_all, None, encrypted, - None, None, None, None, - priv, append_privs, subtract_privs, attributes, tls_requires, module, - password_expire, password_expire_interval, locked=locked) + result = user_mod(cursor=cursor, user=user, host=host, host_all=host_all, password=None, + encrypted=encrypted, plugin=None, plugin_hash_string=None, plugin_auth_string=None, + salt=None, new_priv=priv, append_privs=append_privs, subtract_privs=subtract_privs, + attributes=attributes, tls_requires=tls_requires, module=module, + password_expire=password_expire, password_expire_interval=password_expire_interval, + locked=locked) changed = result['changed'] msg = result['msg'] password_changed = result['password_changed'] From e3775aed61ccc46066945cc3f4e42a0b5b9c947d Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Mon, 10 Mar 2025 21:17:15 +0200 Subject: [PATCH 20/33] locked check should not run for roles. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index c35b945c..311afb2a 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -552,7 +552,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if attribute_support: final_attributes = attributes_get(cursor, user, host) - if user_is_locked(cursor, user, host) != locked: + if not role and user_is_locked(cursor, user, host) != locked: if not module.check_mode: if locked: cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) From 321309bf0fdeecf6b9966b56b070f6e2a7ed8ffa Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Mon, 10 Mar 2025 21:21:20 +0200 Subject: [PATCH 21/33] Fix wrong indent Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../targets/test_mysql_user/tasks/test_user_locking.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index 3268c46c..f9bd07b6 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -117,7 +117,7 @@ register: idempotence_check failed_when: idempotence_check is changed - - name: 'Mysql_user Lock user | create unlocked | Lock test user check_mode: true' + - name: 'Mysql_user Lock user | create unlocked | Lock test user check_mode: true' community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user From 4f0069d511ec352d13339c1646acd1168318bba3 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Mon, 10 Mar 2025 23:23:04 +0200 Subject: [PATCH 22/33] check_mode is set at the task level and not the module level Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../targets/test_mysql_user/tasks/test_user_locking.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml index f9bd07b6..9897e181 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -37,22 +37,22 @@ - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') - name: 'Mysql_user Lock user | create locked | Idempotence check' + check_mode: true community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user locked: true - check_mode: true priv: 'mysql_lock_user_test.*': 'SELECT' register: idempotence_check failed_when: idempotence_check is changed - name: 'Mysql_user Lock user | create locked | Unlock test user check_mode: true' + check_mode: true community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user locked: false - check_mode: true priv: 'mysql_lock_user_test.*': 'SELECT' @@ -107,22 +107,22 @@ - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') - name: 'Mysql_user Lock user | create unlocked | Idempotence check' + check_mode: true community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user locked: false - check_mode: true priv: 'mysql_lock_user_test.*': 'SELECT' register: idempotence_check failed_when: idempotence_check is changed - name: 'Mysql_user Lock user | create unlocked | Lock test user check_mode: true' + check_mode: true community.mysql.mysql_user: <<: *mysql_params name: mysql_locked_user locked: true - check_mode: true priv: 'mysql_lock_user_test.*': 'SELECT' From 178eade8e8a42894d7b439ce1348f2273e2e3973 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 12:44:12 +0200 Subject: [PATCH 23/33] Add user locking to info module and test. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/modules/mysql_info.py | 5 ++++- .../targets/test_mysql_info/tasks/filter_users_info.yml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/modules/mysql_info.py b/plugins/modules/mysql_info.py index 8c3845d3..e421c595 100644 --- a/plugins/modules/mysql_info.py +++ b/plugins/modules/mysql_info.py @@ -318,6 +318,7 @@ get_resource_limits, get_existing_authentication, get_user_implementation, + user_is_locked, ) from ansible.module_utils.six import iteritems from ansible.module_utils._text import to_native @@ -652,8 +653,10 @@ def __get_users_info(self): if authentications: output_dict.update(authentications[0]) + if not line['is_role']: + output_dict['locked'] = user_is_locked(self.cursor, user, host) + # TODO password_option - # TODO lock_option # but both are not supported by mysql_user atm. So no point yet. output.append(output_dict) diff --git a/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml b/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml index 36508f3c..8e80100d 100644 --- a/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml +++ b/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml @@ -261,6 +261,7 @@ resource_limits: "{{ item.resource_limits | default(omit) }}" column_case_sensitive: true state: present + locked: "{{ item.locked | default(omit) }}" loop: "{{ result.users_info }}" loop_control: label: "{{ item.name }}@{{ item.host }}" From 3d9df81d944d21306413fabe59592ed0ee0516da Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 13:09:05 +0200 Subject: [PATCH 24/33] Based on the previous CI build is_role is not (always?) present so checking for it, right now with pprint of the line for debug purposes Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/modules/mysql_info.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/modules/mysql_info.py b/plugins/modules/mysql_info.py index e421c595..17ab591c 100644 --- a/plugins/modules/mysql_info.py +++ b/plugins/modules/mysql_info.py @@ -653,8 +653,11 @@ def __get_users_info(self): if authentications: output_dict.update(authentications[0]) - if not line['is_role']: + if line.get('is_role') and not line['is_role']: output_dict['locked'] = user_is_locked(self.cursor, user, host) + else: + from pprint import pprint + pprint(line) # TODO password_option # but both are not supported by mysql_user atm. So no point yet. From 13cb20768f16a053cc413b43b745a6c83fc1c199 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 13:26:08 +0200 Subject: [PATCH 25/33] Hopefully fixed condition Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/modules/mysql_info.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/modules/mysql_info.py b/plugins/modules/mysql_info.py index 17ab591c..bd3686c7 100644 --- a/plugins/modules/mysql_info.py +++ b/plugins/modules/mysql_info.py @@ -653,11 +653,8 @@ def __get_users_info(self): if authentications: output_dict.update(authentications[0]) - if line.get('is_role') and not line['is_role']: + if line.get('is_role') and line['is_role'] == 'N': output_dict['locked'] = user_is_locked(self.cursor, user, host) - else: - from pprint import pprint - pprint(line) # TODO password_option # but both are not supported by mysql_user atm. So no point yet. From 72482fc5ad0d8cc2f75e3a2c027535742dd2d1c7 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 13:51:20 +0200 Subject: [PATCH 26/33] Debug CI error Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 311afb2a..f4ba1230 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -58,8 +58,11 @@ def user_is_locked(cursor, user, host): # Per discussions on irc:libera.chat:#maria the query may return up to 2 rows but "ACCOUNT LOCK" should always be in the first row. result = cursor.fetchone() + from pprint import pprint + pprint(result) + # ACCOUNT LOCK does not have to be the last option in the CREATE USER query. - if result[0].find('ACCOUNT LOCK') > 0: + if result[0] and result[0].find('ACCOUNT LOCK') > 0: return True return False From e6dde5843acd6e02a6ee5e99c5f4a60b804e91fa Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 15:18:53 +0200 Subject: [PATCH 27/33] Handle DictCursor Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index f4ba1230..22140875 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -58,12 +58,15 @@ def user_is_locked(cursor, user, host): # Per discussions on irc:libera.chat:#maria the query may return up to 2 rows but "ACCOUNT LOCK" should always be in the first row. result = cursor.fetchone() - from pprint import pprint - pprint(result) - # ACCOUNT LOCK does not have to be the last option in the CREATE USER query. - if result[0] and result[0].find('ACCOUNT LOCK') > 0: - return True + # Need to handle both DictCursor and non-DictCursor + if type(result) == type([]): + if result[0] and result[0].find('ACCOUNT LOCK') > 0: + return True + elif type(result) == type({}): + for res in result.values(): + if res.find('ACCOUNT LOCK') > 0: + return True return False From a124e77505e22bfaf542d2a94cdc54dde3412f23 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 15:20:57 +0200 Subject: [PATCH 28/33] Cleaner type conditionals Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 22140875..e8657238 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -60,10 +60,10 @@ def user_is_locked(cursor, user, host): # ACCOUNT LOCK does not have to be the last option in the CREATE USER query. # Need to handle both DictCursor and non-DictCursor - if type(result) == type([]): + if type(result) is list: if result[0] and result[0].find('ACCOUNT LOCK') > 0: return True - elif type(result) == type({}): + elif type(result) is dict: for res in result.values(): if res.find('ACCOUNT LOCK') > 0: return True From a8702b88d5d8cff1a19e8fc6da5c17df41b6b48f Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 15:41:48 +0200 Subject: [PATCH 29/33] Add check_mode feedback Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index e8657238..d8b2d5cf 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -566,6 +566,12 @@ def user_mod(cursor, user, host, host_all, password, encrypted, else: cursor.execute("ALTER USER %s@%s ACCOUNT UNLOCK", (user, host)) msg = 'User unlocked' + else: + if locked: + msg = 'User will be locked' + else: + msg = 'User will be unlocked' + changed = True if role: From e4f85f03d2a7c0b204aac70cfdc0d99698a64a2b Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 15:45:26 +0200 Subject: [PATCH 30/33] change type() to isinstance() Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index d8b2d5cf..8b3d856c 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -60,10 +60,10 @@ def user_is_locked(cursor, user, host): # ACCOUNT LOCK does not have to be the last option in the CREATE USER query. # Need to handle both DictCursor and non-DictCursor - if type(result) is list: + if isinstance(result, list): if result[0] and result[0].find('ACCOUNT LOCK') > 0: return True - elif type(result) is dict: + elif isinstance(result, dict): for res in result.values(): if res.find('ACCOUNT LOCK') > 0: return True From 3f7f036cb6e4ea6941671c0e458783ce5f5844b3 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 21:02:54 +0200 Subject: [PATCH 31/33] Add some debugging output to help troubleshoot CI Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 8b3d856c..e23d4301 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -61,13 +61,14 @@ def user_is_locked(cursor, user, host): # ACCOUNT LOCK does not have to be the last option in the CREATE USER query. # Need to handle both DictCursor and non-DictCursor if isinstance(result, list): - if result[0] and result[0].find('ACCOUNT LOCK') > 0: + if result[0].find('ACCOUNT LOCK') > 0: return True elif isinstance(result, dict): for res in result.values(): if res.find('ACCOUNT LOCK') > 0: return True - + from pprint import pprint + pprint(result) return False From f91d15e4779b45da44cdc4f8d8c1b928cf70c5ac Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 21:27:26 +0200 Subject: [PATCH 32/33] It's a tuple and not a list :facepalm: Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- plugins/module_utils/user.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index e23d4301..590d1a4a 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -60,15 +60,14 @@ def user_is_locked(cursor, user, host): # ACCOUNT LOCK does not have to be the last option in the CREATE USER query. # Need to handle both DictCursor and non-DictCursor - if isinstance(result, list): + if isinstance(result, tuple): if result[0].find('ACCOUNT LOCK') > 0: return True elif isinstance(result, dict): for res in result.values(): if res.find('ACCOUNT LOCK') > 0: return True - from pprint import pprint - pprint(result) + return False From 247bf5a65c44f7035a8d29e7af25029b3f744709 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Tue, 11 Mar 2025 21:45:38 +0200 Subject: [PATCH 33/33] Add another builtin account to the exclusion list Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- .../targets/test_mysql_info/tasks/filter_users_info.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml b/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml index 8e80100d..558d3090 100644 --- a/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml +++ b/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml @@ -276,6 +276,7 @@ - item.name != 'mariadb.sys' - item.name != 'mysql.sys' - item.name != 'mysql.infoschema' + - item.name != 'mysql.session' # ================================== Cleanup ============================