Skip to content

Commit

Permalink
Delete identity overrides from environments_v2 table on feature delete
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewelwell committed Feb 6, 2025
1 parent d508e34 commit 4b54d0d
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 0 deletions.
11 changes: 11 additions & 0 deletions api/edge_api/identities/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,14 @@ def update_flagsmith_environments_v2_identity_overrides(
identifier=identifier,
)
dynamodb_wrapper_v2.update_identity_overrides(identity_override_changeset)


@register_task_handler()
def delete_environments_v2_identity_overrides_from_for_feature(feature_id: int) -> None:
dynamodb_wrapper_v2 = DynamoEnvironmentV2Wrapper()

feature = Feature.objects.all_with_deleted().get(id=feature_id)
for environment in feature.project.environments.all():
dynamodb_wrapper_v2.delete_identity_overrides(
environment_id=environment.id, feature_id=feature_id
)
17 changes: 17 additions & 0 deletions api/environments/dynamodb/wrappers/environment_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,20 @@ def delete_environment(self, environment_id: int):
ENVIRONMENTS_V2_SORT_KEY: item["document_key"],
},
)

def delete_identity_overrides(self, environment_id: int, feature_id: int) -> None:
filter_expression = self.get_identity_overrides_key_condition_expression(
environment_id, feature_id
)
query_kwargs: "QueryInputRequestTypeDef" = {
"KeyConditionExpression": filter_expression,
"ProjectionExpression": "document_key",
}

for item in self.query_iter_all_items(**query_kwargs):
self.table.delete_item(
Key={
ENVIRONMENTS_V2_PARTITION_KEY: environment_id,
ENVIRONMENTS_V2_SORT_KEY: item["document_key"],
}
)
11 changes: 11 additions & 0 deletions api/features/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ def create_github_comment(self) -> None:
def create_feature_states(self):
FeatureState.create_initial_feature_states_for_feature(feature=self)

@hook(AFTER_SAVE, when="deleted_at", was=None, is_not=None)
def delete_identity_overrides(self) -> None:
if self.deleted_at and self.project.enable_dynamo_db:
from edge_api.identities.tasks import (
delete_environments_v2_identity_overrides_from_for_feature,
)

delete_environments_v2_identity_overrides_from_for_feature.delay(
kwargs={"feature_id": self.id}
)

def validate_unique(self, *args, **kwargs):
"""
Checks unique constraints on the model and raises ``ValidationError``
Expand Down
66 changes: 66 additions & 0 deletions api/tests/unit/features/test_unit_features_views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import typing
import uuid
from datetime import date, datetime, timedelta
from unittest import mock
Expand Down Expand Up @@ -31,6 +32,10 @@
IDENTITY_FEATURE_STATE_UPDATED_MESSAGE,
)
from audit.models import AuditLog, RelatedObjectType
from environments.dynamodb import (
DynamoEnvironmentV2Wrapper,
DynamoIdentityWrapper,
)
from environments.identities.models import Identity
from environments.models import Environment, EnvironmentAPIKey
from environments.permissions.models import UserEnvironmentPermission
Expand All @@ -50,8 +55,17 @@
WithProjectPermissionsCallable,
)
from users.models import FFAdminUser, UserPermissionGroup
from util.mappers import (
map_engine_feature_state_to_identity_override,
map_engine_identity_to_identity_document,
map_identity_override_to_identity_override_document,
map_identity_to_engine,
)
from webhooks.webhooks import WebhookEventType

if typing.TYPE_CHECKING:
from mypy_boto3_dynamodb.service_resource import Table

# patch this function as it's triggering extra threads and causing errors
mock.patch("features.signals.trigger_feature_state_change_webhooks").start()

Expand Down Expand Up @@ -3506,3 +3520,55 @@ def test_filter_features_with_owners_and_group_owners_together(
assert len(response.data["results"]) == 2
assert response.data["results"][0]["id"] == feature.id
assert response.data["results"][1]["id"] == feature2.id


def test_delete_feature_deletes_any_related_identity_overrides(
flagsmith_environments_v2_table: "Table",
flagsmith_identities_table: "Table",
dynamodb_identity_wrapper: DynamoIdentityWrapper,
dynamodb_wrapper_v2: DynamoEnvironmentV2Wrapper,
environment: Environment,
feature: Feature,
identity_featurestate: FeatureState,
identity: Identity,
admin_client_new: APIClient,
) -> None:
# Given
engine_identity = map_identity_to_engine(identity, with_overrides=True)
dynamodb_identity_wrapper.put_item(
map_engine_identity_to_identity_document(engine_identity)
)
dynamodb_wrapper_v2.write_environments(environments=[environment])
flagsmith_environments_v2_table.put_item(
Item=map_identity_override_to_identity_override_document(
map_engine_feature_state_to_identity_override(
feature_state=engine_identity.identity_features[0],
identity_uuid=str(engine_identity.identity_uuid),
identifier=engine_identity.identifier,
environment_api_key=environment.api_key,
environment_id=environment.id,
),
)
)

feature.project.enable_dynamo_db = True
feature.project.save()

delete_feature_url = reverse(
"api-v1:projects:project-features-detail", args=[feature.project_id, feature.id]
)

# When
delete_feature_response = admin_client_new.delete(delete_feature_url)

# Then
assert delete_feature_response.status_code == status.HTTP_204_NO_CONTENT

assert (
flagsmith_environments_v2_table.query(
KeyConditionExpression=dynamodb_wrapper_v2.get_identity_overrides_key_condition_expression(
environment_id=environment.id, feature_id=feature.id
)
)["Count"]
== 0
)

0 comments on commit 4b54d0d

Please sign in to comment.