diff --git a/src/ldclient_context_filter.erl b/src/ldclient_context_filter.erl index 9ab92c2..76d133c 100644 --- a/src/ldclient_context_filter.erl +++ b/src/ldclient_context_filter.erl @@ -8,7 +8,8 @@ %% API -export([ - format_context_for_event/2 + format_context_for_event/2, + format_context_for_event_with_anonyous_redaction/2 ]). -ifdef(TEST). @@ -25,11 +26,35 @@ -spec format_context_for_event(PrivateAttributes :: [ldclient_attribute_reference:attribute_reference()] | all, Context :: ldclient_context:context() ) -> Formatted :: map(). -format_context_for_event(PrivateAttributes, #{kind := <<"multi">>} = Context) -> +format_context_for_event(PrivateAttributes, Context) -> + internal_format_context_for_event(PrivateAttributes, Context, false). + +%% @doc Format a context for events. +%% +%% Should produce an event schema v4 formatted context. +%% +%% The private attributes specified in the context, and those included in the private attributes parameter, will be +%% removed from the context. Additionally they will be added to the redacted attributes list in _meta. +%% +%% If a provided context is anonymous, all attributes will be redacted except for key, kind, and anonymous. +%% @end +-spec format_context_for_event_with_anonyous_redaction(PrivateAttributes :: [ldclient_attribute_reference:attribute_reference()] | all, + Context :: ldclient_context:context() + ) -> Formatted :: map(). +format_context_for_event_with_anonyous_redaction(PrivateAttributes, Context) -> + internal_format_context_for_event(PrivateAttributes, Context, true). + +-spec internal_format_context_for_event(PrivateAttributes :: [ldclient_attribute_reference:attribute_reference()] | all, + Context :: ldclient_context:context(), + RedactAnonymous :: boolean() + ) -> Formatted :: map(). +internal_format_context_for_event(PrivateAttributes, #{kind := <<"multi">>} = Context, RedactAnonymous) -> maps:fold(fun(Key, Value, Acc) -> - Acc#{ensure_binary(Key) => format_context_part(PrivateAttributes, Key, Value)} + Acc#{ensure_binary(Key) => format_context_part(PrivateAttributes, Key, Value, RedactAnonymous)} end, #{}, Context); -format_context_for_event(PrivateAttributes, Context) -> +internal_format_context_for_event(_PrivateAttributes, #{anonymous := true} = Context, true) -> + internal_format_context_for_event(all, Context, false); +internal_format_context_for_event(PrivateAttributes, Context, _RedactAnonymous) -> Attributes = maps:get(attributes, Context, null), ContextPrivateAttributes = lists:map(fun(Value) -> ldclient_attribute_reference:new(Value) @@ -122,11 +147,12 @@ can_redact(_Components) -> true. -spec format_context_part(PrivateAttributes :: [ldclient_attribute_reference:attribute_reference()], Key :: string() | atom(), - Value :: map() | binary() + Value :: map() | binary(), + RedactAnonymous :: boolean() ) -> map() | binary(). -format_context_part(_PrivateAttributes, kind, Value) -> Value; -format_context_part(PrivateAttributes, _Key, Value) -> - format_context_for_event(PrivateAttributes, Value). +format_context_part(_PrivateAttributes, kind, Value, _RedactAnonymous) -> Value; +format_context_part(PrivateAttributes, _Key, Value, RedactAnonymous) -> + internal_format_context_for_event(PrivateAttributes, Value, RedactAnonymous). -spec ensure_binary(Value :: binary() | atom()) -> binary(). ensure_binary(Value) when is_atom(Value) -> atom_to_binary(Value, utf8); diff --git a/src/ldclient_event_process_server.erl b/src/ldclient_event_process_server.erl index 25fc3c8..7c4feb6 100644 --- a/src/ldclient_event_process_server.erl +++ b/src/ldclient_event_process_server.erl @@ -230,7 +230,7 @@ maybe_set_reason(_Event, OutputEvent) -> -spec format_event_set_context(binary(), ldclient_context:context(), map(), ldclient_config:private_attributes()) -> map(). format_event_set_context(<<"feature">>, Context, OutputEvent, GlobalPrivateAttributes) -> OutputEvent#{ - <<"context">> => ldclient_context_filter:format_context_for_event(GlobalPrivateAttributes, Context) + <<"context">> => ldclient_context_filter:format_context_for_event_with_anonyous_redaction(GlobalPrivateAttributes, Context) }; format_event_set_context(<<"debug">>, Context, OutputEvent, GlobalPrivateAttributes) -> OutputEvent#{ diff --git a/test-service/src/ts_service_request_handler.erl b/test-service/src/ts_service_request_handler.erl index 92a181a..67e830d 100644 --- a/test-service/src/ts_service_request_handler.erl +++ b/test-service/src/ts_service_request_handler.erl @@ -74,7 +74,8 @@ get_service_detail(Req, State) -> <<"tags">>, <<"server-side-polling">>, <<"user-type">>, - <<"inline-context">> + <<"inline-context">>, + <<"anonymous-redaction">> ], <<"clientVersion">> => ldclient_config:get_version() }), diff --git a/test/ldclient_context_filter_SUITE.erl b/test/ldclient_context_filter_SUITE.erl index e16e728..9a6d8b7 100644 --- a/test/ldclient_context_filter_SUITE.erl +++ b/test/ldclient_context_filter_SUITE.erl @@ -17,7 +17,9 @@ redacts_from_meta_single_context/1, handles_missing_attributes/1, can_redact_all_attributes_single_context/1, - can_redact_all_attributes_multi_context/1 + can_redact_all_attributes_multi_context/1, + can_redact_single_context_anonymous_attributes/1, + can_redact_multi_context_anonymous_attributes/1 ]). %%==================================================================== @@ -33,7 +35,9 @@ all() -> redacts_from_meta_single_context, handles_missing_attributes, can_redact_all_attributes_single_context, - can_redact_all_attributes_multi_context + can_redact_all_attributes_multi_context, + can_redact_single_context_anonymous_attributes, + can_redact_multi_context_anonymous_attributes ]. init_per_suite(Config) -> @@ -262,3 +266,56 @@ can_redact_all_attributes_multi_context(_) -> <<"_meta">> := #{<<"redactedAttributes">> := [<<"anAttribute">>, <<"nested">>]} } } = ldclient_context_filter:format_context_for_event(all, TestContext). + +can_redact_single_context_anonymous_attributes(_) -> + TestContext = + ldclient_context:set(name, <<"the-name">>, + ldclient_context:set(anonymous, true, + ldclient_context:set(<<"org">>, <<"anAttribute">>, <<"aValue">>, + ldclient_context:set(<<"org">>, <<"nested">>, #{ + <<"key1">> => <<"value1">>, + <<"key2">> => <<"value2">> + }, + ldclient_context:new(<<"org-key">>, <<"org">>))))), + #{ + <<"kind">> := <<"org">>, + <<"key">> := <<"org-key">>, + <<"anonymous">> := true, + <<"_meta">> := #{ + <<"redactedAttributes">> := [ + <<"name">>, + <<"anAttribute">>, + <<"nested">> + ] + } + } = ldclient_context_filter:format_context_for_event_with_anonyous_redaction([], TestContext). + +can_redact_multi_context_anonymous_attributes(_) -> + TestContext = ldclient_context:new_multi_from([ + ldclient_context:set(<<"org">>, <<"anAttribute">>, <<"aValue">>, + ldclient_context:set(anonymous, true, + ldclient_context:set(<<"org">>, <<"nested">>, #{ + <<"key1">> => <<"value1">>, + <<"key2">> => <<"value2">> + }, + ldclient_context:new(<<"org-key">>, <<"org">>)))), + ldclient_context:set(<<"user">>, <<"anAttribute">>, <<"aValue">>, + ldclient_context:set(<<"user">>, <<"nested">>, #{ + <<"key1">> => <<"value1">>, + <<"key2">> => <<"value2">> + }, + ldclient_context:new(<<"user-key">>, <<"user">>))) + ]), + #{ + <<"kind">> := <<"multi">>, + <<"org">> := #{ + <<"key">> := <<"org-key">>, + <<"anonymous">> := true, + <<"_meta">> := #{<<"redactedAttributes">> := [<<"anAttribute">>, <<"nested">>]} + }, + <<"user">> := #{ + <<"key">> := <<"user-key">>, + <<"anAttribute">> := <<"aValue">>, + <<"nested">> := #{<<"key1">> := <<"value1">>, <<"key2">> := <<"value2">>} + } + } = ldclient_context_filter:format_context_for_event_with_anonyous_redaction([], TestContext).