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

Reply via email to