source: main/waeup.ikoba/trunk/src/waeup/ikoba/customers/authentication.py @ 14022

Last change on this file since 14022 was 13806, checked in by Henrik Bettermann, 9 years ago

Add portal maintenance mode.

See r13394, r13396, r13468.

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