2020-03-02 17:04:54 -05:00
|
|
|
import logging
|
|
|
|
|
2021-10-29 17:06:14 -04:00
|
|
|
from django.conf import settings
|
2016-03-01 11:23:03 -05:00
|
|
|
from django.contrib import messages
|
|
|
|
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
2020-06-02 15:36:31 -04:00
|
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
2019-04-29 11:04:32 -04:00
|
|
|
from django.contrib.auth.models import update_last_login
|
|
|
|
from django.contrib.auth.signals import user_logged_in
|
2021-03-31 13:25:06 -04:00
|
|
|
from django.http import HttpResponseRedirect
|
2017-03-08 11:34:47 -05:00
|
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
2017-04-05 14:40:25 -04:00
|
|
|
from django.urls import reverse
|
2017-05-19 15:47:19 -04:00
|
|
|
from django.utils.decorators import method_decorator
|
2022-08-25 11:48:46 -07:00
|
|
|
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
|
2019-02-13 11:34:16 -05:00
|
|
|
from django.views.decorators.debug import sensitive_post_parameters
|
2017-03-07 23:30:53 -05:00
|
|
|
from django.views.generic import View
|
2021-10-29 17:06:14 -04:00
|
|
|
from social_core.backends.utils import load_backends
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2021-12-08 16:32:31 -05:00
|
|
|
from extras.models import ObjectChange
|
|
|
|
from extras.tables import ObjectChangeTable
|
2022-08-25 11:48:46 -07:00
|
|
|
from netbox.authentication import get_auth_backend_display, get_saml_idps
|
2021-10-26 13:41:56 -04:00
|
|
|
from netbox.config import get_config
|
2017-03-08 11:34:47 -05:00
|
|
|
from utilities.forms import ConfirmationForm
|
2021-12-21 16:29:01 -05:00
|
|
|
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
2022-07-15 09:42:12 -04:00
|
|
|
from .models import Token, UserConfig
|
2022-07-11 15:43:59 -04:00
|
|
|
from .tables import TokenTable
|
2016-03-01 11:23:03 -05:00
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Login/logout
|
|
|
|
#
|
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
class LoginView(View):
|
2020-03-02 17:04:54 -05:00
|
|
|
"""
|
|
|
|
Perform user authentication via the web UI.
|
|
|
|
"""
|
2017-05-19 15:47:19 -04:00
|
|
|
template_name = 'login.html'
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2019-02-13 11:34:16 -05:00
|
|
|
@method_decorator(sensitive_post_parameters('password'))
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super().dispatch(*args, **kwargs)
|
|
|
|
|
2022-08-25 11:48:46 -07:00
|
|
|
def gen_auth_data(self, name, url):
|
|
|
|
display_name, icon_name = get_auth_backend_display(name)
|
|
|
|
return {
|
|
|
|
'display_name': display_name,
|
|
|
|
'icon_name': icon_name,
|
|
|
|
'url': url,
|
|
|
|
}
|
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
def get(self, request):
|
|
|
|
form = LoginForm(request)
|
|
|
|
|
2020-09-04 16:09:05 -04:00
|
|
|
if request.user.is_authenticated:
|
|
|
|
logger = logging.getLogger('netbox.auth.login')
|
|
|
|
return self.redirect_to_next(request, logger)
|
|
|
|
|
2022-08-25 11:48:46 -07:00
|
|
|
auth_backends = []
|
|
|
|
saml_idps = get_saml_idps()
|
|
|
|
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
|
|
|
|
url = reverse('social:begin', args=[name, ])
|
|
|
|
if name.lower() == 'saml' and saml_idps:
|
|
|
|
for idp in saml_idps:
|
|
|
|
params = {'idp': idp}
|
|
|
|
idp_url = f'{url}?{urlencode(params)}'
|
|
|
|
data = self.gen_auth_data(name, idp_url)
|
|
|
|
data['display_name'] = f'{data["display_name"]} ({idp})'
|
|
|
|
auth_backends.append(data)
|
|
|
|
else:
|
|
|
|
auth_backends.append(self.gen_auth_data(name, url))
|
2022-04-14 10:54:07 -04:00
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
return render(request, self.template_name, {
|
|
|
|
'form': form,
|
2022-04-14 10:54:07 -04:00
|
|
|
'auth_backends': auth_backends,
|
2017-05-19 15:47:19 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
def post(self, request):
|
2020-03-02 17:04:54 -05:00
|
|
|
logger = logging.getLogger('netbox.auth.login')
|
2016-03-01 11:23:03 -05:00
|
|
|
form = LoginForm(request, data=request.POST)
|
2020-03-02 17:04:54 -05:00
|
|
|
|
2016-03-01 11:23:03 -05:00
|
|
|
if form.is_valid():
|
2020-03-02 17:04:54 -05:00
|
|
|
logger.debug("Login form validation was successful")
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2019-04-29 11:04:32 -04:00
|
|
|
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
|
|
|
|
# last_login time upon authentication.
|
2021-10-26 13:41:56 -04:00
|
|
|
if get_config().MAINTENANCE_MODE:
|
2020-03-02 17:04:54 -05:00
|
|
|
logger.warning("Maintenance mode enabled: disabling update of most recent login time")
|
2019-04-29 11:04:32 -04:00
|
|
|
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
|
|
|
|
|
2016-03-01 11:23:03 -05:00
|
|
|
# Authenticate user
|
|
|
|
auth_login(request, form.get_user())
|
2020-03-02 17:04:54 -05:00
|
|
|
logger.info(f"User {request.user} successfully authenticated")
|
2022-07-15 09:42:12 -04:00
|
|
|
messages.info(request, f"Logged in as {request.user}.")
|
|
|
|
|
|
|
|
# Ensure the user has a UserConfig defined. (This should normally be handled by
|
|
|
|
# create_userconfig() on user creation.)
|
|
|
|
if not hasattr(request.user, 'config'):
|
|
|
|
config = get_config()
|
|
|
|
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2020-09-04 16:09:05 -04:00
|
|
|
return self.redirect_to_next(request, logger)
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2020-03-02 17:04:54 -05:00
|
|
|
else:
|
|
|
|
logger.debug("Login form validation failed")
|
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
return render(request, self.template_name, {
|
|
|
|
'form': form,
|
2021-10-29 17:06:14 -04:00
|
|
|
'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS),
|
2017-05-19 15:47:19 -04:00
|
|
|
})
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2020-09-04 16:09:05 -04:00
|
|
|
def redirect_to_next(self, request, logger):
|
2022-02-01 13:31:53 -05:00
|
|
|
data = request.POST if request.method == "POST" else request.GET
|
|
|
|
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
|
2020-09-04 16:09:05 -04:00
|
|
|
|
2022-08-08 14:21:42 -04:00
|
|
|
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
|
2022-02-01 13:31:53 -05:00
|
|
|
logger.debug(f"Redirecting user to {redirect_url}")
|
|
|
|
else:
|
|
|
|
if redirect_url:
|
|
|
|
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
|
|
|
|
redirect_url = reverse('home')
|
2020-09-04 16:09:05 -04:00
|
|
|
|
2022-02-01 13:31:53 -05:00
|
|
|
return HttpResponseRedirect(redirect_url)
|
2020-09-04 16:09:05 -04:00
|
|
|
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
class LogoutView(View):
|
2020-03-02 17:04:54 -05:00
|
|
|
"""
|
|
|
|
Deauthenticate a web user.
|
|
|
|
"""
|
2021-04-25 20:11:46 -07:00
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
def get(self, request):
|
2020-03-02 17:04:54 -05:00
|
|
|
logger = logging.getLogger('netbox.auth.logout')
|
2017-12-05 14:19:24 -05:00
|
|
|
|
|
|
|
# Log out the user
|
2020-03-02 17:04:54 -05:00
|
|
|
username = request.user
|
2017-05-19 15:47:19 -04:00
|
|
|
auth_logout(request)
|
2020-03-02 17:04:54 -05:00
|
|
|
logger.info(f"User {username} has logged out")
|
2017-05-24 11:33:11 -04:00
|
|
|
messages.info(request, "You have logged out.")
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2017-12-05 14:19:24 -05:00
|
|
|
# Delete session key cookie (if set) upon logout
|
|
|
|
response = HttpResponseRedirect(reverse('home'))
|
|
|
|
response.delete_cookie('session_key')
|
|
|
|
|
|
|
|
return response
|
2016-03-01 11:23:03 -05:00
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# User profiles
|
|
|
|
#
|
|
|
|
|
2019-04-11 17:27:38 -04:00
|
|
|
class ProfileView(LoginRequiredMixin, View):
|
2017-05-19 15:47:19 -04:00
|
|
|
template_name = 'users/profile.html'
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
def get(self, request):
|
|
|
|
|
2021-12-08 16:32:31 -05:00
|
|
|
# Compile changelog table
|
|
|
|
changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=request.user).prefetch_related(
|
|
|
|
'changed_object_type'
|
|
|
|
)[:20]
|
|
|
|
changelog_table = ObjectChangeTable(changelog)
|
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
return render(request, self.template_name, {
|
2021-12-08 16:32:31 -05:00
|
|
|
'changelog_table': changelog_table,
|
2017-05-19 15:47:19 -04:00
|
|
|
'active_tab': 'profile',
|
|
|
|
})
|
2016-03-01 11:23:03 -05:00
|
|
|
|
|
|
|
|
2020-04-24 10:29:06 -04:00
|
|
|
class UserConfigView(LoginRequiredMixin, View):
|
|
|
|
template_name = 'users/preferences.html'
|
|
|
|
|
|
|
|
def get(self, request):
|
2021-12-21 16:29:01 -05:00
|
|
|
userconfig = request.user.config
|
|
|
|
form = UserConfigForm(instance=userconfig)
|
2020-04-24 10:29:06 -04:00
|
|
|
|
|
|
|
return render(request, self.template_name, {
|
2021-12-21 16:29:01 -05:00
|
|
|
'form': form,
|
2020-04-24 10:29:06 -04:00
|
|
|
'active_tab': 'preferences',
|
|
|
|
})
|
|
|
|
|
|
|
|
def post(self, request):
|
|
|
|
userconfig = request.user.config
|
2021-12-21 16:29:01 -05:00
|
|
|
form = UserConfigForm(request.POST, instance=userconfig)
|
|
|
|
|
|
|
|
if form.is_valid():
|
|
|
|
form.save()
|
|
|
|
|
|
|
|
messages.success(request, "Your preferences have been updated.")
|
2022-07-11 15:43:59 -04:00
|
|
|
return redirect('users:preferences')
|
2021-12-21 16:29:01 -05:00
|
|
|
|
|
|
|
return render(request, self.template_name, {
|
|
|
|
'form': form,
|
|
|
|
'active_tab': 'preferences',
|
|
|
|
})
|
2020-04-24 10:29:06 -04:00
|
|
|
|
|
|
|
|
2019-04-11 17:27:38 -04:00
|
|
|
class ChangePasswordView(LoginRequiredMixin, View):
|
2021-08-24 15:24:03 -04:00
|
|
|
template_name = 'users/password.html'
|
2017-05-19 15:47:19 -04:00
|
|
|
|
|
|
|
def get(self, request):
|
2019-11-03 16:05:53 +03:00
|
|
|
# LDAP users cannot change their password here
|
2019-12-11 07:03:39 +00:00
|
|
|
if getattr(request.user, 'ldap_username', None):
|
2019-11-06 10:01:42 -05:00
|
|
|
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
|
2022-07-11 15:43:59 -04:00
|
|
|
return redirect('users:profile')
|
2019-11-03 16:05:53 +03:00
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
form = PasswordChangeForm(user=request.user)
|
|
|
|
|
|
|
|
return render(request, self.template_name, {
|
|
|
|
'form': form,
|
2021-08-24 15:24:03 -04:00
|
|
|
'active_tab': 'password',
|
2017-05-19 15:47:19 -04:00
|
|
|
})
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
def post(self, request):
|
2016-03-01 11:23:03 -05:00
|
|
|
form = PasswordChangeForm(user=request.user, data=request.POST)
|
|
|
|
if form.is_valid():
|
|
|
|
form.save()
|
|
|
|
update_session_auth_hash(request, form.user)
|
2017-05-24 11:33:11 -04:00
|
|
|
messages.success(request, "Your password has been changed successfully.")
|
2022-07-11 15:43:59 -04:00
|
|
|
return redirect('users:profile')
|
2016-03-01 11:23:03 -05:00
|
|
|
|
2017-05-19 15:47:19 -04:00
|
|
|
return render(request, self.template_name, {
|
|
|
|
'form': form,
|
|
|
|
'active_tab': 'change_password',
|
|
|
|
})
|
2016-03-01 11:23:03 -05:00
|
|
|
|
|
|
|
|
2017-03-07 23:30:53 -05:00
|
|
|
#
|
|
|
|
# API tokens
|
|
|
|
#
|
|
|
|
|
2017-03-08 11:34:47 -05:00
|
|
|
class TokenListView(LoginRequiredMixin, View):
|
2017-03-07 23:30:53 -05:00
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
|
|
|
tokens = Token.objects.filter(user=request.user)
|
2022-07-11 15:43:59 -04:00
|
|
|
table = TokenTable(tokens)
|
|
|
|
table.configure(request)
|
2017-03-07 23:30:53 -05:00
|
|
|
|
|
|
|
return render(request, 'users/api_tokens.html', {
|
|
|
|
'tokens': tokens,
|
2021-08-24 15:24:03 -04:00
|
|
|
'active_tab': 'api-tokens',
|
2022-07-11 15:43:59 -04:00
|
|
|
'table': table,
|
2017-03-07 23:30:53 -05:00
|
|
|
})
|
2017-03-08 11:34:47 -05:00
|
|
|
|
|
|
|
|
|
|
|
class TokenEditView(LoginRequiredMixin, View):
|
|
|
|
|
|
|
|
def get(self, request, pk=None):
|
|
|
|
|
2021-03-31 13:25:06 -04:00
|
|
|
if pk:
|
2017-03-08 11:34:47 -05:00
|
|
|
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
|
|
|
else:
|
|
|
|
token = Token(user=request.user)
|
|
|
|
|
|
|
|
form = TokenForm(instance=token)
|
|
|
|
|
2020-11-11 16:58:29 -05:00
|
|
|
return render(request, 'generic/object_edit.html', {
|
2022-02-11 12:28:49 -05:00
|
|
|
'object': token,
|
2017-03-08 11:34:47 -05:00
|
|
|
'form': form,
|
2022-07-11 15:43:59 -04:00
|
|
|
'return_url': reverse('users:token_list'),
|
2017-03-08 11:34:47 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
def post(self, request, pk=None):
|
|
|
|
|
2021-03-31 13:25:06 -04:00
|
|
|
if pk:
|
2017-03-08 11:34:47 -05:00
|
|
|
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
|
|
|
form = TokenForm(request.POST, instance=token)
|
|
|
|
else:
|
2021-03-31 13:25:06 -04:00
|
|
|
token = Token(user=request.user)
|
2017-03-08 11:34:47 -05:00
|
|
|
form = TokenForm(request.POST)
|
|
|
|
|
|
|
|
if form.is_valid():
|
|
|
|
token = form.save(commit=False)
|
|
|
|
token.user = request.user
|
|
|
|
token.save()
|
|
|
|
|
2021-03-31 13:25:06 -04:00
|
|
|
msg = f"Modified token {token}" if pk else f"Created token {token}"
|
2017-03-08 11:34:47 -05:00
|
|
|
messages.success(request, msg)
|
|
|
|
|
2017-05-10 22:22:49 -04:00
|
|
|
if '_addanother' in request.POST:
|
|
|
|
return redirect(request.path)
|
|
|
|
else:
|
2022-07-11 15:43:59 -04:00
|
|
|
return redirect('users:token_list')
|
2017-03-08 11:34:47 -05:00
|
|
|
|
2020-11-11 16:58:29 -05:00
|
|
|
return render(request, 'generic/object_edit.html', {
|
2022-02-11 12:28:49 -05:00
|
|
|
'object': token,
|
2017-07-19 11:03:13 -04:00
|
|
|
'form': form,
|
2022-07-11 15:43:59 -04:00
|
|
|
'return_url': reverse('users:token_list'),
|
2017-07-19 11:03:13 -04:00
|
|
|
})
|
|
|
|
|
2017-03-08 11:34:47 -05:00
|
|
|
|
2020-06-02 15:36:31 -04:00
|
|
|
class TokenDeleteView(LoginRequiredMixin, View):
|
2017-03-08 11:34:47 -05:00
|
|
|
|
|
|
|
def get(self, request, pk):
|
|
|
|
|
|
|
|
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
|
|
|
initial_data = {
|
2022-07-11 15:43:59 -04:00
|
|
|
'return_url': reverse('users:token_list'),
|
2017-03-08 11:34:47 -05:00
|
|
|
}
|
|
|
|
form = ConfirmationForm(initial=initial_data)
|
|
|
|
|
2020-11-11 16:58:29 -05:00
|
|
|
return render(request, 'generic/object_delete.html', {
|
2022-02-11 12:28:49 -05:00
|
|
|
'object': token,
|
2017-03-08 11:34:47 -05:00
|
|
|
'form': form,
|
2022-07-11 15:43:59 -04:00
|
|
|
'return_url': reverse('users:token_list'),
|
2017-03-08 11:34:47 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
def post(self, request, pk):
|
|
|
|
|
|
|
|
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
|
|
|
form = ConfirmationForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
|
|
token.delete()
|
|
|
|
messages.success(request, "Token deleted")
|
2022-07-11 15:43:59 -04:00
|
|
|
return redirect('users:token_list')
|
2017-03-08 11:34:47 -05:00
|
|
|
|
2020-11-11 16:58:29 -05:00
|
|
|
return render(request, 'generic/object_delete.html', {
|
2022-02-11 12:28:49 -05:00
|
|
|
'object': token,
|
2017-03-08 11:34:47 -05:00
|
|
|
'form': form,
|
2022-07-11 15:43:59 -04:00
|
|
|
'return_url': reverse('users:token_list'),
|
2017-03-08 11:34:47 -05:00
|
|
|
})
|