source: main/waeup.kofa/trunk/src/waeup/kofa/tests/test_authentication.py @ 16032

Last change on this file since 16032 was 15287, checked in by Henrik Bettermann, 6 years ago

Stored insecure passwords are no longer accepted.
Officers with an insecure password can't login and are
redirected to the ChangePasswordRequestPage to request a
new password.

  • Property svn:keywords set to Id
File size: 15.6 KB
Line 
1## $Id: test_authentication.py 15287 2019-01-09 21:17:08Z henrik $
2##
3## Copyright (C) 2011 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##
18import grok
19import logging
20import time
21import unittest
22from cStringIO import StringIO
23from zope.component import getGlobalSiteManager, queryUtility
24from zope.component.hooks import setSite, clearSite
25from zope.interface.verify import verifyClass, verifyObject
26from zope.password.testing import setUpPasswordManagers
27from zope.pluggableauth import PluggableAuthentication
28from zope.pluggableauth.interfaces import (
29    IAuthenticatorPlugin, ICredentialsPlugin)
30from zope.publisher.browser import TestRequest
31from zope.securitypolicy.interfaces import IPrincipalRoleManager
32from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
33from waeup.kofa.app import University
34from waeup.kofa.authentication import (
35    UserAuthenticatorPlugin, Account, KofaPrincipalInfo, FailedLoginInfo,
36    get_principal_role_manager, UsersPlugin, KofaXMLRPCCredentialsPlugin,
37    setup_authentication, UpdatePAUPlugin)
38from waeup.kofa.interfaces import (
39    IAuthPluginUtility, IUserAccount, IFailedLoginInfo, IKofaPrincipalInfo,
40    IKofaPluggable)
41
42SECRET = 'HgtuZZZ8'
43
44class FakeSite(grok.Site, grok.Container):
45    #def getSiteManager(self):
46    #    return None
47    #    return getGlobalSiteManager()
48    pass
49
50
51class FakeAuthPlugin(object):
52    def register(self, pau):
53        pau.credentialsPlugins += ('foo', )
54
55
56class Test_setup_authentication(FunctionalTestCase):
57    # Tests for the `setup_authentication` function
58
59    layer = FunctionalLayer
60
61    def tearDown(self):
62        # clean up registry.
63        gsm = getGlobalSiteManager()
64        for iface, name in ((IAuthPluginUtility, 'myauth'), ):
65            to_delete = queryUtility(iface, name=name)
66            if to_delete is not None:
67                gsm.unregisterUtility(provided=iface, name=name)
68        super(Test_setup_authentication, self).tearDown()
69
70    def test_plugins_are_registered(self):
71        # We can populate a PAU with (hardcoded set of) plugins
72        pau = PluggableAuthentication()
73        setup_authentication(pau)
74        for name in (
75                'No Challenge if Authenticated',
76                'xmlrpc-credentials',
77                'credentials'):
78            assert name in pau.credentialsPlugins
79        for name in ('users', ):
80            assert name in pau.authenticatorPlugins
81
82    def test_external_plugins_are_registered(self):
83        # registered plugins are called as well
84        gsm = getGlobalSiteManager()
85        gsm.registerUtility(
86            FakeAuthPlugin(), IAuthPluginUtility, name='myauth')
87        pau = PluggableAuthentication()
88        setup_authentication(pau)
89        assert 'foo' in pau.credentialsPlugins
90
91
92class KofaXMLRPCCredentialsPluginTests(FunctionalTestCase):
93    # Test for XMLRPC credentials plugin
94
95    layer = FunctionalLayer
96
97    def test_ifaces(self):
98        # we meet interface requirements
99        plugin = KofaXMLRPCCredentialsPlugin()
100        self.assertTrue(
101            verifyClass(ICredentialsPlugin, KofaXMLRPCCredentialsPlugin))
102
103    def test_util_is_registered(self):
104        # we can query this named utility
105        util = queryUtility(ICredentialsPlugin, name='xmlrpc-credentials')
106        assert util is not None
107
108    def test_can_extract_creds(self):
109        # we can extract credentials from appropriate requests
110        req = TestRequest(
111            environ={'HTTP_AUTHORIZATION': u'Basic bWdyOm1ncnB3'})
112        plugin = KofaXMLRPCCredentialsPlugin()
113        assert plugin.extractCredentials(req) == {
114            'login': 'mgr', 'password': 'mgrpw'}
115
116    def test_challenge_disabled(self):
117        # we will not challenge people
118        plugin = KofaXMLRPCCredentialsPlugin()
119        assert plugin.challenge(TestRequest()) is False
120
121    def test_logout_disabled(self):
122        # we do not support logging out. HTTP basic auth cannot do this.
123        plugin = KofaXMLRPCCredentialsPlugin()
124        assert plugin.logout(TestRequest()) is False
125
126
127class UserAuthenticatorPluginTests(FunctionalTestCase):
128    # Must be functional because of various utility lookups and the like
129
130    layer = FunctionalLayer
131
132    def setUp(self):
133        super(UserAuthenticatorPluginTests, self).setUp()
134        self.getRootFolder()['app'] = FakeSite()
135        self.site = self.getRootFolder()['app']
136        self.site['users'] = {'bob': Account('bob', SECRET)}
137        setSite(self.site)
138        return
139
140    def tearDown(self):
141        super(UserAuthenticatorPluginTests, self).tearDown()
142        clearSite()
143        return
144
145    def test_ifaces(self):
146        # make sure, interfaces requirements are met
147        plugin = UserAuthenticatorPlugin()
148        plugin.__parent__ = None # This attribute is required by iface
149        self.assertTrue(
150            verifyClass(IAuthenticatorPlugin, UserAuthenticatorPlugin))
151        self.assertTrue(verifyObject(IAuthenticatorPlugin, plugin))
152        return
153
154    def test_authenticate_credentials(self):
155        # make sure authentication works as expected
156        plugin = UserAuthenticatorPlugin()
157        result1 = plugin.authenticateCredentials(
158            dict(login='bob', password=SECRET))
159        result2 = plugin.authenticateCredentials(
160            dict(login='bob', password='nonsense'))
161        self.assertTrue(isinstance(result1, KofaPrincipalInfo))
162        self.assertTrue(result2 is None)
163        return
164
165    def test_principal_info(self):
166        # make sure we can get a principal info
167        plugin = UserAuthenticatorPlugin()
168        result1 = plugin.principalInfo('bob')
169        result2 = plugin.principalInfo('manfred')
170        self.assertTrue(isinstance(result1, KofaPrincipalInfo))
171        self.assertTrue(result2 is None)
172        return
173
174    def test_get_principal_role_manager(self):
175        # make sure we get different role managers for different situations
176        prm1 = get_principal_role_manager()
177        clearSite(None)
178        prm2 = get_principal_role_manager()
179        self.assertTrue(IPrincipalRoleManager.providedBy(prm1))
180        self.assertTrue(IPrincipalRoleManager.providedBy(prm2))
181        self.assertTrue(prm1._context is self.site)
182        self.assertTrue(hasattr(prm2, '_context') is False)
183        return
184
185    def make_failed_logins(self, num):
186        # do `num` failed logins and a valid one afterwards
187        del self.site['users']
188        self.site['users'] = {'bob': Account('bob', SECRET)}
189        plugin = UserAuthenticatorPlugin()
190        resultlist = []
191        # reset accounts
192        for x in range(num):
193            resultlist.append(plugin.authenticateCredentials(
194                dict(login='bob', password='wrongsecret')))
195        resultlist.append(plugin.authenticateCredentials(
196            dict(login='bob', password=SECRET)))
197        return resultlist
198
199    def DISABLED_test_failed_logins(self):
200        # after three failed logins, an account is blocked
201        # XXX: this tests authenticator with time penalty (currently
202        # disabled)
203        results = []
204        succ_principal = KofaPrincipalInfo(
205            id='bob',
206            title='bob',
207            description=None,
208            email=None,
209            phone=None,
210            public_name=None,
211            user_type=u'user')
212        for x in range(4):
213            results.append(self.make_failed_logins(x))
214        self.assertEqual(results[2], [None, None, succ_principal])
215        # last login was blocked although correctly entered due to
216        # time penalty
217        self.assertEqual(results[3], [None, None, None, None])
218        return
219
220class KofaPrincipalInfoTests(unittest.TestCase):
221
222    def create_info(self):
223        return KofaPrincipalInfo(
224            id='bob',
225            title='bob',
226            description=None,
227            email=None,
228            phone=None,
229            public_name=None,
230            user_type=u'user')
231
232    def test_iface(self):
233        # make sure we implement the promised interfaces
234        info = self.create_info()
235        verifyClass(IKofaPrincipalInfo, KofaPrincipalInfo)
236        verifyObject(IKofaPrincipalInfo, info)
237        return
238
239    def test_equality(self):
240        # we can test two infos for equality
241        info1 = self.create_info()
242        info2 = self.create_info()
243        self.assertEqual(info1, info2)
244        self.assertTrue(info1 == info2)
245        info1.id = 'blah'
246        self.assertTrue(info1 != info2)
247        self.assertTrue((info1 == info2) is False)
248        info1.id = 'bob'
249        info2.id = 'blah'
250        self.assertTrue(info1 != info2)
251        self.assertTrue((info1 == info2) is False)
252        return
253
254class FailedLoginInfoTests(unittest.TestCase):
255
256    def test_iface(self):
257        # make sure we fullfill the promised interfaces
258        info1 = FailedLoginInfo()
259        info2 = FailedLoginInfo(num=1, last=time.time())
260        self.assertTrue(
261            verifyClass(IFailedLoginInfo, FailedLoginInfo))
262        self.assertTrue(verifyObject(IFailedLoginInfo, info1))
263        # make sure the stored values have correct type if not None
264        self.assertTrue(verifyObject(IFailedLoginInfo, info2))
265        return
266
267    def test_default_values(self):
268        # By default we get 0, None
269        info = FailedLoginInfo()
270        self.assertEqual(info.num, 0)
271        self.assertEqual(info.last, None)
272        return
273
274    def test_set_values_by_attribute(self):
275        # we can set values by attribute
276        ts = time.gmtime(0)
277        info = FailedLoginInfo()
278        info.num = 5
279        info.last = ts
280        self.assertEqual(info.num, 5)
281        self.assertEqual(info.last, ts)
282        return
283
284    def test_set_values_by_constructor(self):
285        # we can set values by constructor args
286        ts = time.gmtime(0)
287        info = FailedLoginInfo(5, ts)
288        self.assertEqual(info.num, 5)
289        self.assertEqual(info.last, ts)
290        return
291
292    def test_set_values_by_keywords(self):
293        # we can set values by constructor keywords
294        ts = time.gmtime(0)
295        info = FailedLoginInfo(last=ts, num=3)
296        self.assertEqual(info.num, 3)
297        self.assertEqual(info.last, ts)
298        return
299
300    def test_as_tuple(self):
301        # we can get the info values as tuple
302        ts = time.gmtime(0)
303        info = FailedLoginInfo(last=ts, num=3)
304        self.assertEqual(info.as_tuple(), (3, ts))
305        return
306
307    def test_set_values(self):
308        # we can set the values of a an info instance
309        ts = time.time()
310        info = FailedLoginInfo()
311        info.set_values(num=3, last=ts)
312        self.assertEqual(info.num, 3)
313        self.assertEqual(info.last, ts)
314        return
315
316    def test_increase(self):
317        # we can increase the number of failed logins
318        ts1 = time.time()
319        info = FailedLoginInfo()
320        info.increase()
321        self.assertEqual(info.num, 1)
322        self.assertTrue(info.last > ts1)
323        ts2 = info.last
324        info.increase()
325        self.assertEqual(info.num, 2)
326        self.assertTrue(info.last > ts2)
327        return
328
329    def test_reset(self):
330        # we can reset failed login infos.
331        info = FailedLoginInfo()
332        info.increase()
333        info.reset()
334        self.assertEqual(info.num, 0)
335        self.assertEqual(info.last, None)
336        return
337
338class AccountTests(unittest.TestCase):
339
340    def setUp(self):
341        setUpPasswordManagers()
342        return
343
344    def test_iface(self):
345        acct = Account('bob', 'mypasswd')
346        self.assertTrue(
347            verifyClass(IUserAccount, Account))
348        self.assertTrue(
349            verifyObject(IUserAccount, acct))
350        return
351
352    def test_failed_logins(self):
353        # we can retrieve infos about failed logins
354        ts = time.time()
355        acct = Account('bob', 'mypasswd')
356        self.assertTrue(hasattr(acct, 'failed_logins'))
357        acct.failed_logins.set_values(num=3, last=ts)
358        self.assertEqual(acct.failed_logins.last, ts)
359        self.assertEqual(acct.failed_logins.num, 3)
360        return
361
362    def test_failed_logins_per_inst(self):
363        # we get a different counter for each Account instance
364        acct1 = Account('bob', 'secret')
365        acct2 = Account('alice', 'alsosecret')
366        self.assertTrue(acct1.failed_logins is not acct2.failed_logins)
367        return
368
369class FakeUserAccount(object):
370    pass
371
372
373def get_logger():
374    logger = logging.getLogger('waeup.test')
375    stream = StringIO()
376    handler = logging.StreamHandler(stream)
377    logger.setLevel(logging.DEBUG)
378    logger.propagate = False
379    logger.addHandler(handler)
380    return logger, stream
381
382
383class UsersPluginTests(unittest.TestCase):
384
385    def setUp(self):
386        setUpPasswordManagers()
387        self.site = FakeSite()
388        self.site['users'] = grok.Container()
389        return
390
391    def test_ifaces(self):
392        # make sure we implement the promised interfaces
393        plugin = UsersPlugin()
394        verifyClass(IKofaPluggable, UsersPlugin)
395        verifyObject(IKofaPluggable, plugin)
396        return
397
398    def test_update(self):
399        # make sure user accounts are updated properly.
400        plugin = UsersPlugin()
401        logger, stream = get_logger()
402        plugin.update(self.site, 'app', logger)
403        stream.seek(0)
404        self.assertEqual(stream.read(), '')
405        self.site['users']['bob'] = FakeUserAccount()
406        logger, stream = get_logger()
407        plugin.update(self.site, 'app', logger)
408        stream.seek(0)
409        log_content = stream.read()
410        self.assertTrue(hasattr(self.site['users']['bob'], 'description'))
411        self.assertTrue(hasattr(self.site['users']['bob'], 'failed_logins'))
412        self.assertTrue(
413            isinstance(self.site['users']['bob'].failed_logins,
414                       FailedLoginInfo))
415        self.assertTrue('attribute description added' in log_content)
416        self.assertTrue('attribute failed_logins added' in log_content)
417        return
418
419
420class TestUpdatePAUPlugin(FunctionalTestCase):
421
422    layer = FunctionalLayer
423
424    def setUp(self):
425        super(TestUpdatePAUPlugin, self).setUp()
426        self.getRootFolder()['app'] = University()
427        self.site = self.getRootFolder()['app']
428
429    def tearDown(self):
430        clearSite()
431        super(TestUpdatePAUPlugin, self).tearDown()
432
433    def get_pau(self):
434        # the PAU is registered as a local utility in local site manager.
435        # the name is derived from class name.
436        pau = self.site.getSiteManager()['PluggableAuthentication']
437        assert pau is not None
438        return pau
439
440    def test_update_outdated(self):
441        # we can update outdated sites.
442        plugin = UpdatePAUPlugin()
443        logger, stream = get_logger()
444        pau = self.get_pau()
445        pau.credentialsPlugins = ('foo', 'credentials', 'bar')
446        plugin.update(self.site, 'xmlrpc-credentials', logger)
447        assert 'xmlrpc-credentials' in pau.credentialsPlugins
448        assert pau.credentialsPlugins.index('xmlrpc-credentials') == 1
449
450    def test_update_uptodate(self):
451        # we cope with already updated sites.
452        plugin = UpdatePAUPlugin()
453        logger, stream = get_logger()
454        pau = self.get_pau()
455        pau.credentialsPlugins = ('foo', 'xmlrpc-credentials', 'bar')
456        plugin.update(self.site, 'xmlrpc-credentials', logger)
457        assert pau.credentialsPlugins.count('xmlrpc-credentials') == 1
Note: See TracBrowser for help on using the repository browser.