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

Last change on this file since 8615 was 8350, checked in by uli, 13 years ago

Remove unicodification of passwords.

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