From eaf1a4baec8e5f7fc6d23148e8a47ed7604829ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Oct 2021 11:41:45 +0200 Subject: [PATCH] enforce mfa on all frontend pages --- InvenTree/InvenTree/middleware.py | 20 +++++++++++++- InvenTree/InvenTree/settings.py | 3 ++- InvenTree/InvenTree/urls.py | 45 ++++++++++++++++++------------- InvenTree/InvenTree/views.py | 9 +++++++ 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index 2df90bc5b7..089c178d9f 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -1,12 +1,17 @@ from django.shortcuts import HttpResponseRedirect -from django.urls import reverse_lazy +from django.urls import reverse_lazy, Resolver404 from django.db import connection from django.shortcuts import redirect +from django.conf.urls import include, url import logging import time import operator from rest_framework.authtoken.models import Token +from allauth_2fa.middleware import BaseRequire2FAMiddleware + +from InvenTree.urls import frontendpatterns + logger = logging.getLogger("inventree") @@ -146,3 +151,16 @@ class QueryCountMiddleware(object): print(x[0], ':', x[1]) return response + + +url_matcher = url('', include(frontendpatterns)) + +class Check2FAMiddleware(BaseRequire2FAMiddleware): + def require_2fa(self, request): + # Superusers are require to have 2FA. + try: + if url_matcher.resolve(request.path[1:]): + return True + except Resolver404: + pass + return False diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index f111629bc9..1985ecce65 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -304,7 +304,8 @@ MIDDLEWARE = CONFIG.get('middleware', [ 'allauth_2fa.middleware.AllauthTwoFactorMiddleware', # Flow control for allauth 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'InvenTree.middleware.AuthRequiredMiddleware' + 'InvenTree.middleware.AuthRequiredMiddleware', + 'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA ]) # Error reporting middleware diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 9bc4ac8360..01baa20e03 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -37,7 +37,7 @@ from rest_framework.documentation import include_docs_urls from .views import auth_request from .views import IndexView, SearchView, DatabaseStatsView -from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView +from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView, CustomTwoFactorAuthenticate from .views import CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView @@ -122,15 +122,29 @@ translated_javascript_urls = [ url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'), ] -urlpatterns = [ - url(r'^part/', include(part_urls)), - url(r'^manufacturer-part/', include(manufacturer_part_urls)), - url(r'^supplier-part/', include(supplier_part_urls)), - +backendpatterns = [ # "Dynamic" javascript files which are rendered using InvenTree templating. url(r'^js/dynamic/', include(dynamic_javascript_urls)), url(r'^js/i18n/', include(translated_javascript_urls)), + url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), + url(r'^auth/?', auth_request), + + url(r'^admin/error_log/', include('error_report.urls')), + url(r'^admin/shell/', include('django_admin_shell.urls')), + url(r'^admin/', admin.site.urls, name='inventree-admin'), + + url(r'^api/', include(apipatterns)), + url(r'^api-doc/', include_docs_urls(title='InvenTree API')), + + url(r'^markdownx/', include('markdownx.urls')), +] + +frontendpatterns = [ + url(r'^part/', include(part_urls)), + url(r'^manufacturer-part/', include(manufacturer_part_urls)), + url(r'^supplier-part/', include(supplier_part_urls)), + url(r'^common/', include(common_urls)), url(r'^stock/', include(stock_urls)), @@ -140,37 +154,30 @@ urlpatterns = [ url(r'^build/', include(build_urls)), - url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), - url(r'^settings/', include(settings_urls)), url(r'^edit-user/', EditUserView.as_view(), name='edit-user'), url(r'^set-password/', SetPasswordView.as_view(), name='set-password'), - url(r'^admin/error_log/', include('error_report.urls')), - url(r'^admin/shell/', include('django_admin_shell.urls')), - url(r'^admin/', admin.site.urls, name='inventree-admin'), - url(r'^index/', IndexView.as_view(), name='index'), url(r'^search/', SearchView.as_view(), name='search'), url(r'^stats/', DatabaseStatsView.as_view(), name='stats'), - url(r'^auth/?', auth_request), - - url(r'^api/', include(apipatterns)), - url(r'^api-doc/', include_docs_urls(title='InvenTree API')), - - url(r'^markdownx/', include('markdownx.urls')), - # Single Sign On / allauth # overrides of urlpatterns url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'), url(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'), url(r"^accounts/password/reset/key/(?P[0-9A-Za-z]+)-(?P.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"), + url(r"^accounts/two-factor-authenticate/?$", CustomTwoFactorAuthenticate.as_view(), name="two-factor-authenticate"), url(r'^accounts/', include('allauth_2fa.urls')), # MFA support url(r'^accounts/', include('allauth.urls')), # included urlpatterns ] +urlpatterns = [ + url('', include(frontendpatterns)), + url('', include(backendpatterns)), +] + # Server running in "DEBUG" mode? if settings.DEBUG: # Static file access diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index af97877933..d1d3ca7436 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -29,6 +29,7 @@ from allauth.socialaccount.forms import DisconnectForm from allauth.account.models import EmailAddress from allauth.account.views import EmailView, PasswordResetFromKeyView from allauth.socialaccount.views import ConnectionsView +from allauth_2fa.views import TwoFactorAuthenticate from common.settings import currency_code_default, currency_codes @@ -857,6 +858,14 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView): success_url = reverse_lazy("account_login") +class CustomTwoFactorAuthenticate(TwoFactorAuthenticate): + def dispatch(self, request, *args, **kwargs): + if 'allauth_2fa_user_id' not in request.session and 'otp_token' not in request.POST: + return redirect('account_login') + if hasattr(request.user, 'id'): + request.session['allauth_2fa_user_id'] = request.user.id + return super(FormView, self).dispatch(request, *args, **kwargs) + class CurrencyRefreshView(RedirectView): """ POST endpoint to refresh / update exchange rates