diff --git a/api/pyproject.toml b/api/pyproject.toml index 538b3fa5f090..8c3c75d71bcb 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "backoff>=2.2.1,<2.3.0", "appdirs>=1.4.4,<1.5.0", "django-cors-headers>=3.5.0,<3.6.0", - "djangorestframework>=3.15.2,<3.16.0", + "djangorestframework>=3.17.2,<3.18.0", "gunicorn>=23.0.0,<23.1.0", "pyparsing>=2.4.7,<2.5.0", "requests>=2.33.0,<2.34.0", diff --git a/api/tests/integration/trust_relationships/test_viewset.py b/api/tests/integration/trust_relationships/test_viewset.py index 327047856196..68ca1f848405 100644 --- a/api/tests/integration/trust_relationships/test_viewset.py +++ b/api/tests/integration/trust_relationships/test_viewset.py @@ -327,6 +327,44 @@ def test_create_trust_relationship__duplicate_issuer_and_audience__returns_400( ] +def test_update_trust_relationship__duplicate_issuer_and_audience__returns_400( + admin_client: APIClient, + organisation: int, + trust_relationship: int, +) -> None: + # Given + create_response = admin_client.post( + f"/api/v1/organisations/{organisation}/trust-relationships/", + data={ + "name": "GitLab CI", + "issuer": "https://gitlab.com", + "audience": "https://gitlab.com/Flagsmith", + "is_admin": True, + }, + format="json", + ) + assert create_response.status_code == status.HTTP_201_CREATED + url = ( + f"/api/v1/organisations/{organisation}" + f"/trust-relationships/{create_response.json()['id']}/" + ) + data = { + "name": "GitLab CI", + "issuer": "https://token.actions.githubusercontent.com", + "audience": "https://github.com/Flagsmith", + "is_admin": True, + } + + # When + response = admin_client.put(url, data=data, format="json") + + # Then + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["non_field_errors"] == [ + "The fields issuer, audience must make a unique set." + ] + + def test_create_trust_relationship__same_issuer_and_audience_as_deleted__returns_201( admin_client: APIClient, organisation: int, diff --git a/api/trust_relationships/serializers.py b/api/trust_relationships/serializers.py index 8df7625d4941..f804b20b75af 100644 --- a/api/trust_relationships/serializers.py +++ b/api/trust_relationships/serializers.py @@ -3,6 +3,7 @@ from django.conf import settings from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator from trust_relationships.models import TrustRelationship from trust_relationships.services import ( @@ -62,6 +63,16 @@ class Meta: "created_by", ) read_only_fields = ("id", "created_at", "created_by") + # Declared manually — since DRF 3.16 it can't auto-generate a validator + # for `unique_live_issuer_audience`, whose condition is on `deleted_at`, + # a field not writable here. `objects` excludes soft-deleted rows. + # https://github.com/encode/django-rest-framework/discussions/9777 + validators = [ + UniqueTogetherValidator( + queryset=TrustRelationship.objects.all(), + fields=("issuer", "audience"), + ) + ] def validate_issuer(self, issuer: str) -> str: parsed = urlparse(issuer) diff --git a/api/uv.lock b/api/uv.lock index c2c97e5e5383..16cf62474c29 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1039,14 +1039,14 @@ wheels = [ [[package]] name = "djangorestframework" -version = "3.15.2" +version = "3.17.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/ce/31482eb688bdb4e271027076199e1aa8d02507e530b6d272ab8b4481557c/djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad", size = 1067420, upload-time = "2024-06-19T07:59:32.891Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/35/c96055e700fdff25da3a7b7756cfd1d4dc54f38b9bc6d6c5e19e3a0fdc20/djangorestframework-3.17.2.tar.gz", hash = "sha256:89ed713b6dc83e1539f214b7d10808ae19bb8511004beba886225da6d5c9dafa", size = 906683, upload-time = "2026-08-05T07:47:22.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235, upload-time = "2024-06-19T07:59:26.106Z" }, + { url = "https://files.pythonhosted.org/packages/a2/46/c14108e400b208c394325eb63fbae06c81341b6447fa1a6f9da718b17fe7/djangorestframework-3.17.2-py3-none-any.whl", hash = "sha256:cb0546a7415d5b46c04e0f4fe0a54b2109f4fdd5e83ca773c8c6183a6493d042", size = 899109, upload-time = "2026-08-05T07:47:20.853Z" }, ] [[package]] @@ -1408,7 +1408,7 @@ requires-dist = [ { name = "django-storages", specifier = ">=1.10.1,<1.11.0" }, { name = "django-stubs", specifier = ">=5.1.3,<6.0.0" }, { name = "django-test-migrations", marker = "extra == 'dev'", specifier = ">=1.2.0,<2.0.0" }, - { name = "djangorestframework", specifier = ">=3.15.2,<3.16.0" }, + { name = "djangorestframework", specifier = ">=3.17.2,<3.18.0" }, { name = "djangorestframework-api-key", specifier = ">=2.2.0,<2.3.0" }, { name = "djangorestframework-dataclasses", specifier = ">=1.3.1,<2.0.0" }, { name = "djangorestframework-recursive", specifier = ">=0.1.2,<0.2.0" }, diff --git a/openapi.yaml b/openapi.yaml index 7481be959734..529c69a0fb2d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -25891,6 +25891,7 @@ components: is_system_tag: description: 'Indicates that a tag was created by the system, not the user.' type: boolean + default: false readOnly: true type: description: |- @@ -27985,6 +27986,7 @@ components: is_system_tag: description: 'Indicates that a tag was created by the system, not the user.' type: boolean + default: false readOnly: true type: description: |-