Skip to content

Commit 3ab599e

Browse files
authored
Merge branch 'bugfix' into jira-operation-feedback
2 parents d15fa7d + 0770034 commit 3ab599e

7 files changed

Lines changed: 299 additions & 85 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
title: 'Upgrading to DefectDojo Version 2.56.x'
3+
toc_hide: true
4+
weight: -20260215
5+
description: Deprecation of Questionnaire API Endpoints
6+
---
7+
8+
## Deprecation: Questionnaire API Endpoints
9+
10+
The following Questionnaire API endpoints are being deprecated and will be removed in DefectDojo 2.59.0 on June 1st, 2026:
11+
12+
- `/api/v2/questionnaire_answered_questionnaires/`
13+
- `/api/v2/questionnaire_answers/`
14+
- `/api/v2/questionnaire_engagement_questionnaires/`
15+
- `/api/v2/questionnaire_general_questionnaires/`
16+
- `/api/v2/questionnaire_questions`
17+
18+
### Required Actions
19+
20+
Support for these endpoints will be fully removed in DefectDojo 2.59.0 (scheduled for June 1st, 2026). After this date, any requests to these endpoints will return a 404 Not Found error.
21+
22+
### Timeline
23+
24+
- **DefectDojo 2.56.x onwards**: Endpoints are deprecated with deprecation headers
25+
- **DefectDojo 2.59.0 (June 1st, 2026)**: Endpoints will be removed entirely
26+
27+
For more information, check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.56.0).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
title: 'Upgrading to DefectDojo Version 2.59.x'
3+
toc_hide: true
4+
weight: -20260602
5+
description: Removal of Questionnaire API Endpoints
6+
---
7+
8+
## Removal: Questionnaire API Endpoints
9+
10+
As announced in DefectDojo 2.56.0, the following Questionnaire API endpoints have been removed:
11+
12+
- `/api/v2/questionnaire_answered_questionnaires/`
13+
- `/api/v2/questionnaire_answers/`
14+
- `/api/v2/questionnaire_engagement_questionnaires/`
15+
- `/api/v2/questionnaire_general_questionnaires/`
16+
- `/api/v2/questionnaire_questions`
17+
18+
### Required Actions
19+
20+
Any requests to these endpoints will now return a 404 Not Found error.
21+
22+
For more information, check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.59.0).

dojo/api_v2/permissions.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,15 @@ class UserHasEngagementRelatedObjectPermission(BaseRelatedObjectPermission):
385385
}
386386

387387

388+
class UserHasEngagementNotePermission(BaseRelatedObjectPermission):
389+
permission_map = {
390+
"get_permission": Permissions.Engagement_View,
391+
"put_permission": Permissions.Engagement_Edit,
392+
"delete_permission": Permissions.Engagement_Edit,
393+
"post_permission": Permissions.Engagement_View,
394+
}
395+
396+
388397
class UserHasRiskAcceptancePermission(permissions.BasePermission):
389398
def has_permission(self, request, view):
390399
# The previous implementation only checked for the object permission if the path was
@@ -437,6 +446,15 @@ class UserHasFindingRelatedObjectPermission(BaseRelatedObjectPermission):
437446
}
438447

439448

449+
class UserHasFindingNotePermission(BaseRelatedObjectPermission):
450+
permission_map = {
451+
"get_permission": Permissions.Finding_View,
452+
"put_permission": Permissions.Finding_Edit,
453+
"delete_permission": Permissions.Finding_Edit,
454+
"post_permission": Permissions.Finding_View,
455+
}
456+
457+
440458
class UserHasImportPermission(permissions.BasePermission):
441459
def has_permission(self, request, view):
442460
# permission check takes place before validation, so we don't have access to serializer.validated_data()
@@ -817,6 +835,15 @@ class UserHasTestRelatedObjectPermission(BaseRelatedObjectPermission):
817835
}
818836

819837

838+
class UserHasTestNotePermission(BaseRelatedObjectPermission):
839+
permission_map = {
840+
"get_permission": Permissions.Test_View,
841+
"put_permission": Permissions.Test_Edit,
842+
"delete_permission": Permissions.Test_Edit,
843+
"post_permission": Permissions.Test_View,
844+
}
845+
846+
820847
class UserHasTestImportPermission(permissions.BasePermission):
821848
def has_permission(self, request, view):
822849
return check_post_permission(

dojo/api_v2/views.py

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
Languages,
122122
Network_Locations,
123123
Note_Type,
124+
NoteHistory,
124125
Notes,
125126
Notification_Webhooks,
126127
Notifications,
@@ -242,6 +243,19 @@ class PrefetchDojoModelViewSet(
242243
pass
243244

244245

246+
class DeprecationNoticeMixin:
247+
248+
deprecated: bool | None = None
249+
end_of_life_date: datetime | None = None
250+
251+
def finalize_response(self, request, response, *args, **kwargs):
252+
if self.deprecated is not None:
253+
response["X-Deprecated"] = self.deprecated
254+
if self.end_of_life_date is not None:
255+
response["X-End-Of-Life-Date"] = self.end_of_life_date.isoformat()
256+
return super().finalize_response(request, response, *args, **kwargs)
257+
258+
245259
# Authorization: authenticated users
246260
class RoleViewSet(viewsets.ReadOnlyModelViewSet):
247261
serializer_class = serializers.RoleSerializer
@@ -504,7 +518,7 @@ def generate_report(self, request, pk=None):
504518
request=serializers.AddNewNoteOptionSerializer,
505519
responses={status.HTTP_201_CREATED: serializers.NoteSerializer},
506520
)
507-
@action(detail=True, methods=["get", "post"], permission_classes=[IsAuthenticated, permissions.UserHasEngagementRelatedObjectPermission])
521+
@action(detail=True, methods=["get", "post"], permission_classes=[IsAuthenticated, permissions.UserHasEngagementNotePermission])
508522
def notes(self, request, pk=None):
509523
engagement = self.get_object()
510524
if request.method == "POST":
@@ -532,6 +546,10 @@ def notes(self, request, pk=None):
532546
note_type=note_type,
533547
)
534548
note.save()
549+
# Add an entry to the note history
550+
history = NoteHistory.objects.create(data=note.entry, time=note.date, current_editor=note.author)
551+
note.history.add(history)
552+
# Now add the note to the object
535553
engagement.notes.add(note)
536554
# Determine if we need to send any notifications for user mentioned
537555
process_tag_notifications(
@@ -1096,7 +1114,7 @@ def request_response(self, request, pk=None):
10961114
request=serializers.AddNewNoteOptionSerializer,
10971115
responses={status.HTTP_201_CREATED: serializers.NoteSerializer},
10981116
)
1099-
@action(detail=True, methods=["get", "post"], permission_classes=(IsAuthenticated, permissions.UserHasFindingRelatedObjectPermission))
1117+
@action(detail=True, methods=["get", "post"], permission_classes=(IsAuthenticated, permissions.UserHasFindingNotePermission))
11001118
def notes(self, request, pk=None):
11011119
finding = self.get_object()
11021120
if request.method == "POST":
@@ -1125,6 +1143,10 @@ def notes(self, request, pk=None):
11251143
note_type=note_type,
11261144
)
11271145
note.save()
1146+
# Add an entry to the note history
1147+
history = NoteHistory.objects.create(data=note.entry, time=note.date, current_editor=note.author)
1148+
note.history.add(history)
1149+
# Now add the note to the object
11281150
finding.last_reviewed = note.date
11291151
finding.last_reviewed_by = author
11301152
finding.save(update_fields=["last_reviewed", "last_reviewed_by", "updated"])
@@ -1226,7 +1248,7 @@ def download_file(self, request, file_id, pk=None):
12261248
request=serializers.FindingNoteSerializer,
12271249
responses={status.HTTP_204_NO_CONTENT: ""},
12281250
)
1229-
@action(detail=True, methods=["patch"], permission_classes=(IsAuthenticated, permissions.UserHasFindingRelatedObjectPermission))
1251+
@action(detail=True, methods=["patch"], permission_classes=(IsAuthenticated, permissions.UserHasFindingNotePermission))
12301252
def remove_note(self, request, pk=None):
12311253
"""Remove Note From Finding Note"""
12321254
finding = self.get_object()
@@ -2162,7 +2184,7 @@ def generate_report(self, request, pk=None):
21622184
request=serializers.AddNewNoteOptionSerializer,
21632185
responses={status.HTTP_201_CREATED: serializers.NoteSerializer},
21642186
)
2165-
@action(detail=True, methods=["get", "post"], permission_classes=(IsAuthenticated, permissions.UserHasTestRelatedObjectPermission))
2187+
@action(detail=True, methods=["get", "post"], permission_classes=(IsAuthenticated, permissions.UserHasTestNotePermission))
21662188
def notes(self, request, pk=None):
21672189
test = self.get_object()
21682190
if request.method == "POST":
@@ -2190,6 +2212,10 @@ def notes(self, request, pk=None):
21902212
note_type=note_type,
21912213
)
21922214
note.save()
2215+
# Add an entry to the note history
2216+
history = NoteHistory.objects.create(data=note.entry, time=note.date, current_editor=note.author)
2217+
note.history.add(history)
2218+
# Now add the note to the object
21932219
test.notes.add(note)
21942220
# Determine if we need to send any notifications for user mentioned
21952221
process_tag_notifications(
@@ -3173,7 +3199,10 @@ def get_queryset(self):
31733199
class QuestionnaireQuestionViewSet(
31743200
viewsets.ReadOnlyModelViewSet,
31753201
dojo_mixins.QuestionSubClassFieldsMixin,
3202+
DeprecationNoticeMixin,
31763203
):
3204+
deprecated = True
3205+
end_of_life_date = datetime(2026, 6, 1)
31773206
serializer_class = serializers.QuestionnaireQuestionSerializer
31783207
queryset = Question.objects.none()
31793208
filter_backends = (DjangoFilterBackend,)
@@ -3185,11 +3214,28 @@ class QuestionnaireQuestionViewSet(
31853214
def get_queryset(self):
31863215
return Question.objects.all().order_by("id")
31873216

3217+
@extend_schema(
3218+
deprecated=True,
3219+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3220+
)
3221+
def list(self, request, *args, **kwargs):
3222+
return super().list(request, *args, **kwargs)
3223+
3224+
@extend_schema(
3225+
deprecated=True,
3226+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3227+
)
3228+
def retrieve(self, request, *args, **kwargs):
3229+
return super().retrieve(request, *args, **kwargs)
3230+
31883231

31893232
class QuestionnaireAnswerViewSet(
31903233
viewsets.ReadOnlyModelViewSet,
31913234
dojo_mixins.AnswerSubClassFieldsMixin,
3235+
DeprecationNoticeMixin,
31923236
):
3237+
deprecated = True
3238+
end_of_life_date = datetime(2026, 6, 1)
31933239
serializer_class = serializers.QuestionnaireAnswerSerializer
31943240
queryset = Answer.objects.none()
31953241
filter_backends = (DjangoFilterBackend,)
@@ -3201,10 +3247,27 @@ class QuestionnaireAnswerViewSet(
32013247
def get_queryset(self):
32023248
return Answer.objects.all().order_by("id")
32033249

3250+
@extend_schema(
3251+
deprecated=True,
3252+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3253+
)
3254+
def list(self, request, *args, **kwargs):
3255+
return super().list(request, *args, **kwargs)
3256+
3257+
@extend_schema(
3258+
deprecated=True,
3259+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3260+
)
3261+
def retrieve(self, request, *args, **kwargs):
3262+
return super().retrieve(request, *args, **kwargs)
3263+
32043264

32053265
class QuestionnaireGeneralSurveyViewSet(
32063266
viewsets.ReadOnlyModelViewSet,
3267+
DeprecationNoticeMixin,
32073268
):
3269+
deprecated = True
3270+
end_of_life_date = datetime(2026, 6, 1)
32083271
serializer_class = serializers.QuestionnaireGeneralSurveySerializer
32093272
queryset = General_Survey.objects.none()
32103273
filter_backends = (DjangoFilterBackend,)
@@ -3216,10 +3279,27 @@ class QuestionnaireGeneralSurveyViewSet(
32163279
def get_queryset(self):
32173280
return General_Survey.objects.all().order_by("id")
32183281

3282+
@extend_schema(
3283+
deprecated=True,
3284+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3285+
)
3286+
def list(self, request, *args, **kwargs):
3287+
return super().list(request, *args, **kwargs)
3288+
3289+
@extend_schema(
3290+
deprecated=True,
3291+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3292+
)
3293+
def retrieve(self, request, *args, **kwargs):
3294+
return super().retrieve(request, *args, **kwargs)
3295+
32193296

32203297
class QuestionnaireEngagementSurveyViewSet(
32213298
viewsets.ReadOnlyModelViewSet,
3299+
DeprecationNoticeMixin,
32223300
):
3301+
deprecated = True
3302+
end_of_life_date = datetime(2026, 6, 1)
32233303
serializer_class = serializers.QuestionnaireEngagementSurveySerializer
32243304
queryset = Engagement_Survey.objects.none()
32253305
filter_backends = (DjangoFilterBackend,)
@@ -3232,13 +3312,29 @@ def get_queryset(self):
32323312
return Engagement_Survey.objects.all().order_by("id")
32333313

32343314
@extend_schema(
3235-
request=OpenApiTypes.NONE,
3236-
parameters=[
3237-
OpenApiParameter(
3238-
"engagement_id", OpenApiTypes.INT, OpenApiParameter.PATH,
3239-
),
3240-
],
3241-
responses={status.HTTP_200_OK: serializers.QuestionnaireAnsweredSurveySerializer},
3315+
deprecated=True,
3316+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3317+
)
3318+
def list(self, request, *args, **kwargs):
3319+
return super().list(request, *args, **kwargs)
3320+
3321+
@extend_schema(
3322+
deprecated=True,
3323+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3324+
)
3325+
def retrieve(self, request, *args, **kwargs):
3326+
return super().retrieve(request, *args, **kwargs)
3327+
3328+
@extend_schema(
3329+
deprecated=True,
3330+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3331+
request=OpenApiTypes.NONE,
3332+
parameters=[
3333+
OpenApiParameter(
3334+
"engagement_id", OpenApiTypes.INT, OpenApiParameter.PATH,
3335+
),
3336+
],
3337+
responses={status.HTTP_200_OK: serializers.QuestionnaireAnsweredSurveySerializer},
32423338
)
32433339
@action(
32443340
detail=True, methods=["post"], url_path=r"link_engagement/(?P<engagement_id>\d+)",
@@ -3260,7 +3356,10 @@ class QuestionnaireAnsweredSurveyViewSet(
32603356
prefetch.PrefetchListMixin,
32613357
prefetch.PrefetchRetrieveMixin,
32623358
viewsets.ReadOnlyModelViewSet,
3359+
DeprecationNoticeMixin,
32633360
):
3361+
deprecated = True
3362+
end_of_life_date = datetime(2026, 6, 1)
32643363
serializer_class = serializers.QuestionnaireAnsweredSurveySerializer
32653364
queryset = Answered_Survey.objects.none()
32663365
filter_backends = (DjangoFilterBackend,)
@@ -3272,6 +3371,20 @@ class QuestionnaireAnsweredSurveyViewSet(
32723371
def get_queryset(self):
32733372
return Answered_Survey.objects.all().order_by("id")
32743373

3374+
@extend_schema(
3375+
deprecated=True,
3376+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3377+
)
3378+
def list(self, request, *args, **kwargs):
3379+
return super().list(request, *args, **kwargs)
3380+
3381+
@extend_schema(
3382+
deprecated=True,
3383+
description="This endpoint is deprecated and will be removed on 2026-06-01.",
3384+
)
3385+
def retrieve(self, request, *args, **kwargs):
3386+
return super().retrieve(request, *args, **kwargs)
3387+
32753388

32763389
# Authorization: configuration
32773390
class AnnouncementViewSet(

0 commit comments

Comments
 (0)