diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index a4e9a9fbc..100b2ea03 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -10,7 +10,9 @@ from users.models import ObjectPermission, Token __all__ = ( 'GroupFilterSet', 'ObjectPermissionFilterSet', + 'TokenFilterSet', 'UserFilterSet', + 'UserTokenFilterSet', ) @@ -110,6 +112,54 @@ class TokenFilterSet(BaseFilterSet): ) +class UserTokenFilterSet(BaseFilterSet): + q = django_filters.CharFilter( + method='search', + label=_('Search'), + ) + user_id = django_filters.ModelMultipleChoiceFilter( + field_name='user', + queryset=get_user_model().objects.all(), + label=_('User'), + ) + user = django_filters.ModelMultipleChoiceFilter( + field_name='user__username', + queryset=get_user_model().objects.all(), + to_field_name='username', + label=_('User (name)'), + ) + created = django_filters.DateTimeFilter() + created__gte = django_filters.DateTimeFilter( + field_name='created', + lookup_expr='gte' + ) + created__lte = django_filters.DateTimeFilter( + field_name='created', + lookup_expr='lte' + ) + expires = django_filters.DateTimeFilter() + expires__gte = django_filters.DateTimeFilter( + field_name='expires', + lookup_expr='gte' + ) + expires__lte = django_filters.DateTimeFilter( + field_name='expires', + lookup_expr='lte' + ) + + class Meta: + model = Token + fields = ['id', 'key', 'write_enabled', 'description'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(user__username__icontains=value) | + Q(description__icontains=value) + ) + + class ObjectPermissionFilterSet(BaseFilterSet): q = django_filters.CharFilter( method='search', diff --git a/netbox/users/forms/bulk_edit.py b/netbox/users/forms/bulk_edit.py index 2df698c84..8d63f1406 100644 --- a/netbox/users/forms/bulk_edit.py +++ b/netbox/users/forms/bulk_edit.py @@ -6,16 +6,11 @@ from utilities.forms.widgets import BulkEditNullBooleanSelect __all__ = ( 'ObjectPermissionBulkEditForm', - 'TokenBulkEditForm', 'UserBulkEditForm', + 'UserTokenBulkEditForm', ) -class TokenBulkEditForm(BulkEditForm): - pk = forms.ModelMultipleChoiceField(queryset=Token.objects.all(), widget=forms.MultipleHiddenInput) - description = forms.CharField(max_length=200, required=False) - - class UserBulkEditForm(BootstrapMixin, forms.Form): pk = forms.ModelMultipleChoiceField(queryset=NetBoxUser.objects.all(), widget=forms.MultipleHiddenInput) first_name = forms.CharField(label=_('First name'), max_length=150, required=False) @@ -37,3 +32,8 @@ class ObjectPermissionBulkEditForm(BootstrapMixin, forms.Form): model = ObjectPermission fieldsets = ((None, ('enabled', 'description')),) nullable_fields = ('description',) + + +class UserTokenBulkEditForm(BulkEditForm): + pk = forms.ModelMultipleChoiceField(queryset=Token.objects.all(), widget=forms.MultipleHiddenInput) + description = forms.CharField(max_length=200, required=False) diff --git a/netbox/users/forms/bulk_import.py b/netbox/users/forms/bulk_import.py index a395ad5a5..fd0307062 100644 --- a/netbox/users/forms/bulk_import.py +++ b/netbox/users/forms/bulk_import.py @@ -4,18 +4,11 @@ from utilities.forms import CSVModelForm __all__ = ( 'GroupImportForm', - 'TokenImportForm', 'UserImportForm', + 'UserTokenImportForm', ) -class TokenImportForm(CSVModelForm): - - class Meta: - model = Token - fields = ('description', ) - - class GroupImportForm(CSVModelForm): class Meta: @@ -39,3 +32,10 @@ class UserImportForm(CSVModelForm): self.instance.set_password(self.cleaned_data.get('password')) return super().save(*args, **kwargs) + + +class UserTokenImportForm(CSVModelForm): + + class Meta: + model = Token + fields = ('description', ) diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index 993e88dbc..1f2b276e8 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -14,15 +14,11 @@ from utilities.forms.fields import DynamicModelMultipleChoiceField __all__ = ( 'GroupFilterForm', 'ObjectPermissionFilterForm', - 'TokenFilterForm', 'UserFilterForm', + 'UserTokenFilterForm', ) -class TokenFilterForm(SavedFiltersMixin, FilterForm): - model = Token - - class GroupFilterForm(NetBoxModelFilterSetForm): model = NetBoxGroup fieldsets = ( @@ -117,3 +113,7 @@ class ObjectPermissionFilterForm(NetBoxModelFilterSetForm): ), label=_('Can Delete'), ) + + +class UserTokenFilterForm(SavedFiltersMixin, FilterForm): + model = Token diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 1c0af70fd..55593c8aa 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -32,6 +32,7 @@ __all__ = ( 'TokenForm', 'UserConfigForm', 'UserForm', + 'UserTokenForm', ) @@ -146,6 +147,41 @@ class TokenForm(BootstrapMixin, forms.ModelForm): del self.fields['key'] +class UserTokenForm(BootstrapMixin, forms.ModelForm): + key = forms.CharField( + label=_('Key'), required=False, help_text=_("If no key is provided, one will be generated automatically.") + ) + allowed_ips = SimpleArrayField( + 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' + ), + ) + + class Meta: + model = Token + fields = [ + 'key', + 'write_enabled', + 'expires', + 'description', + 'allowed_ips', + ] + widgets = { + 'expires': DateTimePicker(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Omit the key field if token retrieval is not permitted + if self.instance.pk and not settings.ALLOW_TOKEN_RETRIEVAL: + del self.fields['key'] + + class UserForm(BootstrapMixin, forms.ModelForm): password = forms.CharField( label=_('Password'), diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 741a4b024..8c087aef2 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -9,6 +9,7 @@ __all__ = ( 'ObjectPermissionTable', 'TokenTable', 'UserTable', + 'UserTokenTable', ) @@ -56,6 +57,31 @@ class TokenTable(NetBoxTable): ) +class UserTokenTable(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 = TokenActionsColumn( + actions=('edit', 'delete'), + extra_buttons=COPY_BUTTON + ) + + class Meta(NetBoxTable.Meta): + model = Token + fields = ( + 'pk', 'description', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips', + ) + + class UserTable(NetBoxTable): username = tables.Column( linkify=True diff --git a/netbox/users/views.py b/netbox/users/views.py index 36221e7c2..7fe4f96b4 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -27,7 +27,7 @@ from netbox.config import get_config from netbox.views import generic from . import filtersets, forms, tables -from .filtersets import TokenFilterSet +from .filtersets import TokenFilterSet, UserTokenFilterSet from .models import ( NetBoxGroup, NetBoxUser, @@ -36,7 +36,7 @@ from .models import ( UserConfig, UserToken, ) -from .tables import TokenTable +from .tables import TokenTable, UserTokenTable # # Login/logout @@ -421,9 +421,9 @@ class TokenDeleteView(LoginRequiredMixin, View): class UserTokenListView(generic.ObjectListView): queryset = UserToken.objects.all() - filterset = TokenFilterSet - filterset_form = forms.TokenFilterForm - table = TokenTable + filterset = UserTokenFilterSet + filterset_form = forms.UserTokenFilterForm + table = UserTokenTable @register_model_view(UserToken) @@ -437,7 +437,7 @@ class UserTokenView(generic.ObjectView): @register_model_view(UserToken, 'edit') class UserTokenEditView(generic.ObjectEditView): queryset = UserToken.objects.all() - form = forms.TokenForm + form = forms.UserTokenForm @register_model_view(UserToken, 'delete') @@ -447,18 +447,18 @@ class UserTokenDeleteView(generic.ObjectDeleteView): class UserTokenBulkImportView(generic.BulkImportView): queryset = UserToken.objects.all() - model_form = forms.TokenImportForm + model_form = forms.UserTokenImportForm class UserTokenBulkEditView(generic.BulkEditView): queryset = UserToken.objects.all() table = TokenTable - form = forms.TokenBulkEditForm + form = forms.UserTokenBulkEditForm class UserTokenBulkDeleteView(generic.BulkDeleteView): queryset = UserToken.objects.all() - table = TokenTable + table = UserTokenTable #