From 1b8e58242cb4d2434756fdbbacfb3a0e0b2b2982 Mon Sep 17 00:00:00 2001 From: Brendan Smith Date: Mon, 12 Feb 2024 12:02:23 +0000 Subject: [PATCH] Add party document streaming endpoint --- api/applications/permissions.py | 8 ++ api/applications/tests/test_permissions.py | 38 ++++++ api/applications/urls.py | 5 + api/applications/views/party_documents.py | 17 +++ .../views/tests/test_party_documents.py | 114 ++++++++++++++++++ api/parties/tests/factories.py | 7 +- 6 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 api/applications/permissions.py create mode 100644 api/applications/tests/test_permissions.py create mode 100644 api/applications/views/tests/test_party_documents.py diff --git a/api/applications/permissions.py b/api/applications/permissions.py new file mode 100644 index 0000000000..58da56a7fa --- /dev/null +++ b/api/applications/permissions.py @@ -0,0 +1,8 @@ +from rest_framework import permissions + +from api.organisations.libraries.get_organisation import get_request_user_organisation_id + + +class IsPartyDocumentInOrganisation(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return obj.party.organisation_id == get_request_user_organisation_id(request) diff --git a/api/applications/tests/test_permissions.py b/api/applications/tests/test_permissions.py new file mode 100644 index 0000000000..6c9f4e1ac3 --- /dev/null +++ b/api/applications/tests/test_permissions.py @@ -0,0 +1,38 @@ +from django.contrib.auth.models import User +from django.test import RequestFactory, TestCase +from rest_framework.permissions import IsAdminUser + +from test_helpers.clients import DataTestClient +from api.core.authentication import ORGANISATION_ID +from api.parties.tests.factories import PartyDocumentFactory, PartyFactory +from api.organisations.tests.factories import OrganisationFactory +from api.applications.permissions import IsPartyDocumentInOrganisation + + +class IsPartyDocumentInOrganisationTest(DataTestClient): + def test_permissions_success(self): + organisation = OrganisationFactory() + party = PartyFactory(organisation=organisation) + party_document = PartyDocumentFactory(party=party, s3_key="somekey") + factory = RequestFactory() + + request = factory.get("/") + request.META[ORGANISATION_ID] = organisation.id + + authorised = IsPartyDocumentInOrganisation().has_object_permission(request, None, party_document) + + self.assertTrue(authorised) + + def test_permissions_failure(self): + organisation = OrganisationFactory() + other_organisation = OrganisationFactory() + party = PartyFactory(organisation=organisation) + party_document = PartyDocumentFactory(party=party, s3_key="somekey") + factory = RequestFactory() + + request = factory.get("/") + request.META[ORGANISATION_ID] = other_organisation.id + + authorised = IsPartyDocumentInOrganisation().has_object_permission(request, None, party_document) + + self.assertFalse(authorised) diff --git a/api/applications/urls.py b/api/applications/urls.py index fce70bbc1f..9e83bd8823 100644 --- a/api/applications/urls.py +++ b/api/applications/urls.py @@ -108,6 +108,11 @@ party_documents.PartyDocumentView.as_view(), name="party_document_view", ), + path( + "/parties//document//stream/", + party_documents.PartyDocumentStream.as_view(), + name="party_document_stream", + ), # Sites, locations and countries path("/sites/", sites.ApplicationSites.as_view(), name="application_sites"), path("/contract-types/", countries.ApplicationContractTypes.as_view(), name="contract_types"), diff --git a/api/applications/views/party_documents.py b/api/applications/views/party_documents.py index 647585adc2..0232ea7d8d 100644 --- a/api/applications/views/party_documents.py +++ b/api/applications/views/party_documents.py @@ -7,7 +7,9 @@ from api.audit_trail.enums import AuditType from api.applications.libraries.get_applications import get_application from api.applications.libraries.document_helpers import upload_party_document, delete_party_document, get_party_document +from api.applications.permissions import IsPartyDocumentInOrganisation from api.core.authentication import ExporterAuthentication +from api.core.views import DocumentStreamAPIView from api.core.decorators import authorised_to_view_application from api.parties.models import PartyDocument from api.users.models import ExporterUser @@ -64,3 +66,18 @@ def delete(self, request, **kwargs): ) return HttpResponse(status=status.HTTP_204_NO_CONTENT) + + +class PartyDocumentStream(DocumentStreamAPIView): + authentication_classes = (ExporterAuthentication,) + lookup_url_kwarg = "document_pk" + permission_classes = (IsPartyDocumentInOrganisation,) + + def get_queryset(self): + return PartyDocument.objects.filter( + party_id=self.kwargs["party_pk"], + party__parties_on_application__application__id=self.kwargs["pk"], + ) + + def get_document(self, instance): + return instance diff --git a/api/applications/views/tests/test_party_documents.py b/api/applications/views/tests/test_party_documents.py new file mode 100644 index 0000000000..95f0cb3b78 --- /dev/null +++ b/api/applications/views/tests/test_party_documents.py @@ -0,0 +1,114 @@ +from moto import mock_aws +from parameterized import parameterized + +from django.http import FileResponse +from django.urls import reverse +from rest_framework import status + +from api.applications.tests.factories import PartyOnApplicationFactory, StandardApplicationFactory +from api.parties.tests.factories import PartyFactory, PartyDocumentFactory +from test_helpers.clients import DataTestClient + + +@mock_aws +class PartyDocumentStreamTests(DataTestClient): + def setUp(self): + super().setUp() + self.party = PartyFactory( + organisation=self.organisation, + ) + self.party_on_application = PartyOnApplicationFactory(party=self.party) + self.application = self.party_on_application.application + self.create_default_bucket() + self.put_object_in_default_bucket("thisisakey", b"test") + + def test_get_party_document_stream(self): + party_document = PartyDocumentFactory( + party=self.party, + s3_key="thisisakey", + name="doc1.pdf", + safe=True, + ) + + url = reverse( + "applications:party_document_stream", + kwargs={ + "pk": str(self.application.pk), + "party_pk": str(self.party.pk), + "document_pk": str(party_document.pk), + }, + ) + response = self.client.get(url, **self.exporter_headers) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsInstance(response, FileResponse) + self.assertEqual(b"".join(response.streaming_content), b"test") + + def test_get_party_document_stream_invalid_document_pk(self): + another_party = PartyFactory( + organisation=self.organisation, + ) + party_document = PartyDocumentFactory( + party=self.party, + s3_key="thisisakey", + name="doc1.pdf", + safe=True, + ) + + url = reverse( + "applications:party_document_stream", + kwargs={ + "pk": str(self.application.pk), + "party_pk": str(another_party.pk), + "document_pk": str(party_document.pk), + }, + ) + response = self.client.get(url, **self.exporter_headers) + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_get_party_document_stream_invalid_application_pk(self): + another_application = StandardApplicationFactory() + party_document = PartyDocumentFactory( + party=self.party, + s3_key="thisisakey", + name="doc1.pdf", + safe=True, + ) + + url = reverse( + "applications:party_document_stream", + kwargs={ + "pk": str(another_application.pk), + "party_pk": str(self.party.pk), + "document_pk": str(party_document.pk), + }, + ) + response = self.client.get(url, **self.exporter_headers) + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_get_party_document_stream_forbidden_organisation(self): + other_organisation = self.create_organisation_with_exporter_user()[0] + self.party.organisation = other_organisation + self.party.save() + self.application.organisation = other_organisation + self.application.save() + party_document = PartyDocumentFactory( + party=self.party, + s3_key="thisisakey", + name="doc1.pdf", + safe=True, + ) + + url = reverse( + "applications:party_document_stream", + kwargs={ + "pk": str(self.application.pk), + "party_pk": str(self.party.pk), + "document_pk": str(party_document.pk), + }, + ) + response = self.client.get(url, **self.exporter_headers) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/api/parties/tests/factories.py b/api/parties/tests/factories.py index c16dd2defe..5b9683bc9b 100644 --- a/api/parties/tests/factories.py +++ b/api/parties/tests/factories.py @@ -2,7 +2,7 @@ from api.staticdata.countries.factories import CountryFactory from api.parties.enums import SubType, PartyType -from api.parties.models import Party +from api.parties.models import Party, PartyDocument class PartyFactory(factory.django.DjangoModelFactory): @@ -17,6 +17,11 @@ class Meta: model = Party +class PartyDocumentFactory(factory.django.DjangoModelFactory): + class Meta: + model = PartyDocument + + class ConsigneeFactory(PartyFactory): type = PartyType.CONSIGNEE sub_type = SubType.GOVERNMENT