# Overrides paperless-ngx's own paperless/adapter.py at the same import path # (mounted via subPath in deployment.yaml) - settings.py hardcodes # SOCIALACCOUNT_ADAPTER = "paperless.adapter.CustomSocialAccountAdapter", so # no Django setting needs to change, just the file content underneath it. # # Stock CustomSocialAccountAdapter.populate_user() is a stub ("kept in case # global default permissions are implemented in the future" - they aren't), # so every OIDC signup lands with zero permissions and 403s on every API # endpoint. This adds the actual mapping: Authentik's "permissions" claim # (via the permissions scope, requested in PAPERLESS_SOCIALACCOUNT_PROVIDERS, # computed server-side from group membership by authentik-provision.py) -> # "paperless:write" or "*" (homelab-admins) grants is_staff+is_superuser, # same convention already used for MinIO's policy claim and Grafana's # role_attribute_path. Checking the permission string rather than a literal # group name decouples "what grants access" from which group happens to # hold it - same pattern applies to every other service's Role/RoleBinding # in k8s/infra/rbac/. apiVersion: v1 kind: ConfigMap metadata: name: paperless-adapter data: adapter.py: | from urllib.parse import quote 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 REQUIRED_PERMISSIONS = {"paperless:write", "*"} class CustomAccountAdapter(DefaultAccountAdapter): def is_open_for_signup(self, request): allow_signups = super().is_open_for_signup(request) return getattr(settings, "ACCOUNT_ALLOW_SIGNUPS", allow_signups) def pre_authenticate(self, request, **credentials): if settings.DISABLE_REGULAR_LOGIN: raise ValidationError("Regular login is disabled") return super().pre_authenticate(request, **credentials) def is_safe_url(self, url): from django.utils.http import url_has_allowed_host_and_scheme allowed_hosts = {context.request.get_host()} | set(settings.ALLOWED_HOSTS) if "*" in allowed_hosts: allowed_hosts.remove("*") allowed_hosts.add(context.request.get_host()) return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts) return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts) def get_reset_password_from_key_url(self, key): if settings.PAPERLESS_URL is None: return super().get_reset_password_from_key_url(key) path = reverse( "account_reset_password_from_key", kwargs={"uidb36": "UID", "key": "KEY"}, ) path = path.replace("UID-KEY", quote(key)) return settings.PAPERLESS_URL + path class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): def is_open_for_signup(self, request, sociallogin): allow_signups = super().is_open_for_signup(request, sociallogin) return getattr(settings, "SOCIALACCOUNT_ALLOW_SIGNUPS", allow_signups) def get_connect_redirect_url(self, request, socialaccount): return reverse("base") def populate_user(self, request, sociallogin, data): user = super().populate_user(request, sociallogin, data) perms = set(sociallogin.account.extra_data.get("permissions") or []) if perms & REQUIRED_PERMISSIONS: user.is_staff = True user.is_superuser = True return user def save_user(self, request, sociallogin, form=None): # populate_user() sets the flags on the in-memory user, but # allauth's default save_user() re-derives is_staff from # ACCOUNT_DEFAULT_HTTP_PROTOCOL-independent defaults and can # overwrite them on save - re-apply after super().save_user() # persists the row, matching the permissions check above exactly. user = super().save_user(request, sociallogin, form) perms = set(sociallogin.account.extra_data.get("permissions") or []) if perms & REQUIRED_PERMISSIONS and not (user.is_staff and user.is_superuser): user.is_staff = True user.is_superuser = True user.save(update_fields=["is_staff", "is_superuser"]) return user