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

Last change on this file since 12768 was 12523, checked in by Henrik Bettermann, 10 years ago

Customers can use their email address for authentication.

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