Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
Implemented getAllCredentials method for etcd credentials store (#282)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
eamansour authored Oct 31, 2024
1 parent 6d80007 commit 29ddb56
Show file tree
Hide file tree
Showing 5 changed files with 403 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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<String, String> 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)
Expand All @@ -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<Object, Object> 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);
Expand All @@ -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<String, ICredentials> getAllCredentials() throws CredentialsException {
Map<String, ICredentials> credentials = new HashMap<>();
try {
Map<String, String> credentialsKeyValues = getPrefix(CREDS_PROPERTY_PREFIX);

// Build a set of all credential IDs stored in etcd
Set<Entry<String, String>> credentialsEntries = credentialsKeyValues.entrySet();
Set<String> credentialIds = new HashSet<>();
for (Entry<String, String> 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<String, String> 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<String, String> 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<Object, Object> property : properties.entrySet()) {
String key = (String) property.getKey();
String value = (String) property.getValue();
if (encryptValues) {
value = encryptionService.encrypt(value);
}
put(key, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
Expand Down Expand Up @@ -53,6 +56,28 @@ protected String get(String key) throws InterruptedException, ExecutionException
return retrievedKey;
}

protected Map<String, String> getPrefix(String keyPrefix) throws InterruptedException, ExecutionException {
Map<String, String> keyValues = new HashMap<>();

ByteSequence bsPrefix = ByteSequence.from(keyPrefix, UTF_8);
GetOption options = GetOption.newBuilder().isPrefix(true).build();
CompletableFuture<GetResponse> getFuture = kvClient.get(bsPrefix, options);

GetResponse response = getFuture.get();
List<KeyValue> 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);
Expand Down
Loading

0 comments on commit 29ddb56

Please sign in to comment.