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

Last change on this file since 7234 was 7233, checked in by Henrik Bettermann, 13 years ago

Implement SIRPPrincipalInfo and SIRPPrincipal classes which provide ordinary principals with an extra email and phone attribute.

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