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

Last change on this file since 12862 was 10055, checked in by uli, 11 years ago

Provide infrastructure to remember failed logins.

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