From 29ddb56719ab9f7ed663dce9f979f9753681c44c Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:56:27 +0000 Subject: [PATCH] Implemented getAllCredentials method for etcd credentials store (#282) * feat: Implement getAllCredentials() for etcd creds store Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * fix: Clear any existing properties for a secret before creating/updating it in etcd Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Add description, lastUpdatedTime, and lastUpdatedBy to credentials Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --------- Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .secrets.baseline | 2 +- .../etcd/internal/Etcd3CredentialsStore.java | 140 +++++++++-- .../galasa/cps/etcd/internal/Etcd3Store.java | 25 ++ .../internal/Etcd3CredentialsStoreTest.java | 219 ++++++++++++++++++ .../etcd/internal/mocks/MockEtcdKvClient.java | 54 ++++- 5 files changed, 403 insertions(+), 37 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index e6b35c8..9aeed62 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -81,7 +81,7 @@ "hashed_secret": "1beb7496ebbe82c61151be093956d83dac625c13", "is_secret": false, "is_verified": false, - "line_number": 246, + "line_number": 430, "type": "Secret Keyword", "verified_result": null } diff --git a/galasa-extensions-parent/dev.galasa.cps.etcd/src/main/java/dev/galasa/cps/etcd/internal/Etcd3CredentialsStore.java b/galasa-extensions-parent/dev.galasa.cps.etcd/src/main/java/dev/galasa/cps/etcd/internal/Etcd3CredentialsStore.java index ab3d854..dd5e9e9 100644 --- a/galasa-extensions-parent/dev.galasa.cps.etcd/src/main/java/dev/galasa/cps/etcd/internal/Etcd3CredentialsStore.java +++ b/galasa-extensions-parent/dev.galasa.cps.etcd/src/main/java/dev/galasa/cps/etcd/internal/Etcd3CredentialsStore.java @@ -9,9 +9,15 @@ import java.net.URI; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.Instant; import java.util.Properties; +import java.util.Set; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import javax.crypto.spec.SecretKeySpec; @@ -38,6 +44,9 @@ public class Etcd3CredentialsStore extends Etcd3Store implements ICredentialsSto private final SecretKeySpec key; private final IEncryptionService encryptionService; + private static final String CREDS_NAMESPACE = "secure"; + private static final String CREDS_PROPERTY_PREFIX = CREDS_NAMESPACE + ".credentials."; + /** * This constructor instantiates the Key value client that can retrieve values * from the etcd store. @@ -48,7 +57,7 @@ public class Etcd3CredentialsStore extends Etcd3Store implements ICredentialsSto public Etcd3CredentialsStore(IFramework framework, URI etcd) throws CredentialsException { super(etcd); try { - IConfigurationPropertyStoreService cpsService = framework.getConfigurationPropertyService("secure"); + IConfigurationPropertyStoreService cpsService = framework.getConfigurationPropertyService(CREDS_NAMESPACE); String encryptionKey = cpsService.getProperty("credentials.file", "encryption.key"); if (encryptionKey != null) { key = createKey(encryptionKey); @@ -79,31 +88,16 @@ public Etcd3CredentialsStore(SecretKeySpec key, IEncryptionService encryptionSer * @throws CredentialsException A failure occurred. */ public ICredentials getCredentials(String credentialsId) throws CredentialsException { + ICredentials credentials = null; try { - ICredentials credentials = null; - String token = get("secure.credentials." + credentialsId + ".token"); - String username = get("secure.credentials." + credentialsId + ".username"); - - // Check if the credentials are UsernameToken or Token - if (token != null && username != null) { - credentials = new CredentialsUsernameToken(key, username, token); - } else if (token != null) { - credentials = new CredentialsToken(key, token); - } else if (username != null) { - // We have a username, so check if the credentials are UsernamePassword or Username - String password = get("secure.credentials." + credentialsId + ".password"); - if (password != null) { - credentials = new CredentialsUsernamePassword(key, username, password); - } else { - credentials = new CredentialsUsername(key, username); - } - } - - return credentials; + Map credentialsProperties = getPrefix(CREDS_PROPERTY_PREFIX + credentialsId); + credentials = convertPropertiesIntoCredentials(credentialsProperties, credentialsId); + } catch (InterruptedException | ExecutionException e) { Thread.currentThread().interrupt(); throw new CredentialsException("Failed to get credentials", e); } + return credentials; } private static SecretKeySpec createKey(String secret) @@ -122,11 +116,14 @@ public void shutdown() throws CredentialsException { @Override public void setCredentials(String credentialsId, ICredentials credentials) throws CredentialsException { Properties credentialProperties = credentials.toProperties(credentialsId); + Properties metadataProperties = credentials.getMetadataProperties(credentialsId); try { - for (Entry property : credentialProperties.entrySet()) { - put((String) property.getKey(), encryptionService.encrypt((String) property.getValue())); - } + // Clear any existing properties with the same credentials ID + deleteCredentials(credentialsId); + + putAllProperties(credentialProperties, true); + putAllProperties(metadataProperties, false); } catch (InterruptedException | ExecutionException e) { Thread.currentThread().interrupt(); throw new CredentialsException("Failed to set credentials", e); @@ -136,10 +133,103 @@ public void setCredentials(String credentialsId, ICredentials credentials) throw @Override public void deleteCredentials(String credentialsId) throws CredentialsException { try { - deletePrefix("secure.credentials." + credentialsId); + deletePrefix(CREDS_PROPERTY_PREFIX + credentialsId); } catch (InterruptedException | ExecutionException e) { Thread.currentThread().interrupt(); throw new CredentialsException("Failed to delete credentials", e); } } + + @Override + public Map getAllCredentials() throws CredentialsException { + Map credentials = new HashMap<>(); + try { + Map credentialsKeyValues = getPrefix(CREDS_PROPERTY_PREFIX); + + // Build a set of all credential IDs stored in etcd + Set> credentialsEntries = credentialsKeyValues.entrySet(); + Set credentialIds = new HashSet<>(); + for (Entry entry : credentialsEntries) { + String credsId = getCredentialsIdFromKey(entry.getKey()); + if (credsId != null) { + credentialIds.add(credsId); + } + } + + // For each credential ID, convert its properties into a credentials object for use by the framework + for (String id : credentialIds) { + Map idProperties = credentialsEntries.stream() + .filter(entry -> entry.getKey().contains("." + id + ".")) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + ICredentials convertedCredentials = convertPropertiesIntoCredentials(idProperties, id); + if (convertedCredentials != null) { + credentials.put(id, convertedCredentials); + } + } + + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new CredentialsException("Failed to get credentials", e); + } + return credentials; + } + + private ICredentials convertPropertiesIntoCredentials(Map credProperties, String credentialsId) throws CredentialsException { + String token = credProperties.get(CREDS_PROPERTY_PREFIX + credentialsId + ".token"); + String username = credProperties.get(CREDS_PROPERTY_PREFIX + credentialsId + ".username"); + String password = credProperties.get(CREDS_PROPERTY_PREFIX + credentialsId + ".password"); + + ICredentials credentials = null; + + // Check if the credentials are UsernameToken or Token + if (token != null && username != null) { + credentials = new CredentialsUsernameToken(key, username, token); + } else if (token != null) { + credentials = new CredentialsToken(key, token); + } else if (username != null) { + // We have a username, so check if the credentials are UsernamePassword or Username + if (password != null) { + credentials = new CredentialsUsernamePassword(key, username, password); + } else { + credentials = new CredentialsUsername(key, username); + } + } + + if (credentials != null) { + String description = credProperties.get(CREDS_PROPERTY_PREFIX + credentialsId + ".description"); + String lastUpdatedTime = credProperties.get(CREDS_PROPERTY_PREFIX + credentialsId + ".lastUpdated.time"); + String lastUpdatedUser = credProperties.get(CREDS_PROPERTY_PREFIX + credentialsId + ".lastUpdated.user"); + + credentials.setDescription(description); + credentials.setLastUpdatedByUser(lastUpdatedUser); + if (lastUpdatedTime != null) { + credentials.setLastUpdatedTime(Instant.parse(lastUpdatedTime)); + } + } + return credentials; + } + + private String getCredentialsIdFromKey(String key) { + // Keys for credentials should be in the form: + // secure.credentials.CRED_ID.suffix + // so let's split on "." and grab the third part + String credentialsId = null; + String[] keyParts = key.split("\\."); + if (keyParts.length >= 3) { + credentialsId = keyParts[2]; + } + return credentialsId; + } + + private void putAllProperties(Properties properties, boolean encryptValues) throws CredentialsException, InterruptedException, ExecutionException { + for (Entry property : properties.entrySet()) { + String key = (String) property.getKey(); + String value = (String) property.getValue(); + if (encryptValues) { + value = encryptionService.encrypt(value); + } + put(key, value); + } + } } diff --git a/galasa-extensions-parent/dev.galasa.cps.etcd/src/main/java/dev/galasa/cps/etcd/internal/Etcd3Store.java b/galasa-extensions-parent/dev.galasa.cps.etcd/src/main/java/dev/galasa/cps/etcd/internal/Etcd3Store.java index a8856f9..9fc7d9b 100644 --- a/galasa-extensions-parent/dev.galasa.cps.etcd/src/main/java/dev/galasa/cps/etcd/internal/Etcd3Store.java +++ b/galasa-extensions-parent/dev.galasa.cps.etcd/src/main/java/dev/galasa/cps/etcd/internal/Etcd3Store.java @@ -9,7 +9,9 @@ import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -21,6 +23,7 @@ import io.etcd.jetcd.KeyValue; import io.etcd.jetcd.kv.GetResponse; import io.etcd.jetcd.options.DeleteOption; +import io.etcd.jetcd.options.GetOption; /** * Abstract class containing common methods used to interact with etcd, like getting, setting, @@ -53,6 +56,28 @@ protected String get(String key) throws InterruptedException, ExecutionException return retrievedKey; } + protected Map getPrefix(String keyPrefix) throws InterruptedException, ExecutionException { + Map keyValues = new HashMap<>(); + + ByteSequence bsPrefix = ByteSequence.from(keyPrefix, UTF_8); + GetOption options = GetOption.newBuilder().isPrefix(true).build(); + CompletableFuture getFuture = kvClient.get(bsPrefix, options); + + GetResponse response = getFuture.get(); + List kvs = response.getKvs(); + + for (KeyValue kv : kvs) { + // jetcd's getKey() method strips off the given prefix from matching keys, so add them back in + String key = kv.getKey().toString(UTF_8); + if (!key.startsWith(keyPrefix)) { + key = keyPrefix + key; + } + keyValues.put(key, kv.getValue().toString(UTF_8)); + } + + return keyValues; + } + protected void put(String key, String value) throws InterruptedException, ExecutionException { ByteSequence bytesKey = ByteSequence.from(key, UTF_8); ByteSequence bytesValue = ByteSequence.from(value, UTF_8); diff --git a/galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/Etcd3CredentialsStoreTest.java b/galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/Etcd3CredentialsStoreTest.java index fe519ef..4459113 100644 --- a/galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/Etcd3CredentialsStoreTest.java +++ b/galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/Etcd3CredentialsStoreTest.java @@ -5,11 +5,13 @@ */ package dev.galasa.etcd.internal; +import java.time.Instant; import java.util.HashMap; import java.util.Map; import org.junit.Test; +import dev.galasa.ICredentials; import dev.galasa.cps.etcd.internal.Etcd3CredentialsStore; import dev.galasa.etcd.internal.mocks.MockEncryptionService; import dev.galasa.etcd.internal.mocks.MockEtcdClient; @@ -22,6 +24,155 @@ public class Etcd3CredentialsStoreTest { + @Test + public void testGetAllCredentialsReturnsCredentialsOk() throws Exception { + // Given... + MockEncryptionService mockEncryptionService = new MockEncryptionService(); + Map mockCreds = new HashMap<>(); + String credsId1 = "CRED1"; + String username1 = "my-user"; + String token1 = "my-token"; + + mockCreds.put("secure.credentials." + credsId1 + ".username", username1); + mockCreds.put("secure.credentials." + credsId1 + ".token", token1); + + String credsId2 = "CRED2"; + String username2 = "another-username"; + String password2 = "a-password"; + mockCreds.put("secure.credentials." + credsId2 + ".username", username2); + mockCreds.put("secure.credentials." + credsId2 + ".password", password2); + + String credsId3 = "CRED3"; + String token3 = "another-token"; + mockCreds.put("secure.credentials." + credsId3 + ".token", token3); + + MockEtcdClient mockClient = new MockEtcdClient(mockCreds); + Etcd3CredentialsStore store = new Etcd3CredentialsStore(null, mockEncryptionService, mockClient); + + // When... + Map creds = store.getAllCredentials(); + + // Then... + assertThat(creds).isNotNull(); + assertThat(creds).hasSize(3); + + CredentialsUsernameToken actualCreds1 = (CredentialsUsernameToken) creds.get(credsId1); + CredentialsUsernamePassword actualCreds2 = (CredentialsUsernamePassword) creds.get(credsId2); + CredentialsToken actualCreds3 = (CredentialsToken) creds.get(credsId3); + + assertThat(actualCreds1.getUsername()).isEqualTo(username1); + assertThat(actualCreds1.getToken()).isEqualTo(token1.getBytes()); + + assertThat(actualCreds2.getUsername()).isEqualTo(username2); + assertThat(actualCreds2.getPassword()).isEqualTo(password2); + + assertThat(actualCreds3.getToken()).isEqualTo(token3.getBytes()); + } + + @Test + public void testGetAllCredentialsWithMissingPropertySuffixReturnsValidProperties() throws Exception { + // Given... + MockEncryptionService mockEncryptionService = new MockEncryptionService(); + Map mockCreds = new HashMap<>(); + String credsId1 = "CRED1"; + String username1 = "my-user"; + String token1 = "my-token"; + + mockCreds.put("secure.credentials." + credsId1 + ".username", username1); + mockCreds.put("secure.credentials." + credsId1 + ".token", token1); + + String credsId2 = "CRED2"; + String username2 = "another-username"; + String password2 = "a-password"; + mockCreds.put("secure.credentials." + credsId2 + ".password", password2); + + // This property is missing a credentials ID and suffix, so the property should be ignored + mockCreds.put("secure.credentials", username2); + + MockEtcdClient mockClient = new MockEtcdClient(mockCreds); + Etcd3CredentialsStore store = new Etcd3CredentialsStore(null, mockEncryptionService, mockClient); + + // When... + Map creds = store.getAllCredentials(); + + // Then... + assertThat(creds).isNotNull(); + assertThat(creds).hasSize(1); + + CredentialsUsernameToken actualCreds1 = (CredentialsUsernameToken) creds.get(credsId1); + + assertThat(actualCreds1.getUsername()).isEqualTo(username1); + assertThat(actualCreds1.getToken()).isEqualTo(token1.getBytes()); + } + + @Test + public void testGetAllCredentialsWithBadlyFormedPropertyReturnsValidProperties() throws Exception { + // Given... + MockEncryptionService mockEncryptionService = new MockEncryptionService(); + Map mockCreds = new HashMap<>(); + String credsId1 = "CRED1"; + String username1 = "my-user"; + String token1 = "my-token"; + + mockCreds.put("secure.credentials." + credsId1 + ".username", username1); + mockCreds.put("secure.credentials." + credsId1 + ".token", token1); + + String credsId2 = "CRED2"; + String username2 = "another-username"; + String password2 = "a-password"; + mockCreds.put("secure.credentials." + credsId2 + ".password", password2); + + // This property is missing a ".username" suffix, so the credential should be ignored + mockCreds.put("secure.credentials." + credsId2, username2); + + MockEtcdClient mockClient = new MockEtcdClient(mockCreds); + Etcd3CredentialsStore store = new Etcd3CredentialsStore(null, mockEncryptionService, mockClient); + + // When... + Map creds = store.getAllCredentials(); + + // Then... + assertThat(creds).isNotNull(); + assertThat(creds).hasSize(1); + + CredentialsUsernameToken actualCreds1 = (CredentialsUsernameToken) creds.get(credsId1); + + assertThat(actualCreds1.getUsername()).isEqualTo(username1); + assertThat(actualCreds1.getToken()).isEqualTo(token1.getBytes()); + } + + @Test + public void testGetAllCredentialsWithOtherPrefixesReturnsOnlyCredentials() throws Exception { + // Given... + MockEncryptionService mockEncryptionService = new MockEncryptionService(); + Map mockCreds = new HashMap<>(); + String credsId1 = "CRED1"; + String username1 = "my-user"; + String token1 = "my-token"; + + mockCreds.put("secure.credentials." + credsId1 + ".username", username1); + mockCreds.put("secure.credentials." + credsId1 + ".token", token1); + + String credsId2 = "NOT_A_CRED"; + String username2 = "a-random-value"; + mockCreds.put("secure.not-credentials." + credsId2, username2); + + MockEtcdClient mockClient = new MockEtcdClient(mockCreds); + Etcd3CredentialsStore store = new Etcd3CredentialsStore(null, mockEncryptionService, mockClient); + + // When... + Map creds = store.getAllCredentials(); + + // Then... + assertThat(creds).isNotNull(); + assertThat(creds).hasSize(1); + + CredentialsUsernameToken actualCreds1 = (CredentialsUsernameToken) creds.get(credsId1); + + assertThat(actualCreds1.getUsername()).isEqualTo(username1); + assertThat(actualCreds1.getToken()).isEqualTo(token1.getBytes()); + } + @Test public void testGetUsernameCredentialsReturnsCredentialsOk() throws Exception { // Given... @@ -67,6 +218,39 @@ public void testGetUsernamePasswordCredentialsReturnsCredentialsOk() throws Exce assertThat(creds.getPassword()).isEqualTo(password); } + @Test + public void testGetUsernamePasswordCredentialsWithMetadataReturnsCredentialsOk() throws Exception { + // Given... + MockEncryptionService mockEncryptionService = new MockEncryptionService(); + String credsId = "CRED1"; + String username = "my-user"; + String password = "not-a-password"; + String description = "a description of my credentials"; + String lastUpdatedUser = "myUsername"; + Instant lastUpdatedTime = Instant.EPOCH; + + Map mockCreds = new HashMap<>(); + mockCreds.put("secure.credentials." + credsId + ".username", username); + mockCreds.put("secure.credentials." + credsId + ".password", password); + mockCreds.put("secure.credentials." + credsId + ".description", description); + mockCreds.put("secure.credentials." + credsId + ".lastUpdated.time", lastUpdatedTime.toString()); + mockCreds.put("secure.credentials." + credsId + ".lastUpdated.user", lastUpdatedUser); + + MockEtcdClient mockClient = new MockEtcdClient(mockCreds); + Etcd3CredentialsStore store = new Etcd3CredentialsStore(null, mockEncryptionService, mockClient); + + // When... + CredentialsUsernamePassword creds = (CredentialsUsernamePassword) store.getCredentials(credsId); + + // Then... + assertThat(creds).isNotNull(); + assertThat(creds.getUsername()).isEqualTo(username); + assertThat(creds.getPassword()).isEqualTo(password); + assertThat(creds.getDescription()).isEqualTo(description); + assertThat(creds.getLastUpdatedByUser()).isEqualTo(lastUpdatedUser); + assertThat(creds.getLastUpdatedTime()).isEqualTo(lastUpdatedTime); + } + @Test public void testGetUsernameTokenCredentialsReturnsCredentialsOk() throws Exception { // Given... @@ -275,4 +459,39 @@ public void testShutdownClosesEtcdClientsOk() throws Exception { // Then... assertThat(mockClient.isClientShutDown()).isTrue(); } + + @Test + public void testSetCredentialsWithMetadataSetsCredentialsOk() throws Exception { + // Given... + MockEncryptionService mockEncryptionService = new MockEncryptionService(); + String credsId = "CRED1"; + String username = "a-username"; + String lastUpdatedUser = "myuser"; + Instant lastUpdatedTime = Instant.EPOCH; + String description = "this is a description of my username secret"; + + + Map mockCreds = new HashMap<>(); + MockEtcdClient mockClient = new MockEtcdClient(mockCreds); + Etcd3CredentialsStore store = new Etcd3CredentialsStore(null, mockEncryptionService, mockClient); + + CredentialsUsername mockUsernameCreds = new CredentialsUsername(username); + mockUsernameCreds.setDescription(description); + mockUsernameCreds.setLastUpdatedByUser(lastUpdatedUser); + mockUsernameCreds.setLastUpdatedTime(lastUpdatedTime); + + // When... + store.setCredentials(credsId, mockUsernameCreds); + + // Then... + assertThat(mockCreds).hasSize(4); + assertThat(mockCreds.get("secure.credentials." + credsId + ".username")).isEqualTo(username); + assertThat(mockCreds.get("secure.credentials." + credsId + ".description")).isEqualTo(description); + assertThat(mockCreds.get("secure.credentials." + credsId + ".lastUpdated.time")).isEqualTo(lastUpdatedTime.toString()); + assertThat(mockCreds.get("secure.credentials." + credsId + ".lastUpdated.user")).isEqualTo(lastUpdatedUser); + + // The credentials should have been encrypted when being set, but the metadata should not be encrypted + assertThat(mockEncryptionService.getEncryptCount()).isEqualTo(1); + assertThat(mockEncryptionService.getDecryptCount()).isEqualTo(0); + } } diff --git a/galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/mocks/MockEtcdKvClient.java b/galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/mocks/MockEtcdKvClient.java index 0062b3b..fed5da4 100644 --- a/galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/mocks/MockEtcdKvClient.java +++ b/galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/mocks/MockEtcdKvClient.java @@ -5,11 +5,15 @@ */ package dev.galasa.etcd.internal.mocks; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import com.google.protobuf.ByteString; @@ -45,17 +49,42 @@ public CompletableFuture get(ByteSequence key) { if (value == null) { rangeResponse = RangeResponse.newBuilder().build(); } else { - ByteString keyByteStr = ByteString.copyFromUtf8(keyStr); - Builder builder = KeyValue.newBuilder().setKey(keyByteStr); - ByteString valueByteStr = ByteString.copyFromUtf8(value); - builder = builder.setValue(valueByteStr); - KeyValue kv = builder.build(); - rangeResponse = RangeResponse.newBuilder().addKvs(kv).build(); + rangeResponse = RangeResponse.newBuilder() + .addKvs(createKeyValue(keyStr, value)) + .build(); } GetResponse mockResponse = new GetResponse(rangeResponse, key); return CompletableFuture.completedFuture(mockResponse); } + @Override + public CompletableFuture get(ByteSequence key, GetOption options) { + CompletableFuture response = null; + String keyStr = key.toString(); + if (options.isPrefix()) { + Map matchingKeyValues = kvContents.entrySet() + .stream() + .filter(entry -> entry.getKey().startsWith(keyStr)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + RangeResponse rangeResponse; + if (matchingKeyValues.isEmpty()) { + rangeResponse = RangeResponse.newBuilder().build(); + } else { + List keyValues = new ArrayList<>(); + for (Entry matchingEntry : matchingKeyValues.entrySet()) { + KeyValue kv = createKeyValue(matchingEntry.getKey(), matchingEntry.getValue()); + keyValues.add(kv); + } + rangeResponse = RangeResponse.newBuilder().addAllKvs(keyValues).build(); + + } + GetResponse mockResponse = new GetResponse(rangeResponse, key); + response = CompletableFuture.completedFuture(mockResponse); + } + return response; + } + @Override public CompletableFuture put(ByteSequence key, ByteSequence value) { String keyStr = key.toString(); @@ -87,6 +116,14 @@ public CompletableFuture delete(ByteSequence key, DeleteOption o return CompletableFuture.completedFuture(null); } + private KeyValue createKeyValue(String key, String value) { + ByteString keyByteStr = ByteString.copyFromUtf8(key); + Builder builder = KeyValue.newBuilder().setKey(keyByteStr); + ByteString valueByteStr = ByteString.copyFromUtf8(value); + builder = builder.setValue(valueByteStr); + return builder.build(); + } + @Override public CompletableFuture compact(long key) { throw new UnsupportedOperationException("Unimplemented method 'compact'"); @@ -102,11 +139,6 @@ public CompletableFuture delete(ByteSequence key) { throw new UnsupportedOperationException("Unimplemented method 'delete'"); } - @Override - public CompletableFuture get(ByteSequence key, GetOption options) { - throw new UnsupportedOperationException("Unimplemented method 'get'"); - } - @Override public CompletableFuture put(ByteSequence key, ByteSequence value, PutOption options) { throw new UnsupportedOperationException("Unimplemented method 'put'");