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