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>
This commit is contained in:
Thierry Bordaz
2015-05-08 16:12:58 +02:00
committed by Martin Kosek
parent f2e986e01f
commit 0ebcc5b922
5 changed files with 305 additions and 4 deletions

10
API.txt
View File

@@ -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)

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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))