From 677fe1076dce6f4a0d8567dbd8dd4fc8691981f0 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 17 May 2018 11:34:13 -0400 Subject: [PATCH] User unexpire (#39758) * Allow negative values to expires to unexpire a user Fixes #20096 (cherry picked from commit 34f8080a19c09cd20ec9c045fca1e37ef74bb1e6) (cherry picked from commit 54619f70f4b79f121c5062d54e9732d3cbb24377) (cherry picked from commit 8c2fae27d6e2af810112032bb1dfef5459035b7e) (cherry picked from commit db1a32f8caa8c8b9f989baa65784d4b2b5cad1f8) * tweaked and normalized - also added tests, made checking resilient --- lib/ansible/modules/system/user.py | 109 +++++++++++-------- test/integration/targets/user/tasks/main.yml | 53 +++++++++ 2 files changed, 116 insertions(+), 46 deletions(-) diff --git a/lib/ansible/modules/system/user.py b/lib/ansible/modules/system/user.py index 2dd68c7937..a1bfa3a894 100644 --- a/lib/ansible/modules/system/user.py +++ b/lib/ansible/modules/system/user.py @@ -32,12 +32,12 @@ options: - Name of the user to create, remove or modify. required: true aliases: [ user ] - comment: - description: - - Optionally sets the description (aka I(GECOS)) of user account. uid: description: - Optionally sets the I(UID) of the user. + comment: + description: + - Optionally sets the description (aka I(GECOS)) of user account. hidden: required: false type: bool @@ -47,8 +47,7 @@ options: version_added: "2.6" non_unique: description: - - Optionally when used with the -u option, this option allows to - change the user ID to a non-unique value. + - Optionally when used with the -u option, this option allows to change the user ID to a non-unique value. type: bool default: "no" version_added: "1.1" @@ -67,16 +66,14 @@ options: now it should be able to accept YAML lists also. append: description: - - If C(yes), will only add groups, not set them to just the list - in I(groups). + - If C(yes), will only add groups, not set them to just the list in I(groups). type: bool default: "no" shell: description: - Optionally set the user's shell. - - On Mac OS X, before version 2.5, the default shell for non-system users was - /usr/bin/false. Since 2.5, the default shell for non-system users on - Mac OS X is /bin/bash. + - On Mac OS X, before version 2.5, the default shell for non-system users was /usr/bin/false. + Since 2.5, the default shell for non-system users on Mac OS X is /bin/bash. home: description: - Optionally set the user's home directory. @@ -98,39 +95,38 @@ options: create_home: description: - Unless set to C(no), a home directory will be made for the user - when the account is created or if the home directory does not - exist. + when the account is created or if the home directory does not exist. - Changed from C(createhome) to C(create_home) in version 2.5. type: bool default: 'yes' aliases: ['createhome'] move_home: description: - - If set to C(yes) when used with C(home=), attempt to move the - user's home directory to the specified directory if it isn't there - already. + - If set to C(yes) when used with C(home=), attempt to move the user's old home + directory to the specified directory if it isn't there already and the old home exists. type: bool default: "no" system: description: - - When creating an account, setting this to C(yes) makes the user a - system account. This setting cannot be changed on existing users. + - When creating an account C(state=present), setting this to C(yes) makes the user a system account. + This setting cannot be changed on existing users. type: bool default: "no" force: description: - - When used with C(state=absent), behavior is as with C(userdel --force). + - This only affects C(state=absent), it forces removal of the user and associated directories on supported platforms. + The behavior is the same as C(userdel --force), check the man page for C(userdel) on your system for details and support. + type: bool + default: "no" + remove: + description: + - This only affects C(state=absent), it attempts to remove directories associated with the user. + The behavior is the same as C(userdel --remove), check the man page for details and support. type: bool default: "no" login_class: description: - - Optionally sets the user's login class for FreeBSD, DragonFlyBSD, OpenBSD and - NetBSD systems. - remove: - description: - - When used with C(state=absent), behavior is as with C(userdel --remove). - type: bool - default: "no" + - Optionally sets the user's login class, a feature of most BSD OSs. generate_ssh_key: description: - Whether to generate a SSH key for the user in question. @@ -176,7 +172,8 @@ options: expires: description: - An expiry time for the user in epoch, it will be ignored on platforms that do not support this. - Currently supported on Linux, FreeBSD, and DragonFlyBSD. + Currently supported on GNU/Linux, FreeBSD, and DragonFlyBSD. + - Since version 2.6 you can remove the expiry time specify a negative value. Currently supported on GNU/Linux and FreeBSD. version_added: "1.9" password_lock: description: @@ -231,6 +228,12 @@ EXAMPLES = ''' shell: /bin/zsh groups: developers expires: 1422403387 + +- name: starting at version 2.6, modify user, remove expiry time + user: + name: james18 + expires: -1 + ''' import grp @@ -311,11 +314,11 @@ class User(object): if module.params['groups'] is not None: self.groups = ','.join(module.params['groups']) - if module.params['expires']: + if module.params['expires'] is not None: try: self.expires = time.gmtime(module.params['expires']) except Exception as e: - module.fail_json(msg="Invalid expires time %s: %s" % (self.expires, to_native(e))) + module.fail_json(msg="Invalid value for 'expires' %s: %s" % (self.expires, to_native(e))) if module.params['ssh_key_file'] is not None: self.ssh_file = module.params['ssh_key_file'] @@ -409,7 +412,7 @@ class User(object): cmd.append('-s') cmd.append(self.shell) - if self.expires: + if self.expires is not None: cmd.append('-e') cmd.append(time.strftime(self.DATE_FORMAT, self.expires)) @@ -532,17 +535,22 @@ class User(object): cmd.append('-s') cmd.append(self.shell) - if self.expires: - current_expires = self.user_password()[1] + if self.expires is not None: - # Convert days since Epoch to seconds since Epoch as struct_time - total_seconds = int(current_expires) * 86400 - current_expires = time.gmtime(total_seconds) + current_expires = int(self.user_password()[1]) - # Compare year, month, and day only - if current_expires[:3] != self.expires[:3]: - cmd.append('-e') - cmd.append(time.strftime(self.DATE_FORMAT, self.expires)) + if self.expires < time.gmtime(0): + if current_expires > 0: + cmd.append('-e') + cmd.append('') + else: + # Convert days since Epoch to seconds since Epoch as struct_time + current_expire_date = time.gmtime(current_expires * 86400) + + # Current expires is negative or we compare year, month, and day only + if current_expires <= 0 or current_expire_date[:3] != self.expires[:3]: + cmd.append('-e') + cmd.append(time.strftime(self.DATE_FORMAT, self.expires)) if self.password_lock: cmd.append('-L') @@ -647,7 +655,7 @@ class User(object): for line in open(self.SHADOWFILE).readlines(): if line.startswith('%s:' % self.name): passwd = line.split(':')[1] - expires = line.split(':')[self.SHADOWFILE_EXPIRE_INDEX] + expires = line.split(':')[self.SHADOWFILE_EXPIRE_INDEX] or -1 return passwd, expires def get_ssh_key_path(self): @@ -845,7 +853,7 @@ class FreeBsdUser(User): cmd.append('-L') cmd.append(self.login_class) - if self.expires: + if self.expires is not None: cmd.append('-e') cmd.append(time.strftime(self.DATE_FORMAT, self.expires)) @@ -946,13 +954,22 @@ class FreeBsdUser(User): new_groups = groups | set(current_groups) cmd.append(','.join(new_groups)) - if self.expires: - current_expires = time.gmtime(int(self.user_password()[1])) + if self.expires is not None: - # Compare year, month, and day only - if current_expires[:3] != self.expires[:3]: - cmd.append('-e') - cmd.append(time.strftime(self.DATE_FORMAT, self.expires)) + current_expires = int(self.user_password()[1]) + + if self.expires < time.gmtime(0): + if current_expires > 0: + cmd.append('-e') + cmd.append('0') + else: + # Convert days since Epoch to seconds since Epoch as struct_time + current_expire_date = time.gmtime(current_expires) + + # Current expires is negative or we compare year, month, and day only + if current_expires <= 0 or current_expire_date[:3] != self.expires[:3]: + cmd.append('-e') + cmd.append(time.strftime(self.DATE_FORMAT, self.expires)) # modify the user if cmd will do anything if cmd_len != len(cmd): diff --git a/test/integration/targets/user/tasks/main.yml b/test/integration/targets/user/tasks/main.yml index c013a1d5b1..ee20aac5be 100644 --- a/test/integration/targets/user/tasks/main.yml +++ b/test/integration/targets/user/tasks/main.yml @@ -246,3 +246,56 @@ - name: Restore original timezone - {{ original_timezone.diff.before.name }} timezone: name: "{{ original_timezone.diff.before.name }}" + + +- name: Unexpire user + user: + name: ansibulluser + state: present + expires: -1 + register: user_test_expires3 + +- name: Verify un expiration date for Linux + block: + - name: LINUX | Get expiration date for ansibulluser + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be empty or -1, not {{getent_shadow['ansibulluser'][6]}}" + that: + - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] < 0 + when: ansible_os_family in ['RedHat', 'Debian', 'Suse'] + +- name: Verify un expiration date for linux/BSD + block: + - name: Unexpire user again to check for change + user: + name: ansibulluser + state: present + expires: -1 + register: user_test_expires4 + + - name: Ensure first expiration reported a change and second did not + assert: + msg: The second run of the expiration removal task reported a change when it should not + that: + - user_test_expires3 is changed + - user_test_expires4 is not changed + when: ansible_os_family in ['RedHat', 'Debian', 'Suse', 'FreeBSD'] + +- name: Verify un expiration date for BSD + block: + - name: BSD | Get expiration date for ansibulluser + shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' + changed_when: no + register: bsd_account_expiration + + - name: BSD | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be '0', not {{bsd_account_expiration.stdout}}" + that: + - bsd_account_expiration.stdout == '0' + when: ansible_os_family == 'FreeBSD'