Mercurial > p > roundup > code
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 = [] |
