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

Last change on this file since 16186 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
RevLine 
[7193]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
[7194]17##
18import grok
[10055]19import logging
20import time
[6615]21import unittest
[10055]22from cStringIO import StringIO
[14667]23from zope.component import getGlobalSiteManager, queryUtility
[6615]24from zope.component.hooks import setSite, clearSite
25from zope.interface.verify import verifyClass, verifyObject
[10055]26from zope.password.testing import setUpPasswordManagers
[14667]27from zope.pluggableauth import PluggableAuthentication
28from zope.pluggableauth.interfaces import (
29    IAuthenticatorPlugin, ICredentialsPlugin)
30from zope.publisher.browser import TestRequest
[6673]31from zope.securitypolicy.interfaces import IPrincipalRoleManager
[7811]32from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
[14670]33from waeup.kofa.app import University
[7811]34from waeup.kofa.authentication import (
[10055]35    UserAuthenticatorPlugin, Account, KofaPrincipalInfo, FailedLoginInfo,
[14667]36    get_principal_role_manager, UsersPlugin, KofaXMLRPCCredentialsPlugin,
[14670]37    setup_authentication, UpdatePAUPlugin)
[10055]38from waeup.kofa.interfaces import (
[14667]39    IAuthPluginUtility, IUserAccount, IFailedLoginInfo, IKofaPrincipalInfo,
40    IKofaPluggable)
[6615]41
[15287]42SECRET = 'HgtuZZZ8'
[14667]43
[6615]44class FakeSite(grok.Site, grok.Container):
[10055]45    #def getSiteManager(self):
46    #    return None
47    #    return getGlobalSiteManager()
[6615]48    pass
[6617]49
[14667]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(
[14668]86            FakeAuthPlugin(), IAuthPluginUtility, name='myauth')
[14667]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
[6615]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']
[15287]136        self.site['users'] = {'bob': Account('bob', SECRET)}
[6615]137        setSite(self.site)
138        return
139
140    def tearDown(self):
141        super(UserAuthenticatorPluginTests, self).tearDown()
[8427]142        clearSite()
[6615]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(
[15287]158            dict(login='bob', password=SECRET))
[6615]159        result2 = plugin.authenticateCredentials(
160            dict(login='bob', password='nonsense'))
[7819]161        self.assertTrue(isinstance(result1, KofaPrincipalInfo))
[6615]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')
[7819]170        self.assertTrue(isinstance(result1, KofaPrincipalInfo))
[6615]171        self.assertTrue(result2 is None)
172        return
[6673]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
[10055]184
185    def make_failed_logins(self, num):
186        # do `num` failed logins and a valid one afterwards
187        del self.site['users']
[15287]188        self.site['users'] = {'bob': Account('bob', SECRET)}
[10055]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(
[15287]196            dict(login='bob', password=SECRET)))
[10055]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
[14670]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
[10055]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()
[14670]401        logger, stream = get_logger()
[10055]402        plugin.update(self.site, 'app', logger)
403        stream.seek(0)
404        self.assertEqual(stream.read(), '')
405        self.site['users']['bob'] = FakeUserAccount()
[14670]406        logger, stream = get_logger()
[10055]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
[14670]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.