Added login rate limiting feature in Djblets auth and web api
Review Request #8698 — Created Jan. 31, 2017 and submitted
Developed rate-limit for failed login attempts through either the UI
or API. See review request 8768 to see it's implementation in Review
Board's Authentication form.
Create several unit tests for the login rate limiting feature that
test the rate limits as either custom or default values. They are
found in djblets/auth/tests/test_ratelimit.py and in
webapi/tests/test_api_auth_backend.py.
Description | From | Last Updated |
---|---|---|
Care to wrap the description & testing done at 72 characters? |
brennie | |
Col: 80 E501 line too long (97 > 79 characters) |
reviewbot | |
Col: 36 E261 at least two spaces before inline comment |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 4 W291 trailing whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 24 E231 missing whitespace after ',' |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 80 E501 line too long (85 > 79 characters) |
reviewbot | |
Col: 9 E116 unexpected indentation (comment) |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 80 E501 line too long (86 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 68 W292 no newline at end of file |
reviewbot | |
Col: 36 E261 at least two spaces before inline comment |
reviewbot | |
Col: 80 E501 line too long (97 > 79 characters) |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 4 W291 trailing whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 24 E231 missing whitespace after ',' |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 9 E116 unexpected indentation (comment) |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
By calling the variable prep_cache_key, you are shadowing the function prep_cache_key. |
brennie | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 68 W292 no newline at end of file |
reviewbot | |
Do we want to give the frequency or the minimum period between attempts? If you use minimum period you can … |
brennie | |
Col: 36 E261 at least two spaces before inline comment |
reviewbot | |
Col: 80 E501 line too long (97 > 79 characters) |
reviewbot | |
'cache_memoize' imported but unused |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 4 W291 trailing whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 61 W291 trailing whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 52 W291 trailing whitespace |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 61 W291 trailing whitespace |
reviewbot | |
Col: 60 W291 trailing whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
'Group' imported but unused |
reviewbot | |
'User' imported but unused |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 46 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 48 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 46 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 48 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 46 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 48 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Docstrings should use three double-quotes instead of single. The first line should also be joined with the quotes so the … |
david | |
We should list the full type here (django.http.HttpRequest). |
david | |
This should include the type: Returns: unicode: ... |
david | |
We should use six.string_type(...) (importing django.utils.six above). On Python 2, str is a bytestring, while on Python 3, it's a … |
david | |
Same here with the docstring. This should also be written in the imperative mood ("Return ..." instead of "Returns ...") |
david | |
Should be "unicode" instead of "String" |
david | |
Should list the return type here. |
david | |
Same comment with docstrings. |
david | |
Arg types should be unicode. This also needs a Returns. |
david | |
Please add a blank line between each arg. |
david | |
You don't need the u prefix here because you imported unicode_literals at the top. |
david | |
Same docstring comments here. |
david | |
Same docstring comments here. |
david | |
Some of our old test code uses camelCase, but new code should use underscores, such as test_invalid_key |
david | |
Same here. |
david | |
Same here. |
david | |
This can be self.assertFalse(result) |
david | |
This can be self.assertTrue(result) |
david | |
Col: 80 E501 line too long (95 > 79 characters) |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 9 E265 block comment should start with '# ' |
reviewbot | |
Col: 80 E501 line too long (84 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
Col: 64 W291 trailing whitespace |
reviewbot | |
Col: 80 E501 line too long (92 > 79 characters) |
reviewbot | |
Col: 44 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 46 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 9 E303 too many blank lines (2) |
reviewbot | |
Col: 44 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 46 E251 unexpected spaces around keyword / parameter equals |
reviewbot | |
Col: 80 E501 line too long (96 > 79 characters) |
reviewbot | |
'settings_local' imported but unused |
reviewbot | |
'from settings_local import *' used; unable to detect undefined names |
reviewbot | |
Col: 64 W291 trailing whitespace |
reviewbot | |
Col: 45 W291 trailing whitespace |
reviewbot | |
'os' imported but unused |
reviewbot | |
'settings_local' imported but unused |
reviewbot | |
'LOGIN_LIMIT_RATE' imported but unused |
reviewbot | |
undefined name 'dependency_error' |
reviewbot | |
'settings_local' imported but unused |
reviewbot | |
'LOGIN_LIMIT_RATE' imported but unused |
reviewbot | |
Blank line between these (this is in the djblets codebase, so djblets imports are "this module") |
david | |
Since these are "constants", they should be ALL_CAPS |
david | |
Please use single-quotes for strings. |
david | |
This regex string should be prefixed by r (r'([...) to indicate that it's a "raw" string, so the \s behave … |
david | |
Dedent this 4 spaces (we don't indent the definition because there's only ever one return value, unlike args where there … |
david | |
Combine these so the docstring doesn't start with a blank line ("""Return number of ...) |
david | |
This should list a single return value, "tuple of int". The description for that return value should say that it's … |
david | |
If this fails to match, we should print a warning and return a default value. |
david | |
Merge these lines. |
david | |
Should be "unicode" instead of "String" |
david | |
Dedent this 4 spaces. |
david | |
Docstrings should use 3 double-quotes. Also, please merge these lines. |
david | |
String -> unicode. |
david | |
String -> unicode. |
david | |
Should be: Returns: bool: Whether the current user has exceeded ... |
david | |
Leftover debug code? |
david | |
Three double-quotes, and the first line of the docstring should be a single-line summary on the same line as the … |
david | |
String -> unicode. |
david | |
String -> unicode. |
david | |
Returns: dict: A dictionary containing ... |
david | |
Leftover debug code? |
david | |
Let's break this out into multiple lines: return { 'count': count, 'limit': limit, 'time_left': time_left, } |
david | |
Blank line between these. |
david | |
Let's create the requestFactory inside the test-case class, probably in a setUpClass method. That way it won't be done unless … |
david | |
For the unit tests in this file, I think we can merge them all into one test class. |
david | |
This docstring should go within the function instead of above it (right now it's acting as the docstring for the … |
david | |
Test docstrings should be of the format "Testing X" to match the others in the suite output. |
david | |
Test docstrings should be of the format "Testing X" to match the others in the suite output. |
david | |
Test docstrings should be of the format "Testing X" to match the others in the suite output. |
david | |
We shouldn't need/want this. settings_local is something we do in Review Board, but the djblets settings.py file is primarily there … |
david | |
'settings_local' imported but unused |
reviewbot | |
'LOGIN_LIMIT_RATE' imported but unused |
reviewbot | |
Blank line between these two. |
david | |
If I'm reading the code correctly, this will update the rate limit info in the cache, even if the authentication … |
david | |
Leftover debug code? |
david | |
"ck_ratelimit" is kind of a weird variable name. If you get rid of the debug output, we can just move … |
david | |
This can wrap a little better to fit within the 80 columns. The second line should also align with the … |
david | |
Let's rewrap this for better formatting: self.assertEqual( result, (False, 'Maximum number of login attempts exceeded', None)) |
david | |
Let's rewrap this for better formatting: self.assertEqual( result, (False, 'Maximum number of login attempts exceeded', None)) |
david | |
'settings_local' imported but unused |
reviewbot | |
'LOGIN_LIMIT_RATE' imported but unused |
reviewbot | |
Summary should be on first line. |
brennie | |
We don't have return annotation support for multiple returns or namned returns. This technically returns a tuple. However, we can … |
brennie | |
What happens if i set RATE_LIMIT = 'nope'? We should raise ImproperlyConfigured if we cannot parse this value. |
brennie | |
Docstring. |
brennie | |
You will not want to use naive times (i.e., without timezones). You will want to do: from django.utils import timezone … |
brennie | |
Does w stand for window? If so, can we name it that? |
brennie | |
Same comments re: summaries. |
brennie | |
unicode. |
brennie | |
This shouldn't be indented. |
brennie | |
Double quotes for docstrings. Summary should be on this line. Also, docstrings should be in the imperitive mood, i.e. they … |
brennie | |
Double quotes for docstrings. Summary should be on this line. Also, docstrings should be in the imperitive mood, i.e. they … |
brennie | |
This is missing the type annotation (dict). You can also use restructuredText to annotate the keys and values. dict: A … |
brennie | |
We won't want this to be a global. Instead we can put this inside the setUpClass method of RateLimitTests: class … |
brennie | |
Instead of this, you can use django.contrib.auth.User and django.contrib.auth.AnonymousUser. The latter will always have is_authenticated() return False. |
brennie | |
There should be a testcase that tests that improper data raises an exception. |
brennie | |
Blank line between these. Missing period. Missing docstring for test method. |
brennie | |
This should use self.assertEqual It should also probably include a message about the rate being tested. e.g. self.assertEqual(count_seconds, split_rate(rate), "split_rate(%r) … |
brennie | |
RateLimitTests We make our test case class names plural |
brennie | |
Missing period. |
brennie | |
This must call super(RateLimitTests, self).setUp() Also, this will not clear the cache after the last test is run so add: … |
brennie | |
test case names should indicate what feature they are testing. e.g. "Testing rate limit invalid keys" or similar. Also it … |
brennie | |
Testing |
brennie | |
Testing |
brennie | |
settings_local is a Review Board thing. Undo this change. |
brennie | |
'settings_local' imported but unused |
reviewbot | |
'LOGIN_LIMIT_RATE' imported but unused |
reviewbot | |
Single quotes. |
brennie | |
No hanging indent here. It should be flush with the first " |
brennie | |
This file should not be committed. |
brennie | |
Col: 63 E225 missing whitespace around operator |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 80 E501 line too long (109 > 79 characters) |
reviewbot | |
Col: 1 E302 expected 2 blank lines, found 1 |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (83 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (86 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (87 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (85 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (85 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (84 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (83 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (83 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (84 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
Col: 80 E501 line too long (83 > 79 characters) |
reviewbot | |
Col: 21 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 21 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 21 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 21 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 17 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 17 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 17 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 17 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 18 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 18 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 18 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 18 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 18 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 18 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 18 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 18 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 21 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 21 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 21 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 21 E128 continuation line under-indented for visual indent |
reviewbot | |
Bare imports should be above from ... |
chipx86 | |
If this is meant to be public, it should have a comment using #: and a one-line summary. If it's … |
chipx86 | |
No need for [] around \d |
chipx86 | |
How about get_user_id_or_ip? |
chipx86 | |
Maybe mention that this is for the request here. Otherwise it sounds like it'd be generic enough for any users … |
chipx86 | |
Here and in other docstrings, you can fit a lot more per line (up to 79 characters). |
chipx86 | |
"String" is redundant (because of the type), so you can just say "The user ID or IP." |
chipx86 | |
This will want to also factor in two other possible variables often used when standing up a proxy in front … |
chipx86 | |
The name doesn't really make the purpose of this function clear. The docstring isn't quite correct either. What this is … |
chipx86 | |
If PERIOD is meant to be public (which it should be if we're referencing it), then it needs to be … |
chipx86 | |
Any literal (a variable or tuple or similar) should be using double backticks to turn it into an inline code … |
chipx86 | |
It feels like this shouldn't really be handled here. Instead, this function should focus solely on doing this conversion. Callers … |
chipx86 | |
No need to pre-define these. They'll always be set below. |
chipx86 | |
What exceptions are we expecting to hit? |
chipx86 | |
We never want to use print, as this can actually break the web server. Instead, logging statements should be used, … |
chipx86 | |
You can set this below in Rate(...) instead of doing it here. |
chipx86 | |
This can be private. |
chipx86 | |
Docstring summaries must be one line. You have a lot more room to work with on here though. |
chipx86 | |
I don't fully understand this method. It assumes knowledge of what a "window" means in this case. Perhaps you can … |
chipx86 | |
user_id_or_ip is a better variable. |
chipx86 | |
Blank line between these. |
chipx86 | |
int, not Integer. Same elsewhere. |
chipx86 | |
This line should only have the type (int). |
chipx86 | |
We may want time.gmtime here, so timezones aren't an issue? |
chipx86 | |
It'd be nice if you could document the algorithm here. |
chipx86 | |
This can be private. |
chipx86 | |
Same comment as above with the naming. |
chipx86 | |
This can be one statement. In fact, much of the above can be combined into one statement. |
chipx86 | |
For functions returning things, it's better to have get_ or is_ or can_ or similar prefixes (depending on the function). … |
chipx86 | |
Missing docs for increment. |
chipx86 | |
I don't really see value in allowing these to be provided. There's unlikely to be a case where we need … |
chipx86 | |
Blank line here. |
chipx86 | |
No blank line here. |
chipx86 | |
limited is too generic a thing to be tacking onto a request object. You would need to use something like … |
chipx86 | |
Parameters should come after get_usage_count like other functions. |
chipx86 | |
Must be one line. |
chipx86 | |
This is missing increment. |
chipx86 | |
Blank line here. |
chipx86 | |
The dictionary entries cannot be indented past the original text, or this will be displayed wrong. |
chipx86 | |
You shouldn't use a backslash in strings, or it'll look like: Could not understand ratelimit key Instead, you can just … |
chipx86 | |
Since this is the default in the code, do we need it here? Only unit tests and such will be … |
chipx86 | |
Col: 13 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 21 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 5 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 21 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 5 E128 continuation line under-indented for visual indent |
reviewbot | |
'namedtuple' imported but unused |
reviewbot | |
Col: 8 W291 trailing whitespace |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 5 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
Col: 25 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 73 W291 trailing whitespace |
reviewbot | |
'zlib' imported but unused |
reviewbot | |
'ImproperlyConfigured' imported but unused |
reviewbot | |
Col: 68 W291 trailing whitespace |
reviewbot | |
Col: 17 E128 continuation line under-indented for visual indent |
reviewbot | |
Col: 80 E501 line too long (80 > 79 characters) |
reviewbot | |
'User' imported but unused |
reviewbot | |
'ImproperlyConfigured' imported but unused |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
'settings' imported but unused |
reviewbot | |
'DEFAULT_KEY' imported but unused |
reviewbot | |
Col: 21 E126 continuation line over-indented for hanging indent |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
'settings' imported but unused |
reviewbot | |
Col: 29 E126 continuation line over-indented for hanging indent |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
'settings' imported but unused |
reviewbot | |
Missing a period. |
brennie | |
This information belongs in the Returns section. |
brennie | |
Any reason to cast this to a string? The consumer makes it into a cache key, but returning an int … |
brennie | |
typo: "THe" |
brennie | |
No comma here. |
brennie | |
Delete this line. |
brennie | |
Can you rename this to timestamp? I assume thats what ts is short for. |
brennie | |
Args does not match up with the actual argument list. |
brennie | |
This can all be one line. |
brennie | |
This can be one line. |
brennie | |
Single line. |
brennie | |
This should not be called DEFAULT_LIMIT, since it is just the limit. Only constants should be ALLCAPS. |
brennie | |
Missing period. |
brennie | |
Expand what ck stands for, please. |
brennie | |
Add a trailing comma. |
brennie | |
Missing a docstring. |
brennie | |
This should be a constant on the class. |
brennie | |
Compiled regexes are intended to be reused. By compiling it in the function, you only ever use it once (because … |
brennie | |
Instead of this, how about: m = rate_re.match(rate_str) if m: count, multi, period = m.groups() else: logging.warn("...") count, multi, period … |
brennie | |
This should be except Exception:. A empty except clause can also catch things you don't want to catch, such as … |
brennie | |
This should be a constant either on the class or globally. |
brennie | |
Use %r for the rate so that we get the actual representation. That way, if someone just passes an int … |
brennie | |
Missing docstring. |
brennie | |
Missing docstring. |
brennie | |
self.__dict__.keys() is going to return more than you want to iterate over. For example: >>> class Foo(object): ... x = … |
brennie | |
'override_settings' imported but unused |
reviewbot | |
This isn't used. |
brennie | |
We generally use the format: Testing Rate.parse |
brennie | |
Same here re: docstring. |
brennie | |
This can be a single statement. |
brennie | |
Include the method you're testing (is_ratelimited), e.g. """Testing is_ratelimited with invalid rate limit parameter""" |
brennie | |
You should give it a pk= arg, because it won't have one by default until it gets saved to the … |
brennie | |
This can be a singel statement. |
brennie | |
Same here re: docstring. |
brennie | |
You should give it a pk= arg, because it won't have one by default until it gets saved to the … |
brennie | |
This can be a single statement. |
brennie | |
This seems to be repeated from above. |
brennie | |
This should be at the top of the class after setUpClass. |
brennie | |
'settings' imported but unused |
reviewbot | |
Missing a period. This string should also probably be marked for translation with ugettext. |
brennie | |
Are we sure we want to do this and not just return result here? Perhaps double check with David on … |
brennie | |
Col: 13 E126 continuation line over-indented for hanging indent |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 80 E501 line too long (82 > 79 characters) |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
'settings' imported but unused |
reviewbot | |
Col: 80 E501 line too long (83 > 79 characters) |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
'settings' imported but unused |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
'settings' imported but unused |
reviewbot | |
Can you reflow your docstrings to 79 chars wide? They're unnecessarily narrow. |
brennie | |
str is bytes, i.e. binary data. You want to use six.text_type here. However, you can just do: return '%s%s%s' % … |
brennie | |
Unnecessary. |
brennie | |
e.g. over ex. Missing period. |
brennie | |
Variables usually should have noun names. How about prepped_cache_key? |
brennie | |
Technically, is False :) Missing a period. |
brennie | |
This comment is unnecessary. |
brennie | |
Can we call this initial_value ? |
brennie | |
Docstrings should be of the format: """Single line summary. Multi-line description. """ |
brennie | |
Blank line between these. |
brennie | |
If multi is multiplier can we rename it to that? |
brennie | |
Same comment here re: docstrings. Also missing args. |
brennie | |
Missing a "Yields" section. How about: "Yield the count and seconds attributes attributes. Yields: ... """ |
brennie | |
Again, I don't think this is appropriate. |
brennie | |
'override_settings' imported but unused |
reviewbot | |
Blank line between these. |
brennie | |
Col: 67 W291 trailing whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
Col: 1 W293 blank line contains whitespace |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
Did you want this to be prefixed with _ as well? Doesn't seem to be used else where in this … |
KH khp | |
I think this would be better on one line. |
KH khp | |
Could be wrong here but I think we usually just put the return type and not the variable name under … |
KH khp | |
Unless this is something else, integer types are called int, so probably best to use (int) instead of (integer) here. |
KH khp | |
Same as above. |
KH khp | |
Same as above, this could be a private method I think. |
KH khp | |
Should this be at line 167? |
KH khp | |
Just a suggestion, and not 100% sure if it's equivalent: # Initialize cache with cache_key if it doesn't exist already. … |
KH khp | |
There's an extra newline I think. |
KH khp | |
Extra newline here. |
KH khp | |
int is better here I think. There's more below. |
KH khp | |
Extra newline here. |
KH khp | |
'override_settings' imported but unused |
reviewbot | |
This description is kind of confusing because the method doesn't do anything with keys or users. It's really just applying … |
david | |
"the given key" implies that a key is passed in, which it's not. |
david | |
Don't we want to add period back to this value? Based on my reading of this, it returns a "timestamp" … |
david | |
You can remove the single-quotes from the name of the key here. |
david | |
And here. |
david | |
And here. |
david | |
It seems very complex to define a class for this which has a classmethod to parse a string, instantiates and … |
david | |
The first line in the docstring should be on the same line as the """ |
david | |
The first line in the docstring should be on the same line as the """ |
david | |
'override_settings' imported but unused |
reviewbot | |
The places that you use _ are within function bodies, so this should use ugettext instead of ugettext_lazy. |
david | |
'override_settings' imported but unused |
reviewbot | |
'zlib' imported but unused |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
'override_settings' imported but unused |
reviewbot | |
"Utilities" |
brennie | |
Can you document this with a #: doc comment ? |
brennie | |
Reflow this. |
brennie | |
Instead of "user_id_or_ip", "user ID or IP address" |
brennie | |
get_usage_count will never not return a dict with count or limit, so you can just use plain old indexing: return … |
brennie | |
wouldnt this be rate_limit ? |
brennie | |
Missing period. |
brennie | |
Summary should be on the first line. How about: A rate representing login attempt frequency. or similar. |
brennie | |
Instead of this, why not just implement __eq__ to test for equality? |
brennie | |
Missing file docstring. |
brennie | |
'override_settings' imported but unused |
reviewbot | |
This docstring doesnt fit. |
brennie | |
This doesn't need to be inside the with. |
brennie | |
'django.test.utils.override_settings' imported but unused |
reviewbot | |
'django.conf.settings' imported but unused |
reviewbot | |
'django.test.utils.override_settings' imported but unused |
reviewbot | |
The default rate should be a constant. |
brennie | |
The default rate should be a constant. Additionally, I think this function should be responsible for the default instead of … |
brennie | |
Care to add doc comments to these? |
brennie | |
Instead of this, lets make an actual Rate() object. |
brennie | |
I don't think this should fall back to parsing some default. I think we should raise a ValueError. |
brennie | |
I'm still not convinced that destructuring this is useful. This is not a tuple. |
brennie | |
'django.test.utils.override_settings' imported but unused |
reviewbot | |
lets call it rate_str, rate since its not a tuple. |
brennie | |
No blank line here. |
brennie | |
Leading whitespace. |
brennie | |
No blank line here. |
brennie | |
No blank line here. |
brennie | |
'logging' imported but unused |
reviewbot | |
'django.test.utils.override_settings' imported but unused |
reviewbot | |
local variable 'result' is assigned to but never used |
reviewbot | |
undefined name 'assertTrue' |
reviewbot | |
djblets_settings should probably never be used. This is really for internal usage for unit tests and such. |
chipx86 | |
Two blank lines here. |
chipx86 | |
I'd call this "login-ratelimit". |
chipx86 | |
You can speed this up by being optimistic about the keys: try: return request.META['HTTP_X_REAL_IP'] except KeyError: try: return request.META['HTTP_X_FORWARDED_FOR'].split(',')[0].strip() except … |
chipx86 | |
This can be one statement. |
chipx86 | |
This can be one statement, which will also be faster: return '%d/%ds%s%s' % (limit, period, user_id_or_ip, _get_window(period)) |
chipx86 | |
"Check whether the current ..." |
chipx86 | |
"get_usage_count" is an implementation detail. No need for it in the docs here. |
chipx86 | |
bool, optional |
chipx86 | |
bool, optional |
chipx86 | |
You can fit more per line. |
chipx86 | |
This should be checking django.conf.settings, not djblets.settings. The latter will never be overridden. |
chipx86 | |
I don't like having this in here. This should be solely up to the unit tests. |
chipx86 | |
This shouldn't be necessary. There should always be a suitable default without requiring the consumer to set one, in this … |
chipx86 | |
You can fit more per line. |
chipx86 | |
I want to suggest an alternative version of this that I think will help readability, reduce communication with the cache … |
chipx86 | |
You can collapse this to a elif. |
chipx86 | |
When referencing a function in this class, use: :py:meth:`parse` |
chipx86 | |
This isn't a function. |
chipx86 | |
Blank line here. |
chipx86 | |
This must follow the form of a one-line summary, blank line, multi-line description. |
chipx86 | |
Same as above with the format. Instead of get_usage_count(), do: :py:func:`get_usage_count` (:py:func: is used for top-level functions, :py:meth: for methods … |
chipx86 | |
I don't really know that we need this, given the suggestion above for settings. |
chipx86 | |
ValueError doesn't turn multiple arguments into a string. The user is actually going to see this error message: "('Could not … |
chipx86 | |
The regex already handles ensuring that we have a lowercase value. Given that, You can collapse these into: seconds = … |
chipx86 | |
seconds *= int(multiplier) |
chipx86 | |
Do we really want this? I think callers should be explicit in using rate.count and rate.seconds. Keeps all code self-documenting. |
chipx86 | |
"Return whether ..." |
chipx86 | |
If this is for testing purposes, it should live in the unit tests only. |
chipx86 | |
bool |
chipx86 | |
Can be: return (isinstance(other, Rate) and self.count == other.count and self.seconds == other.seconds) |
chipx86 | |
Alphabetical order. |
chipx86 | |
We have our own TestCase class that should be used instead. See djblets/testing/ |
chipx86 | |
Should be more descriptive, so someone looking at these tests can figure out exactly what is being tested. I suggest: … |
chipx86 | |
This should be setUp, not setUpClass, so you have a fresh factory for each test. |
chipx86 | |
This needs to happen in tearDown, not tearDownClass, or each of the unit tests will be sharing the same cache … |
chipx86 | |
You're not testing that Django's settings decorator works. No need for this. Same with ones below. |
chipx86 | |
This can be one statement. You should also have a blank line between the setup and the assertions. Same below. |
chipx86 | |
Blank line between code and new blocks (like with). |
chipx86 | |
This is actually testing if context has a value, and isn't testing the string. The string is not correct either. … |
chipx86 | |
Condense, and blank line before. |
chipx86 | |
testing should never be added to methods. |
chipx86 | |
Why is this change needed? |
chipx86 | |
Should be reverted once testing is removed. |
chipx86 | |
Here, too. |
chipx86 | |
I wouldn't test defaults vs. custom values here. We don't frankly care at this level, so long as the core … |
chipx86 | |
Better to do this in a loop. Reads better. |
chipx86 | |
Should also be in a loop. |
chipx86 | |
Rather than having this here, I'd have the unit tests that depend on this set the value for their tests. … |
chipx86 |
- Description:
-
~ Added additional methods for ratelimit.py, not done and still need to test.
~ Completed code but still need to test.
- Testing Done:
-
+ Not done yet.
- Commit:
-
cfcebea0fb1bcdf627fa725260041a71bd73e89bdd241e3fdfa5bd8910385d3689924ed709a613ae
-
-
-
Do we want to give the frequency or the minimum period between attempts? If you use minimum period you can just write this as the value in seconds and you don't have to worry about parsing it.
e.g.
LOGIN_RATE_LIMIT = 20
would be equivalent to 5/mMight wanna double check with Christian on this, though.
- Description:
-
~ Completed code but still need to test.
~ There's been a request to create a rate-limit for logins through either the UI or API. I have created a main ratelimit.py file that will allow rate limiting of login attempts by either an authenticated user or an IP address. This is still not complete since it needs to be implemented in djblet's webapi and ReviewBoard's Authentication Form.
- Testing Done:
-
~ Not done yet.
~ I still need to create more unit tests, however I did create a couple of unit tests in djblets/auth/tests/test_ratelimit.py.
- Commit:
-
dd241e3fdfa5bd8910385d3689924ed709a613aea0b3cba765bf3bcd622bd527687bf55538454ffb
-
Tool: Pyflakes Processed Files: djblets/auth/tests/test_ratelimit.py djblets/auth/ratelimit.py djblets/settings.py tests/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/auth/tests/test_ratelimit.py djblets/auth/ratelimit.py djblets/settings.py tests/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Testing Done:
-
~ I still need to create more unit tests, however I did create a couple of unit tests in djblets/auth/tests/test_ratelimit.py.
~ I still need to create more unit tests, however I did create a couple of unit tests that are passing in djblets/auth/tests/test_ratelimit.py.
- Commit:
-
a0b3cba765bf3bcd622bd527687bf55538454ffb6b7217fd78b1632f11dcff88782ccaeeb505a0eb
-
Tool: Pyflakes Processed Files: djblets/auth/tests/test_ratelimit.py djblets/auth/ratelimit.py djblets/settings.py tests/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/auth/tests/test_ratelimit.py djblets/auth/ratelimit.py djblets/settings.py tests/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
These comments are mostly related to documentation style. Looks like a pretty solid start!
-
Docstrings should use three double-quotes instead of single. The first line should also be joined with the quotes so the docstring doesn't start with a
\n
character:def user_or_ip(request): """Obtain user ID or IP address from request. If ... """
The text should also be full sentences with good grammar (when possible). In this case, probably "Return the user's ID or IP address."
-
-
-
We should use
six.string_type(...)
(importingdjango.utils.six
above). On Python 2,str
is a bytestring, while on Python 3, it's a unicode string. -
Same here with the docstring. This should also be written in the imperative mood ("Return ..." instead of "Returns ...")
-
-
-
-
-
-
-
-
-
Some of our old test code uses camelCase, but new code should use underscores, such as
test_invalid_key
-
-
-
-
- Change Summary:
-
Added implementation of ratelimit to djblet's webapi authentication backend along with unit tests.
- Description:
-
~ There's been a request to create a rate-limit for logins through either the UI or API. I have created a main ratelimit.py file that will allow rate limiting of login attempts by either an authenticated user or an IP address. This is still not complete since it needs to be implemented in djblet's webapi and ReviewBoard's Authentication Form.
~ There's been a request to create a rate-limit for logins through either the UI or API. I have created a main ratelimit.py file that will allow rate limiting of login attempts by either an authenticated user or an IP address. This is still not complete since it needs to be implemented in ReviewBoard's Authentication Form. I also still need to add a custom djblets settings for custom rate limits (override djblet settings).
- Testing Done:
-
~ I still need to create more unit tests, however I did create a couple of unit tests that are passing in djblets/auth/tests/test_ratelimit.py.
~ Not done with all unit tests, however I did create a couple of unit tests that are passing in djblets/auth/tests/test_ratelimit.py and in webapi/tests/test_api_auth_backend.py.
- Commit:
-
6b7217fd78b1632f11dcff88782ccaeeb505a0eb471be93f5d5eddac2a1a8baeac112cbdcd6cb22b
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
- Change Summary:
-
Fixed bug in ratelimit.py: use getattr to obtain default login limit rate
- Commit:
-
077539e2a3f516d4a555097a961b2f2b992018ef54fd5444ae0da4384a5d1165425f5154e85beb56
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py settings_local.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
-
This regex string should be prefixed by
r
(r'([...
) to indicate that it's a "raw" string, so the \s behave like we want. -
Dedent this 4 spaces (we don't indent the definition because there's only ever one return value, unlike args where there are many).
-
-
This should list a single return value, "tuple of int". The description for that return value should say that it's a 2-tuple of (count, seconds) and explain what count and seconds are.
-
-
-
-
-
-
-
-
-
-
Three double-quotes, and the first line of the docstring should be a single-line summary on the same line as the quotes.
-
-
-
-
-
Let's break this out into multiple lines:
return { 'count': count, 'limit': limit, 'time_left': time_left, }
-
-
Let's create the requestFactory inside the test-case class, probably in a
setUpClass
method. That way it won't be done unless we're actually executing the test cases (right now it's done when the file is parsed) -
-
This docstring should go within the function instead of above it (right now it's acting as the docstring for the class).
Test docstrings should be of the format "Testing X" to match the others in the suite output.
-
-
-
-
We shouldn't need/want this.
settings_local
is something we do in Review Board, but the djblets settings.py file is primarily there to run the unit tests, and isn't used by consumers as anything other than a reference. -
-
If I'm reading the code correctly, this will update the rate limit info in the cache, even if the authentication is sucessful. That means that we'll be inadvertantly rate-limiting all API requests, even if the credentials are fine.
I think what we want to do is first check whether the current request is rate-limited and fail if so, but not actually store anything. We can then store iff authentication fails.
-
-
"ck_ratelimit" is kind of a weird variable name. If you get rid of the debug output, we can just move the check inline:
if is_ratelimited(...): ...
-
This can wrap a little better to fit within the 80 columns. The second line should also align with the start of the quotes rather than the start of the text.
-
Let's rewrap this for better formatting:
self.assertEqual( result, (False, 'Maximum number of login attempts exceeded', None))
-
Let's rewrap this for better formatting:
self.assertEqual( result, (False, 'Maximum number of login attempts exceeded', None))
-
-
-
We don't have return annotation support for multiple returns or namned returns. This technically returns a
tuple
.However, we can create a new type with
namedtuple
(fromcollections
). This allows us to create a new tuple class that has named fields that can be accessed with either indices or the field names as attributes. e.g.,from collections import namedtuple #: Type summary. #: #: Type description. #: #: Attributes: #: count (int): #: ... #: #: seconds (int): #: ... RateType = namedtuple('RateType', ('count', 'seconds'))
Then we can annotate this return type like so:
def split_rate(rate) """... Returns: RateType: .... """ return RateType(count=count, seconds=seconds)
RateType
is maybe a bad name. PerhapsRate
? I'll leave that up to you. -
What happens if i set
RATE_LIMIT = 'nope'
? We should raiseImproperlyConfigured
if we cannot parse this value. -
-
You will not want to use naive times (i.e., without timezones). You will want to do:
from django.utils import timezone ts = timezone.now()
-
-
-
-
-
Double quotes for docstrings. Summary should be on this line.
Also, docstrings should be in the imperitive mood, i.e. they should be a command. So this should read "Return ...".
-
Double quotes for docstrings. Summary should be on this line.
Also, docstrings should be in the imperitive mood, i.e. they should be a command. SO this should read "Return ..."
Summaries should only be one line.
-
This is missing the type annotation (
dict
). You can also use restructuredText to annotate the keys and values.dict: A dictionary with the following keys: ``'count'`` (:py:class:`int`): ... ``'limit'`` (:py:class:`int`): ... ``'time_left'`` (:py:class:`int`): ...
The above will render as a definition list with links to the appropriate types.
-
We won't want this to be a global. Instead we can put this inside the
setUpClass
method ofRateLimitTests
:class RateLimitTests(TestCase): @classmethod def setUpClass(cls): super(RateLimitTests, cls).setUpClass() cls.request_factory = RequestFactory()
Then it will be available as
self.request_factory
inside the test methods. -
Instead of this, you can use
django.contrib.auth.User
anddjango.contrib.auth.AnonymousUser
. The latter will always haveis_authenticated()
returnFalse
. -
-
-
This should use
self.assertEqual
It should also probably include a message about the rate being tested. e.g.
self.assertEqual(count_seconds, split_rate(rate), "split_rate(%r) != %r" % (rate, count_seconds))
-
-
-
This must call
super(RateLimitTests, self).setUp()
Also, this will not clear the cache after the last test is run so add:
@classmethod def tearDownClass(cls): super(RateLimitTests, cls).tearDownClass() cache.clear()
-
test case names should indicate what feature they are testing. e.g. "Testing rate limit invalid keys" or similar.
Also it should begin with "Testing"
-
-
-
-
-
-
- Change Summary:
-
Fixing up ratelimiting code based on reviews, in progress.
- Commit:
-
252be3961fd87bbae343d834d4852043f6818d460f10cd97d91db85edf4eae6d990bb44518290f3f
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
- Change Summary:
-
Made sure only failed login attempts are cached instead of all API requests.
Still need to implement this feature for reviewboard's authentication. - Description:
-
~ There's been a request to create a rate-limit for logins through either the UI or API. I have created a main ratelimit.py file that will allow rate limiting of login attempts by either an authenticated user or an IP address. This is still not complete since it needs to be implemented in ReviewBoard's Authentication Form. I also still need to add a custom djblets settings for custom rate limits (override djblet settings).
~ There's been a request to create a rate-limit for failed logins through either the UI or API. I have created a main ratelimit.py file that will allow rate limiting of failed login attempts by either an authenticated user or an IP address. This is still not complete since it needs to be implemented in ReviewBoard's Authentication Form. I also still need to add a custom djblets settings for custom rate limits (override djblet settings).
- Commit:
-
2164ed953ef02f28da105151550aa684506f8561f82f0cbf9a46079f29113484cdeed1b68a0c7cd6
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
- Summary:
-
Added new ratelimit file for rate limiting authentication (in progress)Added login rate limiting feature in Djblets auth and web api
- Description:
-
~ There's been a request to create a rate-limit for failed logins through either the UI or API. I have created a main ratelimit.py file that will allow rate limiting of failed login attempts by either an authenticated user or an IP address. This is still not complete since it needs to be implemented in ReviewBoard's Authentication Form. I also still need to add a custom djblets settings for custom rate limits (override djblet settings).
~ There's been a request to create a rate-limit for failed logins through either the UI or API. I have created a main ratelimit.py file that will allow rate limiting of failed login attempts by either an authenticated user or an IP address. See review request 8768 to see it's implementation in reviewboard's authentication form.
- Testing Done:
-
~ Not done with all unit tests, however I did create a couple of unit tests that are passing in djblets/auth/tests/test_ratelimit.py and in webapi/tests/test_api_auth_backend.py.
~ Create several unit tests to ensure the login rate limiting feature works in the web API and UI, whether the rate limits are custom or default values. They are currently passing in djblets/auth/tests/test_ratelimit.py and in webapi/tests/test_api_auth_backend.py.
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
This isn't a complete review. I just have to run, and don't want you blocked on me :) Again, don't feel bad about the number of comments. That's normal for first big projects.
-
-
If this is meant to be public, it should have a comment using
#:
and a one-line summary.If it's meant to be private (I suspect it is), it's better to have a
_
prefix (for this and anything else that's not intended to be imported).Also, this is a constant, so it can be all uppercase.
-
-
-
Maybe mention that this is for the request here. Otherwise it sounds like it'd be generic enough for any users to pass in.
-
-
-
This will want to also factor in two other possible variables often used when standing up a proxy in front of a webapp:
HTTP_X_REAL_IP
andHTTP_X_FORWARDED_FOR
.HTTP_X_REAL_IP
, if it exists, is usually going to be the true IP address.HTTP_X_FORWARDED_FOR
, if it exists, is a list of IPs, comma-separated. The first item in the list is the real IP address. Additional items, if they exist, would be the IPs of proxies that this went through. So you can parse this with:request.META['HTTP_X_FORWARDED_FOR'].split(',')[0].strip()
-
The name doesn't really make the purpose of this function clear. The docstring isn't quite correct either.
What this is doing is parsing a string and returning a
Rate
. So what I'd actually do is convertRate
above into a class that has aparse
class method. Something like:class Rate(object): @classmethod def parse(cls, rate_str): """Return a Rate parsed from the given rate string. ... """ ... return cls(count=..., seconds=...) def __init__(self, count, seconds): count = count seconds = seconds
Then this can be called with
Rate.parse(...)
. It helps tie these together and reduces the number of globals.You can also move
rate_re
andPERIODS
into here. -
If
PERIOD
is meant to be public (which it should be if we're referencing it), then it needs to be documented above and referenced here like::py:data:`PERIOD`
I think, though, that a lot of the docs for this method focuses far too much on internals (like mentioning
regex_re
. It should be more high-level and talk about what it does without going into what variables it's accessing. -
Any literal (a variable or tuple or similar) should be using double backticks to turn it into an inline code block.
Given the above recommendations, you can nuke the tuple bit. In fact, for callers,
Rate
isn't a true tuple anyway, so you wouldn't want/need to refer to it as one. -
It feels like this shouldn't really be handled here. Instead, this function should focus solely on doing this conversion. Callers should be careful to pass in the right thing.
-
-
-
We never want to use
print
, as this can actually break the web server. Instead, logging statements should be used, and it should be a single log statement per thing you want to log.import logging ... logging.warning('Could not parse ...', rate, DEFAULT_LIMIT, ..., request=request)
(Ideally, your method would optionally take the request so we can log info about it).
Also, you never want to build a string by using
+
. You should always use%s
formatting, so that we can localize and so that types convert correctly. -
-
-
-
I don't fully understand this method. It assumes knowledge of what a "window" means in this case. Perhaps you can flesh out the description a bit, make it more high-level?
-
-
-
-
-
-
-
-
-
-
For functions returning things, it's better to have
get_
oris_
orcan_
or similar prefixes (depending on the function). I would call thisis_user_rate_limited
. -
-
I don't really see value in allowing these to be provided. There's unlikely to be a case where we need to have custom cache keys for different callers, and the rate is in
settings
.What I'd actually do is nuke these and make sure to always re-get the rate from settings, instead of defining it globally. This allows the value in settings to be changed dynamically (which we'll eventually want to do) without restarting the web server to re-fetch the value.
-
-
-
limited
is too generic a thing to be tacking onto arequest
object. You would need to use something like_djblets_is_rate_limited
.Trying to understand why we need to inject this though. These request objects won't carry over to future requests, and it looks like we always calculate?
-
-
-
-
-
-
You shouldn't use a backslash in strings, or it'll look like:
Could not understand ratelimit key
Instead, you can just do:
raise ImproperlyConfigured('Could not understand ' 'ratelimit key ...')
Or, better:
raise ImproperlyConfigured( 'Could not understand ratelimit key ...')
-
Since this is the default in the code, do we need it here? Only unit tests and such will be using this.
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
Any reason to cast this to a string? The consumer makes it into a cache key, but returning an int in this case should be fine.
-
-
-
-
-
-
-
-
-
This should not be called
DEFAULT_LIMIT
, since it is just thelimit
. Only constants should beALLCAPS
. -
-
-
-
-
-
Compiled regexes are intended to be reused. By compiling it in the function, you only ever use it once (because it will get re-compiled every call). Instead, move this into a class constant.
-
Instead of this, how about:
m = rate_re.match(rate_str) if m: count, multi, period = m.groups() else: logging.warn("...") count, multi, period = ...
This doesn't require an exception to be thrown. In general, exceptions aren't great for flow control.
-
This should be
except Exception:
. A emptyexcept
clause can also catch things you don't want to catch, such asKeyboardInterrupt
orRuntimeError
, which don't inherit fromException
. -
-
Use
%r
for the rate so that we get the actual representation. That way, if someone just passes an int we will get5
, but if they pass a string we would get"5"
. -
-
-
self.__dict__.keys()
is going to return more than you want to iterate over. For example:>>> class Foo(object): ... x = 1 ... y = 2 ... def __init__(self): ... self.z = 3 ... def foo(self): ... pass ... >>> Foo.__dict__.keys() ['__module__', '__dict__', 'y', 'x', 'foo', '__weakref__', '__doc__', '__init__']
Just
yield
the attributes you want. -
-
-
-
-
Include the method you're testing (
is_ratelimited
), e.g."""Testing is_ratelimited with invalid rate limit parameter"""
-
You should give it a
pk=
arg, because it won't have one by default until it gets saved to the database. -
-
-
You should give it a
pk=
arg, because it won't have one by default until it gets saved to the database. -
-
-
-
-
Are we sure we want to do this and not just return
result
here? Perhaps double check with David on this.NB: If he already told you to do this, drop this issue.
- Change Summary:
-
Addressed most of Barrett's reviews.
- Commit:
-
46c165c93374cdd417277f34ee3c1b86da4db79f06e89515f3615cbd0882e79317babb415c82d3cd
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
str
isbytes
, i.e. binary data. You want to usesix.text_type
here.However, you can just do:
return '%s%s%s' % (safe_rate, user_id_or_ip, window)
This will avoid constructing a list and casting will be unnecessary (as
%s
will cast everything tosix.text_type
). -
-
-
-
-
-
-
-
-
-
-
-
Missing a "Yields" section.
How about:
"Yield the count and seconds attributes attributes.
Yields:
...
""" -
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
-
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
Cool stuff, this was interesting to read =) I've added some formatting and a refactoring suggestions as well.
-
Did you want this to be prefixed with
_
as well? Doesn't seem to be used else where in this diff at least. -
-
Could be wrong here but I think we usually just put the return type and not the variable name under
Returns:
. -
Unless this is something else, integer types are called int, so probably best to use
(int)
instead of(integer)
here. -
-
-
-
Just a suggestion, and not 100% sure if it's equivalent:
# Initialize cache with cache_key if it doesn't exist already. cache.add(cache_key, 0) if increment: try: count = cache.incr(cache_key) except ValueError count = 1 else: count = cache.get(cache_key, 0) return { 'count': count, 'limit': limit, 'time_left': time_left, }
-
-
-
-
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
This description is kind of confusing because the method doesn't do anything with keys or users. It's really just applying the period to the current time and returning that, which is used in creating the cache key.
-
-
Don't we want to add
period
back to this value? Based on my reading of this, it returns a "timestamp" which is in the past, rather than the future.I don't see any code which uses the
time_left
value which is calculated from this, which is probably why you haven't noticed it being problematic. -
-
-
-
It seems very complex to define a class for this which has a classmethod to parse a string, instantiates and instance, and then uses
__iter__
to return the results. Why not just have a function which parses the string and returns a 2-tuple? -
-
-
The places that you use
_
are within function bodies, so this should useugettext
instead ofugettext_lazy
.
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
-
Tool: Pyflakes Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py Tool: PEP8 Style Checker Processed Files: djblets/webapi/auth/backends/base.py tests/settings.py djblets/auth/tests/test_ratelimit.py djblets/webapi/tests/test_api_auth_backend.py djblets/auth/ratelimit.py djblets/settings.py Ignored Files: djblets/auth/tests/__init__.py
-
- Description:
-
~ There's been a request to create a rate-limit for failed logins through either the UI or API. I have created a main ratelimit.py file that will allow rate limiting of failed login attempts by either an authenticated user or an IP address. See review request 8768 to see it's implementation in reviewboard's authentication form.
~ Developed rate-limit for failed logins through either the UI or API.
+ See review request 8768 to see it's implementation in Review Board's + Authentication form. - Testing Done:
-
~ Create several unit tests to ensure the login rate limiting feature works in the web API and UI, whether the rate limits are custom or default values. They are currently passing in djblets/auth/tests/test_ratelimit.py and in webapi/tests/test_api_auth_backend.py.
~ Create several unit tests for the login rate limiting feature that
+ test the rate limits as either custom or default values. They are + found in djblets/auth/tests/test_ratelimit.py and in + webapi/tests/test_api_auth_backend.py. - Commit:
-
6911d7083734ecfa25f1f9a4ff689b7e7db414bfb130a7a88ffac515f162dac77c7f4a29d35f9fe3
- Description:
-
~ Developed rate-limit for failed logins through either the UI or API.
~ See review request 8768 to see it's implementation in Review Board's ~ Authentication form. ~ Developed rate-limit for failed login attempts through either the UI
~ or API. See review request 8768 to see it's implementation in Review ~ Board's Authentication form.
-
-
-
The default rate should be a constant.
Additionally, I think this function should be responsible for the default instead of parse, e.g.
try: rate_limit = getattr(settings, 'LOGIN_LIMIT_RATE') rate_limit = Rate.parse(rate_limit) except ValueError: logging.warn('Could not parse rate limit "%s"; using default "%s" instead', rate_limit, Rate.DEFAULT_LIMIT) rate_limit = Rate.DEFAULT_LIMIT except AttributeError: logging.warn('No configured rate limit; using default "%s" instead', rate_limit, RATE.DEFAULT_LIMIT) rate_limit = Rate.DEFAULT_LIMIT
This refactors the default-checking code into a single place and will make everything else simpler.
Also in the case where
settings.LOGIN_LIMIT_RATE
is bogus or not found, are we sure we don't want toraise ImproperlyConfigured
? -
-
-
-
-
-
-
-
-
- Commit:
-
fa1c12da84a1f827f4ef1ada1c30094ede099660230b9167b41ae32f5b6032149b8116a8c2f3fb01
Checks run (1 failed, 1 succeeded, 1 failed with error)
Pyflakes
- Commit:
-
230b9167b41ae32f5b6032149b8116a8c2f3fb016121b4c5d7602c3648f6218333e7e7cbe6ecca59
Checks run (2 succeeded, 1 failed with error)
-
-
djblets_settings
should probably never be used. This is really for internal usage for unit tests and such. -
-
-
You can speed this up by being optimistic about the keys:
try: return request.META['HTTP_X_REAL_IP'] except KeyError: try: return request.META['HTTP_X_FORWARDED_FOR'].split(',')[0].strip() except KeyError: return request.META['REMOTE_ADDR']
It nests more, but it reduces the number of lookups required.
-
-
This can be one statement, which will also be faster:
return '%d/%ds%s%s' % (limit, period, user_id_or_ip, _get_window(period))
-
-
-
-
-
-
This should be checking
django.conf.settings
, notdjblets.settings
. The latter will never be overridden. -
-
This shouldn't be necessary. There should always be a suitable default without requiring the consumer to set one, in this case.
I would do:
rate_limit = getattr(settings, 'LOGIN_RATE_LIMIT', '5/m')
Note also that you shouldn't need
Rate.DEFAULT_LIMIT
above. We want exactly one place where the rate limit is fetched/default computed, and that's here. -
-
I want to suggest an alternative version of this that I think will help readability, reduce communication with the cache server, and prevent some redundancy in variables (
count
andinitial_value
):count = None if increment: try: count = cache.incr(cache_key) except ValueError: cache.add(cache_key, 1) if count is None: count = cache.get(cache_key, 0)
So what's happening here is that if we're incrementing, we do so optimistically, and if not, we add a default to say we're incrementing from 0 to 1. We increment (unless I'm mistaken) when we're increasing the number of attempts, so that's really the time when we care about a value persisting in the cache.
If we're not incrementing, then having a value of 0 in the cache is no different than not having a value, since when we go to increment later, we handle that case of a missing 0 anyway. So we can skip adding to the cache here.
In the event that we either incremented but didn't have an existing value in cache, or aren't incrementing, we can just take the value found in cache as-is, defaulting to 0 attempts.
This should keep things tidy, avoid extra communication, and avoid race conditions.
(You should check my logic to make sure I wasn't completely off on how that's intended to work.)
-
-
-
-
-
-
Same as above with the format.
Instead of
get_usage_count()
, do::py:func:`get_usage_count`
(
:py:func:
is used for top-level functions,:py:meth:
for methods in a class.) -
-
ValueError
doesn't turn multiple arguments into a string. The user is actually going to see this error message:"('Could not parse given rate: %s.', <the value of rate_str here>)"
Instead, use
'...' % rate_str
-
The regex already handles ensuring that we have a lowercase value. Given that, You can collapse these into:
seconds = Rate.PERIODS[period or 's']
-
-
Do we really want this? I think callers should be explicit in using
rate.count
andrate.seconds
. Keeps all code self-documenting. -
-
-
-
Can be:
return (isinstance(other, Rate) and self.count == other.count and self.seconds == other.seconds)
-
-
-
Should be more descriptive, so someone looking at these tests can figure out exactly what is being tested. I suggest: "Unit tests for djblets.auth.ratelimit."
-
-
This needs to happen in
tearDown
, nottearDownClass
, or each of the unit tests will be sharing the same cache state. -
-
This can be one statement.
You should also have a blank line between the setup and the assertions.
Same below.
-
-
This is actually testing if
context
has a value, and isn't testing the string. The string is not correct either. This should be:with self.assertRaises(ValueError) as e: is_ratelimited(...) self.assertEqual(six.text_type(e), 'Could not parse given rate: blah')
-
-
-
-
-
-
I wouldn't test defaults vs. custom values here. We don't frankly care at this level, so long as the core rate limiting functions are doing the correct thing with defaults vs. custom values (which belongs in that test suite).
-
-
-
Rather than having this here, I'd have the unit tests that depend on this set the value for their tests. Tests should be as self-contained as possible.