Skip to content

Commit

Permalink
Preventively patch xmlrpc using the defusedxml package
Browse files Browse the repository at this point in the history
and don't import directly from the standard xmlrpc package eventhough it
is mostly used during testing.

only tcms/issuestracker/bugzilla.py was using xmlrpc directly!
  • Loading branch information
atodorov committed Dec 26, 2023
1 parent a0e7145 commit d5a6098
Show file tree
Hide file tree
Showing 26 changed files with 240 additions and 28 deletions.
1 change: 1 addition & 0 deletions docs/source/modules/tcms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ Submodules
tcms.handlers
tcms.signals
tcms.wsgi
tcms.xmlrpc_wrapper
7 changes: 7 additions & 0 deletions docs/source/modules/tcms.xmlrpc_wrapper.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
tcms.xmlrpc\_wrapper module
===========================

.. automodule:: tcms.xmlrpc_wrapper
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
allpairspy==2.5.1
bleach==6.1.0
bleach-allowlist==1.0.3
defusedxml==0.7.1
Django==4.2.8
django-attachments==1.11
django-colorfield==0.11.0
Expand Down
3 changes: 2 additions & 1 deletion tcms/bugs/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# pylint: disable=attribute-defined-outside-init
# pylint: disable=wrong-import-position
import unittest
from xmlrpc.client import Fault as XmlRPCFault

from django.conf import settings

from tcms.xmlrpc_wrapper import XmlRPCFault

if "tcms.bugs.apps.AppConfig" not in settings.INSTALLED_APPS:
raise unittest.SkipTest("tcms.bugs is disabled")

Expand Down
4 changes: 2 additions & 2 deletions tcms/issuetracker/bugzilla_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import os
import tempfile
from urllib.parse import urlencode
from xmlrpc.client import Fault

import bugzilla
from django.conf import settings

from tcms.core.contrib.linkreference.models import LinkReference
from tcms.issuetracker import base
from tcms.xmlrpc_wrapper import XmlRPCFault


class Bugzilla(base.IssueTrackerType):
Expand Down Expand Up @@ -94,7 +94,7 @@ def _report_issue(self, execution, user):
is_defect=True,
)
return (new_bug, new_bug.weburl)
except Fault:
except XmlRPCFault:
pass

url = self.bug_system.base_url
Expand Down
1 change: 0 additions & 1 deletion tcms/issuetracker/tests/test_gitlab_ce.py

This file was deleted.

192 changes: 192 additions & 0 deletions tcms/issuetracker/tests/test_gitlab_ce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# pylint: disable=attribute-defined-outside-init

import os
import time
import unittest

from tcms.core.contrib.linkreference.models import LinkReference
from tcms.issuetracker.types import Gitlab
from tcms.rpc.tests.utils import APITestCase
from tcms.testcases.models import BugSystem
from tcms.tests.factories import ComponentFactory, TestExecutionFactory


@unittest.skipUnless(
os.getenv("TEST_BUGTRACKER_INTEGRATION"),
"Bug tracker integration testing not enabled",
)
class TestGitlabIntegration(APITestCase):
existing_bug_id = 1
existing_bug_url = "http://bugtracker.kiwitcms.org/root/kiwitcms/issues/1"
existing_bug_url_in_group = (
"http://bugtracker.kiwitcms.org/group/sub_group/kiwitcms_in_group/issues/1"
)

def _fixture_setup(self):
super()._fixture_setup()

self.execution_1 = TestExecutionFactory()
self.execution_1.case.text = "Given-When-Then"
self.execution_1.case.save() # will generate history object

self.component = ComponentFactory(
name="Gitlab integration", product=self.execution_1.run.plan.product
)
self.execution_1.case.add_component(self.component)

bug_system = BugSystem.objects.create( # nosec:B106:hardcoded_password_funcarg
name="GitLab-EE for root/kiwitcms",
tracker_type="tcms.issuetracker.types.Gitlab",
base_url="http://bugtracker.kiwitcms.org/root/kiwitcms/",
api_url="http://bugtracker.kiwitcms.org",
api_password="ypCa3Dzb23o5nvsixwPA",
)
self.integration = Gitlab(bug_system, None)

def test_bug_id_from_url(self):
result = self.integration.bug_id_from_url(self.existing_bug_url)
self.assertEqual(self.existing_bug_id, result)

# this is an alternative URL, with a dash
result = self.integration.bug_id_from_url(
"http://bugtracker.kiwitcms.org/root/kiwitcms/-/issues/1"
)
self.assertEqual(self.existing_bug_id, result)

def test_bug_id_from_url_in_group(self):
bug_system = BugSystem.objects.create( # nosec:B106:hardcoded_password_funcarg
name="GitLab-EE for group/sub_group/kiwitcms_in_group",
tracker_type="tcms.issuetracker.types.Gitlab",
base_url="http://bugtracker.kiwitcms.org/group/sub_group/kiwitcms_in_group/",
api_url="http://bugtracker.kiwitcms.org",
api_password="ypCa3Dzb23o5nvsixwPA",
)
integration = Gitlab(bug_system, None)

result = integration.bug_id_from_url(self.existing_bug_url_in_group)
self.assertEqual(self.existing_bug_id, result)

# this is an alternative URL, with a dash
result = integration.bug_id_from_url(
"http://bugtracker.kiwitcms.org/group/sub_group/kiwitcms_in_group/-/issues/1"
)
self.assertEqual(self.existing_bug_id, result)

def test_details_for_public_url(self):
result = self.integration.details(self.existing_bug_url)

self.assertEqual("Hello GitLab", result["title"])
self.assertEqual("Created via CLI", result["description"])

def test_details_for_public_url_in_group(self):
bug_system = BugSystem.objects.create( # nosec:B106:hardcoded_password_funcarg
name="GitLab-EE for group/sub_group/kiwitcms_in_group",
tracker_type="tcms.issuetracker.types.Gitlab",
base_url="http://bugtracker.kiwitcms.org/group/sub_group/kiwitcms_in_group/",
api_url="http://bugtracker.kiwitcms.org",
api_password="ypCa3Dzb23o5nvsixwPA",
)
integration = Gitlab(bug_system, None)

result = integration.details(self.existing_bug_url_in_group)

self.assertEqual("Hello GitLab Group", result["title"])
self.assertEqual("Created via CLI", result["description"])

def test_details_for_private_url(self):
bug_system = BugSystem.objects.create( # nosec:B106:hardcoded_password_funcarg
name="Private GitLab for root/katinar",
tracker_type="tcms.issuetracker.types.Gitlab",
base_url="http://bugtracker.kiwitcms.org/root/katinar/",
api_url="http://bugtracker.kiwitcms.org",
api_password="ypCa3Dzb23o5nvsixwPA",
)
integration = Gitlab(bug_system, None)

result = integration.details(
"http://bugtracker.kiwitcms.org/root/katinar/-/issues/1"
)

self.assertEqual("Hello Private Issue", result["title"])
self.assertEqual("Created in secret via CLI", result["description"])

def test_auto_update_bugtracker(self):
repo_id = self.integration.repo_id
gl_project = self.integration.rpc.projects.get(repo_id)
gl_issue = gl_project.issues.get(self.existing_bug_id)

# make sure there are no comments to confuse the test
initial_comment_count = 0
for comment in gl_issue.notes.list():
initial_comment_count += 1
self.assertNotIn("Confirmed via test execution", comment.body)

# simulate user adding a new bug URL to a TE and clicking
# 'Automatically update bug tracker'
result = self.rpc_client.TestExecution.add_link(
{
"execution_id": self.execution_1.pk,
"is_defect": True,
"url": self.existing_bug_url,
},
True,
)

# making sure RPC above returned the same URL
self.assertEqual(self.existing_bug_url, result["url"])

# wait until comments have been refreshed b/c this seem to happen async
retries = 0
while len(gl_issue.notes.list()) <= initial_comment_count:
time.sleep(1)
retries += 1
self.assertLess(retries, 20)

# sort by id b/c the gitlab library returns newest comments first but
# that may be depending on configuration !
last_comment = sorted(gl_issue.notes.list(), key=lambda x: x.id)[-1]

# assert that a comment has been added as the last one
# and also verify its text
for expected_string in [
"Confirmed via test execution",
f"TR-{self.execution_1.run_id}: {self.execution_1.run.summary}",
self.execution_1.run.get_full_url(),
f"TE-{self.execution_1.pk}: {self.execution_1.case.summary}",
]:
self.assertIn(expected_string, last_comment.body)

def test_report_issue_from_test_execution_1click_works(self):
# simulate user clicking the 'Report bug' button in TE widget, TR page
result = self.rpc_client.Bug.report(
self.execution_1.pk, self.integration.bug_system.pk
)
self.assertEqual(result["rc"], 0)
self.assertIn(self.integration.bug_system.base_url, result["response"])
self.assertIn("/-/issues/", result["response"])

# assert that the result looks like valid URL parameters
new_issue_id = self.integration.bug_id_from_url(result["response"])
repo_id = self.integration.repo_id
gl_project = self.integration.rpc.projects.get(repo_id)
issue = gl_project.issues.get(new_issue_id)

self.assertEqual(f"Failed test: {self.execution_1.case.summary}", issue.title)
for expected_string in [
f"Filed from execution {self.execution_1.get_full_url()}",
"Reporter",
self.execution_1.build.version.product.name,
self.component.name,
"Steps to reproduce",
self.execution_1.case.text,
]:
self.assertIn(expected_string, issue.description)

# verify that LR has been added to TE
self.assertTrue(
LinkReference.objects.filter(
execution=self.execution_1,
url=result["response"],
is_defect=True,
).exists()
)
10 changes: 5 additions & 5 deletions tcms/kiwi_attachments/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
# pylint: disable=attribute-defined-outside-init, invalid-name, objects-update-used

import base64
from xmlrpc.client import Fault

from django.utils.translation import gettext_lazy as _
from parameterized import parameterized

from tcms.rpc.tests.utils import APITestCase
from tcms.xmlrpc_wrapper import XmlRPCFault


class TestValidators(APITestCase):
Expand All @@ -23,7 +23,7 @@ def test_uploading_svg_with_inline_script_should_fail(self, file_name):

tag_name = "script"
message = str(_(f"File contains forbidden tag: <{tag_name}>"))
with self.assertRaisesRegex(Fault, message):
with self.assertRaisesRegex(XmlRPCFault, message):
self.rpc_client.User.add_attachment("inline_javascript.svg", b64)

@parameterized.expand(
Expand All @@ -37,18 +37,18 @@ def test_uploading_svg_with_forbidden_attributes_should_fail(self, file_name):

attr_name = "onload"
message = str(_(f"File contains forbidden attribute: `{attr_name}`"))
with self.assertRaisesRegex(Fault, message):
with self.assertRaisesRegex(XmlRPCFault, message):
self.rpc_client.User.add_attachment("image.svg", b64)

def test_uploading_filename_ending_in_dot_exe_should_fail(self):
message = str(_("Uploading executable files is forbidden"))
with self.assertRaisesRegex(Fault, message):
with self.assertRaisesRegex(XmlRPCFault, message):
self.rpc_client.User.add_attachment("hello.exe", "a2l3aXRjbXM=")

def test_uploading_real_exe_file_should_fail(self):
with open("tests/ui/data/reactos_csrss.exe", "rb") as exe_file:
b64 = base64.b64encode(exe_file.read()).decode()

message = str(_("Uploading executable files is forbidden"))
with self.assertRaisesRegex(Fault, message):
with self.assertRaisesRegex(XmlRPCFault, message):
self.rpc_client.User.add_attachment("csrss.exe_from_reactos", b64)
2 changes: 1 addition & 1 deletion tcms/rpc/tests/test_attachment.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# pylint: disable=attribute-defined-outside-init, invalid-name, objects-update-used

from xmlrpc.client import Fault as XmlRPCFault

from tcms.rpc.tests.utils import APIPermissionsTestCase, APITestCase
from tcms.tests import user_should_have_perm
from tcms.tests.factories import TestPlanFactory
from tcms.xmlrpc_wrapper import XmlRPCFault


class TestRemoveAttachment(APITestCase):
Expand Down
2 changes: 1 addition & 1 deletion tcms/rpc/tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# pylint: disable=attribute-defined-outside-init

from xmlrpc.client import Fault as XmlRPCFault

from tcms.rpc.tests.utils import APITestCase
from tcms.xmlrpc_wrapper import XmlRPCFault


class TestAuthLogin(APITestCase):
Expand Down
2 changes: 1 addition & 1 deletion tcms/rpc/tests/test_build.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
# pylint: disable=invalid-name, attribute-defined-outside-init, objects-update-used

from xmlrpc.client import Fault as XmlRPCFault

from django.test import override_settings

from tcms.rpc.tests.utils import APITestCase
from tcms.tests.factories import BuildFactory, VersionFactory
from tcms.xmlrpc_wrapper import XmlRPCFault


@override_settings(LANGUAGE_CODE="en")
Expand Down
2 changes: 1 addition & 1 deletion tcms/rpc/tests/test_category.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# pylint: disable=attribute-defined-outside-init, invalid-name, objects-update-used

from xmlrpc.client import Fault as XmlRPCFault

from tcms.rpc.tests.utils import APIPermissionsTestCase, APITestCase
from tcms.testcases.models import Category
from tcms.tests.factories import CategoryFactory, ProductFactory
from tcms.xmlrpc_wrapper import XmlRPCFault


class TestCategory(APITestCase):
Expand Down
2 changes: 1 addition & 1 deletion tcms/rpc/tests/test_classification.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# pylint: disable=attribute-defined-outside-init

from xmlrpc.client import Fault as XmlRPCFault

from tcms.management.models import Classification
from tcms.rpc.tests.utils import APIPermissionsTestCase, APITestCase
from tcms.tests.factories import ClassificationFactory
from tcms.xmlrpc_wrapper import XmlRPCFault


class TestClassificationFilter(APITestCase):
Expand Down
2 changes: 1 addition & 1 deletion tcms/rpc/tests/test_component.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# pylint: disable=attribute-defined-outside-init, invalid-name

from xmlrpc.client import Fault as XmlRPCFault

from tcms.rpc.tests.utils import APITestCase
from tcms.tests.factories import ComponentFactory, ProductFactory
from tcms.xmlrpc_wrapper import XmlRPCFault


class TestFilterComponents(APITestCase):
Expand Down
2 changes: 1 addition & 1 deletion tcms/rpc/tests/test_environment.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
from xmlrpc.client import Fault as XmlRPCFault

from django.test import override_settings

from tcms.rpc.tests.utils import APIPermissionsTestCase, APITestCase
from tcms.testruns.models import Environment
from tcms.xmlrpc_wrapper import XmlRPCFault


class TestFilterPermission(APIPermissionsTestCase):
Expand Down
2 changes: 1 addition & 1 deletion tcms/rpc/tests/test_plantype.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# pylint: disable=attribute-defined-outside-init

from xmlrpc.client import Fault as XmlRPCFault

from tcms.rpc.tests.utils import APIPermissionsTestCase
from tcms.testplans.models import PlanType
from tcms.tests.factories import PlanTypeFactory
from tcms.xmlrpc_wrapper import XmlRPCFault


class TestPlanTypeFilter(APIPermissionsTestCase):
Expand Down

0 comments on commit d5a6098

Please sign in to comment.