Hello, This patch (RFE 3813) is related to the stageuser plugin that handle the workflow from/to Stage users. ipa stageuser-add <login> [--from-delete] [<options>] ipa stageuser-mod <login> <options> ipa stageuser-del <login> ipa stageuser-find <pattern> ipa stageuser-show <login> ipa stageuser-activate <login>
What it contains is: * Creation of Stage,Active, Delete containers * configuration of attribute uniqueness (krbPrincipalName, krbCanonicalName and ipaUniqueId) on Active container * the stageuser CLIs What is missing: * Configuration of attribute uniqueness (uid) to scope Active/Delete containers * Configuration of IPA uniqueID generator to scope Active container * Configuration of DNA plugin to scope Active container thanks thierry
>From f9f7cd6e64a181c4925b30e73bd013de75d45720 Mon Sep 17 00:00:00 2001 From: "Thierry bordaz (tbordaz)" <tbor...@redhat.com> Date: Wed, 11 Jun 2014 17:19:18 +0200 Subject: [PATCH] Ticket 3813 - User Life Cycle: introduction of stageuser plugin Bug Description: User Life Cycle is designed http://www.freeipa.org/page/V4/User_Life-Cycle_Management It manages 3 containers (Staging, Active, Delete) with some workflow CLI. This patch supports Staging container with the following CLIs ipa stageuser-add <login> [--from-delete] [<options>] ipa stageuser-mod <login> <options> ipa stageuser-del <login> ipa stageuser-find <pattern> ipa stageuser-show <login> ipa stageuser-active <login> Fix Description: stageuser-add: - support a --from-delete option ('givenname' and 'sn' are not required) that do a MODRDN from Delete container to Staging - Create a Stage entry with the following particularities: - description: __no_upg__ - uidNumber/gidNumber: -1 - nsAccountLock: True - manager is checked to be an Active user stageuser-del: stageuser-find: stageuser-show: - nothing special stageuser-mod: - prevent modification of nsAccountLock - manager is checked to be an Active user stageuser-activate: - reset syntax DN attributes (except 'manager', 'secretary' and 'managedby') - value __no_upg__ removed from description - nsAccountLock: False Reviewed by: ? Platforms tested: F20 Flag Day: no Doc impact: no https://fedorahosted.org/freeipa/ticket/3813 --- install/share/bootstrap-template.ldif | 24 + install/share/unique-attributes.ldif | 6 +- ipalib/constants.py | 2 + ipalib/plugins/stageuser.py | 863 ++++++++++++++++++++++++++++++++++ ipapython/ipaldap.py | 16 + 5 files changed, 908 insertions(+), 3 deletions(-) create mode 100644 ipalib/plugins/stageuser.py diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif index f603ad5cee26953f063ea6c28cddf1eb34a55c4a..de65979d1ce2c7c87837d8bf09a60e3c91a10e24 100644 --- a/install/share/bootstrap-template.ldif +++ b/install/share/bootstrap-template.ldif @@ -34,6 +34,30 @@ objectClass: top objectClass: nsContainer cn: hostgroups +dn: cn=provisioning,$SUFFIX +changetype: add +objectClass: top +objectClass: nsContainer +cn: provisioning + +dn: cn=accounts,cn=provisioning,$SUFFIX +changetype: add +objectClass: top +objectClass: nsContainer +cn: accounts + +dn: cn=staged users,cn=accounts,cn=provisioning,$SUFFIX +changetype: add +objectClass: top +objectClass: nsContainer +cn: staged users + +dn: cn=deleted users,cn=accounts,cn=provisioning,$SUFFIX +changetype: add +objectClass: top +objectClass: nsContainer +cn: deleted users + dn: cn=alt,$SUFFIX changetype: add objectClass: nsContainer diff --git a/install/share/unique-attributes.ldif b/install/share/unique-attributes.ldif index 0e680a0e45b455469f9be9555aed1e63f1d97faf..3af71ef5e2f78860fb5388247a9fdbc2055173e8 100644 --- a/install/share/unique-attributes.ldif +++ b/install/share/unique-attributes.ldif @@ -9,7 +9,7 @@ nsslapd-pluginInitfunc: NSUniqueAttr_Init nsslapd-pluginType: preoperation nsslapd-pluginEnabled: on nsslapd-pluginarg0: krbPrincipalName -nsslapd-pluginarg1: $SUFFIX +nsslapd-pluginarg1: cn=accounts,$SUFFIX nsslapd-plugin-depends-on-type: database nsslapd-pluginId: NSUniqueAttr nsslapd-pluginVersion: 1.1.0 @@ -27,7 +27,7 @@ nsslapd-pluginInitfunc: NSUniqueAttr_Init nsslapd-pluginType: preoperation nsslapd-pluginEnabled: on nsslapd-pluginarg0: krbCanonicalName -nsslapd-pluginarg1: $SUFFIX +nsslapd-pluginarg1: cn=accounts,$SUFFIX nsslapd-plugin-depends-on-type: database nsslapd-pluginId: NSUniqueAttr nsslapd-pluginVersion: 1.1.0 @@ -63,7 +63,7 @@ nsslapd-pluginInitfunc: NSUniqueAttr_Init nsslapd-pluginType: preoperation nsslapd-pluginEnabled: on nsslapd-pluginarg0: ipaUniqueID -nsslapd-pluginarg1: $SUFFIX +nsslapd-pluginarg1: cn=accounts,$SUFFIX nsslapd-plugin-depends-on-type: database nsslapd-pluginId: NSUniqueAttr nsslapd-pluginVersion: 1.1.0 diff --git a/ipalib/constants.py b/ipalib/constants.py index 2269189f443844f77f3d8499cda3b88ae6d1d760..d3ccd51882f487f4636d2a3694ba8717d5943217 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -77,6 +77,8 @@ DEFAULT_CONFIG = ( # LDAP containers: ('container_accounts', DN(('cn', 'accounts'))), ('container_user', DN(('cn', 'users'), ('cn', 'accounts'))), + ('container_stageuser', DN(('cn', 'staged users'), ('cn', 'accounts'), ('cn', 'provisioning'))), + ('container_deleteuser', DN(('cn', 'deleted users'), ('cn', 'accounts'), ('cn', 'provisioning'))), ('container_group', DN(('cn', 'groups'), ('cn', 'accounts'))), ('container_service', DN(('cn', 'services'), ('cn', 'accounts'))), ('container_host', DN(('cn', 'computers'), ('cn', 'accounts'))), diff --git a/ipalib/plugins/stageuser.py b/ipalib/plugins/stageuser.py new file mode 100644 index 0000000000000000000000000000000000000000..5ca0a1ce92e7de07c3e1d3319c063254e6ff1d5a --- /dev/null +++ b/ipalib/plugins/stageuser.py @@ -0,0 +1,863 @@ +# Authors: +# Jason Gerard DeRose <jder...@redhat.com> +# Pavel Zuna <pz...@redhat.com> +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +from time import gmtime, strftime +from copy import deepcopy +import string +import posixpath +import os + +from ipalib import api, errors +from ipalib import Flag, Int, Password, Str, Bool +from ipalib.plugins.baseldap import * +from ipalib import Command +from ipalib.plugins import baseldap +from ipalib.request import context +from ipalib import _, ngettext +from ipalib import output +from ipalib.plugable import Registry +from ipapython.ipautil import ipa_generate_password +from ipapython.ipavalidate import Email +from ipalib.capabilities import client_has_capability +from ipalib.util import (normalize_sshpubkey, validate_sshpubkey, + convert_sshpubkey_post) +if api.env.in_server and api.env.context in ['lite', 'server']: + from ipaserver.plugins.ldap2 import ldap2 + +__doc__ = _(""" +Stageusers + +Manage user entries. All users are POSIX users. + +IPA supports a wide range of username formats, but you need to be aware of any +restrictions that may apply to your particular environment. For example, +usernames that start with a digit or usernames that exceed a certain length +may cause problems for some UNIX systems. +Use 'ipa config-mod' to change the username format allowed by IPA tools. + + +Password management is not a part of this module. For more information +about this topic please see: ipa help passwd + +Account lockout on password failure happens per IPA master. The user-status +command can be used to identify which master the user is locked out on. +It is on that master the administrator must unlock the user. + +EXAMPLES: + + Add a new stageuser: + ipa stageuser-add --first=Tim --last=User --password tuser1 + + Add a stageuser from the Delete container + ipa stageuser-add tuser1 --from-delete + + Find all staged users whose entries include the string "Tim": + ipa stageuser-find Tim + + Find all staged users with "Tim" as the first name: + ipa user-find --first=Tim + + Delete a stage user: + ipa stageuser-del tuser1 +""") + +register = Registry() + +NO_UPG_MAGIC = '__no_upg__' + +stageuser_output_params = ( + Flag('has_keytab', + label=_('Kerberos keys available'), + ), + Str('sshpubkeyfp*', + label=_('SSH public key fingerprint'), + ), + ) + +status_output_params = ( + Str('server', + label=_('Server'), + ), + Str('krbloginfailedcount', + label=_('Failed logins'), + ), + Str('krblastsuccessfulauth', + label=_('Last successful authentication'), + ), + Str('krblastfailedauth', + label=_('Last failed authentication'), + ), + Str('now', + label=_('Time now'), + ), + ) + +# characters to be used for generating random user passwords +user_pwdchars = string.digits + string.ascii_letters + '_,.@+-=' + +def convert_nsaccountlock(entry_attrs): + if not 'nsaccountlock' in entry_attrs: + entry_attrs['nsaccountlock'] = False + else: + nsaccountlock = Bool('temp') + entry_attrs['nsaccountlock'] = nsaccountlock.convert(entry_attrs['nsaccountlock'][0]) + +def split_principal(principal): + """ + Split the principal into its components and do some basic validation. + + Automatically append our realm if it wasn't provided. + """ + realm = None + parts = principal.split('@') + user = parts[0].lower() + if len(parts) > 2: + raise errors.MalformedUserPrincipal(principal=principal) + + if len(parts) == 2: + realm = parts[1].upper() + # At some point we'll support multiple realms + if realm != api.env.realm: + raise errors.RealmMismatch() + else: + realm = api.env.realm + + return (user, realm) + +def validate_principal(ugettext, principal): + """ + All the real work is done in split_principal. + """ + (user, realm) = split_principal(principal) + return None + +def normalize_principal(principal): + """ + Ensure that the name in the principal is lower-case. The realm is + upper-case by convention but it isn't required. + + The principal is validated at this point. + """ + (user, realm) = split_principal(principal) + return unicode('%s@%s' % (user, realm)) + + +@register() +class stageuser(LDAPObject): + """ + Stage User object + A Stage user is not an Active user and can not be used to bind with. + Stage container is: cn=staged users,cn=accounts,cn=provisioning,SUFFIX + Stage entry conforms the schema + Stage entry RDN attribute is 'uid' + Stage entry are disabled (nsAccountLock: True) + """ + container_dn = api.env.container_stageuser + stage_container_dn = container_dn + active_container_dn = api.env.container_user + delete_container_dn = api.env.container_deleteuser + object_name = _('user') + object_name_plural = _('users') + object_class = ['posixaccount'] + object_class_config = 'ipauserobjectclasses' + possible_objectclasses = ['meporiginentry'] + disallow_object_classes = ['krbticketpolicyaux'] + search_attributes_config = 'ipausersearchfields' + default_attributes = [ + 'uid', 'givenname', 'sn', 'homedirectory', 'loginshell', + 'uidnumber', 'gidnumber', 'mail', 'ou', + 'telephonenumber', 'title', 'memberof', 'nsaccountlock', + 'memberofindirect', + ] + search_display_attributes = [ + 'uid', 'givenname', 'sn', 'homedirectory', 'loginshell', + 'mail', 'telephonenumber', 'title', 'nsaccountlock', + 'uidnumber', 'gidnumber', 'sshpubkeyfp', + ] + uuid_attribute = 'ipauniqueid' + attribute_members = { + 'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], + 'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], + } + rdn_is_primary_key = True + bindable = True + password_attributes = [('userpassword', 'has_password'), + ('krbprincipalkey', 'has_keytab')] + + label = _('Stage Users') + label_singular = _('Stage User') + + takes_params = ( + Str('uid', + pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', + pattern_errmsg='may only include letters, numbers, _, -, . and $', + maxlength=255, + cli_name='login', + label=_('User login'), + primary_key=True, + default_from=lambda givenname, sn: givenname[0] + sn, + normalizer=lambda value: value.lower(), + ), + Str('givenname?', + cli_name='first', + label=_('First name'), + ), + Str('sn?', + cli_name='last', + label=_('Last name'), + ), + Str('cn?', + label=_('Full name'), + default_from=lambda givenname, sn: '%s %s' % (givenname, sn), + autofill=True, + ), + Str('displayname?', + label=_('Display name'), + default_from=lambda givenname, sn: '%s %s' % (givenname, sn), + autofill=True, + ), + Str('initials?', + label=_('Initials'), + default_from=lambda givenname, sn: '%c%c' % (givenname[0], sn[0]), + autofill=True, + ), + Str('homedirectory?', + cli_name='homedir', + label=_('Home directory'), + ), + Str('gecos?', + label=_('GECOS'), + default_from=lambda givenname, sn: '%s %s' % (givenname, sn), + autofill=True, + ), + Str('loginshell?', + cli_name='shell', + label=_('Login shell'), + ), + Str('krbprincipalname?', validate_principal, + cli_name='principal', + label=_('Kerberos principal'), + default_from=lambda uid: '%s@%s' % (uid.lower(), api.env.realm), + autofill=True, + flags=['no_update'], + normalizer=lambda value: normalize_principal(value), + ), + Str('mail*', + cli_name='email', + label=_('Email address'), + ), + Password('userpassword?', + cli_name='password', + label=_('Password'), + doc=_('Prompt to set the user password'), + # FIXME: This is temporary till bug is fixed causing updates to + # bomb out via the webUI. + exclude='webui', + ), + Flag('random?', + doc=_('Generate a random user password'), + flags=('no_search', 'virtual_attribute'), + default=False, + ), + Str('randompassword?', + label=_('Random password'), + flags=('no_create', 'no_update', 'no_search', 'virtual_attribute'), + ), + Int('uidnumber?', + cli_name='uid', + label=_('UID'), + doc=_('User ID Number (system will assign one if not provided)'), + minvalue=1, + ), + Int('gidnumber?', + label=_('GID'), + doc=_('Group ID Number'), + minvalue=1, + ), + Str('street?', + cli_name='street', + label=_('Street address'), + ), + Str('l?', + cli_name='city', + label=_('City'), + ), + Str('st?', + cli_name='state', + label=_('State/Province'), + ), + Str('postalcode?', + label=_('ZIP'), + ), + Str('telephonenumber*', + cli_name='phone', + label=_('Telephone Number') + ), + Str('mobile*', + label=_('Mobile Telephone Number') + ), + Str('pager*', + label=_('Pager Number') + ), + Str('facsimiletelephonenumber*', + cli_name='fax', + label=_('Fax Number'), + ), + Str('ou?', + cli_name='orgunit', + label=_('Org. Unit'), + ), + Str('title?', + label=_('Job Title'), + ), + Str('manager?', + label=_('Manager'), + ), + Str('carlicense?', + label=_('Car License'), + ), + Str('ipasshpubkey*', validate_sshpubkey, + cli_name='sshpubkey', + label=_('SSH public key'), + normalizer=normalize_sshpubkey, + csv=True, + flags=['no_search'], + ), + ) + + def _normalize_and_validate_email(self, email, config=None): + if not config: + config = self.backend.get_ipa_config() + + # check if default email domain should be added + defaultdomain = config.get('ipadefaultemaildomain', [None])[0] + if email: + norm_email = [] + if not isinstance(email, (list, tuple)): + email = [email] + for m in email: + if isinstance(m, basestring): + if '@' not in m and defaultdomain: + m = m + u'@' + defaultdomain + if not Email(m): + raise errors.ValidationError(name='email', error=_('invalid e-mail format: %(email)s') % dict(email=m)) + norm_email.append(m) + else: + if not Email(m): + raise errors.ValidationError(name='email', error=_('invalid e-mail format: %(email)s') % dict(email=m)) + norm_email.append(m) + return norm_email + + return email + + def _active_user(self, user): + container_dn = DN(self.active_container_dn, api.env.basedn) + + assert isinstance(user, DN) + return user.endswith(container_dn) + + def _normalize_manager(self, manager): + """ + Given a userid verify the user's existence in the ACTIVE container and return the dn. + If a Stage user refers a not ACTIVE user, this value will be wiped when the user will be + activated. Better to ignore an invalid value. + """ + if not manager: + return None + + if not isinstance(manager, list): + manager = [manager] + try: + container_dn = DN(self.active_container_dn, api.env.basedn) + for m in xrange(len(manager)): + if isinstance(manager[m], DN) and manager[m].endswith(container_dn): + continue + entry_attrs = self.backend.find_entry_by_attr( + self.primary_key.name, manager[m], self.object_class, [''], + container_dn + ) + manager[m] = entry_attrs.dn + except errors.NotFound: + raise errors.NotFound(reason=_('manager %(manager)s not found') % dict(manager=manager[m])) + + return manager + + def _convert_manager(self, entry_attrs, **options): + """ + Convert a manager dn into a userid + """ + if options.get('raw', False): + return + + if 'manager' in entry_attrs: + for m in xrange(len(entry_attrs['manager'])): + entry_attrs['manager'][m] = self.get_primary_key_from_dn(entry_attrs['manager'][m]) + +@register() +class stageuser_add(LDAPCreate): + __doc__ = _('Add a new stage user.') + + msg_summary = _('Added stage user "%(value)s"') + + has_output_params = LDAPCreate.has_output_params + stageuser_output_params + + takes_options = LDAPCreate.takes_options + ( + Flag('from_delete?', + doc=_('Create Stage user in from a delete user'), + cli_name='from_delete', + default=False, + ), + ) + + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + assert isinstance(dn, DN) + + if not options.get('from_delete'): + # then givenname and sn are required attributes + if 'givenname' not in entry_attrs: + raise errors.RequirementError(name='givenname', error=_('givenname is required')) + + if 'sn' not in entry_attrs: + raise errors.RequirementError(name='sn', error=_('sn is required')) + + # we don't want an user private group to be created for this user + # add NO_UPG_MAGIC description attribute to let the DS plugin know + entry_attrs.setdefault('description', []) + entry_attrs['description'].append(NO_UPG_MAGIC) + + # uidNumber/gidNumber + entry_attrs.setdefault('uidnumber', baseldap.DNA_MAGIC) + entry_attrs.setdefault('gidnumber', baseldap.DNA_MAGIC) + + if not client_has_capability( + options['version'], 'optional_uid_params'): + # https://fedorahosted.org/freeipa/ticket/2886 + # Old clients say 999 (OLD_DNA_MAGIC) when they really mean + # "assign a value dynamically". + OLD_DNA_MAGIC = 999 + if entry_attrs.get('uidnumber') == OLD_DNA_MAGIC: + entry_attrs['uidnumber'] = baseldap.DNA_MAGIC + if entry_attrs.get('gidnumber') == OLD_DNA_MAGIC: + entry_attrs['gidnumber'] = baseldap.DNA_MAGIC + + + # Prevent to authenticate with a Stage user account + entry_attrs['nsaccountlock'] = 'true' + + # Check the lenght of the RDN (uid) value + config = ldap.get_ipa_config() + if 'ipamaxusernamelength' in config: + if len(keys[-1]) > int(config.get('ipamaxusernamelength')[0]): + raise errors.ValidationError( + name=self.obj.primary_key.cli_name, + error=_('can be at most %(len)d characters') % dict( + len = int(config.get('ipamaxusernamelength')[0]) + ) + ) + + # Shell setting + # (order is : option, placeholder (TBD), CLI default value) + default_shell = config.get('ipadefaultloginshell', ['/bin/sh'])[0] + entry_attrs.setdefault('loginshell', default_shell) + + # hack so we can request separate first and last name in CLI + full_name = '%s %s' % (entry_attrs['givenname'], entry_attrs['sn']) + entry_attrs.setdefault('cn', full_name) + + # Homedirectory + # (order is : option, placeholder (TBD), CLI default value (here in config)) + if 'homedirectory' not in entry_attrs: + # get home's root directory from config + homes_root = config.get('ipahomesrootdir', ['/home'])[0] + # build user's home directory based on his uid + entry_attrs['homedirectory'] = posixpath.join(homes_root, keys[-1]) + + # Kerberos principal + entry_attrs.setdefault('krbprincipalname', '%s@%s' % (entry_attrs['uid'], api.env.realm)) + + + # If requested, generate a userpassword + if 'userpassword' not in entry_attrs and options.get('random'): + entry_attrs['userpassword'] = ipa_generate_password(user_pwdchars) + # save the password so it can be displayed in post_callback + setattr(context, 'randompassword', entry_attrs['userpassword']) + + # Check the email or create it + if 'mail' in entry_attrs: + entry_attrs['mail'] = self.obj._normalize_and_validate_email(entry_attrs['mail'], config) + else: + # No e-mail passed in. If we have a default e-mail domain set + # then we'll add it automatically. + defaultdomain = config.get('ipadefaultemaildomain', [None])[0] + if defaultdomain: + entry_attrs['mail'] = self.obj._normalize_and_validate_email(keys[-1], config) + + # If the manager is defined, check it is a ACTIVE user to validate it + if 'manager' in entry_attrs: + entry_attrs['manager'] = self.obj._normalize_manager(entry_attrs['manager']) + + return dn + + def execute(self, *keys, **options): + ''' + A stage entry may be taken from the Delete container. + In that case we rather do 'MODRDN' than 'ADD'. + ''' + if options.get('from_delete'): + ldap = self.obj.backend + + staging_dn = self.obj.get_dn(*keys, **options) + delete_dn = DN(staging_dn[0], self.obj.delete_container_dn, api.env.basedn) + + # Check that this value is a Active user + try: + entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(delete_dn, ['dn']) + except errors.NotFound: + raise + self._exc_wrapper(keys, options, ldap.move_entry_newsuperior)(delete_dn, str(DN(self.obj.stage_container_dn, api.env.basedn))) + + entry_attrs = entry_to_dict(entry_attrs, **options) + entry_attrs['dn'] = delete_dn + + if self.obj.primary_key and keys[-1] is not None: + return dict(result=entry_attrs, value=keys[-1]) + return dict(result=entry_attrs, value=u'') + else: + return super(stageuser_add, self).execute(*keys, **options) + + +@register() +class stageuser_del(LDAPDelete): + __doc__ = _('Delete a stage user.') + + msg_summary = _('Deleted stage user "%(value)s"') + + def pre_callback(self, ldap, dn, *keys, **options): + assert isinstance(dn, DN) + return dn + + + + +@register() +class stageuser_mod(LDAPUpdate): + __doc__ = _('Modify a stage user.') + + msg_summary = _('Modified stage user "%(value)s"') + + has_output_params = LDAPUpdate.has_output_params + stageuser_output_params + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + assert isinstance(dn, DN) + + # Check the lenght of the RDN (uid) value + if options.get('rename') is not None: + config = ldap.get_ipa_config() + if 'ipamaxusernamelength' in config: + if len(options['rename']) > int(config.get('ipamaxusernamelength')[0]): + raise errors.ValidationError( + name=self.obj.primary_key.cli_name, + error=_('can be at most %(len)d characters') % dict( + len = int(config.get('ipamaxusernamelength')[0]) + ) + ) + if 'mail' in entry_attrs: + entry_attrs['mail'] = self.obj._normalize_and_validate_email(entry_attrs['mail']) + + # If the manager is defined, check it is a ACTIVE user to validate it + if 'manager' in entry_attrs: + entry_attrs['manager'] = self.obj._normalize_manager(entry_attrs['manager']) + + # Make sure it is not possible to authenticate with a Stage user account + if 'nsaccountlock' in entry_attrs: + del entry_attrs['nsaccountlock'] + + # If requested, generate a userpassword + if 'userpassword' not in entry_attrs and options.get('random'): + entry_attrs['userpassword'] = ipa_generate_password(user_pwdchars) + # save the password so it can be displayed in post_callback + setattr(context, 'randompassword', entry_attrs['userpassword']) + + if 'ipasshpubkey' in entry_attrs: + if 'objectclass' in entry_attrs: + obj_classes = entry_attrs['objectclass'] + else: + _entry_attrs = ldap.get_entry(dn, ['objectclass']) + obj_classes = entry_attrs['objectclass'] = _entry_attrs['objectclass'] + if 'ipasshuser' not in obj_classes: + obj_classes.append('ipasshuser') + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + assert isinstance(dn, DN) + if options.get('random', False): + try: + entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) + except AttributeError: + # if both randompassword and userpassword options were used + pass + convert_nsaccountlock(entry_attrs) + self.obj._convert_manager(entry_attrs, **options) + self.obj.get_password_attributes(ldap, dn, entry_attrs) + convert_sshpubkey_post(ldap, dn, entry_attrs) + return dn + + +@register() +class stageuser_find(LDAPSearch): + __doc__ = _('Search for stage users.') + + member_attributes = ['memberof'] + has_output_params = LDAPSearch.has_output_params + stageuser_output_params + + takes_options = LDAPSearch.takes_options + ( + Flag('whoami', + label=_('Self'), + doc=_('Display stage user record for current Kerberos principal'), + ), + ) + + def execute(self, *args, **options): + # assure the manager attr is a dn, not just a bare uid + manager = options.get('manager') + if manager is not None: + options['manager'] = self.obj._normalize_manager(manager) + return super(stageuser_find, self).execute(self, *args, **options) + + def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, **options): + assert isinstance(base_dn, DN) + if options.get('whoami'): + return ("(&(objectclass=posixaccount)(krbprincipalname=%s))"%\ + getattr(context, 'principal'), base_dn, scope) + + return (filter, base_dn, scope) + + def post_callback(self, ldap, entries, truncated, *args, **options): + if options.get('pkey_only', False): + return truncated + for attrs in entries: + self.obj._convert_manager(attrs, **options) + self.obj.get_password_attributes(ldap, attrs.dn, attrs) + convert_nsaccountlock(attrs) + convert_sshpubkey_post(ldap, attrs.dn, attrs) + return truncated + + msg_summary = ngettext( + '%(count)d user matched', '%(count)d users matched', 0 + ) + + + +@register() +class stageuser_show(LDAPRetrieve): + __doc__ = _('Display information about a user.') + + has_output_params = LDAPRetrieve.has_output_params + stageuser_output_params + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + assert isinstance(dn, DN) + convert_nsaccountlock(entry_attrs) + self.obj._convert_manager(entry_attrs, **options) + self.obj.get_password_attributes(ldap, dn, entry_attrs) + convert_sshpubkey_post(ldap, dn, entry_attrs) + return dn + + +@register() +class stageuser_activate(LDAPQuery): + __doc__ = ('Activate a stage user.') + + preserved_DN_syntax_attrs = ('manager', 'managedby', 'secretary') + + searched_operational_attributes = ['uidNumber', 'gidNumber', 'nsAccountLock', 'ipauniqueid'] + + has_output_params = LDAPQuery.has_output_params + stageuser_output_params + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + assert isinstance(dn, DN) + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + assert isinstance(dn, DN) + return dn + + def __dict_new_entry(self, *args, **options): + ldap = self.obj.backend + + entry_attrs = self.args_options_2_entry(*args, **options) + entry_attrs = ldap.make_entry(DN(), entry_attrs) + + self.process_attr_options(entry_attrs, None, args, options) + + entry_attrs['objectclass'] = deepcopy(self.obj.object_class) + + if self.obj.object_class_config: + config = ldap.get_ipa_config() + entry_attrs['objectclass'] = config.get( + self.obj.object_class_config, entry_attrs['objectclass'] + ) + + return(entry_attrs) + + def __merge_values(self, args, options, entry_from, entry_to, attr): + ''' + This routine merges the values of attr taken from entry_from, into entry_to. + If attr is a syntax DN attribute, it is replaced by an empty value. It is a preferable solution + compare to skiping it because the final entry may no longer conform the schema. + An exception of this is for a limited set of syntax DN attribute that we want to + preserved (defined in preserved_DN_syntax_attrs) + ''' + if not attr in entry_to: + if isinstance(entry_from[attr], (list, tuple)): + # attr is multi value attribute + entry_to[attr] = [] + else: + # attr single valued attribute + entry_to[attr] = None + + # At this point entry_to contains for all resulting attributes + # either a list (possibly empty) or a value (possibly None) + + for value in entry_from[attr]: + # merge all the values from->to + v = self.__value_2_add(args, options, attr, value) + if v == u'': + self.log.debug("Value %s: %s wiped" % (attr, value)) + if isinstance(entry_to[attr], (list, tuple)): + # multi value attribute + if v not in entry_to[attr]: + # it may has been added before in the loop + # so add it only if it not present + entry_to[attr].append(v) + else: + # single value attribute + # keep the value defined in staging + entry_to[attr] = v + else: + self.log.debug("Add %s: %s" % (attr, value)) + if isinstance(entry_to[attr], (list, tuple)): + # multi value attribute + if value not in entry_to[attr]: + entry_to[attr].append(value) + else: + # single value attribute + if value: + entry_to[attr] = value + + + def __value_2_add(self, args, options, attr, value): + ''' + If the attribute is NOT syntax DN it returns its value. + Else it checks if the value can be preserved. + To be preserved: + - attribute must be in preserved_DN_syntax_attrs + - value must be an active user DN (in Active container) + - the active user entry exists + ''' + ldap = self.obj.backend + + if ldap.has_dn_syntax(attr): + if attr.lower() in self.preserved_DN_syntax_attrs: + # we are about to add a DN syntax value + # Check this is a valid DN + if not isinstance(value, DN): + return u'' + + if not self.obj._active_user(value): + return u'' + + # Check that this value is a Active user + try: + entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)(value, ['dn']) + return value + except errors.NotFound: + return u'' + else: + return u'' + else: + return value + + + def execute(self, *args, **options): + + ldap = self.obj.backend + + staging_dn = self.obj.get_dn(*args, **options) + assert isinstance(staging_dn, DN) + + + # retrieve the current entry + try: + entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)( + staging_dn, ['*'] + ) + except errors.NotFound: + self.obj.handle_not_found(*args) + entry_attrs = dict((k.lower(), v) for (k, v) in entry_attrs.iteritems()) + + # Check it does not exist an active entry with the same RDN + active_dn = DN(staging_dn[0], api.env.container_user, api.env.basedn) + try: + test_entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)( + active_dn, ['dn'] + ) + assert isinstance(staging_dn, DN) + raise errors.DuplicateEntry(message=_('Active user %(user)s already exists') % dict( + user=test_entry_attrs.dn)) + except errors.NotFound: + pass + + + # Time to build the new entry + new_entry_attrs = self.__dict_new_entry() + for (attr, values) in entry_attrs.iteritems(): + self.__merge_values(args, options, entry_attrs, new_entry_attrs, attr) + + + # unlock the entry + new_entry_attrs['nsAccountLock'] = 'False' + + # Allow Managed entry plugin to do its work + if 'description' in new_entry_attrs and NO_UPG_MAGIC in new_entry_attrs['description']: + new_entry_attrs['description'].remove(NO_UPG_MAGIC) + + for (k,v) in new_entry_attrs.iteritems(): + self.log.debug("new entry: k=%r and v=%r)" % (k, v)) + + # Add the Active entry + entry = ldap.make_entry(active_dn, new_entry_attrs) + self._exc_wrapper(args, options, ldap.add_entry)(entry) + + # Now delete the Staging entry + try: + self._exc_wrapper(args, options, ldap.delete_entry)(staging_dn) + except: + try: + self.log.error("Fail to delete the Staging user after activating it %s " % (staging_dn)) + self._exc_wrapper(args, options, ldap.delete_entry)(active_dn) + except: + self.log.error("Fail to cleanup activation. The user remains active %s" % (active_dn)) + pass + raise + return dict(result=new_entry_attrs) + + + diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py index 677e4f8071b2303c9beeb0a58972684814ecc382..24fd2c7ae42e7748f0ed43ef059288d9eb4daa25 100644 --- a/ipapython/ipaldap.py +++ b/ipapython/ipaldap.py @@ -1590,6 +1590,22 @@ class LDAPClient(object): with self.error_handler(): self.conn.rename_s(dn, new_rdn, delold=int(del_old)) time.sleep(.3) # Give memberOf plugin a chance to work + + def move_entry_newsuperior(self, dn, newsuperior): + ''' + Move an entry (with the same RDN) under a new superior + ''' + assert isinstance(dn, DN) + assert isinstance(newsuperior, (DN, str)) + if isinstance(newsuperior, DN): + newsuperior_str = str(newsuperior) + else: + newsuperior_str = newsuperior + + with self.error_handler(): + self.conn.rename_s(dn, dn[0], newsuperior=newsuperior_str, delold=0) + time.sleep(.3) # Give memberOf plugin a chance to work + def update_entry(self, entry, entry_attrs=None): """Update entry's attributes. -- 1.7.11.7
_______________________________________________ Freeipa-devel mailing list Freeipa-devel@redhat.com https://www.redhat.com/mailman/listinfo/freeipa-devel