Explorar o código

Initial work on SSO support (WIP)

jeremystretch %!s(int64=4) %!d(string=hai) anos
pai
achega
339776c139

+ 8 - 0
base_requirements.txt

@@ -102,6 +102,14 @@ PyYAML
 # https://github.com/andymccurdy/redis-py
 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)
 # https://github.com/mozman/svgwrite
 svgwrite

+ 1 - 10
netbox/netbox/middleware.py

@@ -8,7 +8,6 @@ from django.contrib import auth
 from django.core.exceptions import ImproperlyConfigured
 from django.db import ProgrammingError
 from django.http import Http404, HttpResponseRedirect
-from django.urls import reverse
 
 from extras.context_managers import change_logging
 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.
     """
-
     def __init__(self, get_response):
         self.get_response = get_response
 
     def __call__(self, request):
         # 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:
-            # Determine exempt paths
-            exempt_paths = [
-                reverse('api-root'),
-                reverse('graphql'),
-            ]
-            if settings.METRICS_ENABLED:
-                exempt_paths.append(reverse('prometheus-django-metrics'))
 
             # 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())}'
                 return HttpResponseRedirect(login_url)
 

+ 24 - 1
netbox/netbox/settings.py

@@ -305,6 +305,7 @@ INSTALLED_APPS = [
     'graphene_django',
     'mptt',
     'rest_framework',
+    'social_django',
     'taggit',
     'timezone_field',
     'circuits',
@@ -400,7 +401,8 @@ MESSAGE_TAGS = {
 }
 
 # 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
 
@@ -414,6 +416,27 @@ EXEMPT_EXCLUDE_MODELS = (
     ('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

+ 1 - 0
netbox/netbox/urls.py

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

+ 8 - 0
netbox/templates/login.html

@@ -39,6 +39,14 @@
       </form>
     </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 #}
     {% if form.non_field_errors %}
       <div class="alert alert-danger" role="alert">

+ 6 - 2
netbox/users/views.py

@@ -1,5 +1,6 @@
 import logging
 
+from django.conf import settings
 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.mixins import LoginRequiredMixin
@@ -12,6 +13,7 @@ from django.utils.decorators import method_decorator
 from django.utils.http import is_safe_url
 from django.views.decorators.debug import sensitive_post_parameters
 from django.views.generic import View
+from social_core.backends.utils import load_backends
 
 from netbox.config import get_config
 from utilities.forms import ConfirmationForm
@@ -42,6 +44,7 @@ class LoginView(View):
 
         return render(request, self.template_name, {
             'form': form,
+            'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS),
         })
 
     def post(self, request):
@@ -69,13 +72,14 @@ class LoginView(View):
 
         return render(request, self.template_name, {
             'form': form,
+            'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS),
         })
 
     def redirect_to_next(self, request, logger):
         if request.method == "POST":
-            redirect_to = request.POST.get('next', reverse('home'))
+            redirect_to = request.POST.get('next', settings.LOGIN_REDIRECT_URL)
         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()):
             logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")

+ 2 - 0
requirements.txt

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