From 7e47ee9afb6fc270c38348419cb5d1f75093ad7b Mon Sep 17 00:00:00 2001 From: Sai Kumar Battinoju <88789928+saikumarrs@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:46:18 +0530 Subject: [PATCH] fix: handle consent management configuration fallback for gcm (#4355) --- processor/consent.go | 51 +++-- processor/consent_test.go | 433 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 458 insertions(+), 26 deletions(-) diff --git a/processor/consent.go b/processor/consent.go index d6e7c2464f..1d5af5bf5c 100644 --- a/processor/consent.go +++ b/processor/consent.go @@ -50,43 +50,42 @@ func (proc *Handle) getConsentFilteredDestinations(event types.SingularEventT, d } return lo.Filter(destinations, func(dest backendconfig.DestinationT, _ int) bool { - // This field differentiates legacy and generic consent management - if consentManagementInfo.Provider != "" { - // Generic consent management + // Generic consent management + if cmpData := proc.getGCMData(dest.ID, consentManagementInfo.Provider); len(cmpData.Consents) > 0 { - if cmpData := proc.getGCMData(dest.ID, consentManagementInfo.Provider); len(cmpData.Consents) > 0 { + finalResolutionStrategy := consentManagementInfo.ResolutionStrategy - finalResolutionStrategy := consentManagementInfo.ResolutionStrategy - - // For custom provider, the resolution strategy is to be picked from the destination config - if consentManagementInfo.Provider == "custom" { - finalResolutionStrategy = cmpData.ResolutionStrategy - } + // For custom provider, the resolution strategy is to be picked from the destination config + if consentManagementInfo.Provider == "custom" { + finalResolutionStrategy = cmpData.ResolutionStrategy + } - switch finalResolutionStrategy { - // The user must consent to at least one of the configured consents in the destination - case "or": - return !lo.Every(consentManagementInfo.DeniedConsentIDs, cmpData.Consents) + switch finalResolutionStrategy { + // The user must consent to at least one of the configured consents in the destination + case "or": + return !lo.Every(consentManagementInfo.DeniedConsentIDs, cmpData.Consents) - // The user must consent to all of the configured consents in the destination - default: // "and" - return len(lo.Intersect(cmpData.Consents, consentManagementInfo.DeniedConsentIDs)) == 0 - } + // The user must consent to all of the configured consents in the destination + default: // "and" + return len(lo.Intersect(cmpData.Consents, consentManagementInfo.DeniedConsentIDs)) == 0 } - return true } // Legacy consent management - - // If the destination has oneTrustCookieCategories, returns false if any of the oneTrustCategories are present in deniedCategories - if oneTrustCategories := proc.getOneTrustConsentData(dest.ID); len(oneTrustCategories) > 0 { - return len(lo.Intersect(oneTrustCategories, consentManagementInfo.DeniedConsentIDs)) == 0 + if consentManagementInfo.Provider == "" || consentManagementInfo.Provider == "oneTrust" { + // If the destination has oneTrustCookieCategories, returns false if any of the oneTrustCategories are present in deniedCategories + if oneTrustCategories := proc.getOneTrustConsentData(dest.ID); len(oneTrustCategories) > 0 { + return len(lo.Intersect(oneTrustCategories, consentManagementInfo.DeniedConsentIDs)) == 0 + } } - // If the destination has ketchConsentPurposes, returns false if all ketchPurposes are present in deniedCategories - if ketchPurposes := proc.getKetchConsentData(dest.ID); len(ketchPurposes) > 0 { - return !lo.Every(consentManagementInfo.DeniedConsentIDs, ketchPurposes) + if consentManagementInfo.Provider == "" || consentManagementInfo.Provider == "ketch" { + // If the destination has ketchConsentPurposes, returns false if all ketchPurposes are present in deniedCategories + if ketchPurposes := proc.getKetchConsentData(dest.ID); len(ketchPurposes) > 0 { + return !lo.Every(consentManagementInfo.DeniedConsentIDs, ketchPurposes) + } } + return true }) } diff --git a/processor/consent_test.go b/processor/consent_test.go index 3086bddbb1..11c9fc0942 100644 --- a/processor/consent_test.go +++ b/processor/consent_test.go @@ -137,6 +137,116 @@ func TestFilterDestinations(t *testing.T) { }, }, }, + destinations: []backendconfig.DestinationT{ + { + ID: "destID-1", + Config: map[string]interface{}{ + "consentManagement": map[string]interface{}{ // this should be ignored + "provider": "oneTrust", + "consents": []map[string]interface{}{ + { + "consent": "foo-1", + }, + { + "consent": "foo-2", + }, + }, + }, + "oneTrustCookieCategories": []interface{}{ + map[string]interface{}{}, + }, + }, + }, + { + ID: "destID-2", + Config: map[string]interface{}{ + "consentManagement": map[string]interface{}{ // this should be ignored + "provider": "oneTrust", + "consents": []map[string]interface{}{ + { + "consent": "foo-1", + }, + { + "consent": "foo-2", + }, + }, + }, + "oneTrustCookieCategories": []interface{}{ + map[string]interface{}{ + "oneTrustCookieCategory": "foo-4", + }, + }, + }, + }, + { + ID: "destID-3", + Config: map[string]interface{}{ + "oneTrustCookieCategories": []interface{}{ + map[string]interface{}{ + "oneTrustCookieCategory": "foo-1", + }, + }, + }, + }, + { + ID: "destID-4", + Config: map[string]interface{}{ + "oneTrustCookieCategories": []interface{}{ + map[string]interface{}{ + "oneTrustCookieCategory": "foo-1", + }, + map[string]interface{}{ + "oneTrustCookieCategory": "foo-4", + }, + }, + }, + }, + { + ID: "destID-5", + Config: map[string]interface{}{ + "oneTrustCookieCategories": []interface{}{ + map[string]interface{}{ + "oneTrustCookieCategory": "foo-1", + }, + map[string]interface{}{ + "oneTrustCookieCategory": "foo-2", + }, + map[string]interface{}{ + "oneTrustCookieCategory": "foo-3", + }, + }, + }, + }, + { + ID: "destID-6", + Config: map[string]interface{}{ + "oneTrustCookieCategories": []interface{}{ + map[string]interface{}{ + "oneTrustCookieCategory": "foo-1", + }, + map[string]interface{}{ + "oneTrustCookieCategory": "foo-1", + }, + map[string]interface{}{ + "oneTrustCookieCategory": "foo-1", + }, + }, + }, + }, + }, + expectedDestIDs: []string{"destID-1", "destID-2"}, + }, + { + description: "filter out destination with oneTrustCookieCategories with event containing provider details", + event: types.SingularEventT{ + "context": map[string]interface{}{ + "consentManagement": map[string]interface{}{ + "provider": "oneTrust", // this should be ignored + "resolutionStrategy": "and", // this should be ignored + "deniedConsentIds": []interface{}{"foo-1", "foo-2", "foo-3"}, + }, + }, + }, destinations: []backendconfig.DestinationT{ { ID: "destID-1", @@ -223,6 +333,127 @@ func TestFilterDestinations(t *testing.T) { }, }, }, + destinations: []backendconfig.DestinationT{ + { + ID: "destID-1", + Config: map[string]interface{}{ + "consentManagement": map[string]interface{}{ // this should be ignored + "provider": "ketch", + "consents": []map[string]interface{}{ + { + "consent": "foo-1", + }, + { + "consent": "foo-2", + }, + }, + }, + "ketchConsentPurposes": []interface{}{ + map[string]interface{}{}, + }, + }, + }, + { + ID: "destID-2", + Config: map[string]interface{}{ + "consentManagement": map[string]interface{}{ // this should be ignored + "provider": "ketch", + "consents": []map[string]interface{}{ + { + "consent": "foo-1", + }, + { + "consent": "foo-2", + }, + }, + }, + "ketchConsentPurposes": []interface{}{ + map[string]interface{}{ + "purpose": "foo-4", + }, + }, + }, + }, + { + ID: "destID-3", + Config: map[string]interface{}{ + "ketchConsentPurposes": []interface{}{ + map[string]interface{}{ + "purpose": "foo-1", + }, + }, + }, + }, + { + ID: "destID-4", + Config: map[string]interface{}{ + "consentManagement": map[string]interface{}{ // this should be ignored + "provider": "ketch", + "consents": []map[string]interface{}{ + { + "consent": "foo-1", + }, + { + "consent": "foo-2", + }, + }, + }, + "ketchConsentPurposes": []interface{}{ + map[string]interface{}{ + "purpose": "foo-1", + }, + map[string]interface{}{ + "purpose": "foo-4", + }, + }, + }, + }, + { + ID: "destID-5", + Config: map[string]interface{}{ + "ketchConsentPurposes": []interface{}{ + map[string]interface{}{ + "purpose": "foo-1", + }, + map[string]interface{}{ + "purpose": "foo-2", + }, + map[string]interface{}{ + "purpose": "foo-3", + }, + }, + }, + }, + { + ID: "destID-6", + Config: map[string]interface{}{ + "ketchConsentPurposes": []interface{}{ + map[string]interface{}{ + "purpose": "foo-1", + }, + map[string]interface{}{ + "purpose": "foo-1", + }, + map[string]interface{}{ + "purpose": "foo-1", + }, + }, + }, + }, + }, + expectedDestIDs: []string{"destID-1", "destID-2", "destID-4"}, + }, + { + description: "filter out destination with ketchConsentPurposes with event containing provider details", + event: types.SingularEventT{ + "context": map[string]interface{}{ + "consentManagement": map[string]interface{}{ + "provider": "ketch", // this should be ignored + "resolutionStrategy": "or", // this should be ignored + "deniedConsentIds": []interface{}{"foo-1", "foo-2", "foo-3"}, + }, + }, + }, destinations: []backendconfig.DestinationT{ { ID: "destID-1", @@ -398,6 +629,77 @@ func TestFilterDestinations(t *testing.T) { }, expectedDestIDs: []string{"destID-1", "destID-2", "destID-4"}, }, + { + description: "filter out destination when generic consent management is unavailable and falls back to legacy consents (Ketch)", + event: types.SingularEventT{ + "context": map[string]interface{}{ + "consentManagement": map[string]interface{}{ + "provider": "ketch", + "resolutionStrategy": "or", + "deniedConsentIds": []interface{}{"foo-1", "foo-2", "foo-3"}, + }, + }, + }, + destinations: []backendconfig.DestinationT{ + { + ID: "destID-1", + Config: map[string]interface{}{ + "consentManagement": []interface{}{ + map[string]interface{}{ + "provider": "custom", + "consents": []map[string]interface{}{ + {}, + }, + }, + }, + "ketchConsentPurposes": []interface{}{ // should fallback to this + map[string]interface{}{ + "purpose": "foo-5", + }, + }, + }, + }, + { + ID: "destID-2", + Config: map[string]interface{}{ + "ketchConsentPurposes": []interface{}{ // should fallback to this + map[string]interface{}{ + "purpose": "foo-4", + }, + }, + }, + }, + { + ID: "destID-3", + Config: map[string]interface{}{ + "oneTrustCookieCategories": []interface{}{ // should not fallback to this just because ketch and gcm is unavailable + map[string]interface{}{ + "oneTrustCookieCategory": "foo-1", + }, + map[string]interface{}{ + "oneTrustCookieCategory": "foo-2", + }, + }, + }, + }, + { + ID: "destID-4", + Config: map[string]interface{}{ + "consentManagement": []interface{}{ + map[string]interface{}{ + "provider": "ketch", + "consents": []map[string]interface{}{ + {"consent": "foo-1"}, + {"consent": "foo-1"}, + {"consent": "foo-1"}, + }, + }, + }, + }, + }, + }, + expectedDestIDs: []string{"destID-1", "destID-2", "destID-3"}, + }, { description: "filter out destination with generic consent management (OneTrust)", event: types.SingularEventT{ @@ -496,6 +798,74 @@ func TestFilterDestinations(t *testing.T) { }, expectedDestIDs: []string{"destID-1", "destID-2"}, }, + { + description: "filter out destination when generic consent management is unavailable and falls back to legacy consents (OneTrust)", + event: types.SingularEventT{ + "context": map[string]interface{}{ + "consentManagement": map[string]interface{}{ + "provider": "oneTrust", + "resolutionStrategy": "and", + "deniedConsentIds": []interface{}{"foo-1", "foo-2", "foo-3"}, + }, + }, + }, + destinations: []backendconfig.DestinationT{ + { + ID: "destID-1", + Config: map[string]interface{}{ + "consentManagement": []interface{}{ + map[string]interface{}{ + "provider": "ketch", + "consents": []map[string]interface{}{ + {}, + }, + }, + }, + "oneTrustCookieCategories": []interface{}{ // should fallback to this + map[string]interface{}{ + "oneTrustCookieCategory": "foo-5", + }, + }, + }, + }, + { + ID: "destID-2", + Config: map[string]interface{}{ + "oneTrustCookieCategories": []interface{}{ // should fallback to this + map[string]interface{}{ + "oneTrustCookieCategory": "foo-4", + }, + }, + }, + }, + { + ID: "destID-3", + Config: map[string]interface{}{ + "ketchConsentPurposes": []interface{}{ // should not fallback to this just because oneTrust and gcm is unavailable + map[string]interface{}{ + "purpose": "foo-1", + }, + }, + }, + }, + { + ID: "destID-6", + Config: map[string]interface{}{ + "consentManagement": []interface{}{ + map[string]interface{}{ + "provider": "oneTrust", + "consents": []map[string]interface{}{ + {"consent": "foo-1"}, + {"consent": "foo-1"}, + {"consent": "foo-1"}, + }, + }, + }, + }, + }, + }, + expectedDestIDs: []string{"destID-1", "destID-2", "destID-3"}, + }, { description: "filter out destination with generic consent management (Custom - AND)", event: types.SingularEventT{ @@ -703,6 +1073,40 @@ func TestFilterDestinations(t *testing.T) { }, expectedDestIDs: []string{"destID-1", "destID-2", "destID-4"}, }, + { + description: "filter out destination when generic consent management (Custom) is unavailable but doesn't falls back to legacy consents", + event: types.SingularEventT{ + "context": map[string]interface{}{ + "consentManagement": map[string]interface{}{ + "provider": "custom", + "resolutionStrategy": "or", + "deniedConsentIds": []interface{}{"foo-1", "foo-2", "foo-3"}, + }, + }, + }, + destinations: []backendconfig.DestinationT{ + { + ID: "destID-1", + Config: map[string]interface{}{ + "consentManagement": []interface{}{ + map[string]interface{}{ + "provider": "ketch", + "consents": []map[string]interface{}{ + { + "consent": "foo-1", + }, + }, + }, + }, + }, + }, + { + ID: "destID-2", + Config: map[string]interface{}{}, + }, + }, + expectedDestIDs: []string{"destID-1", "destID-2"}, + }, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { @@ -899,6 +1303,35 @@ func TestGetConsentManagementInfo(t *testing.T) { ResolutionStrategy: "and", }, }, + { + description: "should return consent management info when consent management data is sent from older SDKs", + input: types.SingularEventT{ + "anonymousId": "123", + "type": "track", + "event": "test", + "properties": map[string]interface{}{ + "category": "test", + }, + "context": map[string]interface{}{ + "consentManagement": map[string]interface{}{ + "deniedConsentIds": []string{ + "consent category 3", + "", + "consent category 4", + "", + }, + }, + }, + }, + expected: ConsentManagementInfo{ + Provider: "", + DeniedConsentIDs: []string{ + "consent category 3", + "consent category 4", + }, + ResolutionStrategy: "", + }, + }, } for _, testCase := range testCases {