diff --git a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java index 0fb3bebe38b..bc0372c9773 100644 --- a/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/PrivacyEnforcementService.java @@ -8,6 +8,7 @@ import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.User; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; @@ -27,6 +28,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.util.HttpUtil; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; @@ -101,9 +103,11 @@ Future> mask(AuctionContext auctionContext, final AccountGdprConfig accountConfig = auctionContext.getAccount().getGdpr(); final Timeout timeout = auctionContext.getTimeout(); final MetricName requestType = auctionContext.getRequestTypeMetric(); + final MultiMap headers = auctionContext.getRoutingContext().request().headers(); return getBidderToEnforcementAction(device, bidders, aliases, extUser, regs, accountConfig, timeout) - .map(bidderToEnforcement -> updatePrivacyMetrics(bidderToEnforcement, requestType, device)) - .map(bidderToEnforcement -> getBidderToPrivacyResult(bidderToUser, device, bidderToEnforcement)); + .map(bidderToEnforcement -> updatePrivacyMetrics(bidderToEnforcement, requestType, device, headers)) + .map(bidderToEnforcement -> getBidderToPrivacyResult(bidderToUser, device, + bidderToEnforcement, headers)); } public boolean isCcpaEnforced(Ccpa ccpa) { @@ -259,7 +263,8 @@ private void updateCcpaMetrics(Ccpa ccpa) { } private Map updatePrivacyMetrics( - Map bidderToEnforcement, MetricName requestType, Device device) { + Map bidderToEnforcement, MetricName requestType, + Device device, MultiMap headers) { for (final Map.Entry bidderEnforcement : bidderToEnforcement.entrySet()) { final String bidder = bidderEnforcement.getKey(); @@ -278,6 +283,10 @@ private Map updatePrivacyMetrics( metrics.updatePrivacyLmtMetric(); } + if (isDntEnabled(device, headers.get(HttpUtil.DNT_HEADER))) { + metrics.updatePrivacyDntMetric(); + } + return bidderToEnforcement; } @@ -286,23 +295,21 @@ private Map updatePrivacyMetrics( * {@link BidderPrivacyResult}. Masking depends on GDPR and COPPA. */ private List getBidderToPrivacyResult( - Map bidderToUser, Device device, Map bidderToEnforcement) { - - final boolean isLmtEnabled = isLmtEnabled(device); + Map bidderToUser, Device device, Map bidderToEnforcement, + MultiMap headers) { + final String dntHeader = headers.get(HttpUtil.DNT_HEADER); return bidderToUser.entrySet().stream() .map(bidderUserEntry -> createBidderPrivacyResult(bidderUserEntry.getValue(), device, - bidderUserEntry.getKey(), isLmtEnabled, bidderToEnforcement)) + bidderUserEntry.getKey(), bidderToEnforcement, dntHeader)) .collect(Collectors.toList()); } /** * Returns {@link BidderPrivacyResult} with GDPR masking. */ - private BidderPrivacyResult createBidderPrivacyResult(User user, - Device device, - String bidder, - boolean isLmtEnabled, - Map bidderToEnforcement) { + private BidderPrivacyResult createBidderPrivacyResult(User user, Device device, String bidder, + Map bidderToEnforcement, + String dntHeader) { final PrivacyEnforcementAction privacyEnforcementAction = bidderToEnforcement.get(bidder); final boolean blockBidderRequest = privacyEnforcementAction.isBlockBidderRequest(); @@ -314,13 +321,16 @@ private BidderPrivacyResult createBidderPrivacyResult(User user, .blockedAnalyticsByTcf(blockAnalyticsReport) .build(); } + final boolean isLmtEnabled = isLmtEnabled(device); + final boolean isDntEnabled = isDntEnabled(device, dntHeader); - final boolean maskGeo = privacyEnforcementAction.isMaskGeo() || isLmtEnabled; - final boolean maskUserIds = privacyEnforcementAction.isRemoveUserIds() || isLmtEnabled; + boolean isLmtOrDntEnabled = isLmtEnabled || isDntEnabled; + final boolean maskGeo = privacyEnforcementAction.isMaskGeo() || isLmtOrDntEnabled; + final boolean maskUserIds = privacyEnforcementAction.isRemoveUserIds() || isLmtOrDntEnabled; final User maskedUser = maskTcfUser(user, maskUserIds, maskGeo); - final boolean maskIp = privacyEnforcementAction.isMaskDeviceIp() || isLmtEnabled; - final boolean maskInfo = privacyEnforcementAction.isMaskDeviceInfo() || isLmtEnabled; + final boolean maskIp = privacyEnforcementAction.isMaskDeviceIp() || isLmtOrDntEnabled; + final boolean maskInfo = privacyEnforcementAction.isMaskDeviceInfo() || isLmtOrDntEnabled; final Device maskedDevice = maskTcfDevice(device, maskIp, maskGeo, maskInfo); return BidderPrivacyResult.builder() @@ -463,4 +473,11 @@ private static String maskIp(String ip, String delimiter, int groups) { private static boolean isLmtEnabled(Device device) { return device != null && Objects.equals(device.getLmt(), 1); } + + private boolean isDntEnabled(Device device, String dntHeader) { + boolean deviceDntEnabled = device != null && Objects.equals(device.getDnt(), 1); + boolean headerDntEnabled = dntHeader != null && Objects.equals(dntHeader, "1"); + return deviceDntEnabled || headerDntEnabled; + } + } diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 505eb261ef3..9f5fd706eea 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -5,6 +5,7 @@ import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; +import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; @@ -118,6 +119,16 @@ private static Set activeBidders(BidderCatalog bidderCatalog) { public void handle(RoutingContext context) { metrics.updateCookieSyncRequestMetric(); + final MultiMap requestHeaders = context.request().headers(); + if (requestHeaders != null && requestHeaders.contains(HttpUtil.DNT_HEADER) + && Objects.equals(requestHeaders.get(HttpUtil.DNT_HEADER), "1")) { + final int status = HttpResponseStatus.NO_CONTENT.code(); + final String message = "Do-Not-Track is enabled"; + context.response().setStatusCode(status).setStatusMessage(message).end(); + analyticsReporter.processEvent(CookieSyncEvent.error(status, message)); + return; + } + final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(context); if (!uidsCookie.allowsSync()) { final int status = HttpResponseStatus.UNAUTHORIZED.code(); diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index eb236594ce9..0e058294324 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -4,6 +4,7 @@ import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; +import io.vertx.core.MultiMap; import io.vertx.core.http.Cookie; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; @@ -80,6 +81,15 @@ public SetuidHandler(long defaultTimeout, UidsCookieService uidsCookieService, @Override public void handle(RoutingContext context) { + final MultiMap requestHeaders = context.request().headers(); + if (requestHeaders != null && requestHeaders.contains(HttpUtil.DNT_HEADER) + && Objects.equals(requestHeaders.get(HttpUtil.DNT_HEADER), "1")) { + int status = HttpResponseStatus.NO_CONTENT.code(); + respondWith(context, status, "Do-Not-Track is enabled"); + analyticsReporter.processEvent(SetuidEvent.error(status)); + return; + } + final UidsCookie uidsCookie = uidsCookieService.parseFromRequest(context); if (!uidsCookie.allowsSync()) { final int status = HttpResponseStatus.UNAUTHORIZED.code(); diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index dee25f05190..4c57cd293a5 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -76,6 +76,7 @@ public enum MetricName { // privacy coppa, lmt, + dnt, specified, opt_out("opt-out"), invalid, diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 872134b5fe2..7792ca0298b 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -302,6 +302,10 @@ public void updatePrivacyLmtMetric() { privacy().incCounter(MetricName.lmt); } + public void updatePrivacyDntMetric() { + privacy().incCounter(MetricName.dnt); + } + public void updatePrivacyCcpaMetrics(boolean isSpecified, boolean isEnforced) { if (isSpecified) { privacy().usp().incCounter(MetricName.specified); diff --git a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java index 9093c2925a2..bc032198f8c 100644 --- a/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/PrivacyEnforcementServiceTest.java @@ -7,6 +7,9 @@ import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.User; import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.ext.web.RoutingContext; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -32,6 +35,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUserEid; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; import org.prebid.server.settings.model.Account; +import org.prebid.server.util.HttpUtil; import java.time.Clock; import java.time.Instant; @@ -74,6 +78,12 @@ public class PrivacyEnforcementServiceTest extends VertxTest { private TcfDefinerService tcfDefinerService; @Mock private Metrics metrics; + @Mock + private RoutingContext routingContext; + @Mock + private HttpServerRequest httpServerRequest; + @Mock + private MultiMap headers; private PrivacyEnforcementService privacyEnforcementService; @@ -81,6 +91,8 @@ public class PrivacyEnforcementServiceTest extends VertxTest { @Before public void setUp() { + given(routingContext.request()).willReturn(httpServerRequest); + given(httpServerRequest.headers()).willReturn(headers); given(tcfDefinerService.resultForBidderNames(anySet(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture( TcfResponse.of(true, singletonMap(BIDDER_NAME, restrictDeviceAndUser()), null))); @@ -125,6 +137,83 @@ public void shouldMaskForCoppaWhenDeviceLmtIsOneAndRegsCoppaIsOneAndDoesNotCallT verifyZeroInteractions(tcfDefinerService); } + @Test + public void shouldMaskForTcfWhenTcfServiceAllowAllAndDeviceDntIsOne() { + given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any(), any(), any(), any())) + .willReturn(Future.succeededFuture( + TcfResponse.of(true, singletonMap(BIDDER_NAME, PrivacyEnforcementAction.allowAll()), null))); + + final ExtUser extUser = ExtUser.builder().build(); + final User user = notMaskedUser(); + final Device device = givenNotMaskedDevice(deviceBuilder -> deviceBuilder.dnt(1)); + final Regs regs = Regs.of(0, null); + final Map bidderToUser = singletonMap(BIDDER_NAME, user); + + final BidRequest bidRequest = givenBidRequest(givenSingleImp( + singletonMap(BIDDER_NAME, 1)), + bidRequestBuilder -> bidRequestBuilder + .user(user) + .device(device) + .regs(regs)); + + final AuctionContext context = auctionContext(bidRequest); + + // when + final List result = privacyEnforcementService + .mask(context, bidderToUser, extUser, singletonList(BIDDER_NAME), BidderAliases.of(null, null)) + .result(); + + // then + final BidderPrivacyResult expectedBidderPrivacy = BidderPrivacyResult.builder() + .user(userTcfMasked()) + .device(givenTcfMaskedDevice(deviceBuilder -> deviceBuilder.dnt(1))) + .requestBidder(BIDDER_NAME) + .build(); + assertThat(result).containsOnly(expectedBidderPrivacy); + + verify(tcfDefinerService) + .resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), isNull(), any(), any(), any(), eq(timeout)); + } + + @Test + public void shouldMaskForTcfWhenTcfServiceAllowAllAndDntHeaderIsOne() { + given(tcfDefinerService.resultForBidderNames(any(), any(), any(), any(), any(), any(), any())) + .willReturn(Future.succeededFuture( + TcfResponse.of(true, singletonMap(BIDDER_NAME, PrivacyEnforcementAction.allowAll()), null))); + + final ExtUser extUser = ExtUser.builder().build(); + final User user = notMaskedUser(); + final Device device = notMaskedDevice(); + final Regs regs = Regs.of(0, null); + final Map bidderToUser = singletonMap(BIDDER_NAME, user); + + final BidRequest bidRequest = givenBidRequest(givenSingleImp( + singletonMap(BIDDER_NAME, 1)), + bidRequestBuilder -> bidRequestBuilder + .user(user) + .device(device) + .regs(regs)); + + given(headers.get(HttpUtil.DNT_HEADER)).willReturn("1"); + final AuctionContext context = auctionContext(bidRequest); + + // when + final List result = privacyEnforcementService + .mask(context, bidderToUser, extUser, singletonList(BIDDER_NAME), BidderAliases.of(null, null)) + .result(); + + // then + final BidderPrivacyResult expectedBidderPrivacy = BidderPrivacyResult.builder() + .user(userTcfMasked()) + .device(deviceTcfMasked()) + .requestBidder(BIDDER_NAME) + .build(); + assertThat(result).containsOnly(expectedBidderPrivacy); + + verify(tcfDefinerService) + .resultForBidderNames(eq(singleton(BIDDER_NAME)), any(), isNull(), any(), any(), any(), eq(timeout)); + } + @Test public void shouldMaskForCcpaWhenUsPolicyIsValidAndCoppaIsZeroAndDoesNotCallTcfServices() { // given @@ -858,6 +947,7 @@ public void shouldReturnCorrectMaskedForMultipleBidders() { private AuctionContext auctionContext(BidRequest bidRequest) { return AuctionContext.builder() + .routingContext(routingContext) .account(Account.builder().build()) .bidRequest(bidRequest) .timeout(timeout) diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java index 30080cc14e7..8e3cfbd0874 100644 --- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java @@ -4,7 +4,9 @@ import io.netty.util.AsciiString; import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.http.impl.headers.VertxHttpHeaders; import io.vertx.ext.web.RoutingContext; import org.junit.Before; import org.junit.Rule; @@ -34,6 +36,7 @@ import org.prebid.server.proto.response.CookieSyncResponse; import org.prebid.server.proto.response.UsersyncInfo; import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.util.HttpUtil; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountGdprConfig; @@ -103,6 +106,8 @@ public class CookieSyncHandlerTest extends VertxTest { private RoutingContext routingContext; @Mock private HttpServerResponse httpResponse; + @Mock + private HttpServerRequest httpRequest; private Usersyncer rubiconUsersyncer; @@ -114,6 +119,7 @@ public void setUp() { .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper)); given(routingContext.response()).willReturn(httpResponse); + given(routingContext.request()).willReturn(httpRequest); given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse); cookieSyncHandler = new CookieSyncHandler("http://external-url", 2000, uidsCookieService, applicationSettings, bidderCatalog, tcfDefinerService, privacyEnforcementService, 1, false, false, emptyList(), @@ -158,6 +164,26 @@ public void shouldRespondWithErrorIfRequestBodyIsMissing() { verifyNoMoreInteractions(httpResponse, tcfDefinerService); } + @Test + public void shouldPassNoContentIfDNTHeaderIsPresented() { + // given + final VertxHttpHeaders headers = new VertxHttpHeaders(); + headers.add(HttpUtil.DNT_HEADER, "1"); + given(httpRequest.headers()).willReturn(headers); + + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + given(httpResponse.setStatusMessage(anyString())).willReturn(httpResponse); + + // when + cookieSyncHandler.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(eq(204)); + verify(httpResponse).setStatusMessage(eq("Do-Not-Track is enabled")); + verify(httpResponse).end(); + verifyNoMoreInteractions(httpResponse, tcfDefinerService); + } + @Test public void shouldRespondWithErrorIfRequestBodyCouldNotBeParsed() { // given diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 4addf151892..76f210e11d4 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -6,6 +6,7 @@ import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.http.impl.headers.VertxHttpHeaders; import io.vertx.ext.web.RoutingContext; import org.junit.Before; import org.junit.Rule; @@ -30,6 +31,7 @@ import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; import org.prebid.server.privacy.gdpr.model.TcfResponse; import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.util.HttpUtil; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountGdprConfig; @@ -518,6 +520,21 @@ public void shouldUpdateSetsMetric() { verify(metrics).updateUserSyncSetsMetric(eq(RUBICON)); } + @Test + public void shouldPassNoContentIfDNTHeaderIsPresented() { + // given + final VertxHttpHeaders headers = new VertxHttpHeaders(); + headers.add(HttpUtil.DNT_HEADER, "1"); + given(httpRequest.headers()).willReturn(headers); + + // when + setuidHandler.handle(routingContext); + + // then + final SetuidEvent setuidEvent = captureSetuidEvent(); + assertThat(setuidEvent).isEqualTo(SetuidEvent.builder().status(204).build()); + } + @Test public void shouldPassUnauthorizedEventToAnalyticsReporterIfOptedOut() { // given