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

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

propset svn:keywords "Id"

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