Skip to content

Commit

Permalink
feat: Redact anonymous attributes within feature events (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 committed Mar 14, 2024
1 parent e5c6cc4 commit d334861
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 12 deletions.
42 changes: 34 additions & 8 deletions src/ldclient_context_filter.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/ldclient_event_process_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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#{
Expand Down
3 changes: 2 additions & 1 deletion test-service/src/ts_service_request_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}),
Expand Down
61 changes: 59 additions & 2 deletions test/ldclient_context_filter_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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
]).

%%====================================================================
Expand All @@ -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) ->
Expand Down Expand Up @@ -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).

0 comments on commit d334861

Please sign in to comment.