Enhancement: support disabling regular login (#5816)

This commit is contained in:
shamoon 2024-02-25 21:17:21 -08:00 committed by GitHub
parent 90b4691f16
commit 1335ab5f1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 115 additions and 57 deletions

View 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.

View 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/)

View 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,
}

View File

@ -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" %}

View 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 ""

View 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

View 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:

View 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):