-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathSharingService.java
More file actions
469 lines (407 loc) · 19.4 KB
/
SharingService.java
File metadata and controls
469 lines (407 loc) · 19.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
package com.uid2.admin.vertx.service;
import com.uid2.admin.auth.AdminAuthMiddleware;
import com.uid2.admin.auth.AdminKeyset;
import com.uid2.admin.legacy.LegacyClientKey;
import com.uid2.admin.legacy.RotatingLegacyClientKeyProvider;
import com.uid2.admin.store.reader.RotatingAdminKeysetStore;
import com.uid2.admin.vertx.RequestUtil;
import com.uid2.admin.vertx.WriteLock;
import com.uid2.admin.managers.KeysetManager;
import com.uid2.admin.vertx.ResponseUtil;
import com.uid2.shared.Const;
import com.uid2.shared.audit.AuditParams;
import com.uid2.shared.auth.Role;
import com.uid2.shared.model.ClientType;
import com.uid2.shared.model.SiteUtil;
import com.uid2.shared.store.reader.RotatingSiteStore;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import static com.uid2.admin.vertx.Endpoints.*;
public class SharingService implements IService {
private final AdminAuthMiddleware auth;
private final WriteLock writeLock;
private final RotatingAdminKeysetStore keysetProvider;
private final RotatingSiteStore siteProvider;
private final KeysetManager keysetManager;
private final RotatingLegacyClientKeyProvider clientKeyProvider;
private static final Logger LOGGER = LoggerFactory.getLogger(SharingService.class);
private final boolean enableKeysets;
public SharingService(AdminAuthMiddleware auth,
WriteLock writeLock,
RotatingAdminKeysetStore keysetProvider,
KeysetManager keysetManager,
RotatingSiteStore siteProvider,
boolean enableKeyset,
RotatingLegacyClientKeyProvider clientKeyProvider) {
this.auth = auth;
this.writeLock = writeLock;
this.keysetProvider = keysetProvider;
this.keysetManager = keysetManager;
this.siteProvider = siteProvider;
this.enableKeysets = enableKeyset;
this.clientKeyProvider = clientKeyProvider;
}
@Override
public void setupRoutes(Router router) {
if(!enableKeysets) return;
router.get(API_SHARING_LISTS.toString()).handler(
auth.handle(this::handleListAllAllowedSites, Role.MAINTAINER, Role.SHARING_PORTAL, Role.METRICS_EXPORT)
);
router.get(API_SHARING_LIST_SITEID.toString()).handler(
auth.handle(this::handleListAllowedSites, Role.MAINTAINER, Role.SHARING_PORTAL)
);
router.post(API_SHARING_LIST_SITEID.toString()).handler(
auth.handle(this::handleSetAllowedSites, new AuditParams(Collections.emptyList(), List.of("hash", "allowed_sites", "allowed_types")), Role.MAINTAINER, Role.SHARING_PORTAL)
);
router.get(API_SHARING_KEYSETS.toString()).handler(
auth.handle(this::handleListAllKeysets, Role.MAINTAINER)
);
router.post(API_SHARING_KEYSET.toString()).handler(
auth.handle(this::handleSetKeyset, new AuditParams(Collections.emptyList(), List.of("site_id", "name", "allowed_sites", "allowed_types")), Role.MAINTAINER)
);
router.get(API_SHARING_KEYSET_KEYSETID.toString()).handler(
auth.handle(this::handleListKeyset, Role.MAINTAINER)
);
router.get(API_SHARING_KEYSETS_RELATED.toString()).handler(
auth.handle(this::handleListAllKeysetsRelated, Role.MAINTAINER)
);
}
private void handleSetKeyset(RoutingContext rc) {
synchronized (writeLock) {
try {
keysetProvider.loadContent();
siteProvider.loadContent();
} catch (Exception e) {
ResponseUtil.errorInternal(rc, "Failed to load keysets", e);
return;
}
final Map<Integer, AdminKeyset> keysetsById = this.keysetProvider.getSnapshot().getAllKeysets();
final JsonObject body = rc.body().asJsonObject();
final JsonArray allowedSites = body.getJsonArray("allowed_sites");
final JsonArray allowedTypes = body.getJsonArray("allowed_types");
final Integer requestKeysetId = body.getInteger("keyset_id");
final Integer requestSiteId = body.getInteger("site_id");
final String requestName = body.getString("name", "");
if ((requestKeysetId == null && requestSiteId == null)
|| (requestKeysetId != null && requestSiteId != null)) {
ResponseUtil.error(rc, 400, "You must specify exactly one of: keyset_id, site_id");
return;
}
final int siteId;
final Integer keysetId;
final String name;
if(requestSiteId != null) {
siteId = requestSiteId;
name = requestName;
// Check if the site id is valid
if (!isSiteIdEditable(siteId)) {
ResponseUtil.error(rc, 400, "Site id " + siteId + " not valid");
return;
}
// Trying to add a keyset for a site that already has one
if (keysetsById.values().stream().anyMatch(k -> k.getSiteId() == siteId)) {
ResponseUtil.error(rc, 400, "Keyset already exists for site: " + siteId);
return;
}
keysetId = null;
if (keysetsById.values().stream().anyMatch(item -> // for multiple keysets. See commented out SharingServiceTest#KeysetSetNewIdenticalNameAndSiteId
item.getSiteId() == siteId && item.getName().equalsIgnoreCase(name))) {
ResponseUtil.error(rc, 400, "Keyset with same site_id and name already exists");
return;
}
} else {
AdminKeyset keyset = keysetsById.get(requestKeysetId);
if (keyset == null) {
ResponseUtil.error(rc, 404, "Could not find keyset for keyset_id: " + requestKeysetId);
return;
}
keysetId = requestKeysetId;
if(isSpecialKeyset(keysetId)) {
ResponseUtil.error(rc, 400, "Keyset id: " + keysetId + " is not valid");
return;
}
siteId = keyset.getSiteId();
name = requestName.equals("") ? keyset.getName() : requestName;
}
try {
AdminKeyset newKeyset = setAdminKeyset(rc, allowedSites, allowedTypes, siteId, keysetId, name);
if(newKeyset == null) return;
JsonObject jo = jsonFullKeyset(newKeyset);
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(jo.encode());
} catch (Exception e) {
rc.fail(500, e);
}
}
}
private void handleListAllKeysetsRelated(RoutingContext rc) {
try {
// Get value for site id
final Optional<Integer> siteIdOpt = RequestUtil.getSiteId(rc, "site_id");
if (!siteIdOpt.isPresent()) {
ResponseUtil.error(rc, 400, "must specify a site id");
return;
}
final int siteId = siteIdOpt.get();
if (!SiteUtil.isValidSiteId(siteId)) {
ResponseUtil.error(rc, 400, "must specify a valid site id");
return;
}
// Get value for client type from the backend
Set<ClientType> clientTypes = this.siteProvider.getSite(siteId).getClientTypes();
// Check if this site has any client key that has an ID_READER role
boolean isIdReaderRole = false;
List<LegacyClientKey> clientKeysForThisSite = this.clientKeyProvider.getAll().stream().filter(legacyClientKey -> legacyClientKey.getSiteId() == siteId).collect(Collectors.toList());
for (LegacyClientKey c : clientKeysForThisSite) {
if (c.getRoles().contains(Role.ID_READER)) {
isIdReaderRole = true;
}
}
// Get the keyset ids that need to be rotated
final JsonArray ja = new JsonArray();
Map<Integer, AdminKeyset> collection = this.keysetProvider.getSnapshot().getAllKeysets();
for (Map.Entry<Integer, AdminKeyset> keyset : collection.entrySet()) {
// The keysets meet any of the below conditions ALL need to be rotated:
// a. Keysets where allowed_types include any of the clientTypes of the site
// b. If this participant has a client key with ID_READER role, we want to rotate all the keysets where allowed_sites is set to null
// c. Keysets where allowed_sites include the leaked site
// d. Keysets belonging to the leaked site itself
if (!Collections.disjoint(keyset.getValue().getAllowedTypes(), clientTypes) ||
isIdReaderRole && keyset.getValue().getAllowedSites() == null ||
keyset.getValue().getAllowedSites() != null && keyset.getValue().getAllowedSites().contains(siteId) ||
keyset.getValue().getSiteId() == siteId) {
// TODO: We have functions below which check if a keysetkey is accessible by a client. We should move the logic of checking keyset to shared as well.
// https://github.com/IABTechLab/uid2-shared/blob/19edb010c6a4d753d03c89268c238be10a8f6722/src/main/java/com/uid2/shared/auth/KeysetSnapshot.java#L13
ja.add(jsonFullKeyset(keyset.getValue()));
}
}
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(ja.encode());
} catch (Exception e) {
rc.fail(500, e);
}
}
// Returns if a keyset is one of the reserved ones
private static boolean isSpecialKeyset(int keysetId) {
return keysetId == Const.Data.MasterKeysetId || keysetId == Const.Data.RefreshKeysetId
|| keysetId == Const.Data.FallbackPublisherKeysetId;
}
// Returns if a site ID is not a special site and it does exist
private boolean isSiteIdEditable(int siteId) {
return SiteUtil.isValidSiteId(siteId) && siteProvider.getSite(siteId) != null;
}
private void handleListKeyset(RoutingContext rc) {
int keysetId;
try {
keysetId = Integer.parseInt(rc.pathParam("keyset_id"));
} catch (Exception e) {
LOGGER.warn("Failed to parse a keyset_id from list request", e);
rc.fail(400, e);
return;
}
AdminKeyset keyset = this.keysetProvider.getSnapshot().getAllKeysets().get(keysetId);
if (keyset == null) {
ResponseUtil.error(rc, 404, "Failed to find keyset for keyset_id: " + keysetId);
return;
}
JsonObject jo = jsonFullKeyset(keyset);
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(jo.encode());
}
private AdminKeyset getDefaultKeyset(Map<Integer, AdminKeyset> keysets, Integer siteId) {
for(AdminKeyset keyset: keysets.values()) {
if(keyset.getSiteId() == siteId && keyset.isDefault()) {
return keyset;
}
}
return null;
}
private void handleListAllKeysets(RoutingContext rc) {
try {
JsonArray ja = new JsonArray();
Map<Integer, AdminKeyset> collection = this.keysetProvider.getSnapshot().getAllKeysets();
for (Map.Entry<Integer, AdminKeyset> keyset : collection.entrySet()) {
JsonObject jo = jsonFullKeyset(keyset.getValue());
ja.add(jo);
}
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(ja.encode());
} catch (Exception e) {
rc.fail(500, e);
}
}
private void handleListAllowedSites(RoutingContext rc) {
int siteId;
try {
siteId = Integer.parseInt(rc.pathParam("siteId"));
} catch (Exception e) {
LOGGER.warn("Failed to parse a site id from list request", e);
rc.fail(400, e);
return;
}
AdminKeyset keyset = getDefaultKeyset(this.keysetProvider.getSnapshot().getAllKeysets(), siteId);
if (keyset == null) {
LOGGER.warn("Failed to find keyset for site id: " + siteId);
rc.fail(404);
return;
}
JsonObject jo = new JsonObject();
Set<Integer> allowedSites = keyset.getAllowedSites();
jo.put("allowed_sites", allowedSites != null ? allowedSites.stream().sorted().toArray() : null);
jo.put("allowed_types", keyset.getAllowedTypes());
jo.put("hash", keyset.hashCode());
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(jo.encode());
}
private void handleListAllAllowedSites(RoutingContext rc) {
try {
JsonArray ja = new JsonArray();
Map<Integer, AdminKeyset> collection = this.keysetProvider.getSnapshot().getAllKeysets();
for (Map.Entry<Integer, AdminKeyset> keyset : collection.entrySet()) {
JsonObject jo = new JsonObject();
jo.put("keyset_id", keyset.getValue().getKeysetId());
jo.put("site_id", keyset.getValue().getSiteId());
Set<Integer> allowedSites = keyset.getValue().getAllowedSites();
jo.put("allowed_sites", allowedSites != null ? allowedSites.stream().sorted().toArray() : null);
jo.put("allowed_types", keyset.getValue().getAllowedTypes());
jo.put("hash", keyset.getValue().hashCode());
ja.add(jo);
}
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(ja.encode());
} catch (Exception e) {
rc.fail(500, e);
}
}
private void handleSetAllowedSites(RoutingContext rc) {
synchronized (writeLock) {
int siteId;
try {
siteId = Integer.parseInt(rc.pathParam("siteId"));
} catch (Exception e) {
LOGGER.warn("Failed to parse a site id from list request", e);
rc.fail(400, e);
return;
}
if (!isSiteIdEditable(siteId)) {
ResponseUtil.error(rc, 400, "Site id " + siteId + " not valid");
return;
}
try {
keysetProvider.loadContent();
} catch (Exception e) {
ResponseUtil.errorInternal(rc, "Failed to load keysets", e);
return;
}
final Map<Integer, AdminKeyset> keysetsById = this.keysetProvider.getSnapshot().getAllKeysets();
AdminKeyset keyset = getDefaultKeyset(keysetsById, siteId);
final JsonObject body = rc.body().asJsonObject();
final JsonArray allowedSites = body.getJsonArray("allowed_sites");
final JsonArray allowedTypes = body.getJsonArray("allowed_types");
final int hash = body.getInteger("hash");
if (keyset != null && hash != keyset.hashCode()) {
rc.fail(409);
return;
}
Integer keysetId = null;
String name;
if (keyset == null) {
name = "";
} else {
keysetId = keyset.getKeysetId();
name = keyset.getName();
}
try {
AdminKeyset newKeyset = setAdminKeyset(rc, allowedSites, allowedTypes, siteId, keysetId, name);
if(newKeyset == null) return;
JsonObject jo = new JsonObject();
jo.put("allowed_sites", newKeyset.getAllowedSites());
jo.put("allowed_types", newKeyset.getAllowedTypes());
jo.put("hash", newKeyset.hashCode());
rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(jo.encode());
} catch (Exception e) {
rc.fail(500, e);
}
}
}
private AdminKeyset setAdminKeyset(RoutingContext rc, JsonArray allowedSites, JsonArray allowedTypes,
Integer siteId, Integer keysetId, String name)
throws Exception{
Set<Integer> existingSites = new HashSet<>();
if (keysetId == null) {
keysetId = this.keysetManager.getNextKeysetId();
name = "";
} else {
existingSites = this.keysetProvider.getSnapshot().getAllKeysets().get(keysetId).getAllowedSites();
}
final Set<Integer> newlist;
if (allowedSites != null){
final Set<Integer> existingAllowedSites = (existingSites != null) ? existingSites : new HashSet<>();
OptionalInt firstInvalidSite = allowedSites.stream()
.mapToInt(s -> (Integer) s)
.filter(s -> !existingAllowedSites.contains(s) && !isSiteIdEditable(s))
.findFirst();
if (firstInvalidSite.isPresent()) {
ResponseUtil.error(rc, 400, "Site id " + firstInvalidSite.getAsInt() + " not valid");
return null;
}
boolean containsDuplicates = allowedSites.stream().distinct().count() < allowedSites.stream().count();
if (containsDuplicates) {
ResponseUtil.error(rc, 400, "Duplicate site_ids not permitted");
return null;
}
newlist = allowedSites.stream()
.mapToInt(s -> (Integer) s)
.filter(s -> !Objects.equals(s, siteId))
.boxed()
.collect(Collectors.toSet());
} else {
newlist = new HashSet<>();
}
Set<ClientType> newAllowedTypes = null;
if(allowedTypes == null || allowedTypes.isEmpty()) {
newAllowedTypes = new HashSet<>();
} else {
try {
newAllowedTypes = allowedTypes.stream()
.map(s -> Enum.valueOf(ClientType.class, s.toString()))
.collect(Collectors.toSet());
} catch (Exception e) {
ResponseUtil.error(rc, 400, "Invalid Client Type");
return null;
}
}
final AdminKeyset newKeyset = new AdminKeyset(keysetId, siteId, name,
newlist, Instant.now().getEpochSecond(), true, true, newAllowedTypes);
this.keysetManager.addOrReplaceKeyset(newKeyset);
return newKeyset;
}
private JsonObject jsonFullKeyset(AdminKeyset keyset) {
JsonObject jo = new JsonObject();
jo.put("keyset_id", keyset.getKeysetId());
jo.put("site_id", keyset.getSiteId());
jo.put("name", keyset.getName());
jo.put("allowed_sites", keyset.getAllowedSites());
jo.put("allowed_types", keyset.getAllowedTypes());
jo.put("created", keyset.getCreated());
jo.put("is_enabled", keyset.isEnabled());
jo.put("is_default", keyset.isDefault());
return jo;
}
}