Skip to content

Commit a3e8fbd

Browse files
feat: Bypass server-side filtering for server keys (#130)
* feat: Bypass server-side filtering for server keys * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 4d01e56 commit a3e8fbd

File tree

2 files changed

+124
-13
lines changed

2 files changed

+124
-13
lines changed

src/edge_proxy/environments.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
logger = structlog.get_logger(__name__)
2828

29+
SERVER_API_KEY_PREFIX = "ser."
30+
2931

3032
class EnvironmentService:
3133
def __init__(
@@ -77,11 +79,12 @@ def get_flags_response_data(
7779
) -> dict[str, typing.Any]:
7880
environment_document = self.get_environment(environment_key)
7981
environment = EnvironmentModel.model_validate(environment_document)
82+
is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX)
8083

8184
if feature:
8285
feature_state = get_environment_feature_state(environment, feature)
8386

84-
if not filter_out_server_key_only_feature_states(
87+
if not is_server_key and not filter_out_server_key_only_feature_states(
8588
feature_states=[feature_state],
8689
environment=environment,
8790
):
@@ -90,10 +93,12 @@ def get_flags_response_data(
9093
data = map_feature_state_to_response_data(feature_state)
9194

9295
else:
93-
feature_states = filter_out_server_key_only_feature_states(
94-
feature_states=get_environment_feature_states(environment),
95-
environment=environment,
96-
)
96+
feature_states = get_environment_feature_states(environment)
97+
if not is_server_key:
98+
feature_states = filter_out_server_key_only_feature_states(
99+
feature_states=feature_states,
100+
environment=environment,
101+
)
97102
data = map_feature_states_to_response_data(feature_states)
98103

99104
return data
@@ -103,21 +108,26 @@ def get_identity_response_data(
103108
) -> dict[str, typing.Any]:
104109
environment_document = self.get_environment(environment_key)
105110
environment = EnvironmentModel.model_validate(environment_document)
111+
is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX)
112+
106113
identity = IdentityModel.model_validate(
107114
self.cache.get_identity(
108115
environment_api_key=environment_key,
109116
identifier=input_data.identifier,
110117
)
111118
)
112119
trait_models = input_data.traits
113-
flags = filter_out_server_key_only_feature_states(
114-
feature_states=get_identity_feature_states(
115-
environment,
116-
identity,
117-
override_traits=trait_models,
118-
),
119-
environment=environment,
120+
flags = get_identity_feature_states(
121+
environment,
122+
identity,
123+
override_traits=trait_models,
120124
)
125+
126+
if not is_server_key:
127+
flags = filter_out_server_key_only_feature_states(
128+
feature_states=flags,
129+
environment=environment,
130+
)
121131
data = {
122132
"traits": map_traits_to_response_data(trait_models),
123133
"flags": map_feature_states_to_response_data(

tests/test_environments.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from pytest_mock import MockerFixture
1010

1111
from edge_proxy.environments import EnvironmentService
12-
from edge_proxy.exceptions import FlagsmithUnknownKeyError
12+
from edge_proxy.exceptions import (
13+
FeatureNotFoundError,
14+
FlagsmithUnknownKeyError,
15+
)
1316
from edge_proxy.models import IdentityWithTraits
1417
from edge_proxy.settings import (
1518
EndpointCacheSettings,
@@ -230,3 +233,101 @@ async def test_get_identity_flags_response_skips_cache_for_different_identity(
230233
assert environment_service.get_identity_response_data.cache_info().currsize == 2
231234
assert environment_service.get_identity_response_data.cache_info().misses == 2
232235
assert environment_service.get_identity_response_data.cache_info().hits == 0
236+
237+
238+
@pytest.mark.asyncio
239+
async def test_get_flags_response_data_skips_filter_for_server_key(
240+
mocker: MockerFixture,
241+
) -> None:
242+
# Given
243+
# We create a new settings object that contains a server key as a client_side_key
244+
api_key = "ser." + environment_1_api_key
245+
_settings = AppSettings(
246+
environment_key_pairs=[
247+
{"client_side_key": api_key, "server_side_key": "ser.key"}
248+
]
249+
)
250+
251+
mocked_client = mocker.AsyncMock()
252+
mocked_client.get.return_value = mocker.MagicMock(
253+
text=orjson.dumps(environment_1), raise_for_status=lambda: None
254+
)
255+
256+
environment_service = EnvironmentService(settings=_settings, client=mocked_client)
257+
await environment_service.refresh_environment_caches()
258+
259+
# When
260+
# We retrieve the flag response data
261+
flags = environment_service.get_flags_response_data(api_key)
262+
specific_flag = environment_service.get_flags_response_data(api_key, "feature_3")
263+
264+
# Then
265+
# we get the server-side only flag
266+
assert len(flags) == 3
267+
assert flags[2].get("feature").get("name") == "feature_3"
268+
assert specific_flag.get("feature").get("name") == "feature_3"
269+
270+
271+
@pytest.mark.asyncio
272+
async def test_get_flags_response_data_filters_server_side_features_for_client_key(
273+
mocker: MockerFixture,
274+
) -> None:
275+
# Given
276+
# We create a new settings object that contains a client side key
277+
_settings = AppSettings(
278+
environment_key_pairs=[
279+
{"client_side_key": environment_1_api_key, "server_side_key": "ser.key"}
280+
]
281+
)
282+
283+
mocked_client = mocker.AsyncMock()
284+
mocked_client.get.return_value = mocker.MagicMock(
285+
text=orjson.dumps(environment_1), raise_for_status=lambda: None
286+
)
287+
288+
environment_service = EnvironmentService(settings=_settings, client=mocked_client)
289+
await environment_service.refresh_environment_caches()
290+
291+
# When
292+
# We retrieve the flag response data
293+
flags = environment_service.get_flags_response_data(environment_1_api_key)
294+
with pytest.raises(FeatureNotFoundError):
295+
environment_service.get_flags_response_data(environment_1_api_key, "feature_3")
296+
297+
# Then
298+
# we only get the two client side flags
299+
assert len(flags) == 2
300+
301+
302+
@pytest.mark.asyncio
303+
async def test_get_identity_flags_response_skips_filter_for_server_key(
304+
mocker: MockerFixture,
305+
) -> None:
306+
# Given
307+
# We create a new settings object that contains a server key as a client_side_key
308+
api_key = "ser." + environment_1_api_key
309+
_settings = AppSettings(
310+
environment_key_pairs=[
311+
{"client_side_key": api_key, "server_side_key": "ser.key"}
312+
]
313+
)
314+
315+
mocked_client = mocker.AsyncMock()
316+
mocked_client.get.return_value = mocker.MagicMock(
317+
text=orjson.dumps(environment_1), raise_for_status=lambda: None
318+
)
319+
320+
environment_service = EnvironmentService(settings=_settings, client=mocked_client)
321+
await environment_service.refresh_environment_caches()
322+
323+
# When
324+
# We retrieve the flags for an identity
325+
result = environment_service.get_identity_response_data(
326+
IdentityWithTraits(identifier="foo"), api_key
327+
)
328+
329+
# Then
330+
# we get the server-side only flag
331+
flags = result.get("flags")
332+
assert len(flags) == 3
333+
assert flags[2].get("feature").get("name") == "feature_3"

0 commit comments

Comments
 (0)