mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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:
committed by
Martin Kosek
parent
f2e986e01f
commit
0ebcc5b922
10
API.txt
10
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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user