diff --git a/component_catalog/templates/component_catalog/includes/scan_package_modal.html b/component_catalog/templates/component_catalog/includes/scan_package_modal.html index c1b8ad6b..ffd9a6e4 100644 --- a/component_catalog/templates/component_catalog/includes/scan_package_modal.html +++ b/component_catalog/templates/component_catalog/includes/scan_package_modal.html @@ -44,9 +44,12 @@ Submit Scan Request {% else %} - - Submit Scan Request - +
+ {% csrf_token %} + +
{% endif %} diff --git a/component_catalog/templates/component_catalog/modals/scan_delete_modal.html b/component_catalog/templates/component_catalog/modals/scan_delete_modal.html index cb034ef3..16701e41 100644 --- a/component_catalog/templates/component_catalog/modals/scan_delete_modal.html +++ b/component_catalog/templates/component_catalog/modals/scan_delete_modal.html @@ -15,7 +15,10 @@ diff --git a/component_catalog/templates/component_catalog/modals/scan_refresh_modal.html b/component_catalog/templates/component_catalog/modals/scan_refresh_modal.html index a60f3ad3..1b1ea361 100644 --- a/component_catalog/templates/component_catalog/modals/scan_refresh_modal.html +++ b/component_catalog/templates/component_catalog/modals/scan_refresh_modal.html @@ -15,7 +15,10 @@ diff --git a/component_catalog/templates/component_catalog/scan_list.html b/component_catalog/templates/component_catalog/scan_list.html index 98be39da..75c093e5 100644 --- a/component_catalog/templates/component_catalog/scan_list.html +++ b/component_catalog/templates/component_catalog/scan_list.html @@ -39,14 +39,18 @@ document.addEventListener('DOMContentLoaded', function () { document.querySelectorAll('.scan_delete_link').forEach(link => { link.addEventListener('click', function() { - let deleteUrl = this.getAttribute('data-delete-url'); - document.querySelector('#scan-delete-modal a.delete-confirm').pathname = deleteUrl; + let deleteUrl = new URL(this.getAttribute('data-delete-url'), window.location.origin); + if (deleteUrl.origin === window.location.origin) { + document.querySelector('#scan-delete-modal form.delete-confirm').action = deleteUrl.pathname; + } }); }); document.querySelectorAll('.scan_refresh_link').forEach(link => { link.addEventListener('click', function() { - let refreshUrl = this.getAttribute('data-refresh-url'); - document.querySelector('#scan-refresh-modal a.refresh-confirm').pathname = refreshUrl; + let refreshUrl = new URL(this.getAttribute('data-refresh-url'), window.location.origin); + if (refreshUrl.origin === window.location.origin) { + document.querySelector('#scan-refresh-modal form.refresh-confirm').action = refreshUrl.pathname; + } }); }); }); diff --git a/component_catalog/templates/component_catalog/tabs/tab_scan.html b/component_catalog/templates/component_catalog/tabs/tab_scan.html index b1e07a13..7e44c083 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_scan.html +++ b/component_catalog/templates/component_catalog/tabs/tab_scan.html @@ -13,7 +13,7 @@ {% include 'component_catalog/includes/scan_package_modal.html' %} @@ -33,14 +33,18 @@ diff --git a/component_catalog/tests/test_views.py b/component_catalog/tests/test_views.py index c3feac17..2d7de4f0 100644 --- a/component_catalog/tests/test_views.py +++ b/component_catalog/tests/test_views.py @@ -2074,6 +2074,14 @@ def test_package_details_view_scan_package_button(self): response = self.client.get(self.package1_tab_scan_url) self.assertContains(response, expected1) self.assertContains(response, expected2) + scan_url = reverse( + "component_catalog:package_scan", args=[self.dataspace.name, self.package1.uuid] + ) + expected_form = ( + rf'
' + r'\s*' + ) + self.assertRegex(response.content.decode(), expected_form) alternate = Dataspace.objects.create(name="Alternate") alternate_user = create_superuser("alternate_user", alternate) @@ -2990,30 +2998,111 @@ def test_delete_scan_view(self, mock_fetch_scan_list, mock_delete_scan): project_uuid = "348df847-f48f-4ac7-b864-5785b44c65e2" delete_url = reverse("component_catalog:scan_delete", args=[project_uuid]) - response = self.client.get(delete_url) + response = self.client.post(delete_url) self.assertRedirects(response, f"/login/?next={delete_url}") self.client.login(username=self.super_user.username, password="secret") response = self.client.get(delete_url) + self.assertEqual(405, response.status_code) + mock_delete_scan.assert_not_called() + + response = self.client.post(delete_url) self.assertEqual(404, response.status_code) self.dataspace.enable_package_scanning = True self.dataspace.save() mock_fetch_scan_list.return_value = None - response = self.client.get(delete_url) + response = self.client.post(delete_url) self.assertEqual(404, response.status_code) mock_fetch_scan_list.return_value = {"count": 1} mock_delete_scan.return_value = True - response = self.client.get(delete_url, follow=True) + response = self.client.post(delete_url, follow=True) scan_list_url = reverse("component_catalog:scan_list") self.assertRedirects(response, scan_list_url) self.assertContains(response, "Scan deleted.") mock_delete_scan.return_value = False - response = self.client.get(delete_url) + response = self.client.post(delete_url) + self.assertEqual(404, response.status_code) + + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.refresh_scan") + def test_refresh_scan_view(self, mock_refresh_scan): + project_uuid = "348df847-f48f-4ac7-b864-5785b44c65e2" + refresh_url = reverse("component_catalog:scan_refresh", args=[project_uuid]) + + response = self.client.post(refresh_url) + self.assertRedirects(response, f"/login/?next={refresh_url}") + + self.client.login(username=self.super_user.username, password="secret") + response = self.client.get(refresh_url) + self.assertEqual(405, response.status_code) + mock_refresh_scan.assert_not_called() + + response = self.client.post(refresh_url) + self.assertEqual(404, response.status_code) + + self.dataspace.enable_package_scanning = True + self.dataspace.save() + mock_refresh_scan.return_value = mock.Mock(status_code=200) + response = self.client.post(refresh_url, follow=True) + self.assertRedirects(response, reverse("component_catalog:scan_list")) + self.assertContains(response, "Refresh Scan started.") + mock_refresh_scan.assert_called_once_with(project_uuid) + + mock_refresh_scan.return_value = mock.Mock(status_code=400) + response = self.client.post(refresh_url) + self.assertEqual(404, response.status_code) + + @mock.patch("component_catalog.views.tasks.scancodeio_submit_scan") + @mock.patch("component_catalog.views.is_available") + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_configured") + def test_package_scan_view(self, mock_is_configured, mock_is_available, mock_submit_scan): + mock_is_configured.return_value = True + mock_is_available.return_value = True + self.dataspace.enable_package_scanning = True + self.dataspace.save() + self.package1.download_url = "https://download.url/package.zip" + self.package1.save() + scan_url = reverse( + "component_catalog:package_scan", args=[self.dataspace.name, self.package1.uuid] + ) + + response = self.client.post(scan_url) + self.assertRedirects(response, f"/login/?next={scan_url}") + + self.client.login(username=self.super_user.username, password="secret") + response = self.client.get(scan_url) + self.assertEqual(405, response.status_code) + mock_submit_scan.assert_not_called() + + response = self.client.post(scan_url, follow=True) + self.assertRedirects(response, f"{self.package1.details_url}#scan") + self.assertContains(response, "The Package URL was submitted to ScanCode.io for scanning.") + mock_submit_scan.assert_called_once_with( + uris=self.package1.download_url, + user_uuid=self.super_user.uuid, + dataspace_uuid=self.dataspace.uuid, + ) + + mock_is_configured.return_value = False + response = self.client.post(scan_url, headers={"HX-Request": "true"}) self.assertEqual(404, response.status_code) + def test_scan_modals_post_with_csrf_token(self): + self.client.login(username=self.super_user.username, password="secret") + self.dataspace.enable_package_scanning = True + self.dataspace.save() + + with mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_scan_list") as fetch: + fetch.return_value = None + response = self.client.get(reverse("component_catalog:scan_list")) + + content = response.content.decode() + csrf_input = r'\s*' + csrf_input) + self.assertRegex(content, r'' + csrf_input) + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_scan_data") @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_available") def test_send_scan_data_as_file_view(self, mock_is_available, mock_fetch_scan_data): diff --git a/component_catalog/views.py b/component_catalog/views.py index 77d268d8..3f2eb9a5 100644 --- a/component_catalog/views.py +++ b/component_catalog/views.py @@ -1432,6 +1432,7 @@ def post(self, request, *args, **kwargs): @login_required +@require_POST def package_scan_view(request, dataspace, uuid): user = request.user dataspace = user.dataspace @@ -1477,7 +1478,7 @@ def package_scan_view(request, dataspace, uuid): messages.error(request, scancode_msg) if is_hxr: - return Http404 + raise Http404 return redirect(f"{package.details_url}#scan") @@ -1672,6 +1673,7 @@ def send_scan_data_as_file_view(request, project_uuid, filename): @login_required +@require_POST def delete_scan_view(request, project_uuid): dataspace = request.user.dataspace if not dataspace.enable_package_scanning: @@ -1696,6 +1698,7 @@ def delete_scan_view(request, project_uuid): @login_required +@require_POST def refresh_scan_view(request, project_uuid): user = request.user dataspace = user.dataspace diff --git a/dejacode/urls.py b/dejacode/urls.py index eb656555..e2f28527 100644 --- a/dejacode/urls.py +++ b/dejacode/urls.py @@ -16,7 +16,6 @@ from django.views.generic import RedirectView from django.views.generic import TemplateView -from notifications.views import mark_all_as_read from rest_framework.routers import DefaultRouter from component_catalog.api import ComponentViewSet @@ -42,6 +41,7 @@ from dje.views import api_docs_view from dje.views import home_view from dje.views import index_dispatch +from dje.views import mark_all_notifications_as_read_view from dje.views import urn_resolve_view from license_library.api import LicenseAnnotationViewSet from license_library.api import LicenseViewSet @@ -160,7 +160,7 @@ notification_patterns = [ path("", UnreadNotificationsList.as_view(), name="unread"), path("all/", AllNotificationsList.as_view(), name="all"), - path("mark_all_as_read/", mark_all_as_read, name="mark_all_as_read"), + path("mark_all_as_read/", mark_all_notifications_as_read_view, name="mark_all_as_read"), path( "send_scan_notification//", send_scan_notification, name="send_scan_notification" ), diff --git a/dje/admin.py b/dje/admin.py index 200d4944..10b884d6 100644 --- a/dje/admin.py +++ b/dje/admin.py @@ -53,6 +53,7 @@ from django.utils.html import mark_safe from django.utils.http import urlencode from django.utils.translation import gettext as _ +from django.views.decorators.http import require_POST from django.views.generic import RedirectView from django_registration.backends.activation.views import RegistrationView @@ -1697,7 +1698,7 @@ def get_urls(self): ), path( "/send_activation_email/", - self.admin_site.admin_view(self.send_activation_email), + self.admin_site.admin_view(require_POST(self.send_activation_email)), name="{}_{}_send_activation_email".format(*info), ), ] diff --git a/dje/templates/admin/dje/dataspace/change_form.html b/dje/templates/admin/dje/dataspace/change_form.html index 6cf89e18..f4a88bd7 100644 --- a/dje/templates/admin/dje/dataspace/change_form.html +++ b/dje/templates/admin/dje/dataspace/change_form.html @@ -15,7 +15,8 @@ {% endif %} {% if change and template_dataspace and user.is_superuser and user.dataspace.is_reference %}
  • - {% trans 'Clone dataset from' %} {{ template_dataspace }} + {% csrf_token %}
  • + {% trans 'Clone dataset from' %} {{ template_dataspace }} {% endif %} {% endblock %} \ No newline at end of file diff --git a/dje/templates/admin/dje/dejacode_user/change_form.html b/dje/templates/admin/dje/dejacode_user/change_form.html index 5df50830..7f9eea00 100644 --- a/dje/templates/admin/dje/dejacode_user/change_form.html +++ b/dje/templates/admin/dje/dejacode_user/change_form.html @@ -6,7 +6,8 @@ {{ block.super }} {% if change %}
  • - {% trans 'Re-send activation email' %} + + {% trans 'Re-send activation email' %}
  • {% endif %} {% endblock %} diff --git a/dje/templates/notifications/list.html b/dje/templates/notifications/list.html index 886e8d09..2f7ac197 100644 --- a/dje/templates/notifications/list.html +++ b/dje/templates/notifications/list.html @@ -18,7 +18,10 @@

    - Mark all as read +
    + {% csrf_token %} + +
    diff --git a/dje/tests/test_user.py b/dje/tests/test_user.py index dfd51148..1d4e0011 100644 --- a/dje/tests/test_user.py +++ b/dje/tests/test_user.py @@ -375,7 +375,19 @@ def test_user_admin_send_activation_email_view(self): self.assertTrue(self.other_user.is_active) url = reverse("admin:dje_dejacodeuser_send_activation_email", args=[self.other_user.id]) - response = self.client.get(url, follow=True) + response = self.client.get(url) + self.assertEqual(405, response.status_code) + self.other_user.refresh_from_db() + self.assertTrue(self.other_user.is_active) + self.assertEqual(len(mail.outbox), 0) + + change_url = reverse("admin:dje_dejacodeuser_change", args=[self.other_user.id]) + response = self.client.get(change_url) + self.assertContains( + response, f'