mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Enhancement: support disabling regular login (#5816)
This commit is contained in:
		| @@ -692,3 +692,7 @@ PAPERLESS_SOCIALACCOUNT_PROVIDERS=' | ||||
| ``` | ||||
|  | ||||
| More details about configuration option for various providers can be found in the [allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html#provider-specifics). | ||||
|  | ||||
| ### Disabling Regular Login | ||||
|  | ||||
| Once external auth is set up, 'regular' login can be disabled with the [PAPERLESS_DISABLE_REGULAR_LOGIN](configuration.md#PAPERLESS_DISABLE_REGULAR_LOGIN) setting. | ||||
|   | ||||
| @@ -572,6 +572,12 @@ system. See the corresponding | ||||
|  | ||||
|     Defaults to 'https' | ||||
|  | ||||
| #### [`PAPERLESS_DISABLE_REGULAR_LOGIN=<bool>`](#PAPERLESS_DISABLE_REGULAR_LOGIN) {#PAPERLESS_DISABLE_REGULAR_LOGIN} | ||||
|  | ||||
| : Disables the regular frontend username / password login, i.e. once you have setup SSO. Note that the Django admin login cannot be disabled. | ||||
|  | ||||
|     Defaults to False | ||||
|  | ||||
| ## OCR settings {#ocr} | ||||
|  | ||||
| Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/) | ||||
|   | ||||
| @@ -5,4 +5,5 @@ def settings(request): | ||||
|     return { | ||||
|         "EMAIL_ENABLED": django_settings.EMAIL_HOST != "localhost" | ||||
|         or django_settings.EMAIL_HOST_USER != "", | ||||
|         "DISABLE_REGULAR_LOGIN": django_settings.DISABLE_REGULAR_LOGIN, | ||||
|     } | ||||
|   | ||||
| @@ -58,29 +58,33 @@ | ||||
|           {% translate "Share link has expired." %} | ||||
|         </div> | ||||
| 			{% endif %} | ||||
| 			{% translate "Username" as i18n_username %} | ||||
| 			{% translate "Password" as i18n_password %} | ||||
|       <div class="form-floating"> | ||||
|         <input type="text" name="login" id="inputUsername" placeholder="{{ i18n_username }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus> | ||||
|         <label for="inputUsername">{{ i18n_username }}</label> | ||||
|       </div> | ||||
|       <div class="form-floating"> | ||||
|         <input type="password" name="password" id="inputPassword" placeholder="{{ i18n_password }}" class="form-control" required> | ||||
| 			  <label for="inputPassword">{{ i18n_password }}</label> | ||||
|       </div> | ||||
|       <div class="d-grid mt-3"> | ||||
|         <button class="btn btn-lg btn-primary" type="submit">{% translate "Sign in" %}</button> | ||||
|       </div> | ||||
|       {% if EMAIL_ENABLED %} | ||||
|       <div class="d-grid mt-3"> | ||||
|         <a class="btn btn-link" href="{% url 'account_reset_password' %}">{% translate "Forgot your password?" %}</a> | ||||
|       </div> | ||||
|       {% if not DISABLE_REGULAR_LOGIN %} | ||||
|         {% translate "Username" as i18n_username %} | ||||
|         {% translate "Password" as i18n_password %} | ||||
|         <div class="form-floating"> | ||||
|           <input type="text" name="login" id="inputUsername" placeholder="{{ i18n_username }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus> | ||||
|           <label for="inputUsername">{{ i18n_username }}</label> | ||||
|         </div> | ||||
|         <div class="form-floating"> | ||||
|           <input type="password" name="password" id="inputPassword" placeholder="{{ i18n_password }}" class="form-control" required> | ||||
|           <label for="inputPassword">{{ i18n_password }}</label> | ||||
|         </div> | ||||
|         <div class="d-grid mt-3"> | ||||
|           <button class="btn btn-lg btn-primary" type="submit">{% translate "Sign in" %}</button> | ||||
|         </div> | ||||
|         {% if EMAIL_ENABLED %} | ||||
|         <div class="d-grid mt-3"> | ||||
|           <a class="btn btn-link" href="{% url 'account_reset_password' %}">{% translate "Forgot your password?" %}</a> | ||||
|         </div> | ||||
|         {% endif %} | ||||
|       {% endif %} | ||||
| 		</form> | ||||
| {% load allauth socialaccount %} | ||||
| {% get_providers as socialaccount_providers %} | ||||
| {% if socialaccount_providers %} | ||||
|     <p class="mt-3">{% translate "or sign in via" %}</p> | ||||
|     {% if not DISABLE_REGULAR_LOGIN %} | ||||
|       <p class="mt-3">{% translate "or sign in via" %}</p> | ||||
|     {% endif %} | ||||
|     <ul class="m-0 p-0"> | ||||
|         {% for provider in socialaccount_providers %} | ||||
|             {% if provider.id == "openid" %} | ||||
|   | ||||
| @@ -2,7 +2,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: paperless-ngx\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2024-02-14 16:47-0800\n" | ||||
| "POT-Creation-Date: 2024-02-18 22:27-0800\n" | ||||
| "PO-Revision-Date: 2022-02-17 04:17\n" | ||||
| "Last-Translator: \n" | ||||
| "Language-Team: English\n" | ||||
| @@ -806,24 +806,24 @@ msgstr "" | ||||
| msgid "Share link has expired." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:61 | ||||
| #: documents/templates/account/login.html:62 | ||||
| #: documents/templates/socialaccount/signup.html:56 | ||||
| msgid "Username" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:62 | ||||
| #: documents/templates/account/login.html:63 | ||||
| msgid "Password" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:72 | ||||
| #: documents/templates/account/login.html:73 | ||||
| msgid "Sign in" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:76 | ||||
| #: documents/templates/account/login.html:77 | ||||
| msgid "Forgot your password?" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/templates/account/login.html:83 | ||||
| #: documents/templates/account/login.html:86 | ||||
| msgid "or sign in via" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -1120,131 +1120,131 @@ msgstr "" | ||||
| msgid "paperless application settings" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:642 | ||||
| #: paperless/settings.py:644 | ||||
| msgid "English (US)" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:643 | ||||
| #: paperless/settings.py:645 | ||||
| msgid "Arabic" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:644 | ||||
| #: paperless/settings.py:646 | ||||
| msgid "Afrikaans" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:645 | ||||
| #: paperless/settings.py:647 | ||||
| msgid "Belarusian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:646 | ||||
| #: paperless/settings.py:648 | ||||
| msgid "Bulgarian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:647 | ||||
| #: paperless/settings.py:649 | ||||
| msgid "Catalan" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:648 | ||||
| #: paperless/settings.py:650 | ||||
| msgid "Czech" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:649 | ||||
| #: paperless/settings.py:651 | ||||
| msgid "Danish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:650 | ||||
| #: paperless/settings.py:652 | ||||
| msgid "German" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:651 | ||||
| #: paperless/settings.py:653 | ||||
| msgid "Greek" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:652 | ||||
| #: paperless/settings.py:654 | ||||
| msgid "English (GB)" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:653 | ||||
| #: paperless/settings.py:655 | ||||
| msgid "Spanish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:654 | ||||
| #: paperless/settings.py:656 | ||||
| msgid "Finnish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:655 | ||||
| #: paperless/settings.py:657 | ||||
| msgid "French" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:656 | ||||
| #: paperless/settings.py:658 | ||||
| msgid "Hungarian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:657 | ||||
| #: paperless/settings.py:659 | ||||
| msgid "Italian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:658 | ||||
| #: paperless/settings.py:660 | ||||
| msgid "Japanese" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:659 | ||||
| #: paperless/settings.py:661 | ||||
| msgid "Luxembourgish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:660 | ||||
| #: paperless/settings.py:662 | ||||
| msgid "Norwegian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:661 | ||||
| #: paperless/settings.py:663 | ||||
| msgid "Dutch" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:662 | ||||
| #: paperless/settings.py:664 | ||||
| msgid "Polish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:663 | ||||
| #: paperless/settings.py:665 | ||||
| msgid "Portuguese (Brazil)" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:664 | ||||
| #: paperless/settings.py:666 | ||||
| msgid "Portuguese" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:665 | ||||
| #: paperless/settings.py:667 | ||||
| msgid "Romanian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:666 | ||||
| #: paperless/settings.py:668 | ||||
| msgid "Russian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:667 | ||||
| #: paperless/settings.py:669 | ||||
| msgid "Slovak" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:668 | ||||
| #: paperless/settings.py:670 | ||||
| msgid "Slovenian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:669 | ||||
| #: paperless/settings.py:671 | ||||
| msgid "Serbian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:670 | ||||
| #: paperless/settings.py:672 | ||||
| msgid "Swedish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:671 | ||||
| #: paperless/settings.py:673 | ||||
| msgid "Turkish" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:672 | ||||
| #: paperless/settings.py:674 | ||||
| msgid "Ukrainian" | ||||
| msgstr "" | ||||
|  | ||||
| #: paperless/settings.py:673 | ||||
| #: paperless/settings.py:675 | ||||
| msgid "Chinese Simplified" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,36 @@ from allauth.account.adapter import DefaultAccountAdapter | ||||
| from allauth.core import context | ||||
| from allauth.socialaccount.adapter import DefaultSocialAccountAdapter | ||||
| from django.conf import settings | ||||
| from django.forms import ValidationError | ||||
| from django.urls import reverse | ||||
|  | ||||
|  | ||||
| class CustomAccountAdapter(DefaultAccountAdapter): | ||||
|     def is_open_for_signup(self, request): | ||||
|         """ | ||||
|         Check whether the site is open for signups, which can be | ||||
|         disabled via the ACCOUNT_ALLOW_SIGNUPS setting. | ||||
|         """ | ||||
|         allow_signups = super().is_open_for_signup(request) | ||||
|         # Override with setting, otherwise default to super. | ||||
|         return getattr(settings, "ACCOUNT_ALLOW_SIGNUPS", allow_signups) | ||||
|  | ||||
|     def pre_authenticate(self, request, **credentials): | ||||
|         """ | ||||
|         Called prior to calling the authenticate method on the | ||||
|         authentication backend. If login is disabled using DISABLE_REGULAR_LOGIN, | ||||
|         raise ValidationError to prevent the login. | ||||
|         """ | ||||
|         if settings.DISABLE_REGULAR_LOGIN: | ||||
|             raise ValidationError("Regular login is disabled") | ||||
|  | ||||
|         return super().pre_authenticate(request, **credentials) | ||||
|  | ||||
|     def is_safe_url(self, url): | ||||
|         # see https://github.com/paperless-ngx/paperless-ngx/issues/5780 | ||||
|         """ | ||||
|         Check if the URL is a safe URL. | ||||
|         See https://github.com/paperless-ngx/paperless-ngx/issues/5780 | ||||
|         """ | ||||
|         from django.utils.http import url_has_allowed_host_and_scheme | ||||
|  | ||||
|         # get_host already validates the given host, so no need to check it again | ||||
| @@ -29,6 +48,10 @@ class CustomAccountAdapter(DefaultAccountAdapter): | ||||
|  | ||||
| class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): | ||||
|     def is_open_for_signup(self, request, sociallogin): | ||||
|         """ | ||||
|         Check whether the site is open for signups via social account, which can be | ||||
|         disabled via the SOCIALACCOUNT_ALLOW_SIGNUPS setting. | ||||
|         """ | ||||
|         allow_signups = super().is_open_for_signup(request, sociallogin) | ||||
|         # Override with setting, otherwise default to super. | ||||
|         return getattr(settings, "SOCIALACCOUNT_ALLOW_SIGNUPS", allow_signups) | ||||
| @@ -42,5 +65,9 @@ class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): | ||||
|         return url | ||||
|  | ||||
|     def populate_user(self, request, sociallogin, data): | ||||
|         """ | ||||
|         Populate the user with data from the social account. Stub is kept in case | ||||
|         global default permissions are implemented in the future. | ||||
|         """ | ||||
|         # TODO: If default global permissions are implemented, should also be here | ||||
|         return super().populate_user(request, sociallogin, data)  # pragma: no cover | ||||
|   | ||||
| @@ -437,6 +437,8 @@ SOCIALACCOUNT_PROVIDERS = json.loads( | ||||
|     os.getenv("PAPERLESS_SOCIALACCOUNT_PROVIDERS", "{}"), | ||||
| ) | ||||
|  | ||||
| DISABLE_REGULAR_LOGIN = __get_boolean("PAPERLESS_DISABLE_REGULAR_LOGIN") | ||||
|  | ||||
| AUTO_LOGIN_USERNAME = os.getenv("PAPERLESS_AUTO_LOGIN_USERNAME") | ||||
|  | ||||
| if AUTO_LOGIN_USERNAME: | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from allauth.account.adapter import get_adapter | ||||
| from allauth.core import context | ||||
| from allauth.socialaccount.adapter import get_adapter as get_social_adapter | ||||
| from django.conf import settings | ||||
| from django.forms import ValidationError | ||||
| from django.http import HttpRequest | ||||
| from django.test import TestCase | ||||
| from django.test import override_settings | ||||
| @@ -47,6 +48,19 @@ class TestCustomAccountAdapter(TestCase): | ||||
|             # False because request host is not in allowed hosts | ||||
|             self.assertFalse(adapter.is_safe_url(url)) | ||||
|  | ||||
|     @mock.patch("allauth.core.ratelimit._consume_rate", return_value=True) | ||||
|     def test_pre_authenticate(self, mock_consume_rate): | ||||
|         adapter = get_adapter() | ||||
|         request = HttpRequest() | ||||
|         request.get_host = mock.Mock(return_value="example.com") | ||||
|  | ||||
|         settings.DISABLE_REGULAR_LOGIN = False | ||||
|         adapter.pre_authenticate(request) | ||||
|  | ||||
|         settings.DISABLE_REGULAR_LOGIN = True | ||||
|         with self.assertRaises(ValidationError): | ||||
|             adapter.pre_authenticate(request) | ||||
|  | ||||
|  | ||||
| class TestCustomSocialAccountAdapter(TestCase): | ||||
|     def test_is_open_for_signup(self): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon