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

Last change on this file since 17574 was 15606, checked in by Henrik Bettermann, 5 years ago

Parents access implementation (part 1)

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