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.forms import AuthenticationForm, PasswordChangeForm from django.contrib.auth.mixins import LoginRequiredMixin 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 urlencode from django.utils.translation import gettext_lazy as _ 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 core.models import ObjectChange from core.tables import ObjectChangeTable from extras.models import Bookmark from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable 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 forms, tables from users.models import UserConfig from utilities.request import safe_for_redirect from utilities.string import remove_linebreaks 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_source = get_auth_backend_display(name) icon_name = None icon_img = None if icon_source: if '://' in icon_source: icon_img = icon_source else: icon_name = icon_source return { 'display_name': display_name, 'icon_name': icon_name, 'icon_img': icon_img, '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 = AuthenticationForm(request) if request.user.is_authenticated: logger = logging.getLogger('netbox.auth.login') return self.redirect_to_next(request, logger) login_form_hidden = settings.LOGIN_FORM_HIDDEN return render(request, self.template_name, { 'form': form, 'auth_backends': self.get_auth_backends(request), 'login_form_hidden': login_form_hidden, }) def post(self, request): logger = logging.getLogger('netbox.auth.login') form = AuthenticationForm(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, _("Logged in as {user}.").format(user=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'): request.user.config = get_config() UserConfig(user=request.user, data=request.user.config.DEFAULT_USER_PREFERENCES).save() response = self.redirect_to_next(request, logger) # Set the user's preferred language (if any) if language := request.user.config.get('locale.language'): response.set_cookie( key=settings.LANGUAGE_COOKIE_NAME, value=language, max_age=request.session.get_expiry_age(), secure=settings.SESSION_COOKIE_SECURE, ) return response else: username = form['username'].value() logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}") 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 safe_for_redirect(redirect_url): logger.debug(f"Redirecting user to {remove_linebreaks(redirect_url)}") else: if redirect_url: logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {remove_linebreaks(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 & language cookies (if set) upon logout response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL)) response.delete_cookie('session_key') response.delete_cookie(settings.LANGUAGE_COOKIE_NAME) return response # # User profiles # class ProfileView(LoginRequiredMixin, View): template_name = 'account/profile.html' def get(self, request): # Compile changelog table changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=request.user)[:20] changelog_table = ObjectChangeTable(changelog) changelog_table.orderable = False changelog_table.configure(request) return render(request, self.template_name, { 'changelog_table': changelog_table, 'active_tab': 'profile', }) class UserConfigView(LoginRequiredMixin, View): template_name = 'account/preferences.html' def get(self, request): userconfig = request.user.config form = forms.UserConfigForm(instance=userconfig) return render(request, self.template_name, { 'form': form, 'active_tab': 'preferences', }) def post(self, request): userconfig = request.user.config form = forms.UserConfigForm(request.POST, instance=userconfig) if form.is_valid(): form.save() messages.success(request, _("Your preferences have been updated.")) response = redirect('account:preferences') # Set/clear language cookie if language := form.cleaned_data['locale.language']: response.set_cookie( key=settings.LANGUAGE_COOKIE_NAME, value=language, max_age=request.session.get_expiry_age(), secure=settings.SESSION_COOKIE_SECURE, ) else: response.delete_cookie(settings.LANGUAGE_COOKIE_NAME) return response return render(request, self.template_name, { 'form': form, 'active_tab': 'preferences', }) class ChangePasswordView(LoginRequiredMixin, View): template_name = 'account/password.html' def get(self, request): # LDAP users cannot change their password here if getattr(request.user, 'ldap_username', None): messages.warning(request, _("LDAP-authenticated user credentials cannot be changed within NetBox.")) return redirect('account:profile') form = PasswordChangeForm(user=request.user) return render(request, self.template_name, { 'form': form, 'active_tab': 'password', }) def post(self, request): form = PasswordChangeForm(user=request.user, data=request.POST) if form.is_valid(): form.save() update_session_auth_hash(request, form.user) messages.success(request, _("Your password has been changed successfully.")) return redirect('account:profile') return render(request, self.template_name, { 'form': form, 'active_tab': 'change_password', }) # # Bookmarks # class BookmarkListView(LoginRequiredMixin, generic.ObjectListView): table = BookmarkTable template_name = 'account/bookmarks.html' def get_queryset(self, request): return Bookmark.objects.filter(user=request.user) def get_extra_context(self, request): return { 'active_tab': 'bookmarks', } # # Notifications & subscriptions # class NotificationListView(LoginRequiredMixin, generic.ObjectListView): table = NotificationTable template_name = 'account/notifications.html' def get_queryset(self, request): return request.user.notifications.all() def get_extra_context(self, request): return { 'active_tab': 'notifications', } class SubscriptionListView(LoginRequiredMixin, generic.ObjectListView): table = SubscriptionTable template_name = 'account/subscriptions.html' def get_queryset(self, request): return request.user.subscriptions.all() def get_extra_context(self, request): return { 'active_tab': 'subscriptions', } # # User views for token management # class UserTokenListView(LoginRequiredMixin, View): def get(self, request): tokens = UserToken.objects.filter(user=request.user) table = tables.UserTokenTable(tokens) table.configure(request) return render(request, 'account/token_list.html', { 'tokens': tokens, 'active_tab': 'api-tokens', 'table': table, }) @register_model_view(UserToken) class UserTokenView(LoginRequiredMixin, View): def get(self, request, pk): token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk) key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None return render(request, 'account/token.html', { 'object': token, 'key': key, }) @register_model_view(UserToken, 'edit') class UserTokenEditView(generic.ObjectEditView): queryset = UserToken.objects.all() form = forms.UserTokenForm default_return_url = 'account:usertoken_list' def alter_object(self, obj, request, url_args, url_kwargs): if not obj.pk: obj.user = request.user return obj @register_model_view(UserToken, 'delete') class UserTokenDeleteView(generic.ObjectDeleteView): queryset = UserToken.objects.all() default_return_url = 'account:usertoken_list'