diff --git a/config/settings/base.py b/config/settings/base.py index c441e9980..bb853e77c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -546,6 +546,7 @@ "DEFAULT_AUTHENTICATION_CLASSES": [ "lemarche.api.authentication.CustomBearerAuthentication", ], + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], } diff --git a/lemarche/api/authentication.py b/lemarche/api/authentication.py index ea195db3d..8e909a423 100644 --- a/lemarche/api/authentication.py +++ b/lemarche/api/authentication.py @@ -13,21 +13,15 @@ class CustomBearerAuthentication(BaseAuthentication): """ Authentication via: 1. Authorization header: Bearer (recommended). - 2. URL parameter ?token= (deprecated, temporary support). """ def authenticate(self, request): token = None - warning_issued = False # Priority to the Authorization header auth_header = request.headers.get("Authorization") if auth_header and auth_header.startswith("Bearer "): token = auth_header.split("Bearer ")[1] - elif request.GET.get("token"): # Otherwise, try the URL parameter - token = request.GET.get("token") - warning_issued = True - logger.info("Authentication via URL token detected. This method is deprecated and less secure.") # If no token is provided if not token: @@ -46,10 +40,6 @@ def authenticate(self, request): except User.DoesNotExist: raise AuthenticationFailed("Invalid or expired token") - # Add a warning in the response for URL tokens - if warning_issued: - request._deprecated_auth_warning = True # Marker for middleware or view - # Return the user and the token return (user, token) @@ -58,35 +48,3 @@ def authenticate_header(self, request): Returns the expected header for 401 responses. """ return 'Bearer realm="api"' - - -class DeprecationWarningMiddleware: - """ - Middleware to inform users that authentication via URL `?token=` is deprecated. - - This middleware checks if the request contains a deprecated authentication token - and adds a warning header to the response if it does. - - Attributes: - get_response (callable): The next middleware or view in the chain. - - Methods: - __call__(request): - Processes the request and adds a deprecation warning header to the response - if the request contains a deprecated authentication token. - """ - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - - # Add a warning if the marker is set in the request - if hasattr(request, "_deprecated_auth_warning") and request._deprecated_auth_warning: - response.headers["Deprecation-Warning"] = ( - "URL token authentication is deprecated and will be removed on 2025/01. " - "Please use Authorization header with Bearer tokens." - ) - - return response diff --git a/lemarche/api/emails/views.py b/lemarche/api/emails/views.py index cb4f5aaeb..478daf3fc 100644 --- a/lemarche/api/emails/views.py +++ b/lemarche/api/emails/views.py @@ -47,7 +47,7 @@ def has_permission(self, request, view) -> bool: class InboundParsingEmailView(APIView): - permission_classes = [BrevoWhitelistPermission] + APIView.permission_classes + permission_classes = [BrevoWhitelistPermission] # override the default class that requires Authentication @extend_schema(exclude=True) def post(self, request): diff --git a/lemarche/api/networks/tests.py b/lemarche/api/networks/tests.py index dd9bfe6b6..4fbcdabd0 100644 --- a/lemarche/api/networks/tests.py +++ b/lemarche/api/networks/tests.py @@ -10,11 +10,12 @@ class NetworkApiTest(TestCase): def setUpTestData(cls): NetworkFactory(name="Reseau 1") NetworkFactory(name="Reseau 2") - UserFactory(api_key="admin") + cls.token = "a" * 64 + UserFactory(api_key=cls.token) def test_should_return_network_list(self): - url = reverse("api:networks-list") # anonymous user - response = self.client.get(url) + url = reverse("api:networks-list") + response = self.client.get(url, headers={"authorization": f"Bearer {self.token}"}) self.assertEqual(response.data["count"], 2) self.assertEqual(len(response.data["results"]), 2) self.assertTrue("slug" in response.data["results"][0]) diff --git a/lemarche/api/perimeters/tests.py b/lemarche/api/perimeters/tests.py index 13c8ec9f6..ad0984ef0 100644 --- a/lemarche/api/perimeters/tests.py +++ b/lemarche/api/perimeters/tests.py @@ -24,25 +24,25 @@ def setUpTestData(cls): cls.perimeter_region = PerimeterFactory( name="Auvergne-Rhône-Alpes", kind=Perimeter.KIND_REGION, insee_code="R84" ) - UserFactory(api_key="admin") + cls.token = "a" * 64 + UserFactory(api_key=cls.token) + cls.authenticated_client = cls.client_class(headers={"authorization": f"Bearer {cls.token}"}) def test_should_return_perimeter_list(self): - url = reverse("api:perimeters-list") # anonymous user - response = self.client.get(url) + url = reverse("api:perimeters-list") + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 3) self.assertEqual(len(response.data["results"]), 3) def test_should_filter_perimeter_list_by_kind(self): # single - url = reverse("api:perimeters-list") + f"?kind={Perimeter.KIND_CITY}" # anonymous user - response = self.client.get(url) + url = reverse("api:perimeters-list") + f"?kind={Perimeter.KIND_CITY}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1) self.assertEqual(len(response.data["results"]), 1) # multiple - url = ( - reverse("api:perimeters-list") + f"?kind={Perimeter.KIND_CITY}&kind={Perimeter.KIND_DEPARTMENT}" - ) # anonymous user # noqa - response = self.client.get(url) + url = reverse("api:perimeters-list") + f"?kind={Perimeter.KIND_CITY}&kind={Perimeter.KIND_DEPARTMENT}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1 + 1) self.assertEqual(len(response.data["results"]), 1 + 1) @@ -127,8 +127,10 @@ def test_should_filter_perimeters_autocomplete_by_result_count(self): class PerimeterChoicesApiTest(TestCase): def test_should_return_perimeter_kinds_list(self): - url = reverse("api:perimeter-kinds-list") # anonymous user - response = self.client.get(url) + token = "a" * 64 + UserFactory(api_key=token) + url = reverse("api:perimeter-kinds-list") + response = self.client.get(url, headers={"authorization": f"Bearer {token}"}) self.assertEqual(response.data["count"], 3) self.assertEqual(len(response.data["results"]), 3) self.assertTrue("id" in response.data["results"][0]) diff --git a/lemarche/api/perimeters/views.py b/lemarche/api/perimeters/views.py index 61682be3f..4c45aee36 100644 --- a/lemarche/api/perimeters/views.py +++ b/lemarche/api/perimeters/views.py @@ -21,6 +21,7 @@ class PerimeterAutocompleteViewSet(mixins.ListModelMixin, viewsets.GenericViewSe queryset = Perimeter.objects.all() serializer_class = PerimeterSimpleSerializer filterset_class = PerimeterAutocompleteFilter + permission_classes = [] # override default permission, this endpoint is open pagination_class = None http_method_names = ["get"] diff --git a/lemarche/api/sectors/tests.py b/lemarche/api/sectors/tests.py index 81b42f85b..d50e2ec61 100644 --- a/lemarche/api/sectors/tests.py +++ b/lemarche/api/sectors/tests.py @@ -10,11 +10,12 @@ class SectorApiTest(TestCase): def setUpTestData(cls): SectorFactory() SectorFactory() - UserFactory(api_key="admin") + cls.token = "a" * 64 + UserFactory(api_key=cls.token) def test_should_return_sector_list(self): - url = reverse("api:sectors-list") # anonymous user - response = self.client.get(url) + url = reverse("api:sectors-list") + response = self.client.get(url, headers={"authorization": f"Bearer {self.token}"}) self.assertEqual(response.data["count"], 2) self.assertEqual(len(response.data["results"]), 2) self.assertTrue("slug" in response.data["results"][0]) diff --git a/lemarche/api/siaes/serializers.py b/lemarche/api/siaes/serializers.py index af8bc9858..cca660c82 100644 --- a/lemarche/api/siaes/serializers.py +++ b/lemarche/api/siaes/serializers.py @@ -74,31 +74,3 @@ class Meta: "created_at", "updated_at", ] - - -class SiaeListSerializer(SiaeDetailSerializer): - class Meta: - model = Siae - fields = [ - "id", - "name", - "brand", - "slug", - "siret", - "nature", - "kind", - "kind_parent", - "contact_website", - "logo_url", - # additional contact_ fields available in detail - "address", - "city", - "post_code", - "department", - "region", - "is_active", - # is_ boolean fields available in detail - # M2M fields available in detail - "created_at", - "updated_at", - ] diff --git a/lemarche/api/siaes/tests.py b/lemarche/api/siaes/tests.py index 58f0dded4..b1fc39650 100644 --- a/lemarche/api/siaes/tests.py +++ b/lemarche/api/siaes/tests.py @@ -21,20 +21,11 @@ def setUpTestData(cls): def test_should_return_siae_sublist_to_anonymous_users(self): url = reverse("api:siae-list") # anonymous user response = self.client.get(url) - # self.assertEqual(response.data["count"], 12) - # self.assertEqual(len(response.data["results"]), 10) # results aren't paginated - self.assertEqual(len(response.data), 10) - self.assertTrue("id" in response.data[0]) - self.assertTrue("name" in response.data[0]) - self.assertTrue("siret" in response.data[0]) - self.assertTrue("kind" in response.data[0]) - self.assertTrue("kind_parent" in response.data[0]) - self.assertTrue("department" in response.data[0]) - self.assertTrue("created_at" in response.data[0]) + self.assertEqual(response.status_code, 401) def test_should_return_detailed_siae_list_with_pagination_to_authenticated_users(self): - url = reverse("api:siae-list") + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + response = self.client.get(url, headers={"authorization": f"Bearer {self.user_token}"}) self.assertEqual(response.data["count"], 12) self.assertEqual(len(response.data["results"]), 12) self.assertTrue("id" in response.data["results"][0]) @@ -46,8 +37,8 @@ def test_should_return_detailed_siae_list_with_pagination_to_authenticated_users self.assertTrue("created_at" in response.data["results"][0]) def test_should_return_401_if_token_unknown(self): - url = reverse("api:siae-list") + "?token=wrong" - response = self.client.get(url) + url = reverse("api:siae-list") + response = self.client.get(url, headers={"authorization": "wrong"}) self.assertEqual(response.status_code, 401) @@ -73,13 +64,14 @@ def setUpTestData(cls): siae_with_network_2.networks.add(cls.network_2) cls.user_token = generate_random_string() UserFactory(api_key=cls.user_token) + cls.authenticated_client = cls.client_class(headers={"authorization": f"Bearer {cls.user_token}"}) def test_siae_count(self): self.assertEqual(Siae.objects.count(), 9) def test_should_return_siae_list(self): - url = reverse("api:siae-list") + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 4 + 2 + 2) self.assertEqual(len(response.data["results"]), 4 + 2 + 2) @@ -87,87 +79,72 @@ def test_should_not_filter_siae_list_for_anonymous_user(self): # single url = reverse("api:siae-list") + f"?kind={siae_constants.KIND_ETTI}" # anonymous user response = self.client.get(url) - # self.assertEqual(response.data["count"], 1) - # self.assertEqual(len(response.data["results"]), 4 + 2 + 2) # results aren't paginated - self.assertEqual(len(response.data), 4 + 2 + 2) + self.assertEqual(response.status_code, 401) def test_should_filter_siae_list_by_is_active(self): - url = reverse("api:siae-list") + "?is_active=false&token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + "?is_active=false" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1) self.assertEqual(len(response.data["results"]), 1) - url = reverse("api:siae-list") + "?is_active=true&token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + "?is_active=true" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 3 + 2 + 2) self.assertEqual(len(response.data["results"]), 3 + 2 + 2) def test_should_filter_siae_list_by_kind(self): # single - url = reverse("api:siae-list") + f"?kind={siae_constants.KIND_ETTI}&token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + f"?kind={siae_constants.KIND_ETTI}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1) self.assertEqual(len(response.data["results"]), 1) # multiple - url = ( - reverse("api:siae-list") - + f"?kind={siae_constants.KIND_ETTI}&kind={siae_constants.KIND_ACI}&token=" - + self.user_token - ) - response = self.client.get(url) + url = reverse("api:siae-list") + f"?kind={siae_constants.KIND_ETTI}&kind={siae_constants.KIND_ACI}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1 + 1) self.assertEqual(len(response.data["results"]), 1 + 1) def test_should_filter_siae_list_by_presta_type(self): # single - url = reverse("api:siae-list") + f"?presta_types={siae_constants.PRESTA_BUILD}&token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + f"?presta_types={siae_constants.PRESTA_BUILD}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1) self.assertEqual(len(response.data["results"]), 1) # multiple url = ( reverse("api:siae-list") - + f"?presta_types={siae_constants.PRESTA_BUILD}&presta_types={siae_constants.PRESTA_PREST}&token=" - + self.user_token + + f"?presta_types={siae_constants.PRESTA_BUILD}&presta_types={siae_constants.PRESTA_PREST}" ) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1 + 1) self.assertEqual(len(response.data["results"]), 1 + 1) def test_should_filter_siae_list_by_department(self): - url = reverse("api:siae-list") + "?department=38&token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + "?department=38" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1) self.assertEqual(len(response.data["results"]), 1) def test_should_filter_siae_list_by_sector(self): # single - url = reverse("api:siae-list") + f"?sectors={self.sector_1.slug}&token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + f"?sectors={self.sector_1.slug}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1) self.assertEqual(len(response.data["results"]), 1) # multiple - url = ( - reverse("api:siae-list") - + f"?sectors={self.sector_1.slug}§ors={self.sector_2.slug}&token=" - + self.user_token - ) - response = self.client.get(url) + url = reverse("api:siae-list") + f"?sectors={self.sector_1.slug}§ors={self.sector_2.slug}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1 + 1) self.assertEqual(len(response.data["results"]), 1 + 1) def test_should_filter_siae_list_by_network(self): # single - url = reverse("api:siae-list") + f"?networks={self.network_1.slug}&token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-list") + f"?networks={self.network_1.slug}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1) self.assertEqual(len(response.data["results"]), 1) # multiple - url = ( - reverse("api:siae-list") - + f"?networks={self.network_1.slug}&networks={self.network_2.slug}&token=" - + self.user_token - ) - response = self.client.get(url) + url = reverse("api:siae-list") + f"?networks={self.network_1.slug}&networks={self.network_2.slug}" + response = self.authenticated_client.get(url) self.assertEqual(response.data["count"], 1 + 1) self.assertEqual(len(response.data["results"]), 1 + 1) @@ -178,34 +155,22 @@ def setUpTestData(cls): cls.siae = SiaeFactory() cls.user_token = generate_random_string() UserFactory(api_key=cls.user_token) + cls.authenticated_client = cls.client_class(headers={"authorization": f"Bearer {cls.user_token}"}) def test_should_return_4O4_if_siae_excluded(self): siae_opcs = SiaeFactory(kind="OPCS") - url = reverse("api:siae-detail", args=[siae_opcs.id]) # anonymous - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - url = reverse("api:siae-detail", args=[siae_opcs.id]) + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-detail", args=[siae_opcs.id]) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 404) - def test_should_return_simple_siae_object_to_anonymous_users(self): + def test_should_return_401_to_anonymous_users(self): url = reverse("api:siae-detail", args=[self.siae.id]) # anonymous user response = self.client.get(url) - self.assertTrue("id" in response.data) - self.assertTrue("name" in response.data) - self.assertTrue("siret" in response.data) - self.assertTrue("slug" in response.data) - self.assertTrue("kind" in response.data) - self.assertTrue("kind_parent" in response.data) - self.assertTrue("sectors" not in response.data) - self.assertTrue("networks" not in response.data) - self.assertTrue("offers" not in response.data) - self.assertTrue("client_references" not in response.data) - self.assertTrue("labels" not in response.data) + self.assertEqual(response.status_code, 401) def test_should_return_detailed_siae_object_to_authenticated_users(self): - url = reverse("api:siae-detail", args=[self.siae.id]) + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-detail", args=[self.siae.id]) + response = self.authenticated_client.get(url) self.assertTrue("id" in response.data) self.assertTrue("name" in response.data) self.assertTrue("siret" in response.data) @@ -225,33 +190,25 @@ def setUpTestData(cls): cls.user_token = generate_random_string() SiaeFactory(name="Une structure", siret="12312312312345", department="38") UserFactory(api_key=cls.user_token) + cls.authenticated_client = cls.client_class(headers={"authorization": f"Bearer {cls.user_token}"}) def test_should_return_404_if_slug_unknown(self): - url = reverse("api:siae-retrieve-by-slug", args=["test-123"]) # anonymous user - response = self.client.get(url) + url = reverse("api:siae-retrieve-by-slug", args=["test-123"]) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 404) def test_should_return_4O4_if_siae_excluded(self): siae_opcs = SiaeFactory(kind="OPCS") url = reverse("api:siae-retrieve-by-slug", args=[siae_opcs.slug]) # anonymous response = self.client.get(url) + self.assertEqual(response.status_code, 401) + url = reverse("api:siae-retrieve-by-slug", args=[siae_opcs.slug]) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 404) - url = reverse("api:siae-retrieve-by-slug", args=[siae_opcs.slug]) + "?token=" + self.user_token - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - - def test_should_return_siae_if_slug_known(self): - url = reverse("api:siae-retrieve-by-slug", args=["une-structure-38"]) # anonymous user - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - # self.assertEqual(type(response.data), dict) - self.assertEqual(response.data["siret"], "12312312312345") - self.assertEqual(response.data["slug"], "une-structure-38") - self.assertTrue("sectors" not in response.data) def test_should_return_detailed_siae_object_to_authenticated_user(self): - url = reverse("api:siae-retrieve-by-slug", args=["une-structure-38"]) + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-retrieve-by-slug", args=["une-structure-38"]) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data["siret"], "12312312312345") self.assertEqual(response.data["slug"], "une-structure-38") @@ -266,18 +223,17 @@ def setUpTestData(cls): SiaeFactory(name="Une autre structure avec le meme siret", siret="22222222233333", department="69") cls.user_token = generate_random_string() UserFactory(api_key=cls.user_token) + cls.authenticated_client = cls.client_class(headers={"authorization": f"Bearer {cls.user_token}"}) def test_should_return_400_if_siren_malformed(self): - # anonymous user for siren in ["123", "12312312312345"]: url = reverse("api:siae-retrieve-by-siren", args=[siren]) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 400) def test_should_return_empty_list_if_siren_unknown(self): - # anonymous user url = reverse("api:siae-retrieve-by-siren", args=["444444444"]) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 0) @@ -286,15 +242,14 @@ def test_should_return_4O4_if_siae_excluded(self): siae_opcs = SiaeFactory(kind="OPCS", siret="99999999999999") url = reverse("api:siae-retrieve-by-siren", args=[siae_opcs.siren]) # anonymous response = self.client.get(url) - self.assertEqual(len(response.data), 0) - url = reverse("api:siae-retrieve-by-siren", args=[siae_opcs.siren]) + "?token=" + self.user_token - response = self.client.get(url) + self.assertEqual(response.status_code, 401) + url = reverse("api:siae-retrieve-by-siren", args=[siae_opcs.siren]) + response = self.authenticated_client.get(url) self.assertEqual(len(response.data), 0) def test_should_return_siae_list_if_siren_known(self): - # anonymous user url = reverse("api:siae-retrieve-by-siren", args=["123123123"]) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 1) @@ -302,24 +257,23 @@ def test_should_return_siae_list_if_siren_known(self): self.assertEqual(response.data[0]["slug"], "une-structure-38") self.assertTrue("sectors" not in response.data) url = reverse("api:siae-retrieve-by-siren", args=["222222222"]) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 2) self.assertEqual(response.data[0]["siret"], "22222222233333") self.assertEqual(response.data[1]["siret"], "22222222233333") - self.assertTrue("sectors" not in response.data[0]) # authenticated user - url = reverse("api:siae-retrieve-by-siren", args=["123123123"]) + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-retrieve-by-siren", args=["123123123"]) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]["siret"], "12312312312345") self.assertEqual(response.data[0]["slug"], "une-structure-38") self.assertTrue("sectors" in response.data[0]) - url = reverse("api:siae-retrieve-by-siren", args=["222222222"]) + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-retrieve-by-siren", args=["222222222"]) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 2) @@ -336,18 +290,17 @@ def setUpTestData(cls): SiaeFactory(name="Une autre structure avec le meme siret", siret="22222222233333", department="69") cls.user_token = generate_random_string() UserFactory(api_key=cls.user_token) + cls.authenticated_client = cls.client_class(headers={"authorization": f"Bearer {cls.user_token}"}) def test_should_return_404_if_siret_malformed(self): - # anonymous user for siret in ["123", "123123123123456", "123 123 123 12345"]: url = reverse("api:siae-retrieve-by-siret", args=[siret]) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 400) def test_should_return_empty_list_if_siret_unknown(self): - # anonymous user url = reverse("api:siae-retrieve-by-siret", args=["44444444444444"]) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 0) @@ -356,40 +309,37 @@ def test_should_return_4O4_if_siae_excluded(self): siae_opcs = SiaeFactory(kind="OPCS", siret="99999999999999") url = reverse("api:siae-retrieve-by-siret", args=[siae_opcs.siret]) # anonymous response = self.client.get(url) - self.assertEqual(len(response.data), 0) - url = reverse("api:siae-retrieve-by-siret", args=[siae_opcs.siret]) + "?token=" + self.user_token - response = self.client.get(url) + self.assertEqual(response.status_code, 401) + url = reverse("api:siae-retrieve-by-siret", args=[siae_opcs.siret]) + response = self.authenticated_client.get(url) self.assertEqual(len(response.data), 0) def test_should_return_siae_list_if_siret_known(self): - # anonymous user url = reverse("api:siae-retrieve-by-siret", args=["12312312312345"]) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]["siret"], "12312312312345") self.assertEqual(response.data[0]["slug"], "une-structure-38") - self.assertTrue("sectors" not in response.data[0]) url = reverse("api:siae-retrieve-by-siret", args=["22222222233333"]) - response = self.client.get(url) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 2) self.assertEqual(response.data[0]["siret"], "22222222233333") self.assertEqual(response.data[1]["siret"], "22222222233333") - self.assertTrue("sectors" not in response.data[0]) # authenticated user - url = reverse("api:siae-retrieve-by-siret", args=["12312312312345"]) + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-retrieve-by-siret", args=["12312312312345"]) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]["siret"], "12312312312345") self.assertEqual(response.data[0]["slug"], "une-structure-38") self.assertTrue("sectors" in response.data[0]) - url = reverse("api:siae-retrieve-by-siret", args=["22222222233333"]) + "?token=" + self.user_token - response = self.client.get(url) + url = reverse("api:siae-retrieve-by-siret", args=["22222222233333"]) + response = self.authenticated_client.get(url) self.assertEqual(response.status_code, 200) # self.assertEqual(type(response.data), list) self.assertEqual(len(response.data), 2) @@ -399,20 +349,25 @@ def test_should_return_siae_list_if_siret_known(self): class SiaeChoicesApiTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.user_token = "a" * 64 + UserFactory(api_key=cls.user_token) + cls.authenticated_client = cls.client_class(headers={"authorization": f"Bearer {cls.user_token}"}) + def test_should_return_siae_kinds_list(self): - # anonymous user url = reverse("api:siae-kinds-list") - response = self.client.get(url) + response = self.authenticated_client.get(url) + self.assertEqual(response.status_code, 200) self.assertEqual(response.data["count"], 10) self.assertEqual(len(response.data["results"]), 10) self.assertTrue("id" in response.data["results"][0]) self.assertTrue("name" in response.data["results"][0]) - self.assertTrue("parent" in response.data["results"][0]) def test_should_return_siae_presta_types_list(self): - # anonymous user url = reverse("api:siae-presta-types-list") - response = self.client.get(url) + response = self.authenticated_client.get(url) + self.assertEqual(response.status_code, 200) self.assertEqual(response.data["count"], 3) self.assertEqual(len(response.data["results"]), 3) self.assertTrue("id" in response.data["results"][0]) diff --git a/lemarche/api/siaes/views.py b/lemarche/api/siaes/views.py index fcc6e44b1..8187177dd 100644 --- a/lemarche/api/siaes/views.py +++ b/lemarche/api/siaes/views.py @@ -1,12 +1,12 @@ from django.db.models import F from django.http import HttpResponseBadRequest from django.shortcuts import get_object_or_404 -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import extend_schema from rest_framework import mixins, viewsets from rest_framework.response import Response from lemarche.api.siaes.filters import SiaeFilter -from lemarche.api.siaes.serializers import SiaeDetailSerializer, SiaeListSerializer +from lemarche.api.siaes.serializers import SiaeDetailSerializer from lemarche.api.utils import BasicChoiceSerializer, BasicChoiceWithParentSerializer from lemarche.siaes import constants as siae_constants from lemarche.siaes.models import Siae @@ -18,7 +18,7 @@ class SiaeViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Gen """ queryset = Siae.objects.api_query_set() - serializer_class = SiaeListSerializer + serializer_class = SiaeDetailSerializer filterset_class = SiaeFilter def get_queryset(self): @@ -28,11 +28,6 @@ def get_queryset(self): @extend_schema( summary="Lister toutes les structures", tags=[Siae._meta.verbose_name_plural], - parameters=[ - OpenApiParameter( - name="token", description="Token Utilisateur (pour compatibilité ancienne)", required=False, type=str - ), - ], ) def list(self, request, format=None): """ @@ -40,25 +35,11 @@ def list(self, request, format=None): Un token est nécessaire pour l'accès complet à cette ressource. """ - if request.user.is_authenticated: - # Utilisateur authentifié : accès complet - return super().list(request, format) - else: - # Utilisateur non authentifié : limiter à 10 résultats - serializer = SiaeListSerializer( - self.get_queryset()[:10], - many=True, - ) - return Response(serializer.data) + return super().list(request, format) @extend_schema( summary="Détail d'une structure (par son id)", tags=[Siae._meta.verbose_name_plural], - parameters=[ - OpenApiParameter( - name="token", description="Token Utilisateur (pour compatibilité ancienne)", required=False, type=str - ), - ], responses=SiaeDetailSerializer, ) def retrieve(self, request, pk=None, format=None): @@ -72,11 +53,6 @@ def retrieve(self, request, pk=None, format=None): @extend_schema( summary="Détail d'une structure (par son slug)", tags=[Siae._meta.verbose_name_plural], - parameters=[ - OpenApiParameter( - name="token", description="Token Utilisateur (pour compatibilité ancienne)", required=False, type=str - ), - ], responses=SiaeDetailSerializer, ) def retrieve_by_slug(self, request, slug=None, format=None): @@ -91,11 +67,6 @@ def retrieve_by_slug(self, request, slug=None, format=None): @extend_schema( summary="Détail d'une structure (par son siren)", tags=[Siae._meta.verbose_name_plural], - parameters=[ - OpenApiParameter( - name="token", description="Token Utilisateur (pour compatibilité ancienne)", required=False, type=str - ), - ], responses=SiaeDetailSerializer, ) def retrieve_by_siren(self, request, siren=None, format=None): @@ -111,11 +82,6 @@ def retrieve_by_siren(self, request, siren=None, format=None): @extend_schema( summary="Détail d'une structure (par son siret)", tags=[Siae._meta.verbose_name_plural], - parameters=[ - OpenApiParameter( - name="token", description="Token Utilisateur (pour compatibilité ancienne)", required=False, type=str - ), - ], responses=SiaeDetailSerializer, ) def retrieve_by_siret(self, request, siret=None, format=None): @@ -129,29 +95,17 @@ def retrieve_by_siret(self, request, siret=None, format=None): return self._list_return(request, queryset, format) def _retrieve_return(self, request, queryset, format): - if not request.user.is_authenticated: - serializer = SiaeListSerializer( - queryset, - many=False, - ) - else: - serializer = SiaeDetailSerializer( - queryset, - many=False, - ) + serializer = SiaeDetailSerializer( + queryset, + many=False, + ) return Response(serializer.data) def _list_return(self, request, queryset, format): - if not request.user.is_authenticated: - serializer = SiaeListSerializer( - queryset, - many=True, - ) - else: - serializer = SiaeDetailSerializer( - queryset, - many=True, - ) + serializer = SiaeDetailSerializer( + queryset, + many=True, + ) return Response(serializer.data) diff --git a/lemarche/api/tenders/tests.py b/lemarche/api/tenders/tests.py index c17b7fac2..1259abdc1 100644 --- a/lemarche/api/tenders/tests.py +++ b/lemarche/api/tenders/tests.py @@ -4,7 +4,6 @@ from django.test import TestCase from django.urls import reverse -from lemarche.api.utils import generate_random_string from lemarche.perimeters.factories import PerimeterFactory from lemarche.sectors.factories import SectorFactory from lemarche.tenders import constants as tender_constants @@ -45,8 +44,8 @@ class TenderCreateApiTest(TestCase): @classmethod def setUpTestData(cls): - cls.user_token = generate_random_string() - cls.url = reverse("api:tenders-list") + "?token=" + cls.user_token + cls.user_token = "a" * 64 + cls.url = reverse("api:tenders-list") cls.user = UserFactory() cls.user_buyer = UserFactory(kind=User.KIND_BUYER, company_name="Entreprise Buyer") cls.user_with_token = UserFactory(email="admin@example.com", api_key=cls.user_token) @@ -66,7 +65,12 @@ def setup_mock_user_and_tender_creation(self, mock_get_user, user=None, title="T tender_data["extra_data"] = extra_data or {} # Tender creation - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) tender = Tender.objects.get(title=title) return response, tender, user @@ -77,15 +81,22 @@ def test_anonymous_user_cannot_create_tender(self): self.assertEqual(response.status_code, 401) def test_user_with_unknown_api_key_cannot_create_tender(self): - url = reverse("api:tenders-list") + "?token=test" - response = self.client.post(url, data=TENDER_JSON, content_type="application/json") + url = reverse("api:tenders-list") + response = self.client.post( + url, data=TENDER_JSON, content_type="application/json", headers={"authorization": "Bearer !!!!!!"} + ) self.assertEqual(response.status_code, 401) def test_user_with_valid_api_key_can_create_tender(self): # test with other email tender_data = TENDER_JSON.copy() tender_data["title"] = "Test author 1" - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 201) self.assertIn("slug", response.data.keys()) tender = Tender.objects.get(title="Test author 1") @@ -99,7 +110,12 @@ def test_user_with_valid_api_key_can_create_tender(self): tender_data = TENDER_JSON.copy() tender_data["title"] = "Test author 2" tender_data["contact_email"] = self.user_with_token.email - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 201) self.assertIn("slug", response.data.keys()) tender = Tender.objects.get(title="Test author 2") @@ -114,7 +130,12 @@ def test_create_tender_with_location(self): tender_data = TENDER_JSON.copy() tender_data["title"] = "Test location" tender_data["location"] = self.perimeter.slug - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 201) tender = Tender.objects.get(title="Test location") self.assertEqual(tender.location, self.perimeter) @@ -122,20 +143,35 @@ def test_create_tender_with_location(self): tender_data = TENDER_JSON.copy() tender_data["title"] = "Test empty location" tender_data["location"] = "" - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 201) # location must be valid tender_data = TENDER_JSON.copy() tender_data["title"] = "Test wrong location" tender_data["location"] = self.perimeter.slug + "wrong" - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 400) def test_create_tender_with_sectors(self): tender_data = TENDER_JSON.copy() tender_data["title"] = "Test sectors" tender_data["sectors"] = [self.sector_1.slug, self.sector_2.slug] - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 201) tender = Tender.objects.get(title="Test sectors") self.assertEqual(tender.sectors.count(), 2) @@ -143,18 +179,28 @@ def test_create_tender_with_sectors(self): tender_data = TENDER_JSON.copy() tender_data["title"] = "Test empty sectors" tender_data["sectors"] = [] - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 201) # sectors must be valid tender_data = TENDER_JSON.copy() tender_data["title"] = "Test wrong sectors" tender_data["sectors"] = [self.sector_1.slug + "wrong"] - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 400) tender_data = TENDER_JSON.copy() tender_data["title"] = "Test wrong empty sectors" tender_data["sectors"] = "" - response = self.client.post(self.url, data=tender_data) + response = self.client.post(self.url, data=tender_data, headers={"authorization": f"Bearer {self.user_token}"}) self.assertEqual(response.status_code, 400) @patch("lemarche.api.tenders.views.add_to_contact_list") @@ -216,7 +262,12 @@ def test_create_tender_with_different_contact_data(self): tender_data["contact_buyer_kind_detail"] = user_constants.BUYER_KIND_DETAIL_PUBLIC_ASSOCIATION tender_data["contact_company_name"] = "Une asso" tender_data["extra_data"] = {"source": "TALLY"} - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.user_token}"}, + ) self.assertEqual(response.status_code, 201) tender = Tender.objects.get(title="Test tally contact") self.assertEqual(tender.source, tender_constants.SOURCE_TALLY) @@ -268,8 +319,8 @@ def test_create_tender_with_distance_location(self): class TenderCreateApiPartnerTest(TestCase): @classmethod def setUpTestData(cls): - cls.api_token_approch = generate_random_string() - cls.url = reverse("api:tenders-list") + "?token=" + cls.api_token_approch + cls.api_token_approch = "a" * 64 + cls.url = reverse("api:tenders-list") cls.user_partner_with_token = UserFactory(email="approch@example.com", api_key=cls.api_token_approch) def test_partner_approch_can_create_tender(self): @@ -278,7 +329,12 @@ def test_partner_approch_can_create_tender(self): tender_data = TENDER_JSON.copy() tender_data["contact_email"] = self.user_partner_with_token.email tender_data["extra_data"] = {"id": 123} - response = self.client.post(self.url, data=tender_data, content_type="application/json") + response = self.client.post( + self.url, + data=tender_data, + content_type="application/json", + headers={"authorization": f"Bearer {self.api_token_approch}"}, + ) self.assertEqual(response.status_code, 201) self.assertEqual(Tender.objects.count(), 1) tender = Tender.objects.last() @@ -307,7 +363,10 @@ def test_partner_approch_can_update_tender(self): "deadline_date": "2024-12-31", } response = self.client.post( - self.url, data={**TENDER_JSON.copy(), **new_tender_partner_data}, content_type="application/json" + self.url, + data={**TENDER_JSON.copy(), **new_tender_partner_data}, + content_type="application/json", + headers={"authorization": f"Bearer {self.api_token_approch}"}, ) self.assertEqual(response.status_code, 201) self.assertEqual(Tender.objects.count(), 1) @@ -337,7 +396,10 @@ def test_partner_approch_new_tender_if_kind_changes(self): "deadline_date": "2024-12-31", } response = self.client.post( - self.url, data={**TENDER_JSON.copy(), **new_tender_partner_data}, content_type="application/json" + self.url, + data={**TENDER_JSON.copy(), **new_tender_partner_data}, + content_type="application/json", + headers={"authorization": f"Bearer {self.api_token_approch}"}, ) self.assertEqual(response.status_code, 201) self.assertEqual(Tender.objects.count(), 2) @@ -348,17 +410,25 @@ def test_partner_approch_new_tender_if_kind_changes(self): class TenderChoicesApiTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.user_token = "a" * 64 + UserFactory(api_key=cls.user_token) + cls.authenticated_client = cls.client_class(headers={"authorization": f"Bearer {cls.user_token}"}) + def test_should_return_tender_kinds_list(self): - url = reverse("api:tender-kinds-list") # anonymous user - response = self.client.get(url) + url = reverse("api:tender-kinds-list") + response = self.authenticated_client.get(url) + self.assertEqual(response.status_code, 200) self.assertEqual(response.data["count"], 3) self.assertEqual(len(response.data["results"]), 3) self.assertTrue("id" in response.data["results"][0]) self.assertTrue("name" in response.data["results"][0]) def test_should_return_tender_amounts_list(self): - url = reverse("api:tender-amounts-list") # anonymous user - response = self.client.get(url) + url = reverse("api:tender-amounts-list") + response = self.authenticated_client.get(url) + self.assertEqual(response.status_code, 200) self.assertEqual(response.data["count"], 14) self.assertEqual(len(response.data["results"]), 14) self.assertTrue("id" in response.data["results"][0]) diff --git a/lemarche/api/tenders/views.py b/lemarche/api/tenders/views.py index ece25440b..d44d2212d 100644 --- a/lemarche/api/tenders/views.py +++ b/lemarche/api/tenders/views.py @@ -1,6 +1,6 @@ from django.conf import settings from django.utils import timezone -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import extend_schema from rest_framework import mixins, viewsets from rest_framework.permissions import IsAuthenticated @@ -23,9 +23,6 @@ class TenderViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): @extend_schema( summary="Déposer un besoin d'achat", tags=[Tender._meta.verbose_name_plural], - parameters=[ - OpenApiParameter(name="token", description="Token Utilisateur", required=True, type=str), - ], ) def create(self, request, *args, **kwargs): return super().create(request, args, kwargs) diff --git a/lemarche/api/tests.py b/lemarche/api/tests.py index a7528b4b5..ba947692e 100644 --- a/lemarche/api/tests.py +++ b/lemarche/api/tests.py @@ -1,8 +1,7 @@ -from django.http import HttpResponse from django.test import RequestFactory, TestCase from rest_framework.exceptions import AuthenticationFailed -from lemarche.api.authentication import CustomBearerAuthentication, DeprecationWarningMiddleware +from lemarche.api.authentication import CustomBearerAuthentication from lemarche.api.utils import generate_random_string from lemarche.users.factories import UserFactory @@ -39,25 +38,6 @@ def test_authentication_with_authorization_header(self): self.assertEqual(user, self.user) self.assertEqual(token, self.user_token) - def test_authentication_with_url_token(self): - """ - Test the authentication process using a token provided in the URL. - - This test simulates a GET request with a token appended to the URL query string. - It verifies that the authentication mechanism correctly identifies the user and - token from the request. - - Assertions: - - The authenticated user should match the expected user. - - The token extracted from the request should match the expected user token. - """ - request = self.factory.get(self.url + "?token=" + self.user_token) - - user, token = self.authentication.authenticate(request) - - self.assertEqual(user, self.user) - self.assertEqual(token, self.user_token) - def test_authentication_with_short_token(self): """ Test the authentication process with a short token. @@ -117,44 +97,3 @@ def test_authentication_with_no_token(self): result = self.authentication.authenticate(request) self.assertIsNone(result) - - -class DeprecationWarningMiddlewareTest(TestCase): - def setUp(self): - self.factory = RequestFactory() - self.middleware = DeprecationWarningMiddleware(lambda request: HttpResponse("Test response")) - - def test_no_deprecation_warning(self): - """ - Test that no deprecation warning is present in the response. - - This test sends a GET request to a specific API endpoint and checks - that the response does not contain a 'Deprecation-Warning' attribute. - """ - request = self.factory.get("/api/some-endpoint/") - response = self.middleware(request) - - self.assertFalse(hasattr(response, "Deprecation-Warning")) - - def test_with_deprecation_warning(self): - """ - Test that a deprecation warning is included in the response when the request - contains the _deprecated_auth_warning marker. - - This test simulates a request to an endpoint with the _deprecated_auth_warning - marker set to True. It then checks that the response includes a "Deprecation-Warning" - header with the expected deprecation message indicating that URL token authentication - is deprecated and will be removed by January 2025, and advises to use the Authorization - header with Bearer tokens instead. - """ - request = self.factory.get("/api/some-endpoint/") - request._deprecated_auth_warning = True # Ajouter le marqueur - - response = self.middleware(request) - - self.assertIn("Deprecation-Warning", response) - self.assertEqual( - response["Deprecation-Warning"], - "URL token authentication is deprecated and will be removed on 2025/01. " - "Please use Authorization header with Bearer tokens.", - ) diff --git a/lemarche/templates/api/home.html b/lemarche/templates/api/home.html index 5689b6f34..2ba20b482 100644 --- a/lemarche/templates/api/home.html +++ b/lemarche/templates/api/home.html @@ -64,12 +64,7 @@

Comment obtenir un token ?

Comment se servir du token ?

-

Rien de plus simple, il suffit de rajouter token=<VOTRE-TOKEN> dans l'URL de vos requêtes API

-

- Exemples :
- - https://lemarche.inclusion.beta.gouv.fr/api/siae/?token=test
- - https://lemarche.inclusion.beta.gouv.fr/api/siae/?department=38&kind=ESAT&token=test -

+

Rien de plus simple, il suffit de rajouter Bearer <VOTRE-TOKEN> dans le header "authorization" de vos requêtes API