From 14786648dd8987e51c2ef2e33c7a3d80d7bacfcc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 31 Jul 2023 09:00:59 -0400 Subject: [PATCH] Move login & logout views to account app --- netbox/account/views.py | 143 +++++++++++++++++++++++++++++++++++++++- netbox/netbox/urls.py | 5 +- netbox/users/views.py | 142 +-------------------------------------- 3 files changed, 143 insertions(+), 147 deletions(-) diff --git a/netbox/account/views.py b/netbox/account/views.py index fbced593a..3156b2102 100644 --- a/netbox/account/views.py +++ b/netbox/account/views.py @@ -1,19 +1,156 @@ +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 from django.contrib.auth import update_session_auth_hash from django.contrib.auth.mixins import LoginRequiredMixin -from django.shortcuts import get_object_or_404, redirect, render +from django.contrib.auth.models import update_last_login +from django.contrib.auth.signals import user_logged_in +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import render, resolve_url +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme, urlencode +from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import View +from social_core.backends.utils import load_backends from account.models import UserToken from extras.models import Bookmark, ObjectChange from extras.tables import BookmarkTable, ObjectChangeTable +from netbox.authentication import get_auth_backend_display, get_saml_idps +from netbox.config import get_config from netbox.views import generic -from users import filtersets, forms, tables -from users.models import Token +from users import forms, tables +from users.models import UserConfig from utilities.views import register_model_view +# +# Login/logout +# + +class LoginView(View): + """ + Perform user authentication via the web UI. + """ + template_name = 'login.html' + + @method_decorator(sensitive_post_parameters('password')) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + def gen_auth_data(self, name, url, params): + display_name, icon_name = get_auth_backend_display(name) + return { + 'display_name': display_name, + 'icon_name': icon_name, + 'url': f'{url}?{urlencode(params)}', + } + + def get_auth_backends(self, request): + auth_backends = [] + saml_idps = get_saml_idps() + + for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys(): + url = reverse('social:begin', args=[name]) + params = {} + if next := request.GET.get('next'): + params['next'] = next + if name.lower() == 'saml' and saml_idps: + for idp in saml_idps: + params['idp'] = idp + data = self.gen_auth_data(name, url, params) + data['display_name'] = f'{data["display_name"]} ({idp})' + auth_backends.append(data) + else: + auth_backends.append(self.gen_auth_data(name, url, params)) + + return auth_backends + + def get(self, request): + form = forms.LoginForm(request) + + if request.user.is_authenticated: + logger = logging.getLogger('netbox.auth.login') + return self.redirect_to_next(request, logger) + + return render(request, self.template_name, { + 'form': form, + 'auth_backends': self.get_auth_backends(request), + }) + + def post(self, request): + logger = logging.getLogger('netbox.auth.login') + form = forms.LoginForm(request, data=request.POST) + + if form.is_valid(): + logger.debug("Login form validation was successful") + + # If maintenance mode is enabled, assume the database is read-only, and disable updating the user's + # last_login time upon authentication. + if get_config().MAINTENANCE_MODE: + logger.warning("Maintenance mode enabled: disabling update of most recent login time") + user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login') + + # Authenticate user + auth_login(request, form.get_user()) + logger.info(f"User {request.user} successfully authenticated") + messages.success(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() + + return self.redirect_to_next(request, logger) + + else: + logger.debug(f"Login form validation failed for username: {form['username'].value()}") + + return render(request, self.template_name, { + 'form': form, + 'auth_backends': self.get_auth_backends(request), + }) + + def redirect_to_next(self, request, logger): + data = request.POST if request.method == "POST" else request.GET + redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL) + + if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None): + 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') + + return HttpResponseRedirect(redirect_url) + + +class LogoutView(View): + """ + Deauthenticate a web user. + """ + + def get(self, request): + logger = logging.getLogger('netbox.auth.logout') + + # Log out the user + username = request.user + auth_logout(request) + logger.info(f"User {username} has logged out") + messages.info(request, "You have logged out.") + + # Delete session key cookie (if set) upon logout + response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL)) + response.delete_cookie('session_key') + + return response + + # # User profiles # diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 842fb7cb8..595a9001f 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,19 +1,18 @@ from django.conf import settings from django.conf.urls import include -from django.urls import path, re_path +from django.urls import path from django.views.decorators.csrf import csrf_exempt from django.views.static import serve from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from account.views import LoginView, LogoutView from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns from netbox.api.views import APIRootView, StatusView from netbox.graphql.schema import schema from netbox.graphql.views import GraphQLView from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx -from users.views import LoginView, LogoutView from .admin import admin_site - _patterns = [ # Base views diff --git a/netbox/users/views.py b/netbox/users/views.py index f141520dd..7ff9a8a4d 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -1,151 +1,11 @@ -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 -from django.contrib.auth.models import update_last_login -from django.contrib.auth.signals import user_logged_in from django.db.models import Count -from django.http import HttpResponseRedirect -from django.shortcuts import render, resolve_url -from django.urls import reverse -from django.utils.decorators import method_decorator -from django.utils.http import url_has_allowed_host_and_scheme, urlencode -from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic import View -from social_core.backends.utils import load_backends from extras.models import ObjectChange from extras.tables import ObjectChangeTable -from netbox.authentication import get_auth_backend_display, get_saml_idps -from netbox.config import get_config from netbox.views import generic from utilities.views import register_model_view from . import filtersets, forms, tables -from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserConfig - - -# -# Login/logout -# - -class LoginView(View): - """ - Perform user authentication via the web UI. - """ - template_name = 'login.html' - - @method_decorator(sensitive_post_parameters('password')) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - - def gen_auth_data(self, name, url, params): - display_name, icon_name = get_auth_backend_display(name) - return { - 'display_name': display_name, - 'icon_name': icon_name, - 'url': f'{url}?{urlencode(params)}', - } - - def get_auth_backends(self, request): - auth_backends = [] - saml_idps = get_saml_idps() - - for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys(): - url = reverse('social:begin', args=[name]) - params = {} - if next := request.GET.get('next'): - params['next'] = next - if name.lower() == 'saml' and saml_idps: - for idp in saml_idps: - params['idp'] = idp - data = self.gen_auth_data(name, url, params) - data['display_name'] = f'{data["display_name"]} ({idp})' - auth_backends.append(data) - else: - auth_backends.append(self.gen_auth_data(name, url, params)) - - return auth_backends - - def get(self, request): - form = forms.LoginForm(request) - - if request.user.is_authenticated: - logger = logging.getLogger('netbox.auth.login') - return self.redirect_to_next(request, logger) - - return render(request, self.template_name, { - 'form': form, - 'auth_backends': self.get_auth_backends(request), - }) - - def post(self, request): - logger = logging.getLogger('netbox.auth.login') - form = forms.LoginForm(request, data=request.POST) - - if form.is_valid(): - logger.debug("Login form validation was successful") - - # If maintenance mode is enabled, assume the database is read-only, and disable updating the user's - # last_login time upon authentication. - if get_config().MAINTENANCE_MODE: - logger.warning("Maintenance mode enabled: disabling update of most recent login time") - user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login') - - # Authenticate user - auth_login(request, form.get_user()) - logger.info(f"User {request.user} successfully authenticated") - messages.success(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() - - return self.redirect_to_next(request, logger) - - else: - logger.debug(f"Login form validation failed for username: {form['username'].value()}") - - return render(request, self.template_name, { - 'form': form, - 'auth_backends': self.get_auth_backends(request), - }) - - def redirect_to_next(self, request, logger): - data = request.POST if request.method == "POST" else request.GET - redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL) - - if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None): - 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') - - return HttpResponseRedirect(redirect_url) - - -class LogoutView(View): - """ - Deauthenticate a web user. - """ - - def get(self, request): - logger = logging.getLogger('netbox.auth.logout') - - # Log out the user - username = request.user - auth_logout(request) - logger.info(f"User {username} has logged out") - messages.info(request, "You have logged out.") - - # Delete session key cookie (if set) upon logout - response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL)) - response.delete_cookie('session_key') - - return response +from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token #