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