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

Last change on this file since 17782 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
Line 
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
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        # on fast machines the timestamp may not have increased
323        self.assertTrue(info.last >= ts1)
324        ts2 = info.last
325        info.increase()
326        self.assertEqual(info.num, 2)
327        self.assertTrue(info.last >= ts2)
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
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
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()
402        logger, stream = get_logger()
403        plugin.update(self.site, 'app', logger)
404        stream.seek(0)
405        self.assertEqual(stream.read(), '')
406        self.site['users']['bob'] = FakeUserAccount()
407        logger, stream = get_logger()
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
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.