From 7600d7b3449e4327713c1f550aa8a1ed5aab9ec0 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 26 Jul 2023 00:43:40 +0700 Subject: [PATCH] Closes #13228: Move token management views to primary UI --- netbox/netbox/navigation/menu.py | 3 +- netbox/netbox/settings.py | 1 + netbox/templates/inc/profile_button.html | 2 +- netbox/templates/users/account/api_token.html | 58 -------- netbox/templates/users/account/base.html | 2 +- netbox/templates/users/account/token.html | 69 ++++++++++ .../{api_tokens.html => token_list.html} | 4 +- netbox/templates/users/token.html | 56 ++++++++ netbox/users/admin/__init__.py | 21 --- netbox/users/admin/forms.py | 21 --- netbox/users/filtersets.py | 1 + netbox/users/forms/bulk_edit.py | 43 +++++- netbox/users/forms/bulk_import.py | 18 ++- netbox/users/forms/filtersets.py | 35 +++++ netbox/users/forms/model_forms.py | 28 +++- netbox/users/migrations/0005_usertoken.py | 25 ++++ netbox/users/models.py | 24 +++- netbox/users/tables.py | 45 +++++-- netbox/users/tests/test_views.py | 52 ++++++- netbox/users/urls.py | 14 +- netbox/users/views.py | 127 ++++++++++++------ 21 files changed, 482 insertions(+), 167 deletions(-) delete mode 100644 netbox/templates/users/account/api_token.html create mode 100644 netbox/templates/users/account/token.html rename netbox/templates/users/account/{api_tokens.html => token_list.html} (82%) create mode 100644 netbox/templates/users/token.html delete mode 100644 netbox/users/admin/forms.py create mode 100644 netbox/users/migrations/0005_usertoken.py diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 45de28f2b..7e5d26186 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -353,7 +353,7 @@ ADMIN_MENU = Menu( icon_class='mdi mdi-account-multiple', groups=( MenuGroup( - label=_('Users'), + label=_('Authentication'), items=( # Proxy model for auth.User MenuItem( @@ -399,6 +399,7 @@ ADMIN_MENU = Menu( ) ) ), + get_model_item('users', 'token', _('API Tokens')), get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']), ), ), diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 7d2da2996..da58b0dd6 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -469,6 +469,7 @@ EXEMPT_EXCLUDE_MODELS = ( ('auth', 'group'), ('auth', 'user'), ('users', 'objectpermission'), + ('users', 'token'), ) # All URLs starting with a string listed here are exempt from login enforcement diff --git a/netbox/templates/inc/profile_button.html b/netbox/templates/inc/profile_button.html index 932b91275..a5d8cef61 100644 --- a/netbox/templates/inc/profile_button.html +++ b/netbox/templates/inc/profile_button.html @@ -34,7 +34,7 @@
  • - + API Tokens
  • diff --git a/netbox/templates/users/account/api_token.html b/netbox/templates/users/account/api_token.html deleted file mode 100644 index 7fd6f064d..000000000 --- a/netbox/templates/users/account/api_token.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends 'generic/object.html' %} -{% load form_helpers %} -{% load helpers %} -{% load plugins %} - -{% block content %} -
    -
    - {% if not settings.ALLOW_TOKEN_RETRIEVAL %} - - {% endif %} -
    -
    Token
    -
    - - - - - - - - - - - - - - - - - - - - - -
    Key -
    - {% copy_content "token_id" %} -
    -
    {{ key }}
    -
    Description{{ object.description|placeholder }}
    User{{ object.user }}
    Created{{ object.created|annotated_date }}
    Expires - {% if object.expires %} - {{ object.expires|annotated_date }} - {% else %} - Never - {% endif %} -
    -
    -
    - -
    -
    -{% endblock %} diff --git a/netbox/templates/users/account/base.html b/netbox/templates/users/account/base.html index f492f89ec..9ac61bced 100644 --- a/netbox/templates/users/account/base.html +++ b/netbox/templates/users/account/base.html @@ -18,7 +18,7 @@ {% endif %} {% endblock %} diff --git a/netbox/templates/users/account/token.html b/netbox/templates/users/account/token.html new file mode 100644 index 000000000..6df1d1367 --- /dev/null +++ b/netbox/templates/users/account/token.html @@ -0,0 +1,69 @@ +{% extends 'generic/object.html' %} +{% load form_helpers %} +{% load helpers %} +{% load i18n %} +{% load plugins %} + +{% block breadcrumbs %} + +{% endblock breadcrumbs %} + +{% block title %}{% trans "Token" %} {{ object }}{% endblock %} + +{% block subtitle %}{% endblock %} + +{% block content %} +
    +
    + {% if key and not settings.ALLOW_TOKEN_RETRIEVAL %} + + {% endif %} +
    +
    {% trans "Token" %}
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {% trans "Key" %} + {% if key %} +
    + {% copy_content "token_id" %} +
    +
    {{ key }}
    + {% else %} + {{ object.partial }} + {% endif %} +
    {% trans "Description" %}{{ object.description|placeholder }}
    {% trans "Write enabled" %}{% checkmark object.write_enabled %}
    {% trans "Created" %}{{ object.created|annotated_date }}
    {% trans "Expires" %}{{ object.expires|placeholder }}
    {% trans "Last used" %}{{ object.last_used|placeholder }}
    {% trans "Allowed IPs" %}{{ object.allowed_ips|join:", "|placeholder }}
    +
    +
    +
    +
    +{% endblock %} diff --git a/netbox/templates/users/account/api_tokens.html b/netbox/templates/users/account/token_list.html similarity index 82% rename from netbox/templates/users/account/api_tokens.html rename to netbox/templates/users/account/token_list.html index 25f5f02e6..e30b1ae96 100644 --- a/netbox/templates/users/account/api_tokens.html +++ b/netbox/templates/users/account/token_list.html @@ -2,12 +2,12 @@ {% 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/templates/users/token.html b/netbox/templates/users/token.html new file mode 100644 index 000000000..0fa8c572e --- /dev/null +++ b/netbox/templates/users/token.html @@ -0,0 +1,56 @@ +{% extends 'generic/object.html' %} +{% load i18n %} +{% load helpers %} +{% load render_table from django_tables2 %} + +{% block title %}{% trans "Token" %} {{ object }}{% endblock %} + +{% block subtitle %}{% endblock %} + +{% block content %} +
    +
    +
    +
    {% trans "Token" %}
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {% trans "Key" %}{% if settings.ALLOW_TOKEN_RETRIEVAL %}{{ object.key }}{% else %}{{ object.partial }}{% endif %}
    {% trans "User" %} + {{ object.user }} +
    {% trans "Description" %}{{ object.description|placeholder }}
    {% trans "Write enabled" %}{% checkmark object.write_enabled %}
    {% trans "Created" %}{{ object.created|annotated_date }}
    {% trans "Expires" %}{{ object.expires|placeholder }}
    {% trans "Last used" %}{{ object.last_used|placeholder }}
    {% trans "Allowed IPs" %}{{ object.allowed_ips|join:", "|placeholder }}
    +
    +
    +
    +
    +{% endblock %} diff --git a/netbox/users/admin/__init__.py b/netbox/users/admin/__init__.py index 316346c50..bc7bf7ab2 100644 --- a/netbox/users/admin/__init__.py +++ b/netbox/users/admin/__init__.py @@ -1,11 +1,6 @@ from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as UserAdmin_ from django.contrib.auth.models import Group, User -from users.models import ObjectPermission, Token -from . import filters, forms, inlines - - # # Users & groups # @@ -13,19 +8,3 @@ from . import filters, forms, inlines # Unregister the built-in GroupAdmin and UserAdmin classes so that we can use our custom admin classes below admin.site.unregister(Group) admin.site.unregister(User) - - -# -# REST API tokens -# - -@admin.register(Token) -class TokenAdmin(admin.ModelAdmin): - form = forms.TokenAdminForm - list_display = [ - 'key', 'user', 'created', 'expires', 'last_used', 'write_enabled', 'description', 'list_allowed_ips' - ] - - def list_allowed_ips(self, obj): - return obj.allowed_ips or 'Any' - list_allowed_ips.short_description = "Allowed IPs" diff --git a/netbox/users/admin/forms.py b/netbox/users/admin/forms.py deleted file mode 100644 index 7db6a124c..000000000 --- a/netbox/users/admin/forms.py +++ /dev/null @@ -1,21 +0,0 @@ -from django import forms -from django.utils.translation import gettext as _ - -from users.models import Token - -__all__ = ( - 'TokenAdminForm', -) - - -class TokenAdminForm(forms.ModelForm): - key = forms.CharField( - required=False, - help_text=_("If no key is provided, one will be generated automatically.") - ) - - class Meta: - fields = [ - 'user', 'key', 'write_enabled', 'expires', 'description', 'allowed_ips' - ] - model = Token diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index a4e9a9fbc..0f590e012 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -10,6 +10,7 @@ from users.models import ObjectPermission, Token __all__ = ( 'GroupFilterSet', 'ObjectPermissionFilterSet', + 'TokenFilterSet', 'UserFilterSet', ) diff --git a/netbox/users/forms/bulk_edit.py b/netbox/users/forms/bulk_edit.py index db40283ba..0e29109a4 100644 --- a/netbox/users/forms/bulk_edit.py +++ b/netbox/users/forms/bulk_edit.py @@ -1,13 +1,17 @@ from django import forms +from django.contrib.postgres.forms import SimpleArrayField from django.utils.translation import gettext_lazy as _ +from ipam.formfields import IPNetworkFormField +from ipam.validators import prefix_validator from users.models import * -from utilities.forms import BootstrapMixin -from utilities.forms.widgets import BulkEditNullBooleanSelect +from utilities.forms import BootstrapMixin, BulkEditForm +from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker __all__ = ( 'ObjectPermissionBulkEditForm', 'UserBulkEditForm', + 'TokenBulkEditForm', ) @@ -70,3 +74,38 @@ class ObjectPermissionBulkEditForm(BootstrapMixin, forms.Form): (None, ('enabled', 'description')), ) nullable_fields = ('description',) + + +class TokenBulkEditForm(BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=Token.objects.all(), + widget=forms.MultipleHiddenInput + ) + write_enabled = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect, + label=_('Write enabled') + ) + description = forms.CharField( + max_length=200, + required=False, + label=_('Description') + ) + expires = forms.DateTimeField( + required=False, + widget=DateTimePicker(), + label=_('Expires') + ) + allowed_ips = SimpleArrayField( + base_field=IPNetworkFormField(validators=[prefix_validator]), + required=False, + label=_('Allowed IPs') + ) + + model = Token + fieldsets = ( + (None, ('write_enabled', 'description', 'expires', 'allowed_ips')), + ) + nullable_fields = ( + 'expires', 'description', 'allowed_ips', + ) diff --git a/netbox/users/forms/bulk_import.py b/netbox/users/forms/bulk_import.py index 25f779044..d1f03ff3c 100644 --- a/netbox/users/forms/bulk_import.py +++ b/netbox/users/forms/bulk_import.py @@ -1,9 +1,13 @@ -from users.models import NetBoxGroup, NetBoxUser +from django import forms +from django.utils.translation import gettext as _ +from users.models import * from utilities.forms import CSVModelForm + __all__ = ( 'GroupImportForm', 'UserImportForm', + 'TokenImportForm', ) @@ -30,3 +34,15 @@ class UserImportForm(CSVModelForm): self.instance.set_password(self.cleaned_data.get('password')) return super().save(*args, **kwargs) + + +class TokenImportForm(CSVModelForm): + key = forms.CharField( + label=_('Key'), + required=False, + help_text=_("If no key is provided, one will be generated automatically.") + ) + + class Meta: + model = Token + fields = ('user', 'key', 'write_enabled', 'expires', 'description',) diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index eca76dea4..ff56cbc4c 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -1,4 +1,7 @@ from django import forms +from extras.forms.mixins import SavedFiltersMixin +from utilities.forms import FilterForm +from users.models import Token from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.utils.translation import gettext_lazy as _ @@ -7,11 +10,13 @@ from netbox.forms import NetBoxModelFilterSetForm from users.models import NetBoxGroup, NetBoxUser, ObjectPermission from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES from utilities.forms.fields import DynamicModelMultipleChoiceField +from utilities.forms.widgets import DateTimePicker __all__ = ( 'GroupFilterForm', 'ObjectPermissionFilterForm', 'UserFilterForm', + 'TokenFilterForm', ) @@ -109,3 +114,33 @@ class ObjectPermissionFilterForm(NetBoxModelFilterSetForm): ), label=_('Can Delete'), ) + + +class TokenFilterForm(SavedFiltersMixin, FilterForm): + model = Token + fieldsets = ( + (None, ('q', 'filter_id',)), + (_('Token'), ('user_id', 'write_enabled', 'expires', 'last_used')), + ) + user_id = DynamicModelMultipleChoiceField( + queryset=get_user_model().objects.all(), + required=False, + label=_('User') + ) + write_enabled = forms.NullBooleanField( + required=False, + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ), + label=_('Write Enabled'), + ) + expires = forms.DateTimeField( + required=False, + label=_('Expires'), + widget=DateTimePicker() + ) + last_used = forms.DateTimeField( + required=False, + label=_('Last Used'), + widget=DateTimePicker() + ) diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 43b95893a..6ca050110 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -20,11 +20,13 @@ from utilities.permissions import qs_filter_from_constraints from utilities.utils import flatten_dict __all__ = ( + 'UserTokenForm', 'GroupForm', 'ObjectPermissionForm', 'TokenForm', 'UserConfigForm', 'UserForm', + 'TokenForm', ) @@ -107,7 +109,7 @@ class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMe ] -class TokenForm(BootstrapMixin, forms.ModelForm): +class UserTokenForm(BootstrapMixin, forms.ModelForm): key = forms.CharField( label=_('Key'), required=False, @@ -117,8 +119,10 @@ class TokenForm(BootstrapMixin, forms.ModelForm): base_field=IPNetworkFormField(validators=[prefix_validator]), required=False, label=_('Allowed IPs'), - help_text=_('Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. ' - 'Example: 10.1.1.0/24,192.168.10.16/32,2001:db8:1::/64'), + help_text=_( + 'Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. ' + 'Example: 10.1.1.0/24,192.168.10.16/32,2001:db8:1::/64' + ), ) class Meta: @@ -138,6 +142,24 @@ class TokenForm(BootstrapMixin, forms.ModelForm): del self.fields['key'] +class TokenForm(UserTokenForm): + user = forms.ModelChoiceField( + queryset=get_user_model().objects.order_by( + 'username' + ), + required=False + ) + + class Meta: + model = Token + fields = [ + 'user', 'key', 'write_enabled', 'expires', 'description', 'allowed_ips', + ] + widgets = { + 'expires': DateTimePicker(), + } + + class UserForm(BootstrapMixin, forms.ModelForm): password = forms.CharField( label=_('Password'), diff --git a/netbox/users/migrations/0005_usertoken.py b/netbox/users/migrations/0005_usertoken.py new file mode 100644 index 000000000..c6aef0590 --- /dev/null +++ b/netbox/users/migrations/0005_usertoken.py @@ -0,0 +1,25 @@ +# 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': [], + 'verbose_name': 'token', + }, + bases=('users.token',), + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index a8060dd63..71434d5ce 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -26,6 +26,7 @@ __all__ = ( 'ObjectPermission', 'Token', 'UserConfig', + 'UserToken', ) @@ -273,13 +274,20 @@ class Token(models.Model): blank=True, null=True, verbose_name='Allowed IPs', - help_text=_('Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. ' - 'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"'), + help_text=_( + 'Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. ' + 'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"' + ), ) + objects = RestrictedQuerySet.as_manager() + def __str__(self): return self.key if settings.ALLOW_TOKEN_RETRIEVAL else self.partial + def get_absolute_url(self): + return reverse('users:token', args=[self.pk]) + @property def partial(self): return f'**********************************{self.key[-6:]}' if self.key else '' @@ -314,6 +322,18 @@ 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('users:usertoken', args=[self.pk]) + + # # Permissions # diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 741a4b024..3ef885399 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -1,8 +1,8 @@ import django_tables2 as tables +from django.utils.translation import gettext as _ from netbox.tables import NetBoxTable, columns -from users.models import NetBoxGroup, NetBoxUser, ObjectPermission -from .models import Token +from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserToken __all__ = ( 'GroupTable', @@ -31,17 +31,28 @@ class TokenActionsColumn(columns.ActionsColumn): } -class TokenTable(NetBoxTable): +class UserTokenTable(NetBoxTable): + """ + Table for users to manager their own API tokens under account views. + """ key = columns.TemplateColumn( - template_code=TOKEN + verbose_name=_('Key'), + template_code=TOKEN, ) write_enabled = columns.BooleanColumn( - verbose_name='Write' + verbose_name=_('Write Enabled') + ) + created = columns.DateColumn( + verbose_name=_('Created'), + ) + expires = columns.DateColumn( + verbose_name=_('Expires'), + ) + last_used = columns.DateTimeColumn( + verbose_name=_('Last Used'), ) - created = columns.DateColumn() - expired = columns.DateColumn() - last_used = columns.DateTimeColumn() allowed_ips = columns.TemplateColumn( + verbose_name=_('Allowed IPs'), template_code=ALLOWED_IPS ) actions = TokenActionsColumn( @@ -49,10 +60,26 @@ 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): + """ + General-purpose table for API token management. + """ + user = tables.Column( + linkify=True, + verbose_name=_('User') + ) + class Meta(NetBoxTable.Meta): model = Token fields = ( - 'pk', 'description', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips', + 'pk', 'id', 'key', 'user', 'description', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips', ) diff --git a/netbox/users/tests/test_views.py b/netbox/users/tests/test_views.py index ca62f474e..2997052eb 100644 --- a/netbox/users/tests/test_views.py +++ b/netbox/users/tests/test_views.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from users.models import * -from utilities.testing import ViewTestCases +from utilities.testing import ViewTestCases, create_test_user class UserTestCase( @@ -149,3 +149,53 @@ class ObjectPermissionTestCase( cls.bulk_edit_data = { 'description': 'New description', } + + +class TokenTestCase( + ViewTestCases.GetObjectViewTestCase, + ViewTestCases.CreateObjectViewTestCase, + ViewTestCases.EditObjectViewTestCase, + ViewTestCases.DeleteObjectViewTestCase, + ViewTestCases.ListObjectsViewTestCase, + ViewTestCases.BulkImportObjectsViewTestCase, + ViewTestCases.BulkEditObjectsViewTestCase, + ViewTestCases.BulkDeleteObjectsViewTestCase, +): + model = Token + maxDiff = None + + @classmethod + def setUpTestData(cls): + users = ( + create_test_user('User 1'), + create_test_user('User 2'), + ) + tokens = ( + Token(key='123456790123456789012345678901234567890A', user=users[0]), + Token(key='123456790123456789012345678901234567890B', user=users[0]), + Token(key='123456790123456789012345678901234567890C', user=users[1]), + ) + Token.objects.bulk_create(tokens) + + cls.form_data = { + 'user': users[0].pk, + 'description': 'testdescription', + } + + cls.csv_data = ( + "key,user,description", + f"123456790123456789012345678901234567890D,{users[0].pk},testdescriptionD", + f"123456790123456789012345678901234567890E,{users[1].pk},testdescriptionE", + f"123456790123456789012345678901234567890F,{users[1].pk},testdescriptionF", + ) + + cls.csv_update_data = ( + "id,description", + f"{tokens[0].pk},testdescriptionH", + f"{tokens[1].pk},testdescriptionI", + f"{tokens[2].pk},testdescriptionJ", + ) + + cls.bulk_edit_data = { + 'description': 'newdescription', + } diff --git a/netbox/users/urls.py b/netbox/users/urls.py index ca331d144..ed3db4661 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -11,9 +11,17 @@ urlpatterns = [ path('bookmarks/', views.BookmarkListView.as_view(), name='bookmarks'), path('preferences/', views.UserConfigView.as_view(), name='preferences'), path('password/', views.ChangePasswordView.as_view(), name='change_password'), - path('api-tokens/', views.TokenListView.as_view(), name='token_list'), - path('api-tokens/add/', views.TokenEditView.as_view(), name='token_add'), - path('api-tokens//', include(get_model_urls('users', 'token'))), + path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'), + path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'), + path('api-tokens//', include(get_model_urls('users', 'usertoken'))), + + # Tokens + path('tokens/', views.TokenListView.as_view(), name='token_list'), + path('tokens/add/', views.TokenEditView.as_view(), name='token_add'), + path('tokens/import/', views.TokenBulkImportView.as_view(), name='token_import'), + path('tokens/edit/', views.TokenBulkEditView.as_view(), name='token_bulk_edit'), + path('tokens/delete/', views.TokenBulkDeleteView.as_view(), name='token_bulk_delete'), + path('tokens//', include(get_model_urls('users', 'token'))), # Users path('users/', views.UserListView.as_view(), name='netboxuser_list'), diff --git a/netbox/users/views.py b/netbox/users/views.py index 99635b514..bc5cf1eeb 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,53 +249,61 @@ class BookmarkListView(LoginRequiredMixin, generic.ObjectListView): # -# API tokens +# User views for token management # -class TokenListView(LoginRequiredMixin, View): +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/token_list.html', { 'tokens': tokens, 'active_tab': 'api-tokens', 'table': table, }) -@register_model_view(Token, 'edit') -class TokenEditView(LoginRequiredMixin, View): +@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, + }) + + +@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) - - form = forms.TokenForm(instance=token) + 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) - form = forms.TokenForm(request.POST, instance=token) + 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) - form = forms.TokenForm(request.POST) + 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() @@ -304,7 +312,7 @@ class TokenEditView(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/token.html', { 'object': token, 'key': token.key, 'return_url': reverse('users:token_list'), @@ -312,53 +320,91 @@ class TokenEditView(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(Token, 'delete') -class TokenDeleteView(LoginRequiredMixin, View): +@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'), }) + +# +# Tokens +# + +class TokenListView(generic.ObjectListView): + queryset = Token.objects.all() + filterset = filtersets.TokenFilterSet + filterset_form = forms.TokenFilterForm + table = tables.TokenTable + + +@register_model_view(Token) +class TokenView(generic.ObjectView): + queryset = Token.objects.all() + + +@register_model_view(Token, 'edit') +class TokenEditView(generic.ObjectEditView): + queryset = Token.objects.all() + form = forms.TokenForm + + +@register_model_view(Token, 'delete') +class TokenDeleteView(generic.ObjectDeleteView): + queryset = Token.objects.all() + + +class TokenBulkImportView(generic.BulkImportView): + queryset = Token.objects.all() + model_form = forms.TokenImportForm + + +class TokenBulkEditView(generic.BulkEditView): + queryset = Token.objects.all() + table = tables.TokenTable + form = forms.TokenBulkEditForm + + +class TokenBulkDeleteView(generic.BulkDeleteView): + queryset = Token.objects.all() + table = tables.TokenTable + + # # Users # - class UserListView(generic.ObjectListView): queryset = NetBoxUser.objects.all() filterset = filtersets.UserFilterSet @@ -413,7 +459,6 @@ class UserBulkDeleteView(generic.BulkDeleteView): # Groups # - class GroupListView(generic.ObjectListView): queryset = NetBoxGroup.objects.annotate(users_count=Count('user')) filterset = filtersets.GroupFilterSet @@ -448,11 +493,11 @@ class GroupBulkDeleteView(generic.BulkDeleteView): filterset = filtersets.GroupFilterSet table = tables.GroupTable + # # ObjectPermissions # - class ObjectPermissionListView(generic.ObjectListView): queryset = ObjectPermission.objects.all() filterset = filtersets.ObjectPermissionFilterSet