]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: support disabling regular login (#5816)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Mon, 26 Feb 2024 05:17:21 +0000 (21:17 -0800)
committerGitHub <noreply@github.com>
Mon, 26 Feb 2024 05:17:21 +0000 (05:17 +0000)
docs/advanced_usage.md
docs/configuration.md
src/documents/context_processors.py
src/documents/templates/account/login.html
src/locale/en_US/LC_MESSAGES/django.po
src/paperless/adapter.py
src/paperless/settings.py
src/paperless/tests/test_adapter.py

index 7ca854f566dd36d26e7fa602d08af4b8cdc3a984..d4ff80f8783b47bb5c2162f08980b4f19a942dfb 100644 (file)
@@ -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.
index ce85599a5da4bf6afd798a7a3f27aed6a7ddcbaa..43eff582bfc70d6a905ec0cedd7ce00d1f5e91b7 100644 (file)
@@ -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/)
index 90c856aeb9dc8554dd8d13c409cf1a45d99fb4b2..b58c77268122cdafcb6ea6debfe853eee95dcaa6 100644 (file)
@@ -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,
     }
index 777f65409cb6cb5fa5d8f8413d67ed5991e82a6a..0dd8d8e1b5ba934ecf0783eafaf54168b93b7efb 100644 (file)
           {% 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" %}
index bfc3a2069acd32bac78336c80b477b3114be57f4..3cfad68da3a5241dacecaba1da51177f97fc64ab 100644 (file)
@@ -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 ""
 
index 40f95bf301343b02acc688515f35672a5ed71da8..3d521bd66810085137a78f7cee1c7727c3b5f072 100644 (file)
@@ -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
index 2ccad849d1a2c0297f5044f4f381a1dafd908804..5f25b351254655802c39404f64f5f3771fb326f5 100644 (file)
@@ -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:
index f07e0b4225e547dfbdbea445b0b5ae11caecb8be..a77c55f2302c27564b8b4f7f03e3b3948055412f 100644 (file)
@@ -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):