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

Last change on this file since 17929 was 17269, checked in by Henrik Bettermann, 21 months ago

My Macbook is too fast.

  • Property svn:keywords set to Id
File size: 15.7 KB
RevLine 
[7193]1## $Id: test_authentication.py 17269 2023-01-11 08:27:19Z 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)
[17269]322        # on fast machines the timestamp may not have increased
323        self.assertTrue(info.last >= ts1)
[10055]324        ts2 = info.last
325        info.increase()
326        self.assertEqual(info.num, 2)
[17269]327        self.assertTrue(info.last >= ts2)
[10055]328        return
329
330    def test_reset(self):
331        # we can reset failed login infos.
332        info = FailedLoginInfo()
333        info.increase()
334        info.reset()
335        self.assertEqual(info.num, 0)
336        self.assertEqual(info.last, None)
337        return
338
339class AccountTests(unittest.TestCase):
340
341    def setUp(self):
342        setUpPasswordManagers()
343        return
344
345    def test_iface(self):
346        acct = Account('bob', 'mypasswd')
347        self.assertTrue(
348            verifyClass(IUserAccount, Account))
349        self.assertTrue(
350            verifyObject(IUserAccount, acct))
351        return
352
353    def test_failed_logins(self):
354        # we can retrieve infos about failed logins
355        ts = time.time()
356        acct = Account('bob', 'mypasswd')
357        self.assertTrue(hasattr(acct, 'failed_logins'))
358        acct.failed_logins.set_values(num=3, last=ts)
359        self.assertEqual(acct.failed_logins.last, ts)
360        self.assertEqual(acct.failed_logins.num, 3)
361        return
362
363    def test_failed_logins_per_inst(self):
364        # we get a different counter for each Account instance
365        acct1 = Account('bob', 'secret')
366        acct2 = Account('alice', 'alsosecret')
367        self.assertTrue(acct1.failed_logins is not acct2.failed_logins)
368        return
369
370class FakeUserAccount(object):
371    pass
372
[14670]373
374def get_logger():
375    logger = logging.getLogger('waeup.test')
376    stream = StringIO()
377    handler = logging.StreamHandler(stream)
378    logger.setLevel(logging.DEBUG)
379    logger.propagate = False
380    logger.addHandler(handler)
381    return logger, stream
382
383
[10055]384class UsersPluginTests(unittest.TestCase):
385
386    def setUp(self):
387        setUpPasswordManagers()
388        self.site = FakeSite()
389        self.site['users'] = grok.Container()
390        return
391
392    def test_ifaces(self):
393        # make sure we implement the promised interfaces
394        plugin = UsersPlugin()
395        verifyClass(IKofaPluggable, UsersPlugin)
396        verifyObject(IKofaPluggable, plugin)
397        return
398
399    def test_update(self):
400        # make sure user accounts are updated properly.
401        plugin = UsersPlugin()
[14670]402        logger, stream = get_logger()
[10055]403        plugin.update(self.site, 'app', logger)
404        stream.seek(0)
405        self.assertEqual(stream.read(), '')
406        self.site['users']['bob'] = FakeUserAccount()
[14670]407        logger, stream = get_logger()
[10055]408        plugin.update(self.site, 'app', logger)
409        stream.seek(0)
410        log_content = stream.read()
411        self.assertTrue(hasattr(self.site['users']['bob'], 'description'))
412        self.assertTrue(hasattr(self.site['users']['bob'], 'failed_logins'))
413        self.assertTrue(
414            isinstance(self.site['users']['bob'].failed_logins,
415                       FailedLoginInfo))
416        self.assertTrue('attribute description added' in log_content)
417        self.assertTrue('attribute failed_logins added' in log_content)
418        return
[14670]419
420
421class TestUpdatePAUPlugin(FunctionalTestCase):
422
423    layer = FunctionalLayer
424
425    def setUp(self):
426        super(TestUpdatePAUPlugin, self).setUp()
427        self.getRootFolder()['app'] = University()
428        self.site = self.getRootFolder()['app']
429
430    def tearDown(self):
431        clearSite()
432        super(TestUpdatePAUPlugin, self).tearDown()
433
434    def get_pau(self):
435        # the PAU is registered as a local utility in local site manager.
436        # the name is derived from class name.
437        pau = self.site.getSiteManager()['PluggableAuthentication']
438        assert pau is not None
439        return pau
440
441    def test_update_outdated(self):
442        # we can update outdated sites.
443        plugin = UpdatePAUPlugin()
444        logger, stream = get_logger()
445        pau = self.get_pau()
446        pau.credentialsPlugins = ('foo', 'credentials', 'bar')
447        plugin.update(self.site, 'xmlrpc-credentials', logger)
448        assert 'xmlrpc-credentials' in pau.credentialsPlugins
449        assert pau.credentialsPlugins.index('xmlrpc-credentials') == 1
450
451    def test_update_uptodate(self):
452        # we cope with already updated sites.
453        plugin = UpdatePAUPlugin()
454        logger, stream = get_logger()
455        pau = self.get_pau()
456        pau.credentialsPlugins = ('foo', 'xmlrpc-credentials', 'bar')
457        plugin.update(self.site, 'xmlrpc-credentials', logger)
458        assert pau.credentialsPlugins.count('xmlrpc-credentials') == 1
Note: See TracBrowser for help on using the repository browser.