Skip to content
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

Potential bugfix for AttributeError: 'PasswordResetFromKeyView' object has no attribute 'redirect_field_name' #178

Closed
wants to merge 9 commits into from

Conversation

pbadeer
Copy link
Contributor

@pbadeer pbadeer commented Jul 17, 2023

Getting this error when submitting the change password form after opening a key-based password reset link.

AttributeError: 'PasswordResetFromKeyView' object has no attribute 'redirect_field_name'

I'm using an Adapter that combines the OTPAdapter from this library and InvitationsAdapter from django-invitations, so this issue may or may not be present for others, but the change adds a simple safety check.

Full stack trace:

127.0.0.1 - - [17/Jul/2023 11:50:26] "POST /accounts/password/reset/key/1-set-password/ HTTP/1.1" 500 -
Traceback (most recent call last):
  File "\lib\site-packages\django\contrib\staticfiles\handlers.py", line 76, in __call__
    return self.application(environ, start_response)
  File "\lib\site-packages\django\core\handlers\wsgi.py", line 133, in __call__
    response = self.get_response(request)
  File "\lib\site-packages\django\core\handlers\base.py", line 130, in get_response
    response = self._middleware_chain(request)
  File "\lib\site-packages\django\core\handlers\exception.py", line 49, in inner
    response = response_for_exception(request, exc)
  File "\lib\site-packages\django\core\handlers\exception.py", line 114, in response_for_exception
    response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
  File "\lib\site-packages\django\core\handlers\exception.py", line 149, in handle_uncaught_exception
    return debug.technical_500_response(request, *exc_info)
  File "\lib\site-packages\django_extensions\management\technical_response.py", line 40, in null_technical_500_response
    raise exc_value.with_traceback(tb)
  File "\lib\site-packages\django\core\handlers\exception.py", line 47, in inner
    response = get_response(request)
  File "\lib\site-packages\django\core\handlers\base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "\lib\site-packages\django\views\generic\base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "\lib\site-packages\django\utils\decorators.py", line 43, in _wrapper
    return bound_method(*args, **kwargs)
  File "\lib\site-packages\allauth\decorators.py", line 20, in wrap
    resp = function(request, *args, **kwargs)
  File "\lib\site-packages\allauth\account\views.py", line 763, in dispatch
    return super(PasswordResetFromKeyView, self).dispatch(
  File "\lib\site-packages\django\views\generic\base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "\lib\site-packages\allauth\account\views.py", line 105, in post
    response = self.form_valid(form)
  File "\lib\site-packages\allauth\account\views.py", line 822, in form_valid
    return perform_login(
  File "\lib\site-packages\allauth\account\utils.py", line 168, in perform_login
    response = adapter.pre_login(request, user, **hook_kwargs)
  File "\src\django-allauth-2fa\allauth_2fa\adapter.py", line 33, in pre_login
    redirect_url = self.get_2fa_authenticate_url(request)
  File "\src\django-allauth-2fa\allauth_2fa\adapter.py", line 53, in get_2fa_authenticate_url
    query_params[view.redirect_field_name] = success_url
AttributeError: 'PasswordResetFromKeyView' object has no attribute 'redirect_field_name'

@codecov-commenter
Copy link

codecov-commenter commented Jul 17, 2023

Codecov Report

Merging #178 (13a2177) into main (cdc1602) will increase coverage by 0.10%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main     #178      +/-   ##
==========================================
+ Coverage   92.95%   93.06%   +0.10%     
==========================================
  Files          15       15              
  Lines         568      577       +9     
==========================================
+ Hits          528      537       +9     
  Misses         40       40              
Impacted Files Coverage Δ
allauth_2fa/adapter.py 82.14% <100.00%> (ø)
tests/test_allauth_2fa.py 100.00% <100.00%> (ø)

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@valberg
Copy link
Collaborator

valberg commented Jul 17, 2023

Hi @pbadeer!

Thank you!

Can you maybe write a test case which can prove that this fixes your bug?

@pbadeer
Copy link
Contributor Author

pbadeer commented Jul 17, 2023

I can certainly try! I'm not well versed on pytest and I've never used Hatch so I'm struggling a bit, but I'll throw some time at it.

@pbadeer
Copy link
Contributor Author

pbadeer commented Jul 17, 2023

BTW I got 34 test failures (18 passes) with a new test environment due to:

FAILED tests/test_allauth_2fa.py::test_require_2fa_middleware[tests.adapter.CustomAdapter-True] - django.template.library.InvalidTemplateLibrary: Invalid template library specified. ImportError raised when trying to load 'django_extensions.templatetags...

When I changed the requirements-dev.txt line:
django-extensions==2.2.6
and removed the pin to look like this:
django-extensions all tests passed.

One of the fuller stack traces, they're all the same error:

\\lib\\site-packages\\django_extensions\\templatetags\\__init__.py'>

    def get_package_libraries(pkg):
        """
        Recursively yield template tag libraries defined in submodules of a
        package.
        """
        for entry in walk_packages(pkg.__path__, pkg.__name__ + "."):
            try:
                module = import_module(entry[1])
            except ImportError as e:
>               raise InvalidTemplateLibrary(
                    "Invalid template library specified. ImportError raised when "
                    "trying to load '%s': %s" % (entry[1], e)
                ) from e
E               django.template.library.InvalidTemplateLibrary: Invalid template library specified. ImportError raised when trying to load 'django_extensions.templatetags.widont': cannot import name 'force_text' from 'django.utils.encoding' (\lib\site-packages\django\utils\encoding.py)

\lib\site-packages\django\template\backends\django.py:130: InvalidTemplateLibrary

@pbadeer
Copy link
Contributor Author

pbadeer commented Jul 17, 2023

@valberg Test case added. Let me know if I screwed anything up, apologies in advance!

@pbadeer
Copy link
Contributor Author

pbadeer commented Jul 18, 2023

pre-commit.ci autofix

1 similar comment
@pbadeer
Copy link
Contributor Author

pbadeer commented Jul 18, 2023

pre-commit.ci autofix

Comment on lines 47 to 58
# Add "next" parameter to the URL if possible.
# If the view function smells like a class-based view, we can interrogate it.
if getattr(request.resolver_match.func, "view_class", None):
if getattr(request, "resolver_match", None) and getattr(
request.resolver_match.func,
"view_class",
None,
):
view = request.resolver_match.func.view_class()
view.request = request
success_url = view.get_success_url()
query_params = request.GET.copy()
if success_url:
if success_url and hasattr(view, "redirect_field_name"):
query_params[view.redirect_field_name] = success_url
if query_params:
redirect_url += f"?{urlencode(query_params)}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is getting complex enough to turn it into a separate free utility function, and in that case we can just use try: except:s, something like

def get_next_query_string(request: HttpRequest) -> str | None:
    """
    Get the query string (including the prefix `?`) to redirect to after a successful POST.
    
    If a query string can't be determined, returns None.
    """
    # If the view function smells like a class-based view, we can interrogate it.
    try:
        view = request.resolver_match.func.view_class()
        redirect_field_name = view.redirect_field_name
    except AttributeError:
        # Interrogation failed :(
        return None

    view.request = request
    query_params = request.GET.copy()
    success_url = view.get_success_url()
    if success_url:
        query_params[redirect_field_name] = success_url
    if query_params:
        return f"?{urlencode(query_params)}"
    return None

What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds fine by me :) Then we can test that directly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pbadeer Are you up for implementing the function as @akx suggests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@valberg We had to remove this library from our project so I haven't been keeping eyes on this, sorry. I threw @akx's util into the mix, hopefully this is what you were envisioning.

@uberfresh
Copy link

This PR would be great. I have the same problem and am waiting for a fix 😅.

@pescheck-bram
Copy link

This PR would be great. I have the same problem and am waiting for a fix 😅.

Same here, we are waiting for this to push our code to production.

@violuke
Copy link

violuke commented Sep 25, 2023

Ditto the above, I can confirm this fix has worked for us, so a merge and release would be great 👍 Thank you

@pbadeer
Copy link
Contributor Author

pbadeer commented Nov 13, 2023

pre-commit.ci autofix

@pbadeer pbadeer requested review from valberg and akx November 13, 2023 02:25
pbadeer and others added 9 commits November 13, 2023 14:20
Getting this error when submitting the change password form after opening a key-based password reset link.

AttributeError: 'PasswordResetFromKeyView' object has no attribute 'redirect_field_name'
…ck so that we can use FixtureRequests on OTPAdapter without it crashing.
Add get_next_query_string util from @akx
Use new get_next_query_string util
Fixes for ruff finds:

allauth_2fa/utils.py:40:36: F821 Undefined name `HttpRequest`
allauth_2fa/utils.py:42:89: E501 Line too long (91 > 88 characters)
Comment on lines -49 to 58
if getattr(request.resolver_match.func, "view_class", None):
if get_next_query_string(request):
view = request.resolver_match.func.view_class()
view.request = request
success_url = view.get_success_url()
query_params = request.GET.copy()
if success_url:
if success_url and hasattr(view, "redirect_field_name"):
query_params[view.redirect_field_name] = success_url
if query_params:
redirect_url += f"?{urlencode(query_params)}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Erm, shouldn't this just be

Suggested change
if getattr(request.resolver_match.func, "view_class", None):
if get_next_query_string(request):
view = request.resolver_match.func.view_class()
view.request = request
success_url = view.get_success_url()
query_params = request.GET.copy()
if success_url:
if success_url and hasattr(view, "redirect_field_name"):
query_params[view.redirect_field_name] = success_url
if query_params:
redirect_url += f"?{urlencode(query_params)}"
query_string = get_next_query_string(request)
if query_string:
redirect_url += query_string

now that the logic is refactored..?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to take this over! I don't use this library anymore and was just trying to pass this change forwards since it seems many of your users need it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants