From 825a224d38006848c54019cbbb4591fc74a5f4bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tobias=20R=C3=BCetschi?=
Date: Sat, 27 Aug 2016 01:07:32 +0200
Subject: [PATCH] Feature udm user (#2406)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Creating directory univention
* UCS udm_user: added
Signed-off-by: Tobias Rüetschi
* UCS udm_user: updating, added support to modify user groups
Signed-off-by: Tobias Rüetschi
* UCS udm_user: add key homedrive
Signed-off-by: Tobias Rüetschi
* UCS udm_user: add key userexpiry
Signed-off-by: Tobias Rüetschi
* python styling
Signed-off-by: Tobias Rüetschi
* UCS udm_user: updated, add supports check mode
Signed-off-by: Tobias Rüetschi
* UCS udm_user: updated, add support to modify users
* UCS udm_user: change string formating
* UCS udm_user: add type definitions to the argument specification
* UCS udm_user: only modify object if it has changed
* UCS udm_user: if user not exists, changed is always true
* UCS udm_user: import common code for univention from ansible.module_utils.univention_umc
* UCS udm_user: add a lot more attributes
* UCS udm_user: add license information
* UCS udm_user: fix API serviceprovider and unixhome
* UCS udm_user: add documentation
* univention udm_user: import only AnsibleModule from ansible.module_utils.basic
* univention udm_user: reorder documentation options
* univention udm_user: fix documentation
* univention udm_user: dont log password
* univention udm_user: add more examples
---
.travis.yml | 2 +-
univention/__init__.py | 0
univention/udm_user.py | 530 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 531 insertions(+), 1 deletion(-)
create mode 100644 univention/__init__.py
create mode 100644 univention/udm_user.py
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()