Skip to content

Commit bf7caee

Browse files
committed
Finish up login rate limit code. Set config item to 0 disables, make
sure config item can't be negative integer.
1 parent a96f367 commit bf7caee

File tree

4 files changed

+74
-28
lines changed

4 files changed

+74
-28
lines changed

roundup/cgi/actions.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,32 +1233,35 @@ def handle(self):
12331233
try:
12341234
# Implement rate limiting of logins by login name.
12351235
# Use prefix to prevent key collisions maybe??
1236-
rlkey="LOGIN-" + self.client.user
1237-
limit=self.loginLimit
1238-
gcra=Gcra()
1239-
otk=self.client.db.Otk
1240-
try:
1241-
val=otk.getall(rlkey)
1242-
gcra.set_tat_as_string(rlkey, val['tat'])
1243-
except KeyError:
1244-
# ignore if tat not set, it's 1970-1-1 by default.
1245-
pass
1246-
# see if rate limit exceeded and we need to reject the attempt
1247-
reject=gcra.update(rlkey, limit)
1248-
1249-
# Calculate a timestamp that will make OTK expire the
1250-
# unused entry 1 hour in the future
1251-
ts = time.time() - (60 * 60 * 24 * 7) + 3600
1252-
otk.set(rlkey, tat=gcra.get_tat_as_string(rlkey),
1253-
__timestamp=ts)
1254-
otk.commit()
1255-
1256-
if reject:
1257-
# User exceeded limits: find out how long to wait
1258-
status=gcra.status(rlkey, limit)
1259-
raise Reject(_("Logins occurring too fast. Please wait: %d seconds.")%status['Retry-After'])
1260-
else:
1261-
self.verifyLogin(self.client.user, password)
1236+
# set client.db.config.WEB_LOGIN_ATTEMPTS_MIN to 0
1237+
# to disable
1238+
if self.client.db.config.WEB_LOGIN_ATTEMPTS_MIN: # if 0 - off
1239+
rlkey="LOGIN-" + self.client.user
1240+
limit=self.loginLimit
1241+
gcra=Gcra()
1242+
otk=self.client.db.Otk
1243+
try:
1244+
val=otk.getall(rlkey)
1245+
gcra.set_tat_as_string(rlkey, val['tat'])
1246+
except KeyError:
1247+
# ignore if tat not set, it's 1970-1-1 by default.
1248+
pass
1249+
# see if rate limit exceeded and we need to reject the attempt
1250+
reject=gcra.update(rlkey, limit)
1251+
1252+
# Calculate a timestamp that will make OTK expire the
1253+
# unused entry 1 hour in the future
1254+
ts = time.time() - (60 * 60 * 24 * 7) + 3600
1255+
otk.set(rlkey, tat=gcra.get_tat_as_string(rlkey),
1256+
__timestamp=ts)
1257+
otk.commit()
1258+
1259+
if reject:
1260+
# User exceeded limits: find out how long to wait
1261+
status=gcra.status(rlkey, limit)
1262+
raise Reject(_("Logins occurring too fast. Please wait: %d seconds.")%status['Retry-After'])
1263+
1264+
self.verifyLogin(self.client.user, password)
12621265
except exceptions.LoginError as err:
12631266
self.client.make_user_anonymous()
12641267
for arg in err.args:

roundup/configuration.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,21 @@ def str2value(self, value):
474474
except ValueError:
475475
raise OptionValueError(self, value, "Integer number required")
476476

477+
class IntegerNumberGeqZeroOption(Option):
478+
479+
"""Integer numbers greater than or equal to zero."""
480+
481+
def str2value(self, value):
482+
try:
483+
v = int(value)
484+
if v < 0:
485+
raise OptionValueError(self, value,
486+
"Integer number greater than or equal to zero required")
487+
except OptionValueError:
488+
raise # pass through subclass
489+
except ValueError:
490+
raise OptionValueError(self, value, "Integer number required")
491+
477492
class OctalNumberOption(Option):
478493

479494
"""Octal Integer numbers"""
@@ -775,12 +790,13 @@ def str2value(self, value):
775790
"variables supplied by your web server (in that order).\n"
776791
"Set this option to 'no' if you do not wish to use HTTP Basic\n"
777792
"Authentication in your web interface."),
778-
(IntegerNumberOption, 'login_attempts_min', "3",
793+
(IntegerNumberGeqZeroOption, 'login_attempts_min', "3",
779794
"Limit login attempts per user per minute to this number.\n"
780795
"By default the 4th login attempt in a minute will notify\n"
781796
"the user that they need to wait 20 seconds before trying to\n"
782797
"log in again. This limits password guessing attacks and\n"
783-
"shouldn't need to be changed.\n"),
798+
"shouldn't need to be changed. Rate limiting on login can\n"
799+
"be disabled by setting the value to 0."),
784800
(SameSiteSettingOption, 'samesite_cookie_setting', "Lax",
785801
"""Set the mode of the SameSite cookie option for
786802
the session cookie. Choices are 'Lax' or

test/test_actions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,19 @@ def testLoginRateLimit(self):
395395
self.assertRaisesMessage(Reject, LoginAction(self.client).handle,
396396
'Logins occurring too fast. Please wait: 3 seconds.')
397397

398+
def testLoginRateLimitOff(self):
399+
''' Set number of logins to 0 per minute. Verify that
400+
we can do 1000 which for manual login might as well be off.
401+
'''
402+
403+
self.client.db.config.WEB_LOGIN_ATTEMPTS_MIN = 0
404+
405+
# Do the first login setting an invalid login name
406+
self.assertLoginLeavesMessages(['Invalid login'], 'nouser')
407+
for i in range(1000):
408+
self.client._error_message = []
409+
self.assertLoginLeavesMessages(['Invalid login'])
410+
398411
class EditItemActionTestCase(ActionTestCase):
399412
def setUp(self):
400413
ActionTestCase.setUp(self)

test/test_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,17 @@ def testTrackerWeb(self):
6868
self.assertRaises(configuration.OptionValueError,
6969
config._get_option('TRACKER_WEB').set, "htt://foo.example/bar")
7070

71+
def testLoginRateLimit(self):
72+
config = configuration.CoreConfig()
73+
74+
self.assertEqual(None,
75+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set("0"))
76+
self.assertEqual(None,
77+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set("200"))
78+
79+
self.assertRaises(configuration.OptionValueError,
80+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set, "fred")
81+
82+
self.assertRaises(configuration.OptionValueError,
83+
config._get_option('WEB_LOGIN_ATTEMPTS_MIN').set, "-1")
84+

0 commit comments

Comments
 (0)