diff options
Diffstat (limited to 'ipalib/plugins/baseuser.py')
-rw-r--r-- | ipalib/plugins/baseuser.py | 471 |
1 files changed, 471 insertions, 0 deletions
diff --git a/ipalib/plugins/baseuser.py b/ipalib/plugins/baseuser.py new file mode 100644 index 000000000..16c7b2a88 --- /dev/null +++ b/ipalib/plugins/baseuser.py @@ -0,0 +1,471 @@ +# 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 + +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 DN, LDAPObject, \ + LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete, LDAPRetrieve +from ipalib.plugins import baseldap +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) +if api.env.in_server and api.env.context in ['lite', 'server']: + from ipaserver.plugins.ldap2 import ldap2 + +__doc__ = _(""" +Baseuser + +This contains common definitions for user/stageuser +""") + +register = Registry() + +NO_UPG_MAGIC = '__no_upg__' + +baseuser_output_params = ( + Flag('has_keytab', + label=_('Kerberos keys available'), + ), + Str('sshpubkeyfp*', + label=_('SSH public key fingerprint'), + ), + ) + +status_baseuser_output_params = ( + Str('server', + label=_('Server'), + ), + Str('krbloginfailedcount', + label=_('Failed logins'), + ), + Str('krblastsuccessfulauth', + label=_('Last successful authentication'), + ), + Str('krblastfailedauth', + label=_('Last failed authentication'), + ), + Str('now', + label=_('Time now'), + ), + ) + +UPG_DEFINITION_DN = DN(('cn', 'UPG Definition'), + ('cn', 'Definitions'), + ('cn', 'Managed Entries'), + ('cn', 'etc'), + api.env.basedn) + +# characters to be used for generating random user passwords +baseuser_pwdchars = string.digits + string.ascii_letters + '_,.@+-=' + +def validate_nsaccountlock(entry_attrs): + if 'nsaccountlock' in entry_attrs: + nsaccountlock = entry_attrs['nsaccountlock'] + if not isinstance(nsaccountlock, (bool, Bool)): + if not isinstance(nsaccountlock, basestring): + raise errors.OnlyOneValueAllowed(attr='nsaccountlock') + if nsaccountlock.lower() not in ('true', 'false'): + raise errors.ValidationError(name='nsaccountlock', + error=_('must be TRUE or FALSE')) + +def radius_dn2pk(api, entry_attrs): + cl = entry_attrs.get('ipatokenradiusconfiglink', None) + if cl: + pk = api.Object['radiusproxy'].get_primary_key_from_dn(cl[0]) + entry_attrs['ipatokenradiusconfiglink'] = [pk] + +def convert_nsaccountlock(entry_attrs): + if not 'nsaccountlock' in entry_attrs: + entry_attrs['nsaccountlock'] = False + else: + nsaccountlock = Bool('temp') + entry_attrs['nsaccountlock'] = nsaccountlock.convert(entry_attrs['nsaccountlock'][0]) + +def split_principal(principal): + """ + Split the principal into its components and do some basic validation. + + Automatically append our realm if it wasn't provided. + """ + realm = None + parts = principal.split('@') + user = parts[0].lower() + if len(parts) > 2: + raise errors.MalformedUserPrincipal(principal=principal) + + if len(parts) == 2: + realm = parts[1].upper() + # At some point we'll support multiple realms + if realm != api.env.realm: + raise errors.RealmMismatch() + else: + realm = api.env.realm + + return (user, realm) + +def validate_principal(ugettext, principal): + """ + All the real work is done in split_principal. + """ + (user, realm) = split_principal(principal) + return None + +def normalize_principal(principal): + """ + Ensure that the name in the principal is lower-case. The realm is + upper-case by convention but it isn't required. + + The principal is validated at this point. + """ + (user, realm) = split_principal(principal) + return unicode('%s@%s' % (user, realm)) + + + +def fix_addressbook_permission_bindrule(name, template, is_new, + anonymous_read_aci, + **other_options): + """Fix bind rule type for Read User Addressbook/IPA Attributes permission + + When upgrading from an old IPA that had the global read ACI, + or when installing the first replica with granular read permissions, + we need to keep allowing anonymous access to many user attributes. + This fixup_function changes the bind rule type accordingly. + """ + if is_new and anonymous_read_aci: + template['ipapermbindruletype'] = 'anonymous' + + + +class baseuser(LDAPObject): + """ + baseuser object. + """ + + stage_container_dn = api.env.container_stageuser + active_container_dn = api.env.container_user + delete_container_dn = api.env.container_deleteuser + object_class = ['posixaccount'] + object_class_config = 'ipauserobjectclasses' + possible_objectclasses = [ + 'meporiginentry', 'ipauserauthtypeclass', 'ipauser', + 'ipatokenradiusproxyuser' + ] + disallow_object_classes = ['krbticketpolicyaux'] + permission_filter_objectclasses = ['posixaccount'] + search_attributes_config = 'ipausersearchfields' + default_attributes = [ + 'uid', 'givenname', 'sn', 'homedirectory', 'loginshell', + 'uidnumber', 'gidnumber', 'mail', 'ou', + 'telephonenumber', 'title', 'memberof', 'nsaccountlock', + 'memberofindirect', 'ipauserauthtype', 'userclass', + 'ipatokenradiusconfiglink', 'ipatokenradiususername', + 'krbprincipalexpiration' + ] + search_display_attributes = [ + 'uid', 'givenname', 'sn', 'homedirectory', 'loginshell', + 'mail', 'telephonenumber', 'title', 'nsaccountlock', + 'uidnumber', 'gidnumber', 'sshpubkeyfp', + ] + uuid_attribute = 'ipauniqueid' + attribute_members = { + 'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], + 'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], + } + rdn_is_primary_key = True + bindable = True + password_attributes = [('userpassword', 'has_password'), + ('krbprincipalkey', 'has_keytab')] + label = _('Users') + label_singular = _('User') + + takes_params = ( + Str('uid', + pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', + pattern_errmsg='may only include letters, numbers, _, -, . and $', + maxlength=255, + cli_name='login', + label=_('User login'), + primary_key=True, + default_from=lambda givenname, sn: givenname[0] + sn, + normalizer=lambda value: value.lower(), + ), + Str('givenname', + cli_name='first', + label=_('First name'), + ), + Str('sn', + cli_name='last', + label=_('Last name'), + ), + Str('cn', + label=_('Full name'), + default_from=lambda givenname, sn: '%s %s' % (givenname, sn), + autofill=True, + ), + Str('displayname?', + label=_('Display name'), + default_from=lambda givenname, sn: '%s %s' % (givenname, sn), + autofill=True, + ), + Str('initials?', + label=_('Initials'), + default_from=lambda givenname, sn: '%c%c' % (givenname[0], sn[0]), + autofill=True, + ), + Str('homedirectory?', + cli_name='homedir', + label=_('Home directory'), + ), + Str('gecos?', + label=_('GECOS'), + default_from=lambda givenname, sn: '%s %s' % (givenname, sn), + autofill=True, + ), + Str('loginshell?', + cli_name='shell', + label=_('Login shell'), + ), + Str('krbprincipalname?', validate_principal, + cli_name='principal', + label=_('Kerberos principal'), + default_from=lambda uid: '%s@%s' % (uid.lower(), api.env.realm), + autofill=True, + flags=['no_update'], + normalizer=lambda value: normalize_principal(value), + ), + DateTime('krbprincipalexpiration?', + cli_name='principal_expiration', + label=_('Kerberos principal expiration'), + ), + Str('mail*', + cli_name='email', + label=_('Email address'), + ), + Password('userpassword?', + cli_name='password', + label=_('Password'), + doc=_('Prompt to set the user password'), + # FIXME: This is temporary till bug is fixed causing updates to + # bomb out via the webUI. + exclude='webui', + ), + Flag('random?', + doc=_('Generate a random user password'), + flags=('no_search', 'virtual_attribute'), + default=False, + ), + Str('randompassword?', + label=_('Random password'), + flags=('no_create', 'no_update', 'no_search', 'virtual_attribute'), + ), + Int('uidnumber?', + cli_name='uid', + label=_('UID'), + doc=_('User ID Number (system will assign one if not provided)'), + minvalue=1, + ), + Int('gidnumber?', + label=_('GID'), + doc=_('Group ID Number'), + minvalue=1, + ), + Str('street?', + cli_name='street', + label=_('Street address'), + ), + Str('l?', + cli_name='city', + label=_('City'), + ), + Str('st?', + cli_name='state', + label=_('State/Province'), + ), + Str('postalcode?', + label=_('ZIP'), + ), + Str('telephonenumber*', + cli_name='phone', + label=_('Telephone Number') + ), + Str('mobile*', + label=_('Mobile Telephone Number') + ), + Str('pager*', + label=_('Pager Number') + ), + Str('facsimiletelephonenumber*', + cli_name='fax', + label=_('Fax Number'), + ), + Str('ou?', + cli_name='orgunit', + label=_('Org. Unit'), + ), + Str('title?', + label=_('Job Title'), + ), + Str('manager?', + label=_('Manager'), + ), + Str('carlicense*', + label=_('Car License'), + ), + Str('ipasshpubkey*', validate_sshpubkey, + cli_name='sshpubkey', + label=_('SSH public key'), + normalizer=normalize_sshpubkey, + csv=True, + flags=['no_search'], + ), + StrEnum('ipauserauthtype*', + cli_name='user_auth_type', + label=_('User authentication types'), + doc=_('Types of supported user authentication'), + values=(u'password', u'radius', u'otp'), + csv=True, + ), + Str('userclass*', + cli_name='class', + label=_('Class'), + doc=_('User category (semantics placed on this attribute are for ' + 'local interpretation)'), + ), + Str('ipatokenradiusconfiglink?', + cli_name='radius', + label=_('RADIUS proxy configuration'), + ), + Str('ipatokenradiususername?', + cli_name='radius_username', + label=_('RADIUS proxy username'), + ), + Str('departmentnumber*', + label=_('Department Number'), + ), + Str('employeenumber?', + label=_('Employee Number'), + ), + Str('employeetype?', + label=_('Employee Type'), + ), + Str('preferredlanguage?', + label=_('Preferred Language'), + pattern='^(([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?' \ + + '(\s*,\s*[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?)*)|(\*))$', + pattern_errmsg='must match RFC 2068 - 14.4, e.g., "da, en-gb;q=0.8, en;q=0.7"', + ), + ) + + def normalize_and_validate_email(self, email, config=None): + if not config: + config = self.backend.get_ipa_config() + + # check if default email domain should be added + defaultdomain = config.get('ipadefaultemaildomain', [None])[0] + if email: + norm_email = [] + if not isinstance(email, (list, tuple)): + email = [email] + for m in email: + if isinstance(m, basestring): + if '@' not in m and defaultdomain: + m = m + u'@' + defaultdomain + if not Email(m): + raise errors.ValidationError(name='email', error=_('invalid e-mail format: %(email)s') % dict(email=m)) + norm_email.append(m) + else: + if not Email(m): + raise errors.ValidationError(name='email', error=_('invalid e-mail format: %(email)s') % dict(email=m)) + norm_email.append(m) + return norm_email + + return email + + def normalize_manager(self, manager, container): + """ + Given a userid verify the user's existence (in the appropriate containter) and return the dn. + """ + if not manager: + return None + + if not isinstance(manager, list): + manager = [manager] + try: + container_dn = DN(container, api.env.basedn) + for m in xrange(len(manager)): + if isinstance(manager[m], DN) and manager[m].endswith(container_dn): + continue + entry_attrs = self.backend.find_entry_by_attr( + self.primary_key.name, manager[m], self.object_class, [''], + container_dn + ) + manager[m] = entry_attrs.dn + except errors.NotFound: + raise errors.NotFound(reason=_('manager %(manager)s not found') % dict(manager=manager[m])) + + return manager + + def convert_manager(self, entry_attrs, **options): + """ + Convert a manager dn into a userid + """ + if options.get('raw', False): + return + + if 'manager' in entry_attrs: + for m in xrange(len(entry_attrs['manager'])): + entry_attrs['manager'][m] = self.get_primary_key_from_dn(entry_attrs['manager'][m]) + +class baseuser_add(LDAPCreate): + """ + Prototype command plugin to be implemented by real plugin + """ + +class baseuser_del(LDAPDelete): + """ + Prototype command plugin to be implemented by real plugin + """ + +class baseuser_mod(LDAPUpdate): + """ + Prototype command plugin to be implemented by real plugin + """ + +class baseuser_find(LDAPSearch): + """ + Prototype command plugin to be implemented by real plugin + """ + +class baseuser_show(LDAPRetrieve): + """ + Prototype command plugin to be implemented by real plugin + """ |