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

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

Add public_name to IKofaPrincipalInfo and IKofaPrincipal.

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