diff --git a/src/main/java/org/prebid/server/auction/gpp/model/GppModelWrapper.java b/src/main/java/org/prebid/server/auction/gpp/model/GppModelWrapper.java index e8f7d4e008b..1bde141d72f 100644 --- a/src/main/java/org/prebid/server/auction/gpp/model/GppModelWrapper.java +++ b/src/main/java/org/prebid/server/auction/gpp/model/GppModelWrapper.java @@ -17,7 +17,33 @@ public class GppModelWrapper extends GppModel { private IntObjectMap sectionIdToEncodedString; public GppModelWrapper(String encodedString) throws DecodingException { - super(encodedString); + super(padSections(encodedString)); + } + + private static String padSections(String gpp) { + final StringBuilder gppBuilder = new StringBuilder(gpp); + + int subsectionStart = 0; + int offset = 0; + for (int i = 1; i < gpp.length(); i++) { + final char currentChar = gpp.charAt(i); + + if (currentChar == '~' || currentChar == '.') { + if ((i - subsectionStart) % 4 != 0 && gpp.charAt(i - 1) != '=') { + gppBuilder.insert(i + offset, "A"); + offset++; + } + + subsectionStart = i + 1; + } + } + + final int lastSubsectionLength = gpp.length() - subsectionStart; + if (lastSubsectionLength > 0 && lastSubsectionLength % 4 != 0 && !gpp.endsWith("=")) { + gppBuilder.append("A"); + } + + return gppBuilder.toString(); } private void init() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy index a6d9d6e7aad..657e0fef1e5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy @@ -483,6 +483,39 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS cookie sync call when privacy module contain invalid GPP string should exclude bidders URLs"() { + given: "Cookie sync request with link to account" + def accountId = PBSUtils.randomString + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = US_NAT_V1.value + it.account = accountId + it.gpp = INVALID_GPP_STRING + } + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with cookie sync and privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should not contain any URLs for bidders" + assert !response.bidderStatus.userSync.url + + and: "Response should not contain any warning" + assert !response.warnings + } + def "PBS cookie sync call when request have different gpp consent but match and rejecting should exclude bidders URLs"() { given: "Cookie sync request with link to account" def accountId = PBSUtils.randomString @@ -1326,6 +1359,41 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS setuid request when privacy module contain invalid GPP string should reject bidders with status code invalidStatusCode"() { + given: "Cookie sync SetuidRequest with accountId" + def accountId = PBSUtils.randomString + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.gpp = INVALID_GPP_STRING + } + + and: "UIDS Cookie" + def uidsCookie = UidsCookie.defaultUidsCookie + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with cookie sync and allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes cookie sync request" + activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == INVALID_STATUS_CODE + assert exception.responseBody == INVALID_STATUS_MESSAGE + } + def "PBS setuid request when request have different gpp consent but match and rejecting should reject bidders with status code invalidStatusCode"() { given: "Cookie sync SetuidRequest with accountId" def accountId = PBSUtils.randomString diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy index 73dfda85a23..edd720700ca 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy @@ -77,6 +77,7 @@ import static org.prebid.server.functional.model.request.auction.PrivacyModule.I import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ONTARIO @@ -842,6 +843,45 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS auction call when privacy module contain invalid GPP string should remove EIDS fields in request"() { + given: "Default Generic BidRequests with EIDS fields and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = INVALID_GPP_STRING + } + + and: "Activities set for transmitEIDS with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have empty EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll { + !genericBidderRequest.user.eids + !genericBidderRequest.user?.ext?.eids + } + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors + } + def "PBS auction call when request have different gpp consent but match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequests with EIDS fields and account id" def accountId = PBSUtils.randomNumber as String @@ -1830,6 +1870,55 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS amp call when privacy module contain invalid GPP string should remove EIDS fields in request"() { + given: "Default Generic BidRequest with EIDS fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = INVALID_GPP_STRING + it.consentType = GPP + } + + and: "Activities set for transmitEIDS with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have empty EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + !genericBidderRequest.user.eids + !genericBidderRequest.user?.ext?.eids + } + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain amp error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == ["Amp request parameter consent_string has invalid format: $INVALID_GPP_STRING"] + } + def "PBS amp call when request have different gpp consent but match and rejecting should remove EIDS fields in request"() { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy index 920196ea576..0eaafecc6f0 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy @@ -15,16 +15,10 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.Activity import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities -import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition -import org.prebid.server.functional.model.request.auction.Data import org.prebid.server.functional.model.request.auction.Device -import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo import org.prebid.server.functional.model.request.auction.RegsExt -import org.prebid.server.functional.model.request.auction.User -import org.prebid.server.functional.model.request.auction.UserExt -import org.prebid.server.functional.model.request.auction.UserExtData import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -87,6 +81,7 @@ import static org.prebid.server.functional.model.request.auction.PrivacyModule.I import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_GENERAL import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ONTARIO @@ -1114,6 +1109,59 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS auction call when privacy module contain invalid GPP string should remove UFPD fields in request"() { + given: "Default Generic BidRequests with UFPD fields and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = INVALID_GPP_STRING + } + + and: "Activities set for transmitUfpd with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def response= activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have empty UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll { + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.ext + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors + } + def "PBS auction call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String @@ -2387,6 +2435,69 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { ] } + def "PBS amp call when privacy module contain invalid GPP string should remove UFPD fields in request"() { + given: "Default Generic BidRequest with UFPD fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = INVALID_GPP_STRING + it.consentType = GPP + } + + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have empty UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + !bidderRequest.device.didsha1 + !bidderRequest.device.didmd5 + !bidderRequest.device.dpidsha1 + !bidderRequest.device.ifa + !bidderRequest.device.macsha1 + !bidderRequest.device.macmd5 + !bidderRequest.device.dpidmd5 + !bidderRequest.user.id + !bidderRequest.user.buyeruid + !bidderRequest.user.yob + !bidderRequest.user.gender + !bidderRequest.user.data + !bidderRequest.user.ext + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain amp error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == ["Amp request parameter consent_string has invalid format: $INVALID_GPP_STRING"] + } + def "PBS amp call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index ed633ec5316..aff17c4d49b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -95,6 +95,7 @@ abstract class PrivacyBaseSpec extends BaseSpec { private static final Map GDPR_EEA_COUNTRY = ["gdpr.eea-countries": "$BULGARIA.ISOAlpha2, SK, VK" as String] protected static final String VENDOR_LIST_PATH = "/app/prebid-server/data/vendorlist-v{VendorVersion}/{VendorVersion}.json" + protected static final String INVALID_GPP_STRING = "DBABLA~BVQqAAAAAg.YA" // TODO replace BVQqAAAAAg with ${PBSUtils.getRandomString(7)} when proper fix is ready protected static final String VALID_VALUE_FOR_GPC_HEADER = "1" protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UsNatV1Consent.Builder().setGpc(true).build() protected static final VendorList vendorListResponse = new VendorList(networkServiceContainer) diff --git a/src/test/java/org/prebid/server/auction/gpp/model/GppContextCreatorTest.java b/src/test/java/org/prebid/server/auction/gpp/model/GppContextCreatorTest.java index dd5efdbe4c2..2d977ac65e7 100644 --- a/src/test/java/org/prebid/server/auction/gpp/model/GppContextCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/gpp/model/GppContextCreatorTest.java @@ -37,7 +37,7 @@ public void fromShouldReturnGppContextWrapperWithErrorOnInvalidGpp() { assertThat(gppContext.regions()).isEqualTo(GppContext.Regions.builder().build()); }); assertThat(gppContextWrapper.getErrors()) - .containsExactly("GPP string invalid: Unable to decode 'invalid'"); + .containsExactly("GPP string invalid: Unable to decode 'invalidA'"); } @Test diff --git a/src/test/java/org/prebid/server/auction/gpp/model/GppModelWrapperTest.java b/src/test/java/org/prebid/server/auction/gpp/model/GppModelWrapperTest.java index 587721151d5..4f921fae0c3 100644 --- a/src/test/java/org/prebid/server/auction/gpp/model/GppModelWrapperTest.java +++ b/src/test/java/org/prebid/server/auction/gpp/model/GppModelWrapperTest.java @@ -5,12 +5,16 @@ import com.iab.gpp.encoder.error.EncodingException; import com.iab.gpp.encoder.section.HeaderV1; import com.iab.gpp.encoder.section.TcfEuV2; +import com.iab.gpp.encoder.section.UsNat; import com.iab.gpp.encoder.section.UspV1; import org.junit.jupiter.api.Test; import java.util.Comparator; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; public class GppModelWrapperTest { @@ -35,7 +39,7 @@ public class GppModelWrapperTest { + "1YN-"; @Test - public void test() throws DecodingException, EncodingException { + public void wrapperShouldStoreSomeOfOriginalSections() throws DecodingException, EncodingException { // given and when final GppModel originalGpp = new GppModel(GPP_STRING); final GppModel wrappedGpp = new GppModelWrapper(GPP_STRING); @@ -48,6 +52,73 @@ public void test() throws DecodingException, EncodingException { assertThat(wrappedGpp.encodeSection(UspV1.ID)).isEqualTo(originalGpp.encodeSection(UspV1.ID)); } + @Test + public void wrapperShouldPadSectionsIfNeeded() { + // given + final List samples = List.of( + "DBABLA~BVQqAAAAAg", + "DBABLA~BVVqCAAACg", + "DBABLA~BVVVBAAABg", + "DBABLA~BVVqCACACg", + "DBABLA~BVQVAAAAAg", + "DBABLA~BVVVBABABg"); + + for (String sample : samples) { + // when + final GppModel originalGpp = new GppModel(sample); + final GppModel wrappedGpp = new GppModelWrapper(sample); + + // then + assertThatExceptionOfType(DecodingException.class) + .isThrownBy(() -> originalGpp.getUsNatSection().getMspaCoveredTransaction()); + assertThatNoException() + .isThrownBy(() -> wrappedGpp.getUsNatSection().getMspaCoveredTransaction()); + } + } + + @Test + public void wrapperShouldNotModifyValidBase64SubsectionsWithPadChars() { + // given + final String gpp = "DBABLA~BVVVQAAARlA=.QA=="; + + // when + final GppModel wrappedGpp = new GppModelWrapper(gpp); + + // then + assertThat(wrappedGpp.encodeSection(UsNat.ID)).isEqualTo("BVVVQAAARlA=.QA=="); + } + + @Test + public void wrapperShouldNotModifyValidBase64SubsectionsWithoutPadChars() { + // given + final String gpp = "DBABLA~CqqqgAAAAIJo.YA=="; + + // when + final GppModel wrappedGpp = new GppModelWrapper(gpp); + + // then + assertThat(wrappedGpp.encodeSection(UsNat.ID)).isEqualTo("CqqqgAAAAIJo.YA=="); + assertThatNoException() + .isThrownBy(() -> wrappedGpp.getUsNatSection().getMspaCoveredTransaction()); + } + + @Test + public void wrapperShouldPadSubsections() { + // given + final String gpp = "DBABLA~BVVVQAAARl.Q"; + + // when + final GppModel originalGpp = new GppModel(gpp); + final GppModel wrappedGpp = new GppModelWrapper(gpp); + + // then + assertThat(wrappedGpp.encodeSection(UsNat.ID)).isEqualTo("BVVVQAAARlA.QA"); + assertThatExceptionOfType(DecodingException.class) + .isThrownBy(() -> originalGpp.getUsNatSection().getMspaCoveredTransaction()); + assertThatNoException() + .isThrownBy(() -> wrappedGpp.getUsNatSection().getMspaCoveredTransaction()); + } + public static String normalizeEncodedTcfEuV2Section(String encodedSection) { try { final GppModel normalizer = new GppModel();