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

Last change on this file since 8525 was 8344, checked in by uli, 13 years ago

Make password checking of users more robust.

  • Property svn:keywords set to Id
File size: 12.1 KB
Line 
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##
18"""Authentication for Kofa.
19"""
20import grok
21from zope.event import notify
22from zope.component import getUtility, getUtilitiesFor
23from zope.interface import Interface
24from zope.securitypolicy.interfaces import (
25    IPrincipalRoleMap, IPrincipalRoleManager)
26from zope.pluggableauth.factories import Principal
27from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
28from zope.pluggableauth.interfaces import (
29        ICredentialsPlugin, IAuthenticatorPlugin,
30        IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated)
31from zope.publisher.interfaces import IRequest
32from zope.password.interfaces import IPasswordManager
33from zope.securitypolicy.principalrole import principalRoleManager
34from waeup.kofa.interfaces import (ILocalRoleSetEvent,
35    IUserAccount, IAuthPluginUtility, IPasswordValidator,
36    IKofaPrincipal, IKofaPrincipalInfo)
37
38def setup_authentication(pau):
39    """Set up plugguble authentication utility.
40
41    Sets up an IAuthenticatorPlugin and
42    ICredentialsPlugin (for the authentication mechanism)
43
44    Then looks for any external utilities that want to modify the PAU.
45    """
46    pau.credentialsPlugins = ('No Challenge if Authenticated', 'credentials')
47    pau.authenticatorPlugins = ('users',)
48
49    # Give any third-party code and subpackages a chance to modify the PAU
50    auth_plugin_utilities = getUtilitiesFor(IAuthPluginUtility)
51    for name, util in auth_plugin_utilities:
52        util.register(pau)
53
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
65class KofaSessionCredentialsPlugin(grok.GlobalUtility,
66                                    SessionCredentialsPlugin):
67    grok.provides(ICredentialsPlugin)
68    grok.name('credentials')
69
70    loginpagename = 'login'
71    loginfield = 'form.login'
72    passwordfield = 'form.password'
73
74class KofaPrincipalInfo(object):
75    """An implementation of IKofaPrincipalInfo.
76
77    A Kofa principal info is created with id, login, title, description,
78    phone, email and user_type.
79    """
80    grok.implements(IKofaPrincipalInfo)
81
82    def __init__(self, id, title, description, email, phone, user_type):
83        self.id = id
84        self.title = title
85        self.description = description
86        self.email = email
87        self.phone = phone
88        self.user_type = user_type
89        self.credentialsPlugin = None
90        self.authenticatorPlugin = None
91
92class KofaPrincipal(Principal):
93    """A portal principal.
94
95    Kofa principals provide an extra `email`, `phone` and `user_type`
96    attribute extending ordinary principals.
97    """
98
99    grok.implements(IKofaPrincipal)
100
101    def __init__(self, id, title=u'', description=u'', email=u'',
102                 phone=None, user_type=u'', prefix=None):
103        self.id = id
104        if prefix is not None:
105            self.id = '%s.%s' % (prefix, self.id)
106        self.title = title
107        self.description = description
108        self.groups = []
109        self.email = email
110        self.phone = phone
111        self.user_type = user_type
112
113    def __repr__(self):
114        return 'KofaPrincipal(%r)' % self.id
115
116class AuthenticatedKofaPrincipalFactory(grok.MultiAdapter):
117    """Creates 'authenticated' Kofa principals.
118
119    Adapts (principal info, request) to a KofaPrincipal instance.
120
121    This adapter is used by the standard PAU to transform
122    KofaPrincipalInfos into KofaPrincipal instances.
123    """
124    grok.adapts(IKofaPrincipalInfo, IRequest)
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):
132        principal = KofaPrincipal(
133            self.info.id,
134            self.info.title,
135            self.info.description,
136            self.info.email,
137            self.info.phone,
138            self.info.user_type,
139            authentication.prefix,
140            )
141        notify(
142            AuthenticatedPrincipalCreated(
143                authentication, principal, self.info, self.request))
144        return principal
145
146class Account(grok.Model):
147    grok.implements(IUserAccount)
148
149    _local_roles = dict()
150
151    def __init__(self, name, password, title=None, description=None,
152                 email=None, phone=None, roles = []):
153        self.name = name
154        if title is None:
155            title = name
156        self.title = title
157        self.description = description
158        self.email = email
159        self.phone = phone
160        self.setPassword(password)
161        self.setSiteRolesForPrincipal(roles)
162        # We don't want to share this dict with other accounts
163        self._local_roles = dict()
164
165    def setPassword(self, password):
166        passwordmanager = getUtility(IPasswordManager, 'SSHA')
167        self.password = passwordmanager.encodePassword(password)
168
169    def checkPassword(self, password):
170        if not isinstance(password, basestring):
171            return False
172        if not self.password:
173            # unset/empty passwords do never match
174            return False
175        passwordmanager = getUtility(IPasswordManager, 'SSHA')
176        return passwordmanager.checkPassword(self.password, password)
177
178    def getSiteRolesForPrincipal(self):
179        prm = get_principal_role_manager()
180        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
181                 if x[0].startswith('waeup.')]
182        return roles
183
184    def setSiteRolesForPrincipal(self, roles):
185        prm = get_principal_role_manager()
186        old_roles = self.getSiteRolesForPrincipal()
187        if sorted(old_roles) == sorted(roles):
188            return
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:
194            # Convert role to ASCII string to be in line with the
195            # event handler
196            prm.assignRoleToPrincipal(str(role), self.name)
197        return
198
199    roles = property(getSiteRolesForPrincipal, setSiteRolesForPrincipal)
200
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]
213        self._p_changed = True
214        return
215
216class UserAuthenticatorPlugin(grok.GlobalUtility):
217    grok.implements(IAuthenticatorPlugin)
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
231        return KofaPrincipalInfo(
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')
238
239    def principalInfo(self, id):
240        account = self.getAccount(id)
241        if account is None:
242            return None
243        return KofaPrincipalInfo(
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')
250
251    def getAccount(self, login):
252        # ... look up the account object and return it ...
253        userscontainer = self.getUsersContainer()
254        if userscontainer is None:
255            return
256        return userscontainer.get(login, None)
257
258    def addAccount(self, account):
259        userscontainer = self.getUsersContainer()
260        if userscontainer is None:
261            return
262        # XXX: complain if name already exists...
263        userscontainer.addAccount(account)
264
265    def addUser(self, name, password, title=None, description=None):
266        userscontainer = self.getUsersContainer()
267        if userscontainer is None:
268            return
269        userscontainer.addUser(name, password, title, description)
270
271    def getUsersContainer(self):
272        site = grok.getSite()
273        return site['users']
274
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
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
297@grok.subscribe(IUserAccount, grok.IObjectRemovedEvent)
298def handle_account_removed(account, event):
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
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
316    AcademicsOfficer role must be set.
317    """
318    # We set the local Owner role
319    role_manager_account = IPrincipalRoleManager(account)
320    role_manager_account.assignRoleToPrincipal(
321        'waeup.local.Owner', account.name)
322    # We set the global AcademicsOfficer role
323    site = grok.getSite()
324    role_manager_site = IPrincipalRoleManager(site)
325    role_manager_site.assignRoleToPrincipal(
326        'waeup.AcademicsOfficer', account.name)
327    # Finally we have to notify the user account that the local role
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))
357    return
Note: See TracBrowser for help on using the repository browser.