From ee736d034a5d92e9a25b4c9a8d29a84370ffa22c Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 22 Jan 2024 11:31:58 -0500 Subject: [PATCH 1/4] feat: redact anonymous attributes within feature events --- .../client-contract-tests/src/main.cpp | 1 + .../server-contract-tests/src/main.cpp | 1 + .../include/launchdarkly/context_filter.hpp | 30 +++++++++-- libs/internal/src/context_filter.cpp | 40 ++++++++++---- .../src/events/asio_event_processor.cpp | 3 +- libs/internal/tests/context_filter_test.cpp | 53 +++++++++++++++++++ .../tests/event_serialization_test.cpp | 2 +- 7 files changed, 114 insertions(+), 16 deletions(-) diff --git a/contract-tests/client-contract-tests/src/main.cpp b/contract-tests/client-contract-tests/src/main.cpp index 5ab6d34b6..f52590d11 100644 --- a/contract-tests/client-contract-tests/src/main.cpp +++ b/contract-tests/client-contract-tests/src/main.cpp @@ -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}; diff --git a/contract-tests/server-contract-tests/src/main.cpp b/contract-tests/server-contract-tests/src/main.cpp index 1f0c1c0b9..0cd11d688 100644 --- a/contract-tests/server-contract-tests/src/main.cpp +++ b/contract-tests/server-contract-tests/src/main.cpp @@ -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}; diff --git a/libs/internal/include/launchdarkly/context_filter.hpp b/libs/internal/include/launchdarkly/context_filter.hpp index 5580ff94f..38a44ba50 100644 --- a/libs/internal/include/launchdarkly/context_filter.hpp +++ b/libs/internal/include/launchdarkly/context_filter.hpp @@ -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 @@ -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. @@ -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& redactions, std::vector path, - Attributes const& attributes); + Attributes const& attributes, + bool redactAll); /** * Append a container to the parent. @@ -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_; diff --git a/libs/internal/src/context_filter.cpp b/libs/internal/src/context_filter.cpp index 78b75c494..92a80eb06 100644 --- a/libs/internal/src/context_filter.cpp +++ b/libs/internal/src/context_filter.cpp @@ -12,14 +12,25 @@ 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]; + auto kind = context.Kinds()[0]; return FilterSingleContext(kind, INCLUDE_KIND, - context.Attributes(kind)); + context.Attributes(kind.data()), + redactAnonymous); } - return FilterMultiContext(context); + return FilterMultiContext(context, redactAnonymous); } void ContextFilter::Emplace(ContextFilter::StackItem& item, @@ -33,8 +44,9 @@ void ContextFilter::Emplace(ContextFilter::StackItem& item, bool ContextFilter::Redact(std::vector& redactions, std::vector path, - Attributes const& attributes) { - if (all_attributes_private_) { + Attributes const& attributes, + bool redactAll) { + if (redactAll) { redactions.push_back(AttributeReference::PathToStringReference(path)); return true; } @@ -61,7 +73,8 @@ bool ContextFilter::Redact(std::vector& redactions, ContextFilter::JsonValue ContextFilter::FilterSingleContext( std::string_view kind, bool include_kind, - Attributes const& attributes) { + Attributes const& attributes, + bool redactAnonymous) { std::vector stack; JsonValue filtered = JsonObject(); std::vector redactions; @@ -74,9 +87,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{"name"}, - attributes)) { + !Redact(redactions, std::vector{"name"}, attributes, + redactAll)) { filtered.as_object().insert_or_assign("name", attributes.Name()); } @@ -90,7 +105,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; } @@ -170,14 +186,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; diff --git a/libs/internal/src/events/asio_event_processor.cpp b/libs/internal/src/events/asio_event_processor.cpp index 4050df977..e9cf2b525 100644 --- a/libs/internal/src/events/asio_event_processor.cpp +++ b/libs/internal/src/events/asio_event_processor.cpp @@ -255,7 +255,8 @@ std::vector AsioEventProcessor::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) { diff --git a/libs/internal/tests/context_filter_test.cpp b/libs/internal/tests/context_filter_test.cpp index 771b4c0a1..1f28455b9 100644 --- a/libs/internal/tests/context_filter_test.cpp +++ b/libs/internal/tests/context_filter_test.cpp @@ -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") + .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); +} diff --git a/libs/internal/tests/event_serialization_test.cpp b/libs/internal/tests/event_serialization_test.cpp index 053238f40..8ed4c76ac 100644 --- a/libs/internal/tests/event_serialization_test.cpp +++ b/libs/internal/tests/event_serialization_test.cpp @@ -28,7 +28,7 @@ TEST(EventSerialization, FeatureEvent) { std::nullopt, }), - filter.filter(context)}; + filter.Filter(context)}; auto event_json = boost::json::value_from(event); From dfe470985c43723fc0659bbf0e22a1c09356b004 Mon Sep 17 00:00:00 2001 From: "Matthew M. Keeler" Date: Mon, 29 Jan 2024 10:37:49 -0500 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Casey Waldren --- libs/internal/include/launchdarkly/context_filter.hpp | 2 +- libs/internal/src/context_filter.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/internal/include/launchdarkly/context_filter.hpp b/libs/internal/include/launchdarkly/context_filter.hpp index 38a44ba50..5b29afcd0 100644 --- a/libs/internal/include/launchdarkly/context_filter.hpp +++ b/libs/internal/include/launchdarkly/context_filter.hpp @@ -60,7 +60,7 @@ class ContextFilter { }; /** - * Internal delegate function for Filter_and FilterWithAnonymousRedaction. + * Internal delegate function for Filter and FilterWithAnonymousRedaction. * * @param context The context to redact. * @param redactAnonymous Whether to redact all attributes from anonymous diff --git a/libs/internal/src/context_filter.cpp b/libs/internal/src/context_filter.cpp index 92a80eb06..22fd5fa1e 100644 --- a/libs/internal/src/context_filter.cpp +++ b/libs/internal/src/context_filter.cpp @@ -25,7 +25,7 @@ ContextFilter::JsonValue ContextFilter::Filter(Context const& context, // Context should be validated before calling this method. assert(context.Valid()); if (context.Kinds().size() == 1) { - auto kind = context.Kinds()[0]; + std::string const& kind = context.Kinds()[0]; return FilterSingleContext(kind, INCLUDE_KIND, context.Attributes(kind.data()), redactAnonymous); From 8dd34987c2f8897aebb4b8ef64cc605f122fe08d Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 29 Jan 2024 10:38:13 -0500 Subject: [PATCH 3/4] Code review suggestions --- libs/internal/src/context_filter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/internal/src/context_filter.cpp b/libs/internal/src/context_filter.cpp index 22fd5fa1e..7660335d5 100644 --- a/libs/internal/src/context_filter.cpp +++ b/libs/internal/src/context_filter.cpp @@ -27,7 +27,7 @@ ContextFilter::JsonValue ContextFilter::Filter(Context const& context, if (context.Kinds().size() == 1) { std::string const& kind = context.Kinds()[0]; return FilterSingleContext(kind, INCLUDE_KIND, - context.Attributes(kind.data()), + context.Attributes(kind), redactAnonymous); } return FilterMultiContext(context, redactAnonymous); From 0111ddf362e3490311a5aa4efdf4680d649bef42 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 29 Jan 2024 11:38:08 -0800 Subject: [PATCH 4/4] clang format --- libs/internal/src/context_filter.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/internal/src/context_filter.cpp b/libs/internal/src/context_filter.cpp index 7660335d5..9ad9e50eb 100644 --- a/libs/internal/src/context_filter.cpp +++ b/libs/internal/src/context_filter.cpp @@ -26,8 +26,7 @@ ContextFilter::JsonValue ContextFilter::Filter(Context const& context, 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, redactAnonymous);