From 11fc260038ebfea348789dff1046aae35345a48c Mon Sep 17 00:00:00 2001 From: Nimish Date: Sat, 14 Mar 2026 13:26:08 +0800 Subject: [PATCH] fix: enforce token ownership in DeleteUserTokenMutation --- .../backend/graphene/mutations/environment.py | 14 ++--- backend/tests/api/mutations/__init__.py | 0 .../api/mutations/test_delete_user_token.py | 52 +++++++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 backend/tests/api/mutations/__init__.py create mode 100644 backend/tests/api/mutations/test_delete_user_token.py diff --git a/backend/backend/graphene/mutations/environment.py b/backend/backend/graphene/mutations/environment.py index 39bf61941..d27bad3b2 100644 --- a/backend/backend/graphene/mutations/environment.py +++ b/backend/backend/graphene/mutations/environment.py @@ -505,15 +505,15 @@ class Arguments: def mutate(cls, root, info, token_id): user = info.context.user token = UserToken.objects.get(id=token_id) - org = token.user.organisation - if user_is_org_member(user.userId, org.id): - token.deleted_at = timezone.now() - token.save() + # Verify the token belongs to the requesting user + if token.user.user != user: + raise GraphQLError("You don't have permission to delete this token") - return DeleteUserTokenMutation(ok=True) - else: - raise GraphQLError("You don't have permission to perform this action") + token.deleted_at = timezone.now() + token.save() + + return DeleteUserTokenMutation(ok=True) class CreateServiceTokenMutation(graphene.Mutation): diff --git a/backend/tests/api/mutations/__init__.py b/backend/tests/api/mutations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/tests/api/mutations/test_delete_user_token.py b/backend/tests/api/mutations/test_delete_user_token.py new file mode 100644 index 000000000..994e3b0f2 --- /dev/null +++ b/backend/tests/api/mutations/test_delete_user_token.py @@ -0,0 +1,52 @@ +import pytest +from unittest.mock import MagicMock, patch +from graphql import GraphQLError + + +class TestDeleteUserTokenMutationOwnership: + """Tests that DeleteUserTokenMutation enforces token ownership.""" + + @patch("backend.graphene.mutations.environment.UserToken") + def test_rejects_deletion_of_another_users_token(self, MockUserToken): + """A user should not be able to delete another user's token.""" + from backend.graphene.mutations.environment import DeleteUserTokenMutation + + requesting_user = MagicMock() + requesting_user.userId = "user-A" + + token_owner = MagicMock() + # token.user is an OrgMember, token.user.user is the actual User + mock_token = MagicMock() + mock_token.user.user = token_owner # Different user object + + MockUserToken.objects.get.return_value = mock_token + + mock_info = MagicMock() + mock_info.context.user = requesting_user + + with pytest.raises(GraphQLError, match="You don't have permission to delete this token"): + DeleteUserTokenMutation.mutate(None, mock_info, "token-123") + + # Token should not have been modified + mock_token.save.assert_not_called() + + @patch("backend.graphene.mutations.environment.UserToken") + def test_allows_deletion_of_own_token(self, MockUserToken): + """A user should be able to delete their own token.""" + from backend.graphene.mutations.environment import DeleteUserTokenMutation + + the_user = MagicMock() + + mock_token = MagicMock() + mock_token.user.user = the_user # Same user object + + MockUserToken.objects.get.return_value = mock_token + + mock_info = MagicMock() + mock_info.context.user = the_user # Same user + + result = DeleteUserTokenMutation.mutate(None, mock_info, "token-123") + + assert result.ok is True + mock_token.save.assert_called_once() + assert mock_token.deleted_at is not None