mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-17 20:46:30 -06:00
Merge pull request #7709 from netbox-community/7649-social-auth
Closes #7649: Add support for SSO
This commit is contained in:
commit
200aca470b
@ -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
|
||||||
|
37
docs/administration/authentication.md
Normal file
37
docs/administration/authentication.md
Normal 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.
|
@ -1,6 +1,6 @@
|
|||||||
# Permissions
|
# 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!}
|
{!models/users/objectpermission.md!}
|
||||||
|
|
||||||
|
@ -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.
|
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
|
### Enhancements
|
||||||
|
|
||||||
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
|
* [#1337](https://github.com/netbox-community/netbox/issues/1337) - Add WWN field to interfaces
|
||||||
|
@ -84,6 +84,7 @@ nav:
|
|||||||
- Using Plugins: 'plugins/index.md'
|
- Using Plugins: 'plugins/index.md'
|
||||||
- Developing Plugins: 'plugins/development.md'
|
- Developing Plugins: 'plugins/development.md'
|
||||||
- Administration:
|
- Administration:
|
||||||
|
- Authentication: 'administration/authentication.md'
|
||||||
- Permissions: 'administration/permissions.md'
|
- Permissions: 'administration/permissions.md'
|
||||||
- Housekeeping: 'administration/housekeeping.md'
|
- Housekeeping: 'administration/housekeeping.md'
|
||||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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')),
|
||||||
|
@ -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">
|
||||||
|
@ -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}")
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user