diff --git a/src/main/java/com/uid2/admin/Main.java b/src/main/java/com/uid2/admin/Main.java index 07c5aece..a5cfebf4 100644 --- a/src/main/java/com/uid2/admin/Main.java +++ b/src/main/java/com/uid2/admin/Main.java @@ -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) }; diff --git a/src/main/java/com/uid2/admin/cloudencryption/CloudEncryptionKeyGenerator.java b/src/main/java/com/uid2/admin/cloudencryption/CloudEncryptionKeyGenerator.java new file mode 100644 index 00000000..ed08d886 --- /dev/null +++ b/src/main/java/com/uid2/admin/cloudencryption/CloudEncryptionKeyGenerator.java @@ -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 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 existingKeys) { + this.lastId = existingKeys.stream().map(CloudEncryptionKey::getId).max(Integer::compareTo).orElse(0); + } + + public int nextId() { + return ++lastId; + } + } +} diff --git a/src/main/java/com/uid2/admin/cloudencryption/CloudKeyRotationStrategy.java b/src/main/java/com/uid2/admin/cloudencryption/CloudKeyRotationStrategy.java index 34c280a9..fec1f636 100644 --- a/src/main/java/com/uid2/admin/cloudencryption/CloudKeyRotationStrategy.java +++ b/src/main/java/com/uid2/admin/cloudencryption/CloudKeyRotationStrategy.java @@ -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; @@ -27,48 +27,29 @@ public CloudKeyRotationStrategy( } public Set computeDesiredKeys( - Collection existingKeys, - Collection sites + Collection existingCloudKeys, + Collection operatorKeys ) { - var idGenerator = new KeyIdGenerator(existingKeys); - Map> existingKeysBySite = existingKeys + var keyGenerator = new CloudEncryptionKeyGenerator(clock, secretGenerator, existingCloudKeys); + Map> 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 desiredKeysForSite( Integer siteId, - KeyIdGenerator idGenerator, + CloudEncryptionKeyGenerator keyGenerator, Set 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 existingKeys) { - this.lastId = existingKeys.stream().map(CloudEncryptionKey::getId).max(Integer::compareTo).orElse(0); - } - - public int nextId() { - return ++lastId; - } - } } diff --git a/src/main/java/com/uid2/admin/vertx/service/CloudEncryptionKeyService.java b/src/main/java/com/uid2/admin/vertx/service/CloudEncryptionKeyService.java index b426bec9..f98add26 100644 --- a/src/main/java/com/uid2/admin/vertx/service/CloudEncryptionKeyService.java +++ b/src/main/java/com/uid2/admin/vertx/service/CloudEncryptionKeyService.java @@ -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; @@ -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(); @@ -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; } @@ -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(); diff --git a/src/test/java/com/uid2/admin/vertx/CloudEncryptionKeyServiceTest.java b/src/test/java/com/uid2/admin/vertx/CloudEncryptionKeyServiceTest.java index 15c07250..6e24237a 100644 --- a/src/test/java/com/uid2/admin/vertx/CloudEncryptionKeyServiceTest.java +++ b/src/test/java/com/uid2/admin/vertx/CloudEncryptionKeyServiceTest.java @@ -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; @@ -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(); @@ -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() { @@ -54,7 +53,7 @@ protected IService createService() { auth, cloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter, - siteProvider, + operatorKeyProvider, rotationStrategy ); } @@ -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); @@ -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); @@ -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); @@ -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); @@ -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 + ); + } }