source: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/authentication.py @ 10371

Last change on this file since 10371 was 9211, checked in by uli, 12 years ago

Rollback r9209. Looks like multiple merges from trunk confuse svn when merging back into trunk.

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