From 0ebcc5b9222efcd4b9814a2948f266abbf71fdfc Mon Sep 17 00:00:00 2001 From: Thierry Bordaz Date: Fri, 8 May 2015 16:12:58 +0200 Subject: 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 --- API.txt | 10 ++ daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c | 48 +++++ install/updates/30-provisioning.update | 28 ++- ipalib/plugins/baseuser.py | 16 ++ ipalib/plugins/stageuser.py | 207 +++++++++++++++++++++- 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', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), 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)) -- cgit