Skip to content

Commit

Permalink
Syw UId2-1354 key sharing endpoint providing site domain name data (#193
Browse files Browse the repository at this point in the history
)

* providing site domain names for key/sharing endpoint
* always sort the Site set and its domain name set before writing them out in /key/sharing response in order to produce a deterministic result all the time
  • Loading branch information
sunnywu authored Sep 19, 2023
1 parent dce2ab7 commit dae0c92
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 8 deletions.
3 changes: 2 additions & 1 deletion conf/local-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@
"optout_max_partitions": 30,
"optout_partition_interval": 86400,
"client_side_token_generate": true,
"client_side_token_generate_domain_name_check_enabled": true
"client_side_token_generate_domain_name_check_enabled": true,
"key_sharing_endpoint_provide_site_domain_names": true
}
1 change: 1 addition & 0 deletions conf/local-e2e-docker-public-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@
"optout_partition_interval": 86400,
"client_side_token_generate": true,
"client_side_token_generate_domain_name_check_enabled": false,
"key_sharing_endpoint_provide_site_domain_names": true,
"validate_service_links": true
}
3 changes: 2 additions & 1 deletion conf/local-e2e-public-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@
"optout_max_partitions": 30,
"optout_partition_interval": 86400,
"client_side_token_generate": true,
"client_side_token_generate_domain_name_check_enabled": false
"client_side_token_generate_domain_name_check_enabled": false,
"key_sharing_endpoint_provide_site_domain_names": true
}
44 changes: 43 additions & 1 deletion src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static com.uid2.operator.IdentityConst.ClientSideTokenGenerateOptOutIdentityForEmail;
import static com.uid2.operator.IdentityConst.ClientSideTokenGenerateOptOutIdentityForPhone;
Expand Down Expand Up @@ -115,6 +116,8 @@ public class UIDOperatorVerticle extends AbstractVerticle {
private final boolean cstgDoDomainNameCheck;
public final static int MASTER_KEYSET_ID_FOR_SDKS = 9999999; //this is because SDKs have an issue where they assume keyset ids are always positive; that will be fixed.

protected boolean keySharingEndpointProvideSiteDomainNames;

public UIDOperatorVerticle(JsonObject config,
boolean clientSideTokenGenerate,
ISiteStore siteProvider,
Expand Down Expand Up @@ -148,6 +151,7 @@ public UIDOperatorVerticle(JsonObject config,
this.phoneSupport = config.getBoolean("enable_phone_support", true);
this.tcfVendorId = config.getInteger("tcf_vendor_id", 21);
this.cstgDoDomainNameCheck = config.getBoolean("client_side_token_generate_domain_name_check_enabled", true);
this.keySharingEndpointProvideSiteDomainNames = config.getBoolean("key_sharing_endpoint_provide_site_domain_names", false);
this._statsCollectorQueue = statsCollectorQueue;
}

Expand Down Expand Up @@ -510,6 +514,8 @@ public void handleKeysSharing(RoutingContext rc) {
try {
final ClientKey clientKey = AuthMiddleware.getAuthClient(ClientKey.class, rc);
final JsonArray keys = new JsonArray();
final JsonArray sites = new JsonArray();
final Set<Integer> accessibleSites = new HashSet<>();

KeyManagerSnapshot keyManagerSnapshot = this.keyManager.getKeyManagerSnapshot(clientKey.getSiteId());
List<KeysetKey> keysetKeyStore = keyManagerSnapshot.getKeysetKeys();
Expand Down Expand Up @@ -556,9 +562,45 @@ public void handleKeysSharing(RoutingContext rc) {
keyObj.put("expires", key.getExpires().getEpochSecond());
keyObj.put("secret", EncodingUtils.toBase64String(key.getKeyBytes()));
keys.add(keyObj);
accessibleSites.add(keyset.getSiteId());
}
resp.put("keys", keys);

//without cstg enabled, operator won't have site data and siteProvider could be null
//and adding keySharingEndpointProvideSiteDomainNames in case something goes wrong
//and we can still enable cstg feature but turn off site domain name download in
// key/sharing endpoint
if(keySharingEndpointProvideSiteDomainNames && clientSideTokenGenerate) {
for (Integer siteId : accessibleSites.stream().sorted().collect(Collectors.toList())) {
Site s = siteProvider.getSite(siteId);
if(s == null || s.getDomainNames().isEmpty()) {
continue;
}
JsonObject siteObj = new JsonObject();
siteObj.put("id", siteId);
siteObj.put("domain_names", s.getDomainNames().stream().sorted().collect(Collectors.toList()));
sites.add(siteObj);
}
/*
The end result will look something like this:
"site_data": [
{
"id": 101,
"domain_names": [
"101.co.uk",
"101.com"
]
},
{
"id": 102,
"domain_names": [
"102.co.uk",
"102.com"
]
}
]
*/
resp.put("site_data", sites);
}
ResponseUtil.SuccessV2(rc, resp);
} catch (Exception e) {
LOGGER.error("handleKeysSharing", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ public ExtendedUIDOperatorVerticle(JsonObject config,
public IUIDOperatorService getIdService() {
return this.idService;
}

public void setKeySharingEndpointProvideSiteDomainNames(boolean enable) {
this.keySharingEndpointProvideSiteDomainNames = enable;
}

}
85 changes: 80 additions & 5 deletions src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ private void setupConfig(JsonObject config) {
config.put("advertising_token_v4", getTokenVersion() == TokenVersion.V4);
config.put("identity_v3", useIdentityV3());
config.put("client_side_token_generate", true);
config.put("key_sharing_endpoint_provide_site_domain_names", true);
}

private static byte[] makeAesKey(String prefix) {
Expand Down Expand Up @@ -3385,15 +3386,60 @@ void keySharingKeysets_CorrectFiltering(Vertx vertx, VertxTestContext testContex
});
}

@Test
//set some default domain names for all possible sites for each unit test first
private void setupSiteDomainNameMock(int... siteIds) {

Map<Integer, Site> sites = new HashMap<>();
for(int siteId : siteIds) {
Site site = new Site(siteId, "site"+siteId, true, new HashSet<>(Arrays.asList(siteId+".com", siteId+".co.uk")));
sites.put(site.getId(), site);
}

when(siteProvider.getAllSites()).thenReturn(new HashSet<>(sites.values()));
when(siteProvider.getSite(anyInt())).thenAnswer(invocation -> {
int siteId = invocation.getArgument(0);
return sites.get(siteId);
});
}

public HashMap<Integer, List<String>> setupExpectation(int... siteIds)
{
HashMap<Integer, List<String>> expectedSites = new HashMap();
for (int siteId : siteIds)
{
List<String> siteDomains = Arrays.asList(siteId+".co.uk", siteId+".com");
expectedSites.put(siteId, siteDomains);
}
return expectedSites;
}

public void verifyExpectedSiteDetail(HashMap<Integer, List<String>> expectedSites, JsonArray actualResult) {

assertEquals(actualResult.size(), expectedSites.size());
for(int i = 0; i < actualResult.size(); i++) {

JsonObject siteDetail = actualResult.getJsonObject(i);
int siteId = siteDetail.getInteger("id");
assertTrue(expectedSites.get(siteId).containsAll((Collection<String>) siteDetail.getMap().get("domain_names")));
}

return;
}

@ParameterizedTest
@ValueSource(booleans = {true, false})
// Tests:
// ID_READER has access to a keyset that has the same site_id as ID_READER's - direct access
// ID_READER has access to a keyset with a missing allowed_sites - access through sharing
// ID_READER has access to a keyset with allowed_sites that includes us - access through sharing
// ID_READER has no access to a keyset that is disabled - direct reject
// ID_READER has no access to a keyset with an empty allowed_sites - reject by sharing
// ID_READER has no access to a keyset with an allowed_sites for other sites - reject by sharing
void keySharingKeysets_IDREADER(Vertx vertx, VertxTestContext testContext) {
void keySharingKeysets_IDREADER(boolean provideSiteDomainNames, Vertx vertx, VertxTestContext testContext) {

if (!provideSiteDomainNames) {
this.uidOperatorVerticle.setKeySharingEndpointProvideSiteDomainNames(false);
}
String apiVersion = "v2";
int clientSiteId = 101;
fakeAuth(clientSiteId, Role.ID_READER);
Expand Down Expand Up @@ -3426,6 +3472,10 @@ void keySharingKeysets_IDREADER(Vertx vertx, VertxTestContext testContext) {
createKey(1024, now.minusSeconds(5), now.minusSeconds(2), 9)
};

setupSiteDomainNameMock(101, 102, 103, 105);
//site 104 domain name list will be returned but we will set a blank list for it
doReturn(new Site(104, "site104", true, new HashSet<>())).when(siteProvider).getSite(104);

Arrays.sort(expectedKeys, Comparator.comparing(KeysetKey::getId));
send(apiVersion, vertx, apiVersion + "/key/sharing", true, null, null, 200, respJson -> {
System.out.println(respJson);
Expand All @@ -3434,6 +3484,17 @@ void keySharingKeysets_IDREADER(Vertx vertx, VertxTestContext testContext) {
assertEquals(UIDOperatorVerticle.MASTER_KEYSET_ID_FOR_SDKS, respJson.getJsonObject("body").getInteger("master_keyset_id"));
assertEquals(4, respJson.getJsonObject("body").getInteger("default_keyset_id"));
checkEncryptionKeysSharing(respJson, clientSiteId, expectedKeys);

if(provideSiteDomainNames) {
HashMap<Integer, List<String>> expectedSites = setupExpectation(101, 102);
// site 104 has empty domain name list intentionally previously so while site 104 should be included in
// this /key/sharing response, it won't appear in this domain name list
verifyExpectedSiteDetail(expectedSites, respJson.getJsonObject("body").getJsonArray("site_data"));
}
else {
//otherwise we shouldn't even have a 'sites' field
assertNull(respJson.getJsonObject("body").getJsonArray("site_data"));
}
testContext.completeNow();
});
}
Expand All @@ -3452,7 +3513,7 @@ void keySharingKeysets_SHARER(Vertx vertx, VertxTestContext testContext) {
fakeAuth(clientSiteId, Role.SHARER);
MultipleKeysetsTests test = new MultipleKeysetsTests();
//To read these tests, open the MultipleKeysetsTests() constructor in another window so you can see the keyset contents and validate against expectedKeys

setupSiteDomainNameMock(101, 102, 103, 104, 105);
//Keys from these keysets are not expected: keyset6 (disabled keyset), keyset7 (sharing with ID_READERs but not SHARERs), keyset8 (not sharing with 101), keyset10 (not sharing with anyone)
KeysetKey[] expectedKeys = {
createKey(1001, now.minusSeconds(5), now.plusSeconds(3600), MasterKeysetId),
Expand Down Expand Up @@ -3481,6 +3542,10 @@ void keySharingKeysets_SHARER(Vertx vertx, VertxTestContext testContext) {
assertEquals(UIDOperatorVerticle.MASTER_KEYSET_ID_FOR_SDKS, respJson.getJsonObject("body").getInteger("master_keyset_id"));
assertEquals(4, respJson.getJsonObject("body").getInteger("default_keyset_id"));
checkEncryptionKeysSharing(respJson, clientSiteId, expectedKeys);

HashMap<Integer, List<String>> expectedSites = setupExpectation(101, 104);
verifyExpectedSiteDetail(expectedSites, respJson.getJsonObject("body").getJsonArray("site_data"));

testContext.completeNow();
});
}
Expand All @@ -3499,10 +3564,11 @@ void keySharingKeysets_ReturnsMasterAndSite(Vertx vertx, VertxTestContext testCo
new KeysetKey(102, "site key".getBytes(), now, now, now.plusSeconds(10), 10),
};
MultipleKeysetsTests test = new MultipleKeysetsTests(Arrays.asList(keysets), Arrays.asList(encryptionKeys));

setupSiteDomainNameMock(101, 102, 103, 104, 105);
Arrays.sort(encryptionKeys, Comparator.comparing(KeysetKey::getId));
send(apiVersion, vertx, apiVersion + "/key/sharing", true, null, null, 200, respJson -> {
System.out.println(respJson);
verifyExpectedSiteDetail(new HashMap<>(), respJson.getJsonObject("body").getJsonArray("site_data"));
checkEncryptionKeysSharing(respJson, siteId, encryptionKeys);
testContext.completeNow();
});
Expand Down Expand Up @@ -3531,7 +3597,7 @@ void keySharingKeysets_CorrectIDS(String testRun, Vertx vertx, VertxTestContext
new KeysetKey(4, "key4".getBytes(), now, now, now.plusSeconds(10), 7),
};
MultipleKeysetsTests test = new MultipleKeysetsTests(Arrays.asList(keysets), Arrays.asList(encryptionKeys));

setupSiteDomainNameMock(10, 11, 12, 13);
switch (testRun) {
case "NoKeyset":
siteId = 8;
Expand All @@ -3557,15 +3623,24 @@ void keySharingKeysets_CorrectIDS(String testRun, Vertx vertx, VertxTestContext
assertEquals(clientSiteId, respJson.getJsonObject("body").getInteger("caller_site_id"));
assertEquals(UIDOperatorVerticle.MASTER_KEYSET_ID_FOR_SDKS, respJson.getJsonObject("body").getInteger("master_keyset_id"));

JsonArray siteData = respJson.getJsonObject("body").getJsonArray("site_data");

switch (testRun) {
case "NoKeyset":
assertNull(respJson.getJsonObject("body").getInteger("default_keyset_id"));
//no site downloaded
verifyExpectedSiteDetail(new HashMap<>(), siteData);
break;
case "NoKey":
assertEquals(4, respJson.getJsonObject("body").getInteger("default_keyset_id"));
//no site downloaded
verifyExpectedSiteDetail(new HashMap<>(), siteData);
break;
case "SharedKey":
assertEquals(6, respJson.getJsonObject("body").getInteger("default_keyset_id"));
//key 4 returned which has keyset id 7 which in turns has site id 13
HashMap<Integer, List<String>> expectedSites = setupExpectation(13);
verifyExpectedSiteDetail(expectedSites, siteData);
break;
}
checkEncryptionKeysSharing(respJson, clientSiteId, expectedKeys);
Expand Down

0 comments on commit dae0c92

Please sign in to comment.