comparison test/test_actions.py @ 5717:cad18de2b988

issue2550949: Rate limit password guesses/login attempts. Generic rate limit mechanism added. Deployed for web page logins. Default is 3 login attempts/minute for a user. After which one login attempt every 20 seconds can be done. Uses gcra algorithm so all I need to store is a username and timestamp in the one time key database. This does mean I don't have a list of all failed login attempts as part of the rate limiter. Set up config setting as well so admin can tune the rate. Maybe 1 every 10 seconds is ok at a site with poor typists who need 6 attempts to get the password right 8-). The gcra method can also be used to limit the rest and xmlrpc interfaces if needed. The mechanism I added also supplies a status method that calculates the expected values for http headers returned as part of rate limiting. Also tests added to test all code paths I hope.
author John Rouillard <rouilj@ieee.org>
date Sat, 11 May 2019 17:24:58 -0400
parents f8893e1cde0d
children 842252c3ee22
comparison
equal deleted inserted replaced
5716:42a713e36def 5717:cad18de2b988
5 from roundup import hyperdb 5 from roundup import hyperdb
6 from roundup.date import Date, Interval 6 from roundup.date import Date, Interval
7 from roundup.cgi.actions import * 7 from roundup.cgi.actions import *
8 from roundup.cgi.client import add_message 8 from roundup.cgi.client import add_message
9 from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError, FormError 9 from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError, FormError
10 from roundup.exceptions import Reject
10 11
11 from roundup.anypy.cmp_ import NoneAndDictComparable 12 from roundup.anypy.cmp_ import NoneAndDictComparable
13 from time import sleep
14 from datetime import datetime
12 15
13 from .mocknull import MockNull 16 from .mocknull import MockNull
14 17
15 def true(*args, **kwargs): 18 def true(*args, **kwargs):
16 return 1 19 return 1
17 20
18 class ActionTestCase(unittest.TestCase): 21 class ActionTestCase(unittest.TestCase):
19 def setUp(self): 22 def setUp(self):
20 self.form = FieldStorage(environ={'QUERY_STRING': ''}) 23 self.form = FieldStorage(environ={'QUERY_STRING': ''})
21 self.client = MockNull() 24 self.client = MockNull()
25 self.client.db.Otk = MockNull()
26 self.client.db.Otk.data = {}
27 self.client.db.Otk.getall = self.data_get
28 self.client.db.Otk.set = self.data_set
29 self.client.db.config = {'WEB_LOGIN_ATTEMPTS_MIN': 20}
22 self.client._ok_message = [] 30 self.client._ok_message = []
23 self.client._error_message = [] 31 self.client._error_message = []
24 self.client.add_error_message = lambda x : add_message( 32 self.client.add_error_message = lambda x : add_message(
25 self.client._error_message, x) 33 self.client._error_message, x)
26 self.client.add_ok_message = lambda x : add_message( 34 self.client.add_ok_message = lambda x : add_message(
28 self.client.form = self.form 36 self.client.form = self.form
29 self.client.base = "http://whoami.com/path/" 37 self.client.base = "http://whoami.com/path/"
30 class TemplatingUtils: 38 class TemplatingUtils:
31 pass 39 pass
32 self.client.instance.interfaces.TemplatingUtils = TemplatingUtils 40 self.client.instance.interfaces.TemplatingUtils = TemplatingUtils
41
42 def data_get(self, key):
43 return self.client.db.Otk.data[key]
44
45 def data_set(self, key, **value):
46 self.client.db.Otk.data[key] = value
33 47
34 class ShowActionTestCase(ActionTestCase): 48 class ShowActionTestCase(ActionTestCase):
35 def assertRaisesMessage(self, exception, callable, message, *args, 49 def assertRaisesMessage(self, exception, callable, message, *args,
36 **kwargs): 50 **kwargs):
37 """An extension of assertRaises, which also checks the exception 51 """An extension of assertRaises, which also checks the exception
354 368
355 # test when there is no query 369 # test when there is no query
356 self.form.value[:] = [] # clear out last test's setup values 370 self.form.value[:] = [] # clear out last test's setup values
357 self.assertLoginRaisesRedirect("http://whoami.com/path/issue255?%40error_message=Invalid+login", 371 self.assertLoginRaisesRedirect("http://whoami.com/path/issue255?%40error_message=Invalid+login",
358 'foo', 'wrong', "http://whoami.com/path/issue255") 372 'foo', 'wrong', "http://whoami.com/path/issue255")
373
374 def testLoginRateLimit(self):
375 ''' Set number of logins in setup to 20 per minute. Three second
376 delay between login attempts doesn't trip rate limit.
377 Default limit is 3/min, but that means we sleep for 20
378 seconds so I override the default limit to speed this up.
379 '''
380 # Do the first login setting an invalid login name
381 self.assertLoginLeavesMessages(['Invalid login'], 'nouser')
382 # use up the rest of the 20 login attempts
383 for i in range(19):
384 self.client._error_message = []
385 self.assertLoginLeavesMessages(['Invalid login'])
386
387 self.assertRaisesMessage(Reject, LoginAction(self.client).handle,
388 'Logins occurring too fast. Please wait: 3 seconds.')
389
390 sleep(3) # sleep as requested so we can do another login
391 self.client._error_message = []
392 self.assertLoginLeavesMessages(['Invalid login']) # this is expected
393
394 # and make sure we need to wait another three seconds
395 self.assertRaisesMessage(Reject, LoginAction(self.client).handle,
396 'Logins occurring too fast. Please wait: 3 seconds.')
359 397
360 class EditItemActionTestCase(ActionTestCase): 398 class EditItemActionTestCase(ActionTestCase):
361 def setUp(self): 399 def setUp(self):
362 ActionTestCase.setUp(self) 400 ActionTestCase.setUp(self)
363 self.result = [] 401 self.result = []

Roundup Issue Tracker: http://roundup-tracker.org/