1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #13309: Introduce the account app (#13310)

* Introduce 'accounts' app for user-specific views & resources
* Move UserTokenTable to account app
* Move login & logout views to account app
This commit is contained in:
Jeremy Stretch
2023-07-31 09:22:04 -04:00
committed by GitHub
parent 9c6c3d3dd4
commit 80376abedf
21 changed files with 434 additions and 416 deletions

View File

View File

@ -1,10 +1,12 @@
# Generated by Django 4.1.10 on 2023-07-25 15:19
# Generated by Django 4.1.10 on 2023-07-30 17:49
from django.db import migrations
class Migration(migrations.Migration):
initial = True
dependencies = [
('users', '0004_netboxgroup_netboxuser'),
]
@ -15,10 +17,10 @@ class Migration(migrations.Migration):
fields=[
],
options={
'verbose_name': 'token',
'proxy': True,
'indexes': [],
'constraints': [],
'verbose_name': 'token',
},
bases=('users.token',),
),

View File

15
netbox/account/models.py Normal file
View File

@ -0,0 +1,15 @@
from django.urls import reverse
from users.models import Token
class UserToken(Token):
"""
Proxy model for users to manage their own API tokens.
"""
class Meta:
proxy = True
verbose_name = 'token'
def get_absolute_url(self):
return reverse('account:usertoken', args=[self.pk])

55
netbox/account/tables.py Normal file
View File

@ -0,0 +1,55 @@
from django.utils.translation import gettext as _
from account.models import UserToken
from netbox.tables import NetBoxTable, columns
__all__ = (
'UserTokenTable',
)
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ record }}</span></samp>"""
ALLOWED_IPS = """{{ value|join:", " }}"""
COPY_BUTTON = """
{% if settings.ALLOW_TOKEN_RETRIEVAL %}
{% copy_content record.pk prefix="token_" color="success" %}
{% endif %}
"""
class UserTokenTable(NetBoxTable):
"""
Table for users to manager their own API tokens under account views.
"""
key = columns.TemplateColumn(
verbose_name=_('Key'),
template_code=TOKEN,
)
write_enabled = columns.BooleanColumn(
verbose_name=_('Write Enabled')
)
created = columns.DateColumn(
verbose_name=_('Created'),
)
expires = columns.DateColumn(
verbose_name=_('Expires'),
)
last_used = columns.DateTimeColumn(
verbose_name=_('Last Used'),
)
allowed_ips = columns.TemplateColumn(
verbose_name=_('Allowed IPs'),
template_code=ALLOWED_IPS
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=COPY_BUTTON
)
class Meta(NetBoxTable.Meta):
model = UserToken
fields = (
'pk', 'id', 'key', 'description', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips',
)

View File

@ -1,5 +1,6 @@
from django.urls import path
from django.urls import include, path
from utilities.urls import get_model_urls
from . import views
app_name = 'account'
@ -12,8 +13,6 @@ urlpatterns = [
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'),
path('api-tokens/<int:pk>/', views.UserTokenView.as_view(), name='usertoken'),
path('api-tokens/<int:pk>/edit/', views.UserTokenEditView.as_view(), name='usertoken_edit'),
path('api-tokens/<int:pk>/delete/', views.UserTokenDeleteView.as_view(), name='usertoken_delete'),
path('api-tokens/<int:pk>/', include(get_model_urls('account', 'usertoken'))),
]

298
netbox/account/views.py Normal file
View File

@ -0,0 +1,298 @@
import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
from social_core.backends.utils import load_backends
from account.models import UserToken
from extras.models import Bookmark, ObjectChange
from extras.tables import BookmarkTable, ObjectChangeTable
from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config
from netbox.views import generic
from users import forms, tables
from users.models import UserConfig
from utilities.views import register_model_view
#
# Login/logout
#
class LoginView(View):
"""
Perform user authentication via the web UI.
"""
template_name = 'login.html'
@method_decorator(sensitive_post_parameters('password'))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def gen_auth_data(self, name, url, params):
display_name, icon_name = get_auth_backend_display(name)
return {
'display_name': display_name,
'icon_name': icon_name,
'url': f'{url}?{urlencode(params)}',
}
def get_auth_backends(self, request):
auth_backends = []
saml_idps = get_saml_idps()
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
url = reverse('social:begin', args=[name])
params = {}
if next := request.GET.get('next'):
params['next'] = next
if name.lower() == 'saml' and saml_idps:
for idp in saml_idps:
params['idp'] = idp
data = self.gen_auth_data(name, url, params)
data['display_name'] = f'{data["display_name"]} ({idp})'
auth_backends.append(data)
else:
auth_backends.append(self.gen_auth_data(name, url, params))
return auth_backends
def get(self, request):
form = forms.LoginForm(request)
if request.user.is_authenticated:
logger = logging.getLogger('netbox.auth.login')
return self.redirect_to_next(request, logger)
return render(request, self.template_name, {
'form': form,
'auth_backends': self.get_auth_backends(request),
})
def post(self, request):
logger = logging.getLogger('netbox.auth.login')
form = forms.LoginForm(request, data=request.POST)
if form.is_valid():
logger.debug("Login form validation was successful")
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
# last_login time upon authentication.
if get_config().MAINTENANCE_MODE:
logger.warning("Maintenance mode enabled: disabling update of most recent login time")
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
# Authenticate user
auth_login(request, form.get_user())
logger.info(f"User {request.user} successfully authenticated")
messages.success(request, f"Logged in as {request.user}.")
# Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.)
if not hasattr(request.user, 'config'):
config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
return self.redirect_to_next(request, logger)
else:
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
return render(request, self.template_name, {
'form': form,
'auth_backends': self.get_auth_backends(request),
})
def redirect_to_next(self, request, logger):
data = request.POST if request.method == "POST" else request.GET
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
logger.debug(f"Redirecting user to {redirect_url}")
else:
if redirect_url:
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
redirect_url = reverse('home')
return HttpResponseRedirect(redirect_url)
class LogoutView(View):
"""
Deauthenticate a web user.
"""
def get(self, request):
logger = logging.getLogger('netbox.auth.logout')
# Log out the user
username = request.user
auth_logout(request)
logger.info(f"User {username} has logged out")
messages.info(request, "You have logged out.")
# Delete session key cookie (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
response.delete_cookie('session_key')
return response
#
# User profiles
#
class ProfileView(LoginRequiredMixin, View):
template_name = 'account/profile.html'
def get(self, request):
# Compile changelog table
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
user=request.user
).prefetch_related(
'changed_object_type'
)[:20]
changelog_table = ObjectChangeTable(changelog)
return render(request, self.template_name, {
'changelog_table': changelog_table,
'active_tab': 'profile',
})
class UserConfigView(LoginRequiredMixin, View):
template_name = 'account/preferences.html'
def get(self, request):
userconfig = request.user.config
form = forms.UserConfigForm(instance=userconfig)
return render(request, self.template_name, {
'form': form,
'active_tab': 'preferences',
})
def post(self, request):
userconfig = request.user.config
form = forms.UserConfigForm(request.POST, instance=userconfig)
if form.is_valid():
form.save()
messages.success(request, "Your preferences have been updated.")
return redirect('account:preferences')
return render(request, self.template_name, {
'form': form,
'active_tab': 'preferences',
})
class ChangePasswordView(LoginRequiredMixin, View):
template_name = 'account/password.html'
def get(self, request):
# LDAP users cannot change their password here
if getattr(request.user, 'ldap_username', None):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('account:profile')
form = forms.PasswordChangeForm(user=request.user)
return render(request, self.template_name, {
'form': form,
'active_tab': 'password',
})
def post(self, request):
form = forms.PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
messages.success(request, "Your password has been changed successfully.")
return redirect('account:profile')
return render(request, self.template_name, {
'form': form,
'active_tab': 'change_password',
})
#
# Bookmarks
#
class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
table = BookmarkTable
template_name = 'account/bookmarks.html'
def get_queryset(self, request):
return Bookmark.objects.filter(user=request.user)
def get_extra_context(self, request):
return {
'active_tab': 'bookmarks',
}
#
# User views for token management
#
class UserTokenListView(LoginRequiredMixin, View):
def get(self, request):
tokens = UserToken.objects.filter(user=request.user)
table = tables.UserTokenTable(tokens)
table.configure(request)
return render(request, 'account/token_list.html', {
'tokens': tokens,
'active_tab': 'api-tokens',
'table': table,
})
@register_model_view(UserToken)
class UserTokenView(LoginRequiredMixin, View):
def get(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None
return render(request, 'account/token.html', {
'object': token,
'key': key,
})
@register_model_view(UserToken, 'edit')
class UserTokenEditView(generic.ObjectEditView):
queryset = UserToken.objects.all()
form = forms.UserTokenForm
default_return_url = 'account:usertoken_list'
def alter_object(self, obj, request, url_args, url_kwargs):
if not obj.pk:
obj.user = request.user
return obj
@register_model_view(UserToken, 'delete')
class UserTokenDeleteView(generic.ObjectDeleteView):
queryset = UserToken.objects.all()
default_return_url = 'account:usertoken_list'

View File

@ -363,6 +363,7 @@ INSTALLED_APPS = [
'taggit',
'timezone_field',
'core',
'account',
'circuits',
'dcim',
'ipam',

View File

@ -1,19 +1,18 @@
from django.conf import settings
from django.conf.urls import include
from django.urls import path, re_path
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from django.views.static import serve
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from account.views import LoginView, LogoutView
from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
from netbox.api.views import APIRootView, StatusView
from netbox.graphql.schema import schema
from netbox.graphql.views import GraphQLView
from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
from users.views import LoginView, LogoutView
from .admin import admin_site
_patterns = [
# Base views
@ -37,7 +36,7 @@ _patterns = [
path('wireless/', include('wireless.urls')),
# Current user views
path('user/', include('users.account_urls')),
path('user/', include('account.urls')),
# HTMX views
path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'),

View File

@ -1,4 +1,4 @@
{% extends 'users/account/base.html' %}
{% extends 'account/base.html' %}
{% load buttons %}
{% load helpers %}
{% load render_table from django_tables2 %}
@ -7,7 +7,6 @@
{% block title %}{% trans "Bookmarks" %}{% endblock %}
{% block content %}
<form method="post" class="form form-horizontal">
{% csrf_token %}
<input type="hidden" name="return_url" value="{% url 'account:bookmarks' %}" />

View File

@ -0,0 +1,21 @@
{% extends 'account/base.html' %}
{% load form_helpers %}
{% load i18n %}
{% block title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal col-md-8 offset-md-2">
{% csrf_token %}
<div class="field-group">
<h5 class="text-center">{% trans "Password" %}</h5>
{% render_field form.old_password %}
{% render_field form.new_password1 %}
{% render_field form.new_password2 %}
</div>
<div class="text-end">
<a href="{% url 'account:profile' %}" class="btn btn-outline-danger">{% trans "Cancel" %}</a>
<button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
</div>
</form>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'users/account/base.html' %}
{% extends 'account/base.html' %}
{% load helpers %}
{% load form_helpers %}
{% load i18n %}

View File

@ -1,4 +1,4 @@
{% extends 'users/account/base.html' %}
{% extends 'account/base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load i18n %}

View File

@ -0,0 +1,26 @@
{% extends 'account/base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block title %}{% trans "My API Tokens" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-12 text-end">
<a href="{% url 'account:usertoken_add' %}" class="btn btn-sm btn-primary my-3">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a Token" %}
</a>
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<div class="card-body table-responsive">
{% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends 'users/account/base.html' %}
{% load form_helpers %}
{% load i18n %}
{% block title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal col-md-8 offset-md-2">
{% csrf_token %}
<div class="field-group">
<h5 class="text-center">{% trans "Password" %}</h5>
{% render_field form.old_password %}
{% render_field form.new_password1 %}
{% render_field form.new_password2 %}
</div>
<div class="text-end">
<a href="{% url 'account:profile' %}" class="btn btn-outline-danger">{% trans "Cancel" %}</a>
<button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
</div>
</form>
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends 'users/account/base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block title %}{% trans "My API Tokens" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-12 text-end">
<a href="{% url 'account:usertoken_add' %}" class="btn btn-sm btn-primary my-3">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a Token" %}
</a>
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<div class="card-body table-responsive">
{% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -26,7 +26,6 @@ __all__ = (
'ObjectPermission',
'Token',
'UserConfig',
'UserToken',
)
@ -322,18 +321,6 @@ class Token(models.Model):
return False
class UserToken(Token):
"""
Proxy model for users to manage their own API tokens.
"""
class Meta:
proxy = True
verbose_name = 'token'
def get_absolute_url(self):
return reverse('account:usertoken', args=[self.pk])
#
# Permissions
#

View File

@ -1,8 +1,9 @@
import django_tables2 as tables
from django.utils.translation import gettext as _
from account.tables import UserTokenTable
from netbox.tables import NetBoxTable, columns
from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserToken
from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token
__all__ = (
'GroupTable',
@ -12,58 +13,7 @@ __all__ = (
)
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ record }}</span></samp>"""
ALLOWED_IPS = """{{ value|join:", " }}"""
COPY_BUTTON = """
{% if settings.ALLOW_TOKEN_RETRIEVAL %}
{% copy_content record.pk prefix="token_" color="success" %}
{% endif %}
"""
class UserTokenTable(NetBoxTable):
"""
Table for users to manager their own API tokens under account views.
"""
key = columns.TemplateColumn(
verbose_name=_('Key'),
template_code=TOKEN,
)
write_enabled = columns.BooleanColumn(
verbose_name=_('Write Enabled')
)
created = columns.DateColumn(
verbose_name=_('Created'),
)
expires = columns.DateColumn(
verbose_name=_('Expires'),
)
last_used = columns.DateTimeColumn(
verbose_name=_('Last Used'),
)
allowed_ips = columns.TemplateColumn(
verbose_name=_('Allowed IPs'),
template_code=ALLOWED_IPS
)
# TODO: Fix permissions evaluation & viewname resolution
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=COPY_BUTTON
)
class Meta(NetBoxTable.Meta):
model = UserToken
fields = (
'pk', 'id', 'key', 'description', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips',
)
class TokenTable(UserTokenTable):
"""
General-purpose table for API token management.
"""
user = tables.Column(
linkify=True,
verbose_name=_('User')

View File

@ -1,298 +1,11 @@
import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
from social_core.backends.utils import load_backends
from extras.models import Bookmark, ObjectChange
from extras.tables import BookmarkTable, ObjectChangeTable
from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config
from extras.models import ObjectChange
from extras.tables import ObjectChangeTable
from netbox.views import generic
from utilities.forms import ConfirmationForm
from utilities.views import register_model_view
from . import filtersets, forms, tables
from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserConfig, UserToken
#
# Login/logout
#
class LoginView(View):
"""
Perform user authentication via the web UI.
"""
template_name = 'login.html'
@method_decorator(sensitive_post_parameters('password'))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def gen_auth_data(self, name, url, params):
display_name, icon_name = get_auth_backend_display(name)
return {
'display_name': display_name,
'icon_name': icon_name,
'url': f'{url}?{urlencode(params)}',
}
def get_auth_backends(self, request):
auth_backends = []
saml_idps = get_saml_idps()
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
url = reverse('social:begin', args=[name])
params = {}
if next := request.GET.get('next'):
params['next'] = next
if name.lower() == 'saml' and saml_idps:
for idp in saml_idps:
params['idp'] = idp
data = self.gen_auth_data(name, url, params)
data['display_name'] = f'{data["display_name"]} ({idp})'
auth_backends.append(data)
else:
auth_backends.append(self.gen_auth_data(name, url, params))
return auth_backends
def get(self, request):
form = forms.LoginForm(request)
if request.user.is_authenticated:
logger = logging.getLogger('netbox.auth.login')
return self.redirect_to_next(request, logger)
return render(request, self.template_name, {
'form': form,
'auth_backends': self.get_auth_backends(request),
})
def post(self, request):
logger = logging.getLogger('netbox.auth.login')
form = forms.LoginForm(request, data=request.POST)
if form.is_valid():
logger.debug("Login form validation was successful")
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
# last_login time upon authentication.
if get_config().MAINTENANCE_MODE:
logger.warning("Maintenance mode enabled: disabling update of most recent login time")
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
# Authenticate user
auth_login(request, form.get_user())
logger.info(f"User {request.user} successfully authenticated")
messages.success(request, f"Logged in as {request.user}.")
# Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.)
if not hasattr(request.user, 'config'):
config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
return self.redirect_to_next(request, logger)
else:
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
return render(request, self.template_name, {
'form': form,
'auth_backends': self.get_auth_backends(request),
})
def redirect_to_next(self, request, logger):
data = request.POST if request.method == "POST" else request.GET
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
logger.debug(f"Redirecting user to {redirect_url}")
else:
if redirect_url:
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
redirect_url = reverse('home')
return HttpResponseRedirect(redirect_url)
class LogoutView(View):
"""
Deauthenticate a web user.
"""
def get(self, request):
logger = logging.getLogger('netbox.auth.logout')
# Log out the user
username = request.user
auth_logout(request)
logger.info(f"User {username} has logged out")
messages.info(request, "You have logged out.")
# Delete session key cookie (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
response.delete_cookie('session_key')
return response
#
# User profiles
#
class ProfileView(LoginRequiredMixin, View):
template_name = 'users/account/profile.html'
def get(self, request):
# Compile changelog table
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
user=request.user
).prefetch_related(
'changed_object_type'
)[:20]
changelog_table = ObjectChangeTable(changelog)
return render(request, self.template_name, {
'changelog_table': changelog_table,
'active_tab': 'profile',
})
class UserConfigView(LoginRequiredMixin, View):
template_name = 'users/account/preferences.html'
def get(self, request):
userconfig = request.user.config
form = forms.UserConfigForm(instance=userconfig)
return render(request, self.template_name, {
'form': form,
'active_tab': 'preferences',
})
def post(self, request):
userconfig = request.user.config
form = forms.UserConfigForm(request.POST, instance=userconfig)
if form.is_valid():
form.save()
messages.success(request, "Your preferences have been updated.")
return redirect('account:preferences')
return render(request, self.template_name, {
'form': form,
'active_tab': 'preferences',
})
class ChangePasswordView(LoginRequiredMixin, View):
template_name = 'users/account/password.html'
def get(self, request):
# LDAP users cannot change their password here
if getattr(request.user, 'ldap_username', None):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('account:profile')
form = forms.PasswordChangeForm(user=request.user)
return render(request, self.template_name, {
'form': form,
'active_tab': 'password',
})
def post(self, request):
form = forms.PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
messages.success(request, "Your password has been changed successfully.")
return redirect('account:profile')
return render(request, self.template_name, {
'form': form,
'active_tab': 'change_password',
})
#
# Bookmarks
#
class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
table = BookmarkTable
template_name = 'users/account/bookmarks.html'
def get_queryset(self, request):
return Bookmark.objects.filter(user=request.user)
def get_extra_context(self, request):
return {
'active_tab': 'bookmarks',
}
#
# User views for token management
#
class UserTokenListView(LoginRequiredMixin, View):
def get(self, request):
tokens = UserToken.objects.filter(user=request.user)
table = tables.UserTokenTable(tokens)
table.configure(request)
return render(request, 'users/account/token_list.html', {
'tokens': tokens,
'active_tab': 'api-tokens',
'table': table,
})
@register_model_view(UserToken)
class UserTokenView(LoginRequiredMixin, View):
def get(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None
return render(request, 'users/account/token.html', {
'object': token,
'key': key,
})
class UserTokenEditView(generic.ObjectEditView):
queryset = UserToken.objects.all()
form = forms.UserTokenForm
default_return_url = 'account:usertoken_list'
def alter_object(self, obj, request, url_args, url_kwargs):
if not obj.pk:
obj.user = request.user
return obj
class UserTokenDeleteView(generic.ObjectDeleteView):
queryset = UserToken.objects.all()
default_return_url = 'account:usertoken_list'
from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token
#