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

Update setuid format parameter and make pixel depends on cookie type #997

Merged
merged 6 commits into from
Apr 13, 2021
Merged
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
2 changes: 1 addition & 1 deletion docs/endpoints/setuid.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This endpoint saves a UserID for a Bidder in the Cookie. Saved IDs will be recog
- `uid`: The ID which the Bidder uses to recognize this user. If undefined, the UID for `bidder` will be deleted.
- `gdpr`: This should be `1` if GDPR is in effect, `0` if not, and undefined if the caller isn't sure
- `gdpr_consent`: This is required if `gdpr` is one, and optional (but encouraged) otherwise. If present, it should be an [unpadded base64-URL](https://tools.ietf.org/html/rfc4648#page-7) encoded [Vendor Consent String](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md#vendor-consent-string-format-).
- `format`: is optional. When `format=img`, response will include `tracking-pixel.png` file.
- `f`: is optional. When `f=i`, response will include `tracking-pixel.png` file, when `f=b` respond with empty html, content-length=0 and text/html content type.

If the `gdpr` and `gdpr_consent` params are included, this endpoint will _not_ write a cookie unless:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ public class SetuidContext {

String cookieName;

@JsonIgnore
String syncType;

PrivacyContext privacyContext;
}
38 changes: 25 additions & 13 deletions src/main/java/org/prebid/server/handler/SetuidHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
Expand Down Expand Up @@ -36,7 +37,6 @@
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

public class SetuidHandler implements Handler<RoutingContext> {
Expand All @@ -45,10 +45,13 @@ public class SetuidHandler implements Handler<RoutingContext> {

private static final String BIDDER_PARAM = "bidder";
private static final String UID_PARAM = "uid";
private static final String FORMAT_PARAM = "format";
private static final String IMG_FORMAT_PARAM = "img";
private static final String FORMAT_PARAM = "f";
private static final String IMG_FORMAT_PARAM = "i";
private static final String BLANK_FORMAT_PARAM = "b";
private static final String PIXEL_FILE_PATH = "static/tracking-pixel.png";
private static final String ACCOUNT_PARAM = "account";
private static final int UNAVAILABLE_FOR_LEGAL_REASONS = 451;
private static final String REDIRECT = "redirect";

private final long defaultTimeout;
private final UidsCookieService uidsCookieService;
Expand All @@ -59,7 +62,7 @@ public class SetuidHandler implements Handler<RoutingContext> {
private final AnalyticsReporterDelegator analyticsDelegator;
private final Metrics metrics;
private final TimeoutFactory timeoutFactory;
private final Set<String> activeCookieFamilyNames;
private final Map<String, String> cookieNameToSyncType;

public SetuidHandler(long defaultTimeout,
UidsCookieService uidsCookieService,
Expand All @@ -82,11 +85,11 @@ public SetuidHandler(long defaultTimeout,
this.metrics = Objects.requireNonNull(metrics);
this.timeoutFactory = Objects.requireNonNull(timeoutFactory);

activeCookieFamilyNames = bidderCatalog.names().stream()
cookieNameToSyncType = bidderCatalog.names().stream()
.filter(bidderCatalog::isActive)
.map(bidderCatalog::usersyncerByName)
.map(Usersyncer::getCookieFamilyName)
.collect(Collectors.toSet());
.distinct() // built-in aliases looks like bidders with the same usersyncers
.collect(Collectors.toMap(Usersyncer::getCookieFamilyName, Usersyncer::getType));
}

private static Integer validateHostVendorId(Integer gdprHostVendorId) {
Expand Down Expand Up @@ -117,6 +120,7 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
.timeout(timeout)
.account(account)
.cookieName(cookieName)
.syncType(cookieNameToSyncType.get(cookieName))
.privacyContext(privacyContext)
.build()));
}
Expand All @@ -125,7 +129,7 @@ private Future<Account> accountById(String accountId, Timeout timeout) {
return StringUtils.isBlank(accountId)
? Future.succeededFuture(Account.empty(accountId))
: applicationSettings.getAccountById(accountId, timeout)
.otherwise(Account.empty(accountId));
.otherwise(Account.empty(accountId));
}

private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextResult,
Expand All @@ -150,7 +154,7 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
private Exception validateSetuidContext(SetuidContext setuidContext) {
final String cookieName = setuidContext.getCookieName();
final boolean isCookieNameBlank = StringUtils.isBlank(cookieName);
if (isCookieNameBlank || !activeCookieFamilyNames.contains(cookieName)) {
if (isCookieNameBlank || !cookieNameToSyncType.containsKey(cookieName)) {
final String cookieNameError = isCookieNameBlank ? "required" : "invalid";
return new InvalidRequestException(String.format("\"bidder\" query param is %s", cookieNameError));
}
Expand All @@ -170,7 +174,7 @@ private Future<HostVendorTcfResponse> isAllowedForHostVendorId(TcfContext tcfCon
return gdprHostVendorId == null
? Future.succeededFuture(HostVendorTcfResponse.allowedVendor())
: tcfDefinerService.resultForVendorIds(Collections.singleton(gdprHostVendorId), tcfContext)
.map(this::toHostVendorTcfResponse);
.map(this::toHostVendorTcfResponse);
}

private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse<Integer> tcfResponse) {
Expand Down Expand Up @@ -204,7 +208,7 @@ private void respondByTcfResponse(AsyncResult<HostVendorTcfResponse> hostTcfResp
} else {
metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);

final int status = HttpResponseStatus.OK.code();
final int status = UNAVAILABLE_FOR_LEGAL_REASONS;
respondWith(routingContext, status, "The gdpr_consent param prevents cookies from being saved");
analyticsDelegator.processEvent(SetuidEvent.error(status), tcfContext);
}
Expand Down Expand Up @@ -243,7 +247,7 @@ private void respondWithCookie(SetuidContext setuidContext) {

// Send pixel file to response if "format=img"
final String format = routingContext.request().getParam(FORMAT_PARAM);
if (StringUtils.equals(format, IMG_FORMAT_PARAM)) {
if (shouldRespondWithPixel(format, setuidContext.getSyncType())) {
routingContext.response().sendFile(PIXEL_FILE_PATH);
} else {
respondWith(routingContext, status, null);
Expand All @@ -258,6 +262,11 @@ private void respondWithCookie(SetuidContext setuidContext) {
.build(), tcfContext);
}

private boolean shouldRespondWithPixel(String format, String syncType) {
return StringUtils.equals(format, IMG_FORMAT_PARAM)
|| !StringUtils.equals(format, BLANK_FORMAT_PARAM) && StringUtils.equals(syncType, REDIRECT);
}

private void handleErrors(Throwable error, RoutingContext routingContext, TcfContext tcfContext) {
final String message = error.getMessage();
final int status;
Expand Down Expand Up @@ -300,7 +309,10 @@ private static void respondWith(RoutingContext context, int status, String body)
if (body != null) {
context.response().end(body);
} else {
context.response().end();
context.response()
.putHeader(HttpHeaders.CONTENT_LENGTH, "0")
.putHeader(HttpHeaders.CONTENT_TYPE, HttpHeaders.TEXT_HTML)
.end();
}
}
}
153 changes: 140 additions & 13 deletions src/test/java/org/prebid/server/handler/SetuidHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.vertx.core.Future;
import io.vertx.core.http.CaseInsensitiveHeaders;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.RoutingContext;
Expand Down Expand Up @@ -47,6 +48,7 @@

import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
Expand Down Expand Up @@ -106,12 +108,16 @@ public void setUp() {
given(routingContext.response()).willReturn(httpResponse);

given(httpResponse.headers()).willReturn(new CaseInsensitiveHeaders());
given(httpResponse.putHeader(any(CharSequence.class), any(CharSequence.class))).willReturn(httpResponse);

given(uidsCookieService.toCookie(any())).willReturn(Cookie.cookie("test", "test"));
given(bidderCatalog.names()).willReturn(new HashSet<>(asList("rubicon", "audienceNetwork")));
given(bidderCatalog.isActive(any())).willReturn(true);
given(bidderCatalog.usersyncerByName(any())).willReturn(
new Usersyncer(RUBICON, null, null, null, false));
given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn(
new Usersyncer(RUBICON, null, null, "iframe", false));

given(bidderCatalog.usersyncerByName(eq(FACEBOOK))).willReturn(
new Usersyncer(FACEBOOK, null, null, "iframe", false));

final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
final TimeoutFactory timeoutFactory = new TimeoutFactory(clock);
Expand Down Expand Up @@ -226,12 +232,12 @@ public void shouldRespondWithoutCookieIfGdprProcessingPreventsCookieSetting() {

// then
verify(routingContext, never()).addCookie(any(Cookie.class));
verify(httpResponse).setStatusCode(eq(200));
verify(httpResponse).setStatusCode(eq(451));
verify(httpResponse).end(eq("The gdpr_consent param prevents cookies from being saved"));
verify(metrics).updateUserSyncTcfBlockedMetric(RUBICON);

final SetuidEvent setuidEvent = captureSetuidEvent();
assertThat(setuidEvent).isEqualTo(SetuidEvent.builder().status(200).build());
assertThat(setuidEvent).isEqualTo(SetuidEvent.builder().status(451).build());
}

@Test
Expand Down Expand Up @@ -337,7 +343,7 @@ public void shouldRemoveUidFromCookieIfMissingInRequest() throws IOException {
.willReturn(new UidsCookie(Uids.builder().uids(uids).build(), jacksonMapper));

given(httpRequest.getParam("bidder")).willReturn(RUBICON);
given(httpRequest.getParam("format")).willReturn("img");
given(httpRequest.getParam("f")).willReturn("i");

// this uids cookie stands for {"tempUIDs":{"adnxs":{"uid":"12345"}}}
given(uidsCookieService.toCookie(any())).willReturn(Cookie
Expand Down Expand Up @@ -368,9 +374,9 @@ public void shouldIgnoreFacebookSentinel() throws IOException {
// this uids cookie value stands for {"tempUIDs":{"audienceNetwork":{"uid":"facebookUid"}}}
given(uidsCookieService.toCookie(any())).willReturn(Cookie
.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhdWRpZW5jZU5ldHdvcmsiOnsidWlkIjoiZmFjZWJvb2tVaWQifX19"));

given(bidderCatalog.names()).willReturn(singleton(FACEBOOK));
given(bidderCatalog.usersyncerByName(any())).willReturn(
new Usersyncer(FACEBOOK, null, null, null, false));
new Usersyncer(FACEBOOK, null, null, "iframe", false));

final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
final TimeoutFactory timeoutFactory = new TimeoutFactory(clock);
Expand Down Expand Up @@ -411,7 +417,6 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException {
.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19"));

given(httpRequest.getParam("bidder")).willReturn(RUBICON);
given(httpRequest.getParam("format")).willReturn("img");
given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT");

given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse);
Expand All @@ -421,14 +426,137 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException {

// then
verify(routingContext, never()).addCookie(any(Cookie.class));
verify(httpResponse).sendFile(any());

final String uidsCookie = getUidsCookie();
final Uids decodedUids = decodeUids(uidsCookie);
assertThat(decodedUids.getUids()).hasSize(1);
assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("J5VLCWQP-26-CWFT");
}

@Test
public void shouldSendPixelWhenFParamIsEqualToIWhenTypeIsIframe() {
// given
given(uidsCookieService.parseFromRequest(any()))
.willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper));

// {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}}
given(uidsCookieService.toCookie(any())).willReturn(Cookie
.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19"));
given(httpRequest.getParam("bidder")).willReturn(RUBICON);
given(httpRequest.getParam("f")).willReturn("i");
given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT");

given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse);

// when
setuidHandler.handle(routingContext);

// then
verify(routingContext, never()).addCookie(any(Cookie.class));
verify(httpResponse).sendFile(any());
}

@Test
public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() {
// given
given(uidsCookieService.parseFromRequest(any()))
.willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper));

// {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}}
given(uidsCookieService.toCookie(any())).willReturn(Cookie
.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19"));

given(httpRequest.getParam("bidder")).willReturn(RUBICON);
given(httpRequest.getParam("f")).willReturn("b");
given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT");
given(bidderCatalog.names()).willReturn(singleton(RUBICON));
given(bidderCatalog.usersyncerByName(any())).willReturn(
new Usersyncer(RUBICON, null, null, "redirect", false));

given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse);
setuidHandler = new SetuidHandler(
2000,
uidsCookieService,
applicationSettings,
bidderCatalog,
privacyEnforcementService,
tcfDefinerService,
null,
analyticsReporterDelegator,
metrics,
new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())));

// when
setuidHandler.handle(routingContext);

// then
verify(routingContext, never()).addCookie(any(Cookie.class));
verify(httpResponse, never()).sendFile(any());
verify(httpResponse).putHeader(eq(HttpHeaders.CONTENT_LENGTH), eq("0"));
verify(httpResponse).putHeader(eq(HttpHeaders.CONTENT_TYPE), eq(HttpHeaders.TEXT_HTML));
}

@Test
public void shouldSendEmptyResponseWhenParamNotDefinedAndTypeIsIframe() {
// given
given(uidsCookieService.parseFromRequest(any()))
.willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper));

// {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}}
given(uidsCookieService.toCookie(any())).willReturn(Cookie
.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19"));

given(httpRequest.getParam("bidder")).willReturn(RUBICON);
given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT");

given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse);

// when
setuidHandler.handle(routingContext);

// then
verify(routingContext, never()).addCookie(any(Cookie.class));
verify(httpResponse, never()).sendFile(any());
verify(httpResponse).putHeader(eq(HttpHeaders.CONTENT_LENGTH), eq("0"));
verify(httpResponse).putHeader(eq(HttpHeaders.CONTENT_TYPE), eq(HttpHeaders.TEXT_HTML));
}

@Test
public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() {
// given
given(uidsCookieService.parseFromRequest(any()))
.willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper));

// {"tempUIDs":{"rubicon":{"uid":"J5VLCWQP-26-CWFT"}}}
given(uidsCookieService.toCookie(any())).willReturn(Cookie
.cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19"));
given(httpRequest.getParam("bidder")).willReturn(RUBICON);
given(bidderCatalog.names()).willReturn(singleton(RUBICON));
given(bidderCatalog.usersyncerByName(any())).willReturn(
new Usersyncer(RUBICON, null, null, "redirect", false));
given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT");

given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse);

setuidHandler = new SetuidHandler(
2000,
uidsCookieService,
applicationSettings,
bidderCatalog,
privacyEnforcementService,
tcfDefinerService,
null,
analyticsReporterDelegator,
metrics,
new TimeoutFactory(Clock.fixed(Instant.now(), ZoneId.systemDefault())));

// when
setuidHandler.handle(routingContext);

// then
verify(routingContext, never()).addCookie(any(Cookie.class));
verify(httpResponse).sendFile(any());
}

@Test
public void shouldUpdateUidInCookieWithRequestValue() throws IOException {
// given
Expand Down Expand Up @@ -498,6 +626,7 @@ public void shouldSkipTcfChecksAndRespondWithCookieIfHostVendorIdNotDefined() th
setuidHandler = new SetuidHandler(2000, uidsCookieService, applicationSettings,
bidderCatalog, privacyEnforcementService, tcfDefinerService, null, analyticsReporterDelegator, metrics,
new TimeoutFactory(clock));

given(tcfDefinerService.resultForVendorIds(anySet(), any()))
.willReturn(Future.succeededFuture(TcfResponse.of(false, emptyMap(), null)));

Expand Down Expand Up @@ -569,9 +698,7 @@ public void shouldPassUnsuccessfulEventToAnalyticsReporterIfFacebookSentinel() {

given(httpRequest.getParam("bidder")).willReturn(FACEBOOK);
given(httpRequest.getParam("uid")).willReturn("0");

given(bidderCatalog.usersyncerByName(any())).willReturn(
new Usersyncer(FACEBOOK, null, null, null, false));
given(bidderCatalog.names()).willReturn(singleton(FACEBOOK));

final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
final TimeoutFactory timeoutFactory = new TimeoutFactory(clock);
Expand Down