Skip to content

Commit 7189c04

Browse files
authored
enrich request to optout service (#2131)
1 parent f369ab0 commit 7189c04

File tree

8 files changed

+218
-13
lines changed

8 files changed

+218
-13
lines changed

src/main/java/com/uid2/operator/service/IUIDOperatorService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public interface IUIDOperatorService {
2121

2222
List<SaltEntry> getModifiedBuckets(Instant sinceTimestamp);
2323

24-
void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTraceId, IdentityEnvironment env, Handler<AsyncResult<Instant>> handler);
24+
void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTraceId, IdentityEnvironment env,
25+
String email, String phone, String clientIp,
26+
Handler<AsyncResult<Instant>> handler);
2527

2628
boolean advertisingTokenMatches(String advertisingToken, UserIdentity userIdentity, Instant asOf, IdentityEnvironment env);
2729
}

src/main/java/com/uid2/operator/service/UIDOperatorService.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,14 @@ private ISaltProvider.ISaltSnapshot getSaltProviderSnapshot(Instant asOf) {
180180
}
181181

182182
@Override
183-
public void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTraceId, IdentityEnvironment env, Handler<AsyncResult<Instant>> handler) {
183+
public void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTraceId, IdentityEnvironment env,
184+
String email, String phone, String clientIp,
185+
Handler<AsyncResult<Instant>> handler) {
184186
final UserIdentity firstLevelHashIdentity = getFirstLevelHashIdentity(userIdentity, asOf);
185187
final MappedIdentity mappedIdentity = getMappedIdentity(firstLevelHashIdentity, asOf, env);
186188

187-
this.optOutStore.addEntry(firstLevelHashIdentity, mappedIdentity.advertisingId, uidTraceId, this.uidInstanceIdProvider.getInstanceId(), r -> {
189+
this.optOutStore.addEntry(firstLevelHashIdentity, mappedIdentity.advertisingId, uidTraceId,
190+
this.uidInstanceIdProvider.getInstanceId(), email, phone, clientIp, r -> {
188191
if (r.succeeded()) {
189192
handler.handle(Future.succeededFuture(r.result()));
190193
} else {

src/main/java/com/uid2/operator/store/CloudSyncOptOutStore.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,24 +88,36 @@ public long getOptOutTimestampByAdId(String adId) {
8888
}
8989

9090
@Override
91-
public void addEntry(UserIdentity firstLevelHashIdentity, byte[] advertisingId, String uidTraceId, String uidInstanceId, Handler<AsyncResult<Instant>> handler) {
91+
public void addEntry(UserIdentity firstLevelHashIdentity,
92+
byte[] advertisingId,
93+
String uidTraceId,
94+
String uidInstanceId,
95+
String email,
96+
String phone,
97+
String clientIp,
98+
Handler<AsyncResult<Instant>> handler) {
9299
if (remoteApiHost == null) {
93100
handler.handle(Future.failedFuture("remote api not set"));
94101
return;
95102
}
96103

97-
HttpRequest<String> request =this.webClient.get(remoteApiPort, remoteApiHost, remoteApiPath).
104+
HttpRequest<String> request = this.webClient.post(remoteApiPort, remoteApiHost, remoteApiPath).
98105
addQueryParam("identity_hash", EncodingUtils.toBase64String(firstLevelHashIdentity.id))
99106
.addQueryParam("advertising_id", EncodingUtils.toBase64String(advertisingId))
100107
.putHeader("Authorization", remoteApiBearerToken)
101108
.putHeader(Audit.UID_INSTANCE_ID_HEADER, uidInstanceId)
102109
.as(BodyCodec.string());
103110

111+
JsonObject payload = new JsonObject();
112+
if (email != null) payload.put("email", email);
113+
if (phone != null) payload.put("phone", phone);
114+
if (clientIp != null) payload.put("client_ip", clientIp);
115+
104116
if (uidTraceId != null) {
105117
request = request.putHeader(Audit.UID_TRACE_ID_HEADER, uidTraceId);
106118
}
107119

108-
request.send(ar -> {
120+
request.sendJson(payload, ar -> {
109121
Exception failure = null;
110122
if (ar.failed()) {
111123
failure = new Exception(ar.cause());

src/main/java/com/uid2/operator/store/IOptOutStore.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,12 @@ public interface IOptOutStore {
1717

1818
long getOptOutTimestampByAdId(String adId);
1919

20-
void addEntry(UserIdentity firstLevelHashIdentity, byte[] advertisingId, String uidTraceId, String uidInstanceId, Handler<AsyncResult<Instant>> handler);
20+
void addEntry(UserIdentity firstLevelHashIdentity,
21+
byte[] advertisingId,
22+
String uidTraceId,
23+
String uidInstanceId,
24+
String email,
25+
String phone,
26+
String clientIp,
27+
Handler<AsyncResult<Instant>> handler);
2128
}

src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,13 @@ private Future handleLogoutAsyncV2(RoutingContext rc) {
986986
final Instant now = Instant.now();
987987

988988
Promise promise = Promise.promise();
989-
this.idService.invalidateTokensAsync(input.toUserIdentity(this.identityScope, 0, now), now, uidTraceId, env, ar -> {
989+
final String email = req == null ? null : req.getString("email");
990+
final String phone = req == null ? null : req.getString("phone");
991+
final String clientIp = req == null ? null : req.getString("clientIp");
992+
993+
this.idService.invalidateTokensAsync(input.toUserIdentity(this.identityScope, 0, now), now, uidTraceId, env,
994+
email, phone, clientIp,
995+
ar -> {
990996
if (ar.succeeded()) {
991997
JsonObject body = new JsonObject();
992998
body.put("optout", "OK");

src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2645,10 +2645,22 @@ void logoutV2(boolean useV4Uid, String contentType, Vertx vertx, VertxTestContex
26452645
req.put("email", "[email protected]");
26462646

26472647
doAnswer(invocation -> {
2648-
Handler<AsyncResult<Instant>> handler = invocation.getArgument(4);
2648+
String passedTrace = invocation.getArgument(2);
2649+
String passedInstanceId = invocation.getArgument(3);
2650+
String passedEmail = invocation.getArgument(4);
2651+
String passedPhone = invocation.getArgument(5);
2652+
String passedClientIp = invocation.getArgument(6);
2653+
2654+
assertEquals("uid-trace-id", passedTrace);
2655+
assertEquals("test-instance-id", passedInstanceId);
2656+
assertEquals("[email protected]", passedEmail);
2657+
assertNull(passedClientIp);
2658+
assertNull(passedPhone);
2659+
2660+
Handler<AsyncResult<Instant>> handler = invocation.getArgument(7);
26492661
handler.handle(Future.succeededFuture(Instant.now()));
26502662
return null;
2651-
}).when(this.optOutStore).addEntry(any(), any(), eq("uid-trace-id"), eq("test-instance-id"), any());
2663+
}).when(this.optOutStore).addEntry(any(), any(), any(), any(), any(), any(), any(), any());
26522664

26532665
send(vertx, "v2/token/logout", req, 200, respJson -> {
26542666
assertEquals("success", respJson.getString("status"));
@@ -2658,6 +2670,42 @@ void logoutV2(boolean useV4Uid, String contentType, Vertx vertx, VertxTestContex
26582670
HttpHeaders.CONTENT_TYPE.toString(), contentType));
26592671
}
26602672

2673+
@Test
2674+
void logoutV2_withClientIp(Vertx vertx, VertxTestContext testContext) {
2675+
final int clientSiteId = 201;
2676+
fakeAuth(clientSiteId, Role.OPTOUT);
2677+
setupKeys();
2678+
setupSalts();
2679+
2680+
JsonObject req = new JsonObject();
2681+
req.put("email", "[email protected]");
2682+
req.put("clientIp", "127.0.0.1");
2683+
2684+
doAnswer(invocation -> {
2685+
String passedTrace = invocation.getArgument(2);
2686+
String passedInstanceId = invocation.getArgument(3);
2687+
String passedEmail = invocation.getArgument(4);
2688+
String passedPhone = invocation.getArgument(5);
2689+
String passedClientIp = invocation.getArgument(6);
2690+
2691+
assertEquals("uid-trace-id", passedTrace);
2692+
assertEquals("test-instance-id", passedInstanceId);
2693+
assertEquals("[email protected]", passedEmail);
2694+
assertNull(passedPhone);
2695+
assertEquals("127.0.0.1", passedClientIp);
2696+
2697+
Handler<AsyncResult<Instant>> handler = invocation.getArgument(7);
2698+
handler.handle(Future.succeededFuture(Instant.now()));
2699+
return null;
2700+
}).when(this.optOutStore).addEntry(any(), any(), any(), any(), any(), any(), any(), any());
2701+
2702+
send(vertx, "v2/token/logout", req, 200, respJson -> {
2703+
assertEquals("success", respJson.getString("status"));
2704+
assertEquals("OK", respJson.getJsonObject("body").getString("optout"));
2705+
testContext.completeNow();
2706+
}, Map.of(Audit.UID_TRACE_ID_HEADER, "uid-trace-id"));
2707+
}
2708+
26612709
@Test
26622710
void logoutV2SaltsExpired(Vertx vertx, VertxTestContext testContext) {
26632711
when(saltProviderSnapshot.getExpires()).thenReturn(Instant.now().minus(1, ChronoUnit.HOURS));
@@ -2670,10 +2718,10 @@ void logoutV2SaltsExpired(Vertx vertx, VertxTestContext testContext) {
26702718
req.put("email", "[email protected]");
26712719

26722720
doAnswer(invocation -> {
2673-
Handler<AsyncResult<Instant>> handler = invocation.getArgument(4);
2721+
Handler<AsyncResult<Instant>> handler = invocation.getArgument(7);
26742722
handler.handle(Future.succeededFuture(Instant.now()));
26752723
return null;
2676-
}).when(this.optOutStore).addEntry(any(), any(), any(), any(), any());
2724+
}).when(this.optOutStore).addEntry(any(), any(), any(), any(), any(), any(), any(), any());
26772725

26782726
send(vertx, "v2/token/logout", req, 200, respJson -> {
26792727
assertEquals("success", respJson.getString("status"));

src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,14 @@ public Instant getLatestEntry(UserIdentity firstLevelHashIdentity) {
196196
}
197197

198198
@Override
199-
public void addEntry(UserIdentity firstLevelHashIdentity, byte[] advertisingId, String uidTraceId, String uidInstanceId, Handler<AsyncResult<Instant>> handler) {
199+
public void addEntry(UserIdentity firstLevelHashIdentity,
200+
byte[] advertisingId,
201+
String uidTraceId,
202+
String uidInstanceId,
203+
String email,
204+
String phone,
205+
String clientIp,
206+
Handler<AsyncResult<Instant>> handler) {
200207
// noop
201208
}
202209

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.uid2.operator.store;
2+
3+
import com.uid2.operator.Const;
4+
import com.uid2.operator.model.IdentityScope;
5+
import com.uid2.operator.model.IdentityType;
6+
import com.uid2.operator.model.UserIdentity;
7+
import com.uid2.operator.service.EncodingUtils;
8+
import com.uid2.shared.cloud.MemCachedStorage;
9+
import com.uid2.shared.audit.Audit;
10+
import io.vertx.core.Vertx;
11+
import io.vertx.core.http.HttpMethod;
12+
import io.vertx.core.http.HttpServer;
13+
import io.vertx.core.json.JsonObject;
14+
import io.vertx.ext.web.Router;
15+
import io.vertx.ext.web.handler.BodyHandler;
16+
import org.junit.jupiter.api.AfterEach;
17+
import org.junit.jupiter.api.BeforeEach;
18+
import org.junit.jupiter.api.Test;
19+
20+
import java.time.Clock;
21+
import java.time.Instant;
22+
import java.util.concurrent.CountDownLatch;
23+
import java.util.concurrent.TimeUnit;
24+
25+
import static org.junit.jupiter.api.Assertions.*;
26+
27+
public class CloudSyncOptOutStoreTest {
28+
private Vertx vertx;
29+
private HttpServer server;
30+
private int port;
31+
private Router router;
32+
33+
@BeforeEach
34+
void setUp() throws InterruptedException {
35+
vertx = Vertx.vertx();
36+
CountDownLatch latch = new CountDownLatch(1);
37+
server = vertx.createHttpServer();
38+
router = Router.router(vertx);
39+
router.route().handler(BodyHandler.create());
40+
server.requestHandler(router);
41+
server.listen(0, ar -> {
42+
if (ar.succeeded()) {
43+
port = ar.result().actualPort();
44+
latch.countDown();
45+
}
46+
});
47+
assertTrue(latch.await(5, TimeUnit.SECONDS));
48+
}
49+
50+
@AfterEach
51+
void tearDown() throws InterruptedException {
52+
CountDownLatch latch = new CountDownLatch(1);
53+
server.close(ar -> latch.countDown());
54+
assertTrue(latch.await(5, TimeUnit.SECONDS));
55+
CountDownLatch latch2 = new CountDownLatch(1);
56+
vertx.close(ar -> latch2.countDown());
57+
assertTrue(latch2.await(5, TimeUnit.SECONDS));
58+
}
59+
60+
@Test
61+
void addEntry_sendsPostWithQueryAndJsonBody() throws Exception {
62+
final String path = "/optout/replicate";
63+
final String operatorKey = "test-operator-key";
64+
final String uidTraceId = "trace-123";
65+
final String email = "[email protected]";
66+
final String phone = null;
67+
final String clientIp = "203.0.113.5";
68+
69+
final byte[] userIdBytes = new byte[] {10, 20, 30};
70+
final byte[] advIdBytes = new byte[] {1, 2, 3};
71+
final String expectedIdentityHash = EncodingUtils.toBase64String(userIdBytes);
72+
final String expectedAdvertisingId = EncodingUtils.toBase64String(advIdBytes);
73+
74+
CountDownLatch received = new CountDownLatch(1);
75+
76+
router.post(path).handler(ctx -> {
77+
try {
78+
assertEquals(HttpMethod.POST, ctx.request().method());
79+
assertEquals(path, ctx.normalisedPath());
80+
assertEquals(expectedIdentityHash, ctx.request().getParam("identity_hash"));
81+
assertEquals(expectedAdvertisingId, ctx.request().getParam("advertising_id"));
82+
assertEquals("Bearer " + operatorKey, ctx.request().getHeader("Authorization"));
83+
assertEquals(uidTraceId, ctx.request().getHeader(Audit.UID_TRACE_ID_HEADER));
84+
85+
JsonObject body = ctx.body().asJsonObject();
86+
assertNotNull(body);
87+
assertEquals(email, body.getString("email"));
88+
assertEquals(clientIp, body.getString("client_ip"));
89+
assertFalse(body.containsKey("unexpected"));
90+
91+
ctx.response().setStatusCode(200).end();
92+
} finally {
93+
received.countDown();
94+
}
95+
});
96+
JsonObject config = new JsonObject()
97+
.put(Const.Config.OptOutApiUriProp, "http://localhost:" + port + path)
98+
.put(Const.Config.OptOutBloomFilterSizeProp, 8192)
99+
.put(Const.Config.OptOutHeapDefaultCapacityProp, 8192)
100+
.put(Const.Config.OptOutStatusApiEnabled, true)
101+
.put(Const.Config.OptOutDataDirProp, "/tmp/uid2-operator-test")
102+
// Additional required config for FileUtils/optout snapshot
103+
.put("optout_delta_rotate_interval", 300)
104+
.put("optout_delta_backtrack_in_days", 1)
105+
.put("optout_partition_interval", 86400)
106+
.put("optout_max_partitions", 30)
107+
.put("optout_s3_folder", "optout/")
108+
.put("optout_s3_path_compat", false);
109+
110+
CloudSyncOptOutStore store = new CloudSyncOptOutStore(vertx, new MemCachedStorage(), config, operatorKey, Clock.systemUTC());
111+
112+
UserIdentity uid = new UserIdentity(IdentityScope.UID2, IdentityType.Email, userIdBytes, 0, Instant.now(), Instant.now());
113+
114+
CountDownLatch done = new CountDownLatch(1);
115+
store.addEntry(uid, advIdBytes, uidTraceId, "local-instance", email, phone, clientIp, ar -> done.countDown());
116+
117+
assertTrue(received.await(5, TimeUnit.SECONDS));
118+
assertTrue(done.await(5, TimeUnit.SECONDS));
119+
}
120+
}

0 commit comments

Comments
 (0)