-
-
Notifications
You must be signed in to change notification settings - Fork 31k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed #31405 -- Added LoginRequiredAuthenticationMiddleware force all views to require authentication by default. #17792
Conversation
90e31fe
to
1f5f02a
Compare
7a22f7b
to
75e3ce9
Compare
why does
instead of accepting:
|
I believe the cause of this is that the formatting tool Black has been recently updated to its 2024 style. The Django code has been updated as well, so if you rebase your branch onto main ( |
75e3ce9
to
18a849e
Compare
My bad, thanks for the fix! |
Amazing work @Hisham-Pak !! Couple of weeks ago friend of mine remind me this.. I was literally thinking about coming back and getting the work done on this PR. |
Thank you for the kind words. Appreciate the effort you put in! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this @Hisham-Pak 👍
This is looking good, I added a few comments.
As a side note, we should keep an eye on the async work that @bigfootjon is doing (PR: #18036 discussion: https://forum.djangoproject.com/t/asyncifying-django-contrib-auth-and-signals-and-maybe-sessions/18770/26). This might require some updates here 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sarahboyce thanks for tagging me! I added a few comments inline. The test cases are essential, but the Middleware
reworking is nice-to-have.
tests/auth_tests/test_middleware.py
Outdated
class EmptyResponseBaseView(View): | ||
def get(self, request, *args, **kwargs): | ||
return HttpResponse() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be worthwhile to have a few async
test cases as well
@@ -34,6 +38,49 @@ def process_request(self, request): | |||
request.auser = partial(auser, request) | |||
|
|||
|
|||
class LoginRequiredMiddleware(MiddlewareMixin): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using MiddlewareMixin
requires a context-switch in async code. Considering that contrib.auth
has an async interface (as applied here: you can use await request.auser()
to get the current user, instead of request.user
). can this middleware be rewritten to not require a context switch in async code?
Ref: 4296102
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the above commit reference after I added testing and cleaned up the implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(just a friendly reminder, when your PR is ready for review uncheck the "patch needs improvement" checkbox on the ticket)
django/contrib/auth/middleware.py
Outdated
def process_view(self, request, view_func, view_args, view_kwargs): | ||
if self.user_is_authenticated: | ||
return None | ||
|
||
view_class = getattr(view_func, "view_class", None) | ||
if view_class and not getattr(view_class, "login_required", True): | ||
return None | ||
|
||
if not getattr(view_func, "login_required", True): | ||
return None | ||
|
||
try: | ||
if request.path.startswith(reverse("admin:index")): | ||
return redirect_to_login( | ||
request.get_full_path(), reverse("admin:login") | ||
) | ||
except NoReverseMatch: | ||
pass | ||
|
||
return redirect_to_login(request.get_full_path()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defining process_view
will still result in context switches :(
Ref this code:
https://github.com/django/django/blob/main/django/core/handlers/base.py#L77-L81
I think you need to rename this function to something else (so it doesn't get caught by that auto-rewrite code) and then call it from __call__
/ __acall__
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I thought nothing in process_view
was io bound so it didn't really matter much if it got wrapped in sync_to_async
?
The way I see to achieve no context switch here is to have our local resolve_request. But I don't know if its worth it? What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I thought nothing in process_view was io bound so it didn't really matter much if it got wrapped in sync_to_async?
Wrapping in sync_to_async still causes a context switch / performance hit :(
Yes, I thought nothing in process_view was io bound so it didn't really matter much if it got wrapped in sync_to_async?
The way I see to achieve no context switch here is to have our local resolve_request. But I don't know if its worth it? What do you think?
I don't think that's necessary, I think you can just change the name to not hit any of the names that will be rewritten and then call this function from __call__
/ __acall__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but the thing is __call__
/__acall__
is not provided a view_func
/callback
it is only passed to process_view
here only after resolving the request.
So the way forward I see is:
- Using a private resolve_request (this gets called twice here and in handler)
- Changing how handler works to support native async (not using same
process_view
name for both sync and async) - Finding a way to not use
view_func
/callback
at all (I didn't look much into it yet)
Am I missing something? Thank you for your patience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing how handler works to support native async (not using same process_view name for both sync and async)
I'm kinda in favor of this idea, but I can understand your hesitancy towards doing it. I think for now we can leave this as you have implemented in this PR and I'll think about how we can solve it better in the general case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for changing the handler, but in a separate ticket and PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Async code LGTM, and thanks for adding the test cases. I leave the rest of the review to @sarahboyce et al
django/contrib/auth/middleware.py
Outdated
def process_view(self, request, view_func, view_args, view_kwargs): | ||
if self.user_is_authenticated: | ||
return None | ||
|
||
view_class = getattr(view_func, "view_class", None) | ||
if view_class and not getattr(view_class, "login_required", True): | ||
return None | ||
|
||
if not getattr(view_func, "login_required", True): | ||
return None | ||
|
||
try: | ||
if request.path.startswith(reverse("admin:index")): | ||
return redirect_to_login( | ||
request.get_full_path(), reverse("admin:login") | ||
) | ||
except NoReverseMatch: | ||
pass | ||
|
||
return redirect_to_login(request.get_full_path()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing how handler works to support native async (not using same process_view name for both sync and async)
I'm kinda in favor of this idea, but I can understand your hesitancy towards doing it. I think for now we can leave this as you have implemented in this PR and I'll think about how we can solve it better in the general case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the updates @Hisham-Pak - we're making progress 🔥
- please see this comment
- we are missing
.. versionadded:: 5.1
in a few places
Other note, if you rebase with the latest changes from main the Python 3.11 failures will go away 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking this on. This is a small feature I’ve wanted to see for a while.
The current approach is insecure for some server configurations - please see my comment on __call__
.
django/contrib/auth/middleware.py
Outdated
def process_view(self, request, view_func, view_args, view_kwargs): | ||
if self.user_is_authenticated: | ||
return None | ||
|
||
view_class = getattr(view_func, "view_class", None) | ||
if view_class and not getattr(view_class, "login_required", True): | ||
return None | ||
|
||
if not getattr(view_func, "login_required", True): | ||
return None | ||
|
||
try: | ||
if request.path.startswith(reverse("admin:index")): | ||
return redirect_to_login( | ||
request.get_full_path(), reverse("admin:login") | ||
) | ||
except NoReverseMatch: | ||
pass | ||
|
||
return redirect_to_login(request.get_full_path()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for changing the handler, but in a separate ticket and PR.
django/contrib/auth/middleware.py
Outdated
self.user_is_authenticated = request.user.is_authenticated | ||
return self.get_response(request) | ||
|
||
async def __acall__(self, request): | ||
self.user_is_authenticated = (await request.auser()).is_authenticated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current approach is insecure. Storing values on self
allows those values to be shared across threads and async tasks. This leads to the possibility that two concurrent requests to a threaded/async server will mix up the “is authenticated” values and incorrectly show a private page.
Please change the middleware to inherit from MiddlewareMixin
, drop the custom __init__
, __call__
and __acall__
, and only implement process_view
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for confirming! Reverted async stuff in 3dd63ed
django/contrib/auth/mixins.py
Outdated
@@ -73,6 +74,15 @@ def dispatch(self, request, *args, **kwargs): | |||
return super().dispatch(request, *args, **kwargs) | |||
|
|||
|
|||
class LoginNotRequiredMixin: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be missing something, but what's the reason for also having a mixin class? It seems like the @login_not_required
decorator will work perfectly well for class-based views too no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it largely comes down to personal preferences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the updates!
I have a few more comments, can you also rebase and squash the commits? ⭐
dbfe048
to
a369114
Compare
91e23ab
to
c1d04b9
Compare
django/contrib/auth/middleware.py
Outdated
""" | ||
Override this method to override the login_url attribute. | ||
""" | ||
login_url = self.login_url or getattr(view_func, "login_url", None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn’t this precedence order incorrect? I would expect we prefer values from view_func
over self.login_url
. If we keep the current implementation, a customized middleware will, for example, make admin views redirect to the middleware’s login page rather than the admin one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, updated 👍
I suggest we do not support class-based attributes on views, as currently defined in Supporting the class-level attributes adds significant complexity to the middleware implementation, as it has to look in two places on views for all of the attributes ( Instead, I think we should only support view-function-level attributes. Class-based views can use This would introduce the oddity that if you subclass a Alternatively, we could make the mixin still work with an implementation like: class LoginNotRequiredMixin(View):
def __init_subclass__(cls):
method_decorator(login_not_required, name='dispatch')(cls) But IMO better to go with only |
0f42613
to
e98c613
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Sarah, that has really tidied up the implementation. I spotted a couple places where view_class
is still being looked at. Can you check the test coverage of the middleware?
I also agree on removing the raise_exception
feature.
django/contrib/auth/middleware.py
Outdated
login_url = getattr(view_func, "login_url", None) | ||
if hasattr(view_func, "view_class"): | ||
login_url = getattr(view_func.view_class, "login_url", login_url) | ||
login_url = login_url or self.login_url or settings.LOGIN_URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The view_class
block can be removed not that we’re no longer using class-level attributes.
login_url = getattr(view_func, "login_url", None) | |
if hasattr(view_func, "view_class"): | |
login_url = getattr(view_func.view_class, "login_url", login_url) | |
login_url = login_url or self.login_url or settings.LOGIN_URL | |
login_url = ( | |
getattr(view_func, "login_url", None) | |
or self.login_url | |
or settings.LOGIN_URL | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you were using LoginRequiredMixin
before, set the login_url
this would now be ignored. I don't think we should ignore it or we will have to state that the LoginRequiredMixin
is incompatible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't intuitive that we respect the login_required
decorator attributes but not the LoginRequiredMixin
attributes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, I also removed the login_url
attribute on the middleware as couldn't find the extra value compared to setting the LOGIN_URL
setting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, right. Yeah, it’s not intuitive that LoginRequiredMixin
wouldn’t be respected. I guess we can add back class-level support for login_url
only..?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redirect_field_name
as well otherwise redirect could be broken after login if it is not the default "next"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel we should either support all the ways login_url
and redirect_field_name
can be updated through LoginRequiredMixin
or not support it.
If we think we need to support it, I can revert the recent changes.
If we think we can keep this as it is but it is not clear in the docs currently I can add a note.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to the release notes that LoginRequiredMixin
is not respected. If users which to use rather than the login_required
decorator, this can be added in a feature release 👍
a6e5420
to
f40a7e5
Compare
d65bd04
to
e208f8a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @sarahboyce, as agreed I went over the code and the docs, I think this is in a very good shape and we can land before feature freeze. I think most of my comments can be addressed, if necessary, in a follow up PR as bug fixes for the feature. I would like to have the missing test (or exception removed from the except block), a definitive check error msg before landing though... would that be doable? Thank you!!! 🌟
79a97df
to
55a37e5
Compare
Co-authored-by: Adam Johnson <me@adamj.eu> Co-authored-by: Mehmet İnce <mehmet@mehmetince.net> Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Ticket: https://code.djangoproject.com/ticket/31405
Mailing list: https://groups.google.com/forum/#!topic/django-developers/PUQQUHIxEXQ
Follows: #12632
Any suggestions are welcome.