2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 20:45:44 +00:00

Update django-allauth version (#6099)

* Patch for django-allauth 0.55.0

- Some breaking changes here
- Add logger error if auth provider cannot be imported
- Fix for API endpoints

* Only provide URLs for configured plugins

* Update for django-allauth 0.56.0

- Remove support for keycloak
- Remove example from configuration template

* Update django-allauth in requirements.txt

* Update requirements.in

* Refactor SSO functions into common file

* Update config template file

* Update docs

* Fix template files

* Log SSO exceptions to the database

- WIll help greatly with debugging installs

* Add note about error handling in docs
This commit is contained in:
Oliver
2023-12-18 17:35:36 +11:00
committed by GitHub
parent 148bf0764b
commit a63529a9cf
12 changed files with 142 additions and 68 deletions

View File

@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _
from allauth.account.adapter import DefaultAccountAdapter
from allauth.account.forms import LoginForm, SignupForm, set_form_field_order
from allauth.exceptions import ImmediateHttpResponse
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth_2fa.adapter import OTPAdapter
from allauth_2fa.utils import user_has_valid_totp_device
@ -24,6 +24,7 @@ from crispy_forms.layout import Field, Layout
from dj_rest_auth.registration.serializers import RegisterSerializer
from rest_framework import serializers
import InvenTree.sso
from common.models import InvenTreeSetting
from InvenTree.exceptions import log_error
@ -228,7 +229,7 @@ class CustomSignupForm(SignupForm):
def registration_enabled():
"""Determine whether user registration is enabled."""
if InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'):
if InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTree.sso.registration_enabled():
if settings.EMAIL_HOST:
return True
else:
@ -358,6 +359,13 @@ class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocial
# Otherwise defer to the original allauth adapter.
return super().login(request, user)
def authentication_error(self, request, provider_id, error=None, exception=None, extra_context=None):
"""Callback method for authentication errors."""
# Log the error to the database
log_error(request.path if request else 'sso')
logger.error("SSO error for provider '%s' - check admin error log", provider_id)
# override dj-rest-auth
class CustomRegisterSerializer(RegisterSerializer):

View File

@ -288,6 +288,7 @@ MIDDLEWARE = CONFIG.get('middleware', [
'InvenTree.middleware.InvenTreeRemoteUserMiddleware', # Remote / proxy auth
'django_otp.middleware.OTPMiddleware', # MFA support
'InvenTree.middleware.CustomAllauthTwoFactorMiddleware', # Flow control for allauth
'allauth.account.middleware.AccountMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'InvenTree.middleware.AuthRequiredMiddleware',
@ -956,6 +957,13 @@ SITE_ID = 1
SOCIAL_BACKENDS = get_setting('INVENTREE_SOCIAL_BACKENDS', 'social_backends', [], typecast=list)
for app in SOCIAL_BACKENDS:
# Ensure that the app starts with 'allauth.socialaccount.providers'
social_prefix = 'allauth.socialaccount.providers.'
if not app.startswith(social_prefix): # pragma: no cover
app = social_prefix + app
INSTALLED_APPS.append(app) # pragma: no cover
SOCIALACCOUNT_PROVIDERS = get_setting('INVENTREE_SOCIAL_PROVIDERS', 'social_providers', None, typecast=dict)

View File

@ -2,13 +2,10 @@
import logging
from importlib import import_module
from django.urls import include, path, reverse
from django.urls import NoReverseMatch, include, path, reverse
from allauth.account.models import EmailAddress
from allauth.socialaccount import providers
from allauth.socialaccount.models import SocialApp
from allauth.socialaccount.providers.keycloak.views import \
KeycloakOAuth2Adapter
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
OAuth2LoginView)
from drf_spectacular.utils import OpenApiResponse, extend_schema
@ -16,6 +13,7 @@ from rest_framework.exceptions import NotFound
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
import InvenTree.sso
from common.models import InvenTreeSetting
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI
from InvenTree.serializers import InvenTreeModelSerializer
@ -51,14 +49,6 @@ def handle_oauth2(adapter: OAuth2Adapter):
]
def handle_keycloak():
"""Define urls for keycloak."""
return [
path('login/', GenericOAuth2ApiLoginView.adapter_view(KeycloakOAuth2Adapter), name='keycloak_api_login'),
path('connect/', GenericOAuth2ApiConnectView.adapter_view(KeycloakOAuth2Adapter), name='keycloak_api_connet'),
]
legacy = {
'twitter': 'twitter_oauth2',
'bitbucket': 'bitbucket_oauth2',
@ -72,10 +62,13 @@ legacy = {
social_auth_urlpatterns = []
provider_urlpatterns = []
for provider in providers.registry.get_list():
for name, provider in providers.registry.provider_map.items():
try:
prov_mod = import_module(provider.get_package() + ".views")
except ImportError:
logger.exception("Could not import authentication provider %s", name)
continue
# Try to extract the adapter class
@ -89,8 +82,6 @@ for provider in providers.registry.get_list():
if provider.id in legacy:
logger.warning('`%s` is not supported on platform UI. Use `%s` instead.', provider.id, legacy[provider.id])
continue
elif provider.id == 'keycloak':
urls = handle_keycloak()
else:
logger.error('Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.', provider.id)
continue
@ -107,26 +98,31 @@ class SocialProviderListView(ListAPI):
def get(self, request, *args, **kwargs):
"""Get the list of providers."""
provider_list = []
for provider in providers.registry.get_list():
for provider in providers.registry.provider_map.values():
provider_data = {
'id': provider.id,
'name': provider.name,
'login': request.build_absolute_uri(reverse(f'{provider.id}_api_login')),
'connect': request.build_absolute_uri(reverse(f'{provider.id}_api_connect')),
'configured': False
}
try:
provider_app = provider.get_app(request)
provider_data['display_name'] = provider_app.name
provider_data['configured'] = True
except SocialApp.DoesNotExist:
provider_data['display_name'] = provider.name
provider_data['login'] = request.build_absolute_uri(reverse(f'{provider.id}_api_login'))
except NoReverseMatch:
provider_data['login'] = None
try:
provider_data['connect'] = request.build_absolute_uri(reverse(f'{provider.id}_api_connect'))
except NoReverseMatch:
provider_data['connect'] = None
provider_data['configured'] = InvenTree.sso.check_provider(provider)
provider_data['display_name'] = InvenTree.sso.provider_display_name(provider)
provider_list.append(provider_data)
data = {
'sso_enabled': InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO'),
'sso_registration': InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'),
'sso_enabled': InvenTree.sso.login_enabled(),
'sso_registration': InvenTree.sso.registration_enabled(),
'mfa_required': InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA'),
'providers': provider_list
}

View File

@ -0,0 +1,81 @@
"""Helper functions for Single Sign On functionality"""
import logging
from common.models import InvenTreeSetting
from InvenTree.helpers import str2bool
logger = logging.getLogger('inventree')
def get_provider_app(provider):
"""Return the SocialApp object for the given provider"""
from allauth.socialaccount.models import SocialApp
try:
apps = SocialApp.objects.filter(provider__iexact=provider.id)
except SocialApp.DoesNotExist:
logger.warning("SSO SocialApp not found for provider '%s'", provider.id)
return None
if apps.count() > 1:
logger.warning("Multiple SocialApps found for provider '%s'", provider.id)
if apps.count() == 0:
logger.warning("SSO SocialApp not found for provider '%s'", provider.id)
return apps.first()
def check_provider(provider, raise_error=False):
"""Check if the given provider is correctly configured.
To be correctly configured, the following must be true:
- Provider must either have a registered SocialApp
- Must have at least one site enabled
"""
import allauth.app_settings
# First, check that the provider is enabled
app = get_provider_app(provider)
if not app:
return False
if allauth.app_settings.SITES_ENABLED:
# At least one matching site must be specified
if not app.sites.exists():
logger.error("SocialApp %s has no sites configured", app)
return False
# At this point, we assume that the provider is correctly configured
return True
def provider_display_name(provider):
"""Return the 'display name' for the given provider"""
if app := get_provider_app(provider):
return app.name
# Fallback value if app not found
return provider.name
def login_enabled() -> bool:
"""Return True if SSO login is enabled"""
return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO'))
def registration_enabled() -> bool:
"""Return True if SSO registration is enabled"""
return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'))
def auto_registration_enabled() -> bool:
"""Return True if SSO auto-registration is enabled"""
return str2bool(InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO'))

View File

@ -233,13 +233,13 @@ remote_login_header: HTTP_REMOTE_USER
# social_backends:
# - 'allauth.socialaccount.providers.google'
# - 'allauth.socialaccount.providers.github'
# - 'allauth.socialaccount.providers.keycloak'
# Add specific settings for social account providers (if required)
# Refer to the djngo-allauth documentation for more details:
# https://docs.allauth.org/en/latest/socialaccount/provider_configuration.html
# social_providers:
# keycloak:
# KEYCLOAK_URL: 'https://keycloak.custom/auth'
# KEYCLOAK_REALM: 'master'
# github:
# VERIFIED_EMAIL: true
# Add LDAP support
# ldap:

View File

@ -1,58 +1,32 @@
"""This module provides template tags pertaining to SSO functionality"""
import logging
from django import template
from common.models import InvenTreeSetting
from InvenTree.helpers import str2bool
import InvenTree.sso
register = template.Library()
logger = logging.getLogger('inventree')
@register.simple_tag()
def sso_login_enabled():
"""Return True if single-sign-on is enabled"""
return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO'))
return InvenTree.sso.login_enabled()
@register.simple_tag()
def sso_reg_enabled():
"""Return True if single-sign-on is enabled for self-registration"""
return str2bool(InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG'))
return InvenTree.sso.registration_enabled()
@register.simple_tag()
def sso_auto_enabled():
"""Return True if single-sign-on is enabled for auto-registration"""
return str2bool(InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO'))
return InvenTree.sso.auto_registration_enabled()
@register.simple_tag()
def sso_check_provider(provider):
"""Return True if the given provider is correctly configured"""
import allauth.app_settings
from allauth.socialaccount.models import SocialApp
# First, check that the provider is enabled
apps = SocialApp.objects.filter(provider__iexact=provider.id)
if not apps.exists():
logging.error(
"SSO SocialApp %s does not exist (known providers: %s)",
provider.id, [obj.provider for obj in SocialApp.objects.all()]
)
return False
# Next, check that the provider is correctly configured
app = apps.first()
if allauth.app_settings.SITES_ENABLED:
# At least one matching site must be specified
if not app.sites.exists():
logger.error("SocialApp %s has no sites configured", app)
return False
# At this point, we assume that the provider is correctly configured
return True
return InvenTree.sso.check_provider(provider)

View File

@ -1,4 +1,4 @@
{% extends "socialaccount/base.html" %}
{% extends "account/base.html" %}
{% load i18n %}

View File

@ -1,4 +1,4 @@
{% extends "socialaccount/base.html" %}
{% extends "account/base.html" %}
{% load i18n %}
{% load sso %}

View File

@ -1,4 +1,4 @@
{% extends "socialaccount/base.html" %}
{% extends "account/base.html" %}
{% load i18n crispy_forms_tags inventree_extras %}