diff options
author | Jan Cholasta <jcholast@redhat.com> | 2016-04-28 10:30:05 +0200 |
---|---|---|
committer | Jan Cholasta <jcholast@redhat.com> | 2016-06-03 09:00:34 +0200 |
commit | 6e44557b601f769d23ee74555a72e8b5cc62c0c9 (patch) | |
tree | eedd3e054b0709341b9f58c190ea54f999f7d13a /ipaserver/plugins/user.py | |
parent | ec841e5d7ab29d08de294b3fa863a631cd50e30a (diff) | |
download | freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.tar.gz freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.tar.xz freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.zip |
ipalib: move server-side plugins to ipaserver
Move the remaining plugin code from ipalib.plugins to ipaserver.plugins.
Remove the now unused ipalib.plugins package.
https://fedorahosted.org/freeipa/ticket/4739
Reviewed-By: David Kupka <dkupka@redhat.com>
Diffstat (limited to 'ipaserver/plugins/user.py')
-rw-r--r-- | ipaserver/plugins/user.py | 1151 |
1 files changed, 1151 insertions, 0 deletions
diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py new file mode 100644 index 000000000..adc59fcba --- /dev/null +++ b/ipaserver/plugins/user.py @@ -0,0 +1,1151 @@ +# Authors: +# Jason Gerard DeRose <jderose@redhat.com> +# Pavel Zuna <pzuna@redhat.com> +# +# Copyright (C) 2008 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/>. + +import time +from time import gmtime, strftime +import posixpath +import os + +import six + +from ipalib import api +from ipalib import errors +from ipalib import Bool, Flag, Str +from .baseuser import ( + baseuser, + baseuser_add, + baseuser_del, + baseuser_mod, + baseuser_find, + baseuser_show, + NO_UPG_MAGIC, + UPG_DEFINITION_DN, + baseuser_output_params, + status_baseuser_output_params, + baseuser_pwdchars, + validate_nsaccountlock, + convert_nsaccountlock, + fix_addressbook_permission_bindrule, + baseuser_add_manager, + baseuser_remove_manager) +from .idviews import remove_ipaobject_overrides +from ipalib.plugable import Registry +from .baseldap import ( + pkey_to_value, + LDAPCreate, + LDAPSearch, + LDAPQuery, + LDAPMultiQuery, + LDAPAddAttribute, + LDAPRemoveAttribute) +from . import baseldap +from ipalib.request import context +from ipalib import _, ngettext +from ipalib import output +from ipaplatform.paths import paths +from ipapython.dn import DN +from ipapython.ipautil import ipa_generate_password +from ipalib.capabilities import client_has_capability + +if api.env.in_server: + from ipaserver.plugins.ldap2 import ldap2 + +if six.PY3: + unicode = str + +__doc__ = _(""" +Users + +Manage user entries. All users are POSIX users. + +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. + +Disabling a user account prevents that user from obtaining new Kerberos +credentials. It does not invalidate any credentials that have already +been issued. + +Password management is not a part of this module. For more information +about this topic please see: ipa help passwd + +Account lockout on password failure happens per IPA master. The user-status +command can be used to identify which master the user is locked out on. +It is on that master the administrator must unlock the user. + +EXAMPLES: + + Add a new user: + ipa user-add --first=Tim --last=User --password tuser1 + + Find all users whose entries include the string "Tim": + ipa user-find Tim + + Find all users with "Tim" as the first name: + ipa user-find --first=Tim + + Disable a user account: + ipa user-disable tuser1 + + Enable a user account: + ipa user-enable tuser1 + + Delete a user: + ipa user-del tuser1 +""") + +register = Registry() + + +user_output_params = baseuser_output_params + +status_output_params = status_baseuser_output_params + + +def check_protected_member(user, protected_group_name=u'admins'): + ''' + Ensure the last enabled member of a protected group cannot be deleted or + disabled by raising LastMemberError. + ''' + + # Get all users in the protected group + result = api.Command.user_find(in_group=protected_group_name) + + # Build list of users in the protected group who are enabled + result = result['result'] + enabled_users = [entry['uid'][0] for entry in result if not entry['nsaccountlock']] + + # If the user is the last enabled user raise LastMemberError exception + if enabled_users == [user]: + raise errors.LastMemberError(key=user, label=_(u'group'), + container=protected_group_name) + +@register() +class user(baseuser): + """ + User object. + """ + + container_dn = baseuser.active_container_dn + label = _('Users') + label_singular = _('User') + object_name = _('user') + object_name_plural = _('users') + managed_permissions = { + 'System: Read User Standard Attributes': { + 'replaces_global_anonymous_aci': True, + 'ipapermbindruletype': 'anonymous', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'objectclass', 'cn', 'sn', 'description', 'title', 'uid', + 'displayname', 'givenname', 'initials', 'manager', 'gecos', + 'gidnumber', 'homedirectory', 'loginshell', 'uidnumber', + 'ipantsecurityidentifier' + }, + }, + 'System: Read User Addressbook Attributes': { + 'replaces_global_anonymous_aci': True, + 'ipapermbindruletype': 'all', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'seealso', 'telephonenumber', + 'facsimiletelephonenumber', 'l', 'ou', 'st', 'postalcode', 'street', + 'destinationindicator', 'internationalisdnnumber', + 'physicaldeliveryofficename', 'postaladdress', 'postofficebox', + 'preferreddeliverymethod', 'registeredaddress', + 'teletexterminalidentifier', 'telexnumber', 'x121address', + 'carlicense', 'departmentnumber', 'employeenumber', + 'employeetype', 'preferredlanguage', 'mail', 'mobile', 'pager', + 'audio', 'businesscategory', 'homephone', 'homepostaladdress', + 'jpegphoto', 'labeleduri', 'o', 'photo', 'roomnumber', + 'secretary', 'usercertificate', + 'usersmimecertificate', 'x500uniqueidentifier', + 'inetuserhttpurl', 'inetuserstatus', + }, + 'fixup_function': fix_addressbook_permission_bindrule, + }, + 'System: Read User IPA Attributes': { + 'replaces_global_anonymous_aci': True, + 'ipapermbindruletype': 'all', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'ipauniqueid', 'ipasshpubkey', 'ipauserauthtype', 'userclass', + }, + 'fixup_function': fix_addressbook_permission_bindrule, + }, + 'System: Read User Kerberos Attributes': { + 'replaces_global_anonymous_aci': True, + 'ipapermbindruletype': 'all', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'krbprincipalname', 'krbcanonicalname', 'krbprincipalaliases', + 'krbprincipalexpiration', 'krbpasswordexpiration', + 'krblastpwdchange', 'nsaccountlock', 'krbprincipaltype', + }, + }, + 'System: Read User Kerberos Login Attributes': { + 'replaces_global_anonymous_aci': True, + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'krblastsuccessfulauth', 'krblastfailedauth', + 'krblastpwdchange', 'krblastadminunlock', + 'krbloginfailedcount', 'krbpwdpolicyreference', + 'krbticketpolicyreference', 'krbupenabled', + }, + 'default_privileges': {'User Administrators'}, + }, + 'System: Read User Membership': { + 'replaces_global_anonymous_aci': True, + 'ipapermbindruletype': 'all', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'memberof', + }, + }, + 'System: Read UPG Definition': { + # Required for adding users + 'replaces_global_anonymous_aci': True, + 'non_object': True, + 'ipapermlocation': UPG_DEFINITION_DN, + 'ipapermtarget': UPG_DEFINITION_DN, + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': {'*'}, + 'default_privileges': {'User Administrators'}, + }, + 'System: Add Users': { + 'ipapermright': {'add'}, + 'replaces': [ + '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Users";allow (add) groupdn = "ldap:///cn=Add Users,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': {'User Administrators'}, + }, + 'System: Add User to default group': { + 'non_object': True, + 'ipapermright': {'write'}, + 'ipapermlocation': DN(api.env.container_group, api.env.basedn), + 'ipapermtarget': DN('cn=ipausers', api.env.container_group, + api.env.basedn), + 'ipapermdefaultattr': {'member'}, + 'replaces': [ + '(targetattr = "member")(target = "ldap:///cn=ipausers,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add user to default group";allow (write) groupdn = "ldap:///cn=Add user to default group,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': {'User Administrators'}, + }, + 'System: Change User password': { + 'ipapermright': {'write'}, + 'ipapermtargetfilter': [ + '(objectclass=posixaccount)', + '(!(memberOf=%s))' % DN('cn=admins', + api.env.container_group, + api.env.basedn), + ], + 'ipapermdefaultattr': { + 'krbprincipalkey', 'passwordhistory', 'sambalmpassword', + 'sambantpassword', 'userpassword' + }, + 'replaces': [ + '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)', + '(targetfilter = "(!(memberOf=cn=admins,cn=groups,cn=accounts,$SUFFIX))")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)', + '(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///uid=passsync,cn=sysaccounts,cn=etc,$SUFFIX";)', + ], + 'default_privileges': { + 'User Administrators', + 'Modify Users and Reset passwords', + 'PassSync Service', + }, + }, + 'System: Manage User SSH Public Keys': { + 'ipapermright': {'write'}, + 'ipapermdefaultattr': {'ipasshpubkey'}, + 'replaces': [ + '(targetattr = "ipasshpubkey")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Manage User SSH Public Keys";allow (write) groupdn = "ldap:///cn=Manage User SSH Public Keys,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': {'User Administrators'}, + }, + 'System: Manage User Certificates': { + 'ipapermright': {'write'}, + 'ipapermdefaultattr': {'usercertificate'}, + 'default_privileges': { + 'User Administrators', + 'Modify Users and Reset passwords', + }, + }, + 'System: Modify Users': { + 'ipapermright': {'write'}, + 'ipapermdefaultattr': { + 'businesscategory', 'carlicense', 'cn', 'departmentnumber', + 'description', 'displayname', 'employeetype', + 'employeenumber', 'facsimiletelephonenumber', + 'gecos', 'givenname', 'homephone', 'inetuserhttpurl', + 'initials', 'l', 'labeleduri', 'loginshell', 'manager', 'mail', + 'mepmanagedentry', 'mobile', 'objectclass', 'ou', 'pager', + 'postalcode', 'roomnumber', 'secretary', 'seealso', 'sn', 'st', + 'street', 'telephonenumber', 'title', 'userclass', + 'preferredlanguage', + }, + 'replaces': [ + '(targetattr = "givenname || sn || cn || displayname || title || initials || loginshell || gecos || homephone || mobile || pager || facsimiletelephonenumber || telephonenumber || street || roomnumber || l || st || postalcode || manager || secretary || description || carlicense || labeleduri || inetuserhttpurl || seealso || employeetype || businesscategory || ou || mepmanagedentry || objectclass")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Users";allow (write) groupdn = "ldap:///cn=Modify Users,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': { + 'User Administrators', + 'Modify Users and Reset passwords', + }, + }, + 'System: Remove Users': { + 'ipapermright': {'delete'}, + 'replaces': [ + '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Remove Users";allow (delete) groupdn = "ldap:///cn=Remove Users,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': {'User Administrators'}, + }, + 'System: Unlock User': { + 'ipapermright': {'write'}, + 'ipapermdefaultattr': { + 'krblastadminunlock', 'krbloginfailedcount', 'nsaccountlock', + }, + 'replaces': [ + '(targetattr = "krbLastAdminUnlock || krbLoginFailedCount")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Unlock user accounts";allow (write) groupdn = "ldap:///cn=Unlock user accounts,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': {'User Administrators'}, + }, + 'System: Read User Compat Tree': { + 'non_object': True, + 'ipapermbindruletype': 'anonymous', + 'ipapermlocation': api.env.basedn, + 'ipapermtarget': DN('cn=users', 'cn=compat', api.env.basedn), + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'objectclass', 'uid', 'cn', 'gecos', 'gidnumber', 'uidnumber', + 'homedirectory', 'loginshell', + }, + }, + 'System: Read User Views Compat Tree': { + 'non_object': True, + 'ipapermbindruletype': 'anonymous', + 'ipapermlocation': api.env.basedn, + 'ipapermtarget': DN('cn=users', 'cn=*', 'cn=views', 'cn=compat', api.env.basedn), + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'objectclass', 'uid', 'cn', 'gecos', 'gidnumber', 'uidnumber', + 'homedirectory', 'loginshell', + }, + }, + 'System: Read User NT Attributes': { + 'ipapermbindruletype': 'permission', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'ntuserdomainid', 'ntuniqueid', 'ntuseracctexpires', + 'ntusercodepage', 'ntuserdeleteaccount', 'ntuserlastlogoff', + 'ntuserlastlogon', + }, + 'default_privileges': {'PassSync Service'}, + }, + } + + takes_params = baseuser.takes_params + ( + Bool('nsaccountlock?', + label=_('Account disabled'), + flags=['no_option'], + ), + Bool('preserved?', + label=_('Preserved user'), + default=False, + flags=['virtual_attribute', 'no_create', 'no_update'], + ), + ) + + def get_either_dn(self, *keys, **options): + ''' + Returns the DN of a user + The user can be active (active container) or delete (delete container) + If the user does not exist, returns the Active user DN + ''' + ldap = self.backend + # Check that this value is a Active user + try: + active_dn = self.get_dn(*keys, **options) + ldap.get_entry(active_dn, ['dn']) + + # The Active user exists + dn = active_dn + except errors.NotFound: + # Check that this value is a Delete user + delete_dn = DN(active_dn[0], self.delete_container_dn, api.env.basedn) + try: + ldap.get_entry(delete_dn, ['dn']) + + # The Delete user exists + dn = delete_dn + except errors.NotFound: + # The user is neither Active/Delete -> returns that Active DN + dn = active_dn + + return dn + + def _normalize_manager(self, manager): + """ + Given a userid verify the user's existence and return the dn. + """ + return super(user, self).normalize_manager(manager, self.active_container_dn) + + def get_preserved_attribute(self, entry, options): + if options.get('raw', False): + return + delete_container_dn = DN(self.delete_container_dn, api.env.basedn) + if entry.dn.endswith(delete_container_dn): + entry['preserved'] = True + elif options.get('all', False): + entry['preserved'] = False + + +@register() +class user_add(baseuser_add): + __doc__ = _('Add a new user.') + + msg_summary = _('Added user "%(value)s"') + + has_output_params = baseuser_add.has_output_params + user_output_params + + takes_options = LDAPCreate.takes_options + ( + Flag('noprivate', + cli_name='noprivate', + doc=_('Don\'t create user private group'), + ), + ) + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + dn = self.obj.get_either_dn(*keys, **options) + if not options.get('noprivate', False): + try: + # The Managed Entries plugin will allow a user to be created + # even if a group has a duplicate name. This would leave a user + # without a private group. Check for both the group and the user. + self.api.Object['group'].get_dn_if_exists(keys[-1]) + try: + self.api.Command['user_show'](keys[-1]) + self.obj.handle_duplicate_entry(*keys) + except errors.NotFound: + raise errors.ManagedGroupExistsError(group=keys[-1]) + except errors.NotFound: + pass + else: + # 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) + + entry_attrs.setdefault('uidnumber', 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 + + validate_nsaccountlock(entry_attrs) + 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) + 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]) + entry_attrs.setdefault('krbprincipalname', '%s@%s' % (entry_attrs['uid'], api.env.realm)) + + if entry_attrs.get('gidnumber') is None: + # gidNumber wasn't specified explicity, find out what it should be + if not options.get('noprivate', False) and ldap.has_upg(): + # User Private Groups - uidNumber == gidNumber + entry_attrs['gidnumber'] = entry_attrs['uidnumber'] + else: + # we're adding new users to a default group, get its gidNumber + # get default group name from config + def_primary_group = config.get('ipadefaultprimarygroup') + group_dn = self.api.Object['group'].get_dn(def_primary_group) + try: + group_attrs = ldap.get_entry(group_dn, ['gidnumber']) + except errors.NotFound: + error_msg = _('Default group for new users not found') + raise errors.NotFound(reason=error_msg) + if 'gidnumber' not in group_attrs: + error_msg = _('Default group for new users is not POSIX') + raise errors.NotFound(reason=error_msg) + entry_attrs['gidnumber'] = group_attrs['gidnumber'] + + 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']) + + 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 'manager' in entry_attrs: + entry_attrs['manager'] = self.obj.normalize_manager(entry_attrs['manager'], self.obj.active_container_dn) + + if 'userclass' in entry_attrs and \ + 'ipauser' not in entry_attrs['objectclass']: + entry_attrs['objectclass'].append('ipauser') + + if 'ipauserauthtype' in entry_attrs and \ + 'ipauserauthtypeclass' not in entry_attrs['objectclass']: + entry_attrs['objectclass'].append('ipauserauthtypeclass') + + rcl = entry_attrs.get('ipatokenradiusconfiglink', None) + if rcl: + if 'ipatokenradiusproxyuser' not in entry_attrs['objectclass']: + entry_attrs['objectclass'].append('ipatokenradiusproxyuser') + + answer = self.api.Object['radiusproxy'].get_dn_if_exists(rcl) + entry_attrs['ipatokenradiusconfiglink'] = answer + + self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, + **options) + + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + assert isinstance(dn, DN) + config = ldap.get_ipa_config() + # add the user we just created into the default primary group + 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(dn, group_dn) + except errors.AlreadyGroupMember: + pass + + # delete description attribute NO_UPG_MAGIC if present + if options.get('noprivate', False): + if not options.get('all', False): + desc_attr = ldap.get_entry(dn, ['description']) + entry_attrs.update(desc_attr) + if 'description' in entry_attrs and NO_UPG_MAGIC in entry_attrs['description']: + entry_attrs['description'].remove(NO_UPG_MAGIC) + kw = {'setattr': unicode('description=%s' % ','.join(entry_attrs['description']))} + try: + self.api.Command['user_mod'](keys[-1], **kw) + except (errors.EmptyModlist, errors.NotFound): + pass + + # 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_preserved_attribute(entry_attrs, options) + + self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) + + return dn + + +@register() +class user_del(baseuser_del): + __doc__ = _('Delete a user.') + + msg_summary = _('Deleted user "%(value)s"') + + takes_options = baseuser_del.takes_options + ( + Bool('preserve?', + exclude='cli', + ), + ) + + def _preserve_user(self, pkey, delete_container, **options): + assert isinstance(delete_container, DN) + + dn = self.obj.get_either_dn(pkey, **options) + delete_dn = DN(dn[0], delete_container) + ldap = self.obj.backend + self.log.debug("preserve move %s -> %s" % (dn, delete_dn)) + + if dn.endswith(delete_container): + raise errors.ExecutionError( + _('%s: user is already preserved' % pkey) + ) + # Check that this value is a Active user + try: + original_entry_attrs = self._exc_wrapper( + pkey, options, ldap.get_entry)(dn, ['dn']) + except errors.NotFound: + self.obj.handle_not_found(pkey) + + for callback in self.get_callbacks('pre'): + dn = callback(self, ldap, dn, pkey, **options) + assert isinstance(dn, DN) + + # start to move the entry to Delete container + self._exc_wrapper(pkey, options, ldap.move_entry)(dn, delete_dn, + del_old=True) + + # Then clear the credential attributes + attrs_to_clear = ['krbPrincipalKey', 'krbLastPwdChange', + 'krbPasswordExpiration', 'userPassword'] + + entry_attrs = self._exc_wrapper(pkey, options, ldap.get_entry)( + delete_dn, attrs_to_clear) + + clearedCredential = False + for attr in attrs_to_clear: + if attr.lower() in entry_attrs: + del entry_attrs[attr] + clearedCredential = True + if clearedCredential: + self._exc_wrapper(pkey, options, ldap.update_entry)(entry_attrs) + + # Then restore some original entry attributes + attrs_to_restore = ['secretary', 'managedby', 'manager', 'ipauniqueid', + 'uidnumber', 'gidnumber', 'passwordHistory'] + + entry_attrs = self._exc_wrapper( + pkey, options, ldap.get_entry)(delete_dn, attrs_to_restore) + + restoreAttr = False + for attr in attrs_to_restore: + if ((attr.lower() in original_entry_attrs) and + not (attr.lower() in entry_attrs)): + restoreAttr = True + entry_attrs[attr.lower()] = original_entry_attrs[attr.lower()] + if restoreAttr: + self._exc_wrapper(pkey, options, ldap.update_entry)(entry_attrs) + + def pre_callback(self, ldap, dn, *keys, **options): + dn = self.obj.get_either_dn(*keys, **options) + + # For User life Cycle: user-del is a common plugin + # command to delete active user (active container) and + # delete user (delete container). + # If the target entry is a Delete entry, skip the orphaning/removal + # of OTP tokens. + check_protected_member(keys[-1]) + + if not options.get('preserve', False): + # Remove any ID overrides tied with this user + try: + remove_ipaobject_overrides(self.obj.backend, self.obj.api, dn) + except errors.NotFound: + self.obj.handle_not_found(*keys) + + if dn.endswith(DN(self.obj.delete_container_dn, api.env.basedn)): + return dn + + # Delete all tokens owned and managed by this user. + # Orphan all tokens owned but not managed by this user. + owner = self.api.Object.user.get_primary_key_from_dn(dn) + results = self.api.Command.otptoken_find( + ipatokenowner=owner, no_members=False)['result'] + for token in results: + orphan = not [x for x in token.get('managedby_user', []) if x == owner] + token = self.api.Object.otptoken.get_primary_key_from_dn(token['dn']) + if orphan: + self.api.Command.otptoken_mod(token, ipatokenowner=None) + else: + self.api.Command.otptoken_del(token) + + return dn + + def execute(self, *keys, **options): + + # We are going to permanent delete or the user is already in the delete container. + delete_container = DN(self.obj.delete_container_dn, self.api.env.basedn) + + # The user to delete is active and there is no 'no_preserve' option + if options.get('preserve', False): + failed = [] + preserved = [] + for pkey in keys[-1]: + try: + self._preserve_user(pkey, delete_container, **options) + preserved.append(pkey_to_value(pkey, options)) + except Exception: + if not options.get('continue', False): + raise + failed.append(pkey_to_value(pkey, options)) + + val = dict(result=dict(failed=failed), value=preserved) + return val + else: + return super(user_del, self).execute(*keys, **options) + + +@register() +class user_mod(baseuser_mod): + __doc__ = _('Modify a user.') + + msg_summary = _('Modified user "%(value)s"') + + has_output_params = baseuser_mod.has_output_params + user_output_params + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + dn = self.obj.get_either_dn(*keys, **options) + self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, + **options) + validate_nsaccountlock(entry_attrs) + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) + self.obj.get_preserved_attribute(entry_attrs, options) + return dn + + +@register() +class user_find(baseuser_find): + __doc__ = _('Search for users.') + + member_attributes = ['memberof'] + has_output_params = baseuser_find.has_output_params + user_output_params + + msg_summary = ngettext( + '%(count)d user matched', '%(count)d users matched', 0 + ) + + takes_options = LDAPSearch.takes_options + ( + Flag('whoami', + label=_('Self'), + doc=_('Display user record for current Kerberos principal'), + ), + ) + + def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, **options): + assert isinstance(base_dn, DN) + self.pre_common_callback(ldap, filter, attrs_list, base_dn, scope, + *keys, **options) + + if options.get('whoami'): + return ("(&(objectclass=posixaccount)(krbprincipalname=%s))"%\ + getattr(context, 'principal'), base_dn, scope) + + preserved = options.get('preserved', False) + if preserved is None: + base_dn = self.api.env.basedn + scope = ldap.SCOPE_SUBTREE + elif preserved: + base_dn = DN(self.obj.delete_container_dn, self.api.env.basedn) + else: + base_dn = DN(self.obj.active_container_dn, self.api.env.basedn) + + return (filter, base_dn, scope) + + def post_callback(self, ldap, entries, truncated, *args, **options): + if options.get('pkey_only', False): + return truncated + + if options.get('preserved', False) is None: + base_dns = ( + DN(self.obj.active_container_dn, self.api.env.basedn), + DN(self.obj.delete_container_dn, self.api.env.basedn), + ) + entries[:] = [e for e in entries + if any(e.dn.endswith(bd) for bd in base_dns)] + + self.post_common_callback(ldap, entries, lockout=False, **options) + for entry in entries: + self.obj.get_preserved_attribute(entry, options) + + return truncated + + +@register() +class user_show(baseuser_show): + __doc__ = _('Display information about a user.') + + has_output_params = baseuser_show.has_output_params + user_output_params + takes_options = baseuser_show.takes_options + ( + Str('out?', + doc=_('file to store certificate in'), + ), + ) + + def pre_callback(self, ldap, dn, attrs_list, *keys, **options): + dn = self.obj.get_either_dn(*keys, **options) + self.pre_common_callback(ldap, dn, attrs_list, *keys, **options) + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + convert_nsaccountlock(entry_attrs) + self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) + self.obj.get_preserved_attribute(entry_attrs, options) + return dn + + +@register() +class user_undel(LDAPQuery): + __doc__ = _('Undelete a delete user account.') + + has_output = output.standard_value + msg_summary = _('Undeleted user account "%(value)s"') + + def execute(self, *keys, **options): + ldap = self.obj.backend + + # First check that the user exists and is a delete one + delete_dn = self.obj.get_either_dn(*keys, **options) + try: + entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(delete_dn) + except errors.NotFound: + self.obj.handle_not_found(*keys) + if delete_dn.endswith(DN(self.obj.active_container_dn, + api.env.basedn)): + raise errors.InvocationError( + message=_('user "%s" is already active') % keys[-1]) + + active_dn = DN(delete_dn[0], self.obj.active_container_dn, api.env.basedn) + + # start to move the entry to the Active container + self._exc_wrapper(keys, options, ldap.move_entry)(delete_dn, active_dn, del_old=True) + + # add the user we just undelete 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 + + return dict( + result=True, + value=pkey_to_value(keys[0], options), + ) + + +@register() +class user_stage(LDAPMultiQuery): + __doc__ = _('Move deleted user into staged area') + + has_output = output.standard_multi_delete + msg_summary = _('Staged user account "%(value)s"') + + def execute(self, *keys, **options): + staged = [] + failed = [] + + for key in keys[-1]: + single_keys = keys[:-1] + (key,) + multi_keys = keys[:-1] + ((key,),) + + user = self.api.Command.user_show(*single_keys, all=True)['result'] + new_options = {} + for param in self.api.Command.stageuser_add.options(): + try: + value = user[param.name] + except KeyError: + continue + if param.multivalue and not isinstance(value, (list, tuple)): + value = [value] + elif not param.multivalue and isinstance(value, (list, tuple)): + value = value[0] + new_options[param.name] = value + + try: + self.api.Command.stageuser_add(*single_keys, **new_options) + try: + self.api.Command.user_del(*multi_keys, preserve=False) + except errors.ExecutionError: + self.api.Command.stageuser_del(*multi_keys) + raise + except errors.ExecutionError: + if not options['continue']: + raise + failed.append(key) + else: + staged.append(key) + + return dict( + result=dict( + failed=pkey_to_value(failed, options), + ), + value=pkey_to_value(staged, options), + ) + + +@register() +class user_disable(LDAPQuery): + __doc__ = _('Disable a user account.') + + has_output = output.standard_value + msg_summary = _('Disabled user account "%(value)s"') + + def execute(self, *keys, **options): + ldap = self.obj.backend + + check_protected_member(keys[-1]) + + dn = self.obj.get_either_dn(*keys, **options) + ldap.deactivate_entry(dn) + + return dict( + result=True, + value=pkey_to_value(keys[0], options), + ) + + +@register() +class user_enable(LDAPQuery): + __doc__ = _('Enable a user account.') + + has_output = output.standard_value + has_output_params = LDAPQuery.has_output_params + user_output_params + msg_summary = _('Enabled user account "%(value)s"') + + def execute(self, *keys, **options): + ldap = self.obj.backend + + dn = self.obj.get_either_dn(*keys, **options) + + ldap.activate_entry(dn) + + return dict( + result=True, + value=pkey_to_value(keys[0], options), + ) + + +@register() +class user_unlock(LDAPQuery): + __doc__ = _(""" + Unlock a user account + + An account may become locked if the password is entered incorrectly too + many times within a specific time period as controlled by password + policy. A locked account is a temporary condition and may be unlocked by + an administrator.""") + + has_output = output.standard_value + msg_summary = _('Unlocked account "%(value)s"') + + def execute(self, *keys, **options): + dn = self.obj.get_either_dn(*keys, **options) + entry = self.obj.backend.get_entry( + dn, ['krbLastAdminUnlock', 'krbLoginFailedCount']) + + entry['krbLastAdminUnlock'] = [strftime("%Y%m%d%H%M%SZ", gmtime())] + entry['krbLoginFailedCount'] = ['0'] + + self.obj.backend.update_entry(entry) + + return dict( + result=True, + value=pkey_to_value(keys[0], options), + ) + + +@register() +class user_status(LDAPQuery): + __doc__ = _(""" + Lockout status of a user account + + An account may become locked if the password is entered incorrectly too + many times within a specific time period as controlled by password + policy. A locked account is a temporary condition and may be unlocked by + an administrator. + + This connects to each IPA master and displays the lockout status on + each one. + + To determine whether an account is locked on a given server you need + to compare the number of failed logins and the time of the last failure. + For an account to be locked it must exceed the maxfail failures within + the failinterval duration as specified in the password policy associated + with the user. + + The failed login counter is modified only when a user attempts a log in + so it is possible that an account may appear locked but the last failed + login attempt is older than the lockouttime of the password policy. This + means that the user may attempt a login again. """) + + has_output = output.standard_list_of_entries + has_output_params = LDAPSearch.has_output_params + status_output_params + + def execute(self, *keys, **options): + ldap = self.obj.backend + dn = self.obj.get_either_dn(*keys, **options) + attr_list = ['krbloginfailedcount', 'krblastsuccessfulauth', 'krblastfailedauth', 'nsaccountlock'] + + disabled = False + masters = [] + # Get list of masters + try: + (masters, truncated) = ldap.find_entries( + None, ['*'], DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn), + ldap.SCOPE_ONELEVEL + ) + except errors.NotFound: + # If this happens we have some pretty serious problems + self.error('No IPA masters found!') + + entries = [] + count = 0 + for master in masters: + host = master['cn'][0] + if host == api.env.host: + other_ldap = self.obj.backend + else: + other_ldap = ldap2(self.api, ldap_uri='ldap://%s' % host) + try: + other_ldap.connect(ccache=os.environ['KRB5CCNAME']) + except Exception as e: + self.error("user_status: Connecting to %s failed with %s" % (host, str(e))) + newresult = {'dn': dn} + newresult['server'] = _("%(host)s failed: %(error)s") % dict(host=host, error=str(e)) + entries.append(newresult) + count += 1 + continue + try: + entry = other_ldap.get_entry(dn, attr_list) + newresult = {'dn': dn} + for attr in ['krblastsuccessfulauth', 'krblastfailedauth']: + newresult[attr] = entry.get(attr, [u'N/A']) + newresult['krbloginfailedcount'] = entry.get('krbloginfailedcount', u'0') + if not options.get('raw', False): + for attr in ['krblastsuccessfulauth', 'krblastfailedauth']: + try: + if newresult[attr][0] == u'N/A': + continue + newtime = time.strptime(newresult[attr][0], '%Y%m%d%H%M%SZ') + newresult[attr][0] = unicode(time.strftime('%Y-%m-%dT%H:%M:%SZ', newtime)) + except Exception as e: + self.debug("time conversion failed with %s" % str(e)) + newresult['server'] = host + if options.get('raw', False): + time_format = '%Y%m%d%H%M%SZ' + else: + time_format = '%Y-%m-%dT%H:%M:%SZ' + newresult['now'] = unicode(strftime(time_format, gmtime())) + convert_nsaccountlock(entry) + if 'nsaccountlock' in entry: + disabled = entry['nsaccountlock'] + self.obj.get_preserved_attribute(entry, options) + entries.append(newresult) + count += 1 + except errors.NotFound: + self.obj.handle_not_found(*keys) + except Exception as e: + self.error("user_status: Retrieving status for %s failed with %s" % (dn, str(e))) + newresult = {'dn': dn} + newresult['server'] = _("%(host)s failed") % dict(host=host) + entries.append(newresult) + count += 1 + + if host != api.env.host: + other_ldap.disconnect() + + return dict(result=entries, + count=count, + truncated=False, + summary=unicode(_('Account disabled: %(disabled)s' % + dict(disabled=disabled))), + ) + + +@register() +class user_add_cert(LDAPAddAttribute): + __doc__ = _('Add one or more certificates to the user entry') + msg_summary = _('Added certificates to user "%(value)s"') + attribute = 'usercertificate' + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, + **options): + dn = self.obj.get_either_dn(*keys, **options) + + self.obj.convert_usercertificate_pre(entry_attrs) + + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + assert isinstance(dn, DN) + + self.obj.convert_usercertificate_post(entry_attrs, **options) + + return dn + + +@register() +class user_remove_cert(LDAPRemoveAttribute): + __doc__ = _('Remove one or more certificates to the user entry') + msg_summary = _('Removed certificates from user "%(value)s"') + attribute = 'usercertificate' + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, + **options): + dn = self.obj.get_either_dn(*keys, **options) + + self.obj.convert_usercertificate_pre(entry_attrs) + + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + assert isinstance(dn, DN) + + self.obj.convert_usercertificate_post(entry_attrs, **options) + + return dn + + +@register() +class user_add_manager(baseuser_add_manager): + __doc__ = _("Add a manager to the user entry") + + +@register() +class user_remove_manager(baseuser_remove_manager): + __doc__ = _("Remove a manager to the user entry") |