2015-03-05 07:25:33 -06:00
|
|
|
# Authors:
|
|
|
|
# Thierry Bordaz <tbordaz@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
|
2015-05-08 09:12:58 -05:00
|
|
|
from copy import deepcopy
|
2015-03-05 07:25:33 -06:00
|
|
|
from ipalib import api, errors
|
|
|
|
from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime
|
|
|
|
from ipalib.plugable import Registry
|
2015-05-08 09:12:58 -05:00
|
|
|
from ipalib.plugins.baseldap import LDAPCreate, LDAPQuery, LDAPSearch, DN, entry_to_dict, pkey_to_value
|
2015-03-05 07:25:33 -06:00
|
|
|
from ipalib.plugins import baseldap
|
2015-05-12 08:03:58 -05:00
|
|
|
from ipalib.plugins.baseuser import baseuser, baseuser_add, baseuser_del, \
|
|
|
|
baseuser_mod, baseuser_find, baseuser_show, \
|
2015-03-05 07:25:33 -06:00
|
|
|
NO_UPG_MAGIC, radius_dn2pk, \
|
|
|
|
baseuser_pwdchars, fix_addressbook_permission_bindrule, normalize_principal, validate_principal, \
|
|
|
|
baseuser_output_params, status_baseuser_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)
|
|
|
|
|
|
|
|
__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
|
|
|
|
|
2015-07-03 11:14:42 -05:00
|
|
|
Delete user entries are Posix users directly under the container: "cn=deleted users,
|
2015-03-05 07:25:33 -06:00
|
|
|
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 = baseuser_output_params
|
|
|
|
|
|
|
|
status_output_params = status_baseuser_output_params
|
|
|
|
|
|
|
|
@register()
|
|
|
|
class stageuser(baseuser):
|
|
|
|
"""
|
|
|
|
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) through cos
|
|
|
|
"""
|
|
|
|
|
|
|
|
container_dn = baseuser.stage_container_dn
|
|
|
|
label = _('Stage Users')
|
|
|
|
label_singular = _('Stage User')
|
|
|
|
object_name = _('stage user')
|
|
|
|
object_name_plural = _('stage users')
|
2015-05-08 03:41:44 -05:00
|
|
|
managed_permissions = {
|
2015-06-11 06:18:27 -05:00
|
|
|
#
|
|
|
|
# Stage container
|
|
|
|
#
|
|
|
|
# Allowed to create stage user
|
|
|
|
'System: Add Stage User': {
|
2015-04-23 11:43:48 -05:00
|
|
|
'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=*)'},
|
|
|
|
'ipapermright': {'add'},
|
|
|
|
'ipapermdefaultattr': {'*'},
|
|
|
|
'default_privileges': {'Stage User Administrators', 'Stage User Provisioning'},
|
|
|
|
},
|
2015-06-11 06:18:27 -05:00
|
|
|
# Allow to read kerberos/password
|
|
|
|
'System: Read Stage User password': {
|
|
|
|
'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=*)'},
|
|
|
|
'ipapermright': {'read', 'search', 'compare'},
|
|
|
|
'ipapermdefaultattr': {
|
|
|
|
'userPassword', 'krbPrincipalKey',
|
|
|
|
},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
|
|
|
# Allow to update stage user
|
|
|
|
'System: Modify Stage User': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=*)'},
|
2015-06-11 06:18:27 -05:00
|
|
|
'ipapermright': {'write'},
|
|
|
|
'ipapermdefaultattr': {'*'},
|
2015-05-08 03:41:44 -05:00
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
2015-06-11 06:18:27 -05:00
|
|
|
# Allow to delete stage user
|
|
|
|
'System: Remove Stage User': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=*)'},
|
2015-06-11 06:18:27 -05:00
|
|
|
'ipapermright': {'delete'},
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermdefaultattr': {'*'},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
2015-06-11 06:18:27 -05:00
|
|
|
# Allow to read any attributes of stage users
|
|
|
|
'System: Read Stage Users': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=*)'},
|
|
|
|
'ipapermright': {'read', 'search', 'compare'},
|
|
|
|
'ipapermdefaultattr': {'*'},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
|
|
|
#
|
2015-06-11 06:18:27 -05:00
|
|
|
# Preserve container
|
2015-05-08 03:41:44 -05:00
|
|
|
#
|
2015-06-11 06:18:27 -05:00
|
|
|
# Allow to read Preserved User
|
|
|
|
'System: Read Preserved Users': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=posixaccount)'},
|
2015-06-11 06:18:27 -05:00
|
|
|
'ipapermright': {'read', 'search', 'compare'},
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermdefaultattr': {'*'},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
2015-06-11 06:18:27 -05:00
|
|
|
# Allow to update Preserved User
|
|
|
|
'System: Modify Preserved Users': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=posixaccount)'},
|
|
|
|
'ipapermright': {'write'},
|
2015-06-11 06:18:27 -05:00
|
|
|
'ipapermdefaultattr': {'*'},
|
2015-05-08 03:41:44 -05:00
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
2015-06-11 06:18:27 -05:00
|
|
|
# Allow to reset Preserved User password
|
|
|
|
'System: Reset Preserved User password': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=posixaccount)'},
|
|
|
|
'ipapermright': {'read', 'search', 'write'},
|
|
|
|
'ipapermdefaultattr': {
|
|
|
|
'userPassword', 'krbPrincipalKey','krbPasswordExpiration','krbLastPwdChange'
|
|
|
|
},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
2015-06-19 10:27:11 -05:00
|
|
|
# Allow to delete preserved user
|
|
|
|
'System: Remove preserved User': {
|
|
|
|
'ipapermlocation': DN(baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=*)'},
|
|
|
|
'ipapermright': {'delete'},
|
|
|
|
'ipapermdefaultattr': {'*'},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
2015-05-08 03:41:44 -05:00
|
|
|
#
|
|
|
|
# Active container
|
|
|
|
#
|
|
|
|
# Stage user administrators need write right on RDN when
|
|
|
|
# the active user is deleted (preserved)
|
2015-06-11 06:18:27 -05:00
|
|
|
'System: Modify User RDN': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(baseuser.active_container_dn, api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtarget': DN('uid=*', baseuser.active_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=posixaccount)'},
|
|
|
|
'ipapermright': {'write'},
|
|
|
|
'ipapermdefaultattr': {'uid'},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
|
|
|
#
|
|
|
|
# Cross containers autorization
|
|
|
|
#
|
2015-06-11 06:18:27 -05:00
|
|
|
# Allow to move active user to preserve container (user-del --preserve)
|
2015-05-08 03:41:44 -05:00
|
|
|
# Note: targetfilter is the target parent container
|
2015-06-11 06:18:27 -05:00
|
|
|
'System: Preserve User': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtargetfrom': DN(baseuser.active_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetto': DN(baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=nsContainer)'},
|
|
|
|
'ipapermright': {'moddn'},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
2015-06-11 06:18:27 -05:00
|
|
|
# Allow to move preserved user to active container (user-undel)
|
2015-05-08 03:41:44 -05:00
|
|
|
# Note: targetfilter is the target parent container
|
2015-06-11 06:18:27 -05:00
|
|
|
'System: Undelete User': {
|
2015-05-08 03:41:44 -05:00
|
|
|
'ipapermlocation': DN(api.env.basedn),
|
|
|
|
'ipapermbindruletype': 'permission',
|
|
|
|
'ipapermtargetfrom': DN(baseuser.delete_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetto': DN(baseuser.active_container_dn, api.env.basedn),
|
|
|
|
'ipapermtargetfilter': {'(objectclass=nsContainer)'},
|
|
|
|
'ipapermright': {'moddn'},
|
|
|
|
'default_privileges': {'Stage User Administrators'},
|
|
|
|
},
|
|
|
|
}
|
2015-03-05 07:25:33 -06:00
|
|
|
|
|
|
|
@register()
|
|
|
|
class stageuser_add(baseuser_add):
|
|
|
|
__doc__ = _('Add a new stage user.')
|
|
|
|
|
|
|
|
msg_summary = _('Added stage user "%(value)s"')
|
|
|
|
|
|
|
|
has_output_params = baseuser_add.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(baseuser_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.obj.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
|
2015-05-12 08:03:58 -05:00
|
|
|
|
|
|
|
@register()
|
|
|
|
class stageuser_del(baseuser_del):
|
|
|
|
__doc__ = _('Delete a stage user.')
|
|
|
|
|
|
|
|
msg_summary = _('Deleted stage user "%(value)s"')
|
|
|
|
|
|
|
|
@register()
|
|
|
|
class stageuser_mod(baseuser_mod):
|
|
|
|
__doc__ = _('Modify a stage user.')
|
|
|
|
|
|
|
|
msg_summary = _('Modified stage user "%(value)s"')
|
|
|
|
|
|
|
|
has_output_params = baseuser_mod.has_output_params + stageuser_output_params
|
|
|
|
|
|
|
|
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
|
|
|
|
self.pre_common_callback(ldap, dn, entry_attrs, **options)
|
|
|
|
# Make sure it is not possible to authenticate with a Stage user account
|
|
|
|
if 'nsaccountlock' in entry_attrs:
|
|
|
|
del entry_attrs['nsaccountlock']
|
|
|
|
return dn
|
|
|
|
|
|
|
|
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
|
|
|
|
self.post_common_callback(ldap, dn, entry_attrs, **options)
|
|
|
|
if 'nsaccountlock' in entry_attrs:
|
|
|
|
del entry_attrs['nsaccountlock']
|
|
|
|
return dn
|
|
|
|
|
|
|
|
@register()
|
|
|
|
class stageuser_find(baseuser_find):
|
|
|
|
__doc__ = _('Search for stage users.')
|
|
|
|
|
|
|
|
member_attributes = ['memberof']
|
|
|
|
has_output_params = baseuser_find.has_output_params + stageuser_output_params
|
|
|
|
|
|
|
|
def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, **options):
|
|
|
|
assert isinstance(base_dn, DN)
|
|
|
|
|
2015-05-11 10:08:16 -05:00
|
|
|
container_filter = "(objectclass=posixaccount)"
|
|
|
|
# provisioning system can create non posixaccount stage user
|
|
|
|
# but then they have to create inetOrgPerson stage user
|
|
|
|
stagefilter = filter.replace(container_filter,
|
|
|
|
"(|%s(objectclass=inetOrgPerson))" % container_filter)
|
|
|
|
self.log.debug("stageuser_find: pre_callback new filter=%s " % (stagefilter))
|
|
|
|
return (stagefilter, base_dn, scope)
|
2015-05-12 08:03:58 -05:00
|
|
|
|
|
|
|
def post_callback(self, ldap, entries, truncated, *args, **options):
|
|
|
|
if options.get('pkey_only', False):
|
|
|
|
return truncated
|
|
|
|
self.post_common_callback(ldap, entries, lockout=True, **options)
|
|
|
|
return truncated
|
|
|
|
|
|
|
|
msg_summary = ngettext(
|
|
|
|
'%(count)d user matched', '%(count)d users matched', 0
|
|
|
|
)
|
|
|
|
|
|
|
|
@register()
|
|
|
|
class stageuser_show(baseuser_show):
|
|
|
|
__doc__ = _('Display information about a stage user.')
|
|
|
|
|
|
|
|
has_output_params = baseuser_show.has_output_params + stageuser_output_params
|
|
|
|
|
|
|
|
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
|
|
|
|
entry_attrs['nsaccountlock'] = True
|
|
|
|
self.post_common_callback(ldap, dn, entry_attrs, **options)
|
|
|
|
return dn
|
2015-05-08 09:12:58 -05:00
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
2015-05-11 10:08:16 -05:00
|
|
|
def _check_validy(self, dn, entry):
|
|
|
|
if dn[0].attr != 'uid':
|
|
|
|
raise errors.ValidationError(
|
|
|
|
name=self.obj.primary_key.cli_name,
|
|
|
|
error=_('Entry RDN is not \'uid\''),
|
|
|
|
)
|
|
|
|
for attr in ('cn', 'sn', 'uid'):
|
|
|
|
if attr not in entry:
|
|
|
|
raise errors.ValidationError(
|
|
|
|
name=self.obj.primary_key.cli_name,
|
2015-07-01 07:46:22 -05:00
|
|
|
error=_('Entry has no \'%(attribute)s\'') % dict(attribute=attr),
|
2015-05-11 10:08:16 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
def _build_new_entry(self, ldap, dn, entry_from, entry_to):
|
|
|
|
config = ldap.get_ipa_config()
|
|
|
|
|
|
|
|
if 'uidnumber' not in entry_from:
|
|
|
|
entry_to['uidnumber'] = baseldap.DNA_MAGIC
|
|
|
|
if 'gidnumber' not in entry_from:
|
|
|
|
entry_to['gidnumber'] = baseldap.DNA_MAGIC
|
|
|
|
if 'homedirectory' not in entry_from:
|
|
|
|
# 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_to['homedirectory'] = posixpath.join(homes_root, dn[0].value)
|
|
|
|
if 'ipamaxusernamelength' in config:
|
|
|
|
if len(dn[0].value) > 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])
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if 'loginshell' not in entry_from:
|
|
|
|
default_shell = config.get('ipadefaultloginshell', [paths.SH])[0]
|
|
|
|
if default_shell:
|
|
|
|
entry_to.setdefault('loginshell', default_shell)
|
|
|
|
|
|
|
|
if 'givenname' not in entry_from:
|
|
|
|
entry_to['givenname'] = entry_from['cn'][0].split()[0]
|
|
|
|
|
|
|
|
if 'krbprincipalname' not in entry_from:
|
|
|
|
entry_to['krbprincipalname'] = '%s@%s' % (entry_from['uid'][0], api.env.realm)
|
|
|
|
|
2015-05-08 09:12:58 -05:00
|
|
|
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)
|
2015-05-11 10:08:16 -05:00
|
|
|
if (isinstance(v, str) and v in ('', None)) or \
|
|
|
|
(isinstance(v, unicode) and v in (u'', None)):
|
2015-05-08 09:12:58 -05:00
|
|
|
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
|
2015-05-11 10:08:16 -05:00
|
|
|
if attr.lower() == 'objectclass':
|
|
|
|
entry_to[attr] = [oc.lower() for oc in entry_to[attr]]
|
|
|
|
value = value.lower()
|
|
|
|
if value not in entry_to[attr]:
|
|
|
|
entry_to[attr].append(value)
|
|
|
|
else:
|
|
|
|
if value not in entry_to[attr]:
|
|
|
|
entry_to[attr].append(value)
|
2015-05-08 09:12:58 -05:00
|
|
|
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)
|
2015-07-10 07:47:59 -05:00
|
|
|
raise errors.DuplicateEntry(
|
|
|
|
message=_('active user with name "%(user)s" already exists') %
|
|
|
|
dict(user=args[-1]))
|
2015-05-08 09:12:58 -05:00
|
|
|
except errors.NotFound:
|
|
|
|
pass
|
|
|
|
|
2015-05-11 10:08:16 -05:00
|
|
|
# Check the original entry is valid
|
|
|
|
self._check_validy(staging_dn, entry_attrs)
|
2015-05-08 09:12:58 -05:00
|
|
|
|
|
|
|
# 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))
|
|
|
|
|
2015-05-11 10:08:16 -05:00
|
|
|
self._build_new_entry(ldap, staging_dn, entry_attrs, new_entry_attrs)
|
|
|
|
|
2015-05-08 09:12:58 -05:00
|
|
|
# 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))
|