From 7f1cc4658bb8dfa6db837963003253b6750742ef Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Tue, 24 Dec 2024 01:53:25 +0100 Subject: [PATCH] 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 --- .../InvenTree/InvenTree/api_version.py | 5 +- .../InvenTree/auth_override_views.py | 40 ++++++++ .../InvenTree/InvenTree/auth_overrides.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 + src/backend/requirements.in | 2 +- .../components/forms/AuthenticationForm.tsx | 2 +- 9 files changed, 147 insertions(+), 16 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 31451506b3..563cb55980 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 0000000000..3362c3bf07 --- /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/auth_overrides.py b/src/backend/InvenTree/InvenTree/auth_overrides.py index 16f2aeb7a1..d46097665f 100644 --- a/src/backend/InvenTree/InvenTree/auth_overrides.py +++ b/src/backend/InvenTree/InvenTree/auth_overrides.py @@ -17,7 +17,9 @@ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth_2fa.adapter import OTPAdapter from allauth_2fa.forms import TOTPDeviceForm from allauth_2fa.utils import user_has_valid_totp_device -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 @@ -264,16 +266,11 @@ class CustomSocialAccountAdapter( # 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 30d6cb7358..2a02e6f669 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -585,12 +585,10 @@ REST_AUTH = { 'TOKEN_MODEL': 'users.models.ApiToken', 'TOKEN_CREATOR': 'users.models.default_create_token', 'USE_JWT': USE_JWT, + 'REGISTER_SERIALIZER': 'InvenTree.auth_overrides.RegisterSerializer', } OLD_PASSWORD_FIELD_ENABLED = True -REST_AUTH_REGISTER_SERIALIZERS = { - 'REGISTER_SERIALIZER': 'InvenTree.auth_overrides.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 27660d2502..3f1fe3a5c6 100644 --- a/src/backend/InvenTree/InvenTree/social_auth_urls.py +++ b/src/backend/InvenTree/InvenTree/social_auth_urls.py @@ -18,6 +18,7 @@ from rest_framework.response import Response import InvenTree.sso from common.settings import get_global_setting +from InvenTree.auth_overrides import registration_enabled from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI from InvenTree.serializers import EmptySerializer, InvenTreeModelSerializer @@ -204,7 +205,7 @@ class SocialProviderListView(ListAPI): 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 31481bc09d..bc279fe4a8 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 allauth.socialaccount.models import SocialAccount, SocialLogin from common.models import InvenTreeSetting from InvenTree import sso from InvenTree.auth_overrides import RegistratonMixin +from InvenTree.unit_test import InvenTreeAPITestCase class Dummy: @@ -119,3 +122,90 @@ class TestSsoGroupSync(TransactionTestCase): 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 21e380d5ed..357e3d5cb4 100644 --- a/src/backend/InvenTree/InvenTree/urls.py +++ b/src/backend/InvenTree/InvenTree/urls.py @@ -29,6 +29,7 @@ import plugin.api import report.api import stock.api import users.api +from InvenTree.auth_override_views import CustomRegisterView from plugin.urls import get_plugin_urls from web.urls import api_urls as web_api_urls from web.urls import urlpatterns as platform_urls @@ -113,6 +114,7 @@ apipatterns = [ 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/requirements.in b/src/backend/requirements.in index c1ce6a7aad..9558aaaa74 100644 --- a/src/backend/requirements.in +++ b/src/backend/requirements.in @@ -31,7 +31,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 3a65ff57f9..1cd64f898c 100644 --- a/src/frontend/src/components/forms/AuthenticationForm.tsx +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -198,7 +198,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`,