source: main/waeup.kofa/trunk/src/waeup/kofa/authentication.py @ 15294

Last change on this file since 15294 was 15287, checked in by Henrik Bettermann, 6 years ago

Stored insecure passwords are no longer accepted.
Officers with an insecure password can't login and are
redirected to the ChangePasswordRequestPage to request a
new password.

  • Property svn:keywords set to Id
File size: 23.6 KB
RevLine 
[7193]1## $Id: authentication.py 15287 2019-01-09 21:17:08Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
[7819]18"""Authentication for Kofa.
[4073]19"""
20import grok
[10055]21import time
[15286]22import re
23from zope.i18n import translate
[7169]24from zope.event import notify
[5900]25from zope.component import getUtility, getUtilitiesFor
[8973]26from zope.component.interfaces import IFactory
[10055]27from zope.interface import Interface, implementedBy
[8756]28from zope.schema import getFields
[7169]29from zope.securitypolicy.interfaces import (
30    IPrincipalRoleMap, IPrincipalRoleManager)
[7233]31from zope.pluggableauth.factories import Principal
[6610]32from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
[14667]33from zope.pluggableauth.plugins.httpplugins import (
[14669]34    HTTPBasicAuthCredentialsPlugin)
[6610]35from zope.pluggableauth.interfaces import (
[14669]36    ICredentialsPlugin, IAuthenticatorPlugin, IAuthenticatedPrincipalFactory,
37    AuthenticatedPrincipalCreated)
[7233]38from zope.publisher.interfaces import IRequest
[6610]39from zope.password.interfaces import IPasswordManager
[4129]40from zope.securitypolicy.principalrole import principalRoleManager
[14669]41from waeup.kofa.interfaces import (
42    ILocalRoleSetEvent, IUserAccount, IAuthPluginUtility, IPasswordValidator,
43    IKofaPrincipal, IKofaPrincipalInfo, IKofaPluggable, IBatchProcessor,
44    IGNORE_MARKER, IFailedLoginInfo)
[8973]45from waeup.kofa.utils.batching import BatchProcessor
[12190]46from waeup.kofa.permissions import get_all_roles
[15286]47from waeup.kofa.interfaces import MessageFactory as _
[4073]48
[14669]49
[4073]50def setup_authentication(pau):
51    """Set up plugguble authentication utility.
52
53    Sets up an IAuthenticatorPlugin and
54    ICredentialsPlugin (for the authentication mechanism)
[5900]55
56    Then looks for any external utilities that want to modify the PAU.
[4073]57    """
[14667]58    pau.credentialsPlugins = (
[14669]59        'No Challenge if Authenticated',
60        'xmlrpc-credentials',
61        'credentials')
[6672]62    pau.authenticatorPlugins = ('users',)
[4073]63
[5900]64    # Give any third-party code and subpackages a chance to modify the PAU
65    auth_plugin_utilities = getUtilitiesFor(IAuthPluginUtility)
[5901]66    for name, util in auth_plugin_utilities:
[5900]67        util.register(pau)
68
[14669]69
[6673]70def get_principal_role_manager():
71    """Get a role manager for principals.
72
73    If we are currently 'in a site', return the role manager for the
74    portal or the global rolemanager else.
75    """
76    portal = grok.getSite()
77    if portal is not None:
78        return IPrincipalRoleManager(portal)
79    return principalRoleManager
80
[14669]81
82class KofaSessionCredentialsPlugin(
83        grok.GlobalUtility, SessionCredentialsPlugin):
[14667]84    """Session plugin that picks usernames/passwords from fields in webforms.
85    """
[4073]86    grok.provides(ICredentialsPlugin)
87    grok.name('credentials')
88
89    loginpagename = 'login'
90    loginfield = 'form.login'
91    passwordfield = 'form.password'
92
[14667]93
94class KofaXMLRPCCredentialsPlugin(
95        grok.GlobalUtility, HTTPBasicAuthCredentialsPlugin):
96    """Plugin that picks useranams/passwords from basic-auth headers.
97
98    As XMLRPC requests send/post their authentication credentials in HTTP
99    basic-auth headers, we need a plugin that can handle this.
100
101    This plugin, however, does no challenging. If a user does not provide
102    basic-auth infos, we will not ask for some. This is correct as we plan to
103    communicate with machines.
104
105    This plugin is planned to be used in "PluggableAuthenitications" registered
106    with `University` instances.
107    """
108    grok.provides(ICredentialsPlugin)
109    grok.name('xmlrpc-credentials')
110
111    def challenge(self, request):
112        """XMLRPC is for machines. No need to challenge.
113        """
114        return False
115
116    def logout(self, request):
117        """Basic auth does not provide any logout possibility.
118        """
119        return False
120
121
[7819]122class KofaPrincipalInfo(object):
123    """An implementation of IKofaPrincipalInfo.
[4073]124
[7819]125    A Kofa principal info is created with id, login, title, description,
[8757]126    phone, email, public_name and user_type.
[7233]127    """
[7819]128    grok.implements(IKofaPrincipalInfo)
[7233]129
[10055]130    def __init__(self, id, title, description, email, phone, public_name,
131                 user_type):
[4073]132        self.id = id
133        self.title = title
134        self.description = description
[7233]135        self.email = email
136        self.phone = phone
[8757]137        self.public_name = public_name
[7240]138        self.user_type = user_type
[4073]139        self.credentialsPlugin = None
140        self.authenticatorPlugin = None
141
[10055]142    def __eq__(self, obj):
143        default = object()
144        result = []
145        for name in ('id', 'title', 'description', 'email', 'phone',
146                     'public_name', 'user_type', 'credentialsPlugin',
147                     'authenticatorPlugin'):
148            result.append(
149                getattr(self, name) == getattr(obj, name, default))
150        return False not in result
151
[14669]152
[7819]153class KofaPrincipal(Principal):
[7233]154    """A portal principal.
155
[10055]156    Kofa principals provide an extra `email`, `phone`, `public_name`
157    and `user_type` attribute extending ordinary principals.
[7233]158    """
159
[7819]160    grok.implements(IKofaPrincipal)
[7233]161
162    def __init__(self, id, title=u'', description=u'', email=u'',
[8757]163                 phone=None, public_name=u'', user_type=u'', prefix=None):
[7233]164        self.id = id
[7239]165        if prefix is not None:
166            self.id = '%s.%s' % (prefix, self.id)
[7233]167        self.title = title
168        self.description = description
169        self.groups = []
170        self.email = email
171        self.phone = phone
[8757]172        self.public_name = public_name
[7240]173        self.user_type = user_type
[7233]174
175    def __repr__(self):
[7819]176        return 'KofaPrincipal(%r)' % self.id
[7233]177
[14669]178
[7819]179class AuthenticatedKofaPrincipalFactory(grok.MultiAdapter):
180    """Creates 'authenticated' Kofa principals.
[7233]181
[7819]182    Adapts (principal info, request) to a KofaPrincipal instance.
[7233]183
184    This adapter is used by the standard PAU to transform
[7819]185    KofaPrincipalInfos into KofaPrincipal instances.
[7233]186    """
[7819]187    grok.adapts(IKofaPrincipalInfo, IRequest)
[7233]188    grok.implements(IAuthenticatedPrincipalFactory)
189
190    def __init__(self, info, request):
191        self.info = info
192        self.request = request
193
194    def __call__(self, authentication):
[7819]195        principal = KofaPrincipal(
[7233]196            self.info.id,
197            self.info.title,
198            self.info.description,
199            self.info.email,
200            self.info.phone,
[8757]201            self.info.public_name,
[7240]202            self.info.user_type,
[7239]203            authentication.prefix,
[7233]204            )
205        notify(
206            AuthenticatedPrincipalCreated(
207                authentication, principal, self.info, self.request))
208        return principal
209
[14669]210
[10055]211class FailedLoginInfo(grok.Model):
212    grok.implements(IFailedLoginInfo)
213
214    def __init__(self, num=0, last=None):
215        self.num = num
216        self.last = last
217        return
218
219    def as_tuple(self):
220        return (self.num, self.last)
221
222    def set_values(self, num=0, last=None):
223        self.num, self.last = num, last
224        self._p_changed = True
225        pass
226
227    def increase(self):
228        self.set_values(num=self.num + 1, last=time.time())
229        pass
230
231    def reset(self):
232        self.set_values(num=0, last=None)
233        pass
234
[14669]235
[4073]236class Account(grok.Model):
[10055]237    """Kofa user accounts store infos about a user.
238
239    Beside the usual data and an (encrypted) password, accounts also
240    have a persistent attribute `failed_logins` which is an instance
241    of `waeup.kofa.authentication.FailedLoginInfo`.
242
243    This attribute can be manipulated directly (set new value,
244    increase values, or reset).
245    """
[4109]246    grok.implements(IUserAccount)
[4129]247
[4125]248    def __init__(self, name, password, title=None, description=None,
[14669]249                 email=None, phone=None, public_name=None, roles=[]):
[4073]250        self.name = name
[4087]251        if title is None:
252            title = name
253        self.title = title
254        self.description = description
[7221]255        self.email = email
[7233]256        self.phone = phone
[8756]257        self.public_name = public_name
[12926]258        self.suspended = False
[4073]259        self.setPassword(password)
[7636]260        self.setSiteRolesForPrincipal(roles)
[10055]261
[6180]262        # We don't want to share this dict with other accounts
263        self._local_roles = dict()
[10055]264        self.failed_logins = FailedLoginInfo()
[4073]265
266    def setPassword(self, password):
[8343]267        passwordmanager = getUtility(IPasswordManager, 'SSHA')
[4073]268        self.password = passwordmanager.encodePassword(password)
269
270    def checkPassword(self, password):
[13394]271        try:
272            blocker = grok.getSite()['configuration'].maintmode_enabled_by
273            if blocker and blocker != self.name:
274                return False
275        except (TypeError, KeyError):  # in unit tests
276            pass
[8344]277        if not isinstance(password, basestring):
278            return False
279        if not self.password:
280            # unset/empty passwords do never match
281            return False
[15287]282        # Do not accept password if password is insecure.
283        validator = getUtility(IPasswordValidator)
284        if validator.validate_secure_password(password, password):
285            return False
[14669]286        if self.suspended:
[12926]287            return False
[8343]288        passwordmanager = getUtility(IPasswordManager, 'SSHA')
[4073]289        return passwordmanager.checkPassword(self.password, password)
290
[7177]291    def getSiteRolesForPrincipal(self):
[6673]292        prm = get_principal_role_manager()
[4129]293        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
294                 if x[0].startswith('waeup.')]
295        return roles
[5055]296
[7177]297    def setSiteRolesForPrincipal(self, roles):
[6673]298        prm = get_principal_role_manager()
[7177]299        old_roles = self.getSiteRolesForPrincipal()
[7658]300        if sorted(old_roles) == sorted(roles):
301            return
[4129]302        for role in old_roles:
303            # Remove old roles, not to be set now...
304            if role.startswith('waeup.') and role not in roles:
305                prm.unsetRoleForPrincipal(role, self.name)
306        for role in roles:
[7658]307            # Convert role to ASCII string to be in line with the
308            # event handler
309            prm.assignRoleToPrincipal(str(role), self.name)
310        return
[4129]311
[7177]312    roles = property(getSiteRolesForPrincipal, setSiteRolesForPrincipal)
[4129]313
[6180]314    def getLocalRoles(self):
315        return self._local_roles
316
317    def notifyLocalRoleChanged(self, obj, role_id, granted=True):
318        objects = self._local_roles.get(role_id, [])
319        if granted and obj not in objects:
320            objects.append(obj)
321        if not granted and obj in objects:
322            objects.remove(obj)
323        self._local_roles[role_id] = objects
324        if len(objects) == 0:
325            del self._local_roles[role_id]
[6182]326        self._p_changed = True
[6180]327        return
328
[14669]329
[4073]330class UserAuthenticatorPlugin(grok.GlobalUtility):
[6625]331    grok.implements(IAuthenticatorPlugin)
[4073]332    grok.provides(IAuthenticatorPlugin)
333    grok.name('users')
334
335    def authenticateCredentials(self, credentials):
336        if not isinstance(credentials, dict):
337            return None
338        if not ('login' in credentials and 'password' in credentials):
339            return None
340        account = self.getAccount(credentials['login'])
341        if account is None:
342            return None
[10055]343        # The following shows how 'time penalties' could be enforced
344        # on failed logins. First three failed logins are 'for
345        # free'. After that the user has to wait for 1, 2, 4, 8, 16,
346        # 32, ... seconds before a login can succeed.
347        # There are, however, some problems to discuss, before we
348        # really use this in all authenticators.
349
350        #num, last = account.failed_logins.as_tuple()
351        #if (num > 2) and (time.time() < (last + 2**(num-3))):
352        #    # tried login while account still blocked due to previous
353        #    # login errors.
354        #    return None
[4073]355        if not account.checkPassword(credentials['password']):
[10055]356            #account.failed_logins.increase()
[4073]357            return None
[7819]358        return KofaPrincipalInfo(
[7315]359            id=account.name,
360            title=account.title,
361            description=account.description,
362            email=account.email,
363            phone=account.phone,
[8757]364            public_name=account.public_name,
[7315]365            user_type=u'user')
[4073]366
367    def principalInfo(self, id):
368        account = self.getAccount(id)
369        if account is None:
370            return None
[7819]371        return KofaPrincipalInfo(
[7315]372            id=account.name,
373            title=account.title,
374            description=account.description,
375            email=account.email,
376            phone=account.phone,
[8757]377            public_name=account.public_name,
[7315]378            user_type=u'user')
[4073]379
380    def getAccount(self, login):
[4087]381        # ... look up the account object and return it ...
[7172]382        userscontainer = self.getUsersContainer()
383        if userscontainer is None:
[4087]384            return
[7172]385        return userscontainer.get(login, None)
[4087]386
387    def addAccount(self, account):
[7172]388        userscontainer = self.getUsersContainer()
389        if userscontainer is None:
[4087]390            return
391        # XXX: complain if name already exists...
[7172]392        userscontainer.addAccount(account)
[4087]393
[4091]394    def addUser(self, name, password, title=None, description=None):
[7172]395        userscontainer = self.getUsersContainer()
396        if userscontainer is None:
[4091]397            return
[7172]398        userscontainer.addUser(name, password, title, description)
[5055]399
[7172]400    def getUsersContainer(self):
[4087]401        site = grok.getSite()
[4743]402        return site['users']
[6203]403
[14669]404
[7147]405class PasswordValidator(grok.GlobalUtility):
406
[14669]407    grok.implements(IPasswordValidator)
[7147]408
[14669]409    def validate_password(self, pw, pw_repeat):
410        errors = []
[15286]411        if len(pw) < 6:
412            errors.append(translate(_('Password must have at least 6 chars.')))
[14669]413        if pw != pw_repeat:
[15286]414            errors.append(translate(_('Passwords do not match.')))
[14669]415        return errors
[7147]416
[15287]417    def validate_secure_password(self, pw, pw_repeat):
[15286]418        """
419        ^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,}$
[14669]420
[15286]421        ^              Start anchor
422        (?=.*[A-Z])    Ensure password has one uppercase letters.
423        (?=.*[0-9])    Ensure password has one digit.
424        (?=.*[a-z])    Ensure password has one lowercase letter.
425        .{8,}          Ensure password is of length 8.
426        $              End anchor.
427        """
[15287]428
429        # temporarily disabled
430        # /kofa/trunk/src/waeup/kofa/doctests/pages.txt line 176 not met
431        return self.validate_password(pw, pw_repeat)
432
[15286]433        check_pw = re.compile(r"^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,}$").match
434        errors = []
435        if not check_pw(pw):
436            errors.append(translate(_(
437                'Passwords must be at least 8 characters long, '
[15287]438                'must contain at least one uppercase letter, '
[15286]439                'one lowercase letter and one digit.')))
440        if pw != pw_repeat:
441            errors.append(translate(_('Passwords do not match.')))
442        return errors
443
444
[7169]445class LocalRoleSetEvent(object):
446
447    grok.implements(ILocalRoleSetEvent)
448
449    def __init__(self, object, role_id, principal_id, granted=True):
450        self.object = object
451        self.role_id = role_id
452        self.principal_id = principal_id
453        self.granted = granted
454
[14669]455
[6203]456@grok.subscribe(IUserAccount, grok.IObjectRemovedEvent)
[6839]457def handle_account_removed(account, event):
[9313]458    """When an account is removed, local and global roles might
459    have to be deleted.
[6203]460    """
461    local_roles = account.getLocalRoles()
462    principal = account.name
[9313]463
[6203]464    for role_id, object_list in local_roles.items():
465        for object in object_list:
466            try:
467                role_manager = IPrincipalRoleManager(object)
468            except TypeError:
[9313]469                # No Account object, no role manager, no local roles to remove
[6203]470                continue
471            role_manager.unsetRoleForPrincipal(role_id, principal)
[9313]472    role_manager = IPrincipalRoleManager(grok.getSite())
473    roles = account.getSiteRolesForPrincipal()
474    for role_id in roles:
475        role_manager.unsetRoleForPrincipal(role_id, principal)
[6203]476    return
[7169]477
[14669]478
[7169]479@grok.subscribe(IUserAccount, grok.IObjectAddedEvent)
480def handle_account_added(account, event):
481    """When an account is added, the local owner role and the global
[7185]482    AcademicsOfficer role must be set.
[7169]483    """
[7173]484    # We set the local Owner role
485    role_manager_account = IPrincipalRoleManager(account)
486    role_manager_account.assignRoleToPrincipal(
[7169]487        'waeup.local.Owner', account.name)
[7185]488    # We set the global AcademicsOfficer role
[7173]489    site = grok.getSite()
490    role_manager_site = IPrincipalRoleManager(site)
491    role_manager_site.assignRoleToPrincipal(
[7185]492        'waeup.AcademicsOfficer', account.name)
[7173]493    # Finally we have to notify the user account that the local role
[7169]494    # of the same object has changed
495    notify(LocalRoleSetEvent(
496        account, 'waeup.local.Owner', account.name, granted=True))
497    return
498
[14669]499
[7169]500@grok.subscribe(Interface, ILocalRoleSetEvent)
501def handle_local_role_changed(obj, event):
502    site = grok.getSite()
503    if site is None:
504        return
505    users = site.get('users', None)
506    if users is None:
507        return
508    if event.principal_id not in users.keys():
509        return
510    user = users[event.principal_id]
511    user.notifyLocalRoleChanged(event.object, event.role_id, event.granted)
512    return
513
[14669]514
[7169]515@grok.subscribe(Interface, grok.IObjectRemovedEvent)
516def handle_local_roles_on_obj_removed(obj, event):
517    try:
518        role_map = IPrincipalRoleMap(obj)
519    except TypeError:
520        # no map, no roles to remove
521        return
522    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
523        notify(LocalRoleSetEvent(
[14669]524            obj, local_role, user_name, granted=False))
[7315]525    return
[8756]526
[14669]527
[8973]528class UserAccountFactory(grok.GlobalUtility):
529    """A factory for user accounts.
530
531    This factory is only needed for imports.
532    """
533    grok.implements(IFactory)
534    grok.name(u'waeup.UserAccount')
535    title = u"Create a user.",
536    description = u"This factory instantiates new user account instances."
537
538    def __call__(self, *args, **kw):
539        return Account(name=None, password='')
540
541    def getInterfaces(self):
542        return implementedBy(Account)
543
[14669]544
[8973]545class UserProcessor(BatchProcessor):
[14669]546    """The User Processor processes user accounts, i.e. `Account` objects in
547    the ``users`` container.
[12869]548
549    The `roles` columns must contain Python list
550    expressions like ``['waeup.PortalManager', 'waeup.ImportManager']``.
551
552    The processor does not import local roles. These can be imported
553    by means of batch processors in the academic section.
[8973]554    """
555    grok.implements(IBatchProcessor)
556    grok.provides(IBatchProcessor)
557    grok.context(Interface)
558    util_name = 'userprocessor'
559    grok.name(util_name)
560
561    name = u'User Processor'
562    iface = IUserAccount
563
[14669]564    location_fields = ['name', ]
[8973]565    factory_name = 'waeup.UserAccount'
566
567    mode = None
568
569    def parentsExist(self, row, site):
570        return 'users' in site.keys()
571
572    def entryExists(self, row, site):
573        return row['name'] in site['users'].keys()
574
575    def getParent(self, row, site):
576        return site['users']
577
578    def getEntry(self, row, site):
579        if not self.entryExists(row, site):
580            return None
581        parent = self.getParent(row, site)
582        return parent.get(row['name'])
583
584    def addEntry(self, obj, row, site):
585        parent = self.getParent(row, site)
586        parent.addAccount(obj)
587        return
588
[13182]589    def delEntry(self, row, site):
590        user = self.getEntry(row, site)
591        if user is not None:
592            parent = self.getParent(row, site)
[14669]593            grok.getSite().logger.info(
594                '%s - %s - User removed' % (self.name, row['name']))
[13182]595            del parent[user.name]
596        pass
597
[9706]598    def updateEntry(self, obj, row, site, filename):
[9312]599        """Update obj to the values given in row.
600        """
601        changed = []
602        for key, value in row.items():
[14669]603            if key == 'roles':
[9312]604                # We cannot simply set the roles attribute here because
605                # we can't assure that the name attribute is set before
606                # the roles attribute is set.
607                continue
608            # Skip fields to be ignored.
609            if value == IGNORE_MARKER:
610                continue
611            if not hasattr(obj, key):
612                continue
613            setattr(obj, key, value)
614            changed.append('%s=%s' % (key, value))
615        roles = row.get('roles', IGNORE_MARKER)
616        if roles not in ('', IGNORE_MARKER):
617            evalvalue = eval(roles)
618            if isinstance(evalvalue, list):
619                setattr(obj, 'roles', evalvalue)
620                changed.append('roles=%s' % roles)
621        # Log actions...
622        items_changed = ', '.join(changed)
[14669]623        grok.getSite().logger.info(
624            '%s - %s - %s - updated: %s' % (
625                self.name, filename, row['name'], items_changed))
[9312]626        return
[8973]627
[12190]628    def checkConversion(self, row, mode='ignore'):
629        """Validates all values in row.
630        """
631        errs, inv_errs, conv_dict = super(
632            UserProcessor, self).checkConversion(row, mode=mode)
633        # We need to check if roles exist.
[13181]634        roles = row.get('roles', IGNORE_MARKER)
[12190]635        all_roles = [i[0] for i in get_all_roles()]
636        if roles not in ('', IGNORE_MARKER):
637            evalvalue = eval(roles)
638            for role in evalvalue:
639                if role not in all_roles:
[14669]640                    errs.append(('roles', 'invalid role'))
[12190]641        return errs, inv_errs, conv_dict
642
[14669]643
[8756]644class UsersPlugin(grok.GlobalUtility):
645    """A plugin that updates users.
646    """
647    grok.implements(IKofaPluggable)
648    grok.name('users')
649
650    deprecated_attributes = []
651
652    def setup(self, site, name, logger):
653        return
654
655    def update(self, site, name, logger):
656        users = site['users']
657        items = getFields(IUserAccount).items()
658        for user in users.values():
659            # Add new attributes
660            for i in items:
[14669]661                if not hasattr(user, i[0]):
662                    setattr(user, i[0], i[1].missing_value)
[8756]663                    logger.info(
664                        'UsersPlugin: %s attribute %s added.' % (
[14669]665                            user.name, i[0]))
[10055]666            if not hasattr(user, 'failed_logins'):
667                # add attribute `failed_logins`...
668                user.failed_logins = FailedLoginInfo()
669                logger.info(
670                    'UsersPlugin: attribute failed_logins added.')
[8756]671            # Remove deprecated attributes
672            for i in self.deprecated_attributes:
673                try:
[14669]674                    delattr(user, i)
[8756]675                    logger.info(
676                        'UsersPlugin: %s attribute %s deleted.' % (
[14669]677                            user.name, i))
[8756]678                except AttributeError:
679                    pass
[10055]680        return
[14670]681
682
683class UpdatePAUPlugin(grok.GlobalUtility):
684    """A plugin that updates a local PAU.
685
686    We insert an 'xmlrpc-credentials' PAU-plugin into a sites PAU if it is not
687    present already. There must be 'credentials' plugin registered already.
688
689    XXX: This Plugin fixes a shortcoming of waeup.kofa 1.5. Sites created or
690         updated afterwards do not need this plugin and it should be removed.
691    """
692    grok.implements(IKofaPluggable)
693    grok.name('site-pluggable-auth')
694
695    def setup(self, site, name, logger):
696        return
697
698    def update(self, site, name, logger):
699        pau = site.getSiteManager()['PluggableAuthentication']
700        if 'xmlrpc-credentials' in pau.credentialsPlugins:
701            return
702        plugins = list(pau.credentialsPlugins)
703        plugins.insert(plugins.index('credentials'), 'xmlrpc-credentials')
704        pau.credentialsPlugins = tuple(plugins)
Note: See TracBrowser for help on using the repository browser.