Reintroduce UserToken proxy model for account views

This commit is contained in:
Jeremy Stretch 2023-07-25 11:19:19 -04:00
parent 5064bd4574
commit b57c101ee4
7 changed files with 96 additions and 53 deletions

View File

@ -4,13 +4,13 @@
{% load plugins %} {% load plugins %}
{% block breadcrumbs %} {% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'users:usertoken_list' %}">Tokens</a></li> <li class="breadcrumb-item"><a href="{% url 'users:usertoken_list' %}">My API Tokens</a></li>
{% endblock breadcrumbs %} {% endblock breadcrumbs %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
{% if not settings.ALLOW_TOKEN_RETRIEVAL %} {% if key and not settings.ALLOW_TOKEN_RETRIEVAL %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
<i class="mdi mdi-alert"></i> Tokens cannot be retrieved at a later time. You must <a href="#" class="copy-content" data-clipboard-target="#token_id" title="Copy to clipboard">copy the token value</a> below and store it securely. <i class="mdi mdi-alert"></i> Tokens cannot be retrieved at a later time. You must <a href="#" class="copy-content" data-clipboard-target="#token_id" title="Copy to clipboard">copy the token value</a> below and store it securely.
</div> </div>
@ -19,15 +19,17 @@
<h5 class="card-header">Token</h5> <h5 class="card-header">Token</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> {% if key %}
<th scope="row">Key</th> <tr>
<td> <th scope="row">Key</th>
<div class="float-end"> <td>
{% copy_content "token_id" %} <div class="float-end">
</div> {% copy_content "token_id" %}
<div id="token_id">{{ key }}</div> </div>
</td> <div id="token_id">{{ key }}</div>
</tr> </td>
</tr>
{% endif %}
<tr> <tr>
<th scope="row">Description</th> <th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
@ -53,10 +55,6 @@
</table> </table>
</div> </div>
</div> </div>
<div class="col col-md-12 text-center">
<a href="{% url 'users:usertoken_add' %}" class="btn btn-outline-primary">Add Another</a>
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{% load helpers %} {% load helpers %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block title %}API Tokens{% endblock %} {% block title %}My API Tokens{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">

View File

@ -0,0 +1,24 @@
# Generated by Django 4.1.10 on 2023-07-25 15:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0004_netboxgroup_netboxuser'),
]
operations = [
migrations.CreateModel(
name='UserToken',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('users.token',),
),
]

View File

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

View File

@ -1,7 +1,7 @@
import django_tables2 as tables import django_tables2 as tables
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserToken
__all__ = ( __all__ = (
'GroupTable', 'GroupTable',
@ -30,7 +30,7 @@ class TokenActionsColumn(columns.ActionsColumn):
} }
class TokenTable(NetBoxTable): class UserTokenTable(NetBoxTable):
key = columns.TemplateColumn( key = columns.TemplateColumn(
verbose_name='Key', verbose_name='Key',
template_code=TOKEN, template_code=TOKEN,
@ -57,6 +57,15 @@ class TokenTable(NetBoxTable):
extra_buttons=COPY_BUTTON 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):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Token model = Token
fields = [ fields = [

View File

@ -13,8 +13,7 @@ urlpatterns = [
path('password/', views.ChangePasswordView.as_view(), name='change_password'), path('password/', views.ChangePasswordView.as_view(), name='change_password'),
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'), path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'), path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'),
path('api-tokens/<int:pk>/edit/', views.UserTokenEditView.as_view(), name='usertoken_edit'), path('api-tokens/<int:pk>/', include(get_model_urls('users', 'usertoken'))),
path('api-tokens/<int:pk>/delete/', views.UserTokenDeleteView.as_view(), name='usertoken_delete'),
# Tokens # Tokens
path('tokens/', views.TokenListView.as_view(), name='token_list'), path('tokens/', views.TokenListView.as_view(), name='token_list'),

View File

@ -24,7 +24,7 @@ from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.views import register_model_view from utilities.views import register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import Token, UserConfig, NetBoxGroup, NetBoxUser, ObjectPermission from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserConfig, UserToken
# #
@ -249,52 +249,61 @@ class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
# #
# API tokens # User views for token management
# #
class UserTokenListView(LoginRequiredMixin, View): class UserTokenListView(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
tokens = UserToken.objects.filter(user=request.user)
tokens = Token.objects.filter(user=request.user) table = tables.UserTokenTable(tokens)
table = tables.TokenTable(tokens)
table.configure(request) table.configure(request)
return render(request, 'users/account/api_tokens.html', { return render(request, 'users/account/usertoken_list.html', {
'tokens': tokens, 'tokens': tokens,
'active_tab': 'api-tokens', 'active_tab': 'api-tokens',
'table': table, 'table': table,
}) })
@register_model_view(UserToken)
class UserTokenView(LoginRequiredMixin, View):
def get(self, request, pk):
token = get_object_or_404(UserToken, pk=pk)
key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None
return render(request, 'users/account/usertoken.html', {
'object': token,
'key': key,
})
@register_model_view(UserToken, 'edit')
class UserTokenEditView(LoginRequiredMixin, View): class UserTokenEditView(LoginRequiredMixin, View):
def get(self, request, pk=None): def get(self, request, pk=None):
if pk: if pk:
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk) token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
else: else:
token = Token(user=request.user) token = UserToken(user=request.user)
form = forms.UserTokenForm(instance=token) form = forms.UserTokenForm(instance=token)
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('users:token_list'), 'return_url': reverse('users:usertoken_list'),
}) })
def post(self, request, pk=None): def post(self, request, pk=None):
if pk: if pk:
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk) token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
form = forms.UserTokenForm(request.POST, instance=token) form = forms.UserTokenForm(request.POST, instance=token)
else: else:
token = Token(user=request.user) token = UserToken(user=request.user)
form = forms.UserTokenForm(request.POST) form = forms.UserTokenForm(request.POST)
if form.is_valid(): if form.is_valid():
token = form.save(commit=False) token = form.save(commit=False)
token.user = request.user token.user = request.user
token.save() token.save()
@ -303,7 +312,7 @@ class UserTokenEditView(LoginRequiredMixin, View):
messages.success(request, msg) messages.success(request, msg)
if not pk and not settings.ALLOW_TOKEN_RETRIEVAL: if not pk and not settings.ALLOW_TOKEN_RETRIEVAL:
return render(request, 'users/account/api_token.html', { return render(request, 'users/account/usertoken.html', {
'object': token, 'object': token,
'key': token.key, 'key': token.key,
'return_url': reverse('users:token_list'), 'return_url': reverse('users:token_list'),
@ -311,45 +320,41 @@ class UserTokenEditView(LoginRequiredMixin, View):
elif '_addanother' in request.POST: elif '_addanother' in request.POST:
return redirect(request.path) return redirect(request.path)
else: else:
return redirect('users:token_list') return redirect('users:usertoken_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('users:token_list'), 'return_url': reverse('users:usertoken_list'),
'disable_addanother': not settings.ALLOW_TOKEN_RETRIEVAL 'disable_addanother': not settings.ALLOW_TOKEN_RETRIEVAL
}) })
@register_model_view(UserToken, 'delete')
class UserTokenDeleteView(LoginRequiredMixin, View): class UserTokenDeleteView(LoginRequiredMixin, View):
def get(self, request, pk): def get(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
initial_data = {
'return_url': reverse('users:token_list'),
}
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': ConfirmationForm(),
'return_url': reverse('users:token_list'), 'return_url': reverse('users:usertoken_list'),
}) })
def post(self, request, pk): def post(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
form = ConfirmationForm(request.POST) form = ConfirmationForm(request.POST)
if form.is_valid(): if form.is_valid():
token.delete() token.delete()
messages.success(request, "Token deleted") messages.success(request, "Token deleted")
return redirect('users:token_list') return redirect('users:usertoken_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('users:token_list'), 'return_url': reverse('users:usertoken_list'),
}) })
@ -368,9 +373,6 @@ class TokenListView(generic.ObjectListView):
class TokenView(generic.ObjectView): class TokenView(generic.ObjectView):
queryset = Token.objects.all() queryset = Token.objects.all()
def get_extra_context(self, request, instance):
return {}
@register_model_view(Token, 'edit') @register_model_view(Token, 'edit')
class TokenEditView(generic.ObjectEditView): class TokenEditView(generic.ObjectEditView):