views.py 12 KB

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