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

Last change on this file since 9678 was 9313, checked in by Henrik Bettermann, 12 years ago

Also site (global) roles must be unset when removin a user.

  • Property svn:keywords set to Id
File size: 16.4 KB
RevLine 
[7193]1## $Id: authentication.py 9313 2012-10-08 09:16:59Z 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
[7169]21from zope.event import notify
[5900]22from zope.component import getUtility, getUtilitiesFor
[8973]23from zope.component.interfaces import IFactory
[7169]24from zope.interface import Interface
[8756]25from zope.schema import getFields
[7169]26from zope.securitypolicy.interfaces import (
27    IPrincipalRoleMap, IPrincipalRoleManager)
[7233]28from zope.pluggableauth.factories import Principal
[6610]29from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
30from zope.pluggableauth.interfaces import (
[7233]31        ICredentialsPlugin, IAuthenticatorPlugin,
32        IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated)
33from zope.publisher.interfaces import IRequest
[6610]34from zope.password.interfaces import IPasswordManager
[4129]35from zope.securitypolicy.principalrole import principalRoleManager
[7811]36from waeup.kofa.interfaces import (ILocalRoleSetEvent,
[7233]37    IUserAccount, IAuthPluginUtility, IPasswordValidator,
[8973]38    IKofaPrincipal, IKofaPrincipalInfo, IKofaPluggable,
[9312]39    IBatchProcessor, IGNORE_MARKER)
[8973]40from waeup.kofa.utils.batching import BatchProcessor
[4073]41
42def setup_authentication(pau):
43    """Set up plugguble authentication utility.
44
45    Sets up an IAuthenticatorPlugin and
46    ICredentialsPlugin (for the authentication mechanism)
[5900]47
48    Then looks for any external utilities that want to modify the PAU.
[4073]49    """
[6661]50    pau.credentialsPlugins = ('No Challenge if Authenticated', 'credentials')
[6672]51    pau.authenticatorPlugins = ('users',)
[4073]52
[5900]53    # Give any third-party code and subpackages a chance to modify the PAU
54    auth_plugin_utilities = getUtilitiesFor(IAuthPluginUtility)
[5901]55    for name, util in auth_plugin_utilities:
[5900]56        util.register(pau)
57
[6673]58def get_principal_role_manager():
59    """Get a role manager for principals.
60
61    If we are currently 'in a site', return the role manager for the
62    portal or the global rolemanager else.
63    """
64    portal = grok.getSite()
65    if portal is not None:
66        return IPrincipalRoleManager(portal)
67    return principalRoleManager
68
[7819]69class KofaSessionCredentialsPlugin(grok.GlobalUtility,
[4073]70                                    SessionCredentialsPlugin):
71    grok.provides(ICredentialsPlugin)
72    grok.name('credentials')
73
74    loginpagename = 'login'
75    loginfield = 'form.login'
76    passwordfield = 'form.password'
77
[7819]78class KofaPrincipalInfo(object):
79    """An implementation of IKofaPrincipalInfo.
[4073]80
[7819]81    A Kofa principal info is created with id, login, title, description,
[8757]82    phone, email, public_name and user_type.
[7233]83    """
[7819]84    grok.implements(IKofaPrincipalInfo)
[7233]85
[8757]86    def __init__(self, id, title, description, email, phone, public_name, user_type):
[4073]87        self.id = id
88        self.title = title
89        self.description = description
[7233]90        self.email = email
91        self.phone = phone
[8757]92        self.public_name = public_name
[7240]93        self.user_type = user_type
[4073]94        self.credentialsPlugin = None
95        self.authenticatorPlugin = None
96
[7819]97class KofaPrincipal(Principal):
[7233]98    """A portal principal.
99
[8757]100    Kofa principals provide an extra `email`, `phone`, `public_name` and `user_type`
[7233]101    attribute extending ordinary principals.
102    """
103
[7819]104    grok.implements(IKofaPrincipal)
[7233]105
106    def __init__(self, id, title=u'', description=u'', email=u'',
[8757]107                 phone=None, public_name=u'', user_type=u'', prefix=None):
[7233]108        self.id = id
[7239]109        if prefix is not None:
110            self.id = '%s.%s' % (prefix, self.id)
[7233]111        self.title = title
112        self.description = description
113        self.groups = []
114        self.email = email
115        self.phone = phone
[8757]116        self.public_name = public_name
[7240]117        self.user_type = user_type
[7233]118
119    def __repr__(self):
[7819]120        return 'KofaPrincipal(%r)' % self.id
[7233]121
[7819]122class AuthenticatedKofaPrincipalFactory(grok.MultiAdapter):
123    """Creates 'authenticated' Kofa principals.
[7233]124
[7819]125    Adapts (principal info, request) to a KofaPrincipal instance.
[7233]126
127    This adapter is used by the standard PAU to transform
[7819]128    KofaPrincipalInfos into KofaPrincipal instances.
[7233]129    """
[7819]130    grok.adapts(IKofaPrincipalInfo, IRequest)
[7233]131    grok.implements(IAuthenticatedPrincipalFactory)
132
133    def __init__(self, info, request):
134        self.info = info
135        self.request = request
136
137    def __call__(self, authentication):
[7819]138        principal = KofaPrincipal(
[7233]139            self.info.id,
140            self.info.title,
141            self.info.description,
142            self.info.email,
143            self.info.phone,
[8757]144            self.info.public_name,
[7240]145            self.info.user_type,
[7239]146            authentication.prefix,
[7233]147            )
148        notify(
149            AuthenticatedPrincipalCreated(
150                authentication, principal, self.info, self.request))
151        return principal
152
[4073]153class Account(grok.Model):
[4109]154    grok.implements(IUserAccount)
[4129]155
[6180]156    _local_roles = dict()
157
[4125]158    def __init__(self, name, password, title=None, description=None,
[8756]159                 email=None, phone=None, public_name=None, roles = []):
[4073]160        self.name = name
[4087]161        if title is None:
162            title = name
163        self.title = title
164        self.description = description
[7221]165        self.email = email
[7233]166        self.phone = phone
[8756]167        self.public_name = public_name
[4073]168        self.setPassword(password)
[7636]169        self.setSiteRolesForPrincipal(roles)
[6180]170        # We don't want to share this dict with other accounts
171        self._local_roles = dict()
[4073]172
173    def setPassword(self, password):
[8343]174        passwordmanager = getUtility(IPasswordManager, 'SSHA')
[4073]175        self.password = passwordmanager.encodePassword(password)
176
177    def checkPassword(self, password):
[8344]178        if not isinstance(password, basestring):
179            return False
180        if not self.password:
181            # unset/empty passwords do never match
182            return False
[8343]183        passwordmanager = getUtility(IPasswordManager, 'SSHA')
[4073]184        return passwordmanager.checkPassword(self.password, password)
185
[7177]186    def getSiteRolesForPrincipal(self):
[6673]187        prm = get_principal_role_manager()
[4129]188        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
189                 if x[0].startswith('waeup.')]
190        return roles
[5055]191
[7177]192    def setSiteRolesForPrincipal(self, roles):
[6673]193        prm = get_principal_role_manager()
[7177]194        old_roles = self.getSiteRolesForPrincipal()
[7658]195        if sorted(old_roles) == sorted(roles):
196            return
[4129]197        for role in old_roles:
198            # Remove old roles, not to be set now...
199            if role.startswith('waeup.') and role not in roles:
200                prm.unsetRoleForPrincipal(role, self.name)
201        for role in roles:
[7658]202            # Convert role to ASCII string to be in line with the
203            # event handler
204            prm.assignRoleToPrincipal(str(role), self.name)
205        return
[4129]206
[7177]207    roles = property(getSiteRolesForPrincipal, setSiteRolesForPrincipal)
[4129]208
[6180]209    def getLocalRoles(self):
210        return self._local_roles
211
212    def notifyLocalRoleChanged(self, obj, role_id, granted=True):
213        objects = self._local_roles.get(role_id, [])
214        if granted and obj not in objects:
215            objects.append(obj)
216        if not granted and obj in objects:
217            objects.remove(obj)
218        self._local_roles[role_id] = objects
219        if len(objects) == 0:
220            del self._local_roles[role_id]
[6182]221        self._p_changed = True
[6180]222        return
223
[4073]224class UserAuthenticatorPlugin(grok.GlobalUtility):
[6625]225    grok.implements(IAuthenticatorPlugin)
[4073]226    grok.provides(IAuthenticatorPlugin)
227    grok.name('users')
228
229    def authenticateCredentials(self, credentials):
230        if not isinstance(credentials, dict):
231            return None
232        if not ('login' in credentials and 'password' in credentials):
233            return None
234        account = self.getAccount(credentials['login'])
235        if account is None:
236            return None
237        if not account.checkPassword(credentials['password']):
238            return None
[7819]239        return KofaPrincipalInfo(
[7315]240            id=account.name,
241            title=account.title,
242            description=account.description,
243            email=account.email,
244            phone=account.phone,
[8757]245            public_name=account.public_name,
[7315]246            user_type=u'user')
[4073]247
248    def principalInfo(self, id):
249        account = self.getAccount(id)
250        if account is None:
251            return None
[7819]252        return KofaPrincipalInfo(
[7315]253            id=account.name,
254            title=account.title,
255            description=account.description,
256            email=account.email,
257            phone=account.phone,
[8757]258            public_name=account.public_name,
[7315]259            user_type=u'user')
[4073]260
261    def getAccount(self, login):
[4087]262        # ... look up the account object and return it ...
[7172]263        userscontainer = self.getUsersContainer()
264        if userscontainer is None:
[4087]265            return
[7172]266        return userscontainer.get(login, None)
[4087]267
268    def addAccount(self, account):
[7172]269        userscontainer = self.getUsersContainer()
270        if userscontainer is None:
[4087]271            return
272        # XXX: complain if name already exists...
[7172]273        userscontainer.addAccount(account)
[4087]274
[4091]275    def addUser(self, name, password, title=None, description=None):
[7172]276        userscontainer = self.getUsersContainer()
277        if userscontainer is None:
[4091]278            return
[7172]279        userscontainer.addUser(name, password, title, description)
[5055]280
[7172]281    def getUsersContainer(self):
[4087]282        site = grok.getSite()
[4743]283        return site['users']
[6203]284
[7147]285class PasswordValidator(grok.GlobalUtility):
286
287  grok.implements(IPasswordValidator)
288
289  def validate_password(self, pw, pw_repeat):
290       errors = []
291       if len(pw) < 3:
292         errors.append('Password must have at least 3 chars.')
293       if pw != pw_repeat:
294         errors.append('Passwords do not match.')
295       return errors
296
[7169]297class LocalRoleSetEvent(object):
298
299    grok.implements(ILocalRoleSetEvent)
300
301    def __init__(self, object, role_id, principal_id, granted=True):
302        self.object = object
303        self.role_id = role_id
304        self.principal_id = principal_id
305        self.granted = granted
306
[6203]307@grok.subscribe(IUserAccount, grok.IObjectRemovedEvent)
[6839]308def handle_account_removed(account, event):
[9313]309    """When an account is removed, local and global roles might
310    have to be deleted.
[6203]311    """
312    local_roles = account.getLocalRoles()
313    principal = account.name
[9313]314
[6203]315    for role_id, object_list in local_roles.items():
316        for object in object_list:
317            try:
318                role_manager = IPrincipalRoleManager(object)
319            except TypeError:
[9313]320                # No Account object, no role manager, no local roles to remove
[6203]321                continue
322            role_manager.unsetRoleForPrincipal(role_id, principal)
[9313]323    role_manager = IPrincipalRoleManager(grok.getSite())
324    roles = account.getSiteRolesForPrincipal()
325    for role_id in roles:
326        role_manager.unsetRoleForPrincipal(role_id, principal)
[6203]327    return
[7169]328
329@grok.subscribe(IUserAccount, grok.IObjectAddedEvent)
330def handle_account_added(account, event):
331    """When an account is added, the local owner role and the global
[7185]332    AcademicsOfficer role must be set.
[7169]333    """
[7173]334    # We set the local Owner role
335    role_manager_account = IPrincipalRoleManager(account)
336    role_manager_account.assignRoleToPrincipal(
[7169]337        'waeup.local.Owner', account.name)
[7185]338    # We set the global AcademicsOfficer role
[7173]339    site = grok.getSite()
340    role_manager_site = IPrincipalRoleManager(site)
341    role_manager_site.assignRoleToPrincipal(
[7185]342        'waeup.AcademicsOfficer', account.name)
[7173]343    # Finally we have to notify the user account that the local role
[7169]344    # of the same object has changed
345    notify(LocalRoleSetEvent(
346        account, 'waeup.local.Owner', account.name, granted=True))
347    return
348
349@grok.subscribe(Interface, ILocalRoleSetEvent)
350def handle_local_role_changed(obj, event):
351    site = grok.getSite()
352    if site is None:
353        return
354    users = site.get('users', None)
355    if users is None:
356        return
357    if event.principal_id not in users.keys():
358        return
359    user = users[event.principal_id]
360    user.notifyLocalRoleChanged(event.object, event.role_id, event.granted)
361    return
362
363@grok.subscribe(Interface, grok.IObjectRemovedEvent)
364def handle_local_roles_on_obj_removed(obj, event):
365    try:
366        role_map = IPrincipalRoleMap(obj)
367    except TypeError:
368        # no map, no roles to remove
369        return
370    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
371        notify(LocalRoleSetEvent(
372                obj, local_role, user_name, granted=False))
[7315]373    return
[8756]374
[8973]375class UserAccountFactory(grok.GlobalUtility):
376    """A factory for user accounts.
377
378    This factory is only needed for imports.
379    """
380    grok.implements(IFactory)
381    grok.name(u'waeup.UserAccount')
382    title = u"Create a user.",
383    description = u"This factory instantiates new user account instances."
384
385    def __call__(self, *args, **kw):
386        return Account(name=None, password='')
387
388    def getInterfaces(self):
389        return implementedBy(Account)
390
391class UserProcessor(BatchProcessor):
392    """A batch processor for IUserAccount objects.
393    """
394    grok.implements(IBatchProcessor)
395    grok.provides(IBatchProcessor)
396    grok.context(Interface)
397    util_name = 'userprocessor'
398    grok.name(util_name)
399
400    name = u'User Processor'
401    iface = IUserAccount
402
403    location_fields = ['name',]
404    factory_name = 'waeup.UserAccount'
405
406    mode = None
407
408    def parentsExist(self, row, site):
409        return 'users' in site.keys()
410
411    def entryExists(self, row, site):
412        return row['name'] in site['users'].keys()
413
414    def getParent(self, row, site):
415        return site['users']
416
417    def getEntry(self, row, site):
418        if not self.entryExists(row, site):
419            return None
420        parent = self.getParent(row, site)
421        return parent.get(row['name'])
422
423    def addEntry(self, obj, row, site):
424        parent = self.getParent(row, site)
425        parent.addAccount(obj)
426        return
427
[9312]428    def updateEntry(self, obj, row, site):
429        """Update obj to the values given in row.
430        """
431        changed = []
432        for key, value in row.items():
433            if  key == 'roles':
434                # We cannot simply set the roles attribute here because
435                # we can't assure that the name attribute is set before
436                # the roles attribute is set.
437                continue
438            # Skip fields to be ignored.
439            if value == IGNORE_MARKER:
440                continue
441            if not hasattr(obj, key):
442                continue
443            setattr(obj, key, value)
444            changed.append('%s=%s' % (key, value))
445        roles = row.get('roles', IGNORE_MARKER)
446        if roles not in ('', IGNORE_MARKER):
447            evalvalue = eval(roles)
448            if isinstance(evalvalue, list):
449                setattr(obj, 'roles', evalvalue)
450                changed.append('roles=%s' % roles)
451        # Log actions...
452        items_changed = ', '.join(changed)
[9313]453        grok.getSite().logger.info('%s - %s - updated: %s'
[9312]454            % (self.name, row['name'], items_changed))
455        return
[8973]456
[8756]457class UsersPlugin(grok.GlobalUtility):
458    """A plugin that updates users.
459    """
460
461    grok.implements(IKofaPluggable)
462    grok.name('users')
463
464    deprecated_attributes = []
465
466    def setup(self, site, name, logger):
467        return
468
469    def update(self, site, name, logger):
470        users = site['users']
471        items = getFields(IUserAccount).items()
472        for user in users.values():
473            # Add new attributes
474            for i in items:
475                if not hasattr(user,i[0]):
476                    setattr(user,i[0],i[1].missing_value)
477                    logger.info(
478                        'UsersPlugin: %s attribute %s added.' % (
479                        user.name,i[0]))
480            # Remove deprecated attributes
481            for i in self.deprecated_attributes:
482                try:
483                    delattr(user,i)
484                    logger.info(
485                        'UsersPlugin: %s attribute %s deleted.' % (
486                        user.name,i))
487                except AttributeError:
488                    pass
489        return
Note: See TracBrowser for help on using the repository browser.