| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- 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.ui import layout
- from netbox.views import generic
- from users import forms
- from users.models import UserConfig
- from users.tables import TokenTable
- from users.ui.panels import TokenExamplePanel, TokenPanel
- 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 = TokenTable(tokens)
- table.columns.hide('user')
- 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):
- layout = layout.SimpleLayout(
- left_panels=[
- TokenPanel(),
- ],
- right_panels=[
- TokenExamplePanel(),
- ],
- )
- def get(self, request, pk):
- token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
- return render(request, 'account/token.html', {
- 'object': token,
- 'layout': self.layout,
- })
- @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'
|