diff --git a/.travis.yml b/.travis.yml index ed64b06f64f..690f1b3609e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -114,7 +114,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|cloud/atomic/atomic_.*\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|cloud/atomic/atomic_.*\.py|univention/.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') diff --git a/univention/__init__.py b/univention/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/univention/udm_user.py b/univention/udm_user.py new file mode 100644 index 00000000000..0654c54aacd --- /dev/null +++ b/univention/udm_user.py @@ -0,0 +1,530 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright (c) 2016, Adfinis SyGroup AG +# Tobias Rueetschi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.univention_umc import ( + umc_module_for_add, + umc_module_for_edit, + ldap_search, + base_dn, +) +from datetime import date +from dateutil.relativedelta import relativedelta +import crypt + + +DOCUMENTATION = ''' +--- +module: udm_user +version_added: "2.2" +author: "Tobias Rueetschi (@2-B)" +short_description: Manage posix users on a univention corporate server +description: + - "This module allows to manage posix users on a univention corporate server (UCS). + It uses the python API of the UCS to create a new object or edit it." +requirements: + - Python >= 2.6 +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the user is present or not. + username: + required: true + description: + - User name + aliases: ['name'] + firstname: + required: false + description: + - First name. Required if C(state=present). + lastname: + required: false + description: + - Last name. Required if C(state=present). + password: + required: false + default: None + description: + - Password. Required if C(state=present). + birthday: + required: false + default: None + description: + - Birthday + city: + required: false + default: None + description: + - City of users business address. + country: + required: false + default: None + description: + - Country of users business address. + departmentNumber: + required: false + default: None + description: + - Department number of users business address. + description: + required: false + default: None + description: + - Description (not gecos) + displayName: + required: false + default: None + description: + - Display name (not gecos) + email: + required: false + default: [''] + description: + - A list of e-mail addresses. + employeeNumber: + required: false + default: None + description: + - Employee number + employeeType: + required: false + default: None + description: + - Employee type + gecos: + required: false + default: None + description: + - GECOS + groups: + required: false + default: [] + description: + - "POSIX groups, the LDAP DNs of the groups will be found with the + LDAP filter for each group as $GROUP: + C((&(objectClass=posixGroup)(cn=$GROUP)))." + homeShare: + required: false + default: None + description: + - "Home NFS share. Must be a LDAP DN, e.g. + C(cn=home,cn=shares,ou=school,dc=example,dc=com)." + homeSharePath: + required: false + default: None + description: + - Path to home NFS share, inside the homeShare. + homeTelephoneNumber: + required: false + default: [] + description: + - List of private telephone numbers. + homedrive: + required: false + default: None + description: + - Windows home drive, e.g. C("H:"). + mailAlternativeAddress: + required: false + default: [] + description: + - List of alternative e-mail addresses. + mailHomeServer: + required: false + default: None + description: + - FQDN of mail server + mailPrimaryAddress: + required: false + default: None + description: + - Primary e-mail address + mobileTelephoneNumber: + required: false + default: [] + description: + - Mobile phone number + organisation: + required: false + default: None + description: + - Organisation + pagerTelephonenumber: + required: false + default: [] + description: + - List of pager telephone numbers. + phone: + required: false + default: [] + description: + - List of telephone numbers. + postcode: + required: false + default: None + description: + - Postal code of users business address. + primaryGroup: + required: false + default: cn=Domain Users,cn=groups,$LDAP_BASE_DN + description: + - Primary group. This must be the group LDAP DN. + profilepath: + required: false + default: None + description: + - Windows profile directory + pwdChangeNextLogin: + required: false + default: None + choices: [ '0', '1' ] + description: + - Change password on next login. + roomNumber: + required: false + default: None + description: + - Room number of users business address. + sambaPrivileges: + required: false + default: [] + description: + - "Samba privilege, like allow printer administration, do domain + join." + sambaUserWorkstations: + required: false + default: [] + description: + - Allow the authentication only on this Microsoft Windows host. + sambahome: + required: false + default: None + description: + - Windows home path, e.g. C('\\\\$FQDN\\$USERNAME'). + scriptpath: + required: false + default: None + description: + - Windows logon script. + secretary: + required: false + default: [] + description: + - A list of superiors as LDAP DNs. + serviceprovider: + required: false + default: [''] + description: + - Enable user for the following service providers. + shell: + required: false + default: '/bin/bash' + description: + - Login shell + street: + required: false + default: None + description: + - Street of users business address. + title: + required: false + default: None + description: + - Title, e.g. C(Prof.). + unixhome: + required: false + default: '/home/$USERNAME' + description: + - Unix home directory + userexpiry: + required: false + default: Today + 1 year + description: + - Account expiry date, e.g. C(1999-12-31). + position: + required: false + default: '' + description: + - "Define the whole position of users object inside the LDAP tree, e.g. + C(cn=employee,cn=users,ou=school,dc=example,dc=com)." + ou: + required: false + default: '' + description: + - "Organizational Unit inside the LDAP Base DN, e.g. C(school) for + LDAP OU C(ou=school,dc=example,dc=com)." + subpath: + required: false + default: 'cn=users' + description: + - "LDAP subpath inside the organizational unit, e.g. + C(cn=teachers,cn=users) for LDAP container + C(cn=teachers,cn=users,dc=example,dc=com)." +''' + + +EXAMPLES = ''' +# Create a user on a UCS +- udm_user: name=FooBar + password=secure_password + firstname=Foo + lastname=Bar + +# Create a user with the DN +# C(uid=foo,cn=teachers,cn=users,ou=school,dc=school,dc=example,dc=com) +- udm_user: name=foo + password=secure_password + firstname=Foo + lastname=Bar + ou=school + subpath='cn=teachers,cn=users' +# or define the position +- udm_user: name=foo + password=secure_password + firstname=Foo + lastname=Bar + position='cn=teachers,cn=users,ou=school,dc=school,dc=example,dc=com' +''' + + +RETURN = '''# ''' + + +def main(): + expiry = date.strftime(date.today()+relativedelta(years=1), "%Y-%m-%d") + module = AnsibleModule( + argument_spec = dict( + birthday = dict(default=None, + type='str'), + city = dict(default=None, + type='str'), + country = dict(default=None, + type='str'), + departmentNumber = dict(default=None, + type='str'), + description = dict(default=None, + type='str'), + displayName = dict(default=None, + type='str'), + email = dict(default=[''], + type='list'), + employeeNumber = dict(default=None, + type='str'), + employeeType = dict(default=None, + type='str'), + firstname = dict(default=None, + type='str'), + gecos = dict(default=None, + type='str'), + groups = dict(default=[], + type='list'), + homeShare = dict(default=None, + type='str'), + homeSharePath = dict(default=None, + type='str'), + homeTelephoneNumber = dict(default=[], + type='list'), + homedrive = dict(default=None, + type='str'), + lastname = dict(default=None, + type='str'), + mailAlternativeAddress = dict(default=[], + type='list'), + mailHomeServer = dict(default=None, + type='str'), + mailPrimaryAddress = dict(default=None, + type='str'), + mobileTelephoneNumber = dict(default=[], + type='list'), + organisation = dict(default=None, + type='str'), + pagerTelephonenumber = dict(default=[], + type='list'), + password = dict(default=None, + type='str', + no_log=True), + phone = dict(default=[], + type='list'), + postcode = dict(default=None, + type='str'), + primaryGroup = dict(default=None, + type='str'), + profilepath = dict(default=None, + type='str'), + pwdChangeNextLogin = dict(default=None, + type='str', + choices=['0', '1']), + roomNumber = dict(default=None, + type='str'), + sambaPrivileges = dict(default=[], + type='list'), + sambaUserWorkstations = dict(default=[], + type='list'), + sambahome = dict(default=None, + type='str'), + scriptpath = dict(default=None, + type='str'), + secretary = dict(default=[], + type='list'), + serviceprovider = dict(default=[''], + type='list'), + shell = dict(default='/bin/bash', + type='str'), + street = dict(default=None, + type='str'), + title = dict(default=None, + type='str'), + unixhome = dict(default=None, + type='str'), + userexpiry = dict(default=expiry, + type='str'), + username = dict(required=True, + aliases=['name'], + type='str'), + position = dict(default='', + type='str'), + ou = dict(default='', + type='str'), + subpath = dict(default='cn=users', + type='str'), + state = dict(default='present', + choices=['present', 'absent'], + type='str') + ), + supports_check_mode=True, + required_if = ([ + ('state', 'present', ['firstname', 'lastname', 'password']) + ]) + ) + username = module.params['username'] + position = module.params['position'] + ou = module.params['ou'] + subpath = module.params['subpath'] + state = module.params['state'] + changed = False + + users = list(ldap_search( + '(&(objectClass=posixAccount)(uid={}))'.format(username), + attr=['uid'] + )) + if position != '': + container = position + else: + if ou != '': + ou = 'ou={},'.format(ou) + if subpath != '': + subpath = '{},'.format(subpath) + container = '{}{}{}'.format(subpath, ou, base_dn()) + user_dn = 'uid={},{}'.format(username, container) + + exists = bool(len(users)) + + if state == 'present': + try: + if not exists: + obj = umc_module_for_add('users/user', container) + else: + obj = umc_module_for_edit('users/user', user_dn) + + if module.params['displayName'] == None: + module.params['displayName'] = '{} {}'.format( + module.params['firstname'], + module.params['lastname'] + ) + if module.params['unixhome'] == None: + module.params['unixhome'] = '/home/{}'.format( + module.params['username'] + ) + for k in obj.keys(): + if (k != 'password' and + k != 'groups' and + module.params.has_key(k) and + module.params[k] != None): + obj[k] = module.params[k] + # handle some special values + obj['e-mail'] = module.params['email'] + password = module.params['password'] + if obj['password'] == None: + obj['password'] = password + else: + old_password = obj['password'].split('}', 2)[1] + if crypt.crypt(password, old_password) != old_password: + obj['password'] = password + + diff = obj.diff() + if exists: + for k in obj.keys(): + if obj.hasChanged(k): + changed = True + else: + changed = True + if not module.check_mode: + if not exists: + obj.create() + elif changed: + obj.modify() + except: + module.fail_json( + msg="Creating/editing user {} in {} failed".format(username, container) + ) + try: + groups = module.params['groups'] + if groups: + filter = '(&(objectClass=posixGroup)(|(cn={})))'.format(')(cn='.join(groups)) + group_dns = list(ldap_search(filter, attr=['dn'])) + for dn in group_dns: + grp = umc_module_for_edit('groups/group', dn[0]) + if user_dn not in grp['users']: + grp['users'].append(user_dn) + if not module.check_mode: + grp.modify() + changed = True + except: + module.fail_json( + msg="Adding groups to user {} failed".format(username) + ) + + if state == 'absent' and exists: + try: + obj = umc_module_for_edit('users/user', user_dn) + if not module.check_mode: + obj.remove() + changed = True + except: + module.fail_json( + msg="Removing user {} failed".format(username) + ) + + module.exit_json( + changed=changed, + username=username, + diff=diff, + container=container + ) + + +if __name__ == '__main__': + main()