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

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

Also site (global) roles must be unset when removin a user.

  • Property svn:keywords set to Id
File size: 16.4 KB
Line 
1## $Id: authentication.py 9313 2012-10-08 09:16:59Z 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##
18"""Authentication for Kofa.
19"""
20import grok
21from zope.event import notify
22from zope.component import getUtility, getUtilitiesFor
23from zope.component.interfaces import IFactory
24from zope.interface import Interface
25from zope.schema import getFields
26from zope.securitypolicy.interfaces import (
27    IPrincipalRoleMap, IPrincipalRoleManager)
28from zope.pluggableauth.factories import Principal
29from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
30from zope.pluggableauth.interfaces import (
31        ICredentialsPlugin, IAuthenticatorPlugin,
32        IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated)
33from zope.publisher.interfaces import IRequest
34from zope.password.interfaces import IPasswordManager
35from zope.securitypolicy.principalrole import principalRoleManager
36from waeup.kofa.interfaces import (ILocalRoleSetEvent,
37    IUserAccount, IAuthPluginUtility, IPasswordValidator,
38    IKofaPrincipal, IKofaPrincipalInfo, IKofaPluggable,
39    IBatchProcessor, IGNORE_MARKER)
40from waeup.kofa.utils.batching import BatchProcessor
41
42def setup_authentication(pau):
43    """Set up plugguble authentication utility.
44
45    Sets up an IAuthenticatorPlugin and
46    ICredentialsPlugin (for the authentication mechanism)
47
48    Then looks for any external utilities that want to modify the PAU.
49    """
50    pau.credentialsPlugins = ('No Challenge if Authenticated', 'credentials')
51    pau.authenticatorPlugins = ('users',)
52
53    # Give any third-party code and subpackages a chance to modify the PAU
54    auth_plugin_utilities = getUtilitiesFor(IAuthPluginUtility)
55    for name, util in auth_plugin_utilities:
56        util.register(pau)
57
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
69class KofaSessionCredentialsPlugin(grok.GlobalUtility,
70                                    SessionCredentialsPlugin):
71    grok.provides(ICredentialsPlugin)
72    grok.name('credentials')
73
74    loginpagename = 'login'
75    loginfield = 'form.login'
76    passwordfield = 'form.password'
77
78class KofaPrincipalInfo(object):
79    """An implementation of IKofaPrincipalInfo.
80
81    A Kofa principal info is created with id, login, title, description,
82    phone, email, public_name and user_type.
83    """
84    grok.implements(IKofaPrincipalInfo)
85
86    def __init__(self, id, title, description, email, phone, public_name, user_type):
87        self.id = id
88        self.title = title
89        self.description = description
90        self.email = email
91        self.phone = phone
92        self.public_name = public_name
93        self.user_type = user_type
94        self.credentialsPlugin = None
95        self.authenticatorPlugin = None
96
97class KofaPrincipal(Principal):
98    """A portal principal.
99
100    Kofa principals provide an extra `email`, `phone`, `public_name` and `user_type`
101    attribute extending ordinary principals.
102    """
103
104    grok.implements(IKofaPrincipal)
105
106    def __init__(self, id, title=u'', description=u'', email=u'',
107                 phone=None, public_name=u'', user_type=u'', prefix=None):
108        self.id = id
109        if prefix is not None:
110            self.id = '%s.%s' % (prefix, self.id)
111        self.title = title
112        self.description = description
113        self.groups = []
114        self.email = email
115        self.phone = phone
116        self.public_name = public_name
117        self.user_type = user_type
118
119    def __repr__(self):
120        return 'KofaPrincipal(%r)' % self.id
121
122class AuthenticatedKofaPrincipalFactory(grok.MultiAdapter):
123    """Creates 'authenticated' Kofa principals.
124
125    Adapts (principal info, request) to a KofaPrincipal instance.
126
127    This adapter is used by the standard PAU to transform
128    KofaPrincipalInfos into KofaPrincipal instances.
129    """
130    grok.adapts(IKofaPrincipalInfo, IRequest)
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):
138        principal = KofaPrincipal(
139            self.info.id,
140            self.info.title,
141            self.info.description,
142            self.info.email,
143            self.info.phone,
144            self.info.public_name,
145            self.info.user_type,
146            authentication.prefix,
147            )
148        notify(
149            AuthenticatedPrincipalCreated(
150                authentication, principal, self.info, self.request))
151        return principal
152
153class Account(grok.Model):
154    grok.implements(IUserAccount)
155
156    _local_roles = dict()
157
158    def __init__(self, name, password, title=None, description=None,
159                 email=None, phone=None, public_name=None, roles = []):
160        self.name = name
161        if title is None:
162            title = name
163        self.title = title
164        self.description = description
165        self.email = email
166        self.phone = phone
167        self.public_name = public_name
168        self.setPassword(password)
169        self.setSiteRolesForPrincipal(roles)
170        # We don't want to share this dict with other accounts
171        self._local_roles = dict()
172
173    def setPassword(self, password):
174        passwordmanager = getUtility(IPasswordManager, 'SSHA')
175        self.password = passwordmanager.encodePassword(password)
176
177    def checkPassword(self, password):
178        if not isinstance(password, basestring):
179            return False
180        if not self.password:
181            # unset/empty passwords do never match
182            return False
183        passwordmanager = getUtility(IPasswordManager, 'SSHA')
184        return passwordmanager.checkPassword(self.password, password)
185
186    def getSiteRolesForPrincipal(self):
187        prm = get_principal_role_manager()
188        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
189                 if x[0].startswith('waeup.')]
190        return roles
191
192    def setSiteRolesForPrincipal(self, roles):
193        prm = get_principal_role_manager()
194        old_roles = self.getSiteRolesForPrincipal()
195        if sorted(old_roles) == sorted(roles):
196            return
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:
202            # Convert role to ASCII string to be in line with the
203            # event handler
204            prm.assignRoleToPrincipal(str(role), self.name)
205        return
206
207    roles = property(getSiteRolesForPrincipal, setSiteRolesForPrincipal)
208
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]
221        self._p_changed = True
222        return
223
224class UserAuthenticatorPlugin(grok.GlobalUtility):
225    grok.implements(IAuthenticatorPlugin)
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
239        return KofaPrincipalInfo(
240            id=account.name,
241            title=account.title,
242            description=account.description,
243            email=account.email,
244            phone=account.phone,
245            public_name=account.public_name,
246            user_type=u'user')
247
248    def principalInfo(self, id):
249        account = self.getAccount(id)
250        if account is None:
251            return None
252        return KofaPrincipalInfo(
253            id=account.name,
254            title=account.title,
255            description=account.description,
256            email=account.email,
257            phone=account.phone,
258            public_name=account.public_name,
259            user_type=u'user')
260
261    def getAccount(self, login):
262        # ... look up the account object and return it ...
263        userscontainer = self.getUsersContainer()
264        if userscontainer is None:
265            return
266        return userscontainer.get(login, None)
267
268    def addAccount(self, account):
269        userscontainer = self.getUsersContainer()
270        if userscontainer is None:
271            return
272        # XXX: complain if name already exists...
273        userscontainer.addAccount(account)
274
275    def addUser(self, name, password, title=None, description=None):
276        userscontainer = self.getUsersContainer()
277        if userscontainer is None:
278            return
279        userscontainer.addUser(name, password, title, description)
280
281    def getUsersContainer(self):
282        site = grok.getSite()
283        return site['users']
284
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
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
307@grok.subscribe(IUserAccount, grok.IObjectRemovedEvent)
308def handle_account_removed(account, event):
309    """When an account is removed, local and global roles might
310    have to be deleted.
311    """
312    local_roles = account.getLocalRoles()
313    principal = account.name
314
315    for role_id, object_list in local_roles.items():
316        for object in object_list:
317            try:
318                role_manager = IPrincipalRoleManager(object)
319            except TypeError:
320                # No Account object, no role manager, no local roles to remove
321                continue
322            role_manager.unsetRoleForPrincipal(role_id, principal)
323    role_manager = IPrincipalRoleManager(grok.getSite())
324    roles = account.getSiteRolesForPrincipal()
325    for role_id in roles:
326        role_manager.unsetRoleForPrincipal(role_id, principal)
327    return
328
329@grok.subscribe(IUserAccount, grok.IObjectAddedEvent)
330def handle_account_added(account, event):
331    """When an account is added, the local owner role and the global
332    AcademicsOfficer role must be set.
333    """
334    # We set the local Owner role
335    role_manager_account = IPrincipalRoleManager(account)
336    role_manager_account.assignRoleToPrincipal(
337        'waeup.local.Owner', account.name)
338    # We set the global AcademicsOfficer role
339    site = grok.getSite()
340    role_manager_site = IPrincipalRoleManager(site)
341    role_manager_site.assignRoleToPrincipal(
342        'waeup.AcademicsOfficer', account.name)
343    # Finally we have to notify the user account that the local role
344    # of the same object has changed
345    notify(LocalRoleSetEvent(
346        account, 'waeup.local.Owner', account.name, granted=True))
347    return
348
349@grok.subscribe(Interface, ILocalRoleSetEvent)
350def handle_local_role_changed(obj, event):
351    site = grok.getSite()
352    if site is None:
353        return
354    users = site.get('users', None)
355    if users is None:
356        return
357    if event.principal_id not in users.keys():
358        return
359    user = users[event.principal_id]
360    user.notifyLocalRoleChanged(event.object, event.role_id, event.granted)
361    return
362
363@grok.subscribe(Interface, grok.IObjectRemovedEvent)
364def handle_local_roles_on_obj_removed(obj, event):
365    try:
366        role_map = IPrincipalRoleMap(obj)
367    except TypeError:
368        # no map, no roles to remove
369        return
370    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
371        notify(LocalRoleSetEvent(
372                obj, local_role, user_name, granted=False))
373    return
374
375class UserAccountFactory(grok.GlobalUtility):
376    """A factory for user accounts.
377
378    This factory is only needed for imports.
379    """
380    grok.implements(IFactory)
381    grok.name(u'waeup.UserAccount')
382    title = u"Create a user.",
383    description = u"This factory instantiates new user account instances."
384
385    def __call__(self, *args, **kw):
386        return Account(name=None, password='')
387
388    def getInterfaces(self):
389        return implementedBy(Account)
390
391class UserProcessor(BatchProcessor):
392    """A batch processor for IUserAccount objects.
393    """
394    grok.implements(IBatchProcessor)
395    grok.provides(IBatchProcessor)
396    grok.context(Interface)
397    util_name = 'userprocessor'
398    grok.name(util_name)
399
400    name = u'User Processor'
401    iface = IUserAccount
402
403    location_fields = ['name',]
404    factory_name = 'waeup.UserAccount'
405
406    mode = None
407
408    def parentsExist(self, row, site):
409        return 'users' in site.keys()
410
411    def entryExists(self, row, site):
412        return row['name'] in site['users'].keys()
413
414    def getParent(self, row, site):
415        return site['users']
416
417    def getEntry(self, row, site):
418        if not self.entryExists(row, site):
419            return None
420        parent = self.getParent(row, site)
421        return parent.get(row['name'])
422
423    def addEntry(self, obj, row, site):
424        parent = self.getParent(row, site)
425        parent.addAccount(obj)
426        return
427
428    def updateEntry(self, obj, row, site):
429        """Update obj to the values given in row.
430        """
431        changed = []
432        for key, value in row.items():
433            if  key == 'roles':
434                # We cannot simply set the roles attribute here because
435                # we can't assure that the name attribute is set before
436                # the roles attribute is set.
437                continue
438            # Skip fields to be ignored.
439            if value == IGNORE_MARKER:
440                continue
441            if not hasattr(obj, key):
442                continue
443            setattr(obj, key, value)
444            changed.append('%s=%s' % (key, value))
445        roles = row.get('roles', IGNORE_MARKER)
446        if roles not in ('', IGNORE_MARKER):
447            evalvalue = eval(roles)
448            if isinstance(evalvalue, list):
449                setattr(obj, 'roles', evalvalue)
450                changed.append('roles=%s' % roles)
451        # Log actions...
452        items_changed = ', '.join(changed)
453        grok.getSite().logger.info('%s - %s - updated: %s'
454            % (self.name, row['name'], items_changed))
455        return
456
457class UsersPlugin(grok.GlobalUtility):
458    """A plugin that updates users.
459    """
460
461    grok.implements(IKofaPluggable)
462    grok.name('users')
463
464    deprecated_attributes = []
465
466    def setup(self, site, name, logger):
467        return
468
469    def update(self, site, name, logger):
470        users = site['users']
471        items = getFields(IUserAccount).items()
472        for user in users.values():
473            # Add new attributes
474            for i in items:
475                if not hasattr(user,i[0]):
476                    setattr(user,i[0],i[1].missing_value)
477                    logger.info(
478                        'UsersPlugin: %s attribute %s added.' % (
479                        user.name,i[0]))
480            # Remove deprecated attributes
481            for i in self.deprecated_attributes:
482                try:
483                    delattr(user,i)
484                    logger.info(
485                        'UsersPlugin: %s attribute %s deleted.' % (
486                        user.name,i))
487                except AttributeError:
488                    pass
489        return
Note: See TracBrowser for help on using the repository browser.