source: main/waeup.sirp/trunk/src/waeup/sirp/students/authentication.py @ 7206

Last change on this file since 7206 was 7190, checked in by Henrik Bettermann, 13 years ago

Change copyright notice and use Id (checkin test for further adjustment)

  • Property svn:keywords set to Id
File size: 9.8 KB
Line 
1## $Id: authentication.py 7190 2011-11-25 07:10:06Z 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"""
19Authenticate students.
20"""
21import grok
22from zope.component import getUtility
23from zope.password.interfaces import IPasswordManager
24from zope.pluggableauth.interfaces import (
25    IAuthenticatorPlugin, ICredentialsPlugin)
26from zope.pluggableauth.plugins.session import (
27    SessionCredentialsPlugin, SessionCredentials)
28from zope.publisher.interfaces.http import IHTTPRequest
29from zope.session.interfaces import ISession
30from waeup.sirp.authentication import PrincipalInfo, get_principal_role_manager
31from waeup.sirp.interfaces import (
32    IAuthPluginUtility, IUserAccount, IPasswordValidator)
33from waeup.sirp.students.interfaces import IStudent
34
35class StudentAccount(grok.Adapter):
36    """An adapter to turn student objects into accounts on-the-fly.
37    """
38    grok.context(IStudent)
39    grok.implements(IUserAccount)
40
41    @property
42    def name(self):
43        return self.context.student_id
44
45    @property
46    def password(self):
47        return getattr(self.context, 'password', None)
48
49    @property
50    def title(self):
51        return self.context.fullname
52
53    @property
54    def description(self):
55        return self.title
56
57    def _get_roles(self):
58        prm = get_principal_role_manager()
59        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
60                 if x[0].startswith('waeup.')]
61        return roles
62
63    def _set_roles(self, roles):
64        """Set roles for principal denoted by this account.
65        """
66        prm = get_principal_role_manager()
67        old_roles = self.roles
68        for role in old_roles:
69            # Remove old roles, not to be set now...
70            if role.startswith('waeup.') and role not in roles:
71                prm.unsetRoleForPrincipal(role, self.name)
72        for role in roles:
73            prm.assignRoleToPrincipal(role, self.name)
74        return
75
76    roles = property(_get_roles, _set_roles)
77
78    def setPassword(self, password):
79        """Set a password (LDAP-compatible) SSHA encoded.
80
81        We do not store passwords in plaintext. Encrypted password is
82        stored as unicode string.
83        """
84        passwordmanager = getUtility(IPasswordManager, 'SSHA')
85        self.context.password = u'%s' % (
86            passwordmanager.encodePassword(password))
87
88    def checkPassword(self, password):
89        """Check whether the given `password` matches the one stored.
90        """
91        if not isinstance(password, basestring):
92            return False
93        if not getattr(self.context, 'password', None):
94            # unset/empty passwords do never match
95            return False
96        passwordmanager = getUtility(IPasswordManager, 'SSHA')
97        return passwordmanager.checkPassword(
98            self.context.password.encode('utf-8'), # turn unicode into bytes
99            password)
100
101class StudentsAuthenticatorPlugin(grok.GlobalUtility):
102    grok.implements(IAuthenticatorPlugin)
103    grok.provides(IAuthenticatorPlugin)
104    grok.name('students')
105
106    def authenticateCredentials(self, credentials):
107        """Authenticate `credentials`.
108
109        `credentials` is a tuple (login, password).
110
111        We look up students to find out whether a respective student
112        exists, then check the password and return the resulting
113        `PrincipalInfo` or ``None`` if no such student can be found.
114        """
115        if not isinstance(credentials, dict):
116            return None
117        if not ('login' in credentials and 'password' in credentials):
118            return None
119        account = self.getAccount(credentials['login'])
120
121        if account is None:
122            return None
123        if not account.checkPassword(credentials['password']):
124            return None
125        return PrincipalInfo(id=account.name,
126                             title=account.title,
127                             description=account.description)
128
129    def principalInfo(self, id):
130        """Get a principal identified by `id`.
131
132        This one is required by IAuthenticatorPlugin.
133        """
134        account = self.getAccount(id)
135        if account is None:
136            return None
137        return PrincipalInfo(id=account.name,
138                             title=account.title,
139                             description=account.description)
140
141    def getAccount(self, login):
142        """Look up a student identified by `login`. Returns an account.
143
144        Currently, we simply look up the key under which the student
145        is stored in the portal. That means we hit if login name and
146        name under which the student is stored match.
147
148        Returns not a student but an account object adapted from any
149        student found.
150
151        If no such student exists, ``None`` is returned.
152        """
153        site = grok.getSite()
154        if site is None:
155            return None
156        studentscontainer = site.get('students', None)
157        if studentscontainer is None:
158            return None
159        student = studentscontainer.get(login, None)
160        if student is None:
161            return None
162        return IUserAccount(student)
163
164class PasswordChangeCredentialsPlugin(grok.GlobalUtility,
165                                      SessionCredentialsPlugin):
166    """A session credentials plugin that handles the case of a user
167       changing his/her own password.
168
169    When users change their own password they might find themselves
170    logged out on next request.
171
172    To avoid this, we support to use a 'change password' page a bit
173    like a regular login page. That means, on each request we lookup
174    the sent data for a login field called 'student_id' and a
175    password.
176
177    If both exist, this means someone sent new credentials.
178
179    We then look for the old credentials stored in the user session.
180    If the new credentials' login (the student_id) matches the old
181    one's, we set the new credentials in session _but_ we return the
182    old credentials for the authentication plugins to check as for the
183    current request (and for the last time) the old credentials apply.
184
185    No valid credentials are returned by this plugin if one of the
186    follwing circumstances is true
187
188    - the sent request is not a regular IHTTPRequest
189
190    - the credentials to set do not match the old ones
191
192    - no credentials are sent with the request
193
194    - no credentials were set before (i.e. the user has no session
195      with credentials set before)
196
197    - no session exists already
198
199    - password and repeated password do not match
200
201    Therefore it is mandatory to put this plugin in the line of all
202    credentials plugins _before_ other plugins, so that the regular
203    credentials plugins can drop in as a 'fallback'.
204
205    This plugin was designed for students to change their passwords,
206    but might be used to allow password resets for other types of
207    accounts as well.
208    """
209    grok.provides(ICredentialsPlugin)
210    grok.name('student_pw_change')
211
212    loginpagename = 'login'
213    loginfield = 'student_id'
214    passwordfield = 'change_password'
215    repeatfield = 'change_password_repeat'
216
217    def extractCredentials(self, request):
218        if not IHTTPRequest.providedBy(request):
219            return None
220        login = request.get(self.loginfield, None)
221        password = request.get(self.passwordfield, None)
222        password_repeat = request.get(self.repeatfield, None)
223
224        if not login or not password:
225            return None
226
227        validator = getUtility(IPasswordValidator)
228        errors = validator.validate_password(password, password_repeat)
229        if errors:
230            return None
231
232        session = ISession(request)
233        sessionData = session.get(
234            'zope.pluggableauth.browserplugins')
235        if not sessionData:
236            return None
237
238        old_credentials = sessionData.get('credentials', None)
239        if old_credentials is None:
240            # Password changes for already authenticated users only!
241            return None
242        if old_credentials.getLogin() != login:
243            # Special treatment only for users that change their own pw.
244            return None
245        old_credentials = {
246            'login': old_credentials.getLogin(),
247            'password': old_credentials.getPassword()}
248
249        # Set new credentials in session. These will be active on next request
250        new_credentials = SessionCredentials(login, password)
251        sessionData['credentials'] = new_credentials
252
253        # Return old credentials for this one request only
254        return old_credentials
255
256class StudentsAuthenticatorSetup(grok.GlobalUtility):
257    """Register or unregister student authentication for a PAU.
258
259    This piece is called when a new site is created.
260    """
261    grok.implements(IAuthPluginUtility)
262
263    def register(self, pau):
264        plugins = list(pau.credentialsPlugins)
265        # this plugin must come before the regular credentials plugins
266        plugins.insert(0, 'student_pw_change')
267        pau.credentialsPlugins = tuple(plugins)
268        plugins = list(pau.authenticatorPlugins)
269        plugins.append('students')
270        pau.authenticatorPlugins = tuple(plugins)
271        return pau
272
273    def unregister(self, pau):
274        plugins = [x for x in pau.authenticatorPlugins
275                   if x != 'students']
276        pau.authenticatorPlugins = tuple(plugins)
277        return pau
Note: See TracBrowser for help on using the repository browser.