Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Identity overrides are not deleted when deleting Edge identities #4460

Merged
merged 2 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 41 additions & 16 deletions api/edge_api/identities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,39 +167,64 @@ def remove_feature_override(self, feature_state: FeatureStateModel) -> None:

def save(self, user: FFAdminUser = None, master_api_key: MasterAPIKey = None):
self.dynamo_wrapper.put_item(self.to_document())
changes = self._get_changes()
if changes["feature_overrides"]:
changeset = self._get_changes()
self._update_feature_overrides(
changeset=changeset,
user=user,
master_api_key=master_api_key,
)
self._reset_initial_state()

def delete(
self, user: FFAdminUser = None, master_api_key: MasterAPIKey = None
) -> None:
self.dynamo_wrapper.delete_item(self._engine_identity_model.composite_key)
self._engine_identity_model.identity_features.clear()
changeset = self._get_changes()
self._update_feature_overrides(
changeset=changeset,
user=user,
master_api_key=master_api_key,
)
self._reset_initial_state()

def synchronise_features(self, valid_feature_names: typing.Collection[str]) -> None:
identity_feature_names = {
fs.feature.name for fs in self._engine_identity_model.identity_features
}
if not identity_feature_names.issubset(valid_feature_names):
self._engine_identity_model.prune_features(list(valid_feature_names))
sync_identity_document_features.delay(args=(str(self.identity_uuid),))

def to_document(self) -> dict:
return map_engine_identity_to_identity_document(self._engine_identity_model)

def _update_feature_overrides(
self,
changeset: IdentityChangeset,
user: FFAdminUser = None,
master_api_key: MasterAPIKey = None,
) -> None:
if changeset["feature_overrides"]:
# TODO: would this be simpler if we put a wrapper around FeatureStateModel instead?
generate_audit_log_records.delay(
kwargs={
"environment_api_key": self.environment_api_key,
"identifier": self.identifier,
"user_id": getattr(user, "id", None),
"changes": changes,
"changes": changeset,
"identity_uuid": str(self.identity_uuid),
"master_api_key_id": getattr(master_api_key, "id", None),
}
)
update_flagsmith_environments_v2_identity_overrides.delay(
kwargs={
"environment_api_key": self.environment_api_key,
"changes": changes,
"changes": changeset,
"identity_uuid": str(self.identity_uuid),
"identifier": self.identifier,
}
)
self._reset_initial_state()

def synchronise_features(self, valid_feature_names: typing.Collection[str]) -> None:
identity_feature_names = {
fs.feature.name for fs in self._engine_identity_model.identity_features
}
if not identity_feature_names.issubset(valid_feature_names):
self._engine_identity_model.prune_features(list(valid_feature_names))
sync_identity_document_features.delay(args=(str(self.identity_uuid),))

def to_document(self) -> dict:
return map_engine_identity_to_identity_document(self._engine_identity_model)

def _get_changes(self) -> IdentityChangeset:
previous_instance = self._initial_state
Expand Down
8 changes: 6 additions & 2 deletions api/edge_api/identities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ def get_environment_from_request(self):
)

def perform_destroy(self, instance):
EdgeIdentity.dynamo_wrapper.delete_item(instance["composite_key"])
edge_identity = EdgeIdentity.from_identity_document(instance)
edge_identity.delete(
user=self.request.user,
master_api_key=getattr(self.request, "master_api_key", None),
)

@swagger_auto_schema(
responses={200: EdgeIdentityTraitsSerializer(many=True)},
Expand Down Expand Up @@ -281,7 +285,7 @@ def list(self, request, *args, **kwargs):
def perform_destroy(self, instance):
self.identity.remove_feature_override(instance)
self.identity.save(
user=self.request.user.id,
user=self.request.user,
master_api_key=getattr(self.request, "master_api_key", None),
)

Expand Down
32 changes: 32 additions & 0 deletions api/tests/integration/edge_api/identities/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,40 @@
from typing import Any

import pytest
from boto3.dynamodb.conditions import Key
from flag_engine.identities.models import IdentityModel

from edge_api.identities.models import EdgeIdentity
from environments.dynamodb.wrappers.environment_wrapper import (
DynamoEnvironmentV2Wrapper,
)


@pytest.fixture()
def webhook_mock(mocker):
return mocker.patch(
"edge_api.identities.serializers.call_environment_webhook_for_feature_state_change"
)


@pytest.fixture()
def identity_overrides_v2(
dynamo_enabled_environment: int,
identity_document_without_fs: dict[str, Any],
identity_document: dict[str, Any],
dynamodb_wrapper_v2: DynamoEnvironmentV2Wrapper,
) -> list[str]:
edge_identity = EdgeIdentity.from_identity_document(identity_document_without_fs)
for feature_override in IdentityModel.model_validate(
identity_document
).identity_features:
edge_identity.add_feature_override(feature_override)
edge_identity.save()
return [
item["document_key"]
for item in dynamodb_wrapper_v2.query_get_all_items(
KeyConditionExpression=Key("environment_id").eq(
str(dynamo_enabled_environment)
),
)
]
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import json
import urllib
from typing import Any

from boto3.dynamodb.conditions import Key
from django.urls import reverse
from rest_framework import status
from rest_framework.exceptions import NotFound
from rest_framework.test import APIClient

from edge_api.identities.views import EdgeIdentityViewSet
from environments.dynamodb.wrappers.environment_wrapper import (
DynamoEnvironmentV2Wrapper,
)
from environments.dynamodb.wrappers.identity_wrapper import (
DynamoIdentityWrapper,
)


def test_get_identities_returns_bad_request_if_dynamo_is_not_enabled(
Expand Down Expand Up @@ -125,33 +134,38 @@ def test_create_identity_returns_400_if_identity_already_exists(


def test_delete_identity(
admin_client,
dynamo_enabled_environment,
environment_api_key,
identity_document,
edge_identity_dynamo_wrapper_mock,
):
admin_client: APIClient,
dynamo_enabled_environment: int,
environment_api_key: str,
identity_document_without_fs: dict[str, Any],
identity_document: dict[str, Any],
dynamodb_identity_wrapper: DynamoIdentityWrapper,
dynamodb_wrapper_v2: DynamoEnvironmentV2Wrapper,
identity_overrides_v2: list[str],
) -> None:
# Given
identity_uuid = identity_document["identity_uuid"]
url = reverse(
"api-v1:environments:environment-edge-identities-detail",
args=[environment_api_key, identity_uuid],
)

edge_identity_dynamo_wrapper_mock.get_item_from_uuid_or_404.return_value = (
identity_document
)
# When
response = admin_client.delete(url)

# Then
assert response.status_code == status.HTTP_204_NO_CONTENT

edge_identity_dynamo_wrapper_mock.get_item_from_uuid_or_404.assert_called_with(
identity_uuid
)
edge_identity_dynamo_wrapper_mock.delete_item.assert_called_with(
identity_document["composite_key"]
assert not dynamodb_identity_wrapper.query_items(
IndexName="identity_uuid-index",
KeyConditionExpression=Key("identity_uuid").eq(identity_uuid),
)["Count"]
assert not list(
dynamodb_wrapper_v2.query_get_all_items(
KeyConditionExpression=Key("environment_id").eq(
str(dynamo_enabled_environment)
)
)
)


Expand Down
Loading