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

UID2-3506 Add functionality on oncall page to list related keysets and rotate them #329

Merged
merged 23 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7456d0e
Add backend change for listing related keysets
cYKatherine Aug 5, 2024
92ecbae
Improve logic on backend filtering
cYKatherine Aug 5, 2024
413f065
Add frontend to highlight related keysets
cYKatherine Aug 5, 2024
837ddbb
Add functionality to rotate keysets
cYKatherine Aug 5, 2024
454aa90
Get ClientTypes from backend
cYKatherine Aug 6, 2024
dce0e23
Remove condition for force check
cYKatherine Aug 6, 2024
dce1109
Check if a site has any client key that has ID_READER role
cYKatherine Aug 6, 2024
950d2a8
Add helper text to explain min age and force option
cYKatherine Aug 6, 2024
5ddf6b0
Check ID_READER role with correct type
cYKatherine Aug 6, 2024
056f414
Add tests for related keyset api
cYKatherine Aug 6, 2024
9c215fc
Update comments
cYKatherine Aug 6, 2024
d25ca9a
Update highlights for client types
cYKatherine Aug 6, 2024
3234fe8
Revert changes for keyset.json
cYKatherine Aug 6, 2024
2e05764
Revert unused functions
cYKatherine Aug 6, 2024
17f4b8c
Revert unused clock
cYKatherine Aug 6, 2024
9875b73
Add changes to show rotation result
cYKatherine Aug 6, 2024
b22494a
Update `min_age_seconds` to 1
cYKatherine Aug 7, 2024
6e0e979
Use `Collections.disjoint` instead of customised `ContainAny`
cYKatherine Aug 7, 2024
bbc1a36
Modify logic for verifying site id
cYKatherine Aug 7, 2024
5c6af71
Add comments for moving checking keyset logic to shared
cYKatherine Aug 7, 2024
d06b81b
Fix logic of disjoint
cYKatherine Aug 7, 2024
49d0971
Add prompt to confirm rotation
cYKatherine Aug 8, 2024
9a20263
Update wordings for the note
cYKatherine Aug 8, 2024
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 src/main/java/com/uid2/admin/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public void run() {
new EnclaveIdService(auth, writeLock, enclaveStoreWriter, enclaveIdProvider, clock),
encryptionKeyService,
new KeyAclService(auth, writeLock, keyAclStoreWriter, keyAclProvider, siteProvider, encryptionKeyService),
new SharingService(auth, writeLock, adminKeysetProvider, keysetManager, siteProvider, enableKeysets),
new SharingService(auth, writeLock, adminKeysetProvider, keysetManager, siteProvider, enableKeysets, clientKeyProvider),
vishalegbert-ttd marked this conversation as resolved.
Show resolved Hide resolved
clientSideKeypairService,
new ServiceService(auth, writeLock, serviceStoreWriter, serviceProvider, siteProvider, serviceLinkProvider),
new ServiceLinkService(auth, writeLock, serviceLinkStoreWriter, serviceLinkProvider, serviceProvider, siteProvider),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.uid2.shared.store.parser.Parser;
import com.uid2.shared.store.parser.ParsingResult;
import com.uid2.shared.Utils;
import com.uid2.shared.auth.KeysetSnapshot;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

Expand Down
65 changes: 64 additions & 1 deletion src/main/java/com/uid2/admin/vertx/service/SharingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

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.auth.KeysetSnapshot;
import com.uid2.shared.auth.Role;
import com.uid2.shared.model.ClientType;
import com.uid2.shared.model.SiteUtil;
Expand All @@ -30,6 +34,7 @@ public class SharingService implements IService {
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;
Expand All @@ -39,13 +44,15 @@ public SharingService(AdminAuthMiddleware auth,
RotatingAdminKeysetStore keysetProvider,
KeysetManager keysetManager,
RotatingSiteStore siteProvider,
boolean enableKeyset) {
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
Expand All @@ -70,6 +77,9 @@ public void setupRoutes(Router router) {
router.get("/api/sharing/keyset/:keyset_id").handler(
auth.handle(this::handleListKeyset, Role.MAINTAINER)
);
router.get("/api/sharing/keysets/related").handler(
auth.handle(this::handleListAllKeysetsRelated, Role.MAINTAINER)
);
}

private void handleSetKeyset(RoutingContext rc) {
Expand Down Expand Up @@ -150,6 +160,59 @@ private void handleSetKeyset(RoutingContext rc) {
}
}

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;
for (LegacyClientKey c : this.clientKeyProvider.getAll()) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ private static void assertAddedClientKeyEquals(ClientKey expected, ClientKey act
.isEqualTo(expected);
}

private static class LegacyClientBuilder {
public static class LegacyClientBuilder {
vishalegbert-ttd marked this conversation as resolved.
Show resolved Hide resolved
private String name = "test_client";
private String contact = "test_contact";
private int siteId = 999;
Expand Down
159 changes: 152 additions & 7 deletions src/test/java/com/uid2/admin/vertx/SharingServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,19 @@
import org.junit.jupiter.params.provider.ValueSource;

import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;

public class SharingServiceTest extends ServiceTestBase {
@Override
protected IService createService() {
KeysetManager keysetManager = new KeysetManager(adminKeysetProvider, adminKeysetWriter, keysetKeyManager, true);
return new SharingService(auth, writeLock, adminKeysetProvider, keysetManager, siteProvider, true);
return new SharingService(auth, writeLock, adminKeysetProvider, keysetManager, siteProvider, true, clientKeyProvider);
}

private void compareKeysetListToResult(AdminKeyset keyset, JsonArray actualList) {
Expand Down Expand Up @@ -1259,4 +1255,153 @@ void KeysetSetNewWithType(Vertx vertx, VertxTestContext testContext) {
testContext.completeNow();
});
}

@Test
void RelatedKeysetSetsWithClientTypes(Vertx vertx, VertxTestContext testContext) {
fakeAuth(Role.MAINTAINER);

AdminKeyset adminKeyset1 = new AdminKeyset(3, 5, "test", Set.of(4,6,7), Instant.now().getEpochSecond(),true, true, new HashSet<>(Arrays.asList(ClientType.DSP, ClientType.PUBLISHER)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth checking out the test cases for canClientAccessKey

AdminKeyset adminKeyset2 = new AdminKeyset(4, 7, "test", Set.of(12), Instant.now().getEpochSecond(),true, true, new HashSet<>(Arrays.asList(ClientType.DSP)));
AdminKeyset adminKeyset3 = new AdminKeyset(5, 4, "test", Set.of(5), Instant.now().getEpochSecond(),true, true, new HashSet<>());

Map<Integer, AdminKeyset> keysets = new HashMap<Integer, AdminKeyset>() {{
put(3, adminKeyset1);
put(4, adminKeyset2);
put(5, adminKeyset3);
}};

setAdminKeysets(keysets);
mockSiteExistence(5, 7, 4, 8, 22, 25, 6);
doReturn(new Site(8, "test-name", true, new HashSet<>(Arrays.asList(ClientType.DSP)), null)).when(siteProvider).getSite(8);


get(vertx, testContext, "/api/sharing/keysets/related?site_id=8", response -> {
assertEquals(200, response.statusCode());

Set<Integer> expectedKeysetIds = new HashSet<>(Arrays.asList(adminKeyset1.getKeysetId(), adminKeyset2.getKeysetId()));

Set<Integer> actualKeysetIds = new HashSet<>();
JsonArray responseArray = response.bodyAsJsonArray();
for (int i = 0; i < responseArray.size(); i++) {
JsonObject item = responseArray.getJsonObject(i);
int keysetId = item.getInteger("keyset_id");
actualKeysetIds.add(keysetId);
}
assertEquals(true, actualKeysetIds.containsAll(expectedKeysetIds));
testContext.completeNow();
});
}

@Test
void RelatedKeysetSetsWithAllowedSites(Vertx vertx, VertxTestContext testContext) {
fakeAuth(Role.MAINTAINER);

AdminKeyset adminKeyset1 = new AdminKeyset(3, 1, "test", Set.of(4,8), Instant.now().getEpochSecond(),true, true, new HashSet<>());
AdminKeyset adminKeyset2 = new AdminKeyset(4, 2, "test", Set.of(5,8), Instant.now().getEpochSecond(),true, true, new HashSet<>());
AdminKeyset adminKeyset3 = new AdminKeyset(5, 3, "test", Set.of(6), Instant.now().getEpochSecond(),true, true, new HashSet<>());

Map<Integer, AdminKeyset> keysets = new HashMap<Integer, AdminKeyset>() {{
put(3, adminKeyset1);
put(4, adminKeyset2);
put(5, adminKeyset3);
}};

setAdminKeysets(keysets);
mockSiteExistence(1,2,3,4,5,6,8);
doReturn(new Site(8, "test-name", true,null)).when(siteProvider).getSite(8);


get(vertx, testContext, "/api/sharing/keysets/related?site_id=8", response -> {
assertEquals(200, response.statusCode());

Set<Integer> expectedKeysetIds = new HashSet<>(Arrays.asList(adminKeyset1.getKeysetId(), adminKeyset2.getKeysetId()));

Set<Integer> actualKeysetIds = new HashSet<>();
JsonArray responseArray = response.bodyAsJsonArray();
for (int i = 0; i < responseArray.size(); i++) {
JsonObject item = responseArray.getJsonObject(i);
int keysetId = item.getInteger("keyset_id");
actualKeysetIds.add(keysetId);
}
assertEquals(true, actualKeysetIds.containsAll(expectedKeysetIds));
testContext.completeNow();
});
}

@Test
void RelatedKeysetSetsWithSameSiteId(Vertx vertx, VertxTestContext testContext) {
fakeAuth(Role.MAINTAINER);

AdminKeyset adminKeyset1 = new AdminKeyset(3, 1, "test", Set.of(4), Instant.now().getEpochSecond(),true, true, new HashSet<>());
AdminKeyset adminKeyset2 = new AdminKeyset(4, 2, "test", Set.of(5), Instant.now().getEpochSecond(),true, true, new HashSet<>());
AdminKeyset adminKeyset3 = new AdminKeyset(5, 8, "test", Set.of(6), Instant.now().getEpochSecond(),true, true, new HashSet<>());

Map<Integer, AdminKeyset> keysets = new HashMap<Integer, AdminKeyset>() {{
put(3, adminKeyset1);
put(4, adminKeyset2);
put(5, adminKeyset3);
}};

setAdminKeysets(keysets);
mockSiteExistence(1,2,4,5,6,8);
doReturn(new Site(8, "test-name", true,null)).when(siteProvider).getSite(8);


get(vertx, testContext, "/api/sharing/keysets/related?site_id=8", response -> {
assertEquals(200, response.statusCode());

Set<Integer> expectedKeysetIds = new HashSet<>(Arrays.asList(adminKeyset3.getKeysetId()));

Set<Integer> actualKeysetIds = new HashSet<>();
JsonArray responseArray = response.bodyAsJsonArray();
for (int i = 0; i < responseArray.size(); i++) {
JsonObject item = responseArray.getJsonObject(i);
int keysetId = item.getInteger("keyset_id");
actualKeysetIds.add(keysetId);
}
assertEquals(true, actualKeysetIds.containsAll(expectedKeysetIds));
testContext.completeNow();
});
}

@Test
void RelatedKeysetSetsWithAllowSiteNull(Vertx vertx, VertxTestContext testContext) {
fakeAuth(Role.MAINTAINER);

AdminKeyset adminKeyset1 = new AdminKeyset(3, 1, "test", Set.of(4), Instant.now().getEpochSecond(),true, true, new HashSet<>());
AdminKeyset adminKeyset2 = new AdminKeyset(4, 2, "test", Set.of(5), Instant.now().getEpochSecond(),true, true, new HashSet<>());
AdminKeyset adminKeyset3 = new AdminKeyset(5, 3, "test", null, Instant.now().getEpochSecond(),true, true, new HashSet<>());

Map<Integer, AdminKeyset> keysets = new HashMap<Integer, AdminKeyset>() {{
put(3, adminKeyset1);
put(4, adminKeyset2);
put(5, adminKeyset3);
}};

setAdminKeysets(keysets);
mockSiteExistence(1,2,3,4,5,8);
doReturn(new Site(8, "test-name", true,null)).when(siteProvider).getSite(8);
setClientKeys(
new ClientKeyServiceTest.LegacyClientBuilder()
.withRoles(new HashSet<>(Arrays.asList(Role.ID_READER)))
.withSiteId(8)
.build());


get(vertx, testContext, "/api/sharing/keysets/related?site_id=8", response -> {
assertEquals(200, response.statusCode());

Set<Integer> expectedKeysetIds = new HashSet<>(Arrays.asList(adminKeyset3.getKeysetId()));

Set<Integer> actualKeysetIds = new HashSet<>();
JsonArray responseArray = response.bodyAsJsonArray();
for (int i = 0; i < responseArray.size(); i++) {
JsonObject item = responseArray.getJsonObject(i);
int keysetId = item.getInteger("keyset_id");
actualKeysetIds.add(keysetId);
}
assertEquals(true, actualKeysetIds.containsAll(expectedKeysetIds));
testContext.completeNow();
});
}
}
26 changes: 26 additions & 0 deletions webroot/adm/oncall/participant-summary.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,32 @@ <h5>Participant Opt-out Webhook</h5>
<pre id="webhooksStandardOutput"></pre>
</div>
</div>
<div class="row px-2">
<div class="col section">
<h5>Participant Related Keysets</h5>
<div><b>A keyset is related to the participant if it matches below criteria:</b><br>
a. Keysets where allowed_types include any of the clientTypes of the site<br>
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<br>
c. Keysets where allowed_sites include the leaked site<br>
d. Keysets belonging to the leaked site itself<br></div>
<pre class="errorDiv" id="relatedKeysetsErrorOutput"></pre>
<pre id="relatedKeysetsStandardOutput"></pre>

<div class="col">
<a href="#" class="btn btn-primary" id="doRotateKeysets">Rotate Keysets</a>
<div>
Normally, keys don't become active for 24 hours when rotated, which gives participants 24 hours before they need to call sdk.refresh().
However in this case, rotation will make the new keys active immediately.
This means participants will not be able to decrypt newly created UID tokens until they have called sdk.refresh().
Note that we recommend calling sdk.refresh() once per hour (see <a href="https://unifiedid.com/docs/getting-started/gs-faqs#where-do-i-get-the-decryption-keys">documentation</a>)
</div>
<div id="output">
<pre id="rotateKeysetsErrorOutput"></pre>
<pre id="rotateKeysetsStandardOutput"></pre>
</div>
</div>
</div>
</div>
</div>
</div>

Expand Down
Loading