Skip to content

Commit

Permalink
password_expire support for mysql_user (#598)
Browse files Browse the repository at this point in the history
* initial commit for password_expire support

* sanity check and default values

* add one more if block for version check

* some changes and integration tests

* docs and sanity and integration test fix

* make integration tests work

* make integration tests work

* fix unneeded commits

* fix verify as well

* Update plugins/modules/mysql_user.py

Co-authored-by: Laurent Indermühle <[email protected]>

* Update tests/integration/targets/test_mysql_user/tasks/test_password_expire.yml

Co-authored-by: Laurent Indermühle <[email protected]>

* Apply suggestions from code review

Co-authored-by: Laurent Indermühle <[email protected]>

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <[email protected]>

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <[email protected]>

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <[email protected]>

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <[email protected]>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <[email protected]>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <[email protected]>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <[email protected]>

* typo and no_log remove for password_expire* vars

* add change log fragment

* move one if statement to module initialiazation

* fix merge conflicts

* fix order

* some fixes

* set no_log to true for password word containing keys

* fix sanity error

* Update changelogs/fragments/598-password_expire-support-for-mysql_user.yml

Co-authored-by: Andrew Klychkov <[email protected]>

---------

Co-authored-by: Laurent Indermühle <[email protected]>
Co-authored-by: Andrew Klychkov <[email protected]>
  • Loading branch information
3 people authored Feb 22, 2024
1 parent 21fe52d commit 40af258
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- "mysql_user - add the ``password_expire`` and ``password_expire_interval`` arguments to implement the password expiration management for mysql user (https://github.com/ansible-collections/community.mysql/pull/598)."
6 changes: 6 additions & 0 deletions plugins/module_utils/implementations/mariadb/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
6 changes: 6 additions & 0 deletions plugins/module_utils/implementations/mysql/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
100 changes: 98 additions & 2 deletions plugins/module_utils/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ def get_existing_authentication(cursor, user, host):

def user_add(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, new_priv,
attributes, tls_requires, reuse_existing_password, module):
attributes, tls_requires, reuse_existing_password, module,
password_expire, password_expire_interval):
# 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 @@ -205,6 +206,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:
if not impl.server_supports_password_expire(cursor):
module.fail_json(msg="The server version does not match the requirements "
"for password_expire parameter. See module's documentation.")
set_password_expire(cursor, user, host, password_expire, password_expire_interval)

if new_priv is not None:
for db_table, priv in iteritems(new_priv):
privileges_grant(cursor, user, host, db_table, priv, tls_requires)
Expand All @@ -230,7 +237,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, attributes, tls_requires, module, role=False, maria_role=False):
append_privs, subtract_privs, attributes, tls_requires, module,
password_expire, password_expire_interval, role=False, maria_role=False):
changed = False
msg = "User unchanged"
grant_option = False
Expand Down Expand Up @@ -312,6 +320,28 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
raise e
changed = True

# Handle password expiration
if bool(password_expire):
if not impl.server_supports_password_expire(cursor):
module.fail_json(msg="The server version does not match the requirements "
"for password_expire parameter. See module's documentation.")
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)
password_expired = is_password_expired(cursor, user, host)
# Check if changes needed to be applied.
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 and password_expire == "interval") or
(password_expire == 'now' and password_expired)):

update = True

if not module.check_mode:
set_password_expire(cursor, user, host, password_expire, password_expire_interval)
password_changed = True
changed = True

# Handle plugin authentication
if plugin and not role:
cursor.execute("SELECT plugin, authentication_string FROM mysql.user "
Expand Down Expand Up @@ -973,6 +1003,72 @@ 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":
statement = "PASSWORD EXPIRE NEVER"
elif password_expire.lower() == "default":
statement = "PASSWORD EXPIRE DEFAULT"
elif password_expire.lower() == "interval":
statement = "PASSWORD EXPIRE INTERVAL %d DAY" % (password_expire_interval)
elif password_expire.lower() == "now":
statement = "PASSWORD EXPIRE"

cursor.execute("ALTER USER %s@%s " + statement, (user, host))


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:
statement = "SELECT IFNULL(password_lifetime, -1) FROM mysql.user \
WHERE User = %s AND Host = %s", (user, host)
else:
statement = "SELECT JSON_EXTRACT(Priv, '$.password_lifetime') AS password_lifetime \
FROM mysql.global_priv \
WHERE User = %s AND Host = %s", (user, host)
cursor.execute(*statement)
policy = cursor.fetchone()[0]
return int(policy)


def is_password_expired(cursor, user, host):
"""Function to check if password is expired
Args:
cursor (cursor): DB driver cursor object.
user (str): User name.
host (str): User hostname.
Returns:
expired (bool): True if expired, else False.
"""
statement = "SELECT password_expired FROM mysql.user \
WHERE User = %s AND Host = %s", (user, host)
cursor.execute(*statement)
expired = cursor.fetchone()[0]
if str(expired) == "Y":
return True
return False


def get_attribute_support(cursor):
"""Checks if the MySQL server supports user attributes.
Expand Down
3 changes: 2 additions & 1 deletion plugins/modules/mysql_role.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,8 @@ def update(self, users, privs, check_mode=False,
result = user_mod(self.cursor, self.name, self.host,
None, None, None, None, None, None,
privs, append_privs, subtract_privs, None, None,
self.module, role=True, maria_role=self.is_mariadb)
self.module, None, None, role=True,
maria_role=self.is_mariadb)
changed = result['changed']

if admin:
Expand Down
32 changes: 29 additions & 3 deletions plugins/modules/mysql_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@
- 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) - I(password) will never expire.
- C(default) - I(password) is defined using global system variable I(default_password_lifetime) setting.
- C(interval) - I(password) will expire in days which is defined in I(password_expire_interval).
- C(now) - I(password) will expire immediately.
type: str
choices: [ now, never, default, interval ]
version_added: '3.9.0'
password_expire_interval:
description:
- Number of days I(password) will expire. Requires I(password_expire=interval).
type: int
version_added: '3.9.0'
column_case_sensitive:
description:
- The default is C(false).
Expand Down Expand Up @@ -429,6 +444,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=['now', 'never', 'default', 'interval'], no_log=True),
password_expire_interval=dict(type='int', required_if=[('password_expire', 'interval', True)], no_log=True),
)
module = AnsibleModule(
argument_spec=argument_spec,
Expand Down Expand Up @@ -466,6 +483,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))
Expand All @@ -476,6 +495,10 @@ def main():
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)

if password_expire_interval and password_expire_interval < 1:
module.fail_json(msg="password_expire_interval value \
should be positive number")

cursor = None
try:
if check_implicit_admin:
Expand Down Expand Up @@ -522,12 +545,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, attributes, tls_requires, module)
priv, append_privs, subtract_privs, attributes, 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, attributes, tls_requires, module)
priv, append_privs, subtract_privs, attributes, tls_requires, module,
password_expire, password_expire_interval)
changed = result['changed']
msg = result['msg']
password_changed = result['password_changed']
Expand All @@ -544,7 +569,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, attributes, tls_requires, reuse_existing_password, module)
priv, attributes, tls_requires, reuse_existing_password, module,
password_expire, password_expire_interval)
changed = result['changed']
password_changed = result['password_changed']
final_attributes = result['attributes']
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/targets/test_mysql_user/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@

- include_tasks: test_idempotency.yml

- include_tasks: test_password_expire.yml

# ============================================================
# Create user with no privileges and verify default privileges are assign
#
Expand Down
Loading

0 comments on commit 40af258

Please sign in to comment.