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

Last change on this file since 8427 was 8344, checked in by uli, 12 years ago

Make password checking of users more robust.

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