On 03/24/2015 01:45 PM, Jan Cholasta wrote:
Dne 19.3.2015 v 13:07 thierry bordaz napsal(a):
On 03/19/2015 07:37 AM, Jan Cholasta wrote:
Dne 18.3.2015 v 19:39 thierry bordaz napsal(a):
On 03/17/2015 08:01 AM, Jan Cholasta wrote:
Dne 16.3.2015 v 12:06 David Kupka napsal(a):
On 03/06/2015 07:30 PM, thierry bordaz wrote:
On 02/19/2015 04:19 PM, Martin Basti wrote:
On 19/02/15 13:01, thierry bordaz wrote:
On 02/04/2015 05:14 PM, Jan Cholasta wrote:
Hi,

Dne 4.2.2015 v 15:25 David Kupka napsal(a):
On 02/03/2015 11:50 AM, thierry bordaz wrote:
On 09/17/2014 12:32 PM, thierry bordaz wrote:
On 09/01/2014 01:08 PM, Petr Viktorin wrote:
On 08/08/2014 03:54 PM, thierry bordaz wrote:
Hi,

The attached patch is related to 'User Life Cycle'
(https://fedorahosted.org/freeipa/ticket/3813)

It creates a stageuser plugin with a first function
stageuser-add.
Stage
user entries are provisioned under 'cn=staged
users,cn=accounts,cn=provisioning,SUFFIX'.

Thanks
thierry

Avoid `from ipalib.plugins.baseldap import *` in new code;
instead
import the module itself and use e.g. `baseldap.LDAPObject`.

The stageuser help (docstring) is copied from the user
plugin, and
discusses things like account lockout and disabling users. It
should
rather explain what stageuser itself does. (And I don't very
much
like the Note about the interface being badly designed...)
Also decide if the docs should call it "staged user" or "stage
user"
or "stageuser".

A lot of the code is copied and pasted over from the users
plugin.
Don't do that. Either import things (e.g.
validate_nsaccountlock)
from the users plugin, or move the reused code into a shared
module.

For the `user` object, since so much is the same, it might be
best to
create a common base class for user and stageuser; and
similarly
for
the Command plugins.

The default permissions need different names, and you don't
need
another copy of the 'non_object' ones. Also, run the makeaci
script.

Hello,

    This modified patch is mainly moving common base class
into a
new
    plugin: accounts.py. user/stageuser plugin inherits from
accounts.
    It also creates a better description of what are stage
user,
how
    to add a new stage user, updates ACI.txt and separate
active/stage
    user managed permissions.

thanks
thierry






_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel


Thanks David for the reviews. Here the last patches




_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel


The freeipa-tbordaz-0002 patch had trailing whitespaces on few
lines so
I'm attaching fixed version (and unchanged patch
freeipa-tbordaz-0003-3
to keep them together).

The ULC feature is still WIP but these patches look good to me
and
don't
break anything as far as I tested.
We should push them now to avoid further rebases. Thierry can
then
prepare other patches delivering the rest of ULC functionality.

Few comments from just reading the patches:

1) I would name the base class "baseuser", "account" does not
necessarily mean user account.

2) This is very wrong:

-class user_add(LDAPCreate):
+class user_add(user, LDAPCreate):

You are creating a plugin which is both an object and an command.

3) This is purely subjective, but I don't like the name
"deleteuser", as it has a verb in it. We usually don't do that and
IMHO we shouldn't do that.

Honza


Thank you for the review. I am attaching the updates patches






_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel
Hello,
I'm getting errors during make rpms:

if [ "" != "yes" ]; then \
    ./makeapi --validate; \
    ./makeaci --validate; \
fi

/root/freeipa/ipalib/plugins/baseuser.py:641 command "baseuser_add"
doc is not internationalized
/root/freeipa/ipalib/plugins/baseuser.py:653 command "baseuser_find"
doc is not internationalized
/root/freeipa/ipalib/plugins/baseuser.py:647 command "baseuser_mod"
doc is not internationalized
0 commands without doc, 3 commands whose doc is not i18n
Command baseuser_add in ipalib, not in API
Command baseuser_find in ipalib, not in API
Command baseuser_mod in ipalib, not in API

There are one or more new commands defined.
Update API.txt and increment the minor version in VERSION.

There are one or more documentation problems.
You must fix these before preceeding

Issues probably caused by this:
1)
You should not use the register decorator, if this class is just for
inheritance
@register()
class baseuser_add(LDAPCreate):

@register()
class baseuser_mod(LDAPUpdate):

@register()
class baseuser_find(LDAPSearch):

see dns.py plugin and "DNSZoneBase" and "dnszone" classes

2)
there might be an issue with
@register()
class baseuser(LDAPObject):

the register decorator should not be there, I was warned by
Petr^3 to
not use permission in parent class. The same permission should be
specified only in one place (for example user class), (otherwise
they
will be generated twice??) I don't know more details about it.

--
Martin Basti

Hello Martin, Jan,

Thanks for your review.
I changed the patch so that it does not register baseuser_*. Also
increase the minor version because of new command.
Finally I moved the managed_permission definition out of the parent
baseuser class.






Martin, could you please verify that the issues you encountered are
fixed?

Thanks!


You bumped wrong version variable:

-IPA_VERSION_MINOR=1
+IPA_VERSION_MINOR=2

It should have been IPA_API_VERSION_MINOR (at the bottom of the file),
including the last change comment below it.


IMO baseuser should include superclasses for all the usual commands
(add, mod, del, show, find) and stageuser/deleteuser commands should
inherit from them.


You don't need to override class properties like active_container_dn
and takes_params on baseuser subclasses when they have the same value
as in baseuser.


Honza

Hello Honza,

    Thanks for the review. I did the modifications you recommended
    within that attached patches

      * Change version

Please also update the comment below (e.g. "# Last change: tbordaz -
Add stageuser_add command")

      * create the baseuser_* plugins commands and use them in the
        user/stageuser plugin commands
      * Do not redefine the class properties in the subclasses.

There are still some in baseuser command classes:

+class baseuser_add(LDAPCreate):
+    """
+    Prototype command plugin to be implemented by real plugin
+    """
+    active_container_dn       = api.env.container_user
+    has_output_params = LDAPCreate.has_output_params

You don't need to set active_container_dn here, you only need to set
it in baseuser. Then in stageuser_add and other subclasses you use
"self.obj.active_container_dn" instead of "self.active_container_dn".

You also don't need to override has_output_params if you are not
changing its value - you are inheriting from LDAPCreate, so
baseuser_add.has_output_params implicitly has the same value as
LDAPCreate.has_output_params.


    Thanks
    thierry



Hello Honza,

    Thanks for your patience .. :-)
    I understand my mistake. Just a question, in a plugin command
(user_add), is 'self.obj' referring to the plugin object (like 'user') ?

Yes, that's correct.


    updated patches (with the appropriate naming and patch versioning).

    thanks
    theirry


One more thing:

Instead of:

class stageuser(baseuser):
    ...
    # take_params does not support 'nsaccountlock' option
    stageuser_takes_params_list = []
    for elt in baseuser.takes_params:
        if isinstance(elt, Bool) and elt.param_spec == 'nsaccountlock?':
            pass
        else:
            stageuser_takes_params_list.append(elt)
    takes_params              = tuple(stageuser_takes_params_list)

I would remove nsaccountlock from baseuser.takes_params and add it in user.takes_params:

class user(baseuser):
    ...
    takes_params = baseuser.takes_params + (
        Bool('nsaccountlock?',
            label=_('Account disabled'),
            flags=['no_option'],
        ),
    )


Right, making this option specific to active user makes sense.

Thanks
thierry
From 7cdcb8bd639f7d904c31fbc4dd2b5cbdd3e26c28 Mon Sep 17 00:00:00 2001
From: "Thierry bordaz (tbordaz)" <tbor...@redhat.com>
Date: Fri, 8 Aug 2014 09:37:23 +0200
Subject: [PATCH 1/2] User Life Cycle: Exclude subtree for ipaUniqueID
 generation

IPA UUID should not generate ipaUniqueID for entries under 'cn=provisioning,SUFFIX'

Add in the configuration the ability to set (optional) 'ipaUuidExcludeSubtree'

https://fedorahosted.org/freeipa/ticket/3813
---
 daemons/ipa-slapi-plugins/ipa-uuid/ipa_uuid.c | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/daemons/ipa-slapi-plugins/ipa-uuid/ipa_uuid.c b/daemons/ipa-slapi-plugins/ipa-uuid/ipa_uuid.c
index 93da0f15b8acfc02beddf4e884a735897a7513fe..ffade14672e8cd9e3f3e18d45a0a7095a6341d30 100644
--- a/daemons/ipa-slapi-plugins/ipa-uuid/ipa_uuid.c
+++ b/daemons/ipa-slapi-plugins/ipa-uuid/ipa_uuid.c
@@ -64,6 +64,7 @@
 #define IPAUUID_GENERATE         "ipaUuidMagicRegen"
 #define IPAUUID_FILTER           "ipaUuidFilter"
 #define IPAUUID_SCOPE            "ipaUuidScope"
+#define IPAUUID_EXCLUDE_SUBTREE  "ipaUuidExcludeSubtree"
 #define IPAUUID_ENFORCE          "ipaUuidEnforce"
 
 #define IPAUUID_FEATURE_DESC      "IPA UUID"
@@ -91,6 +92,7 @@ struct configEntry {
     Slapi_Filter *slapi_filter;
     char *generate;
     char *scope;
+    char *exclude_subtree;
     bool enforce;
 };
 
@@ -537,6 +539,10 @@ ipauuid_parse_config_entry(Slapi_Entry * e, bool apply)
     }
     LOG_CONFIG("----------> %s [%s]\n", IPAUUID_SCOPE, entry->scope);
 
+    value = slapi_entry_attr_get_charptr(e, IPAUUID_EXCLUDE_SUBTREE);
+    entry->exclude_subtree = value;
+    LOG_CONFIG("----------> %s [%s]\n", IPAUUID_EXCLUDE_SUBTREE, entry->exclude_subtree);
+
     entry->enforce = slapi_entry_attr_get_bool(e, IPAUUID_ENFORCE);
     LOG_CONFIG("----------> %s [%s]\n",
                IPAUUID_ENFORCE, entry->enforce ? "True" : "False");
@@ -640,6 +646,10 @@ ipauuid_free_config_entry(struct configEntry **entry)
         slapi_ch_free_string(&e->scope);
     }
 
+    if (e->exclude_subtree) {
+        slapi_ch_free_string(&e->exclude_subtree);
+    }
+
     slapi_ch_free((void **)entry);
 }
 
@@ -918,6 +928,12 @@ static int ipauuid_pre_op(Slapi_PBlock *pb, int modtype)
             }
         }
 
+        if (cfgentry->exclude_subtree) {
+                if (slapi_dn_issuffix(dn, cfgentry->exclude_subtree)) {
+                        continue;
+                }
+        }
+
         /* does the entry match the filter? */
         if (cfgentry->slapi_filter) {
             Slapi_Entry *test_e = NULL;
-- 
1.7.11.7

From 4705a552a231c9128888df001d32b5a4f01c74fe Mon Sep 17 00:00:00 2001
From: "Thierry bordaz (tbordaz)" <tbor...@redhat.com>
Date: Thu, 5 Mar 2015 14:25:33 +0100
Subject: [PATCH 2/2] User life cycle: stageuser-add verb

Add a accounts plugin (accounts class) that defines
variables and methods common to 'users' and 'stageuser'.
accounts is a superclass of users/stageuser

Add the stageuser plugin, with support of stageuser-add verb.

Reviewed By: David Kupka, Martin Basti, Jan Cholasta

https://fedorahosted.org/freeipa/ticket/3813
---
 API.txt                                |  49 ++++
 VERSION                                |   4 +-
 install/updates/30-provisioning.update |  29 +-
 ipalib/constants.py                    |   2 +
 ipalib/plugins/baseuser.py             | 471 +++++++++++++++++++++++++++++++++
 ipalib/plugins/stageuser.py            | 277 +++++++++++++++++++
 ipalib/plugins/user.py                 | 439 +++---------------------------
 7 files changed, 856 insertions(+), 415 deletions(-)
 create mode 100644 ipalib/plugins/baseuser.py
 create mode 100644 ipalib/plugins/stageuser.py

diff --git a/API.txt b/API.txt
index 0c7eda9f5b9176aa6e97ef03f26b0bf0a885fe4e..f747765d7f9c87761fed0277cd59d1bc3fbd57e9 100644
--- a/API.txt
+++ b/API.txt
@@ -3691,6 +3691,55 @@ command: sidgen_was_run
 args: 0,1,1
 option: Str('version?', exclude='webui')
 output: Output('result', None, None)
+command: stageuser_add
+args: 1,43,3
+arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('carlicense', attribute=True, cli_name='carlicense', multivalue=True, required=False)
+option: Str('cn', attribute=True, autofill=True, cli_name='cn', multivalue=False, required=True)
+option: Str('departmentnumber', attribute=True, cli_name='departmentnumber', multivalue=True, required=False)
+option: Str('displayname', attribute=True, autofill=True, cli_name='displayname', multivalue=False, required=False)
+option: Str('employeenumber', attribute=True, cli_name='employeenumber', multivalue=False, required=False)
+option: Str('employeetype', attribute=True, cli_name='employeetype', multivalue=False, required=False)
+option: Str('facsimiletelephonenumber', attribute=True, cli_name='fax', multivalue=True, required=False)
+option: Flag('from_delete?', autofill=True, cli_name='from_delete', default=False)
+option: Str('gecos', attribute=True, autofill=True, cli_name='gecos', multivalue=False, required=False)
+option: Int('gidnumber', attribute=True, cli_name='gidnumber', minvalue=1, multivalue=False, required=False)
+option: Str('givenname', attribute=True, cli_name='first', multivalue=False, required=True)
+option: Str('homedirectory', attribute=True, cli_name='homedir', multivalue=False, required=False)
+option: Str('initials', attribute=True, autofill=True, cli_name='initials', multivalue=False, required=False)
+option: Str('ipasshpubkey', attribute=True, cli_name='sshpubkey', csv=True, multivalue=True, required=False)
+option: Str('ipatokenradiusconfiglink', attribute=True, cli_name='radius', multivalue=False, required=False)
+option: Str('ipatokenradiususername', attribute=True, cli_name='radius_username', multivalue=False, required=False)
+option: StrEnum('ipauserauthtype', attribute=True, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password', u'radius', u'otp'))
+option: DateTime('krbprincipalexpiration', attribute=True, cli_name='principal_expiration', multivalue=False, required=False)
+option: Str('krbprincipalname', attribute=True, autofill=True, cli_name='principal', multivalue=False, required=False)
+option: Str('l', attribute=True, cli_name='city', multivalue=False, required=False)
+option: Str('loginshell', attribute=True, cli_name='shell', multivalue=False, required=False)
+option: Str('mail', attribute=True, cli_name='email', multivalue=True, required=False)
+option: Str('manager', attribute=True, cli_name='manager', multivalue=False, required=False)
+option: Str('mobile', attribute=True, cli_name='mobile', multivalue=True, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Str('ou', attribute=True, cli_name='orgunit', multivalue=False, required=False)
+option: Str('pager', attribute=True, cli_name='pager', multivalue=True, required=False)
+option: Str('postalcode', attribute=True, cli_name='postalcode', multivalue=False, required=False)
+option: Str('preferredlanguage', attribute=True, cli_name='preferredlanguage', multivalue=False, pattern='^(([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\\=((0(\\.[0-9]{0,3})?)|(1(\\.0{0,3})?)))?(\\s*,\\s*[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\\=((0(\\.[0-9]{0,3})?)|(1(\\.0{0,3})?)))?)*)|(\\*))$', required=False)
+option: Flag('random', attribute=False, autofill=True, cli_name='random', default=False, multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('sn', attribute=True, cli_name='last', multivalue=False, required=True)
+option: Str('st', attribute=True, cli_name='state', multivalue=False, required=False)
+option: Str('street', attribute=True, cli_name='street', multivalue=False, required=False)
+option: Str('telephonenumber', attribute=True, cli_name='phone', multivalue=True, required=False)
+option: Str('title', attribute=True, cli_name='title', multivalue=False, required=False)
+option: Int('uidnumber', attribute=True, cli_name='uid', minvalue=1, multivalue=False, required=False)
+option: Str('userclass', attribute=True, cli_name='class', multivalue=True, required=False)
+option: Password('userpassword', attribute=True, cli_name='password', exclude='webui', multivalue=False, required=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: sudocmd_add
 args: 1,7,3
 arg: Str('sudocmd', attribute=True, cli_name='command', multivalue=False, primary_key=True, required=True)
diff --git a/VERSION b/VERSION
index 13c9760c82d587e8fbf9434dc15b58a902d94ba7..b584eb4584ea45881e5329a846dae0df7e231844 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=115
-# Last change: mbasti - Remove NSEC3PARAM record from dnsrecord-* commands
+IPA_API_VERSION_MINOR=116
+# Last change: tbordaz - Add stageuser_add command"
diff --git a/install/updates/30-provisioning.update b/install/updates/30-provisioning.update
index ef6d01a4441764fa7cb8cbb5a46ed14c32458c75..11e01df741f77fd8c3e08e675a88c165fb4d5863 100644
--- a/install/updates/30-provisioning.update
+++ b/install/updates/30-provisioning.update
@@ -1,21 +1,26 @@
 # bootstrap the user life cycle DIT structure.
 
 dn: cn=provisioning,$SUFFIX
-add: objectclass: top
-add: objectclass: nsContainer
-add: cn: provisioning
+default: objectclass: top
+default: objectclass: nsContainer
+default: cn: provisioning
 
 dn: cn=accounts,cn=provisioning,$SUFFIX
-add: objectclass: top
-add: objectclass: nsContainer
-add: cn: accounts
+default: objectclass: top
+default: objectclass: nsContainer
+default: cn: accounts
 
 dn: cn=staged users,cn=accounts,cn=provisioning,$SUFFIX
-add: objectclass: top
-add: objectclass: nsContainer
-add: cn: staged users
+default: objectclass: top
+default: objectclass: nsContainer
+default: cn: staged users
 
 dn: cn=deleted users,cn=accounts,cn=provisioning,$SUFFIX
-add: objectclass: top
-add: objectclass: nsContainer
-add: cn: staged users
+default: objectclass: top
+default: objectclass: nsContainer
+default: cn: staged users
+
+# This is used for the admin to know if credential are set for stage users
+# We can do a query on a DN to see if an attribute exists.
+dn: cn=staged users,cn=accounts,cn=provisioning,$SUFFIX
+add:aci: '(targetattr="userPassword || krbPrincipalKey")(version 3.0; acl "Search existence of password and kerberos keys"; allow(search) userdn = "ldap:///uid=admin,cn=users,cn=accounts,$SUFFIX";;)'
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 50a2b1f7aa7f0d447bacfd005b102c7451e670ce..f1e14702ffdf5a3bd23a62b1fdd2ee3cd95d84f8 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -78,6 +78,8 @@ DEFAULT_CONFIG = (
     # LDAP containers:
     ('container_accounts', DN(('cn', 'accounts'))),
     ('container_user', DN(('cn', 'users'), ('cn', 'accounts'))),
+    ('container_deleteuser', DN(('cn', 'deleted users'), ('cn', 'accounts'), ('cn', 'provisioning'))),
+    ('container_stageuser',  DN(('cn', 'staged 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/baseuser.py b/ipalib/plugins/baseuser.py
new file mode 100644
index 0000000000000000000000000000000000000000..16c7b2a88924969c90d795415ffef898dd7f3cb2
--- /dev/null
+++ b/ipalib/plugins/baseuser.py
@@ -0,0 +1,471 @@
+# Authors:
+#   Thierry Bordaz <tbor...@redhat.com>
+#
+# Copyright (C) 2014  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
+import string
+import posixpath
+import os
+
+from ipalib import api, errors
+from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import DN, LDAPObject, \
+    LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete, LDAPRetrieve
+from ipalib.plugins import baseldap
+from ipalib.request import context
+from ipalib import _, ngettext
+from ipalib import output
+from ipaplatform.paths import paths
+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__ = _("""
+Baseuser
+
+This contains common definitions for user/stageuser
+""")
+
+register = Registry()
+
+NO_UPG_MAGIC = '__no_upg__'
+
+baseuser_output_params = (
+    Flag('has_keytab',
+        label=_('Kerberos keys available'),
+    ),
+    Str('sshpubkeyfp*',
+        label=_('SSH public key fingerprint'),
+    ),
+   )
+
+status_baseuser_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'),
+    ),
+   )
+
+UPG_DEFINITION_DN = DN(('cn', 'UPG Definition'),
+                       ('cn', 'Definitions'),
+                       ('cn', 'Managed Entries'),
+                       ('cn', 'etc'),
+                       api.env.basedn)
+
+# characters to be used for generating random user passwords
+baseuser_pwdchars = string.digits + string.ascii_letters + '_,.@+-='
+
+def validate_nsaccountlock(entry_attrs):
+    if 'nsaccountlock' in entry_attrs:
+        nsaccountlock = entry_attrs['nsaccountlock']
+        if not isinstance(nsaccountlock, (bool, Bool)):
+            if not isinstance(nsaccountlock, basestring):
+                raise errors.OnlyOneValueAllowed(attr='nsaccountlock')
+            if nsaccountlock.lower() not in ('true', 'false'):
+                raise errors.ValidationError(name='nsaccountlock',
+                    error=_('must be TRUE or FALSE'))
+
+def radius_dn2pk(api, entry_attrs):
+    cl = entry_attrs.get('ipatokenradiusconfiglink', None)
+    if cl:
+        pk = api.Object['radiusproxy'].get_primary_key_from_dn(cl[0])
+        entry_attrs['ipatokenradiusconfiglink'] = [pk]
+
+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))
+
+
+
+def fix_addressbook_permission_bindrule(name, template, is_new,
+                                        anonymous_read_aci,
+                                        **other_options):
+    """Fix bind rule type for Read User Addressbook/IPA Attributes permission
+
+    When upgrading from an old IPA that had the global read ACI,
+    or when installing the first replica with granular read permissions,
+    we need to keep allowing anonymous access to many user attributes.
+    This fixup_function changes the bind rule type accordingly.
+    """
+    if is_new and anonymous_read_aci:
+        template['ipapermbindruletype'] = 'anonymous'
+
+
+
+class baseuser(LDAPObject):
+    """
+    baseuser object.
+    """
+
+    stage_container_dn        = api.env.container_stageuser
+    active_container_dn       = api.env.container_user
+    delete_container_dn       = api.env.container_deleteuser
+    object_class = ['posixaccount']
+    object_class_config = 'ipauserobjectclasses'
+    possible_objectclasses = [
+        'meporiginentry', 'ipauserauthtypeclass', 'ipauser',
+        'ipatokenradiusproxyuser'
+    ]
+    disallow_object_classes = ['krbticketpolicyaux']
+    permission_filter_objectclasses = ['posixaccount']
+    search_attributes_config = 'ipausersearchfields'
+    default_attributes = [
+        'uid', 'givenname', 'sn', 'homedirectory', 'loginshell',
+        'uidnumber', 'gidnumber', 'mail', 'ou',
+        'telephonenumber', 'title', 'memberof', 'nsaccountlock',
+        'memberofindirect', 'ipauserauthtype', 'userclass',
+        'ipatokenradiusconfiglink', 'ipatokenradiususername',
+        'krbprincipalexpiration'
+    ]
+    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 = _('Users')
+    label_singular = _('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),
+        ),
+        DateTime('krbprincipalexpiration?',
+            cli_name='principal_expiration',
+            label=_('Kerberos principal expiration'),
+        ),
+        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'],
+        ),
+        StrEnum('ipauserauthtype*',
+            cli_name='user_auth_type',
+            label=_('User authentication types'),
+            doc=_('Types of supported user authentication'),
+            values=(u'password', u'radius', u'otp'),
+            csv=True,
+        ),
+        Str('userclass*',
+            cli_name='class',
+            label=_('Class'),
+            doc=_('User category (semantics placed on this attribute are for '
+                  'local interpretation)'),
+        ),
+        Str('ipatokenradiusconfiglink?',
+            cli_name='radius',
+            label=_('RADIUS proxy configuration'),
+        ),
+        Str('ipatokenradiususername?',
+            cli_name='radius_username',
+            label=_('RADIUS proxy username'),
+        ),
+        Str('departmentnumber*',
+            label=_('Department Number'),
+        ),
+        Str('employeenumber?',
+            label=_('Employee Number'),
+        ),
+        Str('employeetype?',
+            label=_('Employee Type'),
+        ),
+        Str('preferredlanguage?',
+            label=_('Preferred Language'),
+            pattern='^(([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?' \
+             + '(\s*,\s*[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?)*)|(\*))$',
+            pattern_errmsg='must match RFC 2068 - 14.4, e.g., "da, en-gb;q=0.8, en;q=0.7"',
+        ),
+    )
+
+    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 normalize_manager(self, manager, container):
+        """
+        Given a userid verify the user's existence (in the appropriate containter) and return the dn.
+        """
+        if not manager:
+            return None
+
+        if not isinstance(manager, list):
+            manager = [manager]
+        try:
+            container_dn = DN(container, 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])
+
+class baseuser_add(LDAPCreate):
+    """
+    Prototype command plugin to be implemented by real plugin
+    """
+
+class baseuser_del(LDAPDelete):
+    """
+    Prototype command plugin to be implemented by real plugin
+    """
+
+class baseuser_mod(LDAPUpdate):
+    """
+    Prototype command plugin to be implemented by real plugin
+    """
+
+class baseuser_find(LDAPSearch):
+    """
+    Prototype command plugin to be implemented by real plugin
+    """
+
+class baseuser_show(LDAPRetrieve):
+    """
+    Prototype command plugin to be implemented by real plugin
+    """
diff --git a/ipalib/plugins/stageuser.py b/ipalib/plugins/stageuser.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a9a7f41323837a78ff5133cd3c2ffe0c1a3a8a0
--- /dev/null
+++ b/ipalib/plugins/stageuser.py
@@ -0,0 +1,277 @@
+# Authors:
+#   Thierry Bordaz <tbor...@redhat.com>
+#
+# Copyright (C) 2014  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
+import string
+import posixpath
+import os
+
+from ipalib import api, errors
+from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import LDAPCreate, DN, entry_to_dict
+from ipalib.plugins import baseldap
+from ipalib.plugins.baseuser import baseuser, baseuser_add, baseuser_mod, baseuser_find, \
+    NO_UPG_MAGIC, radius_dn2pk, \
+    baseuser_pwdchars, fix_addressbook_permission_bindrule, normalize_principal, validate_principal, \
+    baseuser_output_params, status_baseuser_output_params
+
+from ipalib.request import context
+from ipalib import _, ngettext
+from ipalib import output
+from ipaplatform.paths import paths
+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 stage user entries.
+
+Stage user entries are directly under the container: "cn=stage users,
+cn=accounts, cn=provisioning, SUFFIX".
+User can not authenticate with those entries (even if the entries
+contain credentials) and are candidate to become Active entries.
+
+Active user entries are Posix users directly under the container: "cn=accounts, SUFFIX".
+User can authenticate with Active entries, at the condition they have
+credentials
+
+Delete user enties are Posix users directly under the container: "cn=deleted users,
+cn=accounts, cn=provisioning, SUFFIX".
+User can not authenticate with those entries (even if the entries contain credentials)
+
+The stage user container contains entries
+    - created by 'stageuser-add' commands that are Posix users
+    - created by external provisioning system
+
+A valid stage user entry MUST:
+    - entry RDN is 'uid'
+    - ipaUniqueID is 'autogenerate'
+
+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.
+
+
+EXAMPLES:
+
+ Add a new stageuser:
+   ipa stageuser-add --first=Tim --last=User --password tuser1
+
+ Add a stageuser from the Delete container
+   ipa stageuser-add  --first=Tim --last=User --from-delete tuser1
+
+""")
+
+register = Registry()
+
+
+stageuser_output_params = baseuser_output_params
+
+status_output_params = status_baseuser_output_params
+
+@register()
+class stageuser(baseuser):
+    """
+    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) through cos
+    """
+
+    container_dn              = baseuser.stage_container_dn
+    label                     = _('Stage Users')
+    label_singular            = _('Stage User')
+    object_name               = _('stage user')
+    object_name_plural        = _('stage users')
+    managed_permissions       = {}
+
+@register()
+class stageuser_add(baseuser_add):
+    __doc__ = _('Add a new stage user.')
+
+    msg_summary = _('Added stage user "%(value)s"')
+
+    has_output_params = baseuser_add.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
+
+
+        # 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])
+                    )
+                )
+        default_shell = config.get('ipadefaultloginshell', [paths.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', [paths.HOME_DIR])[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(baseuser_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'], self.obj.active_container_dn)
+
+        if ('objectclass' in entry_attrs
+            and 'userclass' in entry_attrs
+            and 'ipauser' not in entry_attrs['objectclass']):
+            entry_attrs['objectclass'].append('ipauser')
+
+        if 'ipatokenradiusconfiglink' in entry_attrs:
+            cl = entry_attrs['ipatokenradiusconfiglink']
+            if cl:
+                if 'objectclass' not in entry_attrs:
+                    _entry = ldap.get_entry(dn, ['objectclass'])
+                    entry_attrs['objectclass'] = _entry['objectclass']
+
+                if 'ipatokenradiusproxyuser' not in entry_attrs['objectclass']:
+                    entry_attrs['objectclass'].append('ipatokenradiusproxyuser')
+
+                answer = self.api.Object['radiusproxy'].get_dn_if_exists(cl)
+                entry_attrs['ipatokenradiusconfiglink'] = answer
+
+        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)
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+        config = ldap.get_ipa_config()
+
+        # Fetch the entry again to update memberof, mep data, etc updated
+        # at the end of the transaction.
+        newentry = ldap.get_entry(dn, ['*'])
+        entry_attrs.update(newentry)
+
+        if options.get('random', False):
+            try:
+                entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword'))
+            except AttributeError:
+                # if both randompassword and userpassword options were used
+                pass
+
+        self.obj.get_password_attributes(ldap, dn, entry_attrs)
+        convert_sshpubkey_post(ldap, dn, entry_attrs)
+        radius_dn2pk(self.api, entry_attrs)
+        return dn
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index abe5ee26b8e48681eeb0cbb3bcff8617e212225c..dea946e35183319eeda65dd9c86b65d9adee2b8f 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -25,6 +25,12 @@ import os
 
 from ipalib import api, errors
 from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime
+from ipalib.plugins.baseuser import baseuser, baseuser_add, baseuser_del, \
+    baseuser_mod, baseuser_find, baseuser_show, \
+    NO_UPG_MAGIC, UPG_DEFINITION_DN, baseuser_output_params, \
+    status_baseuser_output_params, baseuser_pwdchars, \
+    validate_nsaccountlock, radius_dn2pk, convert_nsaccountlock, split_principal, validate_principal, \
+    normalize_principal, fix_addressbook_permission_bindrule
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import *
 from ipalib.plugins import baseldap
@@ -85,105 +91,10 @@ EXAMPLES:
 
 register = Registry()
 
-NO_UPG_MAGIC = '__no_upg__'
 
-user_output_params = (
-    Flag('has_keytab',
-        label=_('Kerberos keys available'),
-    ),
-    Str('sshpubkeyfp*',
-        label=_('SSH public key fingerprint'),
-    ),
-   )
+user_output_params = baseuser_output_params
 
-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'),
-    ),
-   )
-
-UPG_DEFINITION_DN = DN(('cn', 'UPG Definition'),
-                       ('cn', 'Definitions'),
-                       ('cn', 'Managed Entries'),
-                       ('cn', 'etc'),
-                       api.env.basedn)
-
-# characters to be used for generating random user passwords
-user_pwdchars = string.digits + string.ascii_letters + '_,.@+-='
-
-def validate_nsaccountlock(entry_attrs):
-    if 'nsaccountlock' in entry_attrs:
-        nsaccountlock = entry_attrs['nsaccountlock']
-        if not isinstance(nsaccountlock, (bool, Bool)):
-            if not isinstance(nsaccountlock, basestring):
-                raise errors.OnlyOneValueAllowed(attr='nsaccountlock')
-            if nsaccountlock.lower() not in ('true', 'false'):
-                raise errors.ValidationError(name='nsaccountlock',
-                    error=_('must be TRUE or FALSE'))
-
-def radius_dn2pk(api, entry_attrs):
-    cl = entry_attrs.get('ipatokenradiusconfiglink', None)
-    if cl:
-        pk = api.Object['radiusproxy'].get_primary_key_from_dn(cl[0])
-        entry_attrs['ipatokenradiusconfiglink'] = [pk]
-
-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))
+status_output_params = status_baseuser_output_params
 
 
 def check_protected_member(user, protected_group_name=u'admins'):
@@ -204,60 +115,17 @@ def check_protected_member(user, protected_group_name=u'admins'):
         raise errors.LastMemberError(key=user, label=_(u'group'),
             container=protected_group_name)
 
-
-def fix_addressbook_permission_bindrule(name, template, is_new,
-                                        anonymous_read_aci,
-                                        **other_options):
-    """Fix bind rule type for Read User Addressbook/IPA Attributes permission
-
-    When upgrading from an old IPA that had the global read ACI,
-    or when installing the first replica with granular read permissions,
-    we need to keep allowing anonymous access to many user attributes.
-    This fixup_function changes the bind rule type accordingly.
-    """
-    if is_new and anonymous_read_aci:
-        template['ipapermbindruletype'] = 'anonymous'
-
-
 @register()
-class user(LDAPObject):
+class user(baseuser):
     """
     User object.
     """
-    container_dn = api.env.container_user
-    object_name = _('user')
-    object_name_plural = _('users')
-    object_class = ['posixaccount']
-    object_class_config = 'ipauserobjectclasses'
-    possible_objectclasses = [
-        'meporiginentry', 'ipauserauthtypeclass', 'ipauser',
-        'ipatokenradiusproxyuser'
-    ]
-    disallow_object_classes = ['krbticketpolicyaux']
-    permission_filter_objectclasses = ['posixaccount']
-    search_attributes_config = 'ipausersearchfields'
-    default_attributes = [
-        'uid', 'givenname', 'sn', 'homedirectory', 'loginshell',
-        'uidnumber', 'gidnumber', 'mail', 'ou',
-        'telephonenumber', 'title', 'memberof', 'nsaccountlock',
-        'memberofindirect', 'ipauserauthtype', 'userclass',
-        'ipatokenradiusconfiglink', 'ipatokenradiususername',
-        'krbprincipalexpiration'
-    ]
-    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')]
+
+    container_dn              = baseuser.active_container_dn
+    label                     = _('Users')
+    label_singular            = _('User')
+    object_name               = _('user')
+    object_name_plural        = _('users')
     managed_permissions = {
         'System: Read User Standard Attributes': {
             'replaces_global_anonymous_aci': True,
@@ -460,259 +328,28 @@ class user(LDAPObject):
         },
     }
 
-    label = _('Users')
-    label_singular = _('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),
-        ),
-        DateTime('krbprincipalexpiration?',
-            cli_name='principal_expiration',
-            label=_('Kerberos principal expiration'),
-        ),
-        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'),
-        ),
+    takes_params = baseuser.takes_params + (
         Bool('nsaccountlock?',
             label=_('Account disabled'),
             flags=['no_option'],
         ),
-        Str('ipasshpubkey*', validate_sshpubkey,
-            cli_name='sshpubkey',
-            label=_('SSH public key'),
-            normalizer=normalize_sshpubkey,
-            csv=True,
-            flags=['no_search'],
-        ),
-        StrEnum('ipauserauthtype*',
-            cli_name='user_auth_type',
-            label=_('User authentication types'),
-            doc=_('Types of supported user authentication'),
-            values=(u'password', u'radius', u'otp'),
-            csv=True,
-        ),
-        Str('userclass*',
-            cli_name='class',
-            label=_('Class'),
-            doc=_('User category (semantics placed on this attribute are for '
-                  'local interpretation)'),
-        ),
-        Str('ipatokenradiusconfiglink?',
-            cli_name='radius',
-            label=_('RADIUS proxy configuration'),
-        ),
-        Str('ipatokenradiususername?',
-            cli_name='radius_username',
-            label=_('RADIUS proxy username'),
-        ),
-        Str('departmentnumber*',
-            label=_('Department Number'),
-        ),
-        Str('employeenumber?',
-            label=_('Employee Number'),
-        ),
-        Str('employeetype?',
-            label=_('Employee Type'),
-        ),
-        Str('preferredlanguage?',
-            label=_('Preferred Language'),
-            pattern='^(([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?' \
-             + '(\s*,\s*[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?)*)|(\*))$',
-            pattern_errmsg='must match RFC 2068 - 14.4, e.g., "da, en-gb;q=0.8, en;q=0.7"',
-        ),
     )
 
-    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 _normalize_manager(self, manager):
         """
         Given a userid verify the user's existence and return the dn.
         """
-        if not manager:
-            return None
-
-        if not isinstance(manager, list):
-            manager = [manager]
-        try:
-            container_dn = DN(self.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])
+        return super(user, self).normalize_manager(manager, self.active_container_dn)
 
 
 @register()
-class user_add(LDAPCreate):
+class user_add(baseuser_add):
     __doc__ = _('Add a new user.')
 
     msg_summary = _('Added user "%(value)s"')
 
-    has_output_params = LDAPCreate.has_output_params + user_output_params
+    has_output_params = baseuser_add.has_output_params + user_output_params
 
     takes_options = LDAPCreate.takes_options + (
         Flag('noprivate',
@@ -798,21 +435,21 @@ class user_add(LDAPCreate):
                 entry_attrs['gidnumber'] = group_attrs['gidnumber']
 
         if 'userpassword' not in entry_attrs and options.get('random'):
-            entry_attrs['userpassword'] = ipa_generate_password(user_pwdchars)
+            entry_attrs['userpassword'] = ipa_generate_password(baseuser_pwdchars)
             # save the password so it can be displayed in post_callback
             setattr(context, 'randompassword', entry_attrs['userpassword'])
 
         if 'mail' in entry_attrs:
-            entry_attrs['mail'] = self.obj._normalize_and_validate_email(entry_attrs['mail'], config)
+            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)
+                entry_attrs['mail'] = self.obj.normalize_and_validate_email(keys[-1], config)
 
         if 'manager' in entry_attrs:
-            entry_attrs['manager'] = self.obj._normalize_manager(entry_attrs['manager'])
+            entry_attrs['manager'] = self.obj.normalize_manager(entry_attrs['manager'], self.obj.active_container_dn)
 
         if 'userclass' in entry_attrs and \
            'ipauser' not in entry_attrs['objectclass']:
@@ -847,7 +484,7 @@ class user_add(LDAPCreate):
         except errors.AlreadyGroupMember:
             pass
 
-        self.obj._convert_manager(entry_attrs, **options)
+        self.obj.convert_manager(entry_attrs, **options)
         # delete description attribute NO_UPG_MAGIC if present
         if options.get('noprivate', False):
             if not options.get('all', False):
@@ -880,7 +517,7 @@ class user_add(LDAPCreate):
 
 
 @register()
-class user_del(LDAPDelete):
+class user_del(baseuser_del):
     __doc__ = _('Delete a user.')
 
     msg_summary = _('Deleted user "%(value)s"')
@@ -905,12 +542,12 @@ class user_del(LDAPDelete):
 
 
 @register()
-class user_mod(LDAPUpdate):
+class user_mod(baseuser_mod):
     __doc__ = _('Modify a user.')
 
     msg_summary = _('Modified user "%(value)s"')
 
-    has_output_params = LDAPUpdate.has_output_params + user_output_params
+    has_output_params = baseuser_mod.has_output_params + user_output_params
 
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
         assert isinstance(dn, DN)
@@ -925,12 +562,12 @@ class user_mod(LDAPUpdate):
                         )
                     )
         if 'mail' in entry_attrs:
-            entry_attrs['mail'] = self.obj._normalize_and_validate_email(entry_attrs['mail'])
+            entry_attrs['mail'] = self.obj.normalize_and_validate_email(entry_attrs['mail'])
         if 'manager' in entry_attrs:
-            entry_attrs['manager'] = self.obj._normalize_manager(entry_attrs['manager'])
+            entry_attrs['manager'] = self.obj.normalize_manager(entry_attrs['manager'], self.obj.active_container_dn)
         validate_nsaccountlock(entry_attrs)
         if 'userpassword' not in entry_attrs and options.get('random'):
-            entry_attrs['userpassword'] = ipa_generate_password(user_pwdchars)
+            entry_attrs['userpassword'] = ipa_generate_password(baseuser_pwdchars)
             # save the password so it can be displayed in post_callback
             setattr(context, 'randompassword', entry_attrs['userpassword'])
         if ('ipasshpubkey' in entry_attrs or 'ipauserauthtype' in entry_attrs
@@ -970,7 +607,7 @@ class user_mod(LDAPUpdate):
                 # if both randompassword and userpassword options were used
                 pass
         convert_nsaccountlock(entry_attrs)
-        self.obj._convert_manager(entry_attrs, **options)
+        self.obj.convert_manager(entry_attrs, **options)
         self.obj.get_password_attributes(ldap, dn, entry_attrs)
         convert_sshpubkey_post(ldap, dn, entry_attrs)
         radius_dn2pk(self.api, entry_attrs)
@@ -978,11 +615,11 @@ class user_mod(LDAPUpdate):
 
 
 @register()
-class user_find(LDAPSearch):
+class user_find(baseuser_find):
     __doc__ = _('Search for users.')
 
     member_attributes = ['memberof']
-    has_output_params = LDAPSearch.has_output_params + user_output_params
+    has_output_params = baseuser_find.has_output_params + user_output_params
 
     takes_options = LDAPSearch.takes_options + (
         Flag('whoami',
@@ -995,7 +632,7 @@ class user_find(LDAPSearch):
         # 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)
+            options['manager'] = self.obj.normalize_manager(manager, self.obj.active_container_dn)
 
         # Ensure that the RADIUS config link is a dn, not just the name
         cl = 'ipatokenradiusconfiglink'
@@ -1016,7 +653,7 @@ class user_find(LDAPSearch):
         if options.get('pkey_only', False):
             return truncated
         for attrs in entries:
-            self.obj._convert_manager(attrs, **options)
+            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)
@@ -1028,15 +665,15 @@ class user_find(LDAPSearch):
 
 
 @register()
-class user_show(LDAPRetrieve):
+class user_show(baseuser_show):
     __doc__ = _('Display information about a user.')
 
-    has_output_params = LDAPRetrieve.has_output_params + user_output_params
+    has_output_params = baseuser_show.has_output_params + user_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.convert_manager(entry_attrs, **options)
         self.obj.get_password_attributes(ldap, dn, entry_attrs)
         convert_sshpubkey_post(ldap, dn, entry_attrs)
         radius_dn2pk(self.api, entry_attrs)
-- 
1.7.11.7

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to