source: main/waeup.sirp/trunk/src/waeup/sirp/students/authentication.py @ 7139

Last change on this file since 7139 was 7139, checked in by uli, 14 years ago

Make sure unset/empty student password are never accepted by
authenticatin machinery.

  • Property svn:keywords set to Id
File size: 9.8 KB
RevLine 
[6669]1##
2## authentication.py
3## Login : <uli@pu.smp.net>
4## Started on  Fri Sep  2 15:22:47 2011 Uli Fouquet
5## $Id: authentication.py 7139 2011-11-19 13:08:45Z uli $
6##
7## Copyright (C) 2011 Uli Fouquet
8## This program is free software; you can redistribute it and/or modify
9## it under the terms of the GNU General Public License as published by
10## the Free Software Foundation; either version 2 of the License, or
11## (at your option) any later version.
12##
13## This program is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16## GNU General Public License for more details.
17##
18## You should have received a copy of the GNU General Public License
19## along with this program; if not, write to the Free Software
20## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21##
22"""
23Authenticate students.
24"""
25import grok
26from zope.component import getUtility
27from zope.password.interfaces import IPasswordManager
[6756]28from zope.pluggableauth.interfaces import (
29    IAuthenticatorPlugin, ICredentialsPlugin)
30from zope.pluggableauth.plugins.session import (
31    SessionCredentialsPlugin, SessionCredentials)
32from zope.publisher.interfaces.http import IHTTPRequest
33from zope.session.interfaces import ISession
[6680]34from waeup.sirp.authentication import PrincipalInfo, get_principal_role_manager
[6669]35from waeup.sirp.interfaces import IAuthPluginUtility, IUserAccount
36from waeup.sirp.students.interfaces import IStudent
37
38class StudentAccount(grok.Adapter):
39    """An adapter to turn student objects into accounts on-the-fly.
40    """
41    grok.context(IStudent)
42    grok.implements(IUserAccount)
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):
[6818]54        return self.context.fullname
[6669]55
56    @property
57    def description(self):
58        return self.title
59
[6680]60    def _get_roles(self):
61        prm = get_principal_role_manager()
62        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
63                 if x[0].startswith('waeup.')]
64        return roles
[6669]65
[6680]66    def _set_roles(self, roles):
67        """Set roles for principal denoted by this account.
68        """
69        prm = get_principal_role_manager()
70        old_roles = self.roles
71        for role in old_roles:
72            # Remove old roles, not to be set now...
73            if role.startswith('waeup.') and role not in roles:
74                prm.unsetRoleForPrincipal(role, self.name)
75        for role in roles:
76            prm.assignRoleToPrincipal(role, self.name)
77        return
78
79    roles = property(_get_roles, _set_roles)
80
[6669]81    def setPassword(self, password):
82        """Set a password (LDAP-compatible) SSHA encoded.
83
[6680]84        We do not store passwords in plaintext. Encrypted password is
85        stored as unicode string.
[6669]86        """
87        passwordmanager = getUtility(IPasswordManager, 'SSHA')
[6680]88        self.context.password = u'%s' % (
89            passwordmanager.encodePassword(password))
[6669]90
91    def checkPassword(self, password):
92        """Check whether the given `password` matches the one stored.
93        """
94        if not isinstance(password, basestring):
95            return False
[7139]96        if not getattr(self.context, 'password', None):
97            # unset/empty passwords do never match
98            return False
[6669]99        passwordmanager = getUtility(IPasswordManager, 'SSHA')
[6680]100        return passwordmanager.checkPassword(
101            self.context.password.encode('utf-8'), # turn unicode into bytes
102            password)
[6669]103
104class StudentsAuthenticatorPlugin(grok.GlobalUtility):
105    grok.implements(IAuthenticatorPlugin)
106    grok.provides(IAuthenticatorPlugin)
107    grok.name('students')
108
109    def authenticateCredentials(self, credentials):
110        """Authenticate `credentials`.
111
112        `credentials` is a tuple (login, password).
113
114        We look up students to find out whether a respective student
115        exists, then check the password and return the resulting
116        `PrincipalInfo` or ``None`` if no such student can be found.
117        """
118        if not isinstance(credentials, dict):
119            return None
120        if not ('login' in credentials and 'password' in credentials):
121            return None
122        account = self.getAccount(credentials['login'])
123
124        if account is None:
125            return None
126        if not account.checkPassword(credentials['password']):
127            return None
128        return PrincipalInfo(id=account.name,
129                             title=account.title,
130                             description=account.description)
131
132    def principalInfo(self, id):
133        """Get a principal identified by `id`.
134
135        This one is required by IAuthenticatorPlugin.
136        """
137        account = self.getAccount(id)
138        if account is None:
139            return None
140        return PrincipalInfo(id=account.name,
141                             title=account.title,
142                             description=account.description)
143
144    def getAccount(self, login):
145        """Look up a student identified by `login`. Returns an account.
146
147        Currently, we simply look up the key under which the student
148        is stored in the portal. That means we hit if login name and
149        name under which the student is stored match.
150
151        Returns not a student but an account object adapted from any
152        student found.
153
154        If no such student exists, ``None`` is returned.
155        """
156        site = grok.getSite()
157        if site is None:
158            return None
159        studentscontainer = site.get('students', None)
160        if studentscontainer is None:
161            return None
162        student = studentscontainer.get(login, None)
163        if student is None:
164            return None
165        return IUserAccount(student)
166
[6756]167class PasswordChangeCredentialsPlugin(grok.GlobalUtility,
168                                      SessionCredentialsPlugin):
169    """A session credentials plugin that handles the case of a user
170       changing his/her own password.
171
172    When users change their own password they might find themselves
173    logged out on next request.
174
175    To avoid this, we support to use a 'change password' page a bit
176    like a regular login page. That means, on each request we lookup
177    the sent data for a login field called 'student_id' and a
178    password.
179
180    If both exist, this means someone sent new credentials.
181
182    We then look for the old credentials stored in the user session.
183    If the new credentials' login (the student_id) matches the old
184    one's, we set the new credentials in session _but_ we return the
185    old credentials for the authentication plugins to check as for the
186    current request (and for the last time) the old credentials apply.
187
188    No valid credentials are returned by this plugin if one of the
189    follwing circumstances is true
190
191    - the sent request is not a regular IHTTPRequest
192
193    - the credentials to set do not match the old ones
194
195    - no credentials are sent with the request
196
197    - no credentials were set before (i.e. the user has no session
198      with credentials set before)
199
[6768]200    - no session exists already
201
202    - password and repeated password do not match
203
[6756]204    Therefore it is mandatory to put this plugin in the line of all
205    credentials plugins _before_ other plugins, so that the regular
206    credentials plugins can drop in as a 'fallback'.
207
208    This plugin was designed for students to change their passwords,
209    but might be used to allow password resets for other types of
210    accounts as well.
211    """
212    grok.provides(ICredentialsPlugin)
213    grok.name('student_pw_change')
214
215    loginpagename = 'login'
216    loginfield = 'student_id'
217    passwordfield = 'form.password'
[6768]218    repeatfield = 'form.password_repeat'
[6756]219
220    def extractCredentials(self, request):
221        if not IHTTPRequest.providedBy(request):
222            return None
223        login = request.get(self.loginfield, None)
224        password = request.get(self.passwordfield, None)
[6768]225        password_repeat = request.get(self.repeatfield, None)
226
[6756]227        if not login or not password:
228            return None
[6768]229
230        if password != password_repeat:
231            # At least protect against erraneous password input
232            return None
233
[6756]234        session = ISession(request)
235        sessionData = session.get(
236            'zope.pluggableauth.browserplugins')
[6768]237        if not sessionData:
238            return None
239
[6756]240        old_credentials = sessionData.get('credentials', None)
241        if old_credentials is None:
242            # Password changes for already authenticated users only!
243            return None
244        if old_credentials.getLogin() != login:
245            # Special treatment only for users that change their own pw.
246            return None
247        old_credentials = {
248            'login': old_credentials.getLogin(),
249            'password': old_credentials.getPassword()}
250
251        # Set new credentials in session. These will be active on next request
252        new_credentials = SessionCredentials(login, password)
253        sessionData['credentials'] = new_credentials
254
255        # Return old credentials for this one request only
256        return old_credentials
257
[6669]258class StudentsAuthenticatorSetup(grok.GlobalUtility):
259    """Register or unregister student authentication for a PAU.
260
261    This piece is called when a new site is created.
262    """
263    grok.implements(IAuthPluginUtility)
264
265    def register(self, pau):
[6756]266        plugins = list(pau.credentialsPlugins)
267        # this plugin must come before the regular credentials plugins
268        plugins.insert(0, 'student_pw_change')
269        pau.credentialsPlugins = tuple(plugins)
[6669]270        plugins = list(pau.authenticatorPlugins)
271        plugins.append('students')
272        pau.authenticatorPlugins = tuple(plugins)
273        return pau
274
275    def unregister(self, pau):
276        plugins = [x for x in pau.authenticatorPlugins
277                   if x != 'students']
278        pau.authenticatorPlugins = tuple(plugins)
279        return pau
Note: See TracBrowser for help on using the repository browser.