Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User locking #702

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1380c20
function to check if a user is locked already
Keeper-of-the-Keys Mar 3, 2025
6747ef3
Add the location and logic of where I think user locking would happen.
Keeper-of-the-Keys Mar 3, 2025
5079dc4
Fix missing parameters for execute()
Keeper-of-the-Keys Mar 3, 2025
583407f
Add the locked attribute
Keeper-of-the-Keys Mar 3, 2025
0f590dc
Initial user locking integration tests
Keeper-of-the-Keys Mar 5, 2025
7358c1e
Add attribute documentation
Keeper-of-the-Keys Mar 5, 2025
19ff2d7
More descriptive names in the integration tests
Keeper-of-the-Keys Mar 5, 2025
17e398f
Fixes for sanity checks
Keeper-of-the-Keys Mar 5, 2025
d839871
- Changes requested/suggested by @Andersson007
Keeper-of-the-Keys Mar 5, 2025
fbeeff5
Fix user_is_locked and remove host_all option.
Keeper-of-the-Keys Mar 5, 2025
9209a12
Fix host of user (was % should have been localhost after deleting `ho…
Keeper-of-the-Keys Mar 5, 2025
c7097b9
Fix condition
Keeper-of-the-Keys Mar 5, 2025
e95c3da
Switch locked to named instead of positional.
Keeper-of-the-Keys Mar 6, 2025
3984380
Add check_mode support.
Keeper-of-the-Keys Mar 6, 2025
85b16cc
Change documentation per request of @Andersson007
Keeper-of-the-Keys Mar 6, 2025
7fd6b64
Add check_mode: true test cases
Keeper-of-the-Keys Mar 6, 2025
6b8e721
Fix names that included `check_mode: true`
Keeper-of-the-Keys Mar 6, 2025
3cceba4
Add idempotence checks
Keeper-of-the-Keys Mar 6, 2025
3a8b4a1
Switch calls to user_mod with sequences of None positional arguments …
Keeper-of-the-Keys Mar 6, 2025
e3775ae
locked check should not run for roles.
Keeper-of-the-Keys Mar 10, 2025
321309b
Fix wrong indent
Keeper-of-the-Keys Mar 10, 2025
4f0069d
check_mode is set at the task level and not the module level
Keeper-of-the-Keys Mar 10, 2025
178eade
Add user locking to info module and test.
Keeper-of-the-Keys Mar 11, 2025
3d9df81
Based on the previous CI build is_role is not (always?) present so ch…
Keeper-of-the-Keys Mar 11, 2025
13cb207
Hopefully fixed condition
Keeper-of-the-Keys Mar 11, 2025
72482fc
Debug CI error
Keeper-of-the-Keys Mar 11, 2025
e6dde58
Handle DictCursor
Keeper-of-the-Keys Mar 11, 2025
a124e77
Cleaner type conditionals
Keeper-of-the-Keys Mar 11, 2025
a8702b8
Add check_mode feedback
Keeper-of-the-Keys Mar 11, 2025
e4f85f0
change type() to isinstance()
Keeper-of-the-Keys Mar 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/702-user_locking.yaml
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 40 additions & 2 deletions plugins/module_utils/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, list):
if result[0] and 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:
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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}


Expand All @@ -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=False, role=False, maria_role=False):
changed = False
msg = "User unchanged"
grant_option = False
Expand Down Expand Up @@ -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 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

Expand Down
5 changes: 4 additions & 1 deletion plugins/modules/mysql_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -652,8 +653,10 @@ def __get_users_info(self):
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)
Expand Down
11 changes: 6 additions & 5 deletions plugins/modules/mysql_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 26 additions & 6 deletions plugins/modules/mysql_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,15 @@
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.
default: false
type: bool
version_added: '3.13.0'

attributes:
description:
- "Create, update, or delete user attributes (arbitrary 'key: value' comments) for the user."
Expand Down Expand Up @@ -400,6 +409,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
Expand Down Expand Up @@ -470,6 +486,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='false'),
)
module = AnsibleModule(
argument_spec=argument_spec,
Expand Down Expand Up @@ -510,6 +527,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))
Expand Down Expand Up @@ -577,13 +595,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']
Expand All @@ -601,7 +621,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']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/targets/test_mysql_user/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading