Closes #13038: Establish DEFAULT_PERMISSIONS config parameter (#13308)

* Introduce the DEFAULT_PERMISSIONS config parameter

* Establish default permissions for user token management
This commit is contained in:
Jeremy Stretch
2023-07-30 15:04:58 -04:00
committed by GitHub
parent ca634be7ad
commit 07f68ae579
10 changed files with 99 additions and 108 deletions
+4 -3
View File
@@ -1,6 +1,5 @@
from django.urls import include, path
from django.urls import path
from utilities.urls import get_model_urls
from . import views
app_name = 'account'
@@ -13,6 +12,8 @@ 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/<int:pk>/', include(get_model_urls('users', 'usertoken'))),
path('api-tokens/<int:pk>/', views.UserTokenView.as_view(), name='usertoken'),
path('api-tokens/<int:pk>/edit/', views.UserTokenEditView.as_view(), name='usertoken_edit'),
path('api-tokens/<int:pk>/delete/', views.UserTokenDeleteView.as_view(), name='usertoken_delete'),
]
+1 -12
View File
@@ -49,21 +49,10 @@ class GroupViewSet(NetBoxModelViewSet):
#
class TokenViewSet(NetBoxModelViewSet):
queryset = RestrictedQuerySet(model=Token).prefetch_related('user')
queryset = Token.objects.prefetch_related('user')
serializer_class = serializers.TokenSerializer
filterset_class = filtersets.TokenFilterSet
def get_queryset(self):
"""
Limit the non-superusers to their own Tokens.
"""
queryset = super().get_queryset()
if not self.request.user.is_authenticated:
return queryset.none()
if self.request.user.is_superuser:
return queryset
return queryset.filter(user=self.request.user)
class TokenProvisionView(APIView):
"""
+2 -9
View File
@@ -23,14 +23,6 @@ COPY_BUTTON = """
"""
class TokenActionsColumn(columns.ActionsColumn):
# Subclass ActionsColumn to disregard permissions for edit & delete buttons
actions = {
'edit': columns.ActionsItem('Edit', 'pencil', None, 'warning'),
'delete': columns.ActionsItem('Delete', 'trash-can-outline', None, 'danger'),
}
class UserTokenTable(NetBoxTable):
"""
Table for users to manager their own API tokens under account views.
@@ -55,7 +47,8 @@ class UserTokenTable(NetBoxTable):
verbose_name=_('Allowed IPs'),
template_code=ALLOWED_IPS
)
actions = TokenActionsColumn(
# TODO: Fix permissions evaluation & viewname resolution
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=COPY_BUTTON
)
+22 -8
View File
@@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from users.models import ObjectPermission, Token
from utilities.testing import APIViewTestCases, APITestCase
from utilities.testing import APIViewTestCases, APITestCase, create_test_user
from utilities.utils import deepmerge
@@ -105,24 +105,35 @@ class TokenTest(
def setUp(self):
super().setUp()
# Apply grant_token permission to enable the creation of Tokens for other Users
self.add_permissions('users.grant_token')
@classmethod
def setUpTestData(cls):
users = (
create_test_user('User 1'),
create_test_user('User 2'),
create_test_user('User 3'),
)
tokens = (
# We already start with one Token, created by the test class
Token(user=self.user),
Token(user=self.user),
Token(user=users[0]),
Token(user=users[1]),
Token(user=users[2]),
)
# Use save() instead of bulk_create() to ensure keys get automatically generated
for token in tokens:
token.save()
self.create_data = [
cls.create_data = [
{
'user': self.user.pk,
'user': users[0].pk,
},
{
'user': self.user.pk,
'user': users[1].pk,
},
{
'user': self.user.pk,
'user': users[2].pk,
},
]
@@ -161,6 +172,9 @@ class TokenTest(
"""
Test provisioning a Token for a different User with & without the grant_token permission.
"""
# Clear grant_token permission assigned by setUpTestData
ObjectPermission.objects.filter(users=self.user).delete()
self.add_permissions('users.add_token')
user2 = User.objects.create_user(username='testuser2')
data = {
+11 -74
View File
@@ -279,83 +279,20 @@ class UserTokenView(LoginRequiredMixin, View):
})
@register_model_view(UserToken, 'edit')
class UserTokenEditView(LoginRequiredMixin, View):
class UserTokenEditView(generic.ObjectEditView):
queryset = UserToken.objects.all()
form = forms.UserTokenForm
default_return_url = 'account:usertoken_list'
def get(self, request, pk=None):
if pk:
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
else:
token = UserToken(user=request.user)
form = forms.UserTokenForm(instance=token)
return render(request, 'generic/object_edit.html', {
'object': token,
'form': form,
'return_url': reverse('account:usertoken_list'),
})
def post(self, request, pk=None):
if pk:
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
form = forms.UserTokenForm(request.POST, instance=token)
else:
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()
msg = f"Modified token {token}" if pk else f"Created token {token}"
messages.success(request, msg)
if not pk and not settings.ALLOW_TOKEN_RETRIEVAL:
return render(request, 'users/account/token.html', {
'object': token,
'key': token.key,
'return_url': reverse('users:token_list'),
})
elif '_addanother' in request.POST:
return redirect(request.path)
else:
return redirect('account:usertoken_list')
return render(request, 'generic/object_edit.html', {
'object': token,
'form': form,
'return_url': reverse('account:usertoken_list'),
'disable_addanother': not settings.ALLOW_TOKEN_RETRIEVAL
})
def alter_object(self, obj, request, url_args, url_kwargs):
if not obj.pk:
obj.user = request.user
return obj
@register_model_view(UserToken, 'delete')
class UserTokenDeleteView(LoginRequiredMixin, View):
def get(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
return render(request, 'generic/object_delete.html', {
'object': token,
'form': ConfirmationForm(),
'return_url': reverse('account:usertoken_list'),
})
def post(self, request, 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('account:usertoken_list')
return render(request, 'generic/object_delete.html', {
'object': token,
'form': form,
'return_url': reverse('account:usertoken_list'),
})
class UserTokenDeleteView(generic.ObjectDeleteView):
queryset = UserToken.objects.all()
default_return_url = 'account:usertoken_list'
#