Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions netbox/extras/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from netbox.api.renderers import TextRenderer
from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
from netbox.api.viewsets.mixins import ObjectValidationMixin
from users.models import Token
from utilities.exceptions import RQWorkerNotRunningException
from utilities.request import copy_safe_request
from utilities.rqworker import any_workers_for_queue
Expand Down Expand Up @@ -332,6 +333,12 @@ def post(self, request, pk):
Run a Script identified by its numeric PK or module & name and return the pending Job as the result
"""

# Running a script is a state-changing operation. If token authentication is in use, enforce the token's
# write ability before performing any object lookup. Session-authenticated requests are unaffected
# (request.auth is not a Token).
if isinstance(request.auth, Token) and not request.auth.write_enabled:
raise PermissionDenied("This token does not permit write operations.")

script = self._get_script(pk)

if not request.user.has_perm('extras.run_script', obj=script):
Expand Down
39 changes: 39 additions & 0 deletions netbox/extras/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,45 @@ def test_schedule_script_when_disabled(self):
# Restore the original setting for other tests
self.TestScriptClass.Meta.scheduling_enabled = original

def test_run_token_write_enabled(self):
"""
Running a script is an unsafe (state-changing) action and must be rejected when the calling token has
write_enabled=False, even if the user holds the run_script permission.
"""
self.add_permissions('extras.run_script')
payload = {
'data': {'var1': 'hello', 'var2': 1, 'var3': False},
'commit': True,
}

# A token with write_enabled=False should be rejected
token = Token.objects.create(version=2, user=self.user, write_enabled=False)
token_header = f'Bearer {TOKEN_PREFIX}{token.key}.{token.token}'
response = self.client.post(self.url, payload, format='json', HTTP_AUTHORIZATION=token_header)
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)

# Enabling write ability on the token should allow the script to run
token.write_enabled = True
token.save()
response = self.client.post(self.url, payload, format='json', HTTP_AUTHORIZATION=token_header)
self.assertHttpStatus(response, status.HTTP_200_OK)

def test_run_session_auth(self):
"""
The token write-ability check applies only to token authentication. Session-authenticated requests
(where request.auth is not a Token) must still be allowed to run scripts.
"""
self.add_permissions('extras.run_script')
payload = {
'data': {'var1': 'hello', 'var2': 1, 'var3': False},
'commit': True,
}

# Authenticate via session rather than a token; request.auth is None
self.client.force_authenticate(user=self.user)
response = self.client.post(self.url, payload, format='json')
self.assertHttpStatus(response, status.HTTP_200_OK)


class CreatedUpdatedFilterTestCase(APITestCase):

Expand Down