mirror of
https://github.com/ansible-collections/community.mysql.git
synced 2025-04-01 08:10:32 -07:00
User locking (#702)
* function to check if a user is locked already Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * 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 <es.rosenberg+github@gmail.com> * Fix missing parameters for execute() Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Add the locked attribute Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Initial user locking integration tests Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Add attribute documentation Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * More descriptive names in the integration tests Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * - Changes requested/suggested by @Andersson007 - Example usage - Changelog fragment Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Fix user_is_locked and remove host_all option. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * 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 <es.rosenberg+github@gmail.com> * Switch locked to named instead of positional. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Add check_mode support. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Add check_mode: true test cases Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Fix names that included `check_mode: true` Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Add idempotence checks Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * 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 <es.rosenberg+github@gmail.com> * locked check should not run for roles. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * 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 <es.rosenberg+github@gmail.com> * Add user locking to info module and test. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Handle DictCursor Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Add check_mode feedback Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Add another builtin account to the exclusion list Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Initial switch to default=None for locked, will need to add a test for it. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> * Add check that missing locked argument does not unlock a user Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com> --------- Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>
This commit is contained in:
parent
dd7e297d50
commit
45a29408ad
8 changed files with 285 additions and 14 deletions
2
changelogs/fragments/702-user_locking.yaml
Normal file
2
changelogs/fragments/702-user_locking.yaml
Normal file
|
@ -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.
|
|
@ -52,6 +52,25 @@ def user_exists(cursor, user, host, host_all):
|
|||
return count[0] > 0
|
||||
|
||||
|
||||
def user_is_locked(cursor, user, host):
|
||||
cursor.execute("SHOW CREATE USER %s@%s", (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()
|
||||
|
||||
# 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, 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
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def sanitize_requires(tls_requires):
|
||||
sanitized_requires = {}
|
||||
if tls_requires:
|
||||
|
@ -160,7 +179,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")
|
||||
|
@ -250,6 +269,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", (user, host))
|
||||
|
||||
return {'changed': True, 'password_changed': not used_existing_password, 'attributes': final_attributes}
|
||||
|
||||
|
||||
|
@ -264,7 +286,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=None, role=False, maria_role=False):
|
||||
changed = False
|
||||
msg = "User unchanged"
|
||||
grant_option = False
|
||||
|
@ -536,6 +558,22 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
if attribute_support:
|
||||
final_attributes = attributes_get(cursor, user, host)
|
||||
|
||||
if not role and locked is not None 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))
|
||||
msg = 'User locked'
|
||||
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:
|
||||
continue
|
||||
|
||||
|
|
|
@ -319,6 +319,7 @@ from ansible_collections.community.mysql.plugins.module_utils.user import (
|
|||
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
|
||||
|
@ -653,8 +654,10 @@ class MySQL_Info(object):
|
|||
if authentications:
|
||||
output_dict.update(authentications[0])
|
||||
|
||||
if line.get('is_role') and line['is_role'] == 'N':
|
||||
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)
|
||||
|
|
|
@ -930,11 +930,12 @@ class Role():
|
|||
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:
|
||||
|
|
|
@ -189,6 +189,15 @@ options:
|
|||
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.
|
||||
- If not specified leaves the lock state as is (for a new user creates unlocked).
|
||||
type: bool
|
||||
version_added: '3.13.0'
|
||||
|
||||
attributes:
|
||||
description:
|
||||
- "Create, update, or delete user attributes (arbitrary 'key: value' comments) for the user."
|
||||
|
@ -225,6 +234,7 @@ author:
|
|||
- Lukasz Tomaszkiewicz (@tomaszkiewicz)
|
||||
- kmarse (@kmarse)
|
||||
- Laurent Indermühle (@laurent-indermuehle)
|
||||
- E.S. Rosenberg (@Keeper-of-the-Keys)
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.mysql.mysql
|
||||
|
@ -400,6 +410,13 @@ EXAMPLES = r'''
|
|||
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
|
||||
|
@ -470,6 +487,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'),
|
||||
)
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
|
@ -510,6 +528,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 +596,15 @@ 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=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)
|
||||
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']
|
||||
|
@ -601,7 +622,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=locked)
|
||||
changed = result['changed']
|
||||
password_changed = result['password_changed']
|
||||
final_attributes = result['attributes']
|
||||
|
|
|
@ -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 }}"
|
||||
|
@ -275,6 +276,7 @@
|
|||
- item.name != 'mariadb.sys'
|
||||
- item.name != 'mysql.sys'
|
||||
- item.name != 'mysql.infoschema'
|
||||
- item.name != 'mysql.session'
|
||||
|
||||
|
||||
# ================================== Cleanup ============================
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
---
|
||||
|
||||
- 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 locked | Create test user
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
password: 'msandbox'
|
||||
locked: 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 | Idempotence check'
|
||||
check_mode: true
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
locked: true
|
||||
priv:
|
||||
'mysql_lock_user_test.*': 'SELECT'
|
||||
register: idempotence_check
|
||||
failed_when: idempotence_check is changed
|
||||
|
||||
- name: 'Mysql_user Lock user | create locked | Check that absense of locked does not unlock the user'
|
||||
check_mode: true
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
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
|
||||
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
|
||||
name: mysql_locked_user
|
||||
locked: false
|
||||
priv:
|
||||
'mysql_lock_user_test.*': 'SELECT'
|
||||
|
||||
- name: Mysql_user Lock user | create locked | 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 locked | Remove test user
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
state: absent
|
||||
|
||||
- name: Mysql_user Lock user | create unlocked | Create test user
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
password: 'msandbox'
|
||||
locked: false
|
||||
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 | Idempotence check'
|
||||
check_mode: true
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
locked: false
|
||||
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
|
||||
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
|
||||
name: mysql_locked_user
|
||||
locked: true
|
||||
priv:
|
||||
'mysql_lock_user_test.*': 'SELECT'
|
||||
|
||||
- name: Mysql_user Lock user | create unlocked | 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 unlocked | Remove test user
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
state: absent
|
||||
|
||||
- name: Mysql_user Lock user | create default | Create test user
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
password: 'msandbox'
|
||||
priv:
|
||||
'mysql_lock_user_test.*': 'SELECT'
|
||||
|
||||
- name: Mysql_user Lock user | create default | 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 default | Remove test user
|
||||
community.mysql.mysql_user:
|
||||
<<: *mysql_params
|
||||
name: mysql_locked_user
|
||||
state: absent
|
||||
|
||||
# ========================= Teardown ======================================
|
||||
|
||||
- name: Mysql_user Lock user | Delete test database
|
||||
community.mysql.mysql_db:
|
||||
<<: *mysql_params
|
||||
name: mysql_lock_user_test
|
||||
state: absent
|
Loading…
Add table
Reference in a new issue