diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/general/plugins/modules/homectl.py | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/general/plugins/modules/homectl.py')
-rw-r--r-- | ansible_collections/community/general/plugins/modules/homectl.py | 658 |
1 files changed, 658 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/modules/homectl.py b/ansible_collections/community/general/plugins/modules/homectl.py new file mode 100644 index 000000000..301e388d3 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/homectl.py @@ -0,0 +1,658 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, James Livulpi +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: homectl +author: + - "James Livulpi (@jameslivulpi)" +short_description: Manage user accounts with systemd-homed +version_added: 4.4.0 +description: + - Manages a user's home directory managed by systemd-homed. +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + name: + description: + - The user name to create, remove, or update. + required: true + aliases: [ 'user', 'username' ] + type: str + password: + description: + - Set the user's password to this. + - Homed requires this value to be in cleartext on user creation and updating a user. + - The module takes the password and generates a password hash in SHA-512 with 10000 rounds of salt generation using crypt. + - See U(https://systemd.io/USER_RECORD/). + - This is required for I(state=present). When an existing user is updated this is checked against the stored hash in homed. + type: str + state: + description: + - The operation to take on the user. + choices: [ 'absent', 'present' ] + default: present + type: str + storage: + description: + - Indicates the storage mechanism for the user's home directory. + - If the storage type is not specified, ``homed.conf(5)`` defines which default storage to use. + - Only used when a user is first created. + choices: [ 'classic', 'luks', 'directory', 'subvolume', 'fscrypt', 'cifs' ] + type: str + disksize: + description: + - The intended home directory disk space. + - Human readable value such as C(10G), C(10M), or C(10B). + type: str + resize: + description: + - When used with I(disksize) this will attempt to resize the home directory immediately. + default: false + type: bool + realname: + description: + - The user's real ('human') name. + - This can also be used to add a comment to maintain compatibility with C(useradd). + aliases: [ 'comment' ] + type: str + realm: + description: + - The 'realm' a user is defined in. + type: str + email: + description: + - The email address of the user. + type: str + location: + description: + - A free-form location string describing the location of the user. + type: str + iconname: + description: + - The name of an icon picked by the user, for example for the purpose of an avatar. + - Should follow the semantics defined in the Icon Naming Specification. + - See U(https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html) for specifics. + type: str + homedir: + description: + - Path to use as home directory for the user. + - This is the directory the user's home directory is mounted to while the user is logged in. + - This is not where the user's data is actually stored, see I(imagepath) for that. + - Only used when a user is first created. + type: path + imagepath: + description: + - Path to place the user's home directory. + - See U(https://www.freedesktop.org/software/systemd/man/homectl.html#--image-path=PATH) for more information. + - Only used when a user is first created. + type: path + uid: + description: + - Sets the UID of the user. + - If using I(gid) homed requires the value to be the same. + - Only used when a user is first created. + type: int + gid: + description: + - Sets the gid of the user. + - If using I(uid) homed requires the value to be the same. + - Only used when a user is first created. + type: int + mountopts: + description: + - String separated by comma each indicating mount options for a users home directory. + - Valid options are C(nosuid), C(nodev) or C(noexec). + - Homed by default uses C(nodev) and C(nosuid) while C(noexec) is off. + type: str + umask: + description: + - Sets the umask for the user's login sessions + - Value from C(0000) to C(0777). + type: int + memberof: + description: + - String separated by comma each indicating a UNIX group this user shall be a member of. + - Groups the user should be a member of should be supplied as comma separated list. + aliases: [ 'groups' ] + type: str + skeleton: + description: + - The absolute path to the skeleton directory to populate a new home directory from. + - This is only used when a home directory is first created. + - If not specified homed by default uses C(/etc/skel). + aliases: [ 'skel' ] + type: path + shell: + description: + - Shell binary to use for terminal logins of given user. + - If not specified homed by default uses C(/bin/bash). + type: str + environment: + description: + - String separated by comma each containing an environment variable and its value to + set for the user's login session, in a format compatible with ``putenv()``. + - Any environment variable listed here is automatically set by pam_systemd for all + login sessions of the user. + aliases: [ 'setenv' ] + type: str + timezone: + description: + - Preferred timezone to use for the user. + - Should be a tzdata compatible location string such as C(America/New_York). + type: str + locked: + description: + - Whether the user account should be locked or not. + type: bool + language: + description: + - The preferred language/locale for the user. + - This should be in a format compatible with the C($LANG) environment variable. + type: str + passwordhint: + description: + - Password hint for the given user. + type: str + sshkeys: + description: + - String separated by comma each listing a SSH public key that is authorized to access the account. + - The keys should follow the same format as the lines in a traditional C(~/.ssh/authorized_key) file. + type: str + notbefore: + description: + - A time since the UNIX epoch before which the record should be considered invalid for the purpose of logging in. + type: int + notafter: + description: + - A time since the UNIX epoch after which the record should be considered invalid for the purpose of logging in. + type: int +''' + +EXAMPLES = ''' +- name: Add the user 'james' + community.general.homectl: + name: johnd + password: myreallysecurepassword1! + state: present + +- name: Add the user 'alice' with a zsh shell, uid of 1000, and gid of 2000 + community.general.homectl: + name: alice + password: myreallysecurepassword1! + state: present + shell: /bin/zsh + uid: 1000 + gid: 1000 + +- name: Modify an existing user 'frank' to have 10G of diskspace and resize usage now + community.general.homectl: + name: frank + password: myreallysecurepassword1! + state: present + disksize: 10G + resize: true + +- name: Remove an existing user 'janet' + community.general.homectl: + name: janet + state: absent +''' + +RETURN = ''' +data: + description: A json dictionary returned from C(homectl inspect -j). + returned: success + type: dict + sample: { + "data": { + "binding": { + "e9ed2a5b0033427286b228e97c1e8343": { + "fileSystemType": "btrfs", + "fileSystemUuid": "7bd59491-2812-4642-a492-220c3f0c6c0b", + "gid": 60268, + "imagePath": "/home/james.home", + "luksCipher": "aes", + "luksCipherMode": "xts-plain64", + "luksUuid": "7f05825a-2c38-47b4-90e1-f21540a35a81", + "luksVolumeKeySize": 32, + "partitionUuid": "5a906126-d3c8-4234-b230-8f6e9b427b2f", + "storage": "luks", + "uid": 60268 + } + }, + "diskSize": 3221225472, + "disposition": "regular", + "lastChangeUSec": 1641941238208691, + "lastPasswordChangeUSec": 1641941238208691, + "privileged": { + "hashedPassword": [ + "$6$ov9AKni.trf76inT$tTtfSyHgbPTdUsG0CvSSQZXGqFGdHKQ9Pb6e0BTZhDmlgrL/vA5BxrXduBi8u/PCBiYUffGLIkGhApjKMK3bV." + ] + }, + "signature": [ + { + "data": "o6zVFbymcmk4YTVaY6KPQK23YCp+VkXdGEeniZeV1pzIbFzoaZBvVLPkNKMoPAQbodY5BYfBtuy41prNL78qAg==", + "key": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbs7ELeiEYBxkUQhxZ+5NGyu6J7gTtZtZ5vmIw3jowcY=\n-----END PUBLIC KEY-----\n" + } + ], + "status": { + "e9ed2a5b0033427286b228e97c1e8343": { + "diskCeiling": 21845405696, + "diskFloor": 268435456, + "diskSize": 3221225472, + "service": "io.systemd.Home", + "signedLocally": true, + "state": "inactive" + } + }, + "userName": "james", + } + } +''' + +import crypt +import json +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import jsonify +from ansible.module_utils.common.text.formatters import human_to_bytes + + +class Homectl(object): + '''#TODO DOC STRINGS''' + + def __init__(self, module): + self.module = module + self.state = module.params['state'] + self.name = module.params['name'] + self.password = module.params['password'] + self.storage = module.params['storage'] + self.disksize = module.params['disksize'] + self.resize = module.params['resize'] + self.realname = module.params['realname'] + self.realm = module.params['realm'] + self.email = module.params['email'] + self.location = module.params['location'] + self.iconname = module.params['iconname'] + self.homedir = module.params['homedir'] + self.imagepath = module.params['imagepath'] + self.uid = module.params['uid'] + self.gid = module.params['gid'] + self.umask = module.params['umask'] + self.memberof = module.params['memberof'] + self.skeleton = module.params['skeleton'] + self.shell = module.params['shell'] + self.environment = module.params['environment'] + self.timezone = module.params['timezone'] + self.locked = module.params['locked'] + self.passwordhint = module.params['passwordhint'] + self.sshkeys = module.params['sshkeys'] + self.language = module.params['language'] + self.notbefore = module.params['notbefore'] + self.notafter = module.params['notafter'] + self.mountopts = module.params['mountopts'] + + self.result = {} + + # Cannot run homectl commands if service is not active + def homed_service_active(self): + is_active = True + cmd = ['systemctl', 'show', 'systemd-homed.service', '-p', 'ActiveState'] + rc, show_service_stdout, stderr = self.module.run_command(cmd) + if rc == 0: + state = show_service_stdout.rsplit('=')[1] + if state.strip() != 'active': + is_active = False + return is_active + + def user_exists(self): + exists = False + valid_pw = False + # Get user properties if they exist in json + rc, stdout, stderr = self.get_user_metadata() + if rc == 0: + exists = True + # User exists now compare password given with current hashed password stored in the user metadata. + if self.state != 'absent': # Don't need checking on remove user + stored_pwhash = json.loads(stdout)['privileged']['hashedPassword'][0] + if self._check_password(stored_pwhash): + valid_pw = True + return exists, valid_pw + + def create_user(self): + record = self.create_json_record(create=True) + cmd = [self.module.get_bin_path('homectl', True)] + cmd.append('create') + cmd.append('--identity=-') # Read the user record from standard input. + return self.module.run_command(cmd, data=record) + + def _hash_password(self, password): + method = crypt.METHOD_SHA512 + salt = crypt.mksalt(method, rounds=10000) + pw_hash = crypt.crypt(password, salt) + return pw_hash + + def _check_password(self, pwhash): + hash = crypt.crypt(self.password, pwhash) + return pwhash == hash + + def remove_user(self): + cmd = [self.module.get_bin_path('homectl', True)] + cmd.append('remove') + cmd.append(self.name) + return self.module.run_command(cmd) + + def prepare_modify_user_command(self): + record = self.create_json_record() + cmd = [self.module.get_bin_path('homectl', True)] + cmd.append('update') + cmd.append(self.name) + cmd.append('--identity=-') # Read the user record from standard input. + # Resize disksize now resize = true + # This is not valid in user record (json) and requires it to be passed on command. + if self.disksize and self.resize: + cmd.append('--and-resize') + cmd.append('true') + self.result['changed'] = True + return cmd, record + + def get_user_metadata(self): + cmd = [self.module.get_bin_path('homectl', True)] + cmd.append('inspect') + cmd.append(self.name) + cmd.append('-j') + cmd.append('--no-pager') + rc, stdout, stderr = self.module.run_command(cmd) + return rc, stdout, stderr + + # Build up dictionary to jsonify for homectl commands. + def create_json_record(self, create=False): + record = {} + user_metadata = {} + self.result['changed'] = False + # Get the current user record if not creating a new user record. + if not create: + rc, user_metadata, stderr = self.get_user_metadata() + user_metadata = json.loads(user_metadata) + # Remove elements that are not meant to be updated from record. + # These are always part of the record when a user exists. + user_metadata.pop('signature', None) + user_metadata.pop('binding', None) + user_metadata.pop('status', None) + # Let last change Usec be updated by homed when command runs. + user_metadata.pop('lastChangeUSec', None) + # Now only change fields that are called on leaving whats currently in the record intact. + record = user_metadata + + record['userName'] = self.name + record['secret'] = {'password': [self.password]} + + if create: + password_hash = self._hash_password(self.password) + record['privileged'] = {'hashedPassword': [password_hash]} + self.result['changed'] = True + + if self.uid and self.gid and create: + record['uid'] = self.uid + record['gid'] = self.gid + self.result['changed'] = True + + if self.memberof: + member_list = list(self.memberof.split(',')) + if member_list != record.get('memberOf', [None]): + record['memberOf'] = member_list + self.result['changed'] = True + + if self.realname: + if self.realname != record.get('realName'): + record['realName'] = self.realname + self.result['changed'] = True + + # Cannot update storage unless were creating a new user. + # See 'Fields in the binding section' at https://systemd.io/USER_RECORD/ + if self.storage and create: + record['storage'] = self.storage + self.result['changed'] = True + + # Cannot update homedir unless were creating a new user. + # See 'Fields in the binding section' at https://systemd.io/USER_RECORD/ + if self.homedir and create: + record['homeDirectory'] = self.homedir + self.result['changed'] = True + + # Cannot update imagepath unless were creating a new user. + # See 'Fields in the binding section' at https://systemd.io/USER_RECORD/ + if self.imagepath and create: + record['imagePath'] = self.imagepath + self.result['changed'] = True + + if self.disksize: + # convert humand readble to bytes + if self.disksize != record.get('diskSize'): + record['diskSize'] = human_to_bytes(self.disksize) + self.result['changed'] = True + + if self.realm: + if self.realm != record.get('realm'): + record['realm'] = self.realm + self.result['changed'] = True + + if self.email: + if self.email != record.get('emailAddress'): + record['emailAddress'] = self.email + self.result['changed'] = True + + if self.location: + if self.location != record.get('location'): + record['location'] = self.location + self.result['changed'] = True + + if self.iconname: + if self.iconname != record.get('iconName'): + record['iconName'] = self.iconname + self.result['changed'] = True + + if self.skeleton: + if self.skeleton != record.get('skeletonDirectory'): + record['skeletonDirectory'] = self.skeleton + self.result['changed'] = True + + if self.shell: + if self.shell != record.get('shell'): + record['shell'] = self.shell + self.result['changed'] = True + + if self.umask: + if self.umask != record.get('umask'): + record['umask'] = self.umask + self.result['changed'] = True + + if self.environment: + if self.environment != record.get('environment', [None]): + record['environment'] = list(self.environment.split(',')) + self.result['changed'] = True + + if self.timezone: + if self.timezone != record.get('timeZone'): + record['timeZone'] = self.timezone + self.result['changed'] = True + + if self.locked: + if self.locked != record.get('locked'): + record['locked'] = self.locked + self.result['changed'] = True + + if self.passwordhint: + if self.passwordhint != record.get('privileged', {}).get('passwordHint'): + record['privileged']['passwordHint'] = self.passwordhint + self.result['changed'] = True + + if self.sshkeys: + if self.sshkeys != record.get('privileged', {}).get('sshAuthorizedKeys'): + record['privileged']['sshAuthorizedKeys'] = list(self.sshkeys.split(',')) + self.result['changed'] = True + + if self.language: + if self.locked != record.get('preferredLanguage'): + record['preferredLanguage'] = self.language + self.result['changed'] = True + + if self.notbefore: + if self.locked != record.get('notBeforeUSec'): + record['notBeforeUSec'] = self.notbefore + self.result['changed'] = True + + if self.notafter: + if self.locked != record.get('notAfterUSec'): + record['notAfterUSec'] = self.notafter + self.result['changed'] = True + + if self.mountopts: + opts = list(self.mountopts.split(',')) + if 'nosuid' in opts: + if record.get('mountNoSuid') is not True: + record['mountNoSuid'] = True + self.result['changed'] = True + else: + if record.get('mountNoSuid') is not False: + record['mountNoSuid'] = False + self.result['changed'] = True + + if 'nodev' in opts: + if record.get('mountNoDevices') is not True: + record['mountNoDevices'] = True + self.result['changed'] = True + else: + if record.get('mountNoDevices') is not False: + record['mountNoDevices'] = False + self.result['changed'] = True + + if 'noexec' in opts: + if record.get('mountNoExecute') is not True: + record['mountNoExecute'] = True + self.result['changed'] = True + else: + if record.get('mountNoExecute') is not False: + record['mountNoExecute'] = False + self.result['changed'] = True + + return jsonify(record) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(type='str', default='present', choices=['absent', 'present']), + name=dict(type='str', required=True, aliases=['user', 'username']), + password=dict(type='str', no_log=True), + storage=dict(type='str', choices=['classic', 'luks', 'directory', 'subvolume', 'fscrypt', 'cifs']), + disksize=dict(type='str'), + resize=dict(type='bool', default=False), + realname=dict(type='str', aliases=['comment']), + realm=dict(type='str'), + email=dict(type='str'), + location=dict(type='str'), + iconname=dict(type='str'), + homedir=dict(type='path'), + imagepath=dict(type='path'), + uid=dict(type='int'), + gid=dict(type='int'), + umask=dict(type='int'), + environment=dict(type='str', aliases=['setenv']), + timezone=dict(type='str'), + memberof=dict(type='str', aliases=['groups']), + skeleton=dict(type='path', aliases=['skel']), + shell=dict(type='str'), + locked=dict(type='bool'), + passwordhint=dict(type='str', no_log=True), + sshkeys=dict(type='str', no_log=True), + language=dict(type='str'), + notbefore=dict(type='int'), + notafter=dict(type='int'), + mountopts=dict(type='str'), + ), + supports_check_mode=True, + + required_if=[ + ('state', 'present', ['password']), + ('resize', True, ['disksize']), + ] + ) + + homectl = Homectl(module) + homectl.result['state'] = homectl.state + + # First we need to make sure homed service is active + if not homectl.homed_service_active(): + module.fail_json(msg='systemd-homed.service is not active') + + # handle removing user + if homectl.state == 'absent': + user_exists, valid_pwhash = homectl.user_exists() + if user_exists: + if module.check_mode: + module.exit_json(changed=True) + rc, stdout, stderr = homectl.remove_user() + if rc != 0: + module.fail_json(name=homectl.name, msg=stderr, rc=rc) + homectl.result['changed'] = True + homectl.result['rc'] = rc + homectl.result['msg'] = 'User %s removed!' % homectl.name + else: + homectl.result['changed'] = False + homectl.result['msg'] = 'User does not exist!' + + # Handle adding a user + if homectl.state == 'present': + user_exists, valid_pwhash = homectl.user_exists() + if not user_exists: + if module.check_mode: + module.exit_json(changed=True) + rc, stdout, stderr = homectl.create_user() + if rc != 0: + module.fail_json(name=homectl.name, msg=stderr, rc=rc) + rc, user_metadata, stderr = homectl.get_user_metadata() + homectl.result['data'] = json.loads(user_metadata) + homectl.result['rc'] = rc + homectl.result['msg'] = 'User %s created!' % homectl.name + else: + if valid_pwhash: + # Run this to see if changed would be True or False which is useful for check_mode + cmd, record = homectl.prepare_modify_user_command() + else: + # User gave wrong password fail with message + homectl.result['changed'] = False + homectl.result['msg'] = 'User exists but password is incorrect!' + module.fail_json(**homectl.result) + + if module.check_mode: + module.exit_json(**homectl.result) + + # Now actually modify the user if changed was set to true at any point. + if homectl.result['changed']: + rc, stdout, stderr = module.run_command(cmd, data=record) + if rc != 0: + module.fail_json(name=homectl.name, msg=stderr, rc=rc, changed=False) + rc, user_metadata, stderr = homectl.get_user_metadata() + homectl.result['data'] = json.loads(user_metadata) + homectl.result['rc'] = rc + if homectl.result['changed']: + homectl.result['msg'] = 'User %s modified' % homectl.name + + module.exit_json(**homectl.result) + + +if __name__ == '__main__': + main() |