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.

--
David Kupka
From 0aa8d71acfb6ae63c5cb9a8ab02ad67c7d15a430 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] 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;
-- 
2.1.0

From 85a4754d8e419c0766860b1dab0f0e1ca595e753 Mon Sep 17 00:00:00 2001
From: "Thierry bordaz (tbordaz)" <tbor...@redhat.com>
Date: Fri, 8 Aug 2014 14:14:36 +0200
Subject: [PATCH] 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.

https://fedorahosted.org/freeipa/ticket/3813
---
 ACI.txt                                |  34 --
 API.txt                                |  49 +++
 install/updates/30-provisioning.update |  29 +-
 ipalib/constants.py                    |   2 +
 ipalib/plugins/accounts.py             | 638 +++++++++++++++++++++++++++++++++
 ipalib/plugins/stageuser.py            | 303 ++++++++++++++++
 ipalib/plugins/user.py                 | 632 ++------------------------------
 7 files changed, 1039 insertions(+), 648 deletions(-)
 create mode 100644 ipalib/plugins/accounts.py
 create mode 100644 ipalib/plugins/stageuser.py

diff --git a/ACI.txt b/ACI.txt
index c5483ad4d3428c0449f3e099600e0384e573f17a..c9ddfe86a2fb2078dee46b52291c7a84ec5a1735 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -244,40 +244,6 @@ dn: cn=trusts,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || entryusn || ipantflatname || ipantsecurityidentifier || ipantsidblacklistincoming || ipantsidblacklistoutgoing || ipanttrusteddomainsid || ipanttrustpartner || modifytimestamp || objectclass")(version 3.0;acl "permission:System: Read Trust Information";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: cn=trusts,dc=ipa,dc=example
 aci: (targetattr = "gidnumber || krbprincipalname || uidnumber")(version 3.0;acl "permission:System: Read system trust accounts";allow (compare,read,search) groupdn = "ldap:///cn=System: Read system trust accounts,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=groups,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "member")(target = "ldap:///cn=ipausers,cn=groups,cn=accounts,dc=ipa,dc=example";)(version 3.0;acl "permission:System: Add User to default group";allow (write) groupdn = "ldap:///cn=System: Add User to default group,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Add Users";allow (add) groupdn = "ldap:///cn=System: Add Users,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "krbprincipalkey || passwordhistory || sambalmpassword || sambantpassword || userpassword")(targetfilter = "(&(!(memberOf=cn=admins,cn=groups,cn=accounts,dc=ipa,dc=example))(objectclass=posixaccount))")(version 3.0;acl "permission:System: Change User password";allow (write) groupdn = "ldap:///cn=System: Change User password,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "ipasshpubkey")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Manage User SSH Public Keys";allow (write) groupdn = "ldap:///cn=System: Manage User SSH Public Keys,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "businesscategory || carlicense || cn || description || displayname || employeetype || facsimiletelephonenumber || gecos || givenname || homephone || inetuserhttpurl || initials || l || labeleduri || loginshell || manager || mepmanagedentry || mobile || objectclass || ou || pager || postalcode || preferredlanguage || roomnumber || secretary || seealso || sn || st || street || telephonenumber || title || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Modify Users";allow (write) groupdn = "ldap:///cn=System: Modify Users,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc,dc=ipa,dc=example
-aci: (targetattr = "*")(target = "ldap:///cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read UPG Definition";allow (compare,read,search) groupdn = "ldap:///cn=System: Read UPG Definition,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "audio || businesscategory || carlicense || departmentnumber || destinationindicator || employeenumber || employeetype || fax || homephone || homepostaladdress || inetuserhttpurl || inetuserstatus || internationalisdnnumber || jpegphoto || l || labeleduri || mail || mobile || o || ou || pager || photo || physicaldeliveryofficename || postaladdress || postalcode || postofficebox || preferreddeliverymethod || preferredlanguage || registeredaddress || roomnumber || secretary || seealso || st || street || telephonenumber || teletexterminalidentifier || telexnumber || usercertificate || usersmimecertificate || x121address || x500uniqueidentifier")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Addressbook Attributes";allow (compare,read,search) userdn = "ldap:///all";;)
-dn: dc=ipa,dc=example
-aci: (targetattr = "cn || createtimestamp || entryusn || gecos || gidnumber || homedirectory || loginshell || modifytimestamp || objectclass || uid || uidnumber")(target = "ldap:///cn=users,cn=compat,dc=ipa,dc=example";)(version 3.0;acl "permission:System: Read User Compat Tree";allow (compare,read,search) userdn = "ldap:///anyone";;)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "ipasshpubkey || ipauniqueid || ipauserauthtype || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User IPA Attributes";allow (compare,read,search) userdn = "ldap:///all";;)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "krbcanonicalname || krblastpwdchange || krbpasswordexpiration || krbprincipalaliases || krbprincipalexpiration || krbprincipalname || krbprincipaltype || nsaccountlock")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Kerberos Attributes";allow (compare,read,search) userdn = "ldap:///all";;)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "krblastadminunlock || krblastfailedauth || krblastpwdchange || krblastsuccessfulauth || krbloginfailedcount || krbpwdpolicyreference || krbticketpolicyreference || krbupenabled")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Kerberos Login Attributes";allow (compare,read,search) groupdn = "ldap:///cn=System: Read User Kerberos Login Attributes,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "memberof")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Membership";allow (compare,read,search) userdn = "ldap:///all";;)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "ntuniqueid || ntuseracctexpires || ntusercodepage || ntuserdeleteaccount || ntuserdomainid || ntuserlastlogoff || ntuserlastlogon")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User NT Attributes";allow (compare,read,search) groupdn = "ldap:///cn=System: Read User NT Attributes,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "cn || createtimestamp || description || displayname || entryusn || gecos || gidnumber || givenname || homedirectory || initials || ipantsecurityidentifier || loginshell || manager || modifytimestamp || objectclass || sn || title || uid || uidnumber")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Standard Attributes";allow (compare,read,search) userdn = "ldap:///anyone";;)
-dn: dc=ipa,dc=example
-aci: (targetattr = "cn || createtimestamp || entryusn || gecos || gidnumber || homedirectory || loginshell || modifytimestamp || objectclass || uid || uidnumber")(target = "ldap:///cn=users,cn=*,cn=views,cn=compat,dc=ipa,dc=example";)(version 3.0;acl "permission:System: Read User Views Compat Tree";allow (compare,read,search) userdn = "ldap:///anyone";;)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Remove Users";allow (delete) groupdn = "ldap:///cn=System: Remove Users,cn=permissions,cn=pbac,dc=ipa,dc=example";)
-dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "krblastadminunlock || krbloginfailedcount || nsaccountlock")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Unlock User";allow (write) groupdn = "ldap:///cn=System: Unlock User,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=ca_renewal,cn=ipa,cn=etc,dc=ipa,dc=example
 aci: (target = "ldap:///cn=caSigningCert cert-pki-ca,cn=ca_renewal,cn=ipa,cn=etc,dc=ipa,dc=example")(targetfilter = "(objectclass=pkiuser)")(version 3.0;acl "permission:System: Add CA Certificate For Renewal";allow (add) groupdn = "ldap:///cn=System: Add CA Certificate For Renewal,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=certificates,cn=ipa,cn=etc,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 08615c80476171695dac262575c4e20e72615c36..489b2703c9575217cd01ddb56037f8e1d4011120 100644
--- a/API.txt
+++ b/API.txt
@@ -3695,6 +3695,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/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/accounts.py b/ipalib/plugins/accounts.py
new file mode 100644
index 0000000000000000000000000000000000000000..33969d33acac978922a337877db051816fc5d737
--- /dev/null
+++ b/ipalib/plugins/accounts.py
@@ -0,0 +1,638 @@
+# 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
+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__ = _("""
+Accounts
+
+This contains common definitions for user/stageuser
+""")
+
+register = Registry()
+
+NO_UPG_MAGIC = '__no_upg__'
+
+accounts_output_params = (
+    Flag('has_keytab',
+        label=_('Kerberos keys available'),
+    ),
+    Str('sshpubkeyfp*',
+        label=_('SSH public key fingerprint'),
+    ),
+   )
+
+status_accounts_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
+accounts_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'
+
+
+
+@register()
+class accounts(LDAPObject):
+    """
+    Accounts object.
+    """
+
+    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')]
+    active_user_managed_permissions = {
+        'System: Read User Standard Attributes': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'anonymous',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'objectclass', 'cn', 'sn', 'description', 'title', 'uid',
+                'displayname', 'givenname', 'initials', 'manager', 'gecos',
+                'gidnumber', 'homedirectory', 'loginshell', 'uidnumber',
+                'ipantsecurityidentifier'
+            },
+        },
+        'System: Read User Addressbook Attributes': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'seealso', 'telephonenumber',
+                'fax', 'l', 'ou', 'st', 'postalcode', 'street',
+                'destinationindicator', 'internationalisdnnumber',
+                'physicaldeliveryofficename', 'postaladdress', 'postofficebox',
+                'preferreddeliverymethod', 'registeredaddress',
+                'teletexterminalidentifier', 'telexnumber', 'x121address',
+                'carlicense', 'departmentnumber', 'employeenumber',
+                'employeetype', 'preferredlanguage', 'mail', 'mobile', 'pager',
+                'audio', 'businesscategory', 'homephone', 'homepostaladdress',
+                'jpegphoto', 'labeleduri', 'o', 'photo', 'roomnumber',
+                'secretary', 'usercertificate',
+                'usersmimecertificate', 'x500uniqueidentifier',
+                'inetuserhttpurl', 'inetuserstatus',
+            },
+            'fixup_function': fix_addressbook_permission_bindrule,
+        },
+        'System: Read User IPA Attributes': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'ipauniqueid', 'ipasshpubkey', 'ipauserauthtype', 'userclass',
+            },
+            'fixup_function': fix_addressbook_permission_bindrule,
+        },
+        'System: Read User Kerberos Attributes': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'krbprincipalname', 'krbcanonicalname', 'krbprincipalaliases',
+                'krbprincipalexpiration', 'krbpasswordexpiration',
+                'krblastpwdchange', 'nsaccountlock', 'krbprincipaltype',
+            },
+        },
+        'System: Read User Kerberos Login Attributes': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'krblastsuccessfulauth', 'krblastfailedauth',
+                'krblastpwdchange', 'krblastadminunlock',
+                'krbloginfailedcount', 'krbpwdpolicyreference',
+                'krbticketpolicyreference', 'krbupenabled',
+            },
+            'default_privileges': {'User Administrators'},
+        },
+        'System: Read User Membership': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'memberof',
+            },
+        },
+        'System: Read UPG Definition': {
+            # Required for adding users
+            'replaces_global_anonymous_aci': True,
+            'non_object': True,
+            'ipapermlocation': UPG_DEFINITION_DN,
+            'ipapermtarget': UPG_DEFINITION_DN,
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {'*'},
+            'default_privileges': {'User Administrators'},
+        },
+        'System: Add Users': {
+            'ipapermright': {'add'},
+            'replaces': [
+                '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Add Users";allow (add) groupdn = "ldap:///cn=Add Users,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'User Administrators'},
+        },
+        'System: Add User to default group': {
+            'non_object': True,
+            'ipapermright': {'write'},
+            'ipapermlocation': DN(api.env.container_group, api.env.basedn),
+            'ipapermtarget': DN('cn=ipausers', api.env.container_group,
+                                api.env.basedn),
+            'ipapermdefaultattr': {'member'},
+            'replaces': [
+                '(targetattr = "member")(target = "ldap:///cn=ipausers,cn=groups,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Add user to default group";allow (write) groupdn = "ldap:///cn=Add user to default group,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'User Administrators'},
+        },
+        'System: Change User password': {
+            'ipapermright': {'write'},
+            'ipapermtargetfilter': [
+                '(objectclass=posixaccount)',
+                '(!(memberOf=%s))' % DN('cn=admins',
+                                        api.env.container_group,
+                                        api.env.basedn),
+            ],
+            'ipapermdefaultattr': {
+                'krbprincipalkey', 'passwordhistory', 'sambalmpassword',
+                'sambantpassword', 'userpassword'
+            },
+            'replaces': [
+                '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)',
+                '(targetfilter = "(!(memberOf=cn=admins,cn=groups,cn=accounts,$SUFFIX))")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {
+                'User Administrators',
+                'Modify Users and Reset passwords',
+            },
+        },
+        'System: Manage User SSH Public Keys': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {'ipasshpubkey'},
+            'replaces': [
+                '(targetattr = "ipasshpubkey")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Manage User SSH Public Keys";allow (write) groupdn = "ldap:///cn=Manage User SSH Public Keys,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'User Administrators'},
+        },
+        'System: Modify Users': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'businesscategory', 'carlicense', 'cn', 'description',
+                'displayname', 'employeetype', 'facsimiletelephonenumber',
+                'gecos', 'givenname', 'homephone', 'inetuserhttpurl',
+                'initials', 'l', 'labeleduri', 'loginshell', 'manager',
+                'mepmanagedentry', 'mobile', 'objectclass', 'ou', 'pager',
+                'postalcode', 'roomnumber', 'secretary', 'seealso', 'sn', 'st',
+                'street', 'telephonenumber', 'title', 'userclass',
+                'preferredlanguage',
+            },
+            'replaces': [
+                '(targetattr = "givenname || sn || cn || displayname || title || initials || loginshell || gecos || homephone || mobile || pager || facsimiletelephonenumber || telephonenumber || street || roomnumber || l || st || postalcode || manager || secretary || description || carlicense || labeleduri || inetuserhttpurl || seealso || employeetype || businesscategory || ou || mepmanagedentry || objectclass")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Modify Users";allow (write) groupdn = "ldap:///cn=Modify Users,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {
+                'User Administrators',
+                'Modify Users and Reset passwords',
+            },
+        },
+        'System: Remove Users': {
+            'ipapermright': {'delete'},
+            'replaces': [
+                '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Remove Users";allow (delete) groupdn = "ldap:///cn=Remove Users,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'User Administrators'},
+        },
+        'System: Unlock User': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'krblastadminunlock', 'krbloginfailedcount', 'nsaccountlock',
+            },
+            'replaces': [
+                '(targetattr = "krbLastAdminUnlock || krbLoginFailedCount")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Unlock user accounts";allow (write) groupdn = "ldap:///cn=Unlock user accounts,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'User Administrators'},
+        },
+        'System: Read User Compat Tree': {
+            'non_object': True,
+            'ipapermbindruletype': 'anonymous',
+            'ipapermlocation': api.env.basedn,
+            'ipapermtarget': DN('cn=users', 'cn=compat', api.env.basedn),
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'objectclass', 'uid', 'cn', 'gecos', 'gidnumber', 'uidnumber',
+                'homedirectory', 'loginshell',
+            },
+        },
+        'System: Read User Views Compat Tree': {
+            'non_object': True,
+            'ipapermbindruletype': 'anonymous',
+            'ipapermlocation': api.env.basedn,
+            'ipapermtarget': DN('cn=users', 'cn=*', 'cn=views', 'cn=compat', api.env.basedn),
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'objectclass', 'uid', 'cn', 'gecos', 'gidnumber', 'uidnumber',
+                'homedirectory', 'loginshell',
+            },
+        },
+    }
+    stage_user_managed_permissions = {}
+
+    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'),
+        ),
+        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, 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])
diff --git a/ipalib/plugins/stageuser.py b/ipalib/plugins/stageuser.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b60634b7b59912ade6bf5e0020fc9cd84227de3
--- /dev/null
+++ b/ipalib/plugins/stageuser.py
@@ -0,0 +1,303 @@
+# 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.accounts import accounts, NO_UPG_MAGIC, radius_dn2pk, \
+    accounts_pwdchars, fix_addressbook_permission_bindrule, normalize_principal, validate_principal, \
+    accounts_output_params, status_accounts_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 = accounts_output_params
+
+status_output_params = status_accounts_output_params
+
+@register()
+class stageuser(accounts):
+    """
+    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               = _('stage user')
+    object_name_plural        = _('stage users')
+    object_class              = accounts.object_class
+    object_class_config       = accounts.object_class_config
+    possible_objectclasses    = accounts.possible_objectclasses
+    disallow_object_classes   = accounts.disallow_object_classes
+    permission_filter_objectclasses = accounts.permission_filter_objectclasses
+    search_attributes_config  = accounts.search_attributes_config
+    default_attributes        = accounts.default_attributes
+    search_display_attributes = accounts.search_display_attributes
+
+    uuid_attribute            = accounts.uuid_attribute
+    attribute_members         = accounts.attribute_members
+    rdn_is_primary_key        = accounts.rdn_is_primary_key
+    bindable                  = accounts.bindable
+    password_attributes       = accounts.password_attributes
+    managed_permissions       = accounts.stage_user_managed_permissions
+
+    label                     = _('Stage Users')
+    label_singular            = _('Stage User')
+
+    # take_params does not support 'nsaccountlock' option
+    stageuser_takes_params_list = []
+    for elt in accounts.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)
+
+
+@register()
+class stageuser_add(stageuser, 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
+
+
+        # 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(accounts_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.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 56585b9f86593c0c5879139103bc71707b88e15f..294d2d55df1dc6931dbf7e47aeada29e97ef3497 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -25,6 +25,10 @@ import os
 
 from ipalib import api, errors
 from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime
+from ipalib.plugins.accounts import accounts, NO_UPG_MAGIC, UPG_DEFINITION_DN, accounts_output_params, \
+    status_accounts_output_params, accounts_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 +89,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 = accounts_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_accounts_output_params
 
 
 def check_protected_member(user, protected_group_name=u'admins'):
@@ -204,510 +113,29 @@ 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(accounts):
     """
     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')]
-    managed_permissions = {
-        'System: Read User Standard Attributes': {
-            'replaces_global_anonymous_aci': True,
-            'ipapermbindruletype': 'anonymous',
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'objectclass', 'cn', 'sn', 'description', 'title', 'uid',
-                'displayname', 'givenname', 'initials', 'manager', 'gecos',
-                'gidnumber', 'homedirectory', 'loginshell', 'uidnumber',
-                'ipantsecurityidentifier'
-            },
-        },
-        'System: Read User Addressbook Attributes': {
-            'replaces_global_anonymous_aci': True,
-            'ipapermbindruletype': 'all',
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'seealso', 'telephonenumber',
-                'fax', 'l', 'ou', 'st', 'postalcode', 'street',
-                'destinationindicator', 'internationalisdnnumber',
-                'physicaldeliveryofficename', 'postaladdress', 'postofficebox',
-                'preferreddeliverymethod', 'registeredaddress',
-                'teletexterminalidentifier', 'telexnumber', 'x121address',
-                'carlicense', 'departmentnumber', 'employeenumber',
-                'employeetype', 'preferredlanguage', 'mail', 'mobile', 'pager',
-                'audio', 'businesscategory', 'homephone', 'homepostaladdress',
-                'jpegphoto', 'labeleduri', 'o', 'photo', 'roomnumber',
-                'secretary', 'usercertificate',
-                'usersmimecertificate', 'x500uniqueidentifier',
-                'inetuserhttpurl', 'inetuserstatus',
-            },
-            'fixup_function': fix_addressbook_permission_bindrule,
-        },
-        'System: Read User IPA Attributes': {
-            'replaces_global_anonymous_aci': True,
-            'ipapermbindruletype': 'all',
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'ipauniqueid', 'ipasshpubkey', 'ipauserauthtype', 'userclass',
-            },
-            'fixup_function': fix_addressbook_permission_bindrule,
-        },
-        'System: Read User Kerberos Attributes': {
-            'replaces_global_anonymous_aci': True,
-            'ipapermbindruletype': 'all',
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'krbprincipalname', 'krbcanonicalname', 'krbprincipalaliases',
-                'krbprincipalexpiration', 'krbpasswordexpiration',
-                'krblastpwdchange', 'nsaccountlock', 'krbprincipaltype',
-            },
-        },
-        'System: Read User Kerberos Login Attributes': {
-            'replaces_global_anonymous_aci': True,
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'krblastsuccessfulauth', 'krblastfailedauth',
-                'krblastpwdchange', 'krblastadminunlock',
-                'krbloginfailedcount', 'krbpwdpolicyreference',
-                'krbticketpolicyreference', 'krbupenabled',
-            },
-            'default_privileges': {'User Administrators'},
-        },
-        'System: Read User Membership': {
-            'replaces_global_anonymous_aci': True,
-            'ipapermbindruletype': 'all',
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'memberof',
-            },
-        },
-        'System: Read UPG Definition': {
-            # Required for adding users
-            'replaces_global_anonymous_aci': True,
-            'non_object': True,
-            'ipapermlocation': UPG_DEFINITION_DN,
-            'ipapermtarget': UPG_DEFINITION_DN,
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {'*'},
-            'default_privileges': {'User Administrators'},
-        },
-        'System: Add Users': {
-            'ipapermright': {'add'},
-            'replaces': [
-                '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Add Users";allow (add) groupdn = "ldap:///cn=Add Users,cn=permissions,cn=pbac,$SUFFIX";)',
-            ],
-            'default_privileges': {'User Administrators'},
-        },
-        'System: Add User to default group': {
-            'non_object': True,
-            'ipapermright': {'write'},
-            'ipapermlocation': DN(api.env.container_group, api.env.basedn),
-            'ipapermtarget': DN('cn=ipausers', api.env.container_group,
-                                api.env.basedn),
-            'ipapermdefaultattr': {'member'},
-            'replaces': [
-                '(targetattr = "member")(target = "ldap:///cn=ipausers,cn=groups,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Add user to default group";allow (write) groupdn = "ldap:///cn=Add user to default group,cn=permissions,cn=pbac,$SUFFIX";)',
-            ],
-            'default_privileges': {'User Administrators'},
-        },
-        'System: Change User password': {
-            'ipapermright': {'write'},
-            'ipapermtargetfilter': [
-                '(objectclass=posixaccount)',
-                '(!(memberOf=%s))' % DN('cn=admins',
-                                        api.env.container_group,
-                                        api.env.basedn),
-            ],
-            'ipapermdefaultattr': {
-                'krbprincipalkey', 'passwordhistory', 'sambalmpassword',
-                'sambantpassword', 'userpassword'
-            },
-            'replaces': [
-                '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)',
-                '(targetfilter = "(!(memberOf=cn=admins,cn=groups,cn=accounts,$SUFFIX))")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)',
-                '(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///uid=passsync,cn=sysaccounts,cn=etc,$SUFFIX";;)',
-            ],
-            'default_privileges': {
-                'User Administrators',
-                'Modify Users and Reset passwords',
-                'PassSync Service',
-            },
-        },
-        'System: Manage User SSH Public Keys': {
-            'ipapermright': {'write'},
-            'ipapermdefaultattr': {'ipasshpubkey'},
-            'replaces': [
-                '(targetattr = "ipasshpubkey")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Manage User SSH Public Keys";allow (write) groupdn = "ldap:///cn=Manage User SSH Public Keys,cn=permissions,cn=pbac,$SUFFIX";)',
-            ],
-            'default_privileges': {'User Administrators'},
-        },
-        'System: Modify Users': {
-            'ipapermright': {'write'},
-            'ipapermdefaultattr': {
-                'businesscategory', 'carlicense', 'cn', 'description',
-                'displayname', 'employeetype', 'facsimiletelephonenumber',
-                'gecos', 'givenname', 'homephone', 'inetuserhttpurl',
-                'initials', 'l', 'labeleduri', 'loginshell', 'manager',
-                'mepmanagedentry', 'mobile', 'objectclass', 'ou', 'pager',
-                'postalcode', 'roomnumber', 'secretary', 'seealso', 'sn', 'st',
-                'street', 'telephonenumber', 'title', 'userclass',
-                'preferredlanguage',
-            },
-            'replaces': [
-                '(targetattr = "givenname || sn || cn || displayname || title || initials || loginshell || gecos || homephone || mobile || pager || facsimiletelephonenumber || telephonenumber || street || roomnumber || l || st || postalcode || manager || secretary || description || carlicense || labeleduri || inetuserhttpurl || seealso || employeetype || businesscategory || ou || mepmanagedentry || objectclass")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Modify Users";allow (write) groupdn = "ldap:///cn=Modify Users,cn=permissions,cn=pbac,$SUFFIX";)',
-            ],
-            'default_privileges': {
-                'User Administrators',
-                'Modify Users and Reset passwords',
-            },
-        },
-        'System: Remove Users': {
-            'ipapermright': {'delete'},
-            'replaces': [
-                '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Remove Users";allow (delete) groupdn = "ldap:///cn=Remove Users,cn=permissions,cn=pbac,$SUFFIX";)',
-            ],
-            'default_privileges': {'User Administrators'},
-        },
-        'System: Unlock User': {
-            'ipapermright': {'write'},
-            'ipapermdefaultattr': {
-                'krblastadminunlock', 'krbloginfailedcount', 'nsaccountlock',
-            },
-            'replaces': [
-                '(targetattr = "krbLastAdminUnlock || krbLoginFailedCount")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX";)(version 3.0;acl "permission:Unlock user accounts";allow (write) groupdn = "ldap:///cn=Unlock user accounts,cn=permissions,cn=pbac,$SUFFIX";)',
-            ],
-            'default_privileges': {'User Administrators'},
-        },
-        'System: Read User Compat Tree': {
-            'non_object': True,
-            'ipapermbindruletype': 'anonymous',
-            'ipapermlocation': api.env.basedn,
-            'ipapermtarget': DN('cn=users', 'cn=compat', api.env.basedn),
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'objectclass', 'uid', 'cn', 'gecos', 'gidnumber', 'uidnumber',
-                'homedirectory', 'loginshell',
-            },
-        },
-        'System: Read User Views Compat Tree': {
-            'non_object': True,
-            'ipapermbindruletype': 'anonymous',
-            'ipapermlocation': api.env.basedn,
-            'ipapermtarget': DN('cn=users', 'cn=*', 'cn=views', 'cn=compat', api.env.basedn),
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'objectclass', 'uid', 'cn', 'gecos', 'gidnumber', 'uidnumber',
-                'homedirectory', 'loginshell',
-            },
-        },
-        'System: Read User NT Attributes': {
-            'ipapermbindruletype': 'permission',
-            'ipapermright': {'read', 'search', 'compare'},
-            'ipapermdefaultattr': {
-                'ntuserdomainid', 'ntuniqueid', 'ntuseracctexpires',
-                'ntusercodepage', 'ntuserdeleteaccount', 'ntuserlastlogoff',
-                'ntuserlastlogon',
-            },
-            'default_privileges': {'PassSync Service'},
-        },
-    }
+    container_dn              = api.env.container_user
+    active_container_dn       = api.env.container_user
+    takes_params              = accounts.takes_params
+    label                     = _('Users')
+    label_singular            = _('User')
+    object_name               = _('user')
+    object_name_plural        = _('users')
 
-    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'),
-        ),
-        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(user, LDAPCreate):
     __doc__ = _('Add a new user.')
 
     msg_summary = _('Added user "%(value)s"')
@@ -798,21 +226,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(accounts_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.active_container_dn)
 
         if 'userclass' in entry_attrs and \
            'ipauser' not in entry_attrs['objectclass']:
@@ -847,7 +275,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):
@@ -905,7 +333,7 @@ class user_del(LDAPDelete):
 
 
 @register()
-class user_mod(LDAPUpdate):
+class user_mod(user, LDAPUpdate):
     __doc__ = _('Modify a user.')
 
     msg_summary = _('Modified user "%(value)s"')
@@ -925,12 +353,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.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(accounts_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 +398,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,7 +406,7 @@ class user_mod(LDAPUpdate):
 
 
 @register()
-class user_find(LDAPSearch):
+class user_find(user, LDAPSearch):
     __doc__ = _('Search for users.')
 
     member_attributes = ['memberof']
@@ -995,7 +423,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.active_container_dn)
 
         # Ensure that the RADIUS config link is a dn, not just the name
         cl = 'ipatokenradiusconfiglink'
@@ -1016,7 +444,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)
@@ -1036,7 +464,7 @@ class user_show(LDAPRetrieve):
     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)
-- 
2.1.0

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

Reply via email to