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

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

Implement PasswordValidator? global utility as suggested by Uli.

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