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

Last change on this file since 13264 was 12926, checked in by Henrik Bettermann, 10 years ago

Enable temporary suspension of officer accounts. Plugins must be updated after restart.

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