views.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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
  5. from django.contrib.auth import update_session_auth_hash
  6. from django.contrib.auth.mixins import LoginRequiredMixin
  7. from django.contrib.auth.models import update_last_login
  8. from django.contrib.auth.signals import user_logged_in
  9. from django.http import HttpResponseRedirect
  10. from django.shortcuts import get_object_or_404, redirect
  11. from django.shortcuts import render, resolve_url
  12. from django.urls import reverse
  13. from django.utils.decorators import method_decorator
  14. from django.utils.http import url_has_allowed_host_and_scheme, urlencode
  15. from django.views.decorators.debug import sensitive_post_parameters
  16. from django.views.generic import View
  17. from social_core.backends.utils import load_backends
  18. from account.models import UserToken
  19. from extras.models import Bookmark, ObjectChange
  20. from extras.tables import BookmarkTable, ObjectChangeTable
  21. from netbox.authentication import get_auth_backend_display, get_saml_idps
  22. from netbox.config import get_config
  23. from netbox.views import generic
  24. from users import forms, tables
  25. from users.models import UserConfig
  26. from utilities.views import register_model_view
  27. #
  28. # Login/logout
  29. #
  30. class LoginView(View):
  31. """
  32. Perform user authentication via the web UI.
  33. """
  34. template_name = 'login.html'
  35. @method_decorator(sensitive_post_parameters('password'))
  36. def dispatch(self, *args, **kwargs):
  37. return super().dispatch(*args, **kwargs)
  38. def gen_auth_data(self, name, url, params):
  39. display_name, icon_name = get_auth_backend_display(name)
  40. return {
  41. 'display_name': display_name,
  42. 'icon_name': icon_name,
  43. 'url': f'{url}?{urlencode(params)}',
  44. }
  45. def get_auth_backends(self, request):
  46. auth_backends = []
  47. saml_idps = get_saml_idps()
  48. for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
  49. url = reverse('social:begin', args=[name])
  50. params = {}
  51. if next := request.GET.get('next'):
  52. params['next'] = next
  53. if name.lower() == 'saml' and saml_idps:
  54. for idp in saml_idps:
  55. params['idp'] = idp
  56. data = self.gen_auth_data(name, url, params)
  57. data['display_name'] = f'{data["display_name"]} ({idp})'
  58. auth_backends.append(data)
  59. else:
  60. auth_backends.append(self.gen_auth_data(name, url, params))
  61. return auth_backends
  62. def get(self, request):
  63. form = forms.LoginForm(request)
  64. if request.user.is_authenticated:
  65. logger = logging.getLogger('netbox.auth.login')
  66. return self.redirect_to_next(request, logger)
  67. return render(request, self.template_name, {
  68. 'form': form,
  69. 'auth_backends': self.get_auth_backends(request),
  70. })
  71. def post(self, request):
  72. logger = logging.getLogger('netbox.auth.login')
  73. form = forms.LoginForm(request, data=request.POST)
  74. if form.is_valid():
  75. logger.debug("Login form validation was successful")
  76. # If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
  77. # last_login time upon authentication.
  78. if get_config().MAINTENANCE_MODE:
  79. logger.warning("Maintenance mode enabled: disabling update of most recent login time")
  80. user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
  81. # Authenticate user
  82. auth_login(request, form.get_user())
  83. logger.info(f"User {request.user} successfully authenticated")
  84. messages.success(request, f"Logged in as {request.user}.")
  85. # Ensure the user has a UserConfig defined. (This should normally be handled by
  86. # create_userconfig() on user creation.)
  87. if not hasattr(request.user, 'config'):
  88. config = get_config()
  89. UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
  90. return self.redirect_to_next(request, logger)
  91. else:
  92. logger.debug(f"Login form validation failed for username: {form['username'].value()}")
  93. return render(request, self.template_name, {
  94. 'form': form,
  95. 'auth_backends': self.get_auth_backends(request),
  96. })
  97. def redirect_to_next(self, request, logger):
  98. data = request.POST if request.method == "POST" else request.GET
  99. redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
  100. if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
  101. logger.debug(f"Redirecting user to {redirect_url}")
  102. else:
  103. if redirect_url:
  104. logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
  105. redirect_url = reverse('home')
  106. return HttpResponseRedirect(redirect_url)
  107. class LogoutView(View):
  108. """
  109. Deauthenticate a web user.
  110. """
  111. def get(self, request):
  112. logger = logging.getLogger('netbox.auth.logout')
  113. # Log out the user
  114. username = request.user
  115. auth_logout(request)
  116. logger.info(f"User {username} has logged out")
  117. messages.info(request, "You have logged out.")
  118. # Delete session key cookie (if set) upon logout
  119. response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
  120. response.delete_cookie('session_key')
  121. return response
  122. #
  123. # User profiles
  124. #
  125. class ProfileView(LoginRequiredMixin, View):
  126. template_name = 'account/profile.html'
  127. def get(self, request):
  128. # Compile changelog table
  129. changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
  130. user=request.user
  131. ).prefetch_related(
  132. 'changed_object_type'
  133. )[:20]
  134. changelog_table = ObjectChangeTable(changelog)
  135. return render(request, self.template_name, {
  136. 'changelog_table': changelog_table,
  137. 'active_tab': 'profile',
  138. })
  139. class UserConfigView(LoginRequiredMixin, View):
  140. template_name = 'account/preferences.html'
  141. def get(self, request):
  142. userconfig = request.user.config
  143. form = forms.UserConfigForm(instance=userconfig)
  144. return render(request, self.template_name, {
  145. 'form': form,
  146. 'active_tab': 'preferences',
  147. })
  148. def post(self, request):
  149. userconfig = request.user.config
  150. form = forms.UserConfigForm(request.POST, instance=userconfig)
  151. if form.is_valid():
  152. form.save()
  153. messages.success(request, "Your preferences have been updated.")
  154. return redirect('account:preferences')
  155. return render(request, self.template_name, {
  156. 'form': form,
  157. 'active_tab': 'preferences',
  158. })
  159. class ChangePasswordView(LoginRequiredMixin, View):
  160. template_name = 'account/password.html'
  161. def get(self, request):
  162. # LDAP users cannot change their password here
  163. if getattr(request.user, 'ldap_username', None):
  164. messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
  165. return redirect('account:profile')
  166. form = forms.PasswordChangeForm(user=request.user)
  167. return render(request, self.template_name, {
  168. 'form': form,
  169. 'active_tab': 'password',
  170. })
  171. def post(self, request):
  172. form = forms.PasswordChangeForm(user=request.user, data=request.POST)
  173. if form.is_valid():
  174. form.save()
  175. update_session_auth_hash(request, form.user)
  176. messages.success(request, "Your password has been changed successfully.")
  177. return redirect('account:profile')
  178. return render(request, self.template_name, {
  179. 'form': form,
  180. 'active_tab': 'change_password',
  181. })
  182. #
  183. # Bookmarks
  184. #
  185. class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
  186. table = BookmarkTable
  187. template_name = 'account/bookmarks.html'
  188. def get_queryset(self, request):
  189. return Bookmark.objects.filter(user=request.user)
  190. def get_extra_context(self, request):
  191. return {
  192. 'active_tab': 'bookmarks',
  193. }
  194. #
  195. # User views for token management
  196. #
  197. class UserTokenListView(LoginRequiredMixin, View):
  198. def get(self, request):
  199. tokens = UserToken.objects.filter(user=request.user)
  200. table = tables.UserTokenTable(tokens)
  201. table.configure(request)
  202. return render(request, 'account/token_list.html', {
  203. 'tokens': tokens,
  204. 'active_tab': 'api-tokens',
  205. 'table': table,
  206. })
  207. @register_model_view(UserToken)
  208. class UserTokenView(LoginRequiredMixin, View):
  209. def get(self, request, pk):
  210. token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
  211. key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None
  212. return render(request, 'account/token.html', {
  213. 'object': token,
  214. 'key': key,
  215. })
  216. @register_model_view(UserToken, 'edit')
  217. class UserTokenEditView(generic.ObjectEditView):
  218. queryset = UserToken.objects.all()
  219. form = forms.UserTokenForm
  220. default_return_url = 'account:usertoken_list'
  221. def alter_object(self, obj, request, url_args, url_kwargs):
  222. if not obj.pk:
  223. obj.user = request.user
  224. return obj
  225. @register_model_view(UserToken, 'delete')
  226. class UserTokenDeleteView(generic.ObjectDeleteView):
  227. queryset = UserToken.objects.all()
  228. default_return_url = 'account:usertoken_list'