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

Commit

Permalink
feat: Implement operations to set and delete credentials in etcd (#279)
Browse files Browse the repository at this point in the history
Signed-off-by: Eamonn Mansour <[email protected]>
  • Loading branch information
eamansour authored Oct 21, 2024
1 parent ad14193 commit 2aea7a1
Show file tree
Hide file tree
Showing 11 changed files with 672 additions and 83 deletions.
13 changes: 12 additions & 1 deletion .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,18 @@
"name": "TwilioKeyDetector"
}
],
"results": {},
"results": {
"galasa-extensions-parent/dev.galasa.cps.etcd/src/test/java/dev/galasa/etcd/internal/Etcd3CredentialsStoreTest.java": [
{
"hashed_secret": "1beb7496ebbe82c61151be093956d83dac625c13",
"is_secret": false,
"is_verified": false,
"line_number": 246,
"type": "Secret Keyword",
"verified_result": null
}
]
},
"version": "0.13.1+ibm.62.dss",
"word_list": {
"file": null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'galasa.java'
id 'biz.aQute.bnd.builder'
id 'jacoco'
}

dependencies {
Expand All @@ -17,3 +18,16 @@ dependencies {
testImplementation 'org.awaitility:awaitility:3.0.0'
testImplementation 'org.assertj:assertj-core:3.16.1'
}

test {
finalizedBy jacocoTestReport
}

jacocoTestReport {
dependsOn test
reports {
xml.required = true
csv.required = true
html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
}
}
9 changes: 0 additions & 9 deletions galasa-extensions-parent/dev.galasa.auth.couchdb/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
plugins {
id 'biz.aQute.bnd.builder'
id 'galasa.extensions'
id 'jacoco'
}

description = 'Galasa Authentication - CouchDB'
Expand All @@ -21,14 +20,6 @@ dependencies {
testImplementation(project(':dev.galasa.extensions.mocks'))
}

jacocoTestReport {
reports {
xml.required = true
csv.required = true
html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
}
}

// Note: These values are consumed by the parent build process
// They indicate which packages of functionality this OSGi bundle should be delivered inside,
// or referenced from.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
*/
package dev.galasa.cps.etcd.internal;

import static com.google.common.base.Charsets.UTF_8;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.Properties;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;

import javax.crypto.spec.SecretKeySpec;
Expand All @@ -25,23 +23,20 @@
import dev.galasa.framework.spi.creds.CredentialsUsername;
import dev.galasa.framework.spi.creds.CredentialsUsernamePassword;
import dev.galasa.framework.spi.creds.CredentialsUsernameToken;
import dev.galasa.framework.spi.creds.FrameworkEncryptionService;
import dev.galasa.framework.spi.creds.ICredentialsStore;
import io.etcd.jetcd.ByteSequence;
import dev.galasa.framework.spi.creds.IEncryptionService;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.KeyValue;
import io.etcd.jetcd.kv.GetResponse;

/**
* This class implements the credential store in a etcd store. Usernames,
* Passwords and tokens can be retrieved from etc using the correct key format:
*
* "secure.credentials.{SomeCredentialId};.username"
*/
public class Etcd3CredentialsStore implements ICredentialsStore {
private final Client client;
private final KV kvClient;
public class Etcd3CredentialsStore extends Etcd3Store implements ICredentialsStore {
private final SecretKeySpec key;
private final IEncryptionService encryptionService;

/**
* This constructor instantiates the Key value client that can retrieve values
Expand All @@ -51,22 +46,28 @@ public class Etcd3CredentialsStore implements ICredentialsStore {
* @throws CredentialsException A failure occurred.
*/
public Etcd3CredentialsStore(IFramework framework, URI etcd) throws CredentialsException {
super(etcd);
try {
client = Client.builder().endpoints(etcd).build();
kvClient = client.getKVClient();

IConfigurationPropertyStoreService cpsService = framework.getConfigurationPropertyService("secure");
String encryptionKey = cpsService.getProperty("credentials.file", "encryption.key");
if (encryptionKey != null) {
key = createKey(encryptionKey);
} else {
key = null;
}

this.encryptionService = new FrameworkEncryptionService(key);
} catch (Exception e) {
throw new CredentialsException("Unable to initialise the etcd credentials store", e);
}
}

public Etcd3CredentialsStore(SecretKeySpec key, IEncryptionService encryptionService, Client etcdClient) throws CredentialsException {
super(etcdClient);
this.key = key;
this.encryptionService = encryptionService;
}

/**
* This method checks for the three available credential types in the
* credentials stores and returns the appropiate response.
Expand All @@ -78,50 +79,30 @@ public Etcd3CredentialsStore(IFramework framework, URI etcd) throws CredentialsE
* @throws CredentialsException A failure occurred.
*/
public ICredentials getCredentials(String credentialsId) throws CredentialsException {
String token = get("secure.credentials." + credentialsId + ".token");
if (token != null) {
try {
ICredentials credentials = null;
String token = get("secure.credentials." + credentialsId + ".token");
String username = get("secure.credentials." + credentialsId + ".username");

if (username != null) {
return new CredentialsUsernameToken(key, username, token);
// 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 new CredentialsToken(key, token);
}

String username = get("secure.credentials." + credentialsId + ".username");
String password = get("secure.credentials." + credentialsId + ".password");

if (username == null) {
return null;
}

if (password == null) {
return new CredentialsUsername(key, username);
}

return new CredentialsUsernamePassword(key, username, password);
}

/**
* A get method which interacts with the etcd client correctly.
*
* @param key - the full key to search for with CredId and type of credential.
* @return String vaule response.
* @throws CredentialsException
*/
private String get(String key) throws CredentialsException {
ByteSequence bsKey = ByteSequence.from(key, UTF_8);
CompletableFuture<GetResponse> getFuture = kvClient.get(bsKey);
try {
GetResponse response = getFuture.get();
List<KeyValue> kvs = response.getKvs();
if (kvs.isEmpty()) {
return null;
}
return kvs.get(0).getValue().toString(UTF_8);

return credentials;
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new CredentialsException("Could not retrieve key.", e);
throw new CredentialsException("Failed to get credentials", e);
}
}

Expand All @@ -135,8 +116,30 @@ private static SecretKeySpec createKey(String secret)

@Override
public void shutdown() throws CredentialsException {
kvClient.close();
client.close();
super.shutdownStore();
}

@Override
public void setCredentials(String credentialsId, ICredentials credentials) throws CredentialsException {
Properties credentialProperties = credentials.toProperties(credentialsId);

try {
for (Entry<Object, Object> property : credentialProperties.entrySet()) {
put((String) property.getKey(), encryptionService.encrypt((String) property.getValue()));
}
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new CredentialsException("Failed to set credentials", e);
}
}

@Override
public void deleteCredentials(String credentialsId) throws CredentialsException {
try {
deletePrefix("secure.credentials." + credentialsId);
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new CredentialsException("Failed to delete credentials", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright contributors to the Galasa project
*
* SPDX-License-Identifier: EPL-2.0
*/
package dev.galasa.cps.etcd.internal;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import javax.validation.constraints.NotNull;

import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.KeyValue;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.options.DeleteOption;

/**
* Abstract class containing common methods used to interact with etcd, like getting, setting,
* and deleting properties.
*/
public abstract class Etcd3Store {

protected final Client client;
protected final KV kvClient;

public Etcd3Store(Client client) {
this.client = client;
this.kvClient = client.getKVClient();
}

public Etcd3Store(URI etcdUri) {
this(Client.builder().endpoints(etcdUri).build());
}

protected String get(String key) throws InterruptedException, ExecutionException {
ByteSequence bsKey = ByteSequence.from(key, UTF_8);
CompletableFuture<GetResponse> getFuture = kvClient.get(bsKey);
GetResponse response = getFuture.get();
List<KeyValue> kvs = response.getKvs();

String retrievedKey = null;
if (!kvs.isEmpty()) {
retrievedKey = kvs.get(0).getValue().toString(UTF_8);
}
return retrievedKey;
}

protected void put(String key, String value) throws InterruptedException, ExecutionException {
ByteSequence bytesKey = ByteSequence.from(key, UTF_8);
ByteSequence bytesValue = ByteSequence.from(value, UTF_8);
kvClient.put(bytesKey, bytesValue).get();
}

protected void delete(@NotNull String key) throws InterruptedException, ExecutionException {
ByteSequence bytesKey = ByteSequence.from(key, StandardCharsets.UTF_8);
kvClient.delete(bytesKey).get();
}

protected void deletePrefix(@NotNull String keyPrefix) throws InterruptedException, ExecutionException {
ByteSequence bsKey = ByteSequence.from(keyPrefix, UTF_8);
DeleteOption options = DeleteOption.newBuilder().isPrefix(true).build();
kvClient.delete(bsKey, options).get();
}

protected void shutdownStore() {
kvClient.close();
client.close();
}
}
Loading

0 comments on commit 2aea7a1

Please sign in to comment.