Skip to content
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 src/main/java/com/uid2/admin/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ public void run() {
new PrivateSiteDataRefreshService(auth, jobDispatcher, writeLock, config, rotatingCloudEncryptionKeyProvider),
new JobDispatcherService(auth, jobDispatcher),
new SearchService(auth, clientKeyProvider, operatorKeyProvider),
new CloudEncryptionKeyService(auth, rotatingCloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter, siteProvider, cloudEncryptionKeyRotationStrategy)
new CloudEncryptionKeyService(auth, rotatingCloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter, operatorKeyProvider, cloudEncryptionKeyRotationStrategy)
};


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.uid2.admin.cloudencryption;

import com.uid2.admin.store.Clock;
import com.uid2.shared.model.CloudEncryptionKey;

import java.util.Collection;

public class CloudEncryptionKeyGenerator {
private final Clock clock;
private final CloudSecretGenerator secretGenerator;
private final KeyIdGenerator idGenerator;

public CloudEncryptionKeyGenerator(
Clock clock,
CloudSecretGenerator secretGenerator,
Collection<CloudEncryptionKey> existingKeys) {
this.clock = clock;
this.secretGenerator = secretGenerator;
this.idGenerator = new KeyIdGenerator(existingKeys);
}

public CloudEncryptionKey makeNewKey(Integer siteId) {
var nowSeconds = clock.getEpochSecond();
var keyId = idGenerator.nextId();
var secret = secretGenerator.generate();
return new CloudEncryptionKey(keyId, siteId, nowSeconds, nowSeconds, secret);
}

private static class KeyIdGenerator {
private int lastId;

public KeyIdGenerator(Collection<CloudEncryptionKey> existingKeys) {
this.lastId = existingKeys.stream().map(CloudEncryptionKey::getId).max(Integer::compareTo).orElse(0);
}

public int nextId() {
return ++lastId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.google.common.collect.Streams;
import com.uid2.admin.store.Clock;
import com.uid2.shared.auth.OperatorKey;
import com.uid2.shared.model.CloudEncryptionKey;
import com.uid2.shared.model.Site;

import java.util.Collection;
import java.util.Map;
Expand All @@ -27,48 +27,29 @@ public CloudKeyRotationStrategy(
}

public Set<CloudEncryptionKey> computeDesiredKeys(
Collection<CloudEncryptionKey> existingKeys,
Collection<Site> sites
Collection<CloudEncryptionKey> existingCloudKeys,
Collection<OperatorKey> operatorKeys
) {
var idGenerator = new KeyIdGenerator(existingKeys);
Map<Integer, Set<CloudEncryptionKey>> existingKeysBySite = existingKeys
var keyGenerator = new CloudEncryptionKeyGenerator(clock, secretGenerator, existingCloudKeys);
Map<Integer, Set<CloudEncryptionKey>> existingKeysBySite = existingCloudKeys
.stream()
.collect(Collectors.groupingBy(CloudEncryptionKey::getSiteId, Collectors.toSet()));

return sites
return operatorKeys
.stream()
.map(Site::getId)
.map(OperatorKey::getSiteId)
.distinct()
.flatMap(siteId -> desiredKeysForSite(siteId, idGenerator, existingKeysBySite.getOrDefault(siteId, Set.of())))
.flatMap(siteId -> desiredKeysForSite(siteId, keyGenerator, existingKeysBySite.getOrDefault(siteId, Set.of())))
.collect(Collectors.toSet());
}

private Stream<CloudEncryptionKey> desiredKeysForSite(
Integer siteId,
KeyIdGenerator idGenerator,
CloudEncryptionKeyGenerator keyGenerator,
Set<CloudEncryptionKey> existingKeys
) {
var existingKeysToRetain = keyRetentionStrategy.selectKeysToRetain(existingKeys);
var newKey = makeNewKey(siteId, idGenerator);
var newKey = keyGenerator.makeNewKey(siteId);
return Streams.concat(existingKeysToRetain.stream(), Stream.of(newKey));
}

private CloudEncryptionKey makeNewKey(Integer siteId, KeyIdGenerator idGenerator) {
var nowSeconds = clock.getEpochSecond();
var keyId = idGenerator.nextId();
var secret = secretGenerator.generate();
return new CloudEncryptionKey(keyId, siteId, nowSeconds, nowSeconds, secret);
}

private static class KeyIdGenerator {
private int lastId;

public KeyIdGenerator(Collection<CloudEncryptionKey> existingKeys) {
this.lastId = existingKeys.stream().map(CloudEncryptionKey::getId).max(Integer::compareTo).orElse(0);
}

public int nextId() {
return ++lastId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import com.uid2.admin.store.writer.CloudEncryptionKeyStoreWriter;
import com.uid2.admin.vertx.Endpoints;
import com.uid2.shared.auth.Role;
import com.uid2.shared.auth.RotatingOperatorKeyProvider;
import com.uid2.shared.model.CloudEncryptionKey;
import com.uid2.shared.store.reader.RotatingCloudEncryptionKeyProvider;
import com.uid2.shared.store.reader.RotatingSiteStore;
import com.uid2.shared.util.Mapper;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.Router;
Expand All @@ -24,7 +24,7 @@
public class CloudEncryptionKeyService implements IService {
private final AdminAuthMiddleware auth;
private final RotatingCloudEncryptionKeyProvider keyProvider;
private final RotatingSiteStore siteProvider;
private final RotatingOperatorKeyProvider operatorKeyProvider;
private final CloudEncryptionKeyStoreWriter keyWriter;
private final CloudKeyRotationStrategy rotationStrategy;
private static final ObjectMapper OBJECT_MAPPER = Mapper.getInstance();
Expand All @@ -33,12 +33,12 @@ public CloudEncryptionKeyService(
AdminAuthMiddleware auth,
RotatingCloudEncryptionKeyProvider keyProvider,
CloudEncryptionKeyStoreWriter keyWriter,
RotatingSiteStore siteProvider,
RotatingOperatorKeyProvider operatorKeyProvider,
CloudKeyRotationStrategy rotationStrategy
) {
this.auth = auth;
this.keyProvider = keyProvider;
this.siteProvider = siteProvider;
this.operatorKeyProvider = operatorKeyProvider;
this.keyWriter = keyWriter;
this.rotationStrategy = rotationStrategy;
}
Expand All @@ -57,11 +57,11 @@ public void setupRoutes(Router router) {
private void handleRotate(RoutingContext rc) {
try {
keyProvider.loadContent();
siteProvider.loadContent();
var allSites = siteProvider.getAllSites();
operatorKeyProvider.loadContent(operatorKeyProvider.getMetadata());
var operatorKeys = operatorKeyProvider.getAll();
var existingKeys = keyProvider.getAll().values();

var desiredKeys = rotationStrategy.computeDesiredKeys(existingKeys, allSites);
var desiredKeys = rotationStrategy.computeDesiredKeys(existingKeys, operatorKeys);
writeKeys(desiredKeys);

rc.response().end();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import com.uid2.admin.vertx.service.CloudEncryptionKeyService;
import com.uid2.admin.vertx.service.IService;
import com.uid2.admin.vertx.test.ServiceTestBase;
import com.uid2.shared.auth.OperatorKey;
import com.uid2.shared.auth.Role;
import com.uid2.shared.model.CloudEncryptionKey;
import com.uid2.shared.model.Site;
import com.uid2.shared.util.Mapper;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
Expand All @@ -24,8 +24,7 @@
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;

public class CloudEncryptionKeyServiceTest extends ServiceTestBase {
private static final ObjectMapper OBJECT_MAPPER = Mapper.getInstance();
Expand All @@ -38,12 +37,12 @@ public class CloudEncryptionKeyServiceTest extends ServiceTestBase {
private final int keyId2 = 2;
private final int keyId3 = 3;
private final int keyId4 = 4;
private final String siteName1 = "Site 1";
private final Site site1 = new Site(siteId1, siteName1, true);
private final OperatorKey operator1 = testOperatorKey(siteId1, "one");
private final OperatorKey operator2 = testOperatorKey(siteId1, "two");
private final String secret1 = "secret 1";
private final String secret2 = "secret 2";
private final String secret3 = "secret 3";
private final String secret4 = "secret4";
private final String secret4 = "secret 4";

@Override
protected IService createService() {
Expand All @@ -54,7 +53,7 @@ protected IService createService() {
auth,
cloudEncryptionKeyProvider,
cloudEncryptionKeyStoreWriter,
siteProvider,
operatorKeyProvider,
rotationStrategy
);
}
Expand Down Expand Up @@ -127,30 +126,27 @@ public void testRotate_canBeRotatedBySecretRotationJob(Vertx vertx, VertxTestCon
}

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

setCloudEncryptionKeys();
setSites();
setOperatorKeys();

post(vertx, testContext, Endpoints.CLOUD_ENCRYPTION_KEY_ROTATE, null, rotateResponse -> {
assertEquals(200, rotateResponse.statusCode());

get(vertx, testContext, Endpoints.CLOUD_ENCRYPTION_KEY_LIST, listResponse -> {
assertEquals(200, listResponse.statusCode());
assertEquals(noKeys, parseKeyListResponse(listResponse));
verify(cloudEncryptionKeyStoreWriter).upload(Map.of(), null);

testContext.completeNow();
});
testContext.completeNow();
});
}

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

setCloudEncryptionKeys();
setSites(site1);
setOperatorKeys(operator1);
when(cloudSecretGenerator.generate()).thenReturn(secret1);
when(clock.getEpochSecond()).thenReturn(now);

Expand All @@ -166,7 +162,26 @@ siteId1, new CloudEncryptionKey(keyId1, siteId1, now, now, secret1)
}

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

setCloudEncryptionKeys();
setOperatorKeys(operator1, operator2);

post(vertx, testContext, Endpoints.CLOUD_ENCRYPTION_KEY_ROTATE, null, rotateResponse -> {
assertEquals(200, rotateResponse.statusCode());

get(vertx, testContext, Endpoints.CLOUD_ENCRYPTION_KEY_LIST, listResponse -> {
assertEquals(200, listResponse.statusCode());
assertEquals(noKeys, parseKeyListResponse(listResponse));

testContext.completeNow();
});
});
}

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

var existingKey1 = new CloudEncryptionKey(keyId1, siteId1, before, before, secret1);
Expand All @@ -181,7 +196,7 @@ keyId4, new CloudEncryptionKey(4, siteId1, now, now, secret4)
);

setCloudEncryptionKeys(existingKey1, existingKey2, existingKey3);
setSites(site1);
setOperatorKeys(operator1);
when(cloudSecretGenerator.generate()).thenReturn(secret4);
when(clock.getEpochSecond()).thenReturn(now);

Expand All @@ -205,7 +220,7 @@ key2Id, new CloudEncryptionKey(key2Id, siteId1, now, now, secret2)
);

setCloudEncryptionKeys(existingKey);
setSites(site1);
setOperatorKeys(operator1);
when(cloudSecretGenerator.generate()).thenReturn(secret2);
when(clock.getEpochSecond()).thenReturn(now);

Expand All @@ -220,4 +235,18 @@ private static CloudEncryptionKeyListResponse parseKeyListResponse(HttpResponse<
return OBJECT_MAPPER.readValue(response.bodyAsString(), new TypeReference<>() {
});
}

private static OperatorKey testOperatorKey(int siteId, String keyId) {
return new OperatorKey(
"hash " + keyId,
"key salt " + keyId,
"name " + keyId,
"contact " + keyId,
"protocol " + keyId,
0,
false,
siteId,
keyId
);
}
}