Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contract-tests/client-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ int main(int argc, char* argv[]) {
srv.add_capability("service-endpoints");
srv.add_capability("tags");
srv.add_capability("inline-context");
srv.add_capability("anonymous-redaction");

net::signal_set signals{ioc, SIGINT, SIGTERM};

Expand Down
1 change: 1 addition & 0 deletions contract-tests/server-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ int main(int argc, char* argv[]) {
srv.add_capability("tags");
srv.add_capability("server-side-polling");
srv.add_capability("inline-context");
srv.add_capability("anonymous-redaction");

net::signal_set signals{ioc, SIGINT, SIGTERM};

Expand Down
30 changes: 27 additions & 3 deletions libs/internal/include/launchdarkly/context_filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ class ContextFilter {
*/
JsonValue Filter(Context const& context);

/**
* Filter the given context and produce a JSON value. If a provided context
* is anonymous, all attributes are redacted.
*
* Only call this method for valid contexts.
*
* @param context The context to redact.
* @return JSON suitable for an analytics event.
*/
JsonValue FilterWithAnonymousRedaction(Context const& context);

private:
/**
* The filtering and JSON conversion algorithm is stack based
Expand All @@ -48,6 +59,16 @@ class ContextFilter {
JsonValue& parent;
};

/**
* Internal delegate function for Filter and FilterWithAnonymousRedaction.
*
* @param context The context to redact.
* @param redactAnonymous Whether to redact all attributes from anonymous
* contexts.
* @return JSON suitable for an analytics event.
*/
JsonValue Filter(Context const& context, bool redactAnonymous);

/**
* Put an item into its parent. Either as a pair in a map,
* or pushed onto an array.
Expand All @@ -63,11 +84,13 @@ class ContextFilter {
* @param path The path to check.
* @param attributes Attributes which may contain additional private
* attributes.
* @param redactAll Force all attributes to be redacted.
* @return True if the item was redacted.
*/
bool Redact(std::vector<std::string>& redactions,
std::vector<std::string_view> path,
Attributes const& attributes);
Attributes const& attributes,
bool redactAll);

/**
* Append a container to the parent.
Expand All @@ -85,9 +108,10 @@ class ContextFilter {

JsonValue FilterSingleContext(std::string_view kind,
bool include_kind,
Attributes const& attributes);
Attributes const& attributes,
bool redactAnonymous);

JsonValue FilterMultiContext(Context const& context);
JsonValue FilterMultiContext(Context const& context, bool redactAnonymous);

bool all_attributes_private_;
AttributeReference::SetType const global_private_attributes_;
Expand Down
39 changes: 28 additions & 11 deletions libs/internal/src/context_filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ ContextFilter::ContextFilter(
global_private_attributes_(std::move(global_private_attributes)) {}

ContextFilter::JsonValue ContextFilter::Filter(Context const& context) {
return Filter(context, false);
}

ContextFilter::JsonValue ContextFilter::FilterWithAnonymousRedaction(
Context const& context) {
return Filter(context, true);
}

ContextFilter::JsonValue ContextFilter::Filter(Context const& context,
bool redactAnonymous) {
// Context should be validated before calling this method.
assert(context.Valid());
if (context.Kinds().size() == 1) {
std::string const& kind = context.Kinds()[0];
return FilterSingleContext(kind, INCLUDE_KIND,
context.Attributes(kind));
return FilterSingleContext(kind, INCLUDE_KIND, context.Attributes(kind),
redactAnonymous);
}
return FilterMultiContext(context);
return FilterMultiContext(context, redactAnonymous);
}

void ContextFilter::Emplace(ContextFilter::StackItem& item,
Expand All @@ -33,8 +43,9 @@ void ContextFilter::Emplace(ContextFilter::StackItem& item,

bool ContextFilter::Redact(std::vector<std::string>& redactions,
std::vector<std::string_view> path,
Attributes const& attributes) {
if (all_attributes_private_) {
Attributes const& attributes,
bool redactAll) {
if (redactAll) {
redactions.push_back(AttributeReference::PathToStringReference(path));
return true;
}
Expand All @@ -61,7 +72,8 @@ bool ContextFilter::Redact(std::vector<std::string>& redactions,
ContextFilter::JsonValue ContextFilter::FilterSingleContext(
std::string_view kind,
bool include_kind,
Attributes const& attributes) {
Attributes const& attributes,
bool redactAnonymous) {
std::vector<StackItem> stack;
JsonValue filtered = JsonObject();
std::vector<std::string> redactions;
Expand All @@ -74,9 +86,11 @@ ContextFilter::JsonValue ContextFilter::FilterSingleContext(
filtered.as_object().emplace("anonymous", attributes.Anonymous());
}

bool redactAll =
all_attributes_private_ || (redactAnonymous && attributes.Anonymous());
if (!attributes.Name().empty() &&
!Redact(redactions, std::vector<std::string_view>{"name"},
attributes)) {
!Redact(redactions, std::vector<std::string_view>{"name"}, attributes,
redactAll)) {
filtered.as_object().insert_or_assign("name", attributes.Name());
}

Expand All @@ -90,7 +104,8 @@ ContextFilter::JsonValue ContextFilter::FilterSingleContext(
stack.pop_back();

// Check if the attribute needs to be redacted.
if (!item.path.empty() && Redact(redactions, item.path, attributes)) {
if (!item.path.empty() &&
Redact(redactions, item.path, attributes, redactAll)) {
continue;
}

Expand Down Expand Up @@ -170,14 +185,16 @@ ContextFilter::JsonValue* ContextFilter::AppendContainer(
}

ContextFilter::JsonValue ContextFilter::FilterMultiContext(
Context const& context) {
Context const& context,
bool redactAnonymous) {
JsonValue filtered = JsonObject();
filtered.as_object().emplace("kind", "multi");

for (std::string const& kind : context.Kinds()) {
filtered.as_object().emplace(
kind,
FilterSingleContext(kind, EXCLUDE_KIND, context.Attributes(kind)));
FilterSingleContext(kind, EXCLUDE_KIND, context.Attributes(kind),
redactAnonymous));
}

return filtered;
Expand Down
3 changes: 2 additions & 1 deletion libs/internal/src/events/asio_event_processor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,8 @@ std::vector<OutputEvent> AsioEventProcessor<SDK>::Process(

if (event.require_full_event) {
out.emplace_back(FeatureEvent{
std::move(base), filter_.filter(event.context)});
std::move(base),
filter_.FilterWithAnonymousRedaction(event.context)});
}
},
[&](IdentifyEventParams&& event) {
Expand Down
53 changes: 53 additions & 0 deletions libs/internal/tests/context_filter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,56 @@ TEST(ContextFilterTests, AllAttributesPrivateMultiContext) {

EXPECT_EQ(expected, filtered);
}

TEST(ContextFilterTests, AnonymousRedactionForSingleContext) {
auto global_private_attributes = AttributeReference::SetType{};
ContextFilter filter(false, global_private_attributes);

auto filtered =
filter.FilterWithAnonymousRedaction(ContextBuilder()
.Kind("user", "user-key")
.Name("Bob")
.Anonymous(true)
.Set("isCat", false)
.Build());

auto expected = parse(
"{\"key\":\"user-key\","
"\"kind\":\"user\","
"\"anonymous\":true,"
"\"_meta\":{"
"\"redactedAttributes\":[\"/name\", \"/isCat\"]}}");

EXPECT_EQ(expected, filtered);
}

TEST(ContextFilterTests, AnonymousRedactionForMultiContext) {
auto global_private_attributes = AttributeReference::SetType{};
ContextFilter filter(false, global_private_attributes);

auto filtered = filter.FilterWithAnonymousRedaction(
ContextBuilder()
.Kind("user", "user-key")
.Name("Bob")
.Anonymous(true)
.Set("email", "email.email@email")
.Set("array", {false, true, "bacon"})
.Set("object", Object{{"test", true}, {"second", false}})
.SetPrivate("isCat", false)
.AddPrivateAttribute("/object/test")
.Kind("org", "org-key")
.Set("isCat", true)
.Set("email", "cat@cat.cat")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL that cat is a real TLD (Catalan): https://www.gandi.net/en-US/domain/tld/cat

.Build());

auto expected = parse(
"{\"kind\":\"multi\""
",\"org\":"
"{\"key\":\"org-key\",\"isCat\":true,\"email\":\"cat@cat.cat\"},"
"\"user\":"
"{\"key\":\"user-key\",\"anonymous\":true,\"_meta\":{"
"\"redactedAttributes\":[\"/name\",\"/object\",\"/isCat\",\"/"
"email\",\"/array\"]}}}");

EXPECT_EQ(expected, filtered);
}
2 changes: 1 addition & 1 deletion libs/internal/tests/event_serialization_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ TEST(EventSerialization, FeatureEvent) {
std::nullopt,

}),
filter.filter(context)};
filter.Filter(context)};

auto event_json = boost::json::value_from(event);

Expand Down