Closes #9708: Render user API tokens in a table

This commit is contained in:
jeremystretch 2022-07-11 15:43:59 -04:00
parent 531d961d30
commit 123e758c6d
8 changed files with 83 additions and 90 deletions

View File

@ -19,17 +19,17 @@
{% endif %} {% endif %}
</li> </li>
<li> <li>
<a class="dropdown-item" href="{% url 'user:profile' %}"> <a class="dropdown-item" href="{% url 'users:profile' %}">
<i class="mdi mdi-account"></i> Profile <i class="mdi mdi-account"></i> Profile
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{% url 'user:preferences' %}"> <a class="dropdown-item" href="{% url 'users:preferences' %}">
<i class="mdi mdi-wrench"></i> Preferences <i class="mdi mdi-wrench"></i> Preferences
</a> </a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{% url 'user:token_list' %}"> <a class="dropdown-item" href="{% url 'users:token_list' %}">
<i class="mdi mdi-key"></i> API Tokens <i class="mdi mdi-key"></i> API Tokens
</a> </a>
</li> </li>

View File

@ -1,78 +1,25 @@
{% extends 'users/base.html' %} {% extends 'users/base.html' %}
{% load helpers %} {% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}API Tokens{% endblock %} {% block title %}API Tokens{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-10 offset-md-1"> <div class="col col-md-12 text-end">
{% for token in tokens %} <a href="{% url 'users:token_add' %}" class="btn btn-sm btn-primary my-3">
<div class="card{% if token.is_expired %} bg-danger{% endif %}"> <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add a Token
<div class="card-header"> </a>
<div class="float-end noprint"> </div>
<a class="m-1 btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ token.pk }}">Copy</a> </div>
<a href="{% url 'user:token_edit' pk=token.pk %}" class="m-1 btn btn-sm btn-warning">Edit</a> <div class="row mb-3">
<a href="{% url 'user:token_delete' pk=token.pk %}" class="m-1 btn btn-sm btn-danger">Delete</a> <div class="col col-md-12">
</div> <div class="card">
<i class="mdi mdi-key"></i> <div class="card-body table-responsive">
<samp><span id="token_{{ token.pk }}">{{ token.key }}</span></samp> {% render_table table 'inc/table.html' %}
{% if token.is_expired %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
<span class="badge bg-danger">Expired</span> </div>
{% endif %}
</div>
<div class="card-body">
<div class="row">
<div class="col col-md-3">
<small class="text-muted">Created</small><br />
{{ token.created|annotated_date }}
</div>
<div class="col col-md-3">
<small class="text-muted">Expires</small><br />
{% if token.expires %}
{{ token.expires|annotated_date }}
{% else %}
<span>Never</span>
{% endif %}
</div>
<div class="col col-md-3">
<small class="text-muted">Last Used</small><br />
{% if token.last_used %}
{{ token.last_used|annotated_date }}
{% else %}
<span>Never</span>
{% endif %}
</div>
<div class="col col-md-3">
<small class="text-muted">Create/Edit/Delete Operations</small><br />
{% if token.write_enabled %}
<span class="badge bg-success">Enabled</span>
{% else %}
<span class="badge bg-danger">Disabled</span>
{% endif %}
</div>
<div class="col col-md-3">
<small class="text-muted">Allowed Source IPs</small><br />
{% if token.allowed_ips %}
{{ token.allowed_ips|join:', ' }}
{% else %}
<span>Any</span>
{% endif %}
</div> </div>
{% if token.description %}
<br /><span>{{ token.description }}</span>
{% endif %}
</div>
</div>
{% empty %}
<h6><i class="mdi mdi-information"></i> You do not have any API tokens.</h6>
<p>Tokens are used to authenticate REST and GraphQL API requests.</p>
{% endfor %}
<div class="text-end">
<a href="{% url 'user:token_add' %}" class="btn btn-sm btn-primary my-3">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
Add a Token
</a>
</div>
</div>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -3,18 +3,18 @@
{% block tabs %} {% block tabs %}
<ul class="nav nav-tabs px-3"> <ul class="nav nav-tabs px-3">
<li role="presentation" class="nav-item"> <li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'user:profile' %}">Profile</a> <a class="nav-link{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'users:profile' %}">Profile</a>
</li> </li>
<li role="presentation" class="nav-item"> <li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'user:preferences' %}">Preferences</a> <a class="nav-link{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'users:preferences' %}">Preferences</a>
</li> </li>
{% if not request.user.ldap_username %} {% if not request.user.ldap_username %}
<li role="presentation" class="nav-item"> <li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'password' %} active{% endif %}" href="{% url 'user:change_password' %}">Password</a> <a class="nav-link{% if active_tab == 'password' %} active{% endif %}" href="{% url 'users:change_password' %}">Password</a>
</li> </li>
{% endif %} {% endif %}
<li role="presentation" class="nav-item"> <li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'user:token_list' %}">API Tokens</a> <a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'users:token_list' %}">API Tokens</a>
</li> </li>
</ul> </ul>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,7 @@
{% render_field form.new_password2 %} {% render_field form.new_password2 %}
</div> </div>
<div class="text-end"> <div class="text-end">
<a href="{% url 'user:profile' %}" class="btn btn-outline-danger">Cancel</a> <a href="{% url 'users:profile' %}" class="btn btn-outline-danger">Cancel</a>
<button type="submit" name="_update" class="btn btn-primary">Save</button> <button type="submit" name="_update" class="btn btn-primary">Save</button>
</div> </div>
</form> </form>

View File

@ -79,7 +79,7 @@
</div> </div>
<div class="text-end my-3"> <div class="text-end my-3">
<a class="btn btn-outline-secondary" href="{% url 'user:preferences' %}">Cancel</a> <a class="btn btn-outline-secondary" href="{% url 'users:preferences' %}">Cancel</a>
<button type="submit" name="_update" class="btn btn-primary">Save </button> <button type="submit" name="_update" class="btn btn-primary">Save </button>
</div> </div>
</form> </form>

42
netbox/users/tables.py Normal file
View File

@ -0,0 +1,42 @@
from .models import Token
from netbox.tables import NetBoxTable, columns
__all__ = (
'TokenTable',
)
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ value }}</span></samp>"""
ALLOWED_IPS = """{{ value|join:", " }}"""
COPY_BUTTON = """
<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ record.pk }}" title="Copy to clipboard">
<i class="mdi mdi-content-copy"></i>
</a>
"""
class TokenTable(NetBoxTable):
key = columns.TemplateColumn(
template_code=TOKEN
)
write_enabled = columns.BooleanColumn(
verbose_name='Write'
)
created = columns.DateColumn()
expired = columns.DateColumn()
last_used = columns.DateTimeColumn()
allowed_ips = columns.TemplateColumn(
template_code=ALLOWED_IPS
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=COPY_BUTTON
)
class Meta(NetBoxTable.Meta):
model = Token
fields = (
'pk', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips', 'description',
)

View File

@ -2,7 +2,7 @@ from django.urls import path
from . import views from . import views
app_name = 'user' app_name = 'users'
urlpatterns = [ urlpatterns = [
path('profile/', views.ProfileView.as_view(), name='profile'), path('profile/', views.ProfileView.as_view(), name='profile'),

View File

@ -21,6 +21,7 @@ from netbox.config import get_config
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
from .models import Token from .models import Token
from .tables import TokenTable
# #
@ -157,7 +158,7 @@ class UserConfigView(LoginRequiredMixin, View):
form.save() form.save()
messages.success(request, "Your preferences have been updated.") messages.success(request, "Your preferences have been updated.")
return redirect('user:preferences') return redirect('users:preferences')
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
@ -172,7 +173,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
# LDAP users cannot change their password here # LDAP users cannot change their password here
if getattr(request.user, 'ldap_username', None): if getattr(request.user, 'ldap_username', None):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.") messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('user:profile') return redirect('users:profile')
form = PasswordChangeForm(user=request.user) form = PasswordChangeForm(user=request.user)
@ -187,7 +188,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
form.save() form.save()
update_session_auth_hash(request, form.user) update_session_auth_hash(request, form.user)
messages.success(request, "Your password has been changed successfully.") messages.success(request, "Your password has been changed successfully.")
return redirect('user:profile') return redirect('users:profile')
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
@ -204,10 +205,13 @@ class TokenListView(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
tokens = Token.objects.filter(user=request.user) tokens = Token.objects.filter(user=request.user)
table = TokenTable(tokens)
table.configure(request)
return render(request, 'users/api_tokens.html', { return render(request, 'users/api_tokens.html', {
'tokens': tokens, 'tokens': tokens,
'active_tab': 'api-tokens', 'active_tab': 'api-tokens',
'table': table,
}) })
@ -225,7 +229,7 @@ class TokenEditView(LoginRequiredMixin, View):
return render(request, 'generic/object_edit.html', { return render(request, 'generic/object_edit.html', {
'object': token, 'object': token,
'form': form, 'form': form,
'return_url': reverse('user:token_list'), 'return_url': reverse('users:token_list'),
}) })
def post(self, request, pk=None): def post(self, request, pk=None):
@ -248,12 +252,12 @@ class TokenEditView(LoginRequiredMixin, View):
if '_addanother' in request.POST: if '_addanother' in request.POST:
return redirect(request.path) return redirect(request.path)
else: else:
return redirect('user:token_list') return redirect('users:token_list')
return render(request, 'generic/object_edit.html', { return render(request, 'generic/object_edit.html', {
'object': token, 'object': token,
'form': form, 'form': form,
'return_url': reverse('user:token_list'), 'return_url': reverse('users:token_list'),
}) })
@ -263,14 +267,14 @@ class TokenDeleteView(LoginRequiredMixin, View):
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk) token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
initial_data = { initial_data = {
'return_url': reverse('user:token_list'), 'return_url': reverse('users:token_list'),
} }
form = ConfirmationForm(initial=initial_data) form = ConfirmationForm(initial=initial_data)
return render(request, 'generic/object_delete.html', { return render(request, 'generic/object_delete.html', {
'object': token, 'object': token,
'form': form, 'form': form,
'return_url': reverse('user:token_list'), 'return_url': reverse('users:token_list'),
}) })
def post(self, request, pk): def post(self, request, pk):
@ -280,10 +284,10 @@ class TokenDeleteView(LoginRequiredMixin, View):
if form.is_valid(): if form.is_valid():
token.delete() token.delete()
messages.success(request, "Token deleted") messages.success(request, "Token deleted")
return redirect('user:token_list') return redirect('users:token_list')
return render(request, 'generic/object_delete.html', { return render(request, 'generic/object_delete.html', {
'object': token, 'object': token,
'form': form, 'form': form,
'return_url': reverse('user:token_list'), 'return_url': reverse('users:token_list'),
}) })