Merge pull request #7709 from netbox-community/7649-social-auth

Closes #7649: Add support for SSO
This commit is contained in:
Jeremy Stretch 2021-11-02 12:04:58 -04:00 committed by GitHub
commit 200aca470b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 93 additions and 14 deletions

View File

@ -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

View File

@ -0,0 +1,37 @@
# Authentication
## Local Authentication
Local user accounts and groups can be created in NetBox under the "Authentication and Authorization" section of the administrative user interface. This interface is available only to users with the "staff" permission enabled.
At a minimum, each user account must have a username and password set. User accounts may also denote a first name, last name, and email address. [Permissions](./permissions.md) may also be assigned to users and/or groups within the admin UI.
## Remote Authentication
NetBox may be configured to provide user authenticate via a remote backend in addition to local authentication. This is done by setting the `REMOTE_AUTH_BACKEND` configuration parameter to a suitable backend class. NetBox provides several options for remote authentication.
### LDAP Authentication
```python
REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'
```
NetBox includes an authentication backend which supports LDAP. See the [LDAP installation docs](../installation/6-ldap.md) for more detail about this backend.
### HTTP Header Authentication
```python
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
```
Another option for remote authentication in NetBox is to enable HTTP header-based user assignment. The front end HTTP server (e.g. nginx or Apache) performs client authentication as a process external to NetBox, and passes information about the authenticated user via HTTP headers. By default, the user is assigned via the `REMOTE_USER` header, but this can be customized via the `REMOTE_AUTH_HEADER` configuration parameter.
### Single Sign-On (SSO)
```python
REMOTE_AUTH_BACKEND = 'social_core.backends.google.GoogleOAuth2'
```
NetBox supports single sign-on authentication via the [python-social-auth](https://github.com/python-social-auth) library. To enable SSO, specify the path to the desired authentication backend within the `social_core` Python package. Please see the complete list of [supported authentication backends](https://github.com/python-social-auth/social-core/tree/master/social_core/backends) for the available options.
Most remote authentication backends require some additional configuration through settings prefixed with `SOCIAL_AUTH_`. These will be automatically imported from NetBox's `configuration.py` file. Additionally, the [authentication pipeline](https://python-social-auth.readthedocs.io/en/latest/pipeline.html) can be customized via the `SOCIAL_AUTH_PIPELINE` parameter.

View File

@ -1,6 +1,6 @@
# Permissions
NetBox v2.9 introduced a new object-based permissions framework, which replace's Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
NetBox v2.9 introduced a new object-based permissions framework, which replaces Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range.
{!models/users/objectpermission.md!}

View File

@ -57,6 +57,10 @@ A `bridge` field has been added to the interface model for devices and virtual m
Multiple interfaces can be bridged to a single virtual interface to effect a bridge group. Alternatively, two physical interfaces can be bridged to one another, to effect an internal cross-connect.
#### Single Sign-On (SSO) Authentication ([#7649](https://github.com/netbox-community/netbox/issues/7649))
Support for single sign-on (SSO) authentication has been added via the [python-social-auth](https://github.com/python-social-auth) library. NetBox administrators can configure one of the [supported authentication backends](https://python-social-auth.readthedocs.io/en/latest/intro.html#auth-providers) to enable SSO authentication for users.
### Enhancements
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces

View File

@ -84,6 +84,7 @@ nav:
- Using Plugins: 'plugins/index.md'
- Developing Plugins: 'plugins/development.md'
- Administration:
- Authentication: 'administration/authentication.md'
- Permissions: 'administration/permissions.md'
- Housekeeping: 'administration/housekeeping.md'
- Replicating NetBox: 'administration/replicating-netbox.md'

View File

@ -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)

View File

@ -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

View File

@ -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')),

View File

@ -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">

View File

@ -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}")

View File

@ -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