diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 9364025..7cf2177 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -202,7 +202,7 @@ def user_add(cursor, user, host, host_all, password, encrypted, cursor.execute(*mogrify(*query_with_args_and_tls_requires)) if password_expire: - if not impl.supports_identified_by_password(cursor): + 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) @@ -315,9 +315,12 @@ def user_mod(cursor, user, host, host_all, password, encrypted, 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): + 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 @@ -968,14 +971,19 @@ def set_password_expire(cursor, user, host, password_expire, password_expire_int 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) + statment = "PASSWORD EXPIRE INTERVAL %d DAY" % (password_expire_interval) + elif password_expire.lower() == "now": + statment = "PASSWORD EXPIRE" + if host: + params = (user, host) + query = ["ALTER USER %s@%s"] + else: + params = (user,) + query = ["ALTER USER %s"] + + query.append(statment) + query = ' '.join(query) + cursor.execute(query, params) def get_password_expiration_policy(cursor, user, host, maria_role=False): @@ -991,7 +999,7 @@ def get_password_expiration_policy(cursor, user, host, maria_role=False): policy (int): Current users password policy. """ if not maria_role: - statment = "SELECT password_lifetime FROM mysql.user \ + statment = "SELECT IFNULL(password_lifetime, -1) FROM mysql.user \ WHERE User = %s AND Host = %s", (user, host) else: statment = "SELECT JSON_EXTRACT(Priv, '$.password_lifetime') AS password_lifetime \ @@ -999,11 +1007,28 @@ def get_password_expiration_policy(cursor, user, host, maria_role=False): WHERE User = %s AND Host = %s", (user, host) cursor.execute(*statment) policy = cursor.fetchone()[0] - if not policy: - policy = -1 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. + """ + statment = "SELECT password_expired FROM mysql.user \ + WHERE User = %s AND Host = %s", (user, host) + cursor.execute(*statment) + expired = cursor.fetchone()[0] + if str(expired) == "Y": + return True + return False + 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 d6ab0fa..8075fbb 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -159,13 +159,13 @@ options: 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) + - C(interval) password will expire in days which is defined in I(password_expire_interval). + - C(now) password will expire immediately. type: str - choices: [ never, default, interval ] + choices: [ now, never, default, interval ] 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. + - number of days password will expire. Used with I(password_expire: iterval) type: int column_case_sensitive: @@ -427,7 +427,7 @@ 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'], no_log=True), + password_expire=dict(type='str', choices=['now', 'never', 'default', 'interval'], no_log=True), password_expire_interval=dict(type='int', no_log=True), ) module = AnsibleModule( @@ -481,6 +481,11 @@ def main(): module.fail_json(msg="password_expire value interval \ should be used with password_expire_interval") + if password_expire_interval: + if password_expire_interval < 1: + module.fail_json(msg="password_expire_interval value \ + should be positive number") + cursor = None try: if check_implicit_admin: diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index 4816805..f7d70a2 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -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 # diff --git a/tests/integration/targets/test_mysql_user/tasks/test_password_expire.yml b/tests/integration/targets/test_mysql_user/tasks/test_password_expire.yml new file mode 100644 index 0000000..63841a4 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_password_expire.yml @@ -0,0 +1,113 @@ +--- +# Tests scenarios for password_expire + +- vars: + mysql_parameters: + login_user: "{{ mysql_user }}" + login_password: "{{ mysql_password }}" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + password_expire: "{{ password_expire }}" + + block: + - include_tasks: utils/assert_user_password_expire.yml + vars: + username: "{{ item.username }}" + host: "%" + password_expire: "{{ item.password_expire }}" + password: "{{ user_password_1 }}" + expect_change: "{{ item.expect_change }}" + expect_password_expire_change: "{{ item.expect_password_expire_change }}" + expected_password_lifetime: "{{ item.expected_password_lifetime }}" + password_expire_interval: "{{ item.password_expire_interval | default(omit) }}" + expected_password_expired: "{{ item.expected_password_expired }}" + + loop: + # all variants set the password when nothing exists + # never expires + - username: "{{ user_name_1 }}" + password_expire: never + expect_change: true + expected_password_lifetime: "0" + expected_password_expired: "N" + # expires ussing default policy + - username: "{{ user_name_2 }}" + password_expire: default + expect_change: true + expected_password_lifetime: "-1" + expected_password_expired: "N" + # expires ussing interval + - username: "{{ user_name_3 }}" + password_expire: interval + password_expire_interval: "10" + expect_change: true + expected_password_lifetime: "10" + expected_password_expired: "N" + + # assert idempotency + - username: "{{ user_name_1 }}" + password_expire: never + expect_change: false + expected_password_lifetime: "0" + expected_password_expired: "N" + - username: "{{ user_name_2 }}" + password_expire: default + expect_change: false + expected_password_lifetime: "-1" + expected_password_expired: "N" + - username: "{{ user_name_3 }}" + password_expire: interval + password_expire_interval: "10" + expect_change: false + expected_password_lifetime: "10" + expected_password_expired: "N" + + # assert change is made + - username: "{{ user_name_3 }}" + password_expire: never + expect_change: true + expected_password_lifetime: "0" + expected_password_expired: "N" + - username: "{{ user_name_1 }}" + password_expire: default + expect_change: true + expected_password_lifetime: "-1" + expected_password_expired: "N" + - username: "{{ user_name_2 }}" + password_expire: interval + password_expire_interval: "100" + expect_change: true + expected_password_lifetime: "100" + expected_password_expired: "N" + + # assert password expires now + - username: "{{ user_name_1 }}" + password_expire: now + expect_change: true + expected_password_lifetime: "-1" # password lifetime should be the same + expected_password_expired: "Y" + - username: "{{ user_name_2 }}" + password_expire: now + expect_change: true + expected_password_lifetime: "100" # password lifetime should be the same + expected_password_expired: "Y" + + # assert idempotency password expires now + - username: "{{ user_name_1 }}" + password_expire: now + expect_change: false + expected_password_lifetime: "-1" # password lifetime should be the same + expected_password_expired: "Y" + - username: "{{ user_name_2 }}" + password_expire: now + expect_change: false + expected_password_lifetime: "100" # password lifetime should be the same + expected_password_expired: "Y" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ item.username }}" + loop: + - username: "{{ user_name_1 }}" + - username: "{{ user_name_2 }}" + - username: "{{ user_name_3 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password_expire.yml b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password_expire.yml new file mode 100644 index 0000000..29f0b8c --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password_expire.yml @@ -0,0 +1,65 @@ +--- +- name: Utils | Assert user password_expire | Create modify {{ username }} with password_expire + mysql_user: + login_user: "{{ mysql_parameters.login_user }}" + login_password: "{{ mysql_parameters.login_password }}" + login_host: "{{ mysql_parameters.login_host }}" + login_port: "{{ mysql_parameters.login_port }}" + state: present + name: "{{ username }}" + host: "{{ host }}" + password: "{{ password }}" + password_expire: "{{ password_expire }}" + password_expire_interval: "{{ password_expire_interval | default(omit) }}" + register: result + +- name: Utils | Assert user password_expire | Assert a change occurred + assert: + that: "result.changed == {{ expect_change }}" + +- name: Utils | Assert user password_lifetime | Query user '{{ username }}' + command: '{{ mysql_command }} -BNe "SELECT IFNULL(password_lifetime, -1) FROM mysql.user where user=''{{ username }}'' and host=''{{ host }}''"' + register: password_lifetime + when: + - db_engine == 'mysql' + - db_version is version('5.7.0', '>=') + +- name: Utils | Assert user password_lifetime | Assert password_lifetime is in user stdout + assert: + that: + - "'{{ expected_password_lifetime }}' in password_lifetime.stdout_lines" + when: + - db_engine == 'mysql' + - db_version is version('5.7.0', '>=') + +- name: Utils | Assert user password_lifetime | Query user '{{ username }}' + command: + "{{ mysql_command }} -BNe \"SELECT JSON_EXTRACT(Priv, '$.password_lifetime') AS password_lifetime \ + FROM mysql.global_priv \ + WHERE user='{{ username }}' and host='{{ host }}'\"" + register: password_lifetime + when: + - db_engine == 'mariadb' + - db_version is version('10.4.3', '>=') + +- name: Utils | Assert user password_lifetime | Assert password_lifetime is in user stdout + assert: + that: + - "'{{ expected_password_lifetime }}' in password_lifetime.stdout_lines" + when: + - db_engine == 'mariadb' + - db_version is version('10.4.3', '>=') + +- name: Utils | Assert user password_expired | Query user '{{ username }}' + command: "{{ mysql_command }} -BNe \"SELECT password_expired FROM mysql.user \ + WHERE user='{{ username }}' and host='{{ host }}'\"" + register: password_expired + when: (db_engine == 'mysql' and db_version is version('5.7.0', '>=')) or + (db_engine == 'mariadb' and db_version is version('10.4.3', '>=')) + +- name: Utils | Assert user password_expired | Assert password_expired is in user stdout + assert: + that: + - "'{{ expected_password_expired }}' in password_expired.stdout_lines" + when: (db_engine == 'mysql' and db_version is version('5.7.0', '>=')) or + (db_engine == 'mariadb' and db_version is version('10.4.3', '>='))