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

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

Add UserProcessor? for batch importing/processing of user accounts.

  • Property svn:keywords set to Id
File size: 15.0 KB
RevLine 
[7193]1## $Id: authentication.py 8973 2012-07-11 09:27:23Z 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,
39    IBatchProcessor)
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):
[6203]309    """When an account is removed, local roles might have to be deleted.
310    """
311    local_roles = account.getLocalRoles()
312    principal = account.name
313    for role_id, object_list in local_roles.items():
314        for object in object_list:
315            try:
316                role_manager = IPrincipalRoleManager(object)
317            except TypeError:
318                # No role manager, no roles to remove
319                continue
320            role_manager.unsetRoleForPrincipal(role_id, principal)
321    return
[7169]322
323@grok.subscribe(IUserAccount, grok.IObjectAddedEvent)
324def handle_account_added(account, event):
325    """When an account is added, the local owner role and the global
[7185]326    AcademicsOfficer role must be set.
[7169]327    """
[7173]328    # We set the local Owner role
329    role_manager_account = IPrincipalRoleManager(account)
330    role_manager_account.assignRoleToPrincipal(
[7169]331        'waeup.local.Owner', account.name)
[7185]332    # We set the global AcademicsOfficer role
[7173]333    site = grok.getSite()
334    role_manager_site = IPrincipalRoleManager(site)
335    role_manager_site.assignRoleToPrincipal(
[7185]336        'waeup.AcademicsOfficer', account.name)
[7173]337    # Finally we have to notify the user account that the local role
[7169]338    # of the same object has changed
339    notify(LocalRoleSetEvent(
340        account, 'waeup.local.Owner', account.name, granted=True))
341    return
342
343@grok.subscribe(Interface, ILocalRoleSetEvent)
344def handle_local_role_changed(obj, event):
345    site = grok.getSite()
346    if site is None:
347        return
348    users = site.get('users', None)
349    if users is None:
350        return
351    if event.principal_id not in users.keys():
352        return
353    user = users[event.principal_id]
354    user.notifyLocalRoleChanged(event.object, event.role_id, event.granted)
355    return
356
357@grok.subscribe(Interface, grok.IObjectRemovedEvent)
358def handle_local_roles_on_obj_removed(obj, event):
359    try:
360        role_map = IPrincipalRoleMap(obj)
361    except TypeError:
362        # no map, no roles to remove
363        return
364    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
365        notify(LocalRoleSetEvent(
366                obj, local_role, user_name, granted=False))
[7315]367    return
[8756]368
[8973]369class UserAccountFactory(grok.GlobalUtility):
370    """A factory for user accounts.
371
372    This factory is only needed for imports.
373    """
374    grok.implements(IFactory)
375    grok.name(u'waeup.UserAccount')
376    title = u"Create a user.",
377    description = u"This factory instantiates new user account instances."
378
379    def __call__(self, *args, **kw):
380        return Account(name=None, password='')
381
382    def getInterfaces(self):
383        return implementedBy(Account)
384
385class UserProcessor(BatchProcessor):
386    """A batch processor for IUserAccount objects.
387    """
388    grok.implements(IBatchProcessor)
389    grok.provides(IBatchProcessor)
390    grok.context(Interface)
391    util_name = 'userprocessor'
392    grok.name(util_name)
393
394    name = u'User Processor'
395    iface = IUserAccount
396
397    location_fields = ['name',]
398    factory_name = 'waeup.UserAccount'
399
400    mode = None
401
402    def parentsExist(self, row, site):
403        return 'users' in site.keys()
404
405    def entryExists(self, row, site):
406        return row['name'] in site['users'].keys()
407
408    def getParent(self, row, site):
409        return site['users']
410
411    def getEntry(self, row, site):
412        if not self.entryExists(row, site):
413            return None
414        parent = self.getParent(row, site)
415        return parent.get(row['name'])
416
417    def addEntry(self, obj, row, site):
418        parent = self.getParent(row, site)
419        parent.addAccount(obj)
420        return
421
422
[8756]423class UsersPlugin(grok.GlobalUtility):
424    """A plugin that updates users.
425    """
426
427    grok.implements(IKofaPluggable)
428    grok.name('users')
429
430    deprecated_attributes = []
431
432    def setup(self, site, name, logger):
433        return
434
435    def update(self, site, name, logger):
436        users = site['users']
437        items = getFields(IUserAccount).items()
438        for user in users.values():
439            # Add new attributes
440            for i in items:
441                if not hasattr(user,i[0]):
442                    setattr(user,i[0],i[1].missing_value)
443                    logger.info(
444                        'UsersPlugin: %s attribute %s added.' % (
445                        user.name,i[0]))
446            # Remove deprecated attributes
447            for i in self.deprecated_attributes:
448                try:
449                    delattr(user,i)
450                    logger.info(
451                        'UsersPlugin: %s attribute %s deleted.' % (
452                        user.name,i))
453                except AttributeError:
454                    pass
455        return
Note: See TracBrowser for help on using the repository browser.