summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThierry Bordaz <tbordaz@redhat.com>2015-05-08 16:12:58 +0200
committerMartin Kosek <mkosek@redhat.com>2015-05-18 09:37:21 +0200
commit0ebcc5b9222efcd4b9814a2948f266abbf71fdfc (patch)
tree2ac7cf53c69749711ad7a0f2922372bb060544f6
parentf2e986e01f973a95e95608e1853dca35dcffeb58 (diff)
downloadfreeipa-0ebcc5b9222efcd4b9814a2948f266abbf71fdfc.tar.gz
freeipa-0ebcc5b9222efcd4b9814a2948f266abbf71fdfc.tar.xz
freeipa-0ebcc5b9222efcd4b9814a2948f266abbf71fdfc.zip
User life cycle: new stageuser commands activate
Add plugin commands to stageuser plugin: stageuser_activate: activate entries created by IPA CLIs https://fedorahosted.org/freeipa/ticket/3813 Reviewed-By: David Kupka <dkupka@redhat.com>
-rw-r--r--API.txt10
-rw-r--r--daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c48
-rw-r--r--install/updates/30-provisioning.update28
-rw-r--r--ipalib/plugins/baseuser.py16
-rw-r--r--ipalib/plugins/stageuser.py207
5 files changed, 305 insertions, 4 deletions
diff --git a/API.txt b/API.txt
index 67128b493..d7e4e0baa 100644
--- a/API.txt
+++ b/API.txt
@@ -3692,6 +3692,16 @@ command: sidgen_was_run
args: 0,1,1
option: Str('version?', exclude='webui')
output: Output('result', None, None)
+command: stageuser_activate
+args: 1,4,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, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+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: 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)
diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
index 1c6838052..f830e3bfe 100644
--- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
@@ -151,6 +151,43 @@ done:
return value;
}
+static bool has_krbprincipalkey(Slapi_Entry *entry) {
+ int rc;
+ krb5_key_data *keys = NULL;
+ int num_keys = 0;
+ int mkvno = 0;
+ int hint;
+ Slapi_Attr *attr;
+ Slapi_Value *keys_value;
+ const struct berval *bval;
+
+
+ if (slapi_entry_attr_find(entry, "krbPrincipalKey", &attr)) {
+ return false;
+ }
+
+ /* It exists a krbPrincipalKey attribute checks it exists a valid value */
+ for (hint = slapi_attr_first_value(attr, &keys_value);
+ hint != -1; hint = slapi_attr_next_value(attr, hint, &keys_value)) {
+ bval = slapi_value_get_berval(keys_value);
+ if (NULL != bval && NULL != bval->bv_val) {
+ rc = ber_decode_krb5_key_data(discard_const(bval),
+ &mkvno, &num_keys, &keys);
+
+ if (rc || (num_keys <= 0)) {
+ /* this one is not valid, ignore it */
+ if (keys) ipa_krb5_free_key_data(keys, num_keys);
+ } else {
+ /* It exists at least this one that is valid, no need to continue */
+ if (keys) ipa_krb5_free_key_data(keys, num_keys);
+ return true;
+ }
+ }
+
+ }
+ return false;
+}
+
/* PRE ADD Operation:
* Gets the clean text password (fail the operation if the password came
@@ -245,6 +282,17 @@ static int ipapwd_pre_add(Slapi_PBlock *pb)
return 0;
}
+ /* With User Life Cycle, it could be a stage user that is activated.
+ * The userPassword and krb keys were set while the user was a stage user.
+ * Accept hashed userPassword and krb keys at the condition, it already contains
+ * a valid krbPrincipalKey
+ */
+ if (has_krbprincipalkey(e)) {
+ slapi_pblock_get(pb, SLAPI_TARGET_DN, &dn);
+ LOG("User Life Cycle: %s is a activated stage user (with prehashed password and krb keys)\n", dn ? dn : "unknown");
+ return 0;
+ }
+
LOG("pre-hashed passwords are not valid\n");
errMesg = "pre-hashed passwords are not valid\n";
goto done;
diff --git a/install/updates/30-provisioning.update b/install/updates/30-provisioning.update
index a32312b71..f1666ff3a 100644
--- a/install/updates/30-provisioning.update
+++ b/install/updates/30-provisioning.update
@@ -18,9 +18,31 @@ default: cn: staged users
dn: cn=deleted users,cn=accounts,cn=provisioning,$SUFFIX
default: objectclass: top
default: objectclass: nsContainer
-default: cn: staged users
+default: cn: deleted 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.
+# We can do a query on a DN to see if an attribute exists or retrieve the value
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";)
+add:aci: (targetattr="userPassword || krbPrincipalKey")(version 3.0; acl "Search existence of password and kerberos keys"; allow(read, search) userdn = "ldap:///uid=admin,cn=users,cn=accounts,$SUFFIX";)
+
+# This is used for the admin to reset the delete users credential
+dn: cn=deleted users,cn=accounts,cn=provisioning,$SUFFIX
+add:aci: (targetattr="userPassword || krbPrincipalKey || krbPasswordExpiration || krbLastPwdChange")(version 3.0; acl "Admins allowed to reset password and kerberos keys"; allow(read, search, write) userdn = "ldap:///uid=admin,cn=users,cn=accounts,$SUFFIX";)
+add:aci: (targetattr = "*")(version 3.0; acl "No one can add entry in Delete container"; deny (add) userdn = "ldap:///all";)
+
+dn: cn=provisioning accounts lock,cn=accounts,cn=provisioning,$SUFFIX
+default: objectClass: top
+default: objectClass: cosSuperDefinition
+default: objectClass: cosPointerDefinition
+default: objectClass: ldapSubEntry
+default: costemplatedn: cn=Inactivation cos template,cn=accounts,cn=provisioning,$SUFFIX
+default: cosAttribute: nsaccountlock operational
+default: cn: provisioning accounts lock
+
+dn: cn=Inactivation cos template,cn=accounts,cn=provisioning,$SUFFIX
+default: objectClass: top
+default: objectClass: extensibleObject
+default: objectClass: cosTemplate
+default: cosPriority: 1
+default: cn: Inactivation cos template
+default: nsAccountLock: true
diff --git a/ipalib/plugins/baseuser.py b/ipalib/plugins/baseuser.py
index 4266e876c..a1be29d83 100644
--- a/ipalib/plugins/baseuser.py
+++ b/ipalib/plugins/baseuser.py
@@ -445,6 +445,22 @@ class baseuser(LDAPObject):
for m in xrange(len(entry_attrs['manager'])):
entry_attrs['manager'][m] = self.get_primary_key_from_dn(entry_attrs['manager'][m])
+ def _user_status(self, user, container):
+ assert isinstance(user, DN)
+ return user.endswith(container)
+
+ def active_user(self, user):
+ assert isinstance(user, DN)
+ return self._user_status(user, DN(self.active_container_dn, api.env.basedn))
+
+ def stage_user(self, user):
+ assert isinstance(user, DN)
+ return self._user_status(user, DN(self.stage_container_dn, api.env.basedn))
+
+ def delete_user(self, user):
+ assert isinstance(user, DN)
+ return self._user_status(user, DN(self.delete_container_dn, api.env.basedn))
+
class baseuser_add(LDAPCreate):
"""
Prototype command plugin to be implemented by real plugin
diff --git a/ipalib/plugins/stageuser.py b/ipalib/plugins/stageuser.py
index f788061f0..d31e0d356 100644
--- a/ipalib/plugins/stageuser.py
+++ b/ipalib/plugins/stageuser.py
@@ -22,10 +22,11 @@ import string
import posixpath
import os
+from copy import deepcopy
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.baseldap import LDAPCreate, LDAPQuery, LDAPSearch, DN, entry_to_dict, pkey_to_value
from ipalib.plugins import baseldap
from ipalib.plugins.baseuser import baseuser, baseuser_add, baseuser_del, \
baseuser_mod, baseuser_find, baseuser_show, \
@@ -344,3 +345,207 @@ class stageuser_show(baseuser_show):
entry_attrs['nsaccountlock'] = True
self.post_common_callback(ldap, dn, entry_attrs, **options)
return dn
+
+
+@register()
+class stageuser_activate(LDAPQuery):
+ __doc__ = _('Activate a stage user.')
+
+ msg_summary = _('Activate a stage user "%(value)s"')
+
+ preserved_DN_syntax_attrs = ('manager', 'managedby', 'secretary')
+
+ searched_operational_attributes = ['uidNumber', 'gidNumber', 'nsAccountLock', 'ipauniqueid']
+
+ has_output = output.standard_entry
+ has_output_params = LDAPQuery.has_output_params + stageuser_output_params
+
+ def __dict_new_entry(self, *args, **options):
+ ldap = self.obj.backend
+
+ entry_attrs = self.args_options_2_entry(*args, **options)
+ entry_attrs = ldap.make_entry(DN(), entry_attrs)
+
+ self.process_attr_options(entry_attrs, None, args, options)
+
+ entry_attrs['objectclass'] = deepcopy(self.obj.object_class)
+
+ if self.obj.object_class_config:
+ config = ldap.get_ipa_config()
+ entry_attrs['objectclass'] = config.get(
+ self.obj.object_class_config, entry_attrs['objectclass']
+ )
+
+ return(entry_attrs)
+
+ def __merge_values(self, args, options, entry_from, entry_to, attr):
+ '''
+ This routine merges the values of attr taken from entry_from, into entry_to.
+ If attr is a syntax DN attribute, it is replaced by an empty value. It is a preferable solution
+ compare to skiping it because the final entry may no longer conform the schema.
+ An exception of this is for a limited set of syntax DN attribute that we want to
+ preserved (defined in preserved_DN_syntax_attrs)
+ see http://www.freeipa.org/page/V3/User_Life-Cycle_Management#Adjustment_of_DN_syntax_attributes
+ '''
+ if not attr in entry_to:
+ if isinstance(entry_from[attr], (list, tuple)):
+ # attr is multi value attribute
+ entry_to[attr] = []
+ else:
+ # attr single valued attribute
+ entry_to[attr] = None
+
+ # At this point entry_to contains for all resulting attributes
+ # either a list (possibly empty) or a value (possibly None)
+
+ for value in entry_from[attr]:
+ # merge all the values from->to
+ v = self.__value_2_add(args, options, attr, value)
+ if (isinstance(value, str) and v in ('', None)) or \
+ (isinstance(value, unicode) and v in (u'', None)):
+ try:
+ v.decode('utf-8')
+ self.log.debug("merge: %s:%r wiped" % (attr, v))
+ except:
+ self.log.debug("merge %s: [no_print %s]" % (attr, v.__class__.__name__))
+ pass
+ if isinstance(entry_to[attr], (list, tuple)):
+ # multi value attribute
+ if v not in entry_to[attr]:
+ # it may has been added before in the loop
+ # so add it only if it not present
+ entry_to[attr].append(v)
+ else:
+ # single value attribute
+ # keep the value defined in staging
+ entry_to[attr] = v
+ else:
+ try:
+ v.decode('utf-8')
+ self.log.debug("Add: %s:%r" % (attr, v))
+ except:
+ self.log.debug("Add %s: [no_print %s]" % (attr, v.__class__.__name__))
+ pass
+ if isinstance(entry_to[attr], (list, tuple)):
+ # multi value attribute
+ if value not in entry_to[attr]:
+ entry_to[attr].append(value)
+ else:
+ # single value attribute
+ if value:
+ entry_to[attr] = value
+
+ def __value_2_add(self, args, options, attr, value):
+ '''
+ If the attribute is NOT syntax DN it returns its value.
+ Else it checks if the value can be preserved.
+ To be preserved:
+ - attribute must be in preserved_DN_syntax_attrs
+ - value must be an active user DN (in Active container)
+ - the active user entry exists
+ '''
+ ldap = self.obj.backend
+
+ if ldap.has_dn_syntax(attr):
+ if attr.lower() in self.preserved_DN_syntax_attrs:
+ # we are about to add a DN syntax value
+ # Check this is a valid DN
+ if not isinstance(value, DN):
+ return u''
+
+ if not self.obj.active_user(value):
+ return u''
+
+ # Check that this value is a Active user
+ try:
+ entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)(value, ['dn'])
+ return value
+ except errors.NotFound:
+ return u''
+ else:
+ return u''
+ else:
+ return value
+
+ def execute(self, *args, **options):
+
+ ldap = self.obj.backend
+
+ staging_dn = self.obj.get_dn(*args, **options)
+ assert isinstance(staging_dn, DN)
+
+ # retrieve the current entry
+ try:
+ entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)(
+ staging_dn, ['*']
+ )
+ except errors.NotFound:
+ self.obj.handle_not_found(*args)
+ entry_attrs = dict((k.lower(), v) for (k, v) in entry_attrs.iteritems())
+
+ # Check it does not exist an active entry with the same RDN
+ active_dn = DN(staging_dn[0], api.env.container_user, api.env.basedn)
+ try:
+ test_entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)(
+ active_dn, ['dn']
+ )
+ assert isinstance(staging_dn, DN)
+ raise errors.DuplicateEntry(message=_('Active user %(user)s already exists') % dict(
+ user=test_entry_attrs.dn))
+ except errors.NotFound:
+ pass
+
+
+ # Time to build the new entry
+ result_entry = {'dn' : active_dn}
+ new_entry_attrs = self.__dict_new_entry()
+ for (attr, values) in entry_attrs.iteritems():
+ self.__merge_values(args, options, entry_attrs, new_entry_attrs, attr)
+ result_entry[attr] = values
+
+ # Allow Managed entry plugin to do its work
+ if 'description' in new_entry_attrs and NO_UPG_MAGIC in new_entry_attrs['description']:
+ new_entry_attrs['description'].remove(NO_UPG_MAGIC)
+ if result_entry['description'] == NO_UPG_MAGIC:
+ del result_entry['description']
+
+ for (k,v) in new_entry_attrs.iteritems():
+ self.log.debug("new entry: k=%r and v=%r)" % (k, v))
+
+ # Add the Active entry
+ entry = ldap.make_entry(active_dn, new_entry_attrs)
+ self._exc_wrapper(args, options, ldap.add_entry)(entry)
+
+ # Now delete the Staging entry
+ try:
+ self._exc_wrapper(args, options, ldap.delete_entry)(staging_dn)
+ except:
+ try:
+ self.log.error("Fail to delete the Staging user after activating it %s " % (staging_dn))
+ self._exc_wrapper(args, options, ldap.delete_entry)(active_dn)
+ except:
+ self.log.error("Fail to cleanup activation. The user remains active %s" % (active_dn))
+ pass
+ raise
+
+ # add the user we just created into the default primary group
+ config = ldap.get_ipa_config()
+ def_primary_group = config.get('ipadefaultprimarygroup')
+ group_dn = self.api.Object['group'].get_dn(def_primary_group)
+
+ # if the user is already a member of default primary group,
+ # do not raise error
+ # this can happen if automember rule or default group is set
+ try:
+ ldap.add_entry_to_group(active_dn, group_dn)
+ except errors.AlreadyGroupMember:
+ pass
+
+ # Now retrieve the activated entry
+ result_entry = self._exc_wrapper(args, options, ldap.get_entry)(active_dn)
+ result_entry = entry_to_dict(result_entry, **options)
+ result_entry['dn'] = active_dn
+
+ return dict(result=result_entry,
+ summary=unicode(_('Stage user %s activated' % staging_dn[0].value)),
+ value=pkey_to_value(args[-1], options))