1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on SSO support (WIP)

This commit is contained in:
jeremystretch
2021-10-29 17:06:14 -04:00
parent 4099dd3a05
commit 339776c139
7 changed files with 50 additions and 13 deletions

View File

@ -102,6 +102,14 @@ PyYAML
# https://github.com/andymccurdy/redis-py # https://github.com/andymccurdy/redis-py
redis redis
# Social authentication framework
# https://github.com/python-social-auth/social-core
social-auth-core[all]
# Django app for social-auth-core
# https://github.com/python-social-auth/social-app-django
social-auth-app-django
# SVG image rendering (used for rack elevations) # SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite # https://github.com/mozman/svgwrite
svgwrite svgwrite

View File

@ -8,7 +8,6 @@ from django.contrib import auth
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import ProgrammingError from django.db import ProgrammingError
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.urls import reverse
from extras.context_managers import change_logging from extras.context_managers import change_logging
from netbox.config import clear_config from netbox.config import clear_config
@ -20,23 +19,15 @@ class LoginRequiredMiddleware:
""" """
If LOGIN_REQUIRED is True, redirect all non-authenticated users to the login page. If LOGIN_REQUIRED is True, redirect all non-authenticated users to the login page.
""" """
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
def __call__(self, request): def __call__(self, request):
# Redirect unauthenticated requests (except those exempted) to the login page if LOGIN_REQUIRED is true # Redirect unauthenticated requests (except those exempted) to the login page if LOGIN_REQUIRED is true
if settings.LOGIN_REQUIRED and not request.user.is_authenticated: if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
# Determine exempt paths
exempt_paths = [
reverse('api-root'),
reverse('graphql'),
]
if settings.METRICS_ENABLED:
exempt_paths.append(reverse('prometheus-django-metrics'))
# Redirect unauthenticated requests # Redirect unauthenticated requests
if not request.path_info.startswith(tuple(exempt_paths)) and request.path_info != settings.LOGIN_URL: if not request.path_info.startswith(settings.EXEMPT_PATHS):
login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}' login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}'
return HttpResponseRedirect(login_url) return HttpResponseRedirect(login_url)

View File

@ -305,6 +305,7 @@ INSTALLED_APPS = [
'graphene_django', 'graphene_django',
'mptt', 'mptt',
'rest_framework', 'rest_framework',
'social_django',
'taggit', 'taggit',
'timezone_field', 'timezone_field',
'circuits', 'circuits',
@ -400,7 +401,8 @@ MESSAGE_TAGS = {
} }
# Authentication URLs # Authentication URLs
LOGIN_URL = '/{}login/'.format(BASE_PATH) LOGIN_URL = f'/{BASE_PATH}login/'
LOGIN_REDIRECT_URL = f'/{BASE_PATH}'
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
@ -414,6 +416,27 @@ EXEMPT_EXCLUDE_MODELS = (
('users', 'objectpermission'), ('users', 'objectpermission'),
) )
# All URLs starting with a string listed here are exempt from login enforcement
EXEMPT_PATHS = (
f'/{BASE_PATH}api/',
f'/{BASE_PATH}graphql/',
f'/{BASE_PATH}login/',
f'/{BASE_PATH}oauth/',
f'/{BASE_PATH}metrics/',
)
#
# Django social auth
#
# Load all SOCIAL_AUTH_* settings from the user configuration
for param in dir(configuration):
if param.startswith('SOCIAL_AUTH_'):
globals()[param] = getattr(configuration, param)
SOCIAL_AUTH_JSONFIELD_ENABLED = True
# #
# Django Prometheus # Django Prometheus

View File

@ -39,6 +39,7 @@ _patterns = [
# Login/logout # Login/logout
path('login/', LoginView.as_view(), name='login'), path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'), path('logout/', LogoutView.as_view(), name='logout'),
path('oauth/', include('social_django.urls', namespace='social')),
# Apps # Apps
path('circuits/', include('circuits.urls')), path('circuits/', include('circuits.urls')),

View File

@ -39,6 +39,14 @@
</form> </form>
</div> </div>
{# TODO: Improve the design & layout #}
{% if auth_backends %}
<h6 class="mt-4">Or use an SSO provider:</h6>
{% for name, backend in auth_backends.items %}
<h4><a href="{% url 'social:begin' backend=name %}" class="my-2">{{ name }}</a></h4>
{% endfor %}
{% endif %}
{# Login form errors #} {# Login form errors #}
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">

View File

@ -1,5 +1,6 @@
import logging import logging
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
@ -12,6 +13,7 @@ from django.utils.decorators import method_decorator
from django.utils.http import is_safe_url from django.utils.http import is_safe_url
from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View from django.views.generic import View
from social_core.backends.utils import load_backends
from netbox.config import get_config from netbox.config import get_config
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
@ -42,6 +44,7 @@ class LoginView(View):
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS),
}) })
def post(self, request): def post(self, request):
@ -69,13 +72,14 @@ class LoginView(View):
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS),
}) })
def redirect_to_next(self, request, logger): def redirect_to_next(self, request, logger):
if request.method == "POST": if request.method == "POST":
redirect_to = request.POST.get('next', reverse('home')) redirect_to = request.POST.get('next', settings.LOGIN_REDIRECT_URL)
else: else:
redirect_to = request.GET.get('next', reverse('home')) redirect_to = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()): if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}") logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")

View File

@ -23,6 +23,8 @@ netaddr==0.8.0
Pillow==8.4.0 Pillow==8.4.0
psycopg2-binary==2.9.1 psycopg2-binary==2.9.1
PyYAML==6.0 PyYAML==6.0
social-auth-app-django==5.0.0
social-auth-core==4.1.0
svgwrite==1.4.1 svgwrite==1.4.1
tablib==3.0.0 tablib==3.0.0