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

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

Rebuild applicants package (1st part). Applicants now have an applicant_id and a password and can use the regular login page to enter the portal.

Add user_type attribute to SIRPPrincipal objects.

Add some permissions in students package.

Some tests are still missing and will be re-added soon.

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