From 40245a6c4a408c75844afb1efc045c4f9c40a7d7 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 25 Dec 2024 01:38:02 +0100 Subject: [PATCH] [0.17.x] Fix REST registration endpoint (#8738) (#8763) * Fix REST registration endpoint (#8738) * Re-add html account base Fixes #8690 * fix base template * override dj-rest-auth pattern to fix fixed token model reference * pin req * fix urls.py * move definition out to separate file * fix possible issues where email is not enabled but UI shows that registration is enabled * fix import order * fix token recovery * make sure registration redirects * fix name change * fix import name * adjust description * cleanup * bum api version * add test for registration * add test for registration requirements * fix merge issues * fix merge from https://github.com/inventree/InvenTree/pull/8724 --- .../InvenTree/InvenTree/api_version.py | 5 +- .../InvenTree/auth_override_views.py | 40 ++++++++ src/backend/InvenTree/InvenTree/forms.py | 13 +-- src/backend/InvenTree/InvenTree/settings.py | 4 +- .../InvenTree/InvenTree/social_auth_urls.py | 3 +- .../InvenTree/{test_sso.py => test_auth.py} | 92 ++++++++++++++++++- src/backend/InvenTree/InvenTree/urls.py | 2 + .../InvenTree/templates/account/base.html | 72 ++++++++++++++- src/backend/requirements.in | 2 +- .../components/forms/AuthenticationForm.tsx | 2 +- 10 files changed, 216 insertions(+), 19 deletions(-) create mode 100644 src/backend/InvenTree/InvenTree/auth_override_views.py rename src/backend/InvenTree/InvenTree/{test_sso.py => test_auth.py} (63%) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 31451506b37a..563cb55980de 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 293 +INVENTREE_API_VERSION = 294 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v294 - 2024-12-23 : https://github.com/inventree/InvenTree/pull/8738 + - Extends registration API documentation + v293 - 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658 - Adds new fields to the supplier barcode API endpoints diff --git a/src/backend/InvenTree/InvenTree/auth_override_views.py b/src/backend/InvenTree/InvenTree/auth_override_views.py new file mode 100644 index 000000000000..3362c3bf07de --- /dev/null +++ b/src/backend/InvenTree/InvenTree/auth_override_views.py @@ -0,0 +1,40 @@ +"""Overrides for registration view.""" + +from django.utils.translation import gettext_lazy as _ + +from allauth.account import app_settings as allauth_account_settings +from dj_rest_auth.app_settings import api_settings +from dj_rest_auth.registration.views import RegisterView + + +class CustomRegisterView(RegisterView): + """Registers a new user. + + Accepts the following POST parameters: username, email, password1, password2. + """ + + # Fixes https://github.com/inventree/InvenTree/issues/8707 + # This contains code from dj-rest-auth 7.0 - therefore the version was pinned + def get_response_data(self, user): + """Override to fix check for auth_model.""" + if ( + allauth_account_settings.EMAIL_VERIFICATION + == allauth_account_settings.EmailVerificationMethod.MANDATORY + ): + return {'detail': _('Verification e-mail sent.')} + + if api_settings.USE_JWT: + data = { + 'user': user, + 'access': self.access_token, + 'refresh': self.refresh_token, + } + return api_settings.JWT_SERIALIZER( + data, context=self.get_serializer_context() + ).data + elif self.token_model: + # Only change in this block is below + return api_settings.TOKEN_SERIALIZER( + user.api_tokens.last(), context=self.get_serializer_context() + ).data + return None diff --git a/src/backend/InvenTree/InvenTree/forms.py b/src/backend/InvenTree/InvenTree/forms.py index f2e73b253ee5..f37eafbf6aec 100644 --- a/src/backend/InvenTree/InvenTree/forms.py +++ b/src/backend/InvenTree/InvenTree/forms.py @@ -20,7 +20,9 @@ from crispy_forms.bootstrap import AppendedText, PrependedAppendedText, PrependedText from crispy_forms.helper import FormHelper from crispy_forms.layout import Field, Layout -from dj_rest_auth.registration.serializers import RegisterSerializer +from dj_rest_auth.registration.serializers import ( + RegisterSerializer as DjRestRegisterSerializer, +) from rest_framework import serializers import InvenTree.helpers_model @@ -385,16 +387,11 @@ def authentication_error( # override dj-rest-auth -class CustomRegisterSerializer(RegisterSerializer): - """Override of serializer to use dynamic settings.""" +class RegisterSerializer(DjRestRegisterSerializer): + """Registration requires email, password (twice) and username.""" email = serializers.EmailField() - def __init__(self, instance=None, data=..., **kwargs): - """Check settings to influence which fields are needed.""" - kwargs['email_required'] = get_global_setting('LOGIN_MAIL_REQUIRED') - super().__init__(instance, data, **kwargs) - def save(self, request): """Override to check if registration is open.""" if registration_enabled(): diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 08f7e1c42f83..29d8211f90a4 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -620,12 +620,10 @@ 'TOKEN_MODEL': 'users.models.ApiToken', 'TOKEN_CREATOR': 'users.models.default_create_token', 'USE_JWT': USE_JWT, + 'REGISTER_SERIALIZER': 'InvenTree.forms.RegisterSerializer', } OLD_PASSWORD_FIELD_ENABLED = True -REST_AUTH_REGISTER_SERIALIZERS = { - 'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer' -} # JWT settings - rest_framework_simplejwt if USE_JWT: diff --git a/src/backend/InvenTree/InvenTree/social_auth_urls.py b/src/backend/InvenTree/InvenTree/social_auth_urls.py index a7ad13dd6f62..566809e64529 100644 --- a/src/backend/InvenTree/InvenTree/social_auth_urls.py +++ b/src/backend/InvenTree/InvenTree/social_auth_urls.py @@ -18,6 +18,7 @@ import InvenTree.sso from common.settings import get_global_setting +from InvenTree.forms import registration_enabled from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer @@ -204,7 +205,7 @@ def get(self, request, *args, **kwargs): and get_global_setting('LOGIN_ENFORCE_MFA'), 'mfa_enabled': settings.MFA_ENABLED, 'providers': provider_list, - 'registration_enabled': get_global_setting('LOGIN_ENABLE_REG'), + 'registration_enabled': registration_enabled(), 'password_forgotten_enabled': get_global_setting('LOGIN_ENABLE_PWD_FORGOT'), } return Response(data) diff --git a/src/backend/InvenTree/InvenTree/test_sso.py b/src/backend/InvenTree/InvenTree/test_auth.py similarity index 63% rename from src/backend/InvenTree/InvenTree/test_sso.py rename to src/backend/InvenTree/InvenTree/test_auth.py index e20f11d3ae20..1c734c2badc7 100644 --- a/src/backend/InvenTree/InvenTree/test_sso.py +++ b/src/backend/InvenTree/InvenTree/test_auth.py @@ -1,6 +1,8 @@ -"""Test the sso module functionality.""" +"""Test the sso and auth module functionality.""" +from django.conf import settings from django.contrib.auth.models import Group, User +from django.core.exceptions import ValidationError from django.test import override_settings from django.test.testcases import TransactionTestCase @@ -9,6 +11,7 @@ from common.models import InvenTreeSetting from InvenTree import sso from InvenTree.forms import RegistratonMixin +from InvenTree.unit_test import InvenTreeAPITestCase class Dummy: @@ -119,3 +122,90 @@ def test_sso_group_created_if_not_exists(self): self.assertEqual(Group.objects.filter(name='inventree_group').count(), 0) sso.ensure_sso_groups(None, self.sociallogin) self.assertEqual(Group.objects.filter(name='inventree_group').count(), 1) + + +class EmailSettingsContext: + """Context manager to enable email settings for tests.""" + + def __enter__(self): + """Enable stuff.""" + InvenTreeSetting.set_setting('LOGIN_ENABLE_REG', True) + settings.EMAIL_HOST = 'localhost' + + def __exit__(self, type, value, traceback): + """Exit stuff.""" + InvenTreeSetting.set_setting('LOGIN_ENABLE_REG', False) + settings.EMAIL_HOST = '' + + +class TestAuth(InvenTreeAPITestCase): + """Test authentication functionality.""" + + def email_args(self, user=None, email=None): + """Generate registration arguments.""" + return { + 'username': user or 'user1', + 'email': email or 'test@example.com', + 'password1': '#asdf1234', + 'password2': '#asdf1234', + } + + def test_registration(self): + """Test the registration process.""" + self.logout() + + # Duplicate username + resp = self.post( + '/api/auth/registration/', + self.email_args(user='testuser'), + expected_code=400, + ) + self.assertIn( + 'A user with that username already exists.', resp.data['username'] + ) + + # Registration is disabled + resp = self.post( + '/api/auth/registration/', self.email_args(), expected_code=400 + ) + self.assertIn('Registration is disabled.', resp.data['non_field_errors']) + + # Enable registration - now it should work + with EmailSettingsContext(): + resp = self.post( + '/api/auth/registration/', self.email_args(), expected_code=201 + ) + self.assertIn('key', resp.data) + + def test_registration_email(self): + """Test that LOGIN_SIGNUP_MAIL_RESTRICTION works.""" + self.logout() + + # Check the setting validation is working + with self.assertRaises(ValidationError): + InvenTreeSetting.set_setting( + 'LOGIN_SIGNUP_MAIL_RESTRICTION', 'example.com,inventree.org' + ) + + # Setting setting correctly + correct_setting = '@example.com,@inventree.org' + InvenTreeSetting.set_setting('LOGIN_SIGNUP_MAIL_RESTRICTION', correct_setting) + self.assertEqual( + InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_RESTRICTION'), + correct_setting, + ) + + # Wrong email format + resp = self.post( + '/api/auth/registration/', + self.email_args(email='admin@invenhost.com'), + expected_code=400, + ) + self.assertIn('The provided email domain is not approved.', resp.data['email']) + + # Right format should work + with EmailSettingsContext(): + resp = self.post( + '/api/auth/registration/', self.email_args(), expected_code=201 + ) + self.assertIn('key', resp.data) diff --git a/src/backend/InvenTree/InvenTree/urls.py b/src/backend/InvenTree/InvenTree/urls.py index 43c16a14bf14..4891c372a288 100644 --- a/src/backend/InvenTree/InvenTree/urls.py +++ b/src/backend/InvenTree/InvenTree/urls.py @@ -32,6 +32,7 @@ from build.urls import build_urls from common.urls import common_urls from company.urls import company_urls, manufacturer_part_urls, supplier_part_urls +from InvenTree.auth_override_views import CustomRegisterView from order.urls import order_urls from part.urls import part_urls from plugin.urls import get_plugin_urls @@ -202,6 +203,7 @@ ConfirmEmailView.as_view(), name='account_confirm_email', ), + path('registration/', CustomRegisterView.as_view(), name='rest_register'), path('registration/', include('dj_rest_auth.registration.urls')), path( 'providers/', SocialProviderListView.as_view(), name='social_providers' diff --git a/src/backend/InvenTree/templates/account/base.html b/src/backend/InvenTree/templates/account/base.html index 0e298e8cc0bc..14f92790f445 100644 --- a/src/backend/InvenTree/templates/account/base.html +++ b/src/backend/InvenTree/templates/account/base.html @@ -1,9 +1,48 @@ -{% extends "skeleton.html" %} +{% load static %} {% load i18n %} {% load inventree_extras %} -{% block body %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% inventree_title %} | {% block head_title %}{% endblock head_title %} + +{% block extra_head %} +{% endblock extra_head %} + + +
@@ -34,4 +73,31 @@ {% block extra_body %} {% endblock extra_body %}
-{% endblock body %} + + + + +{% include "third_party_js.html" %} + + + + + diff --git a/src/backend/requirements.in b/src/backend/requirements.in index bb574304733d..685df3c41328 100644 --- a/src/backend/requirements.in +++ b/src/backend/requirements.in @@ -34,7 +34,7 @@ django-weasyprint # django weasyprint integration djangorestframework<3.15 # DRF framework # FIXED 2024-06-26 see https://github.com/inventree/InvenTree/pull/7521 djangorestframework-simplejwt[crypto] # JWT authentication django-xforwardedfor-middleware # IP forwarding metadata -dj-rest-auth # Authentication API endpoints +dj-rest-auth==7.0.0 # Authentication API endpoints # FIXED 2024-12-22 due to https://github.com/inventree/InvenTree/issues/8707 dulwich # pure Python git integration drf-spectacular # DRF API documentation feedparser # RSS newsfeed parser diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx index 5ae0e5f0dbcd..0df8c73efd55 100644 --- a/src/frontend/src/components/forms/AuthenticationForm.tsx +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -192,7 +192,7 @@ export function RegistrationForm() { headers: { Authorization: '' } }) .then((ret) => { - if (ret?.status === 204) { + if (ret?.status === 204 || ret?.status === 201) { setIsRegistering(false); showLoginNotification({ title: t`Registration successful`,