From 57eeeed3ea7ff48aac62be8c9f4e1212bb78464e Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Wed, 14 Jun 2023 16:49:09 -0400 Subject: [PATCH] user/paperless-ngx: experimental SSO support --- ...nnect-SSO-support-via-django-allauth.patch | 438 ++++++++++++++++++ user/paperless-ngx/APKBUILD | 16 +- 2 files changed, 447 insertions(+), 7 deletions(-) create mode 100644 user/paperless-ngx/1746_add-OpenID-Connect-SSO-support-via-django-allauth.patch diff --git a/user/paperless-ngx/1746_add-OpenID-Connect-SSO-support-via-django-allauth.patch b/user/paperless-ngx/1746_add-OpenID-Connect-SSO-support-via-django-allauth.patch new file mode 100644 index 0000000..a265d0f --- /dev/null +++ b/user/paperless-ngx/1746_add-OpenID-Connect-SSO-support-via-django-allauth.patch @@ -0,0 +1,438 @@ +From 40d1e18faf39dd50dd41c5e94ab13350df384e51 Mon Sep 17 00:00:00 2001 +From: Antoine Martin +Date: Wed, 14 Jun 2023 10:20:42 -0400 +Subject: [PATCH 1/1] Add OpenID Connect SSO support via django-allauth (#1746) + +--- + src/documents/templates/account/base.html | 14 + + src/documents/templates/account/login.html | 1 + + src/documents/templates/account/logout.html | 1 + + .../templates/registration/login.html | 72 ++- + .../socialaccount/authentication_error.html | 1 + + src/documents/templatetags/django_settings.py | 9 + + src/paperless/allauth_custom.py | 73 +++ + src/paperless/settings.py | 114 +++++ + src/paperless/urls.py | 9 +- + 10 files changed, 711 insertions(+), 18 deletions(-) + create mode 100644 0001-Add-OpenID-Connect-SSO-support-via-django-allauth-17.patch + create mode 100644 src/documents/templates/account/base.html + create mode 120000 src/documents/templates/account/login.html + create mode 120000 src/documents/templates/account/logout.html + create mode 120000 src/documents/templates/socialaccount/authentication_error.html + create mode 100644 src/documents/templatetags/django_settings.py + create mode 100644 src/paperless/allauth_custom.py + +diff --git a/src/documents/templates/account/base.html b/src/documents/templates/account/base.html +new file mode 100644 +index 00000000..0912c609 +--- /dev/null ++++ b/src/documents/templates/account/base.html +@@ -0,0 +1,14 @@ ++{% load i18n %} ++ ++ ++ {% translate "Redirecting" %} ++ ++ ++ ++ ++ {% translate "Redirecting" %}... ++ ++ +diff --git a/src/documents/templates/account/login.html b/src/documents/templates/account/login.html +new file mode 120000 +index 00000000..03fd169e +--- /dev/null ++++ b/src/documents/templates/account/login.html +@@ -0,0 +1 @@ ++../registration/login.html +\ No newline at end of file +diff --git a/src/documents/templates/account/logout.html b/src/documents/templates/account/logout.html +new file mode 120000 +index 00000000..7cad0aa6 +--- /dev/null ++++ b/src/documents/templates/account/logout.html +@@ -0,0 +1 @@ ++../registration/logged_out.html +\ No newline at end of file +diff --git a/src/documents/templates/registration/login.html b/src/documents/templates/registration/login.html +index d9ff86a7..404f37e6 100644 +--- a/src/documents/templates/registration/login.html ++++ b/src/documents/templates/registration/login.html +@@ -1,4 +1,7 @@ + ++{% load django_settings %} ++{% load socialaccount %} ++{% settings_value "LOGIN_HIDE_PASSWORD_FORM" as hide_password_form %} + + {% load static %} + {% load i18n %} +@@ -38,8 +41,7 @@ + + + +- ++ + +diff --git a/src/documents/templates/socialaccount/authentication_error.html b/src/documents/templates/socialaccount/authentication_error.html +new file mode 120000 +index 00000000..b2ef5434 +--- /dev/null ++++ b/src/documents/templates/socialaccount/authentication_error.html +@@ -0,0 +1 @@ ++../account/login.html +\ No newline at end of file +diff --git a/src/documents/templatetags/django_settings.py b/src/documents/templatetags/django_settings.py +new file mode 100644 +index 00000000..cf415d5c +--- /dev/null ++++ b/src/documents/templatetags/django_settings.py +@@ -0,0 +1,9 @@ ++from django import template ++from django.conf import settings ++ ++register = template.Library() ++ ++ ++@register.simple_tag ++def settings_value(name): ++ return getattr(settings, name, "") +diff --git a/src/paperless/allauth_custom.py b/src/paperless/allauth_custom.py +new file mode 100644 +index 00000000..0e716346 +--- /dev/null ++++ b/src/paperless/allauth_custom.py +@@ -0,0 +1,73 @@ ++import logging ++ ++from allauth.account.adapter import DefaultAccountAdapter ++from allauth.socialaccount.adapter import DefaultSocialAccountAdapter ++from django.conf import settings ++from django.http import Http404 ++from django.urls import include ++from django.urls import path ++from django.urls import re_path ++from django.urls import reverse ++from django.urls import reverse_lazy ++from django.views.generic import RedirectView ++ ++logger = logging.getLogger("paperless.allauth") ++ ++ ++def raise_404(*args, **kwargs): ++ raise Http404 ++ ++ ++class CustomAccountAdapter(DefaultAccountAdapter): ++ def get_login_redirect_url(self, request): ++ return reverse("base") ++ ++ def get_signup_redirect_url(self, request): ++ return self.get_login_redirect_url(request) ++ ++ def is_open_for_signup(self, request): ++ return getattr(settings, "LOGIN_ENABLE_SIGNUP", False) ++ ++ ++class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): ++ def authentication_error( ++ self, ++ request, ++ provider_id, ++ error=None, ++ exception=None, ++ extra_context=None, ++ ): ++ logger.error(f"Authentication error: {exception}") ++ return super().authentication_error( ++ request, ++ provider_id, ++ error, ++ exception, ++ extra_context, ++ ) ++ ++ def is_auto_signup_allowed(self, *args, **kwargs): ++ if getattr(settings, "SSO_AUTO_LINK_MULTIPLE", True): ++ # Skip allauth default logic of checking for an existing user with ++ # the same email address. This requires paperless administrators to ++ # trust the SSO providers connected to paperless. ++ return True ++ return super().is_auto_signup_allowed(*args, **kwargs) ++ ++ def is_open_for_signup(self, request, sociallogin): ++ # True indicates a user should be automatically created on successful ++ # login via configured external provider ++ return getattr(settings, "SSO_AUTO_LINK", True) ++ ++ ++base_url = reverse_lazy("base") ++urlpatterns = [ ++ # Override allauth URLs to disable features we don't want ++ path("signup/", RedirectView.as_view(url=base_url)), ++ re_path("confirm-email/.*", RedirectView.as_view(url=base_url)), ++ re_path("email/.*", RedirectView.as_view(url=base_url)), ++ re_path("password/.*", RedirectView.as_view(url=base_url)), ++ # Import allauth-provided URL patterns ++ path("", include("allauth.urls")), ++] +diff --git a/src/paperless/settings.py b/src/paperless/settings.py +index d3c239b4..a2cba20b 100644 +--- a/src/paperless/settings.py ++++ b/src/paperless/settings.py +@@ -18,6 +18,7 @@ from urllib.parse import urlparse + + from celery.schedules import crontab + from concurrent_log_handler.queue import setup_logging_queues ++from django.utils.text import slugify + from django.utils.translation import gettext_lazy as _ + from dotenv import load_dotenv + +@@ -91,6 +92,22 @@ def __get_list( + return [] + + ++def __get_list( ++ key: str, ++ sep: str = ",", ++ default: Optional[List[str]] = None, ++) -> List[str]: ++ """ ++ Return a list of strings based on the environment variable or an given default ++ list, if provided or an empty list ++ """ ++ if key in os.environ: ++ os.getenv(key).split(sep) ++ elif default is not None: ++ return default ++ return [] ++ ++ + def _parse_redis_url(env_redis: Optional[str]) -> Tuple[str]: + """ + Gets the Redis information from the environment or a default and handles +@@ -256,6 +273,40 @@ SCRATCH_DIR = __get_path( + Path(tempfile.gettempdir()) / "paperless", + ) + ++############################################################################### ++# SSO Configuration ++############################################################################### ++ ++ ++def _get_oidc_server() -> Optional[Dict]: ++ config_id = os.environ.get("PAPERLESS_SSO_OIDC_ID") ++ name = os.environ.get("PAPERLESS_SSO_OIDC_NAME") ++ url = os.environ.get("PAPERLESS_SSO_OIDC_URL") ++ client_id = os.environ.get("PAPERLESS_SSO_OIDC_CLIENT_ID") ++ secret = os.environ.get("PAPERLESS_SSO_OIDC_SECRET") ++ if name and url and client_id and secret: ++ return { ++ "id": config_id or slugify(name)[:30], ++ "name": name, ++ "server_url": url, ++ "APP": { ++ "client_id": client_id, ++ "secret": secret, ++ }, ++ } ++ return None ++ ++ ++_allauth_provider_modules = set(__get_list("PAPERLESS_SSO_MODULES")) ++_oidc_server = _get_oidc_server() ++if _oidc_server: ++ _allauth_provider_modules.add("openid_connect") ++ ++SSO_ENABLED = __get_boolean( ++ "PAPERLESS_SSO_ENABLED", ++ str(bool(_allauth_provider_modules)), ++) ++ + ############################################################################### + # Application Definition # + ############################################################################### +@@ -282,9 +333,18 @@ INSTALLED_APPS = [ + "django_filters", + "django_celery_results", + "guardian", ++ "allauth", ++ "allauth.account", ++ "allauth.socialaccount", + *env_apps, + ] + ++if SSO_ENABLED: ++ INSTALLED_APPS += [ ++ f"allauth.socialaccount.providers.{provider}" ++ for provider in _allauth_provider_modules ++ ] ++ + if DEBUG: + INSTALLED_APPS.append("channels") + +@@ -394,6 +454,10 @@ HTTP_REMOTE_USER_HEADER_NAME = os.getenv( + if ENABLE_HTTP_REMOTE_USER: + MIDDLEWARE.append("paperless.auth.HttpRemoteUserMiddleware") + AUTHENTICATION_BACKENDS.insert(0, "django.contrib.auth.backends.RemoteUserBackend") ++ if SSO_ENABLED: ++ AUTHENTICATION_BACKENDS.append( ++ "allauth.account.auth_backends.AuthenticationBackend", ++ ) + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append( + "rest_framework.authentication.RemoteUserAuthentication", + ) +@@ -949,3 +1013,53 @@ def _get_nltk_language_setting(ocr_lang: str) -> Optional[str]: + NLTK_ENABLED: Final[bool] = __get_boolean("PAPERLESS_ENABLE_NLTK", "yes") + + NLTK_LANGUAGE: Optional[str] = _get_nltk_language_setting(OCR_LANGUAGE) ++ ++ ++############################################################################### ++# Single Sign-On (SSO) ++############################################################################### ++ ++ ++if SSO_ENABLED: ++ SSO_AUTO_LINK = __get_boolean("PAPERLESS_SSO_AUTO_LINK", "yes") ++ SSO_AUTO_LINK_MULTIPLE = __get_boolean( ++ "PAPERLESS_SSO_AUTO_LINK_MULTIPLE", ++ "yes", ++ ) ++ ++ # TODO This setting is unused and not part of django-allauth ++ SSO_SIGNUP_ONLY = __get_boolean( ++ "PAPERLESS_SSO_SIGNUP_ONLY", ++ "yes", ++ ) ++ ACCOUNT_ADAPTER = "paperless.allauth_custom.CustomAccountAdapter" ++ ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" ++ ACCOUNT_EMAIL_VERIFICATION = "none" ++ LOGIN_ENABLE_SIGNUP = __get_boolean("PAPERLESS_LOGIN_ENABLE_SIGNUP", "no") ++ LOGIN_HIDE_PASSWORD_FORM = __get_boolean( ++ "PAPERLESS_LOGIN_HIDE_PASSWORD_FORM", ++ "no", ++ ) ++ ACCOUNT_LOGOUT_ON_GET = True ++ ++ # Disable all allauth forms except for the login form ++ class AllauthFormsOverride(dict): ++ def get(self, key, default=None): ++ return super().get(key, "paperless.allauth_custom.raise_404") ++ ++ ACCOUNT_FORMS = AllauthFormsOverride( ++ login="allauth.account.forms.LoginForm", ++ ) ++ ++ SOCIALACCOUNT_ADAPTER = "paperless.allauth_custom.CustomSocialAccountAdapter" ++ SOCIALACCOUNT_LOGIN_ON_GET = __get_boolean( ++ "PAPERLESS_SSO_LOGIN_ON_GET", ++ "no", ++ ) ++ SOCIALACCOUNT_PROVIDERS = json.loads( ++ os.environ.get("PAPERLESS_SSO_PROVIDERS", "{}"), ++ ) ++ if _oidc_server: ++ SOCIALACCOUNT_PROVIDERS.setdefault("openid_connect", {}) ++ SOCIALACCOUNT_PROVIDERS["openid_connect"].setdefault("SERVERS", []) ++ SOCIALACCOUNT_PROVIDERS["openid_connect"]["SERVERS"] += [_oidc_server] +diff --git a/src/paperless/urls.py b/src/paperless/urls.py +index c2b72d7b..3a3bf342 100644 +--- a/src/paperless/urls.py ++++ b/src/paperless/urls.py +@@ -154,7 +154,14 @@ urlpatterns = [ + ), + # TODO: with localization, this is even worse! :/ + # login, logout +- path("accounts/", include("django.contrib.auth.urls")), ++ path( ++ "accounts/", ++ include( ++ "paperless.allauth_custom" ++ if settings.SSO_ENABLED ++ else "django.contrib.auth.urls", ++ ), ++ ), + # Root of the Frontent + re_path(r".*", login_required(IndexView.as_view()), name="base"), + ] +-- +2.40.1 + diff --git a/user/paperless-ngx/APKBUILD b/user/paperless-ngx/APKBUILD index 1f1e7e7..1603ce3 100644 --- a/user/paperless-ngx/APKBUILD +++ b/user/paperless-ngx/APKBUILD @@ -3,7 +3,7 @@ pkgname=paperless-ngx pkgver=1.15.1 -pkgrel=0 +pkgrel=1 pkgdesc="A community-supported supercharged version of paperless: scan, index and archive all your physical documents" url="https://github.com/paperless-ngx/paperless-ngx" license="GPL-3.0-only" @@ -35,12 +35,6 @@ depends=" py3-blessed py3-certifi py3-celery - py3-django-channels - py3-django-channels-redis - py3-django-compression-middleware - py3-django-celery-results - py3-django-guardian - py3-django-rest-framework-guardian py3-chardet py3-charset-normalizer py3-click @@ -50,9 +44,15 @@ depends=" py3-daphne py3-dateparser py3-deprecation + py3-django-allauth + py3-django-channels + py3-django-channels-redis + py3-django-compression-middleware + py3-django-celery-results py3-django-cors-headers py3-django-extensions py3-django-filter + py3-django-guardian py3-django-picklefield py3-django-q py3-django-rest-framework @@ -123,6 +123,7 @@ builddir="$srcdir"/$pkgname source=" $url/releases/download/v$pkgver/$pkgname-v$pkgver.tar.xz + 1746_add-OpenID-Connect-SSO-support-via-django-allauth.patch paperless-scheduler.openrc paperless-consumer.openrc paperless-webserver.openrc @@ -177,6 +178,7 @@ package() { } sha512sums=" aefa72e0ea5d96209c5089bec452bd7158b478954ebdb219db0379259f4b12540c02a7ae20cb998b49910c579de34be3c9b7351378b87a6f42d07f2884892200 paperless-ngx-v1.15.1.tar.xz +71526db0d48f26168005de06a91f6099c318c6622b70d7af32a899a2e485b34876e94fe14d7434367232d80fbbf8c4d75281874283ab832fd8568dfea3b87bdf 1746_add-OpenID-Connect-SSO-support-via-django-allauth.patch b782dd9479d31d2f6a82e86639bb5e2bb3675c1ffc1d6b08e027e10159dd58ed9f68b5986b9d7c8a326e95384e701bcf9834101d6a6720db7e97465e4d295d36 paperless-scheduler.openrc b4413f48b481d53b3e10542f5ffe830928c40ae016e2dc1da533ae8b546c5b6e4ddfa1129280807f866002f61b283c4eba327be6eb04171e259fe27fec47696a paperless-consumer.openrc e9c517f7fbae269072506316711a12a6ba5568456348305972caf023020de5ebeab45401371fe114fe8dbddfacbcc6cfd01d0fad2b2ade6ee3883f46120b904e paperless-webserver.openrc