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

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

Dedicated officers should be able to login as student with a temporary password set by the system. This is the first part of its implementation.

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