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

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

Add boolean field 'suspended' to IStudent and IApplicant and extend authentication (checkPassword) slightly. Test will follow

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