From 314ec0295568ce47314abd552b8532cdc5e2d769 Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 22 Nov 2023 10:32:57 +0200 Subject: [PATCH] initial commit for password_expire support --- .../implementations/mariadb/user.py | 6 ++ .../implementations/mysql/user.py | 6 ++ plugins/module_utils/user.py | 86 ++++++++++++++++++- plugins/modules/mysql_user.py | 31 ++++++- 4 files changed, 123 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/implementations/mariadb/user.py b/plugins/module_utils/implementations/mariadb/user.py index c1d2b6133..cdc14b217 100644 --- a/plugins/module_utils/implementations/mariadb/user.py +++ b/plugins/module_utils/implementations/mariadb/user.py @@ -23,3 +23,9 @@ def server_supports_alter_user(cursor): version = get_server_version(cursor) return LooseVersion(version) >= LooseVersion("10.2") + + +def server_supports_password_expire(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion("10.4.3") diff --git a/plugins/module_utils/implementations/mysql/user.py b/plugins/module_utils/implementations/mysql/user.py index 1bdad5740..4e41c0542 100644 --- a/plugins/module_utils/implementations/mysql/user.py +++ b/plugins/module_utils/implementations/mysql/user.py @@ -24,3 +24,9 @@ def server_supports_alter_user(cursor): version = get_server_version(cursor) return LooseVersion(version) >= LooseVersion("5.6") + + +def server_supports_password_expire(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion("5.7") diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index dbc1c9b87..b76e9c395 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -151,12 +151,13 @@ 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): + tls_requires, module, reuse_existing_password, + password_expire, password_expire_interval): # 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} # Determine what user management method server uses @@ -200,6 +201,12 @@ def user_add(cursor, user, host, host_all, password, encrypted, query_with_args_and_tls_requires = query_with_args + (tls_requires,) cursor.execute(*mogrify(*query_with_args_and_tls_requires)) + if password_expire and impl.supports_identified_by_password(cursor): + set_password_expire(cursor, user, host, password_expire, password_expire_interval) + else: + module.fail_json(msg="The server version does not match the requirements " + "for password_expire parameter. See module's documentation.") + if new_priv is not None: for db_table, priv in iteritems(new_priv): privileges_grant(cursor, user, host, db_table, priv, tls_requires) @@ -218,7 +225,8 @@ 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, tls_requires, module, + password_expire, password_expire_interval, role=False, maria_role=False): changed = False msg = "User unchanged" grant_option = False @@ -301,6 +309,27 @@ def user_mod(cursor, user, host, host_all, password, encrypted, raise e changed = True + # Handle password expiration + if bool(password_expire): + if impl.server_supports_password_expire(cursor): + update = False + mariadb_role = True if "mariadb" in str(impl.__name__) else False + current_password_policy = get_password_expiration_policy(cursor, user, host, maria_role=mariadb_role) + if not (current_password_policy == -1 and password_expire == "default" or + current_password_policy == 0 and password_expire == "never" or + current_password_policy == password_expire_interval): + + update = True + + if module.check_mode: + return {'changed': True, 'msg': msg, 'password_changed': password_changed} + set_password_expire(cursor, user, host, password_expire, password_expire_interval) + password_changed = True + changed = True + else: + module.fail_json(msg="The server version does not match the requirements " + "for password_expire parameter. See module's documentation.") + # Handle plugin authentication if plugin and not role: cursor.execute("SELECT plugin, authentication_string FROM mysql.user " @@ -924,6 +953,57 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode): return True +def set_password_expire(cursor, user, host, password_expire, password_expire_interval): + """Fuction to set passowrd expiration for user. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User hostname. + password_expire (str): Password expiration mode. + password_expire_days (int): Invterval of days password expires. + """ + if password_expire.lower() == "never": + statment = "PASSWORD EXPIRE NEVER" + elif password_expire.lower() == "default": + statment = "PASSWORD EXPIRE DEFAULT" + elif password_expire.lower() == "interval": + if password_expire_interval > 0: + statment = "PASSWORD EXPIRE INTERVAL %d DAY" % (password_expire_interval) + else: + # expire password now if days <=0 + if isinstance(password_expire_interval, int): + statment = "PASSWORD EXPIRE" + query = "ALTER USER %s@%s %s" % (user, host, statment) + cursor.execute(query) + + +def get_password_expiration_policy(cursor, user, host, maria_role=False): + """Function to get password policy for user. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User hostname. + maria_role (bool, optional): mariadb or mysql. Defaults to False. + + Returns: + policy (int): Current users password policy. + """ + if not maria_role: + statment = "SELECT password_lifetime FROM mysql.user \ + WHERE User = %s AND Host = %s", (user, host) + else: + statment = "SELECT JSON_EXTRACT(Priv, '$.password_lifetime') AS password_lifetime \ + FROM mysql.global_priv \ + WHERE User = %s AND Host = %s", (user, host) + cursor.execute(*statment) + policy = cursor.fetchone()[0] + if not policy: + policy = -1 + return int(policy) + + 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 3e914e6a4..6ab006e73 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -155,6 +155,20 @@ - Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead. type: dict version_added: '3.6.0' + password_expire: + description: + - C(never) password will never expire. + - C(default) password is defined ussing global system varaiable I(default_password_lifetime) setting. + - C(interval) password will expire in days which is defined in I(password_expire_interval) + type: str + choices: [ never, default, interval ] + default: never + password_expire_interval: + description: + - number of days password will expire. Used with I(password_expire) + - if C(password_expire_interval <= 0) password will expire immediately. + type: int + default: None column_case_sensitive: description: @@ -415,6 +429,8 @@ def main(): force_context=dict(type='bool', default=False), session_vars=dict(type='dict'), column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True + password_expire=dict(type='str', choices=['never', 'default', 'interval'], default=None, no_log=True), + password_expire_interval=dict(type='int', default=None, no_log=True), ) module = AnsibleModule( argument_spec=argument_spec, @@ -451,6 +467,8 @@ def main(): resource_limits = module.params["resource_limits"] session_vars = module.params["session_vars"] column_case_sensitive = module.params["column_case_sensitive"] + password_expire = module.params["password_expire"] + password_expire_interval = module.params["password_expire_interval"] 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)) @@ -460,6 +478,10 @@ def main(): if mysql_driver is None: module.fail_json(msg=mysql_driver_fail_msg) + + if password_expire == "interval" and not password_expire_interval: + module.fail_json(msg="password_expire value interval \ + should be used with password_expire_interval") cursor = None try: @@ -506,12 +528,14 @@ def main(): 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, tls_requires, module, + password_expire, password_expire_interval) 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, tls_requires, module, + password_expire, password_expire_interval) changed = result['changed'] msg = result['msg'] password_changed = result['password_changed'] @@ -527,7 +551,8 @@ 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, tls_requires, module, reuse_existing_password, + password_expire, password_expire_interval) changed = result['changed'] password_changed = result['password_changed'] if changed: