From 09499a6bfd3990b8d441c9eb82be250f0bca9606 Mon Sep 17 00:00:00 2001 From: sandeepsajan0 <sandeepsajan0@gmail.com> Date: Thu, 5 May 2022 17:51:34 +0530 Subject: [PATCH] Add feature in wagtail admin to reset 2fa for a user --- .../users/templates/two_factor/reset.html | 31 ++++++ .../templates/wagtailusers/users/edit.html | 96 ++++++++++++++----- hypha/apply/users/templatetags/users_tags.py | 10 ++ hypha/apply/users/urls.py | 2 + hypha/apply/users/views.py | 42 +++++++- 5 files changed, 155 insertions(+), 26 deletions(-) create mode 100644 hypha/apply/users/templates/two_factor/reset.html diff --git a/hypha/apply/users/templates/two_factor/reset.html b/hypha/apply/users/templates/two_factor/reset.html new file mode 100644 index 000000000..a3fc26d46 --- /dev/null +++ b/hypha/apply/users/templates/two_factor/reset.html @@ -0,0 +1,31 @@ +<!-- It is a custom template built on the top of wagtail base, to confirm 2FA disable process with the user's Password --> +{% extends "wagtailadmin/base.html" %} +{% load i18n static wagtailadmin_tags %} + +{% block content %} + + {% trans "Disabling 2FA" as editing_str %} + {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=user.get_username merged=1 tabbed=1 icon="user" %} + + <form class="form" action="" method="POST" novalidate> + <div class="tab-content"> + {% csrf_token %} + + <section id="account" class="active nice-padding"> + <p> Are you sure you want to disable the Two Factor Authentication for this user? Please type your password to confirm.</p> + + <ul class="fields"> + {% block fields %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.password %} + {% endblock %} + + <li> + <button class="button button--primary" type="submit">{% trans 'Disable 2FA' %}</button> + </li> + </ul> + </section> + </div> + </form> + + +{% endblock %} diff --git a/hypha/apply/users/templates/wagtailusers/users/edit.html b/hypha/apply/users/templates/wagtailusers/users/edit.html index 285659a35..e71b10897 100644 --- a/hypha/apply/users/templates/wagtailusers/users/edit.html +++ b/hypha/apply/users/templates/wagtailusers/users/edit.html @@ -1,26 +1,74 @@ +<!-- Override the Wagtail's user edit template to add a custom 'Disable 2FA' button to account section--> {% extends "wagtailusers/users/edit.html" %} +{% load i18n wagtailimages_tags users_tags %} +{% block content %} -{% block fields %} - {% if form.separate_username_field %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %} - {% endif %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.full_name %} - - {% comment %} - First/last name hidden input with dummy values because.. Wagtail admin - See hypha.apply.users.forms.CustomUserEditForm - {% endcomment %} - {{ form.first_name }} - {{ form.last_name }} - - {% if form.password1 %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %} - {% endif %} - {% if form.password2 %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %} - {% endif %} - {% if form.is_active %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %} - {% endif %} -{% endblock fields %} + {% trans "Editing" as editing_str %} + {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=user.get_username merged=1 tabbed=1 icon="user" %} + + <ul class="tab-nav merged" data-tab-nav> + <li class="active"><a href="#account">{% trans "Account" %}</a></li> + <li><a href="#roles">{% trans "Roles" %}</a></li> + </ul> + + <form action="{% url 'wagtailusers_users:edit' user.pk %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}> + <div class="tab-content"> + {% csrf_token %} + + <section id="account" class="active nice-padding"> + <ul class="fields"> + {% block fields %} + <!-- Block Fields are overridden to show fields as per the requirement --> + {% if form.separate_username_field %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %} + {% endif %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.full_name %} + + {% comment %} + First/last name hidden input with dummy values because.. Wagtail admin + See hypha.apply.users.forms.CustomUserEditForm + {% endcomment %} + {{ form.first_name }} + {{ form.last_name }} + + {% if form.password1 %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %} + {% endif %} + {% if form.password2 %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %} + {% endif %} + {% if form.is_active %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %} + {% endif %} + {% endblock fields %} + + <li> + <input type="submit" value="{% trans 'Save' %}" class="button" /> + {% if can_delete %} + <a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a> + {% endif %} + <!-- Add a custom button to user account edit form --> + {% user_2fa_enabled as is_2fa_enabled %} + <a href="{% if is_2fa_enabled %}{% url 'users:two_factor_reset' user.pk %}{% else %}javascript:void(0){% endif %}" class="button {% if not is_2fa_enabled %}disabled{% endif %}">{% trans "Disable 2FA" %}</a> + </li> + </ul> + </section> + <section id="roles" class="nice-padding"> + <ul class="fields"> + {% if form.is_superuser %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_superuser %} + {% endif %} + + {% include "wagtailadmin/shared/field_as_li.html" with field=form.groups %} + <li> + <input type="submit" value="{% trans 'Save' %}" class="button" /> + {% if can_delete %} + <a href="{% url 'wagtailusers_users:delete' user.pk %}" class="button button-secondary no">{% trans "Delete user" %}</a> + {% endif %} + </li> + </ul> + </section> + </div> + </form> +{% endblock %} diff --git a/hypha/apply/users/templatetags/users_tags.py b/hypha/apply/users/templatetags/users_tags.py index a3e8b5432..06d353d10 100644 --- a/hypha/apply/users/templatetags/users_tags.py +++ b/hypha/apply/users/templatetags/users_tags.py @@ -1,4 +1,5 @@ from django import template +from django_otp import devices_for_user from ..utils import can_use_oauth_check @@ -25,3 +26,12 @@ def can_use_oauth(context): user = context.get('user') return can_use_oauth_check(user) + + +@register.simple_tag(takes_context=True) +def user_2fa_enabled(context): + """Checking if 2FA devices exist for the user""" + user = context.get('user') + if len(list(devices_for_user(user))): + return True + return False diff --git a/hypha/apply/users/urls.py b/hypha/apply/users/urls.py index 53273023e..4c0728d4e 100644 --- a/hypha/apply/users/urls.py +++ b/hypha/apply/users/urls.py @@ -12,6 +12,7 @@ from .views import ( TWOFABackupTokensPasswordView, TWOFADisableView, TWOFARequiredMessageView, + TWOFAResetView, become, create_password, oauth, @@ -88,6 +89,7 @@ urlpatterns = [ path('two_factor/required/', TWOFARequiredMessageView.as_view(), name='two_factor_required'), path('two_factor/backup_tokens/password/', TWOFABackupTokensPasswordView.as_view(), name='backup_tokens_password'), path('two_factor/disable/', TWOFADisableView.as_view(), name='disable'), + path('two_factor/reset/<str:user_id>/', TWOFAResetView.as_view(), name='two_factor_reset'), path('confirmation/done/', EmailChangeDoneView.as_view(), name="confirm_link_sent"), path('confirmation/<uidb64>/<token>/', EmailChangeConfirmationView.as_view(), name="confirm_email"), path('activate/', create_password, name="activate_password"), diff --git a/hypha/apply/users/views.py b/hypha/apply/users/views.py index 1cf30f527..bf2472049 100644 --- a/hypha/apply/users/views.py +++ b/hypha/apply/users/views.py @@ -4,7 +4,7 @@ from urllib.parse import urlencode from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model, login, update_session_auth_hash -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.forms import AdminPasswordChangeForm from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.core.exceptions import PermissionDenied @@ -16,15 +16,17 @@ from django.utils.decorators import method_decorator from django.utils.encoding import force_str from django.utils.http import urlsafe_base64_decode from django.utils.translation import gettext_lazy as _ -from django.views.generic import UpdateView +from django.views.generic import UpdateView, View from django.views.generic.base import TemplateView from django.views.generic.edit import FormView +from django_otp import devices_for_user from hijack.views import AcquireUserView from two_factor.forms import AuthenticationTokenForm, BackupTokenForm from two_factor.views import DisableView as TwoFactorDisableView from two_factor.views import LoginView as TwoFactorLoginView from wagtail.admin.views.account import password_management_enabled from wagtail.core.models import Site +from wagtail.users.views.users import change_user_perm from hypha.apply.home.models import ApplyHomePage @@ -309,5 +311,41 @@ class TWOFADisableView(TwoFactorDisableView): return kwargs +@method_decorator(permission_required(change_user_perm, raise_exception=True), name='dispatch') +class TWOFAResetView(FormView): + """ + View for PasswordForm to confirm the Disable 2FA process. + """ + form_class = TWOFAPasswordForm + template_name = 'two_factor/reset.html' + user = None + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + # pass request's user to form to validate the password + kwargs['user'] = self.request.user + # store the user from url for redirecting to the same user's account edit page + self.user = get_object_or_404(User, pk=self.kwargs.get('user_id')) + return kwargs + + def get_form(self, form_class=None): + form = super(TWOFAResetView, self).get_form(form_class=form_class) + form.fields['password'].label = "Password" + return form + + def form_valid(self, form): + for device in devices_for_user(self.user): + device.delete() + return redirect(self.get_success_url()) + + def get_success_url(self): + return reverse('wagtailusers_users:edit', kwargs={'user_id': self.user.id}) + + def get_context_data(self, **kwargs): + ctx = super(TWOFAResetView, self).get_context_data(**kwargs) + ctx['user'] = self.user + return ctx + + class TWOFARequiredMessageView(TemplateView): template_name = 'two_factor/core/two_factor_required.html' -- GitLab