mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-12 11:29:36 -06:00
Closes #16681: Introduce render_config permission for configuration rendering (#20555)
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
* Closes #16681: Introduce render_config permission for configuration rendering Add a new custom permission action `render_config` for rendering device and virtual machine configurations via the REST API. This allows users to render configurations without requiring the `add` permission. Changes: - Add permission check to RenderConfigMixin.render_config() for devices and VMs - Update API tests to use render_config permission instead of add - Add tests verifying permission enforcement (403 without render_config) - Document new permission requirement in configuration-rendering.md Note: Currently requires both render_config AND add permissions due to the automatic POST='add' filter in BaseViewSet.initial(). Removing the add requirement will be addressed in a follow-up commit. * Correct permission denied message and enable translation * Remove add permission requirement for render_config endpoint Remove the add permission requirement from the render-config API endpoint while maintaining token write_enabled enforcement as specified in #16681. Changes: - Add TokenWritePermission class to check token write ability without requiring specific model permissions - Override get_permissions() in RenderConfigMixin to use TokenWritePermission instead of TokenPermissions for render_config action - Replace queryset restriction: use render_config instead of add - Remove add permissions from tests - render_config permission now sufficient - Update tests to expect 404 when permission denied (NetBox standard pattern) Per #16681: 'requirement for write permission makes sense for API calls (because we're accepting and processing arbitrary user data), the specific permission for creating devices does not' * Add render_config permission to ConfigTemplate render endpoint Extend render_config permission requirement to the ConfigTemplate render endpoint per issue comments. Changes: - Add TokenWritePermission check via get_permissions() override in ConfigTemplateViewSet - Restrict queryset to render_config permission in render() method - Add explicit render_config permission check - Add tests for ConfigTemplate.render() with and without permission - Update documentation to include ConfigTemplate endpoint * Address PR feedback on render_config permissions Remove redundant permission checks, add view permission enforcement via chained restrict() calls, and rename ConfigTemplate permission action from render_config to render for consistency. * Address second round of PR feedback on render_config permissions - Remove ConfigTemplate view permission check from render_config endpoint - Add sanity check to TokenWritePermission for non-token auth - Use named URL patterns instead of string concatenation in tests - Remove extras.view_configtemplate from test permissions - Add token write_enabled enforcement tests for all render endpoints * Misc cleanup --------- Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
87505e0bb9
commit
5bbab7eb47
@ -90,3 +90,10 @@ http://netbox:8000/api/extras/config-templates/123/render/ \
|
|||||||
"bar": 123
|
"bar": 123
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! note "Permissions"
|
||||||
|
Rendering configuration templates via the REST API requires appropriate permissions for the relevant object type:
|
||||||
|
|
||||||
|
* To render a device's configuration via `/api/dcim/devices/{id}/render-config/`, assign a permission for "DCIM > Device" with the `render_config` action.
|
||||||
|
* To render a virtual machine's configuration via `/api/virtualization/virtual-machines/{id}/render-config/`, assign a permission for "Virtualization > Virtual Machine" with the `render_config` action.
|
||||||
|
* To render a config template directly via `/api/extras/config-templates/{id}/render/`, assign a permission for "Extras > Config Template" with the `render` action.
|
||||||
|
|||||||
@ -13,7 +13,8 @@ from ipam.choices import VLANQinQRoleChoices
|
|||||||
from ipam.models import ASN, RIR, VLAN, VRF
|
from ipam.models import ASN, RIR, VLAN, VRF
|
||||||
from netbox.api.serializers import GenericObjectSerializer
|
from netbox.api.serializers import GenericObjectSerializer
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from users.models import User
|
from users.constants import TOKEN_PREFIX
|
||||||
|
from users.models import Token, User
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
from wireless.choices import WirelessChannelChoices
|
from wireless.choices import WirelessChannelChoices
|
||||||
@ -1306,7 +1307,6 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
}
|
}
|
||||||
user_permissions = (
|
user_permissions = (
|
||||||
'dcim.view_site', 'dcim.view_rack', 'dcim.view_location', 'dcim.view_devicerole', 'dcim.view_devicetype',
|
'dcim.view_site', 'dcim.view_rack', 'dcim.view_location', 'dcim.view_devicerole', 'dcim.view_devicetype',
|
||||||
'extras.view_configtemplate',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1486,12 +1486,58 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|||||||
device.config_template = configtemplate
|
device.config_template = configtemplate
|
||||||
device.save()
|
device.save()
|
||||||
|
|
||||||
self.add_permissions('dcim.add_device')
|
self.add_permissions('dcim.render_config_device', 'dcim.view_device')
|
||||||
url = reverse('dcim-api:device-detail', kwargs={'pk': device.pk}) + 'render-config/'
|
url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk})
|
||||||
response = self.client.post(url, {}, format='json', **self.header)
|
response = self.client.post(url, {}, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['content'], f'Config for device {device.name}')
|
self.assertEqual(response.data['content'], f'Config for device {device.name}')
|
||||||
|
|
||||||
|
def test_render_config_without_permission(self):
|
||||||
|
configtemplate = ConfigTemplate.objects.create(
|
||||||
|
name='Config Template 1',
|
||||||
|
template_code='Config for device {{ device.name }}'
|
||||||
|
)
|
||||||
|
|
||||||
|
device = Device.objects.first()
|
||||||
|
device.config_template = configtemplate
|
||||||
|
device.save()
|
||||||
|
|
||||||
|
# No permissions added - user has no render_config permission
|
||||||
|
url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk})
|
||||||
|
response = self.client.post(url, {}, format='json', **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
def test_render_config_token_write_enabled(self):
|
||||||
|
configtemplate = ConfigTemplate.objects.create(
|
||||||
|
name='Config Template 1',
|
||||||
|
template_code='Config for device {{ device.name }}'
|
||||||
|
)
|
||||||
|
|
||||||
|
device = Device.objects.first()
|
||||||
|
device.config_template = configtemplate
|
||||||
|
device.save()
|
||||||
|
|
||||||
|
self.add_permissions('dcim.render_config_device', 'dcim.view_device')
|
||||||
|
url = reverse('dcim-api:device-render-config', kwargs={'pk': device.pk})
|
||||||
|
|
||||||
|
# Request without token auth should fail with PermissionDenied
|
||||||
|
response = self.client.post(url, {}, format='json')
|
||||||
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# Create token with write_enabled=False
|
||||||
|
token = Token.objects.create(version=2, user=self.user, write_enabled=False)
|
||||||
|
token_header = f'Bearer {TOKEN_PREFIX}{token.key}.{token.token}'
|
||||||
|
|
||||||
|
# Request with write-disabled token should fail
|
||||||
|
response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# Enable write and retry
|
||||||
|
token.write_enabled = True
|
||||||
|
token.save()
|
||||||
|
response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTest(APIViewTestCases.APIViewTestCase):
|
class ModuleTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Module
|
model = Module
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from rest_framework.renderers import JSONRenderer
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||||
|
|
||||||
|
from netbox.api.authentication import TokenWritePermission
|
||||||
from netbox.api.renderers import TextRenderer
|
from netbox.api.renderers import TextRenderer
|
||||||
from .serializers import ConfigTemplateSerializer
|
from .serializers import ConfigTemplateSerializer
|
||||||
|
|
||||||
@ -64,12 +65,24 @@ class RenderConfigMixin(ConfigTemplateRenderMixin):
|
|||||||
"""
|
"""
|
||||||
Provides a /render-config/ endpoint for REST API views whose model may have a ConfigTemplate assigned.
|
Provides a /render-config/ endpoint for REST API views whose model may have a ConfigTemplate assigned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
# For render_config action, check only token write ability (not model permissions)
|
||||||
|
if self.action == 'render_config':
|
||||||
|
return [TokenWritePermission()]
|
||||||
|
return super().get_permissions()
|
||||||
|
|
||||||
@action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer])
|
@action(detail=True, methods=['post'], url_path='render-config', renderer_classes=[JSONRenderer, TextRenderer])
|
||||||
def render_config(self, request, pk):
|
def render_config(self, request, pk):
|
||||||
"""
|
"""
|
||||||
Resolve and render the preferred ConfigTemplate for this Device.
|
Resolve and render the preferred ConfigTemplate for this Device.
|
||||||
"""
|
"""
|
||||||
|
# Override restrict() on the default queryset to enforce the render_config & view actions
|
||||||
|
self.queryset = self.queryset.model.objects.restrict(request.user, 'render_config').restrict(
|
||||||
|
request.user, 'view'
|
||||||
|
)
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
|
|
||||||
object_type = instance._meta.model_name
|
object_type = instance._meta.model_name
|
||||||
configtemplate = instance.get_config_template()
|
configtemplate = instance.get_config_template()
|
||||||
if not configtemplate:
|
if not configtemplate:
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from rq import Worker
|
|||||||
from extras import filtersets
|
from extras import filtersets
|
||||||
from extras.jobs import ScriptJob
|
from extras.jobs import ScriptJob
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired, TokenWritePermission
|
||||||
from netbox.api.features import SyncedDataMixin
|
from netbox.api.features import SyncedDataMixin
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
from netbox.api.metadata import ContentTypeMetadata
|
||||||
from netbox.api.renderers import TextRenderer
|
from netbox.api.renderers import TextRenderer
|
||||||
@ -238,13 +238,22 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
|
|||||||
serializer_class = serializers.ConfigTemplateSerializer
|
serializer_class = serializers.ConfigTemplateSerializer
|
||||||
filterset_class = filtersets.ConfigTemplateFilterSet
|
filterset_class = filtersets.ConfigTemplateFilterSet
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
# For render action, check only token write ability (not model permissions)
|
||||||
|
if self.action == 'render':
|
||||||
|
return [TokenWritePermission()]
|
||||||
|
return super().get_permissions()
|
||||||
|
|
||||||
@action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
|
@action(detail=True, methods=['post'], renderer_classes=[JSONRenderer, TextRenderer])
|
||||||
def render(self, request, pk):
|
def render(self, request, pk):
|
||||||
"""
|
"""
|
||||||
Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
|
Render a ConfigTemplate using the context data provided (if any). If the client requests "text/plain" data,
|
||||||
return the raw rendered content, rather than serialized JSON.
|
return the raw rendered content, rather than serialized JSON.
|
||||||
"""
|
"""
|
||||||
|
# Override restrict() on the default queryset to enforce the render & view actions
|
||||||
|
self.queryset = self.queryset.model.objects.restrict(request.user, 'render').restrict(request.user, 'view')
|
||||||
configtemplate = self.get_object()
|
configtemplate = self.get_object()
|
||||||
|
|
||||||
context = request.data
|
context = request.data
|
||||||
|
|
||||||
return self.render_configtemplate(request, configtemplate, context)
|
return self.render_configtemplate(request, configtemplate, context)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import datetime
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
from core.events import *
|
from core.events import *
|
||||||
@ -11,7 +12,8 @@ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Loca
|
|||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
|
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
|
||||||
from users.models import Group, User
|
from users.constants import TOKEN_PREFIX
|
||||||
|
from users.models import Group, Token, User
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
|
|
||||||
|
|
||||||
@ -854,6 +856,47 @@ class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
|
|||||||
)
|
)
|
||||||
ConfigTemplate.objects.bulk_create(config_templates)
|
ConfigTemplate.objects.bulk_create(config_templates)
|
||||||
|
|
||||||
|
def test_render(self):
|
||||||
|
configtemplate = ConfigTemplate.objects.first()
|
||||||
|
|
||||||
|
self.add_permissions('extras.render_configtemplate', 'extras.view_configtemplate')
|
||||||
|
url = reverse('extras-api:configtemplate-render', kwargs={'pk': configtemplate.pk})
|
||||||
|
response = self.client.post(url, {'foo': 'bar'}, format='json', **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['content'], 'Foo: bar')
|
||||||
|
|
||||||
|
def test_render_without_permission(self):
|
||||||
|
configtemplate = ConfigTemplate.objects.first()
|
||||||
|
|
||||||
|
# No permissions added - user has no render permission
|
||||||
|
url = reverse('extras-api:configtemplate-render', kwargs={'pk': configtemplate.pk})
|
||||||
|
response = self.client.post(url, {'foo': 'bar'}, format='json', **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
def test_render_token_write_enabled(self):
|
||||||
|
configtemplate = ConfigTemplate.objects.first()
|
||||||
|
|
||||||
|
self.add_permissions('extras.render_configtemplate', 'extras.view_configtemplate')
|
||||||
|
url = reverse('extras-api:configtemplate-render', kwargs={'pk': configtemplate.pk})
|
||||||
|
|
||||||
|
# Request without token auth should fail with PermissionDenied
|
||||||
|
response = self.client.post(url, {'foo': 'bar'}, format='json')
|
||||||
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# Create token with write_enabled=False
|
||||||
|
token = Token.objects.create(version=2, user=self.user, write_enabled=False)
|
||||||
|
token_header = f'Bearer {TOKEN_PREFIX}{token.key}.{token.token}'
|
||||||
|
|
||||||
|
# Request with write-disabled token should fail
|
||||||
|
response = self.client.post(url, {'foo': 'bar'}, format='json', HTTP_AUTHORIZATION=token_header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# Enable write and retry
|
||||||
|
token.write_enabled = True
|
||||||
|
token.save()
|
||||||
|
response = self.client.post(url, {'foo': 'bar'}, format='json', HTTP_AUTHORIZATION=token_header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class ScriptTest(APITestCase):
|
class ScriptTest(APITestCase):
|
||||||
|
|
||||||
|
|||||||
@ -164,6 +164,20 @@ class TokenPermissions(DjangoObjectPermissions):
|
|||||||
return super().has_object_permission(request, view, obj)
|
return super().has_object_permission(request, view, obj)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenWritePermission(BasePermission):
|
||||||
|
"""
|
||||||
|
Verify the token has write_enabled for unsafe methods, without requiring specific model permissions.
|
||||||
|
Used for custom actions that accept user data but don't map to standard CRUD operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not isinstance(request.auth, Token):
|
||||||
|
raise exceptions.PermissionDenied(
|
||||||
|
"TokenWritePermission requires token authentication."
|
||||||
|
)
|
||||||
|
return bool(request.method in SAFE_METHODS or request.auth.write_enabled)
|
||||||
|
|
||||||
|
|
||||||
class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
||||||
"""
|
"""
|
||||||
Returns True if the user is authenticated or LOGIN_REQUIRED is False.
|
Returns True if the user is authenticated or LOGIN_REQUIRED is False.
|
||||||
|
|||||||
@ -12,6 +12,8 @@ from extras.choices import CustomFieldTypeChoices
|
|||||||
from extras.models import ConfigTemplate, CustomField
|
from extras.models import ConfigTemplate, CustomField
|
||||||
from ipam.choices import VLANQinQRoleChoices
|
from ipam.choices import VLANQinQRoleChoices
|
||||||
from ipam.models import Prefix, VLAN, VRF
|
from ipam.models import Prefix, VLAN, VRF
|
||||||
|
from users.constants import TOKEN_PREFIX
|
||||||
|
from users.models import Token
|
||||||
from utilities.testing import (
|
from utilities.testing import (
|
||||||
APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine, disable_logging,
|
APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine, disable_logging,
|
||||||
)
|
)
|
||||||
@ -281,12 +283,60 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
|||||||
vm.config_template = configtemplate
|
vm.config_template = configtemplate
|
||||||
vm.save()
|
vm.save()
|
||||||
|
|
||||||
self.add_permissions('virtualization.add_virtualmachine')
|
self.add_permissions(
|
||||||
url = reverse('virtualization-api:virtualmachine-detail', kwargs={'pk': vm.pk}) + 'render-config/'
|
'virtualization.render_config_virtualmachine', 'virtualization.view_virtualmachine'
|
||||||
|
)
|
||||||
|
url = reverse('virtualization-api:virtualmachine-render-config', kwargs={'pk': vm.pk})
|
||||||
response = self.client.post(url, {}, format='json', **self.header)
|
response = self.client.post(url, {}, format='json', **self.header)
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['content'], f'Config for virtual machine {vm.name}')
|
self.assertEqual(response.data['content'], f'Config for virtual machine {vm.name}')
|
||||||
|
|
||||||
|
def test_render_config_without_permission(self):
|
||||||
|
configtemplate = ConfigTemplate.objects.create(
|
||||||
|
name='Config Template 1',
|
||||||
|
template_code='Config for virtual machine {{ virtualmachine.name }}'
|
||||||
|
)
|
||||||
|
|
||||||
|
vm = VirtualMachine.objects.first()
|
||||||
|
vm.config_template = configtemplate
|
||||||
|
vm.save()
|
||||||
|
|
||||||
|
# No permissions added - user has no render_config permission
|
||||||
|
url = reverse('virtualization-api:virtualmachine-render-config', kwargs={'pk': vm.pk})
|
||||||
|
response = self.client.post(url, {}, format='json', **self.header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
def test_render_config_token_write_enabled(self):
|
||||||
|
configtemplate = ConfigTemplate.objects.create(
|
||||||
|
name='Config Template 1',
|
||||||
|
template_code='Config for virtual machine {{ virtualmachine.name }}'
|
||||||
|
)
|
||||||
|
|
||||||
|
vm = VirtualMachine.objects.first()
|
||||||
|
vm.config_template = configtemplate
|
||||||
|
vm.save()
|
||||||
|
|
||||||
|
self.add_permissions('virtualization.render_config_virtualmachine', 'virtualization.view_virtualmachine')
|
||||||
|
url = reverse('virtualization-api:virtualmachine-render-config', kwargs={'pk': vm.pk})
|
||||||
|
|
||||||
|
# Request without token auth should fail with PermissionDenied
|
||||||
|
response = self.client.post(url, {}, format='json')
|
||||||
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# Create token with write_enabled=False
|
||||||
|
token = Token.objects.create(version=2, user=self.user, write_enabled=False)
|
||||||
|
token_header = f'Bearer {TOKEN_PREFIX}{token.key}.{token.token}'
|
||||||
|
|
||||||
|
# Request with write-disabled token should fail
|
||||||
|
response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# Enable write and retry
|
||||||
|
token.write_enabled = True
|
||||||
|
token.save()
|
||||||
|
response = self.client.post(url, {}, format='json', HTTP_AUTHORIZATION=token_header)
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
|
class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user