diff --git a/api/audit/serializers.py b/api/audit/serializers.py index 202ef42f800f..70e975cfaa2e 100644 --- a/api/audit/serializers.py +++ b/api/audit/serializers.py @@ -24,6 +24,7 @@ class Meta: "environment", "project", "related_object_id", + "related_object_uuid", "related_object_type", "is_system_event", ) @@ -52,6 +53,7 @@ class Meta: "environment", "project", "related_object_id", + "related_object_uuid", "related_object_type", "is_system_event", "change_details", diff --git a/api/features/versioning/permissions.py b/api/features/versioning/permissions.py index f6add7839318..538029704259 100644 --- a/api/features/versioning/permissions.py +++ b/api/features/versioning/permissions.py @@ -35,6 +35,13 @@ def has_object_permission( ) +class EnvironmentFeatureVersionRetrievePermissions(BasePermission): + def has_object_permission(self, request, view, obj): + return request.user.has_environment_permission( + VIEW_ENVIRONMENT, obj.environment + ) + + class EnvironmentFeatureVersionFeatureStatePermissions(BasePermission): def has_permission(self, request: Request, view: GenericViewSet) -> bool: environment_pk = view.kwargs["environment_pk"] diff --git a/api/features/versioning/urls.py b/api/features/versioning/urls.py index a9b97b358fe0..fc7bc9322966 100644 --- a/api/features/versioning/urls.py +++ b/api/features/versioning/urls.py @@ -4,6 +4,7 @@ from features.versioning.views import ( EnvironmentFeatureVersionFeatureStatesViewSet, + EnvironmentFeatureVersionRetrieveAPIView, EnvironmentFeatureVersionViewSet, ) @@ -34,4 +35,9 @@ "environments//features//", include(ef_version_fs_router.urls), ), + path( + "environment-feature-versions//", + EnvironmentFeatureVersionRetrieveAPIView.as_view(), + name="get-efv-by-uuid", + ), ] diff --git a/api/features/versioning/views.py b/api/features/versioning/views.py index c21032f52db5..14fa4929bbcc 100644 --- a/api/features/versioning/views.py +++ b/api/features/versioning/views.py @@ -4,11 +4,11 @@ from django.utils.decorators import method_decorator from drf_yasg.utils import swagger_auto_schema from rest_framework.decorators import action +from rest_framework.generics import RetrieveAPIView from rest_framework.mixins import ( CreateModelMixin, DestroyModelMixin, ListModelMixin, - RetrieveModelMixin, UpdateModelMixin, ) from rest_framework.permissions import IsAuthenticated @@ -26,6 +26,7 @@ from features.versioning.permissions import ( EnvironmentFeatureVersionFeatureStatePermissions, EnvironmentFeatureVersionPermissions, + EnvironmentFeatureVersionRetrievePermissions, ) from features.versioning.serializers import ( EnvironmentFeatureVersionFeatureStateSerializer, @@ -46,7 +47,6 @@ ) class EnvironmentFeatureVersionViewSet( GenericViewSet, - RetrieveModelMixin, ListModelMixin, CreateModelMixin, DestroyModelMixin, @@ -135,6 +135,22 @@ def publish(self, request: Request, **kwargs) -> Response: return Response(serializer.data) +class EnvironmentFeatureVersionRetrieveAPIView(RetrieveAPIView): + """ + This is an additional endpoint to retrieve a specific version without needing + to provide the environment or feature as part of the URL. + """ + + permission_classes = [ + IsAuthenticated, + EnvironmentFeatureVersionRetrievePermissions, + ] + serializer_class = EnvironmentFeatureVersionRetrieveSerializer + + def get_queryset(self): + return EnvironmentFeatureVersion.objects.all() + + class EnvironmentFeatureVersionFeatureStatesViewSet( GenericViewSet, ListModelMixin, diff --git a/api/tests/unit/audit/test_unit_audit_views.py b/api/tests/unit/audit/test_unit_audit_views.py index f64ba6caedba..9c10d46dd4f1 100644 --- a/api/tests/unit/audit/test_unit_audit_views.py +++ b/api/tests/unit/audit/test_unit_audit_views.py @@ -5,10 +5,15 @@ from rest_framework import status from rest_framework.test import APIClient +from audit.constants import ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE from audit.models import AuditLog +from audit.related_object_type import RelatedObjectType from environments.models import Environment +from features.models import Feature +from features.versioning.models import EnvironmentFeatureVersion from organisations.models import Organisation, OrganisationRole from projects.models import Project +from users.models import FFAdminUser def test_audit_log_can_be_filtered_by_environments( @@ -146,3 +151,38 @@ def test_admin_user_cannot_list_audit_log_of_another_organisation( # Then assert response.json()["count"] == 0 + + +def test_retrieve_environment_feature_version_published_audit_log_record_includes_required_fields( + admin_client: APIClient, + admin_user: FFAdminUser, + environment_v2_versioning: Environment, + feature: Feature, +) -> None: + # Given + new_version = EnvironmentFeatureVersion.objects.create( + feature=feature, + environment=environment_v2_versioning, + ) + new_version.publish(published_by=admin_user) + + audit_log = ( + AuditLog.objects.filter(related_object_type=RelatedObjectType.EF_VERSION.name) + .order_by("-created_date") + .first() + ) + url = reverse("api-v1:audit-detail", args=[audit_log.id]) + + # When + response = admin_client.get(url) + + # Then + assert response.status_code == status.HTTP_200_OK + + response_json = response.json() + assert response_json["related_object_uuid"] == str(new_version.uuid) + assert response_json["related_object_type"] == RelatedObjectType.EF_VERSION.name + assert ( + response_json["log"] + == ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE % feature.name + ) diff --git a/api/tests/unit/features/versioning/test_unit_versioning_views.py b/api/tests/unit/features/versioning/test_unit_versioning_views.py index e89fda4c50d3..52a8f8846182 100644 --- a/api/tests/unit/features/versioning/test_unit_versioning_views.py +++ b/api/tests/unit/features/versioning/test_unit_versioning_views.py @@ -126,6 +126,23 @@ def test_delete_feature_version( assert environment_feature_version.deleted is True +def test_retrieve_environment_feature_version_permission_denied( + feature: Feature, + environment_v2_versioning: Environment, + staff_client: APIClient, +) -> None: + # Given + efv = EnvironmentFeatureVersion.objects.first() + + url = reverse("api-v1:versioning:get-efv-by-uuid", args=[str(efv.uuid)]) + + # When + response = staff_client.get(url) + + # Then + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_retrieve_feature_version_with_no_previous_version( feature: Feature, environment_v2_versioning: Environment, @@ -139,12 +156,7 @@ def test_retrieve_feature_version_with_no_previous_version( ) url = reverse( - "api-v1:versioning:environment-feature-versions-detail", - args=[ - environment_v2_versioning.id, - feature.id, - environment_feature_version.uuid, - ], + "api-v1:versioning:get-efv-by-uuid", args=[environment_feature_version.uuid] ) with_environment_permissions([VIEW_ENVIRONMENT]) @@ -181,10 +193,7 @@ def test_retrieve_feature_version_with_previous_version( ) version_2.publish(published_by=staff_user) - url = reverse( - "api-v1:versioning:environment-feature-versions-detail", - args=[environment_v2_versioning.id, feature.id, version_2.uuid], - ) + url = reverse("api-v1:versioning:get-efv-by-uuid", args=[version_2.uuid]) # When response = staff_client.get(url) @@ -216,10 +225,7 @@ def test_retrieve_feature_version_for_unpublished_version( feature=feature, environment=environment_v2_versioning ) - url = reverse( - "api-v1:versioning:environment-feature-versions-detail", - args=[environment_v2_versioning.id, feature.id, version_2.uuid], - ) + url = reverse("api-v1:versioning:get-efv-by-uuid", args=[version_2.uuid]) # When response = staff_client.get(url)