diff --git a/netbox/templates/users/account/api_token.html b/netbox/templates/users/account/usertoken.html similarity index 72% rename from netbox/templates/users/account/api_token.html rename to netbox/templates/users/account/usertoken.html index 587a1db38..ac06d67c9 100644 --- a/netbox/templates/users/account/api_token.html +++ b/netbox/templates/users/account/usertoken.html @@ -4,13 +4,13 @@ {% load plugins %} {% block breadcrumbs %} - + {% endblock breadcrumbs %} {% block content %}
- {% if not settings.ALLOW_TOKEN_RETRIEVAL %} + {% if key and not settings.ALLOW_TOKEN_RETRIEVAL %} @@ -19,15 +19,17 @@
Token
- - - - + {% if key %} + + + + + {% endif %} @@ -53,10 +55,6 @@
Key -
- {% copy_content "token_id" %} -
-
{{ key }}
-
Key +
+ {% copy_content "token_id" %} +
+
{{ key }}
+
Description {{ object.description|placeholder }}
-
- Add Another - Cancel -
{% endblock %} diff --git a/netbox/templates/users/account/api_tokens.html b/netbox/templates/users/account/usertoken_list.html similarity index 93% rename from netbox/templates/users/account/api_tokens.html rename to netbox/templates/users/account/usertoken_list.html index 46addbc47..e30b1ae96 100644 --- a/netbox/templates/users/account/api_tokens.html +++ b/netbox/templates/users/account/usertoken_list.html @@ -2,7 +2,7 @@ {% load helpers %} {% load render_table from django_tables2 %} -{% block title %}API Tokens{% endblock %} +{% block title %}My API Tokens{% endblock %} {% block content %}
diff --git a/netbox/users/migrations/0005_usertoken.py b/netbox/users/migrations/0005_usertoken.py new file mode 100644 index 000000000..6b9f0d333 --- /dev/null +++ b/netbox/users/migrations/0005_usertoken.py @@ -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',), + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index c9f932cdf..107d1b565 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -321,6 +321,17 @@ class Token(models.Model): 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 # diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 86ca838e0..b850befeb 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from netbox.tables import NetBoxTable, columns -from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token +from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserToken __all__ = ( 'GroupTable', @@ -30,7 +30,7 @@ class TokenActionsColumn(columns.ActionsColumn): } -class TokenTable(NetBoxTable): +class UserTokenTable(NetBoxTable): key = columns.TemplateColumn( verbose_name='Key', template_code=TOKEN, @@ -57,6 +57,15 @@ class TokenTable(NetBoxTable): 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): model = Token fields = [ diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 496201b92..ed3db4661 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -13,8 +13,7 @@ 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//edit/', views.UserTokenEditView.as_view(), name='usertoken_edit'), - path('api-tokens//delete/', views.UserTokenDeleteView.as_view(), name='usertoken_delete'), + path('api-tokens//', include(get_model_urls('users', 'usertoken'))), # Tokens path('tokens/', views.TokenListView.as_view(), name='token_list'), diff --git a/netbox/users/views.py b/netbox/users/views.py index 14c8a7d5c..acc45405e 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -24,7 +24,7 @@ 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 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): def get(self, request): - - tokens = Token.objects.filter(user=request.user) - table = tables.TokenTable(tokens) + tokens = UserToken.objects.filter(user=request.user) + table = tables.UserTokenTable(tokens) table.configure(request) - return render(request, 'users/account/api_tokens.html', { + return render(request, 'users/account/usertoken_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, 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): def get(self, request, pk=None): - 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: - token = Token(user=request.user) - + token = UserToken(user=request.user) form = forms.UserTokenForm(instance=token) return render(request, 'generic/object_edit.html', { 'object': token, 'form': form, - 'return_url': reverse('users:token_list'), + 'return_url': reverse('users:usertoken_list'), }) def post(self, request, pk=None): - 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) else: - token = Token(user=request.user) + token = UserToken(user=request.user) form = forms.UserTokenForm(request.POST) if form.is_valid(): - token = form.save(commit=False) token.user = request.user token.save() @@ -303,7 +312,7 @@ class UserTokenEditView(LoginRequiredMixin, View): messages.success(request, msg) 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, 'key': token.key, 'return_url': reverse('users:token_list'), @@ -311,45 +320,41 @@ class UserTokenEditView(LoginRequiredMixin, View): elif '_addanother' in request.POST: return redirect(request.path) else: - return redirect('users:token_list') + return redirect('users:usertoken_list') return render(request, 'generic/object_edit.html', { 'object': token, 'form': form, - 'return_url': reverse('users:token_list'), + 'return_url': reverse('users:usertoken_list'), 'disable_addanother': not settings.ALLOW_TOKEN_RETRIEVAL }) +@register_model_view(UserToken, 'delete') class UserTokenDeleteView(LoginRequiredMixin, View): def get(self, request, 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) + token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk) return render(request, 'generic/object_delete.html', { 'object': token, - 'form': form, - 'return_url': reverse('users:token_list'), + 'form': ConfirmationForm(), + 'return_url': reverse('users:usertoken_list'), }) def post(self, request, 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 = ConfirmationForm(request.POST) + if form.is_valid(): token.delete() messages.success(request, "Token deleted") - return redirect('users:token_list') + return redirect('users:usertoken_list') return render(request, 'generic/object_delete.html', { 'object': token, '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): queryset = Token.objects.all() - def get_extra_context(self, request, instance): - return {} - @register_model_view(Token, 'edit') class TokenEditView(generic.ObjectEditView):