Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DNT header support added #693

Closed
wants to merge 4 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -101,9 +103,11 @@ Future<List<BidderPrivacyResult>> 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) {
Expand Down Expand Up @@ -259,7 +263,8 @@ private void updateCcpaMetrics(Ccpa ccpa) {
}

private Map<String, PrivacyEnforcementAction> updatePrivacyMetrics(
Map<String, PrivacyEnforcementAction> bidderToEnforcement, MetricName requestType, Device device) {
Map<String, PrivacyEnforcementAction> bidderToEnforcement, MetricName requestType,
Device device, MultiMap headers) {

for (final Map.Entry<String, PrivacyEnforcementAction> bidderEnforcement : bidderToEnforcement.entrySet()) {
final String bidder = bidderEnforcement.getKey();
Expand All @@ -278,6 +283,10 @@ private Map<String, PrivacyEnforcementAction> updatePrivacyMetrics(
metrics.updatePrivacyLmtMetric();
}

if (isDntEnabled(device, headers.get(HttpUtil.DNT_HEADER))) {
metrics.updatePrivacyDntMetric();
}

return bidderToEnforcement;
}

Expand All @@ -286,23 +295,21 @@ private Map<String, PrivacyEnforcementAction> updatePrivacyMetrics(
* {@link BidderPrivacyResult}. Masking depends on GDPR and COPPA.
*/
private List<BidderPrivacyResult> getBidderToPrivacyResult(
Map<String, User> bidderToUser, Device device, Map<String, PrivacyEnforcementAction> bidderToEnforcement) {

final boolean isLmtEnabled = isLmtEnabled(device);
Map<String, User> bidderToUser, Device device, Map<String, PrivacyEnforcementAction> 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<String, PrivacyEnforcementAction> bidderToEnforcement) {
private BidderPrivacyResult createBidderPrivacyResult(User user, Device device, String bidder,
Map<String, PrivacyEnforcementAction> bidderToEnforcement,
String dntHeader) {

final PrivacyEnforcementAction privacyEnforcementAction = bidderToEnforcement.get(bidder);
final boolean blockBidderRequest = privacyEnforcementAction.isBlockBidderRequest();
Expand All @@ -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()
Expand Down Expand Up @@ -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;
}
DGarbar marked this conversation as resolved.
Show resolved Hide resolved

}
11 changes: 11 additions & 0 deletions src/main/java/org/prebid/server/handler/CookieSyncHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,6 +119,16 @@ private static Set<String> 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();
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/prebid/server/handler/SetuidHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/prebid/server/metric/MetricName.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public enum MetricName {
// privacy
coppa,
lmt,
dnt,
DGarbar marked this conversation as resolved.
Show resolved Hide resolved
specified,
opt_out("opt-out"),
invalid,
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/prebid/server/metric/Metrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -74,13 +78,21 @@ 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;

private Timeout timeout;

@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)));
Expand Down Expand Up @@ -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<String, User> 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<BidderPrivacyResult> 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<String, User> 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<BidderPrivacyResult> 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
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -103,6 +106,8 @@ public class CookieSyncHandlerTest extends VertxTest {
private RoutingContext routingContext;
@Mock
private HttpServerResponse httpResponse;
@Mock
private HttpServerRequest httpRequest;

private Usersyncer rubiconUsersyncer;

Expand All @@ -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(),
Expand Down Expand Up @@ -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
Expand Down
Loading