Skip to content

Commit a200ad7

Browse files
authored
refactor: decouple Jira integration into dojo/jira package (#14743)
* refactor: decouple Jira integration into dojo/jira package Introduce a new dojo/jira package (models, forms, api, helper, queries, urls, views) and a top-level dojo/jira_facade.py. Route all Jira references from the rest of the codebase (models.py properties, forms, api_v2 serializers and views, view files, tasks, display_tags, risk_acceptance/helper, finding/helper, importers, utils SLA notifications, engagement/services, management commands) through the facade so no module outside the Jira package imports Jira internals directly. Also resolves a circular import in dojo/jira/models.py, updates test mock paths for the new module layout, and fixes ruff lint across the moved files. * fix(importers): remove unused FindingLocationStatus import after dev merge Stale leftover from a prior version of jira-cleanup; the file uses jira_services.is_keep_in_sync directly and never references FindingLocationStatus. Resolves the ruff-linting CI failure on PR #14743 after merging upstream/dev.
1 parent 8428317 commit a200ad7

44 files changed

Lines changed: 1595 additions & 1018 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dryrunsecurity.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ sensitiveCodepaths:
1414
- 'dojo/group/*.py'
1515
- 'dojo/importers/*.py'
1616
- 'dojo/importers/**/*.py'
17-
- 'dojo/jira_link/*.py'
17+
- 'dojo/jira/*.py'
18+
- 'dojo/jira/**/*.py'
1819
- 'dojo/metrics/*.py'
1920
- 'dojo/note_type/*.py'
2021
- 'dojo/notes/*.py'

dojo/api_v2/serializers.py

Lines changed: 15 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from rest_framework.fields import DictField, MultipleChoiceField
2525

2626
import dojo.finding.helper as finding_helper
27-
import dojo.jira_link.helper as jira_helper
2827
import dojo.risk_acceptance.helper as ra_helper
2928
from dojo.authorization.authorization import user_has_permission
3029
from dojo.authorization.roles_permissions import Permissions
@@ -40,6 +39,7 @@
4039
from dojo.importers.base_importer import BaseImporter
4140
from dojo.importers.default_importer import DefaultImporter
4241
from dojo.importers.default_reimporter import DefaultReImporter
42+
from dojo.jira import services as jira_services
4343
from dojo.location.models import Location, LocationFindingReference
4444
from dojo.models import (
4545
DEFAULT_NOTIFICATION,
@@ -75,9 +75,6 @@
7575
Finding_Template,
7676
General_Survey,
7777
Global_Role,
78-
JIRA_Instance,
79-
JIRA_Issue,
80-
JIRA_Project,
8178
Language_Type,
8279
Languages,
8380
Network_Locations,
@@ -1376,79 +1373,11 @@ class Meta:
13761373
fields = "__all__"
13771374

13781375

1379-
class JIRAIssueSerializer(serializers.ModelSerializer):
1380-
url = serializers.SerializerMethodField(read_only=True)
1381-
1382-
class Meta:
1383-
model = JIRA_Issue
1384-
fields = "__all__"
1385-
1386-
def get_url(self, obj) -> str:
1387-
return jira_helper.get_jira_issue_url(obj)
1388-
1389-
def validate(self, data):
1390-
if self.context["request"].method == "PATCH":
1391-
engagement = data.get("engagement", self.instance.engagement)
1392-
finding = data.get("finding", self.instance.finding)
1393-
finding_group = data.get(
1394-
"finding_group", self.instance.finding_group,
1395-
)
1396-
else:
1397-
engagement = data.get("engagement", None)
1398-
finding = data.get("finding", None)
1399-
finding_group = data.get("finding_group", None)
1400-
1401-
if (
1402-
(engagement and not finding and not finding_group)
1403-
or (finding and not engagement and not finding_group)
1404-
or (finding_group and not engagement and not finding)
1405-
):
1406-
pass
1407-
else:
1408-
msg = "Either engagement or finding or finding_group has to be set."
1409-
raise serializers.ValidationError(msg)
1410-
1411-
if finding:
1412-
if (linked_finding := jira_helper.jira_already_linked(finding, data.get("jira_key"), data.get("jira_id"))) is not None:
1413-
msg = "JIRA issue " + data.get("jira_key") + " already linked to " + reverse("view_finding", args=(linked_finding.id,))
1414-
raise serializers.ValidationError(msg)
1415-
1416-
return data
1417-
1418-
1419-
class JIRAInstanceSerializer(serializers.ModelSerializer):
1420-
class Meta:
1421-
model = JIRA_Instance
1422-
fields = "__all__"
1423-
extra_kwargs = {
1424-
"password": {"write_only": True},
1425-
}
1426-
1427-
1428-
class JIRAProjectSerializer(serializers.ModelSerializer):
1429-
class Meta:
1430-
model = JIRA_Project
1431-
fields = "__all__"
1432-
1433-
def validate(self, data):
1434-
if self.context["request"].method == "PATCH":
1435-
engagement = data.get("engagement", self.instance.engagement)
1436-
product = data.get("product", self.instance.product)
1437-
else:
1438-
engagement = data.get("engagement", None)
1439-
product = data.get("product", None)
1440-
1441-
if (engagement and product) or (not engagement and not product):
1442-
msg = "Either engagement or product has to be set."
1443-
raise serializers.ValidationError(msg)
1444-
1445-
if "custom_fields" in data and isinstance(data["custom_fields"], str):
1446-
try:
1447-
data["custom_fields"] = json.loads(data["custom_fields"])
1448-
except json.JSONDecodeError as e:
1449-
raise serializers.ValidationError({"custom_fields": f"Invalid JSON: {e}"}) from e
1450-
1451-
return data
1376+
from dojo.jira.api.serializers import ( # noqa: E402, F401 backward compat
1377+
JIRAInstanceSerializer,
1378+
JIRAIssueSerializer,
1379+
JIRAProjectSerializer,
1380+
)
14521381

14531382

14541383
class SonarqubeIssueSerializer(serializers.ModelSerializer):
@@ -1770,7 +1699,7 @@ def get_test(self, obj):
17701699

17711700
@extend_schema_field(JIRAIssueSerializer)
17721701
def get_jira(self, obj):
1773-
issue = jira_helper.get_jira_issue(obj)
1702+
issue = jira_services.get_issue(obj)
17741703
if issue is None:
17751704
return None
17761705
return JIRAIssueSerializer(read_only=True).to_representation(issue)
@@ -1844,11 +1773,11 @@ def get_accepted_risks(self, obj):
18441773

18451774
@extend_schema_field(serializers.DateTimeField())
18461775
def get_jira_creation(self, obj):
1847-
return jira_helper.get_jira_creation(obj)
1776+
return jira_services.get_creation(obj)
18481777

18491778
@extend_schema_field(serializers.DateTimeField())
18501779
def get_jira_change(self, obj):
1851-
return jira_helper.get_jira_change(obj)
1780+
return jira_services.get_change(obj)
18521781

18531782
@extend_schema_field(FindingRelatedFieldsSerializer)
18541783
def get_related_fields(self, obj):
@@ -1924,9 +1853,9 @@ def update(self, instance, validated_data):
19241853
for location_ref in locations:
19251854
location_ref.location.associate_with_finding(instance)
19261855

1927-
if push_to_jira or finding_helper.is_keep_in_sync_with_jira(instance):
1856+
if push_to_jira or jira_services.is_keep_in_sync(instance):
19281857
# Push synchronously so that we can see jira errors in real time
1929-
success, message = jira_helper.push_to_jira(instance, sync=True)
1858+
success, message = jira_services.push(instance, sync=True)
19301859
if not success:
19311860
raise serializers.ValidationError(message)
19321861

@@ -2083,7 +2012,7 @@ def create(self, validated_data):
20832012
save_vulnerability_ids(new_finding, parsed_vulnerability_ids)
20842013

20852014
if push_to_jira:
2086-
jira_helper.push_to_jira(new_finding)
2015+
jira_services.push(new_finding)
20872016

20882017
# Create a notification
20892018
create_notification(
@@ -3081,9 +3010,9 @@ class ReportGenerateSerializer(serializers.Serializer):
30813010
)
30823011

30833012

3084-
class EngagementUpdateJiraEpicSerializer(serializers.Serializer):
3085-
epic_name = serializers.CharField(required=False, max_length=200)
3086-
epic_priority = serializers.CharField(required=False, allow_null=True)
3013+
from dojo.jira.api.serializers import ( # noqa: E402, F401 backward compat
3014+
EngagementUpdateJiraEpicSerializer,
3015+
)
30873016

30883017

30893018
class TagSerializer(serializers.Serializer):

dojo/api_v2/views.py

Lines changed: 17 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
from rest_framework.response import Response
3838

3939
import dojo.finding.helper as finding_helper
40-
import dojo.jira_link.helper as jira_helper
4140
from dojo.api_v2 import (
4241
mixins as dojo_mixins,
4342
)
@@ -88,10 +87,7 @@
8887
get_authorized_groups,
8988
)
9089
from dojo.importers.auto_create_context import AutoCreateContextManager
91-
from dojo.jira_link.queries import (
92-
get_authorized_jira_issues,
93-
get_authorized_jira_projects,
94-
)
90+
from dojo.jira import services as jira_services
9591
from dojo.labels import get_labels
9692
from dojo.models import (
9793
Announcement,
@@ -117,9 +113,6 @@
117113
Finding_Template,
118114
General_Survey,
119115
Global_Role,
120-
JIRA_Instance,
121-
JIRA_Issue,
122-
JIRA_Project,
123116
Language_Type,
124117
Languages,
125118
Network_Locations,
@@ -720,15 +713,18 @@ def download_file(self, request, file_id, pk=None):
720713
def update_jira_epic(self, request, pk=None):
721714
engagement = self.get_object()
722715
try:
723-
724716
if engagement.has_jira_issue:
725-
dojo_dispatch_task(jira_helper.update_epic, engagement.id, **request.data)
717+
task = jira_services.get_epic_task("update_epic")
718+
if task:
719+
dojo_dispatch_task(task, engagement.id, **request.data)
726720
response = Response(
727721
{"info": "Jira Epic update query sent"},
728722
status=status.HTTP_200_OK,
729723
)
730724
else:
731-
dojo_dispatch_task(jira_helper.add_epic, engagement.id, **request.data)
725+
task = jira_services.get_epic_task("add_epic")
726+
if task:
727+
dojo_dispatch_task(task, engagement.id, **request.data)
732728
response = Response(
733729
{"info": "Jira Epic create query sent"},
734730
status=status.HTTP_200_OK,
@@ -1088,7 +1084,7 @@ class FindingViewSet(
10881084
def perform_update(self, serializer):
10891085
# IF JIRA is enabled and this product has a JIRA configuration
10901086
push_to_jira = serializer.validated_data.get("push_to_jira")
1091-
jira_project = jira_helper.get_jira_project(serializer.instance)
1087+
jira_project = jira_services.get_project(serializer.instance)
10921088
if get_system_setting("enable_jira") and jira_project:
10931089
push_to_jira = push_to_jira or jira_project.push_all_issues
10941090

@@ -1361,9 +1357,9 @@ def notes(self, request, pk=None):
13611357
)
13621358

13631359
if finding.has_jira_issue:
1364-
jira_helper.add_comment(finding, note)
1360+
jira_services.add_comment(finding, note)
13651361
elif finding.has_jira_group_issue:
1366-
jira_helper.add_comment(finding.finding_group, note)
1362+
jira_services.add_comment(finding.finding_group, note)
13671363

13681364
serialized_note = serializers.NoteSerializer(
13691365
{"author": author, "entry": entry, "private": private},
@@ -1769,74 +1765,11 @@ def metadata(self, request, pk=None):
17691765

17701766

17711767
# Authorization: configuration
1772-
class JiraInstanceViewSet(
1773-
DojoModelViewSet,
1774-
):
1775-
serializer_class = serializers.JIRAInstanceSerializer
1776-
queryset = JIRA_Instance.objects.none()
1777-
filter_backends = (DjangoFilterBackend,)
1778-
filterset_fields = ["id", "url"]
1779-
permission_classes = (permissions.UserHasConfigurationPermissionSuperuser,)
1780-
1781-
def get_queryset(self):
1782-
return JIRA_Instance.objects.all().order_by("id")
1783-
1784-
1785-
# Authorization: object-based
1786-
# @extend_schema_view(**schema_with_prefetch())
1787-
# Nested models with prefetch make the response schema too long for Swagger UI
1788-
class JiraIssuesViewSet(
1789-
PrefetchDojoModelViewSet,
1790-
):
1791-
serializer_class = serializers.JIRAIssueSerializer
1792-
queryset = JIRA_Issue.objects.none()
1793-
filter_backends = (DjangoFilterBackend,)
1794-
filterset_fields = [
1795-
"id",
1796-
"jira_id",
1797-
"jira_key",
1798-
"finding",
1799-
"engagement",
1800-
"finding_group",
1801-
]
1802-
1803-
permission_classes = (
1804-
IsAuthenticated,
1805-
permissions.UserHasJiraIssuePermission,
1806-
)
1807-
1808-
def get_queryset(self):
1809-
return get_authorized_jira_issues(Permissions.Product_View)
1810-
1811-
1812-
# Authorization: object-based
1813-
@extend_schema_view(**schema_with_prefetch())
1814-
class JiraProjectViewSet(
1815-
PrefetchDojoModelViewSet,
1816-
):
1817-
serializer_class = serializers.JIRAProjectSerializer
1818-
queryset = JIRA_Project.objects.none()
1819-
filter_backends = (DjangoFilterBackend,)
1820-
filterset_fields = [
1821-
"id",
1822-
"jira_instance",
1823-
"product",
1824-
"engagement",
1825-
"enabled",
1826-
"component",
1827-
"project_key",
1828-
"push_all_issues",
1829-
"enable_engagement_epic_mapping",
1830-
"push_notes",
1831-
]
1832-
1833-
permission_classes = (
1834-
IsAuthenticated,
1835-
permissions.UserHasJiraProductPermission,
1836-
)
1837-
1838-
def get_queryset(self):
1839-
return get_authorized_jira_projects(Permissions.Product_View)
1768+
from dojo.jira.api.views import ( # noqa: E402, F401 backward compat
1769+
JiraInstanceViewSet,
1770+
JiraIssuesViewSet,
1771+
JiraProjectViewSet,
1772+
)
18401773

18411774

18421775
# Authorization: superuser
@@ -2871,7 +2804,7 @@ def perform_create(self, serializer):
28712804
push_to_jira = serializer.validated_data.get("push_to_jira")
28722805
if get_system_setting("enable_jira"):
28732806
jira_driver = engagement or (product or None)
2874-
if jira_project := (jira_helper.get_jira_project(jira_driver) if jira_driver else None):
2807+
if jira_project := (jira_services.get_project(jira_driver) if jira_driver else None):
28752808
push_to_jira = push_to_jira or jira_project.push_all_issues
28762809

28772810
# Add pghistory context for audit trail (adds to existing middleware context).
@@ -3029,7 +2962,7 @@ def perform_create(self, serializer):
30292962
push_to_jira = serializer.validated_data.get("push_to_jira")
30302963
if get_system_setting("enable_jira"):
30312964
jira_driver = test or (engagement or (product or None))
3032-
if jira_project := (jira_helper.get_jira_project(jira_driver) if jira_driver else None):
2965+
if jira_project := (jira_services.get_project(jira_driver) if jira_driver else None):
30332966
push_to_jira = push_to_jira or jira_project.push_all_issues
30342967
logger.debug("push_to_jira: %s", push_to_jira)
30352968
# Add pghistory context for audit trail (adds to existing middleware context)

dojo/engagement/services.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from django.db.models.signals import pre_save
55
from django.dispatch import receiver
66

7-
import dojo.jira_link.helper as jira_helper
87
from dojo.celery_dispatch import dojo_dispatch_task
8+
from dojo.jira import services as jira_services
99
from dojo.models import Engagement
1010

1111
logger = logging.getLogger(__name__)
@@ -16,8 +16,10 @@ def close_engagement(eng):
1616
eng.status = "Completed"
1717
eng.save()
1818

19-
if jira_helper.get_jira_project(eng):
20-
dojo_dispatch_task(jira_helper.close_epic, eng.id, push_to_jira=True)
19+
if jira_services.get_project(eng):
20+
task = jira_services.get_epic_task("close_epic")
21+
if task:
22+
dojo_dispatch_task(task, eng.id, push_to_jira=True)
2123

2224

2325
def reopen_engagement(eng):

0 commit comments

Comments
 (0)