mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 13:06:45 +00:00
[PUI] SSO improvments (#8527)
* disable PUI SSO login button if not fully set up see https://github.com/inventree/InvenTree/issues/7972 * refactor provider urls to also support oidc providers * cleanup
This commit is contained in:
parent
f6124fc4a1
commit
66a769edca
@ -6,6 +6,7 @@ from importlib import import_module
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import NoReverseMatch, include, path, reverse
|
from django.urls import NoReverseMatch, include, path, reverse
|
||||||
|
|
||||||
|
import allauth.socialaccount.providers.openid_connect.views as oidc_views
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from allauth.socialaccount import providers
|
from allauth.socialaccount import providers
|
||||||
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter, OAuth2LoginView
|
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter, OAuth2LoginView
|
||||||
@ -44,7 +45,7 @@ class GenericOAuth2ApiConnectView(GenericOAuth2ApiLoginView):
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def handle_oauth2(adapter: OAuth2Adapter):
|
def handle_oauth2(adapter: OAuth2Adapter, provider=None):
|
||||||
"""Define urls for oauth2 endpoints."""
|
"""Define urls for oauth2 endpoints."""
|
||||||
return [
|
return [
|
||||||
path(
|
path(
|
||||||
@ -60,6 +61,22 @@ def handle_oauth2(adapter: OAuth2Adapter):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_oidc(provider):
|
||||||
|
"""Define urls for oidc endpoints."""
|
||||||
|
return [
|
||||||
|
path(
|
||||||
|
'login/',
|
||||||
|
lambda x: oidc_views.login(x, provider.id),
|
||||||
|
name=f'{provider.id}_api_login',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'connect/',
|
||||||
|
lambda x: oidc_views.callback(x, provider.id),
|
||||||
|
name=f'{provider.id}_api_connect',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
legacy = {
|
legacy = {
|
||||||
'twitter': 'twitter_oauth2',
|
'twitter': 'twitter_oauth2',
|
||||||
'bitbucket': 'bitbucket_oauth2',
|
'bitbucket': 'bitbucket_oauth2',
|
||||||
@ -70,47 +87,53 @@ legacy = {
|
|||||||
|
|
||||||
|
|
||||||
# Collect urls for all loaded providers
|
# Collect urls for all loaded providers
|
||||||
social_auth_urlpatterns = []
|
def get_provider_urls() -> list:
|
||||||
|
"""Collect urls for all loaded providers.
|
||||||
|
|
||||||
provider_urlpatterns = []
|
Returns:
|
||||||
|
list: List of urls for all loaded providers.
|
||||||
|
"""
|
||||||
|
auth_provider_routes = []
|
||||||
|
|
||||||
for name, provider in providers.registry.provider_map.items():
|
for name, provider in providers.registry.provider_map.items():
|
||||||
try:
|
try:
|
||||||
prov_mod = import_module(provider.get_package() + '.views')
|
prov_mod = import_module(provider.get_package() + '.views')
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.exception('Could not import authentication provider %s', name)
|
logger.exception('Could not import authentication provider %s', name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Try to extract the adapter class
|
# Try to extract the adapter class
|
||||||
adapters = [
|
adapters = [
|
||||||
cls
|
cls
|
||||||
for cls in prov_mod.__dict__.values()
|
for cls in prov_mod.__dict__.values()
|
||||||
if isinstance(cls, type)
|
if isinstance(cls, type)
|
||||||
and cls != OAuth2Adapter
|
and cls != OAuth2Adapter
|
||||||
and issubclass(cls, OAuth2Adapter)
|
and issubclass(cls, OAuth2Adapter)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get urls
|
# Get urls
|
||||||
urls = []
|
urls = []
|
||||||
if len(adapters) == 1:
|
if len(adapters) == 1:
|
||||||
urls = handle_oauth2(adapter=adapters[0])
|
if provider.id == 'openid_connect':
|
||||||
elif provider.id in legacy:
|
urls = handle_oidc(provider)
|
||||||
logger.warning(
|
else:
|
||||||
'`%s` is not supported on platform UI. Use `%s` instead.',
|
urls = handle_oauth2(adapter=adapters[0], provider=provider)
|
||||||
provider.id,
|
elif provider.id in legacy:
|
||||||
legacy[provider.id],
|
logger.warning(
|
||||||
)
|
'`%s` is not supported on platform UI. Use `%s` instead.',
|
||||||
continue
|
provider.id,
|
||||||
else:
|
legacy[provider.id],
|
||||||
logger.error(
|
)
|
||||||
'Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.',
|
continue
|
||||||
provider.id,
|
else:
|
||||||
)
|
logger.error(
|
||||||
continue
|
'Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.',
|
||||||
provider_urlpatterns += [path(f'{provider.id}/', include(urls))]
|
provider.id,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
auth_provider_routes += [path(f'{provider.id}/', include(urls))]
|
||||||
|
|
||||||
|
return auth_provider_routes
|
||||||
social_auth_urlpatterns += provider_urlpatterns
|
|
||||||
|
|
||||||
|
|
||||||
class SocialProviderListResponseSerializer(serializers.Serializer):
|
class SocialProviderListResponseSerializer(serializers.Serializer):
|
||||||
|
@ -54,7 +54,7 @@ from .social_auth_urls import (
|
|||||||
EmailRemoveView,
|
EmailRemoveView,
|
||||||
EmailVerifyView,
|
EmailVerifyView,
|
||||||
SocialProviderListView,
|
SocialProviderListView,
|
||||||
social_auth_urlpatterns,
|
get_provider_urls,
|
||||||
)
|
)
|
||||||
from .views import (
|
from .views import (
|
||||||
AboutView,
|
AboutView,
|
||||||
@ -78,6 +78,71 @@ from .views import (
|
|||||||
|
|
||||||
admin.site.site_header = 'InvenTree Admin'
|
admin.site.site_header = 'InvenTree Admin'
|
||||||
|
|
||||||
|
settings_urls = [
|
||||||
|
path('i18n/', include('django.conf.urls.i18n')),
|
||||||
|
path('appearance/', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||||
|
# Catch any other urls
|
||||||
|
path(
|
||||||
|
'',
|
||||||
|
SettingsView.as_view(template_name='InvenTree/settings/settings.html'),
|
||||||
|
name='settings',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
notifications_urls = [
|
||||||
|
# Catch any other urls
|
||||||
|
path('', NotificationsView.as_view(), name='notifications')
|
||||||
|
]
|
||||||
|
|
||||||
|
classic_frontendpatterns = [
|
||||||
|
# Apps
|
||||||
|
#
|
||||||
|
path('build/', include(build_urls)),
|
||||||
|
path('common/', include(common_urls)),
|
||||||
|
path('company/', include(company_urls)),
|
||||||
|
path('order/', include(order_urls)),
|
||||||
|
path('manufacturer-part/', include(manufacturer_part_urls)),
|
||||||
|
path('part/', include(part_urls)),
|
||||||
|
path('stock/', include(stock_urls)),
|
||||||
|
path('supplier-part/', include(supplier_part_urls)),
|
||||||
|
path('edit-user/', EditUserView.as_view(), name='edit-user'),
|
||||||
|
path('set-password/', SetPasswordView.as_view(), name='set-password'),
|
||||||
|
path('index/', IndexView.as_view(), name='index'),
|
||||||
|
path('notifications/', include(notifications_urls)),
|
||||||
|
path('search/', SearchView.as_view(), name='search'),
|
||||||
|
path('settings/', include(settings_urls)),
|
||||||
|
path('about/', AboutView.as_view(), name='about'),
|
||||||
|
path('stats/', DatabaseStatsView.as_view(), name='stats'),
|
||||||
|
# DB user sessions
|
||||||
|
path(
|
||||||
|
'accounts/sessions/other/delete/',
|
||||||
|
view=CustomSessionDeleteOtherView.as_view(),
|
||||||
|
name='session_delete_other',
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r'^accounts/sessions/(?P<pk>\w+)/delete/$',
|
||||||
|
view=CustomSessionDeleteView.as_view(),
|
||||||
|
name='session_delete',
|
||||||
|
),
|
||||||
|
# Single Sign On / allauth
|
||||||
|
# overrides of urlpatterns
|
||||||
|
path('accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
||||||
|
path(
|
||||||
|
'accounts/social/connections/',
|
||||||
|
CustomConnectionsView.as_view(),
|
||||||
|
name='socialaccount_connections',
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r'^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$',
|
||||||
|
CustomPasswordResetFromKeyView.as_view(),
|
||||||
|
name='account_reset_password_from_key',
|
||||||
|
),
|
||||||
|
# Override login page
|
||||||
|
path('accounts/login/', CustomLoginView.as_view(), name='account_login'),
|
||||||
|
path('accounts/', include('allauth_2fa.urls')), # MFA support
|
||||||
|
path('accounts/', include('allauth.urls')), # included urlpatterns
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
apipatterns = [
|
apipatterns = [
|
||||||
# Global search
|
# Global search
|
||||||
@ -167,7 +232,7 @@ apipatterns = [
|
|||||||
path('', EmailListView.as_view(), name='email-list'),
|
path('', EmailListView.as_view(), name='email-list'),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
path('social/', include(social_auth_urlpatterns)),
|
path('social/', include(get_provider_urls())),
|
||||||
path(
|
path(
|
||||||
'social/', SocialAccountListView.as_view(), name='social_account_list'
|
'social/', SocialAccountListView.as_view(), name='social_account_list'
|
||||||
),
|
),
|
||||||
@ -197,21 +262,6 @@ apipatterns = [
|
|||||||
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
|
re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
|
||||||
]
|
]
|
||||||
|
|
||||||
settings_urls = [
|
|
||||||
path('i18n/', include('django.conf.urls.i18n')),
|
|
||||||
path('appearance/', AppearanceSelectView.as_view(), name='settings-appearance'),
|
|
||||||
# Catch any other urls
|
|
||||||
path(
|
|
||||||
'',
|
|
||||||
SettingsView.as_view(template_name='InvenTree/settings/settings.html'),
|
|
||||||
name='settings',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
notifications_urls = [
|
|
||||||
# Catch any other urls
|
|
||||||
path('', NotificationsView.as_view(), name='notifications')
|
|
||||||
]
|
|
||||||
|
|
||||||
# These javascript files are served "dynamically" - i.e. rendered on demand
|
# These javascript files are served "dynamically" - i.e. rendered on demand
|
||||||
dynamic_javascript_urls = [
|
dynamic_javascript_urls = [
|
||||||
@ -400,54 +450,6 @@ if settings.ENABLE_CLASSIC_FRONTEND:
|
|||||||
re_path(r'^js/i18n/', include(translated_javascript_urls)),
|
re_path(r'^js/i18n/', include(translated_javascript_urls)),
|
||||||
]
|
]
|
||||||
|
|
||||||
classic_frontendpatterns = [
|
|
||||||
# Apps
|
|
||||||
#
|
|
||||||
path('build/', include(build_urls)),
|
|
||||||
path('common/', include(common_urls)),
|
|
||||||
path('company/', include(company_urls)),
|
|
||||||
path('order/', include(order_urls)),
|
|
||||||
path('manufacturer-part/', include(manufacturer_part_urls)),
|
|
||||||
path('part/', include(part_urls)),
|
|
||||||
path('stock/', include(stock_urls)),
|
|
||||||
path('supplier-part/', include(supplier_part_urls)),
|
|
||||||
path('edit-user/', EditUserView.as_view(), name='edit-user'),
|
|
||||||
path('set-password/', SetPasswordView.as_view(), name='set-password'),
|
|
||||||
path('index/', IndexView.as_view(), name='index'),
|
|
||||||
path('notifications/', include(notifications_urls)),
|
|
||||||
path('search/', SearchView.as_view(), name='search'),
|
|
||||||
path('settings/', include(settings_urls)),
|
|
||||||
path('about/', AboutView.as_view(), name='about'),
|
|
||||||
path('stats/', DatabaseStatsView.as_view(), name='stats'),
|
|
||||||
# DB user sessions
|
|
||||||
path(
|
|
||||||
'accounts/sessions/other/delete/',
|
|
||||||
view=CustomSessionDeleteOtherView.as_view(),
|
|
||||||
name='session_delete_other',
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r'^accounts/sessions/(?P<pk>\w+)/delete/$',
|
|
||||||
view=CustomSessionDeleteView.as_view(),
|
|
||||||
name='session_delete',
|
|
||||||
),
|
|
||||||
# Single Sign On / allauth
|
|
||||||
# overrides of urlpatterns
|
|
||||||
path('accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
|
||||||
path(
|
|
||||||
'accounts/social/connections/',
|
|
||||||
CustomConnectionsView.as_view(),
|
|
||||||
name='socialaccount_connections',
|
|
||||||
),
|
|
||||||
re_path(
|
|
||||||
r'^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$',
|
|
||||||
CustomPasswordResetFromKeyView.as_view(),
|
|
||||||
name='account_reset_password_from_key',
|
|
||||||
),
|
|
||||||
# Override login page
|
|
||||||
path('accounts/login/', CustomLoginView.as_view(), name='account_login'),
|
|
||||||
path('accounts/', include('allauth_2fa.urls')), # MFA support
|
|
||||||
path('accounts/', include('allauth.urls')), # included urlpatterns
|
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns = []
|
urlpatterns = []
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button } from '@mantine/core';
|
import { Button, Tooltip } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconBrandAzure,
|
IconBrandAzure,
|
||||||
IconBrandBitbucket,
|
IconBrandBitbucket,
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
IconLogin
|
IconLogin
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
@ -49,14 +50,23 @@ export function SsoButton({ provider }: Readonly<{ provider: Provider }>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Tooltip
|
||||||
leftSection={getBrandIcon(provider)}
|
label={
|
||||||
radius='xl'
|
provider.login
|
||||||
component='a'
|
? t`You will be redirected to the provider for further actions.`
|
||||||
onClick={login}
|
: t`This provider is not full set up.`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{provider.display_name}{' '}
|
<Button
|
||||||
</Button>
|
leftSection={getBrandIcon(provider)}
|
||||||
|
radius='xl'
|
||||||
|
component='a'
|
||||||
|
onClick={login}
|
||||||
|
disabled={!provider.login}
|
||||||
|
>
|
||||||
|
{provider.display_name}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function getBrandIcon(provider: Provider) {
|
function getBrandIcon(provider: Provider) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user