views.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import logging
  2. from django.conf import settings
  3. from django.contrib import messages
  4. from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
  5. from django.contrib.auth.mixins import LoginRequiredMixin
  6. from django.contrib.auth.models import update_last_login
  7. from django.contrib.auth.signals import user_logged_in
  8. from django.http import HttpResponseRedirect
  9. from django.shortcuts import get_object_or_404, redirect, render
  10. from django.urls import reverse
  11. from django.utils.decorators import method_decorator
  12. from django.views.decorators.debug import sensitive_post_parameters
  13. from django.views.generic import View
  14. from social_core.backends.utils import load_backends
  15. from extras.models import ObjectChange
  16. from extras.tables import ObjectChangeTable
  17. from netbox.config import get_config
  18. from utilities.forms import ConfirmationForm
  19. from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
  20. from .models import Token
  21. #
  22. # Login/logout
  23. #
  24. class LoginView(View):
  25. """
  26. Perform user authentication via the web UI.
  27. """
  28. template_name = 'login.html'
  29. @method_decorator(sensitive_post_parameters('password'))
  30. def dispatch(self, *args, **kwargs):
  31. return super().dispatch(*args, **kwargs)
  32. def get(self, request):
  33. form = LoginForm(request)
  34. if request.user.is_authenticated:
  35. logger = logging.getLogger('netbox.auth.login')
  36. return self.redirect_to_next(request, logger)
  37. return render(request, self.template_name, {
  38. 'form': form,
  39. 'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS),
  40. })
  41. def post(self, request):
  42. logger = logging.getLogger('netbox.auth.login')
  43. form = LoginForm(request, data=request.POST)
  44. if form.is_valid():
  45. logger.debug("Login form validation was successful")
  46. # If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
  47. # last_login time upon authentication.
  48. if get_config().MAINTENANCE_MODE:
  49. logger.warning("Maintenance mode enabled: disabling update of most recent login time")
  50. user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
  51. # Authenticate user
  52. auth_login(request, form.get_user())
  53. logger.info(f"User {request.user} successfully authenticated")
  54. messages.info(request, "Logged in as {}.".format(request.user))
  55. return self.redirect_to_next(request, logger)
  56. else:
  57. logger.debug("Login form validation failed")
  58. return render(request, self.template_name, {
  59. 'form': form,
  60. 'auth_backends': load_backends(settings.AUTHENTICATION_BACKENDS),
  61. })
  62. def redirect_to_next(self, request, logger):
  63. data = request.POST if request.method == "POST" else request.GET
  64. redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
  65. if redirect_url and redirect_url.startswith('/'):
  66. logger.debug(f"Redirecting user to {redirect_url}")
  67. else:
  68. if redirect_url:
  69. logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
  70. redirect_url = reverse('home')
  71. return HttpResponseRedirect(redirect_url)
  72. class LogoutView(View):
  73. """
  74. Deauthenticate a web user.
  75. """
  76. def get(self, request):
  77. logger = logging.getLogger('netbox.auth.logout')
  78. # Log out the user
  79. username = request.user
  80. auth_logout(request)
  81. logger.info(f"User {username} has logged out")
  82. messages.info(request, "You have logged out.")
  83. # Delete session key cookie (if set) upon logout
  84. response = HttpResponseRedirect(reverse('home'))
  85. response.delete_cookie('session_key')
  86. return response
  87. #
  88. # User profiles
  89. #
  90. class ProfileView(LoginRequiredMixin, View):
  91. template_name = 'users/profile.html'
  92. def get(self, request):
  93. # Compile changelog table
  94. changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=request.user).prefetch_related(
  95. 'changed_object_type'
  96. )[:20]
  97. changelog_table = ObjectChangeTable(changelog)
  98. return render(request, self.template_name, {
  99. 'changelog_table': changelog_table,
  100. 'active_tab': 'profile',
  101. })
  102. class UserConfigView(LoginRequiredMixin, View):
  103. template_name = 'users/preferences.html'
  104. def get(self, request):
  105. userconfig = request.user.config
  106. form = UserConfigForm(instance=userconfig)
  107. return render(request, self.template_name, {
  108. 'form': form,
  109. 'active_tab': 'preferences',
  110. })
  111. def post(self, request):
  112. userconfig = request.user.config
  113. form = UserConfigForm(request.POST, instance=userconfig)
  114. if form.is_valid():
  115. form.save()
  116. messages.success(request, "Your preferences have been updated.")
  117. return redirect('user:preferences')
  118. return render(request, self.template_name, {
  119. 'form': form,
  120. 'active_tab': 'preferences',
  121. })
  122. class ChangePasswordView(LoginRequiredMixin, View):
  123. template_name = 'users/password.html'
  124. def get(self, request):
  125. # LDAP users cannot change their password here
  126. if getattr(request.user, 'ldap_username', None):
  127. messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
  128. return redirect('user:profile')
  129. form = PasswordChangeForm(user=request.user)
  130. return render(request, self.template_name, {
  131. 'form': form,
  132. 'active_tab': 'password',
  133. })
  134. def post(self, request):
  135. form = PasswordChangeForm(user=request.user, data=request.POST)
  136. if form.is_valid():
  137. form.save()
  138. update_session_auth_hash(request, form.user)
  139. messages.success(request, "Your password has been changed successfully.")
  140. return redirect('user:profile')
  141. return render(request, self.template_name, {
  142. 'form': form,
  143. 'active_tab': 'change_password',
  144. })
  145. #
  146. # API tokens
  147. #
  148. class TokenListView(LoginRequiredMixin, View):
  149. def get(self, request):
  150. tokens = Token.objects.filter(user=request.user)
  151. return render(request, 'users/api_tokens.html', {
  152. 'tokens': tokens,
  153. 'active_tab': 'api-tokens',
  154. })
  155. class TokenEditView(LoginRequiredMixin, View):
  156. def get(self, request, pk=None):
  157. if pk:
  158. token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
  159. else:
  160. token = Token(user=request.user)
  161. form = TokenForm(instance=token)
  162. return render(request, 'generic/object_edit.html', {
  163. 'object': token,
  164. 'form': form,
  165. 'return_url': reverse('user:token_list'),
  166. })
  167. def post(self, request, pk=None):
  168. if pk:
  169. token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
  170. form = TokenForm(request.POST, instance=token)
  171. else:
  172. token = Token(user=request.user)
  173. form = TokenForm(request.POST)
  174. if form.is_valid():
  175. token = form.save(commit=False)
  176. token.user = request.user
  177. token.save()
  178. msg = f"Modified token {token}" if pk else f"Created token {token}"
  179. messages.success(request, msg)
  180. if '_addanother' in request.POST:
  181. return redirect(request.path)
  182. else:
  183. return redirect('user:token_list')
  184. return render(request, 'generic/object_edit.html', {
  185. 'object': token,
  186. 'form': form,
  187. 'return_url': reverse('user:token_list'),
  188. })
  189. class TokenDeleteView(LoginRequiredMixin, View):
  190. def get(self, request, pk):
  191. token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
  192. initial_data = {
  193. 'return_url': reverse('user:token_list'),
  194. }
  195. form = ConfirmationForm(initial=initial_data)
  196. return render(request, 'generic/object_delete.html', {
  197. 'object': token,
  198. 'form': form,
  199. 'return_url': reverse('user:token_list'),
  200. })
  201. def post(self, request, pk):
  202. token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
  203. form = ConfirmationForm(request.POST)
  204. if form.is_valid():
  205. token.delete()
  206. messages.success(request, "Token deleted")
  207. return redirect('user:token_list')
  208. return render(request, 'generic/object_delete.html', {
  209. 'object': token,
  210. 'form': form,
  211. 'return_url': reverse('user:token_list'),
  212. })