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

Last change on this file since 14667 was 14667, checked in by uli, 8 years ago

Add XMLRPC-aware auth credentials plugin.

This new plugin can be used to authenticate users in a site
(i.e. normally officers of a University instance) with
regular HTTP basic auth credentials (normally we expect a
web form, where credentials are sent as form-vars).

This plugin is registered in _new_ University instances
automatically, but it is _not_ registered with already
existing PAUs.

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