Skip to content
Merged
6 changes: 6 additions & 0 deletions api/features/versioning/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ class FeatureVersionDeleteError(FeatureVersioningError):

class CannotModifyLiveVersionError(FeatureVersioningError):
status_code = status.HTTP_400_BAD_REQUEST


class DirectFeatureStateWriteNotAllowedError(FeatureVersioningError):
status_code = status.HTTP_400_BAD_REQUEST
default_code = "direct_feature_state_write_not_allowed"
default_detail = "This environment uses v2 feature versioning. Use the environment feature version endpoint instead."
Comment thread
gagantrivedi marked this conversation as resolved.
21 changes: 21 additions & 0 deletions api/features/versioning/versioning_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,30 @@
FlagChangeSet,
FlagChangeSetV2,
)
from features.versioning.exceptions import DirectFeatureStateWriteNotAllowedError
from features.versioning.models import EnvironmentFeatureVersion


def require_direct_state_write(
environment: Environment, *, is_identity_override: bool
) -> None:
if is_identity_override or not environment.use_v2_feature_versioning:
return
raise DirectFeatureStateWriteNotAllowedError()


def require_direct_state_write_for_state(feature_state: FeatureState) -> None:
# FS rows attached to an unpublished EFV are a draft, so direct mutation is
# part of the versioning flow rather than a bypass of it.
efv = feature_state.environment_feature_version
if efv is not None and not efv.published:
return
require_direct_state_write(
environment=feature_state.environment, # type: ignore[arg-type]
is_identity_override=feature_state.identity_id is not None,
)


def get_environment_flags_queryset(
environment: Environment,
feature_name: str = None, # type: ignore[assignment]
Expand Down
27 changes: 27 additions & 0 deletions api/features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@
from .versioning.versioning_service import (
get_environment_flags_list,
get_environment_flags_queryset,
require_direct_state_write,
require_direct_state_write_for_state,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -740,6 +742,11 @@ def create(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
if identity_pk:
data["identity"] = identity_pk

require_direct_state_write(
environment=environment,
is_identity_override=bool(identity_pk),
)

serializer = self.get_serializer(data=data)

if serializer.is_valid(raise_exception=True):
Expand All @@ -763,6 +770,7 @@ def update(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
feature state value.
"""
feature_state_to_update = self.get_object()
require_direct_state_write_for_state(feature_state_to_update)
feature_state_data = request.data

# Check if feature state value was provided with request data. If so, create / update
Expand Down Expand Up @@ -797,6 +805,10 @@ def partial_update(self, request, *args, **kwargs): # type: ignore[no-untyped-d
"""
return self.update(request, *args, **kwargs) # type: ignore[no-untyped-call]

def destroy(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
require_direct_state_write_for_state(self.get_object())
return super().destroy(request, *args, **kwargs)

def update_feature_state_value(self, value, feature_state): # type: ignore[no-untyped-def]
feature_state_value_dict = feature_state.generate_feature_state_value_data(
value
Expand Down Expand Up @@ -930,6 +942,21 @@ class SimpleFeatureStateViewSet(
permission_classes = [FeatureStatePermissions]
filterset_fields = ["environment", "feature", "feature_segment"]

def create(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
environment_id = request.data.get("environment")
targets_version = bool(request.data.get("environment_feature_version"))
if environment_id and not targets_version:
environment = get_object_or_404(Environment, id=environment_id)
require_direct_state_write(
environment=environment,
is_identity_override=bool(request.data.get("identity")),
)
return super().create(request, *args, **kwargs)

def update(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
require_direct_state_write_for_state(self.get_object())
return super().update(request, *args, **kwargs)

def get_queryset(self): # type: ignore[no-untyped-def]
if getattr(self, "swagger_fake_view", False):
return FeatureState.objects.none()
Expand Down
Loading
Loading