views.py 12 KB

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