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

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

KOFA -> Kofa

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