Initial mfa support (for internal users) #93591
@ -1,7 +1,6 @@
|
||||
from django.conf import settings
|
||||
from django.urls import reverse_lazy, path, re_path
|
||||
from django.contrib.auth import views as auth_views
|
||||
from otp_agents.decorators import otp_required
|
||||
|
||||
from . import forms
|
||||
from .views import normal_pages, registration_email, json_api, developer_applications
|
||||
@ -149,11 +148,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
'mfa/',
|
||||
otp_required(
|
||||
accept_trusted_agent=True,
|
||||
if_configured=True,
|
||||
view=normal_pages.MfaView.as_view(),
|
||||
),
|
||||
normal_pages.MfaView.as_view(),
|
||||
name='mfa',
|
||||
),
|
||||
]
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""Pages for displaying and editing OAuth applications."""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.forms import inlineformset_factory
|
||||
from django.urls import reverse
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import UpdateView
|
||||
|
||||
import bid_main.forms
|
||||
from bid_main.views import mixins
|
||||
import bid_api.forms
|
||||
import bid_api.models
|
||||
import bid_main.forms
|
||||
import bid_main.models
|
||||
|
||||
WebhookFormSet = inlineformset_factory(
|
||||
@ -28,14 +28,21 @@ class _OwnedOAuth2ApplicationsMixin:
|
||||
return self.request.user.bid_main_oauth2application
|
||||
|
||||
|
||||
class ListApplicationsView(LoginRequiredMixin, _OwnedOAuth2ApplicationsMixin, ListView):
|
||||
class ListApplicationsView(
|
||||
mixins.MfaRequiredIfConfiguredMixin,
|
||||
_OwnedOAuth2ApplicationsMixin,
|
||||
ListView,
|
||||
):
|
||||
"""List all OAuth 2 applications that currently logged in user is allowed to manage."""
|
||||
|
||||
template_name = 'bid_main/developer_applications.html'
|
||||
|
||||
|
||||
class EditApplicationView(
|
||||
LoginRequiredMixin, SuccessMessageMixin, _OwnedOAuth2ApplicationsMixin, UpdateView
|
||||
mixins.MfaRequiredIfConfiguredMixin,
|
||||
SuccessMessageMixin,
|
||||
_OwnedOAuth2ApplicationsMixin,
|
||||
UpdateView,
|
||||
):
|
||||
"""Edit an OAuth 2 application, if allowed."""
|
||||
|
||||
|
@ -7,17 +7,17 @@ than via bearer tokens.
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.generic import View
|
||||
|
||||
from .. import models
|
||||
from bid_main.views import mixins
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BadgeTogglePrivateView(LoginRequiredMixin, View):
|
||||
class BadgeTogglePrivateView(mixins.MfaRequiredIfConfiguredMixin, View):
|
||||
"""JSON endpoint that toggles 'is_private' flag for badges."""
|
||||
|
||||
def post(self, request, *args, **kwargs) -> JsonResponse:
|
||||
|
@ -2,7 +2,9 @@
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.urls import reverse_lazy
|
||||
from otp_agents.decorators import otp_required
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -35,3 +37,13 @@ class RedirectToPrivacyAgreeMixin:
|
||||
redirect_to = f"{self.privacy_policy_agree_url}?{next_url_qs}"
|
||||
log.debug("Directing user to %s", redirect_to)
|
||||
return redirect_to
|
||||
|
||||
|
||||
class MfaRequiredIfConfiguredMixin(AccessMixin):
|
||||
"""Use this mixin instead of LoginRequiredMixin for bid_main.views."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
decorator = otp_required(accept_trusted_agent=True, if_configured=True)
|
||||
return decorator(super().dispatch)(request, *args, **kwargs)
|
||||
|
@ -9,7 +9,6 @@ import urllib.parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import views as auth_views, logout, get_user_model
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext as _
|
||||
from django.db import transaction, IntegrityError
|
||||
@ -38,7 +37,7 @@ User = get_user_model()
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(LoginRequiredMixin, mixins.PageIdMixin, TemplateView):
|
||||
class IndexView(mixins.MfaRequiredIfConfiguredMixin, mixins.PageIdMixin, TemplateView):
|
||||
page_id = "index"
|
||||
template_name = "bid_main/index.html"
|
||||
login_url = reverse_lazy("bid_main:login")
|
||||
@ -230,7 +229,7 @@ class LogoutView(auth_views.LogoutView):
|
||||
return next_url
|
||||
|
||||
|
||||
class ProfileView(LoginRequiredMixin, UpdateView):
|
||||
class ProfileView(mixins.MfaRequiredIfConfiguredMixin, UpdateView):
|
||||
form_class = forms.UserProfileForm
|
||||
model = User
|
||||
template_name = "bid_main/profile.html"
|
||||
@ -271,7 +270,11 @@ class ProfileView(LoginRequiredMixin, UpdateView):
|
||||
return success_resp
|
||||
|
||||
|
||||
class SwitchUserView(mixins.RedirectToPrivacyAgreeMixin, LoginRequiredMixin, auth_views.LoginView):
|
||||
class SwitchUserView(
|
||||
mixins.RedirectToPrivacyAgreeMixin,
|
||||
mixins.MfaRequiredIfConfiguredMixin,
|
||||
auth_views.LoginView,
|
||||
):
|
||||
template_name = "bid_main/switch_user.html"
|
||||
form_class = forms.AuthenticationForm
|
||||
success_url_allowed_hosts = settings.NEXT_REDIR_AFTER_LOGIN_ALLOWED_HOSTS
|
||||
@ -303,7 +306,12 @@ class GetAppsMixin:
|
||||
return app_model.objects.filter(id__in=app_ids).order_by("name")
|
||||
|
||||
|
||||
class ApplicationTokenView(mixins.PageIdMixin, LoginRequiredMixin, GetAppsMixin, FormView):
|
||||
class ApplicationTokenView(
|
||||
mixins.PageIdMixin,
|
||||
mixins.MfaRequiredIfConfiguredMixin,
|
||||
GetAppsMixin,
|
||||
FormView,
|
||||
):
|
||||
page_id = "auth_tokens"
|
||||
template_name = "bid_main/auth_tokens.html"
|
||||
form_class = forms.AppRevokeTokensForm
|
||||
@ -333,7 +341,7 @@ class ApplicationTokenView(mixins.PageIdMixin, LoginRequiredMixin, GetAppsMixin,
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class PrivacyPolicyAgreeView(mixins.PageIdMixin, LoginRequiredMixin, FormView):
|
||||
class PrivacyPolicyAgreeView(mixins.PageIdMixin, mixins.MfaRequiredIfConfiguredMixin, FormView):
|
||||
page_id = "privacy_policy_agree"
|
||||
template_name = "bid_main/privacy_policy_agree.html"
|
||||
form_class = forms.PrivacyPolicyAgreeForm
|
||||
@ -374,7 +382,7 @@ class PrivacyPolicyAgreeView(mixins.PageIdMixin, LoginRequiredMixin, FormView):
|
||||
|
||||
|
||||
class DeleteUserView(
|
||||
mixins.RedirectToPrivacyAgreeMixin, LoginRequiredMixin, GetAppsMixin, FormView
|
||||
mixins.RedirectToPrivacyAgreeMixin, mixins.MfaRequiredIfConfiguredMixin, GetAppsMixin, FormView
|
||||
):
|
||||
template_name = "bid_main/delete_user.html"
|
||||
form_class = forms.DeleteForm
|
||||
@ -420,7 +428,7 @@ class DeleteUserView(
|
||||
return render(self.request, "bid_main/delete_user/confirm.html", context=ctx)
|
||||
|
||||
|
||||
class ActiveSessionsView(LoginRequiredMixin, TemplateView):
|
||||
class ActiveSessionsView(mixins.MfaRequiredIfConfiguredMixin, TemplateView):
|
||||
template_name = "bid_main/active_sessions.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -434,7 +442,7 @@ class ActiveSessionsView(LoginRequiredMixin, TemplateView):
|
||||
}
|
||||
|
||||
|
||||
class TerminateSessionView(LoginRequiredMixin, View):
|
||||
class TerminateSessionView(mixins.MfaRequiredIfConfiguredMixin, View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
user_session_pk = kwargs.get('pk')
|
||||
if user_session := self.request.user.sessions.filter(pk=user_session_pk).first():
|
||||
@ -443,7 +451,7 @@ class TerminateSessionView(LoginRequiredMixin, View):
|
||||
return HttpResponseNotFound("session not found")
|
||||
|
||||
|
||||
class MfaView(LoginRequiredMixin, TemplateView):
|
||||
class MfaView(mixins.MfaRequiredIfConfiguredMixin, TemplateView):
|
||||
template_name = "bid_main/mfa_setup.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -3,7 +3,6 @@ import logging
|
||||
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
@ -14,6 +13,7 @@ from django.utils import timezone
|
||||
from django.views.generic import CreateView, TemplateView, FormView, View
|
||||
|
||||
from .. import forms, email
|
||||
from . import mixins
|
||||
from ..models import User
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -91,7 +91,7 @@ class InitialSetPasswordView(auth_views.PasswordResetConfirmView):
|
||||
form_class = forms.SetInitialPasswordForm
|
||||
|
||||
|
||||
class ConfirmEmailView(LoginRequiredMixin, FormView):
|
||||
class ConfirmEmailView(mixins.MfaRequiredIfConfiguredMixin, FormView):
|
||||
template_name = "bid_main/confirm_email/start.html"
|
||||
form_class = forms.ConfirmEmailStartForm
|
||||
log = logging.getLogger(f"{__name__}.ConfirmEmailView")
|
||||
@ -127,7 +127,7 @@ class ConfirmEmailView(LoginRequiredMixin, FormView):
|
||||
return redirect("bid_main:confirm-email-sent")
|
||||
|
||||
|
||||
class CancelEmailChangeView(LoginRequiredMixin, View):
|
||||
class CancelEmailChangeView(mixins.MfaRequiredIfConfiguredMixin, View):
|
||||
"""Cancel the user's email change and redirect to the profile page."""
|
||||
|
||||
log = logging.getLogger(f"{__name__}.CancelEmailChangeView")
|
||||
@ -142,11 +142,11 @@ class CancelEmailChangeView(LoginRequiredMixin, View):
|
||||
return redirect("bid_main:index")
|
||||
|
||||
|
||||
class ConfirmEmailSentView(LoginRequiredMixin, TemplateView):
|
||||
class ConfirmEmailSentView(mixins.MfaRequiredIfConfiguredMixin, TemplateView):
|
||||
template_name = "bid_main/confirm_email/sent.html"
|
||||
|
||||
|
||||
class ConfirmEmailPollView(LoginRequiredMixin, View):
|
||||
class ConfirmEmailPollView(mixins.MfaRequiredIfConfiguredMixin, View):
|
||||
"""Returns JSON indicating when the email address has last been confirmed.
|
||||
|
||||
The timestamp is returned as ISO 8601 to allow future periodic checks
|
||||
@ -167,7 +167,7 @@ class ConfirmEmailPollView(LoginRequiredMixin, View):
|
||||
return JsonResponse({"confirmed": timestamp})
|
||||
|
||||
|
||||
class ConfirmEmailVerifiedView(LoginRequiredMixin, TemplateView):
|
||||
class ConfirmEmailVerifiedView(mixins.MfaRequiredIfConfiguredMixin, TemplateView):
|
||||
"""Render explanation on GET, handle confirmation on POST.
|
||||
|
||||
We only perform the actual database change on a POST, since that protects
|
||||
|
Loading…
Reference in New Issue
Block a user