Skip to content

Commit

Permalink
fix(admin): improve second factor behaviour in admin login
Browse files Browse the repository at this point in the history
Completely override Django logic as we want to divegre here.
  • Loading branch information
nijel committed Oct 21, 2024
1 parent 99c4dc0 commit 8892165
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 25 deletions.
52 changes: 27 additions & 25 deletions weblate/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,33 @@ def redirect_single(request: AuthenticatedHttpRequest, backend: str):
)


class WeblateLoginView(LoginView):
class BaseLoginView(LoginView):
def form_invalid(self, form):
rotate_token(self.request)
return super().form_invalid(form)

def form_valid(self, form):
"""Security check complete. Log the user in."""
user = form.get_user()
if user.profile.has_2fa:
# Store session indication for second factor
self.request.session[SESSION_SECOND_FACTOR_USER] = (user.id, user.backend)
# Redirect to second factor login
redirect_to = self.request.POST.get(
self.redirect_field_name, self.request.GET.get(self.redirect_field_name)
)
login_params: dict[str, str] = {}
if redirect_to:
login_params[self.redirect_field_name] = redirect_to
login_url = reverse(
"2fa-login", kwargs={"backend": user.profile.get_second_factor_type()}
)
return HttpResponseRedirect(f"{login_url}?{urlencode(login_params)}")
auth_login(self.request, user)
return HttpResponseRedirect(self.get_success_url())


class WeblateLoginView(BaseLoginView):
"""Login handler, just a wrapper around standard Django login."""

form_class = LoginForm # type: ignore[assignment]
Expand Down Expand Up @@ -818,30 +844,6 @@ def dispatch(self, request: AuthenticatedHttpRequest, *args, **kwargs): # type:

return super().dispatch(request, *args, **kwargs)

def form_invalid(self, form):
rotate_token(self.request)
return super().form_invalid(form)

def form_valid(self, form):
"""Security check complete. Log the user in."""
user = form.get_user()
if user.profile.has_2fa:
# Store session indication for second factor
self.request.session[SESSION_SECOND_FACTOR_USER] = (user.id, user.backend)
# Redirect to second factor login
redirect_to = self.request.POST.get(
self.redirect_field_name, self.request.GET.get(self.redirect_field_name)
)
login_params: dict[str, str] = {}
if redirect_to:
login_params[self.redirect_field_name] = redirect_to
login_url = reverse(
"2fa-login", kwargs={"backend": user.profile.get_second_factor_type()}
)
return HttpResponseRedirect(f"{login_url}?{urlencode(login_params)}")
auth_login(self.request, user)
return HttpResponseRedirect(self.get_success_url())


class WeblateLogoutView(TemplateView):
"""
Expand Down
45 changes: 45 additions & 0 deletions weblate/wladmin/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
from django.conf import settings
from django.contrib import admin
from django.contrib.admin import AdminSite, sites
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext, gettext_lazy
from django.views.decorators.cache import never_cache

if TYPE_CHECKING:
from weblate.auth.models import AuthenticatedHttpRequest
Expand All @@ -32,6 +37,46 @@ def logout(self, request, extra_context=None):

return WeblateLogoutView.as_view()(request)

@method_decorator(never_cache)
def login(self, request, extra_context=None):
"""
Display the login form for the given HttpRequest.
Essentially a copy of django.contrib.admin.sites.AdminSite.login
"""
# Since this module gets imported in the application's root package,
# it cannot import models from other applications at the module level,
# and django.contrib.admin.forms eventually imports User.

from weblate.accounts.views import BaseLoginView

if request.method == "GET" and self.has_permission(request):
# Already logged-in, redirect to admin index
index_path = reverse("admin:index", current_app=self.name)
return HttpResponseRedirect(index_path)

context = {
**self.each_context(request),
"title": gettext("Sign in"),
"subtitle": None,
"app_path": request.get_full_path(),
"username": request.user.get_username(),
}
if (
REDIRECT_FIELD_NAME not in request.GET
and REDIRECT_FIELD_NAME not in request.POST
):
context[REDIRECT_FIELD_NAME] = reverse("admin:index", current_app=self.name)
context.update(extra_context or {})

defaults = {
"extra_context": context,
"authentication_form": self.login_form,
"template_name": self.login_template or "admin/login.html",
}
request.current_app = self.name
return BaseLoginView.as_view(**defaults)(request)

@property
def site_url(self):
if settings.URL_PREFIX:
Expand Down

0 comments on commit 8892165

Please sign in to comment.