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

Last change on this file since 13803 was 13803, checked in by Henrik Bettermann, 8 years ago

Enable temporary suspension of officer accounts. Plugins must be
updated after restart.

See r12926.

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