Skip to content

Commit 19cd637

Browse files
Zaimwa9pre-commit-ci[bot]rolodato
authored
fix: return-empty-trait-list-if-persistence-not-allowed-from-sdk (#5399)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Rodrigo López Dato <rodrigo.lopezdato@flagsmith.com>
1 parent df7a5f4 commit 19cd637

6 files changed

Lines changed: 89 additions & 11 deletions

File tree

api/environments/sdk/serializers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,5 @@ def save(self, **kwargs): # type: ignore[no-untyped-def]
193193
def validate_traits(self, traits: typing.List[dict] = None): # type: ignore[no-untyped-def,type-arg,assignment]
194194
request = self.context["request"]
195195
if traits and not request.environment.trait_persistence_allowed(request):
196-
raise serializers.ValidationError(
197-
"Setting traits not allowed with client key."
198-
)
196+
return []
199197
return traits

api/tests/unit/environments/identities/test_unit_identities_views.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -832,11 +832,10 @@ def test_get_identities_calls_forward_identity_request_with_correct_arguments(
832832
assert kwargs["kwargs"]["query_params"] == {"identifier": identity.identifier}
833833

834834

835-
def test_post_identities_with_traits_fails_if_client_cannot_set_traits(
835+
def test_post_identities_returns_empty_traits_if_client_cannot_set_traits(
836836
identity: Identity,
837837
environment: Environment,
838838
api_client: APIClient,
839-
feature: Feature,
840839
) -> None:
841840
# Given
842841
url = reverse("api-v1:sdk-identities")
@@ -856,7 +855,9 @@ def test_post_identities_with_traits_fails_if_client_cannot_set_traits(
856855

857856
# Then
858857
assert Trait.objects.count() == 0
859-
assert response.status_code == status.HTTP_400_BAD_REQUEST
858+
assert response.status_code == status.HTTP_200_OK
859+
assert response.json()["traits"] == []
860+
assert response.json()["identifier"] == identity.identifier
860861

861862

862863
def test_post_identities_with_traits_success_if_client_cannot_set_traits_server_key(

api/tests/unit/environments/sdk/test_unit_sdk_serializers.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,82 @@ def test_identify_with_traits_serializer__transient__identity_and_traits_not_per
115115
# Then
116116
assert not Identity.objects.filter(identifier=identity_identifier).exists()
117117
assert not Trait.objects.filter(identity__identifier=identity_identifier).exists()
118+
119+
120+
def test_identify_with_traits_serializer_validate_traits_returns_empty_list_when_persistence_not_allowed(
121+
mocker: MockerFixture,
122+
environment: Environment,
123+
) -> None:
124+
# Given
125+
data = {
126+
"identifier": "test_user",
127+
"traits": [
128+
{"trait_key": "key1", "trait_value": "value1"},
129+
{"trait_key": "key2", "trait_value": "value2"},
130+
],
131+
}
132+
133+
environment.allow_client_traits = False
134+
environment.save()
135+
136+
mock_request = mocker.MagicMock()
137+
mock_request.environment = environment
138+
139+
serializer = IdentifyWithTraitsSerializer(
140+
data=data, context={"environment": environment, "request": mock_request}
141+
)
142+
143+
# When
144+
assert serializer.is_valid()
145+
validated_traits = serializer.validated_data.get("traits")
146+
147+
serializer.save() # type: ignore[no-untyped-call]
148+
# Then
149+
assert validated_traits == []
150+
151+
assert Identity.objects.filter(identifier="test_user").exists()
152+
assert not Trait.objects.filter(identity__identifier="test_user").exists()
153+
154+
155+
def test_identify_with_traits_serializer_does_not_erase_existing_traits_when_persistence_not_allowed(
156+
mocker: MockerFixture,
157+
environment: Environment,
158+
) -> None:
159+
# Given
160+
identity = Identity.objects.create(environment=environment, identifier="new_user")
161+
Trait.objects.create(
162+
identity=identity,
163+
trait_key="existing_key",
164+
string_value="existing_value",
165+
value_type="string",
166+
)
167+
168+
environment.allow_client_traits = False
169+
environment.save()
170+
171+
data = {
172+
"identifier": "new_user",
173+
"traits": [
174+
{"trait_key": "new_key", "trait_value": "new_value"},
175+
{"trait_key": "new_second_key", "trait_value": "new_second_value"},
176+
],
177+
}
178+
179+
mock_request = mocker.MagicMock()
180+
mock_request.environment = environment
181+
182+
serializer = IdentifyWithTraitsSerializer(
183+
data=data, context={"environment": environment, "request": mock_request}
184+
)
185+
186+
# When
187+
assert serializer.is_valid()
188+
serializer.save() # type: ignore[no-untyped-call]
189+
190+
# Then
191+
identity.refresh_from_db()
192+
traits = Trait.objects.filter(identity=identity)
193+
194+
assert traits.count() == 1
195+
assert traits[0].trait_key == "existing_key"
196+
assert traits[0].string_value == "existing_value"

docs/docs/basic-features/segments.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ The Flagsmith API to set user traits, e.g. the `setTraits` method from the JavaS
3737
authentication or credentials. This means that users can change their own traits, which could be a security problem if
3838
you are using segments for authorisation or access control. If you must use segments for access control, make sure to
3939
disable the
40-
["Allow client SDKs to set user traits" option](system-administration/security.md#preventing-client-sdks-from-setting-traits)
40+
["Persist traits when using client-side SDK keys" option](system-administration/security.md#preventing-client-sdks-from-setting-traits)
4141
on every environment that needs it, and use server-side SDKs to set traits instead. You can still use client-side SDKs
4242
to read traits and flags derived from segments in this case.
4343

docs/docs/system-administration/security.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ There may be use-cases where you want to prevent client-side SDKs from setting t
88
setting `plan=silver` as a trait, and then enabling/disabling features based on that plan, a malicious user could, with
99
a client-side SDK, update their trait to `plan=gold` and unlock features they have not paid for.
1010

11-
You can prevent this by disabling the "Allow client SDKs to set user traits" option. This option defaults to "On".
11+
You can prevent this by disabling the "Persist traits when using client-side SDK keys" option. This option defaults to "On".
1212
Turning it "Off" will not allow client-side SDKs to write Traits to Flagsmith. In order to write traits, you will need
1313
to use a [server-side SDK and server-side Key](/clients).
1414

frontend/web/components/pages/EnvironmentSettingsPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ const EnvironmentSettingsPage: React.FC<EnvironmentSettingsPageProps> = ({
274274
return
275275
}
276276
const editedEnv = { ...currentEnv, ...newEnv }
277-
277+
278278
AppActions.editEnv(
279279
Object.assign({}, currentEnv, {
280280
allow_client_traits: !!editedEnv?.allow_client_traits,
@@ -794,8 +794,8 @@ const EnvironmentSettingsPage: React.FC<EnvironmentSettingsPageProps> = ({
794794
</div>
795795
<div className='mt-4'>
796796
<Setting
797-
title='Allow client SDKs to set user traits'
798-
description={`Disabling this option will prevent client SDKs from using the client key from setting traits.`}
797+
title='Persist traits when using client-side SDK keys'
798+
description={'If enabled, Flagsmith will persist any non-transient traits sent by SDKs using client-side keys when remotely evaluating flags.'}
799799
checked={currentEnv?.allow_client_traits}
800800
onChange={(value) => {
801801
updateCurrentEnv(

0 commit comments

Comments
 (0)