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

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

Add UserProcessor? for batch importing/processing of user accounts.

  • Property svn:keywords set to Id
File size: 15.0 KB
Line 
1## $Id: authentication.py 8973 2012-07-11 09:27:23Z 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)
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 roles might have to be deleted.
310    """
311    local_roles = account.getLocalRoles()
312    principal = account.name
313    for role_id, object_list in local_roles.items():
314        for object in object_list:
315            try:
316                role_manager = IPrincipalRoleManager(object)
317            except TypeError:
318                # No role manager, no roles to remove
319                continue
320            role_manager.unsetRoleForPrincipal(role_id, principal)
321    return
322
323@grok.subscribe(IUserAccount, grok.IObjectAddedEvent)
324def handle_account_added(account, event):
325    """When an account is added, the local owner role and the global
326    AcademicsOfficer role must be set.
327    """
328    # We set the local Owner role
329    role_manager_account = IPrincipalRoleManager(account)
330    role_manager_account.assignRoleToPrincipal(
331        'waeup.local.Owner', account.name)
332    # We set the global AcademicsOfficer role
333    site = grok.getSite()
334    role_manager_site = IPrincipalRoleManager(site)
335    role_manager_site.assignRoleToPrincipal(
336        'waeup.AcademicsOfficer', account.name)
337    # Finally we have to notify the user account that the local role
338    # of the same object has changed
339    notify(LocalRoleSetEvent(
340        account, 'waeup.local.Owner', account.name, granted=True))
341    return
342
343@grok.subscribe(Interface, ILocalRoleSetEvent)
344def handle_local_role_changed(obj, event):
345    site = grok.getSite()
346    if site is None:
347        return
348    users = site.get('users', None)
349    if users is None:
350        return
351    if event.principal_id not in users.keys():
352        return
353    user = users[event.principal_id]
354    user.notifyLocalRoleChanged(event.object, event.role_id, event.granted)
355    return
356
357@grok.subscribe(Interface, grok.IObjectRemovedEvent)
358def handle_local_roles_on_obj_removed(obj, event):
359    try:
360        role_map = IPrincipalRoleMap(obj)
361    except TypeError:
362        # no map, no roles to remove
363        return
364    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
365        notify(LocalRoleSetEvent(
366                obj, local_role, user_name, granted=False))
367    return
368
369class UserAccountFactory(grok.GlobalUtility):
370    """A factory for user accounts.
371
372    This factory is only needed for imports.
373    """
374    grok.implements(IFactory)
375    grok.name(u'waeup.UserAccount')
376    title = u"Create a user.",
377    description = u"This factory instantiates new user account instances."
378
379    def __call__(self, *args, **kw):
380        return Account(name=None, password='')
381
382    def getInterfaces(self):
383        return implementedBy(Account)
384
385class UserProcessor(BatchProcessor):
386    """A batch processor for IUserAccount objects.
387    """
388    grok.implements(IBatchProcessor)
389    grok.provides(IBatchProcessor)
390    grok.context(Interface)
391    util_name = 'userprocessor'
392    grok.name(util_name)
393
394    name = u'User Processor'
395    iface = IUserAccount
396
397    location_fields = ['name',]
398    factory_name = 'waeup.UserAccount'
399
400    mode = None
401
402    def parentsExist(self, row, site):
403        return 'users' in site.keys()
404
405    def entryExists(self, row, site):
406        return row['name'] in site['users'].keys()
407
408    def getParent(self, row, site):
409        return site['users']
410
411    def getEntry(self, row, site):
412        if not self.entryExists(row, site):
413            return None
414        parent = self.getParent(row, site)
415        return parent.get(row['name'])
416
417    def addEntry(self, obj, row, site):
418        parent = self.getParent(row, site)
419        parent.addAccount(obj)
420        return
421
422
423class UsersPlugin(grok.GlobalUtility):
424    """A plugin that updates users.
425    """
426
427    grok.implements(IKofaPluggable)
428    grok.name('users')
429
430    deprecated_attributes = []
431
432    def setup(self, site, name, logger):
433        return
434
435    def update(self, site, name, logger):
436        users = site['users']
437        items = getFields(IUserAccount).items()
438        for user in users.values():
439            # Add new attributes
440            for i in items:
441                if not hasattr(user,i[0]):
442                    setattr(user,i[0],i[1].missing_value)
443                    logger.info(
444                        'UsersPlugin: %s attribute %s added.' % (
445                        user.name,i[0]))
446            # Remove deprecated attributes
447            for i in self.deprecated_attributes:
448                try:
449                    delattr(user,i)
450                    logger.info(
451                        'UsersPlugin: %s attribute %s deleted.' % (
452                        user.name,i))
453                except AttributeError:
454                    pass
455        return
Note: See TracBrowser for help on using the repository browser.