views.py 9.0 KB

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