From 0d14ebc138deac284391da5a2ac95605f2bb09b1 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:31:03 +0100 Subject: [PATCH 1/2] Add credentials encryption and decryption, mount encryption keys in test pods (#658) * Add initial decryption logic for reading credentials Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * Mount encryption keys secret in test pods, refactor k8s controller settings + unit tests Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * fix: Fix OSGI wiring error, change oldDecryptionKeys to fallbackDecryptionKeys Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * review: Rename RunPoll to TestPodScheduler, parameterise randomiser, separate YAML parsing Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --------- Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .../build.gradle | 3 +- .../k8s/controller/K8sController.java | 9 +- .../framework/k8s/controller/RunDeleted.java | 4 +- .../framework/k8s/controller/Settings.java | 288 +++++++---------- .../{RunPoll.java => TestPodScheduler.java} | 290 +++++++++++------- .../k8s/controller/TestPodSchedulerTest.java | 163 ++++++++++ .../dev.galasa.framework/build.gradle | 1 + .../java/dev/galasa/framework/FileSystem.java | 5 + .../dev/galasa/framework/IFileSystem.java | 6 +- .../framework/spi/creds/Credentials.java | 17 +- .../framework/spi/creds/CredentialsToken.java | 7 +- .../spi/creds/CredentialsUsername.java | 13 +- .../creds/CredentialsUsernamePassword.java | 17 +- .../spi/creds/CredentialsUsernameToken.java | 19 +- .../framework/spi/creds/EncryptionKeys.java | 80 +++++ .../spi/creds/FrameworkEncryptionService.java | 151 +++++++++ .../spi/creds/IEncryptionService.java | 11 + .../framework/mocks/MockFileSystem.java | 9 + .../creds/FrameworkEncryptionServiceTest.java | 212 +++++++++++++ release.yaml | 2 +- 20 files changed, 978 insertions(+), 329 deletions(-) rename galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/{RunPoll.java => TestPodScheduler.java} (60%) create mode 100644 galasa-parent/dev.galasa.framework.k8s.controller/src/test/java/dev/galasa/framework/k8s/controller/TestPodSchedulerTest.java create mode 100644 galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/EncryptionKeys.java create mode 100644 galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/FrameworkEncryptionService.java create mode 100644 galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/IEncryptionService.java create mode 100644 galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/spi/creds/FrameworkEncryptionServiceTest.java diff --git a/galasa-parent/dev.galasa.framework.k8s.controller/build.gradle b/galasa-parent/dev.galasa.framework.k8s.controller/build.gradle index 1eed2c4a8..b97999680 100644 --- a/galasa-parent/dev.galasa.framework.k8s.controller/build.gradle +++ b/galasa-parent/dev.galasa.framework.k8s.controller/build.gradle @@ -5,7 +5,7 @@ plugins { description = 'Galasa Kubernetes Controller' -version '0.36.0' +version '0.38.0' dependencies { @@ -36,6 +36,7 @@ dependencies { } } + testImplementation project(':dev.galasa.framework').sourceSets.test.output } // Note: These values are consumed by the parent build process diff --git a/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/K8sController.java b/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/K8sController.java index 00bd12d27..8bdd8d15e 100644 --- a/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/K8sController.java +++ b/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/K8sController.java @@ -43,7 +43,7 @@ public class K8sController { private Health healthServer; - private RunPoll runPoll; + private TestPodScheduler podScheduler; private ScheduledFuture pollFuture; private RunDeleted runDeleted; @@ -88,6 +88,7 @@ public void run(Properties bootstrapProperties, Properties overrideProperties) t // *** Fetch the settings settings = new Settings(this, api); + settings.init(); // *** Setup defaults and properties @@ -138,7 +139,7 @@ public void run(Properties bootstrapProperties, Properties overrideProperties) t // *** Start the run polling runDeleted = new RunDeleted(settings, api, pc, framework.getFrameworkRuns()); scheduleDelete(); - runPoll = new RunPoll(dss, settings, api, framework.getFrameworkRuns()); + podScheduler = new TestPodScheduler(dss, settings, api, framework.getFrameworkRuns()); schedulePoll(); @@ -183,7 +184,7 @@ private void schedulePoll() { this.pollFuture.cancel(false); } - pollFuture = scheduledExecutorService.scheduleWithFixedDelay(runPoll, 1, settings.getPoll(), TimeUnit.SECONDS); + pollFuture = scheduledExecutorService.scheduleWithFixedDelay(podScheduler, 1, settings.getPoll(), TimeUnit.SECONDS); } private void scheduleDelete() { @@ -233,7 +234,7 @@ public static String nulled(String value) { return value; } - public void pollUpated() { + public void pollUpdated() { if (pollFuture == null) { return; } diff --git a/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/RunDeleted.java b/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/RunDeleted.java index ff3d5b44f..52252dd58 100644 --- a/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/RunDeleted.java +++ b/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/RunDeleted.java @@ -39,8 +39,8 @@ public void run() { logger.info("Starting Deleted runs scan"); try { - List pods = RunPoll.getPods(api, settings); - RunPoll.filterTerminated(pods); + List pods = TestPodScheduler.getPods(api, settings); + TestPodScheduler.filterTerminated(pods); for (V1Pod pod : pods) { Map labels = pod.getMetadata().getLabels(); diff --git a/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/Settings.java b/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/Settings.java index 33252f3e3..30bcdbcdb 100644 --- a/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/Settings.java +++ b/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/Settings.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -15,6 +16,7 @@ import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1ObjectMeta; public class Settings implements Runnable { @@ -33,6 +35,7 @@ public class Settings implements Runnable { private int engineMemoryLimit = 200; private String nodeArch = ""; private String nodePreferredAffinity = ""; + private String encryptionKeysSecretName; private HashSet requiredCapabilities = new HashSet<>(); private HashSet capableCapabilities = new HashSet<>(); @@ -49,178 +52,145 @@ public class Settings implements Runnable { public Settings(K8sController controller, CoreV1Api api) throws K8sControllerException { this.api = api; this.controller = controller; + } + public void init() throws K8sControllerException { loadEnvironmentProperties(); - retrieveConfigMap(); + loadConfigMapProperties(); } @Override public void run() { try { - retrieveConfigMap(); + loadConfigMapProperties(); } catch (K8sControllerException e) { logger.error("Poll for the ConfigMap " + configMapName + " failed", e); } } - private void retrieveConfigMap() throws K8sControllerException { + private void loadConfigMapProperties() throws K8sControllerException { + V1ConfigMap configMap = retrieveConfigMap(); + validateConfigMap(configMap); + updateConfigMapProperties(configMap.getMetadata(), configMap.getData()); + } - V1ConfigMap configMap = null; - try { - configMap = api.readNamespacedConfigMap(configMapName, namespace, "true"); - } catch (ApiException e) { - throw new K8sControllerException("Failed to read configmap '" + configMapName + "' in namespace '" + namespace + "'", e); + private String updateProperty(Map configMapData, String key, String defaultValue, String oldValue) { + String newValue = getPropertyFromData(configMapData, key, defaultValue); + if (!newValue.equals(oldValue)) { + logger.info("Setting " + key + " from '" + oldValue + "' to '" + newValue + "'"); } + return newValue; + } - String newResourceVersion = configMap.getMetadata().getResourceVersion(); - if (newResourceVersion.equals(oldConfigMapResourceVersion)) { - return; + private int updateProperty(Map configMapData, String key, int defaultValue, int oldValue) throws K8sControllerException { + int newValue = getPropertyFromData(configMapData, key, defaultValue); + if (newValue != oldValue) { + logger.info("Setting " + key + " from '" + oldValue + "' to '" + newValue + "'"); } - oldConfigMapResourceVersion = newResourceVersion; - - logger.info("ConfigMap has been changed, reloading parameters"); + return newValue; + } - try { - String newBootstrap = configMap.getData().get("bootstrap"); - if (newBootstrap == null || newBootstrap.trim().isEmpty()) { - newBootstrap = "http://bootstrap"; - } - if (!newBootstrap.equals(this.bootstrap)) { - logger.info("Setting Boostrap from '" + this.bootstrap + "' to '" + newBootstrap + "'"); - this.bootstrap = newBootstrap; - } - } catch (Exception e) { - logger.error("Error processing bootstrap in configmap", e); + private String getPropertyFromData(Map configMapData, String key, String defaultValue) { + String value = configMapData.get(key); + if (value == null || value.isBlank()) { + value = defaultValue; } - try { - String maxEnginesString = configMap.getData().get("max_engines"); - if (maxEnginesString == null || maxEnginesString.trim().isEmpty()) { - maxEnginesString = "1"; - } - int newMaxEngines = Integer.parseInt(maxEnginesString); - if (newMaxEngines != maxEngines) { - logger.info("Setting Max Engines from " + maxEngines + " to " + newMaxEngines); - maxEngines = newMaxEngines; - } - } catch (Exception e) { - logger.error("Error processing max_engines in configmap", e); + if (value != null) { + value = value.trim(); } + return value; + } + private int getPropertyFromData(Map configMapData, String key, int defaultValue) throws K8sControllerException { + int returnValue = defaultValue; try { - String newEngineLabel = configMap.getData().get("engine_label"); - if (newEngineLabel == null || newEngineLabel.trim().isEmpty()) { - newEngineLabel = "k8s-standard-engine"; + String valueStr = configMapData.get(key); + if (valueStr != null && !valueStr.isBlank()) { + returnValue = Integer.parseInt(valueStr); } - if (!newEngineLabel.equals(engineLabel)) { - logger.info("Setting Engine Label from '" + engineLabel + "' to '" + newEngineLabel + "'"); - engineLabel = newEngineLabel; - } - } catch (Exception e) { - logger.error("Error processing engine_label in configmap", e); + } catch (NumberFormatException e) { + throw new K8sControllerException("Invalid value provided for " + key + " in settings configmap"); } + return returnValue; + } + V1ConfigMap retrieveConfigMap() throws K8sControllerException { + V1ConfigMap configMap = null; try { - String newEngineImage = configMap.getData().get("engine_image"); - if (newEngineImage == null || newEngineImage.trim().isEmpty()) { - newEngineImage = "cicsts-docker-local.artifactory.swg-devops.com/galasav3-boot-embedded"; - } - if (!newEngineImage.equals(engineImage)) { - logger.info("Setting Engine Image from '" + engineImage + "' to '" + newEngineImage + "'"); - engineImage = newEngineImage; - } - } catch (Exception e) { - logger.error("Error processing engine_image in configmap", e); + configMap = api.readNamespacedConfigMap(configMapName, namespace, "true"); + } catch (ApiException e) { + throw new K8sControllerException("Failed to read configmap '" + configMapName + "' in namespace '" + namespace + "'", e); } + return configMap; + } - try { - String newEngineMemory = configMap.getData().get("engine_memory"); - if (newEngineMemory == null || newEngineMemory.trim().isEmpty()) { - newEngineMemory = "300"; - } - Integer memory = Integer.parseInt(newEngineMemory); - if (memory != engineMemory) { - logger.info("Setting Engine Memory from '" + engineMemory + "' to '" + memory + "'"); - engineMemory = memory; - } - } catch (Exception e) { - logger.error("Error processing engine_memory in configmap", e); + private void validateConfigMap(V1ConfigMap configMap) throws K8sControllerException { + V1ObjectMeta configMapMetadata = configMap.getMetadata(); + Map configMapData = configMap.getData(); + if (configMapMetadata == null || configMapData == null) { + throw new K8sControllerException("Settings configmap is missing required metadata or data"); } + } - try { - String newEngineMemoryRequest = configMap.getData().get("engine_memory_request"); - if (newEngineMemoryRequest == null || newEngineMemoryRequest.trim().isEmpty()) { - newEngineMemoryRequest = Integer.toString(engineMemory + 50); - } - Integer memory = Integer.parseInt(newEngineMemoryRequest); - if (memory != engineMemoryRequest) { - logger.info("Setting Engine Memory Request from '" + engineMemoryRequest + "' to '" + memory + "'"); - engineMemoryRequest = memory; - } - } catch (Exception e) { - logger.error("Error processing engine_memory_request in configmap", e); + private void updateConfigMapProperties(V1ObjectMeta configMapMetadata, Map configMapData) throws K8sControllerException { + String newResourceVersion = configMapMetadata.getResourceVersion(); + if (newResourceVersion != null && newResourceVersion.equals(oldConfigMapResourceVersion)) { + return; } - try { - String newEngineMemoryLimit = configMap.getData().get("engine_memory_limit"); - if (newEngineMemoryLimit == null || newEngineMemoryLimit.trim().isEmpty()) { - newEngineMemoryLimit = Integer.toString(engineMemory + 100); - } - Integer memory = Integer.parseInt(newEngineMemoryLimit); - if (memory != engineMemoryLimit) { - logger.info("Setting Engine Memory Limit from '" + engineMemoryLimit + "' to '" + memory + "'"); - engineMemoryLimit = memory; - } - } catch (Exception e) { - logger.error("Error processing engine_memory_limit in configmap", e); - } + oldConfigMapResourceVersion = newResourceVersion; - try { - String newRunPoll = configMap.getData().get("run_poll"); - if (newRunPoll == null || newRunPoll.trim().isEmpty()) { - newRunPoll = "20"; - } - Integer poll = Integer.parseInt(newRunPoll); - if (poll != runPoll) { - logger.info("Setting Run Poll from '" + runPoll + "' to '" + poll + "'"); - runPoll = poll; - controller.pollUpated(); - } - } catch (Exception e) { - logger.error("Error processing run_poll in configmap", e); + logger.info("ConfigMap has been changed, reloading parameters"); + + this.bootstrap = updateProperty(configMapData, "bootstrap", "http://bootstrap", this.bootstrap); + this.maxEngines = updateProperty(configMapData, "max_engines", 1, this.maxEngines); + this.engineLabel = updateProperty(configMapData, "engine_label", "k8s-standard-engine", this.engineLabel); + this.engineImage = updateProperty(configMapData, "engine_image", "ghcr.io/galasa-dev/galasa-boot-embedded-amd64", this.engineImage); + this.engineMemory = updateProperty(configMapData, "engine_memory", 300, this.engineMemory); + this.engineMemoryRequest = updateProperty(configMapData, "engine_memory_request", engineMemory + 50, this.engineMemoryRequest); + this.engineMemoryLimit = updateProperty(configMapData, "engine_memory_limit", engineMemory + 100, this.engineMemoryLimit); + this.nodeArch = updateProperty(configMapData, "node_arch", "", this.nodeArch); + this.nodePreferredAffinity = updateProperty(configMapData, "galasa_node_preferred_affinity", "", this.nodePreferredAffinity); + this.encryptionKeysSecretName = updateProperty(configMapData, "encryption_keys_secret_name", "", this.encryptionKeysSecretName); + + int poll = getPropertyFromData(configMapData, "run_poll", 20); + if (poll != runPoll) { + logger.info("Setting Run Poll from '" + runPoll + "' to '" + poll + "'"); + runPoll = poll; + controller.pollUpdated(); } - try { - String newRequestors = configMap.getData().get("scheduled_requestors"); - if (newRequestors == null || newRequestors.trim().isEmpty()) { - newRequestors = ""; - } - ArrayList newRequestorsByScheduleid = new ArrayList<>(); + setRequestorsByScheduleId(configMapData); + setEngineCapabilities(configMapData); + } + + private void setRequestorsByScheduleId(Map configMapData) { + String newRequestors = getPropertyFromData(configMapData, "scheduled_requestors", null); + ArrayList newRequestorsByScheduleid = new ArrayList<>(); + if (newRequestors != null) { String requestors[] = newRequestors.split(","); for (String requestor : requestors) { newRequestorsByScheduleid.add(requestor); } - + if (!requestorsByScheduleID.equals(newRequestorsByScheduleid)) { logger.info("Setting Requestors by Schedule from '" + requestorsByScheduleID + "' to '" + newRequestorsByScheduleid + "'"); requestorsByScheduleID = newRequestorsByScheduleid; } - } catch (Exception e) { - logger.error("Error processing scheduled_requestors in configmap", e); } + } - try { - String newCapabilities = configMap.getData().get("engine_capabilities"); - if (newCapabilities == null || newCapabilities.trim().isEmpty()) { - newCapabilities = ""; - } - ArrayList newRequiredCapabilties = new ArrayList<>(); - ArrayList newCapableCapabilties = new ArrayList<>(); + private void setEngineCapabilities(Map configMapData) { + String newCapabilities = getPropertyFromData(configMapData, "engine_capabilities", null); + ArrayList newRequiredCapabilties = new ArrayList<>(); + ArrayList newCapableCapabilties = new ArrayList<>(); - String capabalities[] = newCapabilities.split(","); - for (String capability : capabalities) { + if (newCapabilities != null) { + String capabilities[] = newCapabilities.split(","); + for (String capability : capabilities) { capability = capability.trim(); if (capability.startsWith("+")) { capability = capability.substring(1); @@ -233,7 +203,7 @@ private void retrieveConfigMap() throws K8sControllerException { } } } - + boolean changed = false; if (newRequiredCapabilties.size() != requiredCapabilities.size() || newCapableCapabilties.size() != capableCapabilities.size()) { @@ -252,7 +222,7 @@ private void retrieveConfigMap() throws K8sControllerException { } } } - + if (changed) { capableCapabilities.clear(); requiredCapabilities.clear(); @@ -260,7 +230,7 @@ private void retrieveConfigMap() throws K8sControllerException { requiredCapabilities.addAll(newRequiredCapabilties); logger.info("Engine set with Required Capabilities - " + requiredCapabilities); logger.info("Engine set with Capabable Capabilities - " + capableCapabilities); - + StringBuilder report = new StringBuilder(); for (String cap : requiredCapabilities) { if (report.length() > 0) { @@ -280,71 +250,28 @@ private void retrieveConfigMap() throws K8sControllerException { } else { reportCapabilties = null; } - } - } catch (Exception e) { - logger.error("Error processing engine_capabilities in configmap", e); } + } - try { - String newNodeArch = configMap.getData().get("node_arch"); - if (newNodeArch == null) { - newNodeArch = ""; - } - newNodeArch = newNodeArch.trim(); - if (!newNodeArch.equals(nodeArch)) { - logger.info("Setting Node Architecture from '" + nodeArch + "' to '" + newNodeArch + "'"); - nodeArch = newNodeArch; - } - } catch (Exception e) { - logger.error("Error processing node_arch in configmap", e); + private String getEnvironmentVariableOrDefault(String envVar, String defaultValue) { + String value = System.getenv(envVar); + if (value == null || value.isBlank()) { + value = defaultValue; } - - try { - String newNodePreferredAffinity = configMap.getData().get("galasa_node_preferred_affinity"); - if (newNodePreferredAffinity == null) { - newNodePreferredAffinity = ""; - } - newNodePreferredAffinity = newNodePreferredAffinity.trim(); - if (!newNodePreferredAffinity.equals(nodePreferredAffinity)) { - logger.info("Setting Node Preferred Affinity from '" + nodePreferredAffinity + "' to '" - + newNodePreferredAffinity + "'"); - nodePreferredAffinity = newNodePreferredAffinity; - } - } catch (Exception e) { - logger.error("Error processing node_preferred_affinity in configmap", e); - } - - return; + return value.trim(); } private void loadEnvironmentProperties() { - namespace = System.getenv("NAMESPACE"); - if (namespace == null || namespace.trim().isEmpty()) { - namespace = "default"; - } else { - namespace = namespace.trim(); - } + namespace = getEnvironmentVariableOrDefault("NAMESPACE", "default"); logger.info("Setting Namespace to '" + namespace + "'"); - podname = System.getenv("PODNAME"); - if (podname == null || podname.trim().isEmpty()) { - podname = "k8s-controller"; - } else { - podname = podname.trim(); - } + podname = getEnvironmentVariableOrDefault("PODNAME", "k8s-controller"); logger.info("Setting Pod Name to '" + podname + "'"); - configMapName = System.getenv("CONFIG"); - if (configMapName == null || configMapName.trim().isEmpty()) { - configMapName = "config"; - } else { - configMapName = configMapName.trim(); - } + configMapName = getEnvironmentVariableOrDefault("CONFIG", "config"); logger.info("Setting ConfigMap to '" + configMapName + "'"); - - return; } public String getPodName() { @@ -399,4 +326,7 @@ public long getPoll() { return this.runPoll; } + public String getEncryptionKeysSecretName() { + return encryptionKeysSecretName; + } } \ No newline at end of file diff --git a/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/RunPoll.java b/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/TestPodScheduler.java similarity index 60% rename from galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/RunPoll.java rename to galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/TestPodScheduler.java index e6297684a..2fbebff7e 100644 --- a/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/RunPoll.java +++ b/galasa-parent/dev.galasa.framework.k8s.controller/src/main/java/dev/galasa/framework/k8s/controller/TestPodScheduler.java @@ -5,6 +5,8 @@ */ package dev.galasa.framework.k8s.controller; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -25,6 +27,7 @@ import dev.galasa.framework.spi.IFrameworkRuns; import dev.galasa.framework.spi.IRun; import dev.galasa.framework.spi.SystemEnvironment; +import dev.galasa.framework.spi.creds.FrameworkEncryptionService; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1Affinity; @@ -44,13 +47,19 @@ import io.kubernetes.client.openapi.models.V1PreferredSchedulingTerm; import io.kubernetes.client.openapi.models.V1ResourceRequirements; import io.kubernetes.client.openapi.models.V1SecretKeySelector; +import io.kubernetes.client.openapi.models.V1SecretVolumeSource; +import io.kubernetes.client.openapi.models.V1Volume; +import io.kubernetes.client.openapi.models.V1VolumeMount; import io.prometheus.client.Counter; -public class RunPoll implements Runnable { +public class TestPodScheduler implements Runnable { private static final String RAS_TOKEN_ENV = "GALASA_RAS_TOKEN"; private static final String EVENT_TOKEN_ENV = "GALASA_EVENT_STREAMS_TOKEN"; + private static final String ENCRYPTION_KEYS_PATH_ENV = FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV; + public static final String ENCRYPTION_KEYS_VOLUME_NAME = "encryption-keys"; + private final Log logger = LogFactory.getLog(getClass()); private final Settings settings; @@ -63,7 +72,12 @@ public class RunPoll implements Runnable { private Environment env = new SystemEnvironment(); - public RunPoll(IDynamicStatusStoreService dss, Settings settings, CoreV1Api api, IFrameworkRuns runs) { + public TestPodScheduler(IDynamicStatusStoreService dss, Settings settings, CoreV1Api api, IFrameworkRuns runs) { + this(new SystemEnvironment(), dss, settings, api, runs); + } + + public TestPodScheduler(Environment env, IDynamicStatusStoreService dss, Settings settings, CoreV1Api api, IFrameworkRuns runs) { + this.env = env; this.settings = settings; this.api = api; this.runs = runs; @@ -160,118 +174,7 @@ private void startPod(IRun run) { return; } - V1Pod newPod = new V1Pod(); - newPod.setApiVersion("v1"); - newPod.setKind("Pod"); - - V1ObjectMeta metadata = new V1ObjectMeta(); - newPod.setMetadata(metadata); - metadata.setName(engineName); - metadata.putLabelsItem("galasa-engine-controller", this.settings.getEngineLabel()); - metadata.putLabelsItem("galasa-run", runName); - - V1PodSpec podSpec = new V1PodSpec(); - newPod.setSpec(podSpec); - podSpec.setRestartPolicy("Never"); - - String nodeArch = this.settings.getNodeArch(); - if (!nodeArch.isEmpty()) { - HashMap nodeSelector = new HashMap<>(); - nodeSelector.put("beta.kubernetes.io/arch", nodeArch); - podSpec.setNodeSelector(nodeSelector); - } - - String nodePreferredAffinity = this.settings.getNodePreferredAffinity(); - if (!nodePreferredAffinity.isEmpty()) { - String[] selection = nodePreferredAffinity.split("="); - if (selection.length == 2) { - V1Affinity affinity = new V1Affinity(); - podSpec.setAffinity(affinity); - - V1NodeAffinity nodeAffinity = new V1NodeAffinity(); - affinity.setNodeAffinity(nodeAffinity); - - V1PreferredSchedulingTerm preferred = new V1PreferredSchedulingTerm(); - nodeAffinity.addPreferredDuringSchedulingIgnoredDuringExecutionItem(preferred); - preferred.setWeight(1); - - V1NodeSelectorTerm selectorTerm = new V1NodeSelectorTerm(); - preferred.setPreference(selectorTerm); - - V1NodeSelectorRequirement requirement = new V1NodeSelectorRequirement(); - selectorTerm.addMatchExpressionsItem(requirement); - requirement.setKey(selection[0]); - requirement.setOperator("In"); - requirement.addValuesItem(selection[1]); - } - } - - V1Container container = new V1Container(); - podSpec.addContainersItem(container); - container.setName("engine"); - container.setImage(this.settings.getEngineImage()); - container.setImagePullPolicy("Always"); // TODO parameterise - - ArrayList commands = new ArrayList<>(); - container.setCommand(commands); - commands.add("java"); - - ArrayList args = new ArrayList<>(); - container.setArgs(args); - args.add("-jar"); - args.add("boot.jar"); - args.add("--obr"); - args.add("file:galasa.obr"); - args.add("--bootstrap"); - args.add(settings.getBootstrap()); - args.add("--run"); - args.add(runName); - if (run.isTrace()) { - args.add("--trace"); - } - - V1ResourceRequirements resources = new V1ResourceRequirements(); - container.setResources(resources); - - // TODO reinstate - // System.out.println("requests=" + - // Integer.toString(this.settings.getEngineMemoryRequest()) + "Mi"); - // System.out.println("limit=" + - // Integer.toString(this.settings.getEngineMemoryLimit()) + "Mi"); - // resources.putRequestsItem("memory", new - // Quantity(Integer.toString(this.settings.getEngineMemoryRequest()) + "Mi")); - // resources.putLimitsItem("memory", new - // Quantity(Integer.toString(this.settings.getEngineMemoryLimit()) + "Mi")); - - ArrayList envs = new ArrayList<>(); - container.setEnv(envs); - // envs.add(createConfigMapEnv("GALASA_URL", configMapName, "galasa_url")); - // envs.add(createConfigMapEnv("GALASA_INFRA_OBR", configMapName, - // "galasa_maven_infra_obr")); - // envs.add(createConfigMapEnv("GALASA_INFRA_REPO", configMapName, - // "galasa_maven_infra_repo")); - // envs.add(createConfigMapEnv("GALASA_TEST_REPO", configMapName, - // "galasa_maven_test_repo")); - // envs.add(createConfigMapEnv("GALASA_HELPER_REPO", configMapName, - // "galasa_maven_helper_repo")); - // - // envs.add(createValueEnv("GALASA_ENGINE_TYPE", engineLabel)); - envs.add(createValueEnv("MAX_HEAP", Integer.toString(this.settings.getEngineMemory()) + "m")); - envs.add(createValueEnv(RAS_TOKEN_ENV, env.getenv(RAS_TOKEN_ENV))); - envs.add(createValueEnv(EVENT_TOKEN_ENV, env.getenv(EVENT_TOKEN_ENV))); - // - // envs.add(createSecretEnv("GALASA_SERVER_USER", "galasa-secret", - // "galasa-server-username")); - // envs.add(createSecretEnv("GALASA_SERVER_PASSWORD", "galasa-secret", - // "galasa-server-password")); - // envs.add(createSecretEnv("GALASA_MAVEN_USER", "galasa-secret", - // "galasa-maven-username")); - // envs.add(createSecretEnv("GALASA_MAVEN_PASSWORD", "galasa-secret", - // "galasa-maven-password")); - // - // envs.add(createValueEnv("GALASA_RUN_ID", runUUID.toString())); - // envs.add(createFieldEnv("GALASA_ENGINE_ID", "metadata.name")); - // envs.add(createFieldEnv("GALASA_K8S_NODE", "spec.nodeName")); + V1Pod newPod = createTestPod(runName, engineName, run.isTrace()); boolean successful = false; int retry = 0; @@ -308,7 +211,164 @@ private void startPod(IRun run) { } catch (Exception e) { logger.error("Failed to start new engine", e); } - return; + } + + V1Pod createTestPod(String runName, String engineName, boolean isTraceEnabled) { + V1Pod newPod = new V1Pod(); + newPod.setApiVersion("v1"); + newPod.setKind("Pod"); + + V1ObjectMeta metadata = new V1ObjectMeta(); + newPod.setMetadata(metadata); + metadata.setName(engineName); + metadata.putLabelsItem("galasa-engine-controller", this.settings.getEngineLabel()); + metadata.putLabelsItem("galasa-run", runName); + + V1PodSpec podSpec = new V1PodSpec(); + newPod.setSpec(podSpec); + podSpec.setRestartPolicy("Never"); + + String nodeArch = this.settings.getNodeArch(); + if (!nodeArch.isEmpty()) { + HashMap nodeSelector = new HashMap<>(); + nodeSelector.put("beta.kubernetes.io/arch", nodeArch); + podSpec.setNodeSelector(nodeSelector); + } + + String nodePreferredAffinity = this.settings.getNodePreferredAffinity(); + if (!nodePreferredAffinity.isEmpty()) { + String[] selection = nodePreferredAffinity.split("="); + if (selection.length == 2) { + V1Affinity affinity = new V1Affinity(); + podSpec.setAffinity(affinity); + + V1NodeAffinity nodeAffinity = new V1NodeAffinity(); + affinity.setNodeAffinity(nodeAffinity); + + V1PreferredSchedulingTerm preferred = new V1PreferredSchedulingTerm(); + nodeAffinity.addPreferredDuringSchedulingIgnoredDuringExecutionItem(preferred); + preferred.setWeight(1); + + V1NodeSelectorTerm selectorTerm = new V1NodeSelectorTerm(); + preferred.setPreference(selectorTerm); + + V1NodeSelectorRequirement requirement = new V1NodeSelectorRequirement(); + selectorTerm.addMatchExpressionsItem(requirement); + requirement.setKey(selection[0]); + requirement.setOperator("In"); + requirement.addValuesItem(selection[1]); + } + } + + podSpec.setVolumes(createTestPodVolumes()); + podSpec.addContainersItem(createTestContainer(runName, engineName, isTraceEnabled)); + return newPod; + } + + private V1Container createTestContainer(String runName, String engineName, boolean isTraceEnabled) { + V1Container container = new V1Container(); + container.setName("engine"); + container.setImage(this.settings.getEngineImage()); + container.setImagePullPolicy("Always"); // TODO parameterise + + ArrayList commands = new ArrayList<>(); + container.setCommand(commands); + commands.add("java"); + + ArrayList args = new ArrayList<>(); + container.setArgs(args); + args.add("-jar"); + args.add("boot.jar"); + args.add("--obr"); + args.add("file:galasa.obr"); + args.add("--bootstrap"); + args.add(settings.getBootstrap()); + args.add("--run"); + args.add(runName); + if (isTraceEnabled) { + args.add("--trace"); + } + + V1ResourceRequirements resources = new V1ResourceRequirements(); + container.setResources(resources); + + // TODO reinstate + // System.out.println("requests=" + + // Integer.toString(this.settings.getEngineMemoryRequest()) + "Mi"); + // System.out.println("limit=" + + // Integer.toString(this.settings.getEngineMemoryLimit()) + "Mi"); + // resources.putRequestsItem("memory", new + // Quantity(Integer.toString(this.settings.getEngineMemoryRequest()) + "Mi")); + // resources.putLimitsItem("memory", new + // Quantity(Integer.toString(this.settings.getEngineMemoryLimit()) + "Mi")); + + container.setVolumeMounts(createTestContainerVolumeMounts()); + container.setEnv(createTestContainerEnvVariables()); + return container; + } + + private List createTestPodVolumes() { + List volumes = new ArrayList<>(); + + V1Volume encryptionKeysVolume = new V1Volume(); + encryptionKeysVolume.setName(ENCRYPTION_KEYS_VOLUME_NAME); + + V1SecretVolumeSource encryptionKeysSecretSource = new V1SecretVolumeSource(); + encryptionKeysSecretSource.setSecretName(this.settings.getEncryptionKeysSecretName()); + + encryptionKeysVolume.setSecret(encryptionKeysSecretSource); + volumes.add(encryptionKeysVolume); + return volumes; + } + + private List createTestContainerVolumeMounts() { + List volumeMounts = new ArrayList<>(); + + String encryptionKeysMountPath = env.getenv(ENCRYPTION_KEYS_PATH_ENV); + if (encryptionKeysMountPath != null && !encryptionKeysMountPath.isBlank()) { + Path encryptionKeysDirectory = Paths.get(encryptionKeysMountPath).getParent().toAbsolutePath(); + + V1VolumeMount encryptionKeysVolumeMount = new V1VolumeMount(); + encryptionKeysVolumeMount.setName(ENCRYPTION_KEYS_VOLUME_NAME); + encryptionKeysVolumeMount.setMountPath(encryptionKeysDirectory.toString()); + encryptionKeysVolumeMount.setReadOnly(true); + + volumeMounts.add(encryptionKeysVolumeMount); + } + return volumeMounts; + } + + private List createTestContainerEnvVariables() { + ArrayList envs = new ArrayList<>(); + // envs.add(createConfigMapEnv("GALASA_URL", configMapName, "galasa_url")); + // envs.add(createConfigMapEnv("GALASA_INFRA_OBR", configMapName, + // "galasa_maven_infra_obr")); + // envs.add(createConfigMapEnv("GALASA_INFRA_REPO", configMapName, + // "galasa_maven_infra_repo")); + // envs.add(createConfigMapEnv("GALASA_TEST_REPO", configMapName, + // "galasa_maven_test_repo")); + // envs.add(createConfigMapEnv("GALASA_HELPER_REPO", configMapName, + // "galasa_maven_helper_repo")); + // + // envs.add(createValueEnv("GALASA_ENGINE_TYPE", engineLabel)); + envs.add(createValueEnv("MAX_HEAP", Integer.toString(this.settings.getEngineMemory()) + "m")); + envs.add(createValueEnv(RAS_TOKEN_ENV, env.getenv(RAS_TOKEN_ENV))); + envs.add(createValueEnv(EVENT_TOKEN_ENV, env.getenv(EVENT_TOKEN_ENV))); + envs.add(createValueEnv(ENCRYPTION_KEYS_PATH_ENV, env.getenv(ENCRYPTION_KEYS_PATH_ENV))); + // + // envs.add(createSecretEnv("GALASA_SERVER_USER", "galasa-secret", + // "galasa-server-username")); + // envs.add(createSecretEnv("GALASA_SERVER_PASSWORD", "galasa-secret", + // "galasa-server-password")); + // envs.add(createSecretEnv("GALASA_MAVEN_USER", "galasa-secret", + // "galasa-maven-username")); + // envs.add(createSecretEnv("GALASA_MAVEN_PASSWORD", "galasa-secret", + // "galasa-maven-password")); + // + // envs.add(createValueEnv("GALASA_RUN_ID", runUUID.toString())); + // envs.add(createFieldEnv("GALASA_ENGINE_ID", "metadata.name")); + // envs.add(createFieldEnv("GALASA_K8S_NODE", "spec.nodeName")); + return envs; } private HashMap getPools(@NotNull List runs) { diff --git a/galasa-parent/dev.galasa.framework.k8s.controller/src/test/java/dev/galasa/framework/k8s/controller/TestPodSchedulerTest.java b/galasa-parent/dev.galasa.framework.k8s.controller/src/test/java/dev/galasa/framework/k8s/controller/TestPodSchedulerTest.java new file mode 100644 index 000000000..97b986909 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.k8s.controller/src/test/java/dev/galasa/framework/k8s/controller/TestPodSchedulerTest.java @@ -0,0 +1,163 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.k8s.controller; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import dev.galasa.framework.mocks.MockEnvironment; +import dev.galasa.framework.mocks.MockIDynamicStatusStoreService; +import dev.galasa.framework.mocks.MockIFrameworkRuns; +import dev.galasa.framework.spi.creds.FrameworkEncryptionService; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1Container; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodSpec; +import io.kubernetes.client.openapi.models.V1Volume; +import io.kubernetes.client.openapi.models.V1VolumeMount; + +public class TestPodSchedulerTest { + + class MockSettings extends Settings { + + private V1ConfigMap mockConfigMap; + + public MockSettings(V1ConfigMap configMap, K8sController controller, CoreV1Api api) throws K8sControllerException { + super(controller, api); + this.mockConfigMap = configMap; + } + + @Override + V1ConfigMap retrieveConfigMap() { + return mockConfigMap; + } + } + + class MockK8sController extends K8sController { + @Override + public void pollUpdated() { + // Do nothing... + } + } + + private V1ConfigMap createMockConfigMap() { + V1ConfigMap configMap = new V1ConfigMap(); + + V1ObjectMeta metadata = new V1ObjectMeta().resourceVersion("mockVersion"); + configMap.setMetadata(metadata); + + Map data = new HashMap<>(); + data.put("bootstrap", "http://my.server/bootstrap"); + data.put("max_engines", "10"); + data.put("engine_label", "my-test-engine"); + data.put("node_arch", "arch"); + data.put("run_poll", "5"); + data.put("encryption_keys_secret_name", "service-encryption-keys-secret"); + + configMap.setData(data); + + return configMap; + } + + private void assertPodDetailsAreCorrect( + V1Pod pod, + String expectedRunName, + String expectedPodName, + String expectedEncryptionKeysMountPath, + Settings settings + ) { + checkPodMetadata(pod, expectedRunName, expectedPodName, settings); + checkPodContainer(pod, expectedEncryptionKeysMountPath, settings); + checkPodVolumes(pod, settings); + } + + @SuppressWarnings("null") + private void checkPodMetadata(V1Pod pod, String expectedRunName, String expectedPodName, Settings settings) { + V1ObjectMeta expectedMetadata = new V1ObjectMeta() + .labels(Map.of("galasa-run", expectedRunName, "galasa-engine-controller", settings.getEngineLabel())) + .name(expectedPodName); + + // Check the pod's metadata is as expected + assertThat(pod).isNotNull(); + assertThat(pod.getApiVersion()).isEqualTo("v1"); + assertThat(pod.getKind()).isEqualTo("Pod"); + + V1ObjectMeta actualMetadata = pod.getMetadata(); + assertThat(actualMetadata.getLabels()).containsExactlyInAnyOrderEntriesOf(expectedMetadata.getLabels()); + assertThat(actualMetadata.getName()).isEqualTo(expectedPodName); + } + + @SuppressWarnings("null") + private void checkPodContainer(V1Pod pod, String expectedEncryptionKeysMountPath, Settings settings) { + // Check that test container has been added + V1PodSpec actualPodSpec = pod.getSpec(); + List actualContainers = actualPodSpec.getContainers(); + assertThat(actualContainers).hasSize(1); + + V1Container testContainer = actualContainers.get(0); + assertThat(testContainer.getCommand()).containsExactly("java"); + assertThat(testContainer.getArgs()).contains("-jar", "boot.jar", "--run", settings.getBootstrap()); + + // Check that the encryption keys have been mounted to the correct location + List testContainerVolumeMounts = testContainer.getVolumeMounts(); + assertThat(testContainerVolumeMounts).hasSize(1); + + V1VolumeMount encryptionKeysVolumeMount = testContainerVolumeMounts.get(0); + assertThat(encryptionKeysVolumeMount.getName()).isEqualTo(TestPodScheduler.ENCRYPTION_KEYS_VOLUME_NAME); + assertThat(encryptionKeysVolumeMount.getMountPath()).isEqualTo(expectedEncryptionKeysMountPath); + assertThat(encryptionKeysVolumeMount.getReadOnly()).isTrue(); + } + + @SuppressWarnings("null") + private void checkPodVolumes(V1Pod pod, Settings settings) { + // Check that the encryption keys volume has been added + V1PodSpec actualPodSpec = pod.getSpec(); + List actualVolumes = actualPodSpec.getVolumes(); + assertThat(actualVolumes).hasSize(1); + + V1Volume encryptionKeysVolume = actualVolumes.get(0); + assertThat(encryptionKeysVolume.getName()).isEqualTo(TestPodScheduler.ENCRYPTION_KEYS_VOLUME_NAME); + assertThat(encryptionKeysVolume.getSecret().getSecretName()).isEqualTo(settings.getEncryptionKeysSecretName()); + } + + @Test + public void testCanCreateTestPodOk() throws Exception { + // Given... + MockEnvironment mockEnvironment = new MockEnvironment(); + + String encryptionKeysMountPath = "/encryption/encryption-keys.yaml"; + mockEnvironment.setenv(FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV, encryptionKeysMountPath); + + MockK8sController controller = new MockK8sController(); + MockIDynamicStatusStoreService mockDss = new MockIDynamicStatusStoreService(); + MockIFrameworkRuns mockFrameworkRuns = new MockIFrameworkRuns(new ArrayList<>()); + + V1ConfigMap mockConfigMap = createMockConfigMap(); + MockSettings settings = new MockSettings(mockConfigMap, controller, null); + settings.init(); + + TestPodScheduler runPoll = new TestPodScheduler(mockEnvironment, mockDss, settings, null, mockFrameworkRuns); + + String runName = "run1"; + String podName = settings.getEngineLabel() + "-" + runName; + boolean isTraceEnabled = false; + + // When... + V1Pod pod = runPoll.createTestPod(runName, podName, isTraceEnabled); + + // Then... + String expectedEncryptionKeysMountPath = "/encryption"; + assertPodDetailsAreCorrect(pod, runName, podName, expectedEncryptionKeysMountPath, settings); + } +} diff --git a/galasa-parent/dev.galasa.framework/build.gradle b/galasa-parent/dev.galasa.framework/build.gradle index 528110732..348689b5a 100644 --- a/galasa-parent/dev.galasa.framework/build.gradle +++ b/galasa-parent/dev.galasa.framework/build.gradle @@ -23,6 +23,7 @@ dependencies { implementation 'org.apache.bcel:bcel:6.7.0' implementation 'commons-io:commons-io:2.16.1' implementation 'com.google.code.gson:gson:2.10.1' + implementation 'org.yaml:snakeyaml:2.0' testImplementation project (':dev.galasa') diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/FileSystem.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/FileSystem.java index f6abf06bc..974d1ad7e 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/FileSystem.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/FileSystem.java @@ -95,4 +95,9 @@ public List readLines(URI uri) throws IOException { } return lines ; } + + @Override + public String readString(Path path) throws IOException { + return Files.readString(path); + } } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/IFileSystem.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/IFileSystem.java index 07c863152..db31527da 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/IFileSystem.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/IFileSystem.java @@ -35,7 +35,9 @@ public interface IFileSystem { Path createFile(Path path, FileAttribute... attrs) throws IOException; - void write(Path rasProperties, byte[] bytes) throws IOException ; + void write(Path rasProperties, byte[] bytes) throws IOException; - List readLines(URI uri) throws IOException ; + List readLines(URI uri) throws IOException; + + String readString(Path path) throws IOException; } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java index f17135889..eaf0238ea 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java @@ -11,12 +11,27 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import dev.galasa.framework.FileSystem; +import dev.galasa.framework.IFileSystem; +import dev.galasa.framework.spi.Environment; +import dev.galasa.framework.spi.SystemEnvironment; + public abstract class Credentials { + private IEncryptionService encryptionService; private final SecretKeySpec key; - public Credentials(SecretKeySpec key) { + public Credentials(SecretKeySpec key) throws CredentialsException { + this(key, new FileSystem(), new SystemEnvironment()); + } + + public Credentials(SecretKeySpec key, IFileSystem fileSystem, Environment environment) throws CredentialsException { this.key = key; + this.encryptionService = new FrameworkEncryptionService(key, fileSystem, environment); + } + + protected String decryptToString(String encryptedText) throws CredentialsException { + return encryptionService.decrypt(encryptedText); } protected byte[] decode(String text) throws CredentialsException { diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java index 8cac60726..fca9f3137 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java @@ -15,7 +15,12 @@ public class CredentialsToken extends Credentials implements ICredentialsToken { public CredentialsToken(SecretKeySpec key, String stoken) throws CredentialsException { super(key); - this.token = decode(stoken); + String decryptedToken = decryptToString(stoken); + if (decryptedToken == null) { + this.token = decode(stoken); + } else { + this.token = decryptedToken.getBytes(); + } } public byte[] getToken() { diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java index 25bebe0a6..74ff1314f 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java @@ -5,7 +5,7 @@ */ package dev.galasa.framework.spi.creds; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import javax.crypto.spec.SecretKeySpec; @@ -16,12 +16,11 @@ public class CredentialsUsername extends Credentials implements ICredentialsUser public CredentialsUsername(SecretKeySpec key, String username) throws CredentialsException { super(key); - try { - this.username = new String(decode(username), "utf-8"); - } catch (UnsupportedEncodingException e) { - throw new CredentialsException("utf-8 is not available for credentials", e); - } catch (CredentialsException e) { - throw e; + + this.username = decryptToString(username); + + if (this.username == null) { + this.username = new String(decode(username), StandardCharsets.UTF_8); } } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java index 760586af2..bf922fbd9 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java @@ -5,7 +5,7 @@ */ package dev.galasa.framework.spi.creds; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import javax.crypto.spec.SecretKeySpec; @@ -19,15 +19,16 @@ public CredentialsUsernamePassword(SecretKeySpec key, String username, String pa throws CredentialsException { super(key); - try { - this.username = new String(decode(username), "utf-8"); - this.password = new String(decode(password), "utf-8"); - } catch (UnsupportedEncodingException e) { - throw new CredentialsException("utf-8 is not available for credentials", e); - } catch (CredentialsException e) { - throw e; + this.username = decryptToString(username); + this.password = decryptToString(password); + + if (this.username == null) { + this.username = new String(decode(username), StandardCharsets.UTF_8); } + if (this.password == null) { + this.password = new String(decode(password), StandardCharsets.UTF_8); + } } public String getUsername() { diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java index be2454317..b4e74b837 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java @@ -5,7 +5,7 @@ */ package dev.galasa.framework.spi.creds; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import javax.crypto.spec.SecretKeySpec; @@ -17,15 +17,18 @@ public class CredentialsUsernameToken extends Credentials implements ICredential public CredentialsUsernameToken(SecretKeySpec key, String username, String token) throws CredentialsException { super(key); - try { - this.username = new String(decode(username), "utf-8"); - this.token = decode(token); - } catch (UnsupportedEncodingException e) { - throw new CredentialsException("utf-8 is not available for credentials", e); - } catch (CredentialsException e) { - throw e; + + this.username = decryptToString(username); + if (this.username == null) { + this.username = new String(decode(username), StandardCharsets.UTF_8); } + String decryptedToken = decryptToString(token); + if (decryptedToken == null) { + this.token = decode(token); + } else { + this.token = decryptedToken.getBytes(); + } } public String getUsername() { diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/EncryptionKeys.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/EncryptionKeys.java new file mode 100644 index 000000000..e3bb70a7e --- /dev/null +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/EncryptionKeys.java @@ -0,0 +1,80 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.spi.creds; + +import java.nio.file.Paths; +import java.util.List; + +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import dev.galasa.framework.IFileSystem; +import dev.galasa.framework.spi.Environment; + +public class EncryptionKeys { + private String encryptionKey; + private List fallbackDecryptionKeys; + + public EncryptionKeys() { + // No-op constructor + } + + public EncryptionKeys(IFileSystem fileSystem, Environment environment) throws CredentialsException { + EncryptionKeys parsedEncryptionKeys = parseEncryptionKeysFile(fileSystem, environment); + this.encryptionKey = parsedEncryptionKeys.getEncryptionKey(); + this.fallbackDecryptionKeys = parsedEncryptionKeys.getFallbackDecryptionKeys(); + } + + public String getEncryptionKey() { + return encryptionKey; + } + + public void setEncryptionKey(String encryptionKey) { + this.encryptionKey = encryptionKey; + } + + public List getFallbackDecryptionKeys() { + return fallbackDecryptionKeys; + } + + public void setFallbackDecryptionKeys(List fallbackDecryptionKeys) { + this.fallbackDecryptionKeys = fallbackDecryptionKeys; + } + + /** + * Parses a YAML file containing a primary encryption key and a list of fallback decryption keys, and returns + * the keys parsed into an EncryptionKeys bean. + * + * All keys in the YAML file must be base64-encoded in order for the keys to be properly decoded. + * The YAML format for the encryption keys is as follows: + * + * encryptionKey: + * fallbackDecryptionKeys: + * - + * - + * + * @return a bean object representing the parsed encryption keys + * @throws CredentialsException if there was an error parsing the encryption keys YAML file + */ + private EncryptionKeys parseEncryptionKeysFile(IFileSystem fileSystem, Environment environment) throws CredentialsException { + EncryptionKeys encryptionKeys = this; + String encryptionKeysLocation = environment.getenv(FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV); + if (encryptionKeysLocation != null) { + try { + String encryptionKeysYamlStr = fileSystem.readString(Paths.get(encryptionKeysLocation)); + if (encryptionKeysYamlStr != null && !encryptionKeysYamlStr.isBlank()) { + Yaml yamlParser = new Yaml(new Constructor(EncryptionKeys.class, new LoaderOptions())); + encryptionKeys = yamlParser.load(encryptionKeysYamlStr); + } + } catch (Exception e) { + throw new CredentialsException("Failed to read encryption keys file", e); + } + } + + return encryptionKeys; + } +} diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/FrameworkEncryptionService.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/FrameworkEncryptionService.java new file mode 100644 index 000000000..3e613fdb5 --- /dev/null +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/FrameworkEncryptionService.java @@ -0,0 +1,151 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.spi.creds; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import dev.galasa.framework.FileSystem; +import dev.galasa.framework.IFileSystem; +import dev.galasa.framework.spi.Environment; +import dev.galasa.framework.spi.SystemEnvironment; + +public class FrameworkEncryptionService implements IEncryptionService { + + public static final String ENCRYPTION_KEYS_PATH_ENV = "GALASA_ENCRYPTION_KEYS_PATH"; + + private static final String KEY_ALGORITHM = "AES"; + private static final String ENCRYPTION_ALGORITHM = "AES/GCM/NoPadding"; + + private static final int GCM_AUTH_TAG_LENGTH_BITS = 128; + private static final int GCM_IV_BYTES_LENGTH = 12; + + private SecretKeySpec encryptionKey; + private List decryptionKeys = new ArrayList<>(); + + private SecureRandom secureRandom; + + public FrameworkEncryptionService(SecretKeySpec encryptionKey) throws CredentialsException { + this(encryptionKey, new FileSystem(), new SystemEnvironment(), new SecureRandom()); + } + + public FrameworkEncryptionService(SecretKeySpec encryptionKey, IFileSystem fileSystem, Environment environment) throws CredentialsException { + this(encryptionKey, fileSystem, environment, new SecureRandom()); + } + + public FrameworkEncryptionService( + SecretKeySpec encryptionKey, + IFileSystem fileSystem, + Environment environment, + SecureRandom secureRandom + ) throws CredentialsException { + this.encryptionKey = encryptionKey; + this.secureRandom = secureRandom; + + if (encryptionKey == null) { + EncryptionKeys encryptionKeys = new EncryptionKeys(fileSystem, environment); + String parsedEncryptionKey = encryptionKeys.getEncryptionKey(); + List parsedFallbackKeys = encryptionKeys.getFallbackDecryptionKeys(); + if (parsedEncryptionKey != null && parsedFallbackKeys != null) { + this.encryptionKey = loadPrimaryEncryptionKey(parsedEncryptionKey); + this.decryptionKeys = loadDecryptionKeys(this.encryptionKey, parsedFallbackKeys); + } + } else { + this.decryptionKeys = new ArrayList<>(); + this.decryptionKeys.add(encryptionKey); + } + } + + private SecretKeySpec loadPrimaryEncryptionKey(String encodedKeyStr) throws CredentialsException { + byte[] decodedPrimaryKeyBytes = Base64.getDecoder().decode(encodedKeyStr); + return new SecretKeySpec(decodedPrimaryKeyBytes, KEY_ALGORITHM); + } + + private List loadDecryptionKeys(SecretKeySpec primaryEncryptionKey, List encodedFallbackKeys) throws CredentialsException { + List decryptionKeys = new ArrayList<>(); + decryptionKeys.add(primaryEncryptionKey); + + for (String fallbackKey : encodedFallbackKeys) { + byte[] decodedFallbackKeyBytes = Base64.getDecoder().decode(fallbackKey); + SecretKeySpec key = new SecretKeySpec(decodedFallbackKeyBytes, KEY_ALGORITHM); + decryptionKeys.add(key); + } + return decryptionKeys; + } + + @Override + public String encrypt(String plainText) throws CredentialsException { + if (this.encryptionKey == null) { + throw new CredentialsException("Unable to encrypt the provided data. No encryption key has been set"); + } + + // Generate a random initialization vector (IV) + byte[] initVector = new byte[GCM_IV_BYTES_LENGTH]; + secureRandom.nextBytes(initVector); + + byte[] encryptedIvAndText; + try { + // Initialise the GCM cipher in encrypt mode + Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_AUTH_TAG_LENGTH_BITS, initVector); + cipher.init(Cipher.ENCRYPT_MODE, this.encryptionKey, gcmParameterSpec); + + // Encrypt the plaintext + byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); + + // Concatenate the IV and the encrypted text + encryptedIvAndText = new byte[initVector.length + encryptedBytes.length]; + System.arraycopy(initVector, 0, encryptedIvAndText, 0, initVector.length); + System.arraycopy(encryptedBytes, 0, encryptedIvAndText, initVector.length, encryptedBytes.length); + + } catch (Exception e) { + throw new CredentialsException("Failed to encrypt the provided data", e); + } + return Base64.getEncoder().encodeToString(encryptedIvAndText); + } + + @Override + public String decrypt(String encryptedText) throws CredentialsException { + String decryptedText = null; + for (SecretKeySpec key : decryptionKeys) { + try { + decryptedText = decrypt(encryptedText, key); + } catch (CredentialsException e) { + // Decryption failed, so let's try the next key... + } + } + return decryptedText; + } + + private String decrypt(String encryptedText, SecretKeySpec decryptionKey) throws CredentialsException { + String decryptedText = null; + try { + byte[] decodedBytes = Base64.getDecoder().decode(encryptedText); + + // Get the IV from the encrypted text + byte[] initVector = new byte[GCM_IV_BYTES_LENGTH]; + System.arraycopy(decodedBytes, 0, initVector, 0, initVector.length); + + Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_AUTH_TAG_LENGTH_BITS, initVector); + cipher.init(Cipher.DECRYPT_MODE, decryptionKey, gcmParameterSpec); + + // Strip off the IV and decrypt the text + byte[] decryptedBytes = cipher.doFinal(decodedBytes, initVector.length, decodedBytes.length - initVector.length); + decryptedText = new String(decryptedBytes, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new CredentialsException("Failed to decrypt the provided data", e); + } + return decryptedText; + } +} diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/IEncryptionService.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/IEncryptionService.java new file mode 100644 index 000000000..2c71a2e7b --- /dev/null +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/IEncryptionService.java @@ -0,0 +1,11 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.spi.creds; + +public interface IEncryptionService { + String encrypt(String plainText) throws CredentialsException; + String decrypt(String encryptedText) throws CredentialsException; +} diff --git a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockFileSystem.java b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockFileSystem.java index 27ad7be1f..fce9ac53e 100644 --- a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockFileSystem.java +++ b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockFileSystem.java @@ -237,6 +237,15 @@ public List readLines(URI uri) throws IOException { return lines ; } + + @Override + public String readString(Path path) throws IOException { + if (!exists(path)) { + throw new FileNotFoundException("File "+path+" was not found"); + } + return getContentsAsString(path); + } + // -------------- Un-implemented methods follow ------------------ @Override diff --git a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/spi/creds/FrameworkEncryptionServiceTest.java b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/spi/creds/FrameworkEncryptionServiceTest.java new file mode 100644 index 000000000..171e33ba3 --- /dev/null +++ b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/spi/creds/FrameworkEncryptionServiceTest.java @@ -0,0 +1,212 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.spi.creds; + +import static org.assertj.core.api.Assertions.*; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Test; + +import dev.galasa.framework.mocks.MockEnvironment; +import dev.galasa.framework.mocks.MockFileSystem; + +public class FrameworkEncryptionServiceTest { + + private final byte MOCK_IV_BYTE = (byte) 1; + + private + class MockRandom extends SecureRandom { + @Override + public void nextBytes(byte[] bytes) { + Arrays.fill(bytes, MOCK_IV_BYTE); + } + } + + private SecretKeySpec generateEncryptionKey() throws NoSuchAlgorithmException { + byte[] keyBytes = RandomStringUtils.randomAlphanumeric(32).getBytes(); + return new SecretKeySpec(keyBytes, "AES"); + } + + private String generateEncodedEncryptionKeyString() throws NoSuchAlgorithmException { + byte[] keyBytes = generateEncryptionKey().getEncoded(); + return Base64.getEncoder().encodeToString(keyBytes); + } + + private String createEncryptionKeysYaml(String primaryEncryptionKey, List oldEncryptionKeys) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("encryptionKey: " + primaryEncryptionKey); + stringBuilder.append("\n"); + stringBuilder.append("fallbackDecryptionKeys: "); + + if (oldEncryptionKeys.isEmpty()) { + stringBuilder.append("[]"); + } else { + stringBuilder.append("\n"); + for (String key : oldEncryptionKeys) { + stringBuilder.append("- " + key); + stringBuilder.append("\n"); + } + } + return stringBuilder.toString(); + } + + @Test + public void testCanEncryptTextOk() throws Exception { + // Given... + SecretKeySpec key = generateEncryptionKey(); + MockFileSystem mockFileSystem = new MockFileSystem(); + MockEnvironment mockEnvironment = new MockEnvironment(); + MockRandom random = new MockRandom(); + FrameworkEncryptionService encryptionService = new FrameworkEncryptionService(key, mockFileSystem, mockEnvironment, random); + String plainText = "encrypt me!"; + + // When... + String encryptedText = encryptionService.encrypt(plainText); + + // Then... + assertThat(encryptedText).isNotNull(); + assertThat(encryptedText).isNotEqualTo(plainText); + + // Check that 12 bytes have been set as the initialization vector + byte[] decodedBytes = Base64.getDecoder().decode(encryptedText); + for (int i = 0; i < 12; i++) { + assertThat(decodedBytes[i]).isEqualTo(MOCK_IV_BYTE); + } + } + + @Test + public void testCanEncryptAndDecryptTextOk() throws Exception { + // Given... + SecretKeySpec key = generateEncryptionKey(); + MockFileSystem mockFileSystem = new MockFileSystem(); + MockEnvironment mockEnvironment = new MockEnvironment(); + SecureRandom random = new SecureRandom(); + FrameworkEncryptionService encryptionService = new FrameworkEncryptionService(key, mockFileSystem, mockEnvironment, random); + String plainText = "encrypt me!"; + + // When... + String encryptedText = encryptionService.encrypt(plainText); + assertThat(encryptedText).isNotNull(); + + String decryptedText = encryptionService.decrypt(encryptedText); + + // Then... + assertThat(decryptedText).isEqualTo(plainText); + } + + @Test + public void testCanLoadAndUseEncryptionKeysFromFileSystemOk() throws Exception { + // Given... + MockFileSystem mockFileSystem = new MockFileSystem(); + MockEnvironment mockEnvironment = new MockEnvironment(); + + String mockEncryptionKeysFilePath = "/encryption-keys.yaml"; + mockEnvironment.setenv(FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV, mockEncryptionKeysFilePath); + + List oldDecryptionKeys = new ArrayList<>(); + String encodedEncryptionkey = generateEncodedEncryptionKeyString(); + String yaml = createEncryptionKeysYaml(encodedEncryptionkey, oldDecryptionKeys); + mockFileSystem.write(Paths.get(mockEncryptionKeysFilePath), yaml.getBytes(StandardCharsets.UTF_8)); + + SecureRandom random = new SecureRandom(); + FrameworkEncryptionService encryptionService = new FrameworkEncryptionService(null, mockFileSystem, mockEnvironment, random); + + String plainText = "encrypt me!"; + + // When... + String encryptedText = encryptionService.encrypt(plainText); + String decryptedText = encryptionService.decrypt(encryptedText); + + // Then... + assertThat(decryptedText).isEqualTo(plainText); + } + + @Test + public void testDecryptTextWithWrongKeyReturnsNullText() throws Exception { + // Given... + MockFileSystem mockFileSystem = new MockFileSystem(); + MockEnvironment mockEnvironment = new MockEnvironment(); + + String mockEncryptionKeysFilePath = "/encryption-keys.yaml"; + mockEnvironment.setenv(FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV, mockEncryptionKeysFilePath); + + List oldDecryptionKeys = List.of( + generateEncodedEncryptionKeyString(), + generateEncodedEncryptionKeyString() + ); + String encodedEncryptionkey = generateEncodedEncryptionKeyString(); + String yaml = createEncryptionKeysYaml(encodedEncryptionkey, oldDecryptionKeys); + mockFileSystem.write(Paths.get(mockEncryptionKeysFilePath), yaml.getBytes(StandardCharsets.UTF_8)); + + SecureRandom random = new SecureRandom(); + FrameworkEncryptionService encryptionService = new FrameworkEncryptionService(null, mockFileSystem, mockEnvironment, random); + String mockEncryptedText = "letspretendthatthisisencrypted"; + + // When... + // The decryption should fail since the provided text was not encrypted with any known encryption keys + String decryptedText = encryptionService.decrypt(mockEncryptedText); + + // Then... + assertThat(decryptedText).isNull(); + } + + @Test + public void testCreateEncryptionServiceFailsWhenNoKeysFileExists() throws Exception { + // Given... + MockFileSystem mockFileSystem = new MockFileSystem(); + MockEnvironment mockEnvironment = new MockEnvironment(); + + String mockEncryptionKeysFilePath = "/encryption-keys.yaml"; + mockEnvironment.setenv(FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV, mockEncryptionKeysFilePath); + SecureRandom random = new SecureRandom(); + + // When... + CredentialsException thrown = catchThrowableOfType(() -> { + new FrameworkEncryptionService(null, mockFileSystem, mockEnvironment, random); + }, CredentialsException.class); + + // Then... + assertThat(thrown).isNotNull(); + assertThat(thrown.getMessage()).contains("Failed to read encryption keys file"); + } + + @Test + public void testEncryptTextWithNoKeyThrowsError() throws Exception { + // Given... + MockFileSystem mockFileSystem = new MockFileSystem(); + MockEnvironment mockEnvironment = new MockEnvironment(); + + String mockEncryptionKeysFilePath = "/encryption-keys.yaml"; + mockEnvironment.setenv(FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV, mockEncryptionKeysFilePath); + + mockFileSystem.write(Paths.get(mockEncryptionKeysFilePath), null); + + SecureRandom random = new SecureRandom(); + FrameworkEncryptionService encryptionService = new FrameworkEncryptionService(null, mockFileSystem, mockEnvironment, random); + String plainText = "encrypt me"; + + // When... + // The encryption should fail since the service does not have an encryption key to use + CredentialsException thrown = catchThrowableOfType(() -> { + encryptionService.encrypt(plainText); + }, CredentialsException.class); + + // Then... + assertThat(thrown).isNotNull(); + assertThat(thrown.getMessage()).contains("Unable to encrypt the provided data"); + } +} diff --git a/release.yaml b/release.yaml index f3c8b8af5..863147bae 100644 --- a/release.yaml +++ b/release.yaml @@ -51,7 +51,7 @@ framework: codecoverage: true - artifact: dev.galasa.framework.k8s.controller - version: 0.36.0 + version: 0.38.0 obr: true mvp: false bom: false From 69cda76e76af729b84953b1ed82f1ae924d94226 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:19:11 +0100 Subject: [PATCH 2/2] Add GalasaSecret resource processing to /resources route (#659) * Add initial decryption logic for reading credentials Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * Mount encryption keys secret in test pods, refactor k8s controller settings + unit tests Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * Refactor: separate resource processing logic out from resources route Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * Start adding GalasaSecret resource processing Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Add GalasaSecret resource processing Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * review: Remove unvalidated values from error messages Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --------- Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .secrets.baseline | 10 + .../build.gradle | 2 +- .../api/common/ServletErrorMessage.java | 20 +- .../common/resources/GalasaResourceType.java | 41 +- .../common/resources/GalasaSecretType.java | 41 ++ .../api/common/resources/ResourceAction.java | 35 + .../api/common/resources/Secret.java | 59 ++ .../resources/TestGalasaResourceType.java | 8 +- .../common/mocks/MockCredentialsService.java | 57 ++ .../api/common/mocks/MockFramework.java | 12 +- .../src/main/resources/openapi.yaml | 47 ++ .../build.gradle | 2 +- .../api/resources/ResourcesServlet.java | 5 +- .../AbstractGalasaResourceProcessor.java | 66 ++ .../processors/GalasaPropertyProcessor.java | 135 ++++ .../processors/GalasaSecretProcessor.java | 207 ++++++ .../processors/IGalasaResourceProcessor.java | 26 + .../api/resources/routes/ResourcesRoute.java | 153 +---- .../GalasaPropertyProcessorTest.java | 386 +++++++++++ .../processors/GalasaSecretProcessorTest.java | 609 ++++++++++++++++++ .../resources/routes/TestResourcesRoute.java | 437 +------------ .../internal/creds/FileCredentialsStore.java | 9 + .../creds/FrameworkCredentialsService.java | 10 + .../framework/spi/creds/Credentials.java | 4 + .../framework/spi/creds/CredentialsToken.java | 13 + .../spi/creds/CredentialsUsername.java | 12 + .../creds/CredentialsUsernamePassword.java | 14 + .../spi/creds/CredentialsUsernameToken.java | 14 + .../spi/creds/FrameworkEncryptionService.java | 1 - .../spi/creds/ICredentialsService.java | 20 + .../spi/creds/ICredentialsStore.java | 4 + .../framework/mocks/MockCredentials.java | 7 + .../framework/mocks/MockCredentialsStore.java | 9 + .../creds/FrameworkEncryptionServiceTest.java | 2 - .../main/java/dev/galasa/ICredentials.java | 4 +- galasa-parent/galasa-testharness/build.gradle | 2 +- .../dev/galasa/testharness/InMemoryCreds.java | 9 + release.yaml | 6 +- 38 files changed, 1924 insertions(+), 574 deletions(-) create mode 100644 galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaSecretType.java create mode 100644 galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/ResourceAction.java create mode 100644 galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/Secret.java create mode 100644 galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockCredentialsService.java create mode 100644 galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/AbstractGalasaResourceProcessor.java create mode 100644 galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessor.java create mode 100644 galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessor.java create mode 100644 galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/IGalasaResourceProcessor.java create mode 100644 galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessorTest.java create mode 100644 galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java diff --git a/.secrets.baseline b/.secrets.baseline index 021519fd8..8d7a76a24 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -104,6 +104,16 @@ "verified_result": null } ], + "galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java": [ + { + "hashed_secret": "0ea7458942ab65e0a340cf4fd28ca00d93c494f3", + "is_secret": false, + "is_verified": false, + "line_number": 321, + "type": "Secret Keyword", + "verified_result": null + } + ], "run-locally.sh": [ { "hashed_secret": "8cbd3af3de67a89adedc45b1e3d99b7b135a8ca4", diff --git a/galasa-parent/dev.galasa.framework.api.common/build.gradle b/galasa-parent/dev.galasa.framework.api.common/build.gradle index 06199c47d..9db06248e 100644 --- a/galasa-parent/dev.galasa.framework.api.common/build.gradle +++ b/galasa-parent/dev.galasa.framework.api.common/build.gradle @@ -7,7 +7,7 @@ plugins { description = 'Framework API - Common Packages' -version = '0.37.0' +version = '0.38.0' dependencies { implementation project(':dev.galasa.framework') diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java index 025b334a1..2b6357784 100644 --- a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java @@ -65,7 +65,7 @@ public enum ServletErrorMessage { GAL5022_UNABLE_TO_PARSE_SHARED_ENVIRONMENT_PHASE (5022, "E: Error occurred trying parse the sharedEnvironmentPhase ''{0}''. Valid options are 'BUILD', 'DISCARD'."), //Galasa Property... - GAL5023_UNABLE_TO_CAST_TO_GALASAPROPERTY (5023, "E: Error occurred trying to interpret resource ''{0}''. P This could indicate a mis-match between client and server levels. Please check with your Ecosystem administrator the level. You may have to upgrade/downgrade your client program."), + GAL5023_UNABLE_TO_CAST_TO_GALASAPROPERTY (5023, "E: Error occurred trying to interpret resource ''{0}''. This could indicate a mis-match between client and server levels. Please check the level with your Ecosystem administrator. You may have to upgrade/downgrade your client program."), GAL5024_INVALID_GALASAPROPERTY (5024, "E: Error occurred because the Galasa Property is invalid. ''{0}''"), GAL5031_EMPTY_NAMESPACE (5031, "E: Invalid namespace. Namespace is empty."), GAL5032_INVALID_FIRST_CHARACTER_NAMESPACE (5032, "E: Invalid namespace name. ''{0}'' must not start with the ''{1}'' character. Allowable first characters are 'a'-'z' or 'A'-'Z'."), @@ -83,11 +83,23 @@ public enum ServletErrorMessage { GAL5044_INVALID_PROPERTY_NAME_TRAILING_DOT (5044, "E: Invalid property name. Property name ''{0}'' must not end with a '.' (dot) separator."), //Resources APIs... - GAL5025_UNSUPPORTED_ACTION (5025, "E: Error occurred. The field 'action' in the request body is invalid. The 'action' value''{0}'' supplied is not supported. Supported actions are: create, apply and update. This could indicate a mis-match between client and server levels. Please check with your Ecosystem administrator the level. You may have to upgrade/downgrade your client program."), - GAL5026_UNSUPPORTED_RESOURCE_TYPE (5026, "E: Error occurred. The field 'kind' in the request body is invalid. The value ''{0}'' is not supported. This could indicate a mis-match between client and server levels. Please check with your Ecosystem administrator the level. You may have to upgrade/downgrade your client program."), - GAL5027_UNSUPPORTED_API_VERSION (5027, "E: Error occurred. The field 'apiVersion' in the request body is invalid. The value ''{0}'' is not a supported version. Currently the ecosystem accepts the ''{1}'' api version. This could indicate a mis-match between client and server levels. Please check with your Ecosystem administrator the level. You may have to upgrade/downgrade your client program."), + GAL5025_UNSUPPORTED_ACTION (5025, "E: Error occurred. The field ''action'' in the request body is invalid. Supported actions are: create, apply and update. This could indicate a mis-match between client and server levels. Please check the level with your Ecosystem administrator. You may have to upgrade/downgrade your client program so that the levels of client and server match."), + GAL5026_UNSUPPORTED_RESOURCE_TYPE (5026, "E: Error occurred. The field ''kind'' in the request body is invalid. This could indicate a mis-match between client and server levels. Please check the level with your Ecosystem administrator. You may have to upgrade/downgrade your client program so that the levels of client and server match."), + GAL5027_UNSUPPORTED_API_VERSION (5027, "E: Error occurred. The field ''apiVersion'' in the request body is invalid. Currently the ecosystem accepts the ''{0}'' api version. This could indicate a mis-match between client and server levels. Please check the level with your Ecosystem administrator. You may have to upgrade/downgrade your client program so that the levels of client and server match."), GAL5067_NULL_RESOURCE_IN_BODY (5067, "E: Error occurred. A ''NULL'' value is not a valid resource. Please check the request format, or check with your Ecosystem administrator."), GAL5068_EMPTY_JSON_RESOURCE_IN_BODY (5068, "E: Error occurred. The JSON element for a resource can not be empty. Please check the request format, or check with your Ecosystem administrator."), + GAL5069_MISSING_REQUIRED_FIELDS (5069, "E: Invalid request body provided. The following mandatory fields are missing from the request body: [{0}]. Please check that your request body contains these fields and try again."), + + // GalasaSecret validation... + GAL5070_INVALID_GALASA_SECRET_MISSING_FIELDS (5070, "E: Invalid GalasaSecret provided. One or more of the following mandatory fields are missing from the ''{0}'' field: [{1}]. Check that your request payload is correct and try again."), + GAL5072_INVALID_GALASA_SECRET_MISSING_TYPE_DATA (5072, "E: Invalid GalasaSecret provided. The ''{0}'' type was provided but the following fields are missing from the ''data'' field: [{1}]. Check that your request payload is correct and try again."), + GAL5073_UNSUPPORTED_GALASA_SECRET_ENCODING (5073, "E: Unsupported data encoding scheme provided. Supported encoding schemes are: [{0}]. Check that your request payload is correct and try again."), + GAL5074_UNKNOWN_GALASA_SECRET_TYPE (5074, "E: Unknown GalasaSecret type provided. Supported GalasaSecret types are: [{0}]. Check that your request payload is correct and try again."), + GAL5075_ERROR_SECRET_ALREADY_EXISTS (5075, "E: Error occurred when trying to create a secret with the given ID. A secret with the provided ID already exists."), + GAL5076_ERROR_SECRET_DOES_NOT_EXIST (5076, "E: Error occurred when trying to update a secret with the given ID. A secret with the provided ID does not exist and therefore cannot be updated."), + GAL5077_FAILED_TO_SET_SECRET (5077, "E: Failed to set a secret with the given ID in the credentials store. The credentials store might be experiencing temporary issues. Report the problem to your Galasa Ecosystem owner."), + GAL5078_FAILED_TO_DELETE_SECRET (5078, "E: Failed to delete a secret with the given ID from the credentials store. The credentials store might be experiencing temporary issues. Report the problem to your Galasa Ecosystem owner."), + GAL5079_FAILED_TO_GET_SECRET (5079, "E: Failed to retrieve the secret with the given ID from the credentials store. A secret with the provided ID does not exist and therefore cannot be updated."), // Auth APIs... GAL5051_INVALID_GALASA_TOKEN_PROVIDED (5051, "E: Invalid GALASA_TOKEN value provided. Please ensure you have set the correct GALASA_TOKEN property for the targeted ecosystem at ''{0}'' and try again."), diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaResourceType.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaResourceType.java index 4e20b969c..0ef551d77 100644 --- a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaResourceType.java +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaResourceType.java @@ -5,28 +5,29 @@ */ package dev.galasa.framework.api.common.resources; - public enum GalasaResourceType{ - GALASAPROPERTY("GalasaProperty"), - ; - private String value; +public enum GalasaResourceType { + GALASA_PROPERTY("GalasaProperty"), + GALASA_SECRET("GalasaSecret"); - private GalasaResourceType(String type){ - this.value = type; - } + private String name; - public static GalasaResourceType getfromString(String typeAsString){ - GalasaResourceType match = null; - for (GalasaResourceType type : GalasaResourceType.values()){ - if (type.toString().equalsIgnoreCase(typeAsString)){ - match = type; - } - } - return match; - } + private GalasaResourceType(String name) { + this.name = name; + } - @Override - public String toString(){ - return value; + public static GalasaResourceType getFromString(String resourceAsString) { + GalasaResourceType match = null; + for (GalasaResourceType resource : values()) { + if (resource.toString().equalsIgnoreCase(resourceAsString.trim())) { + match = resource; + break; + } } + return match; + } - } \ No newline at end of file + @Override + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaSecretType.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaSecretType.java new file mode 100644 index 000000000..6ce4bf7bd --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaSecretType.java @@ -0,0 +1,41 @@ + /* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.common.resources; + +public enum GalasaSecretType { + USERNAME_PASSWORD("UsernamePassword", "username", "password"), + USERNAME_TOKEN("UsernameToken", "username", "token"), + USERNAME("Username", "username"), + TOKEN("Token", "token"); + + private String name; + private String[] requiredDataFields; + + private GalasaSecretType(String type, String... requiredDataFields) { + this.name = type; + this.requiredDataFields = requiredDataFields; + } + + public static GalasaSecretType getFromString(String typeAsString) { + GalasaSecretType match = null; + for (GalasaSecretType resource : values()) { + if (resource.toString().equalsIgnoreCase(typeAsString.trim())) { + match = resource; + break; + } + } + return match; + } + + @Override + public String toString() { + return name; + } + + public String[] getRequiredDataFields() { + return requiredDataFields; + } +} \ No newline at end of file diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/ResourceAction.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/ResourceAction.java new file mode 100644 index 000000000..166433605 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/ResourceAction.java @@ -0,0 +1,35 @@ + /* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.common.resources; + +public enum ResourceAction { + APPLY("apply"), + CREATE("create"), + UPDATE("update"), + DELETE("delete"); + + private String actionLabel; + + private ResourceAction(String action) { + this.actionLabel = action; + } + + public static ResourceAction getFromString(String actionAsString) { + ResourceAction match = null; + for (ResourceAction action : values()) { + if (action.toString().equalsIgnoreCase(actionAsString.trim())) { + match = action; + break; + } + } + return match; + } + + @Override + public String toString() { + return actionLabel; + } +} \ No newline at end of file diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/Secret.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/Secret.java new file mode 100644 index 000000000..90f7d5888 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/Secret.java @@ -0,0 +1,59 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.common.resources; + +import dev.galasa.ICredentials; +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.ServletError; +import dev.galasa.framework.spi.creds.CredentialsException; +import dev.galasa.framework.spi.creds.ICredentialsService; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; + +import javax.servlet.http.HttpServletResponse; + +public class Secret { + + private String secretId; + private ICredentialsService credentialsService; + private ICredentials value; + + public Secret(ICredentialsService credentialsService, String secretName) { + this.secretId = secretName; + this.credentialsService = credentialsService; + } + + public boolean existsInCredentialsStore() { + return value != null; + } + + public void loadValueFromCredentialsStore() throws InternalServletException { + try { + value = credentialsService.getCredentials(secretId); + } catch (CredentialsException e) { + ServletError error = new ServletError(GAL5079_FAILED_TO_GET_SECRET); + throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + public void setSecretToCredentialsStore(ICredentials newValue) throws InternalServletException { + try { + credentialsService.setCredentials(secretId, newValue); + } catch (CredentialsException e) { + ServletError error = new ServletError(GAL5077_FAILED_TO_SET_SECRET); + throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + public void deleteSecretFromCredentialsStore() throws InternalServletException { + try { + credentialsService.deleteCredentials(secretId); + } catch (CredentialsException e) { + ServletError error = new ServletError(GAL5078_FAILED_TO_DELETE_SECRET); + throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/galasa-parent/dev.galasa.framework.api.common/src/test/java/dev/galasa/framework/api/common/resources/TestGalasaResourceType.java b/galasa-parent/dev.galasa.framework.api.common/src/test/java/dev/galasa/framework/api/common/resources/TestGalasaResourceType.java index 45ddd49d6..542f5fa72 100644 --- a/galasa-parent/dev.galasa.framework.api.common/src/test/java/dev/galasa/framework/api/common/resources/TestGalasaResourceType.java +++ b/galasa-parent/dev.galasa.framework.api.common/src/test/java/dev/galasa/framework/api/common/resources/TestGalasaResourceType.java @@ -14,7 +14,7 @@ public class TestGalasaResourceType { @Test public void TestResourceTypeGalasaPropertyReturnGalasaProperty(){ //Given... - GalasaResourceType resourceType = GalasaResourceType.GALASAPROPERTY; + GalasaResourceType resourceType = GalasaResourceType.GALASA_PROPERTY; //When... String typeValue = resourceType.toString(); //Then... @@ -24,7 +24,7 @@ public void TestResourceTypeGalasaPropertyReturnGalasaProperty(){ @Test public void TestResourceTypeGalasaPropertyLowerCaseFromStringReturnGalasaProperty(){ //Given... - GalasaResourceType resourceType = GalasaResourceType.getfromString("galasaproperty"); + GalasaResourceType resourceType = GalasaResourceType.getFromString("galasaproperty"); //When... String typeValue = resourceType.toString(); //Then... @@ -34,7 +34,7 @@ public void TestResourceTypeGalasaPropertyLowerCaseFromStringReturnGalasaPropert @Test public void TestResourceTypeGalasaPropertyUpperCaseFromStringReturnGalasaProperty(){ //Given... - GalasaResourceType resourceType = GalasaResourceType.getfromString("GALASAPROPERTY"); + GalasaResourceType resourceType = GalasaResourceType.getFromString("GALASAPROPERTY"); //When... String typeValue = resourceType.toString(); //Then... @@ -44,7 +44,7 @@ public void TestResourceTypeGalasaPropertyUpperCaseFromStringReturnGalasaPropert @Test public void TestResourceTypeGalasaPropertyMixedCaseFromStringReturnGalasaProperty(){ //Given... - GalasaResourceType resourceType = GalasaResourceType.getfromString("GaLaSaPrOpErTy"); + GalasaResourceType resourceType = GalasaResourceType.getFromString("GaLaSaPrOpErTy"); //When... String typeValue = resourceType.toString(); //Then... diff --git a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockCredentialsService.java b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockCredentialsService.java new file mode 100644 index 000000000..aa69207f4 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockCredentialsService.java @@ -0,0 +1,57 @@ +package dev.galasa.framework.api.common.mocks; + +import java.util.HashMap; +import java.util.Map; + +import javax.validation.constraints.NotNull; + +import dev.galasa.ICredentials; +import dev.galasa.framework.spi.creds.CredentialsException; +import dev.galasa.framework.spi.creds.ICredentialsService; + +public class MockCredentialsService implements ICredentialsService { + + private Map creds = new HashMap<>(); + + private boolean throwError = false; + + public MockCredentialsService(Map creds) { + this.creds = creds; + } + + @Override + public ICredentials getCredentials(@NotNull String credentialsId) throws CredentialsException { + if (throwError) { + throwMockError(); + } + return this.creds.get(credentialsId); + } + + @Override + public void setCredentials(String credentialsId, ICredentials credentials) throws CredentialsException { + if (throwError) { + throwMockError(); + } + this.creds.put(credentialsId, credentials); + } + + @Override + public void deleteCredentials(String credentialsId) throws CredentialsException { + if (throwError) { + throwMockError(); + } + this.creds.remove(credentialsId); + } + + public Map getAllCredentials() { + return creds; + } + + public void setThrowError(boolean throwError) { + this.throwError = throwError; + } + + private void throwMockError() throws CredentialsException { + throw new CredentialsException("simulating a credentials service error"); + } +} diff --git a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockFramework.java b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockFramework.java index 0b10b5f94..edcd8608e 100644 --- a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockFramework.java +++ b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockFramework.java @@ -26,6 +26,7 @@ import dev.galasa.framework.spi.creds.ICredentialsService; import java.net.URL; +import java.util.HashMap; import java.util.Properties; import java.util.Random; import javax.validation.constraints.NotNull; @@ -34,6 +35,7 @@ public class MockFramework implements IFramework { IResultArchiveStore archiveStore; IFrameworkRuns frameworkRuns; MockIConfigurationPropertyStoreService cpsService = new MockIConfigurationPropertyStoreService("framework"); + MockCredentialsService creds = new MockCredentialsService(new HashMap<>()); IAuthStoreService authStoreService; public MockFramework() { @@ -85,6 +87,11 @@ public IFrameworkRuns getFrameworkRuns() throws FrameworkException { return this.frameworkRuns; } + @Override + public @NotNull ICredentialsService getCredentialsService() throws CredentialsException { + return this.creds; + } + @Override public void setFrameworkProperties(Properties overrideProperties) { throw new UnsupportedOperationException("Unimplemented method 'setFrameworkProperties'"); @@ -116,11 +123,6 @@ public boolean isInitialised() { throw new UnsupportedOperationException("Unimplemented method 'getConfidentialTextService'"); } - @Override - public @NotNull ICredentialsService getCredentialsService() throws CredentialsException { - throw new UnsupportedOperationException("Unimplemented method 'getCredentialsService'"); - } - @Override public String getTestRunName() { throw new UnsupportedOperationException("Unimplemented method 'getTestRunName'"); diff --git a/galasa-parent/dev.galasa.framework.api.openapi/src/main/resources/openapi.yaml b/galasa-parent/dev.galasa.framework.api.openapi/src/main/resources/openapi.yaml index 303a40baa..007c66f87 100644 --- a/galasa-parent/dev.galasa.framework.api.openapi/src/main/resources/openapi.yaml +++ b/galasa-parent/dev.galasa.framework.api.openapi/src/main/resources/openapi.yaml @@ -1881,6 +1881,53 @@ components: description: | The value of the property, maximum length of approximately 1,500,000 characters. It cannot be a blank value. + GalasaSecret: + type: object + properties: + apiVersion: + type: string + kind: + type: string + enum: [GalasaSecret] + metadata: + type: object + properties: + name: + type: string + description: | + The name of the Galasa Secret to perform a resource action on. + encoding: + type: string + description: | + The character encoding scheme that has already been applied to all the values within the 'data' field. + Currently, 'base64' is the only supported encoding scheme. + type: + type: string + enum: [UsernamePassword, Username, UsernameToken, Token] + description: | + The type of the Galasa Secret resource. Supported types are 'UsernamePassword', 'Username', + 'UsernameToken', 'Token'. + data: + type: object + properties: + username: + type: string + description: | + A username for a system. Required if the Secret 'type' is set to 'UsernamePassword', 'Username', + or 'UsernameToken'. If the 'encoding' has been set in the metadata, this value must already be + encoded with the given encoding scheme (e.g. base64). + password: + type: string + description: | + A password for a system. Required if the Secret 'type' is set to 'UsernamePassword'. + If the 'encoding' has been set in the metadata, this value must already be encoded + with the given encoding scheme (e.g. base64). + token: + type: string + description: | + A token for a system. Required if the Secret 'type' is set to 'UsernameToken' or 'Token'. + If the 'encoding' has been set in the metadata, this value must already be encoded with + the given encoding scheme (e.g. base64). APIErrorArray: type: array description: An array of API Errors relating to the resources that failed to complete as expected diff --git a/galasa-parent/dev.galasa.framework.api.resources/build.gradle b/galasa-parent/dev.galasa.framework.api.resources/build.gradle index 94fe6b47c..74864c88c 100644 --- a/galasa-parent/dev.galasa.framework.api.resources/build.gradle +++ b/galasa-parent/dev.galasa.framework.api.resources/build.gradle @@ -8,7 +8,7 @@ plugins { description = 'Galasa API - Resources' -version '0.37.0' +version '0.38.0' dependencies { implementation project(':dev.galasa.framework') diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/ResourcesServlet.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/ResourcesServlet.java index 29a85d350..645687562 100644 --- a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/ResourcesServlet.java +++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/ResourcesServlet.java @@ -22,6 +22,7 @@ import dev.galasa.framework.api.resources.routes.ResourcesRoute; import dev.galasa.framework.spi.ConfigurationPropertyStoreException; import dev.galasa.framework.spi.IFramework; +import dev.galasa.framework.spi.creds.CredentialsException; /* * Proxy Servlet for the /resources/* endpoints */ @@ -53,8 +54,8 @@ public void init() throws ServletException { super.init(); try { - addRoute(new ResourcesRoute(getResponseBuilder(), new CPSFacade(framework))); - } catch (ConfigurationPropertyStoreException e) { + addRoute(new ResourcesRoute(getResponseBuilder(), new CPSFacade(framework), framework.getCredentialsService())); + } catch (ConfigurationPropertyStoreException | CredentialsException e) { logger.error("Failed to initialise the Resources servlet", e); throw new ServletException("Failed to initialise the Resources servlet", e); } diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/AbstractGalasaResourceProcessor.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/AbstractGalasaResourceProcessor.java new file mode 100644 index 000000000..9d4aca528 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/AbstractGalasaResourceProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.resources.processors; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; +import static dev.galasa.framework.api.common.resources.ResourceAction.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.servlet.http.HttpServletResponse; + +import com.google.gson.JsonObject; + +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.ServletError; +import dev.galasa.framework.api.common.resources.ResourceAction; +import dev.galasa.framework.spi.utils.GalasaGson; + +public abstract class AbstractGalasaResourceProcessor { + protected static final Set updateActions = Set.of(APPLY, UPDATE); + protected static final GalasaGson gson = new GalasaGson(); + + protected void checkResourceHasRequiredFields( + JsonObject resourceJson, + String expectedApiVersion, + ResourceAction action + ) throws InternalServletException { + List requiredFields = getRequiredResourceFields(action); + List missingFields = getMissingResourceFields(resourceJson, requiredFields); + if (!missingFields.isEmpty()) { + ServletError error = new ServletError(GAL5069_MISSING_REQUIRED_FIELDS, String.join(", ", missingFields)); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + + String apiVersion = resourceJson.get("apiVersion").getAsString(); + if (!apiVersion.equals(expectedApiVersion)) { + ServletError error = new ServletError(GAL5027_UNSUPPORTED_API_VERSION, expectedApiVersion); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + } + + protected List getMissingResourceFields(JsonObject resourceJson, List requiredFields) { + List missingFields = new ArrayList<>(); + for (String field : requiredFields) { + if (!resourceJson.has(field)) { + missingFields.add(field); + } + } + return missingFields; + } + + private List getRequiredResourceFields(ResourceAction action) { + List requiredFields = new ArrayList<>(); + requiredFields.add("apiVersion"); + requiredFields.add("metadata"); + if (action != DELETE) { + requiredFields.add("data"); + } + return requiredFields; + } +} diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessor.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessor.java new file mode 100644 index 000000000..ea93ab5ce --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessor.java @@ -0,0 +1,135 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.resources.processors; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; +import static dev.galasa.framework.api.common.resources.ResourceAction.*; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import dev.galasa.framework.api.beans.GalasaProperty; +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.ServletError; +import dev.galasa.framework.api.common.resources.CPSFacade; +import dev.galasa.framework.api.common.resources.CPSNamespace; +import dev.galasa.framework.api.common.resources.CPSProperty; +import dev.galasa.framework.api.common.resources.ResourceAction; +import dev.galasa.framework.api.common.resources.ResourceNameValidator; +import dev.galasa.framework.spi.ConfigurationPropertyStoreException; + +public class GalasaPropertyProcessor extends AbstractGalasaResourceProcessor implements IGalasaResourceProcessor { + + private CPSFacade cps; + static final ResourceNameValidator nameValidator = new ResourceNameValidator(); + + public GalasaPropertyProcessor(CPSFacade cps) { + this.cps = cps; + } + + @Override + public List processResource(JsonObject resource, ResourceAction action) throws InternalServletException { + List errors = checkGalasaPropertyJsonStructure(resource, action); + try { + if (errors.isEmpty()) { + GalasaProperty galasaProperty = gson.fromJson(resource, GalasaProperty.class); + CPSNamespace namespace = cps.getNamespace(galasaProperty.getNamespace()); + + //getPropertyFromStore() will only return null if the property is in a hidden namespace + CPSProperty property = namespace.getPropertyFromStore(galasaProperty.getName()); + + if (action == DELETE) { + property.deletePropertyFromStore(); + } else { + /* + * The logic below is used to determine if the exclusive Not Or condition in property.setPropertyToStore + * (i.e. "the property exists" must equal to "is this an update action") will action the request or error + * + * Logic Table to Determine actions + * If the action is equal to "update" (force update) the updateProperty is set to true (update property, + * will error if the property does not exist in CPS) + * If the action is either "update" or "apply" and the property exists in CPS the updateProperty is set to true (update property) + * If the action is equal to "apply" and the property does not exist in CPS the updateProperty is set to false (create property) + * If the action is equal to "create" (force create) the updateProperty is set to false (create property, will error if the property exists in CPS) + */ + boolean updateProperty = false; + if ((updateActions.contains(action) && property.existsInStore()) || action == UPDATE) { + updateProperty = true; + } + property.setPropertyToStore(galasaProperty, updateProperty); + } + } + } catch (ConfigurationPropertyStoreException e){ + ServletError error = new ServletError(GAL5000_GENERIC_API_ERROR, e.getMessage()); + throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } + return errors; + } + + private List checkGalasaPropertyJsonStructure(JsonObject propertyJson, ResourceAction action) throws InternalServletException { + checkResourceHasRequiredFields(propertyJson, GalasaProperty.DEFAULTAPIVERSION, action); + + List validationErrors = new ArrayList(); + validatePropertyMetadata(propertyJson, validationErrors); + + // Delete operations shouldn't require a 'data' section, just the metadata to identify + // the property to delete + if (action != DELETE) { + validatePropertyData(propertyJson, validationErrors); + } + return validationErrors; + } + + private void validatePropertyMetadata(JsonObject propertyJson, List validationErrors) { + //Check metadata is not null and contains name and namespace fields in the correct format + JsonObject metadata = propertyJson.get("metadata").getAsJsonObject(); + if (metadata.has("name") && metadata.has("namespace")) { + JsonElement name = metadata.get("name"); + JsonElement namespace = metadata.get("namespace"); + + // Use the ResourceNameValidator to check that the name is correctly formatted and not null + try { + nameValidator.assertPropertyNameCharPatternIsValid(name.getAsString()); + } catch (InternalServletException e) { + // All ResourceNameValidator error should be added to the list of reasons why the property action has failed + validationErrors.add(e.getMessage()); + } + + // Use the ResourceNameValidator to check that the namespace is correctly formatted and not null + try { + nameValidator.assertNamespaceCharPatternIsValid(namespace.getAsString()); + } catch (InternalServletException e) { + validationErrors.add(e.getMessage()); + } + } else { + String message = "The 'metadata' field cannot be empty. The fields 'name' and 'namespace' are mandatory for the type GalasaProperty."; + ServletError error = new ServletError(GAL5024_INVALID_GALASAPROPERTY, message); + validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); + } + } + + private void validatePropertyData(JsonObject propertyJson, List validationErrors) { + //Check that data is not null and contains the value field + JsonObject data = propertyJson.get("data").getAsJsonObject(); + if (data.size() > 0 && data.has("value")) { + String value = data.get("value").getAsString(); + if (value == null || value.isBlank()) { + String message = "The 'value' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty."; + ServletError error = new ServletError(GAL5024_INVALID_GALASAPROPERTY, message); + validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); + } + } else { + String message = "The 'data' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty."; + ServletError error = new ServletError(GAL5024_INVALID_GALASAPROPERTY, message); + validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); + } + } +} diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessor.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessor.java new file mode 100644 index 000000000..2ab2485f5 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessor.java @@ -0,0 +1,207 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.resources.processors; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; +import static dev.galasa.framework.api.common.resources.ResourceAction.*; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Base64.Decoder; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.google.gson.JsonObject; + +import dev.galasa.ICredentials; +import dev.galasa.framework.api.beans.generated.GalasaSecret; +import dev.galasa.framework.api.beans.generated.GalasaSecretdata; +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.ServletError; +import dev.galasa.framework.api.common.resources.GalasaSecretType; +import dev.galasa.framework.api.common.resources.ResourceAction; +import dev.galasa.framework.api.common.resources.Secret; +import dev.galasa.framework.spi.creds.CredentialsToken; +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.ICredentialsService; + +/** + * Processor class to handle creating, updating, and deleting GalasaSecret resources + */ +public class GalasaSecretProcessor extends AbstractGalasaResourceProcessor implements IGalasaResourceProcessor { + + private final Log logger = LogFactory.getLog(getClass()); + + private static final String DEFAULT_API_VERSION = "galasa-dev/v1alpha1"; + private static final List SUPPORTED_ENCODING_SCHEMES = List.of("base64"); + + private ICredentialsService credentialsService; + + public GalasaSecretProcessor(ICredentialsService credentialsService) { + this.credentialsService = credentialsService; + } + + @Override + public List processResource(JsonObject resourceJson, ResourceAction action) throws InternalServletException { + logger.info("Processing GalasaSecret resource"); + List errors = checkGalasaSecretJsonStructure(resourceJson, action); + if (errors.isEmpty()) { + logger.info("GalasaSecret validated successfully"); + GalasaSecret galasaSecret = gson.fromJson(resourceJson, GalasaSecret.class); + String credentialsId = galasaSecret.getmetadata().getname(); + Secret secret = new Secret(credentialsService, credentialsId); + + if (action == DELETE) { + logger.info("Deleting secret from credentials store"); + secret.deleteSecretFromCredentialsStore(); + logger.info("Deleted secret from credentials store OK"); + } else { + secret.loadValueFromCredentialsStore(); + boolean secretExists = secret.existsInCredentialsStore(); + if (action == CREATE && secretExists) { + ServletError error = new ServletError(GAL5075_ERROR_SECRET_ALREADY_EXISTS); + throw new InternalServletException(error, HttpServletResponse.SC_CONFLICT); + } else if (action == UPDATE && !secretExists) { + ServletError error = new ServletError(GAL5076_ERROR_SECRET_DOES_NOT_EXIST); + throw new InternalServletException(error, HttpServletResponse.SC_NOT_FOUND); + } + + GalasaSecretType secretType = GalasaSecretType.getFromString(galasaSecret.getmetadata().gettype().toString()); + GalasaSecretdata decodedData = decodeSecretData(galasaSecret); + ICredentials credentials = getCredentialsFromSecret(secretType, decodedData); + + logger.info("Setting secret in credentials store"); + secret.setSecretToCredentialsStore(credentials); + logger.info("Secret set in credentials store OK"); + } + logger.info("Processed GalasaSecret resource OK"); + } + return errors; + } + + private GalasaSecretdata decodeSecretData(GalasaSecret galasaSecret) throws InternalServletException { + String encoding = galasaSecret.getmetadata().getencoding(); + GalasaSecretdata existingData = galasaSecret.getdata(); + + GalasaSecretdata decodedData = new GalasaSecretdata(); + + if (encoding == null) { + decodedData = existingData; + } else if (encoding.equalsIgnoreCase("base64")) { + logger.info("Base64-decoding the provided GalasaSecret resource data"); + Decoder decoder = Base64.getDecoder(); + + String username = existingData.getusername(); + String password = existingData.getpassword(); + String token = existingData.gettoken(); + + if (username != null) { + decodedData.setusername(new String(decoder.decode(username), StandardCharsets.UTF_8)); + } + + if (password != null) { + decodedData.setpassword(new String(decoder.decode(password), StandardCharsets.UTF_8)); + } + + if (token != null) { + decodedData.settoken(new String(decoder.decode(token), StandardCharsets.UTF_8)); + } + logger.info("Decoded the provided GalasaSecret resource data OK"); + } else { + // This should never be reached since the secret JSON has already been validated + ServletError error = new ServletError(GAL5073_UNSUPPORTED_GALASA_SECRET_ENCODING, String.join(", ", SUPPORTED_ENCODING_SCHEMES)); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + return decodedData; + } + + private List checkGalasaSecretJsonStructure(JsonObject secretJson, ResourceAction action) throws InternalServletException { + checkResourceHasRequiredFields(secretJson, DEFAULT_API_VERSION, action); + + List validationErrors = new ArrayList<>(); + validateSecretMetadata(secretJson, validationErrors); + + // Delete operations shouldn't require a 'data' section, just the metadata to identify + // the credentials entry to delete + if (validationErrors.isEmpty() && action != DELETE) { + validateSecretData(secretJson, validationErrors); + } + return validationErrors; + } + + private ICredentials getCredentialsFromSecret(GalasaSecretType secretType, GalasaSecretdata decodedData) { + ICredentials credentials = null; + switch (secretType) { + case USERNAME: + credentials = new CredentialsUsername(decodedData.getusername()); + break; + case TOKEN: + credentials = new CredentialsToken(decodedData.gettoken()); + break; + case USERNAME_PASSWORD: + credentials = new CredentialsUsernamePassword(decodedData.getusername(), decodedData.getpassword()); + break; + case USERNAME_TOKEN: + credentials = new CredentialsUsernameToken(decodedData.getusername(), decodedData.gettoken()); + break; + default: + break; + } + return credentials; + } + + private void validateSecretMetadata(JsonObject secretJson, List validationErrors) { + JsonObject metadata = secretJson.get("metadata").getAsJsonObject(); + + // Check if the secret has a name and a type + if (!metadata.has("name") || !metadata.has("type")) { + ServletError error = new ServletError(GAL5070_INVALID_GALASA_SECRET_MISSING_FIELDS, "metadata", "name, type"); + validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); + } + + // Check if the given secret type is a valid type + if (metadata.has("type")) { + GalasaSecretType secretType = GalasaSecretType.getFromString(metadata.get("type").getAsString()); + if (secretType == null) { + String supportedSecretTypes = Arrays.stream(GalasaSecretType.values()) + .map(GalasaSecretType::toString) + .collect(Collectors.joining(", ")); + + ServletError error = new ServletError(GAL5074_UNKNOWN_GALASA_SECRET_TYPE, supportedSecretTypes); + validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); + } + } + + // Check if the given encoding scheme is supported + if (metadata.has("encoding") && !SUPPORTED_ENCODING_SCHEMES.contains(metadata.get("encoding").getAsString())) { + ServletError error = new ServletError(GAL5073_UNSUPPORTED_GALASA_SECRET_ENCODING, String.join(", ", SUPPORTED_ENCODING_SCHEMES)); + validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); + } + } + + private void validateSecretData(JsonObject secretJson, List validationErrors) { + JsonObject metadata = secretJson.get("metadata").getAsJsonObject(); + JsonObject data = secretJson.get("data").getAsJsonObject(); + + GalasaSecretType secretType = GalasaSecretType.getFromString(metadata.get("type").getAsString()); + String[] requiredTypeFields = secretType.getRequiredDataFields(); + List missingFields = getMissingResourceFields(data, Arrays.asList(requiredTypeFields)); + + if (!missingFields.isEmpty()) { + ServletError error = new ServletError(GAL5072_INVALID_GALASA_SECRET_MISSING_TYPE_DATA, secretType.toString(), String.join(", ", missingFields)); + validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); + } + } +} diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/IGalasaResourceProcessor.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/IGalasaResourceProcessor.java new file mode 100644 index 000000000..5679b1c34 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/processors/IGalasaResourceProcessor.java @@ -0,0 +1,26 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.resources.processors; + +import java.util.List; + +import com.google.gson.JsonObject; + +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.resources.ResourceAction; + +public interface IGalasaResourceProcessor { + /** + * Performs a given action on a provided Galasa resource. + * The action could be to create, update, or delete the given resource. + * + * @param resourceJson the resource to perform an action on + * @param action the action to perform + * @return a list of validation errors encountered when processing the given JSON payload + * @throws InternalServletException if there was an issue processing the resource + */ + List processResource(JsonObject resourceJson, ResourceAction action) throws InternalServletException; +} diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/routes/ResourcesRoute.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/routes/ResourcesRoute.java index a61352c6a..ee5e6ebb5 100644 --- a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/routes/ResourcesRoute.java +++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/routes/ResourcesRoute.java @@ -6,12 +6,13 @@ package dev.galasa.framework.api.resources.routes; import static dev.galasa.framework.api.common.ServletErrorMessage.*; +import static dev.galasa.framework.api.common.resources.GalasaResourceType.*; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -21,18 +22,20 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import dev.galasa.framework.api.beans.GalasaProperty; import dev.galasa.framework.api.common.BaseRoute; import dev.galasa.framework.api.common.InternalServletException; import dev.galasa.framework.api.common.QueryParameters; import dev.galasa.framework.api.common.ResponseBuilder; import dev.galasa.framework.api.common.ServletError; import dev.galasa.framework.api.common.resources.CPSFacade; -import dev.galasa.framework.api.common.resources.CPSNamespace; -import dev.galasa.framework.api.common.resources.CPSProperty; +import dev.galasa.framework.api.common.resources.GalasaResourceType; +import dev.galasa.framework.api.common.resources.ResourceAction; import dev.galasa.framework.api.common.resources.ResourceNameValidator; -import dev.galasa.framework.spi.ConfigurationPropertyStoreException; +import dev.galasa.framework.api.resources.processors.GalasaPropertyProcessor; +import dev.galasa.framework.api.resources.processors.GalasaSecretProcessor; +import dev.galasa.framework.api.resources.processors.IGalasaResourceProcessor; import dev.galasa.framework.spi.FrameworkException; +import dev.galasa.framework.spi.creds.ICredentialsService; import dev.galasa.framework.spi.utils.GalasaGson; public class ResourcesRoute extends BaseRoute{ @@ -42,16 +45,16 @@ public class ResourcesRoute extends BaseRoute{ static final ResourceNameValidator nameValidator = new ResourceNameValidator(); protected static final String path = "\\/"; - private static final Set validActions = Collections.unmodifiableSet(Set.of("apply","create","update", "delete")); - private static final Set updateActions = Collections.unmodifiableSet(Set.of("apply","update")); + + private Map resourceProcessors = new HashMap<>(); protected List errors = new ArrayList(); - private CPSFacade cps; - - public ResourcesRoute(ResponseBuilder responseBuilder, CPSFacade cps) { + public ResourcesRoute(ResponseBuilder responseBuilder, CPSFacade cps, ICredentialsService credentialsService) { super(responseBuilder, path); - this.cps = cps; + + resourceProcessors.put(GALASA_PROPERTY, new GalasaPropertyProcessor(cps)); + resourceProcessors.put(GALASA_SECRET, new GalasaSecretProcessor(credentialsService)); } @Override @@ -74,12 +77,13 @@ public HttpServletResponse handlePostRequest(String pathInfo, QueryParameters qu } protected List processRequest(JsonObject body) throws InternalServletException{ - String action = body.get("action").getAsString().toLowerCase().trim(); - if (validActions.contains(action)){ + String actionStr = body.get("action").getAsString().toLowerCase().trim(); + ResourceAction action = ResourceAction.getFromString(actionStr); + if (action != null){ JsonArray jsonArray = body.get("data").getAsJsonArray(); processDataArray(jsonArray, action); } else { - ServletError error = new ServletError(GAL5025_UNSUPPORTED_ACTION, action); + ServletError error = new ServletError(GAL5025_UNSUPPORTED_ACTION); throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); } return errors; @@ -101,121 +105,24 @@ protected String getErrorsAsJson(List errorsList){ return gson.toJson(json); } - protected void processDataArray(JsonArray jsonArray, String action) throws InternalServletException{ - for (JsonElement element: jsonArray){ + protected void processDataArray(JsonArray jsonArray, ResourceAction action) throws InternalServletException{ + for (JsonElement element: jsonArray) { try { checkJsonElementIsValidJSON(element); JsonObject resource = element.getAsJsonObject(); - String kind = resource.get("kind").getAsString(); - switch (kind){ - case "GalasaProperty": - processGalasaProperty(resource,action); - break; - default: - ServletError error = new ServletError(GAL5026_UNSUPPORTED_RESOURCE_TYPE,kind); - throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); - } - } catch(InternalServletException s){ - errors.add(s.getMessage()); - } - } - } - - private boolean checkGalasaPropertyJsonStructure(JsonObject propertyJson) throws InternalServletException{ - List validationErrors = new ArrayList(); - if (propertyJson.has("apiVersion")&& propertyJson.has("metadata")&&propertyJson.has("data")){ - //Check metadata is not null and contains name and namespace fields in the correct format - JsonObject metadata = propertyJson.get("metadata").getAsJsonObject(); - if (metadata.size() > 0){ - JsonElement name = metadata.get("name"); - JsonElement namespace = metadata.get("namespace"); - // Use the ResourceNameValidator to check that the name is correctly formatted and not null - try { - nameValidator.assertPropertyNameCharPatternIsValid(name.getAsString()); - } catch (InternalServletException e){ - // All ResourceNameValidator error should be added to the list of reasons why the property action has failed - validationErrors.add(e.getMessage()); - } - // Use the ResourceNameValidator to check that the namesapce is correctly formatted and not null - try { - nameValidator.assertNamespaceCharPatternIsValid(namespace.getAsString()); - } catch (InternalServletException e){ - // All ResourceNameValidator error should be added to the list of reasons why the property action has failed - validationErrors.add(e.getMessage()); - } - - } else { - String message = "The 'metadata' field cannot be empty. The fields 'name' and 'namespace' are mandatory for the type GalasaProperty."; - ServletError error = new ServletError(GAL5024_INVALID_GALASAPROPERTY, message); - validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); - } - - //Check that data is not null and contains the value field - JsonObject data = propertyJson.get("data").getAsJsonObject(); - if (data.size() > 0){ - if (data.has("value")){ - String value = data.get("value").getAsString(); - if (value == null || value.isBlank()) { - String message = "The 'value' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty."; - ServletError error = new ServletError(GAL5024_INVALID_GALASAPROPERTY, message); - validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); - } - } - } else { - String message = "The 'data' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty."; - ServletError error = new ServletError(GAL5024_INVALID_GALASAPROPERTY, message); - validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); - } + String kindStr = resource.get("kind").getAsString(); - } else { - // Caused by bad Key Names in the JSON object i.e. apiversion instead of apiVersion - ServletError error = new ServletError(GAL5400_BAD_REQUEST,propertyJson.toString()); - validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); - } - errors.addAll(validationErrors); - return validationErrors.size() ==0; - } - - protected void processGalasaProperty(JsonObject resource, String action) throws InternalServletException{ - try { - if (checkGalasaPropertyJsonStructure(resource)){ - String apiversion = resource.get("apiVersion").getAsString(); - String expectedApiVersion = GalasaProperty.DEFAULTAPIVERSION; - if (apiversion.equals(expectedApiVersion)) { - GalasaProperty galasaProperty = gson.fromJson(resource, GalasaProperty.class); - CPSNamespace namespace = cps.getNamespace(galasaProperty.getNamespace()); - - //getPropertyFromStore() will only return null if the property is in a hidden namespace - CPSProperty property = namespace.getPropertyFromStore(galasaProperty.getName()); - - if (action.equals("delete")) { - property.deletePropertyFromStore(); - } else { - /* - * The logic below is used to determine if the exclusive Not Or condition in property.setPropertyToStore - * (i.e. "the property exists" must equal to "is this an update action") will action the request or error - * - * Logic Table to Determine actions - * If the action is equal to "update" (force update) the updateProperty is set to true (update property, - * will error if the property does not exist in CPS) - * If the action is either "update" or "apply" and the property exists in CPS the updateProperty is set to true (update property) - * If the action is equal to "apply" and the property does not exist in CPS the updateProperty is set to false (create property) - * If the action is equal to "create" (force create) the updateProperty is set to false (create property, will error if the property exists in CPS) - */ - boolean updateProperty = false; - if ((updateActions.contains(action) && property.existsInStore()) || action.equals("update")){ - updateProperty = true; - } - property.setPropertyToStore(galasaProperty, updateProperty); - } - } else { - ServletError error = new ServletError(GAL5027_UNSUPPORTED_API_VERSION, apiversion, expectedApiVersion); + GalasaResourceType kind = GalasaResourceType.getFromString(kindStr); + if (kind == null) { + ServletError error = new ServletError(GAL5026_UNSUPPORTED_RESOURCE_TYPE); throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); } + + errors.addAll(resourceProcessors.get(kind).processResource(resource, action)); + + } catch (InternalServletException s) { + errors.add(s.getMessage()); } - } catch (ConfigurationPropertyStoreException e){ - ServletError error = new ServletError(GAL5000_GENERIC_API_ERROR, e.getMessage()); - throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } } } diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessorTest.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessorTest.java new file mode 100644 index 000000000..363d33989 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaPropertyProcessorTest.java @@ -0,0 +1,386 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.resources.processors; + +import static org.assertj.core.api.Assertions.*; +import static dev.galasa.framework.api.common.resources.ResourceAction.*; + +import java.util.List; + +import org.junit.Test; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import dev.galasa.framework.api.common.resources.CPSFacade; +import dev.galasa.framework.api.resources.ResourcesServletTest; +import dev.galasa.framework.api.resources.mocks.MockResourcesServlet; + +public class GalasaPropertyProcessorTest extends ResourcesServletTest { + + @Test + public void testProcessGalasaPropertyValidPropertyReturnsOK() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = "property.name"; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + checkPropertyInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyPropertyWithNewNamespaceReturnsOK() throws Exception { + //Given... + String namespace = "newnamespace"; + String propertyname = "property.name"; + String value = "myvalue"; + setServlet("framework"); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + checkPropertyInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyInvalidPropertyNameReturnsError() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = "property1!"; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5043E: Invalid property name. Property name 'property1!' much have at least two parts separated by a . (dot)."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyPropertyNameWithTrailingDotReturnsError() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = "property.name."; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5044E: Invalid property name. Property name 'property.name.' must not end with a . (dot) separator."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyPropertyNameWithLeadingDotReturnsError() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = ".property.name"; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5041E: Invalid property name. '.property.name' must not start with the '.' character. Allowable first characters are a-z or A-Z."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyBadPropertyNameReturnsError() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = "property"; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5043E: Invalid property name. Property name 'property' much have at least two parts separated by a . (dot)."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyMissingPropertyNameReturnsError() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = ""; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5040E: Invalid property name. Property name is missing or empty."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyMissingPropertyNamespaceReturnsError() throws Exception { + //Given... + String namespace = ""; + String propertyname = "property.name"; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5031E: Invalid namespace. Namespace is empty."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyBadNamespaceReturnsError() throws Exception { + //Given... + String namespace = "namespace@"; + String propertyname = "property.name"; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5033E: Invalid namespace name. 'namespace@' must not contain the '@' character. Allowable characters after the first character are a-z, A-Z, 0-9."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyNamespaceWithTrailingDotReturnsError() throws Exception { + //Given... + String namespace = "namespace."; + String propertyname = "property.name"; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5033E: Invalid namespace name. 'namespace.' must not contain the '.' character. Allowable characters after the first character are a-z, A-Z, 0-9."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyNamespaceWithLeadingDotReturnsError() throws Exception { + //Given... + String namespace = ".namespace"; + String propertyname = "property.name"; + String value = "myvalue"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5032E: Invalid namespace name. '.namespace' must not start with the '.' character. Allowable first characters are a-z or A-Z."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyMissingPropertyValueReturnsError() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = "property.name"; + String value = ""; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.get(0)).contains("GAL5024E: Error occurred because the Galasa Property is invalid.", + "The 'value' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyEmptyFieldsReturnsError() throws Exception { + //Given... + String namespace = ""; + String propertyname = ""; + String value = ""; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(3); + assertThat(errors.get(0)).contains("GAL5040E: Invalid property name. Property name is missing or empty."); + assertThat(errors.get(1)).contains("GAL5031E: Invalid namespace. Namespace is empty."); + assertThat(errors.get(2)).contains("GAL5024E: Error occurred because the Galasa Property is invalid. 'The 'value' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty.'"); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyNoMetadataOrDataReturnsError() throws Exception { + //Given... + String namespace = ""; + String propertyname = ""; + String value = ""; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + String jsonString = "{\"apiVersion\": \"galasa-dev/v1alpha1\",\n\"kind\": \"GalasaProperty\",\"metadata\": {},\"data\": {}}"; + JsonObject propertyJson = JsonParser.parseString(jsonString).getAsJsonObject(); + + //When... + List errors = propertyProcessor.processResource(propertyJson, APPLY); + + //Then... + assertThat(errors).isNotNull(); + assertThat(errors.size()).isEqualTo(2); + assertThat(errors.get(0)).contains("GAL5024E: Error occurred because the Galasa Property is invalid.", + "The 'metadata' field cannot be empty. The fields 'name' and 'namespace' are mandatory for the type GalasaProperty."); + assertThat(errors.get(1)).contains("GAL5024E: Error occurred because the Galasa Property is invalid.", + "The 'data' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyMissingApiVersionReturnsError() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = "property.name"; + String value = "value"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, ""); + + //When... + Throwable thrown = catchThrowable(() -> { + propertyProcessor.processResource(propertyJson, APPLY); + }); + + //Then... + assertThat(thrown).isNotNull(); + assertThat(thrown.getMessage()).contains("GAL5027E: Error occurred. The field 'apiVersion' in the request body is invalid."); + checkPropertyNotInNamespace(namespace,propertyname,value); + } + + @Test + public void testProcessGalasaPropertyBadJsonReturnsError() throws Exception { + //Given... + String namespace = "framework"; + String propertyname = "property.name"; + String value = "value"; + setServlet(namespace); + MockResourcesServlet servlet = getServlet(); + CPSFacade cps = new CPSFacade(servlet.getFramework()); + GalasaPropertyProcessor propertyProcessor = new GalasaPropertyProcessor(cps); + String jsonString = "{\"apiVersion\":\"galasa-dev/v1alpha1\","+namespace+"."+propertyname+":"+value+"}"; + JsonObject propertyJson = JsonParser.parseString(jsonString).getAsJsonObject(); + + //When... + Throwable thrown = catchThrowable(() -> { + propertyProcessor.processResource(propertyJson, APPLY); + }); + + //Then... + assertThat(thrown).isNotNull(); + checkErrorStructure( + thrown.getMessage(), + 5069, + "GAL5069E", + "Invalid request body provided. The following mandatory fields are missing", + "[metadata, data]" + ); + checkPropertyNotInNamespace(namespace,propertyname,value); + } +} diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java new file mode 100644 index 000000000..8f1a16c12 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/processors/GalasaSecretProcessorTest.java @@ -0,0 +1,609 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.resources.processors; + +import static org.assertj.core.api.Assertions.*; +import static dev.galasa.framework.api.common.resources.ResourceAction.*; + +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Base64.Encoder; + +import org.junit.Test; + +import com.google.gson.JsonObject; + +import dev.galasa.ICredentials; +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.mocks.MockCredentialsService; +import dev.galasa.framework.api.resources.ResourcesServletTest; +import dev.galasa.framework.spi.creds.CredentialsToken; +import dev.galasa.framework.spi.creds.CredentialsUsername; +import dev.galasa.framework.spi.creds.CredentialsUsernamePassword; +import dev.galasa.framework.spi.creds.CredentialsUsernameToken; + +public class GalasaSecretProcessorTest extends ResourcesServletTest { + + private JsonObject generateSecretJson(String secretName, String type, String encoding, String username, String password) { + return generateSecretJson(secretName, type, encoding, username, password, null); + } + + private JsonObject generateSecretJson( + String secretName, + String type, + String encoding, + String username, + String password, + String token + ) { + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("apiVersion", "galasa-dev/v1alpha1"); + secretJson.addProperty("kind", "GalasaSecret"); + + JsonObject secretMetadata = new JsonObject(); + secretMetadata.addProperty("name", secretName); + secretMetadata.addProperty("type", type); + + if (encoding != null) { + secretMetadata.addProperty("encoding", encoding); + } + + JsonObject secretData = new JsonObject(); + if (username != null) { + secretData.addProperty("username", username); + } + + if (password != null) { + secretData.addProperty("password", password); + } + + if (token != null) { + secretData.addProperty("token", token); + } + + secretJson.add("metadata", secretMetadata); + secretJson.add("data", secretData); + + // Expecting a JSON structure in the form: + // { + // "apiVersion": "galasa-dev/v1alpha1", + // "kind": "GalasaSecret", + // "metadata": { + // "name": "SECRET1", + // "type": "Username", + // "encoding": "base64" + // }, + // "data": { + // "username": "a-username" + // } + // } + return secretJson; + } + + @Test + public void testApplySecretWithMissingNameReturnsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = "a-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // Remove the secret's name from the JSON payload to force an error + secretJson.get("metadata").getAsJsonObject().remove("name"); + + // When... + List errors = secretProcessor.processResource(secretJson, APPLY); + + // Then... + assertThat(errors).hasSize(1); + checkErrorStructure(errors.get(0), 5070, "GAL5070E", + "Invalid GalasaSecret provided. One or more of the following mandatory fields are missing from the 'metadata' field:", + "[name, type]"); + } + + @Test + public void testApplySecretWithMissingSecretTypeReturnsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = "a-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // Remove the secret's type from the JSON payload to force an error + secretJson.get("metadata").getAsJsonObject().remove("type"); + + // When... + List errors = secretProcessor.processResource(secretJson, APPLY); + + // Then... + assertThat(errors).hasSize(1); + checkErrorStructure(errors.get(0), 5070, "GAL5070E", + "Invalid GalasaSecret provided. One or more of the following mandatory fields are missing from the 'metadata' field:", + "[name, type]"); + } + + @Test + public void testApplySecretWithMissingDataThrowsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = "a-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // Remove the data from the JSON payload to force an error + secretJson.remove("data"); + + // When... + InternalServletException thrown = catchThrowableOfType(() -> { + secretProcessor.processResource(secretJson, APPLY); + }, InternalServletException.class); + + // Then... + assertThat(thrown).isNotNull(); + checkErrorStructure(thrown.getMessage(), 5069, "GAL5069E", + "Invalid request body provided. The following mandatory fields are missing from the request body", + "[data]"); + } + + @Test + public void testApplySecretWithMissingMetadataThrowsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = "a-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // Remove the metadata from the JSON payload to force an error + secretJson.remove("metadata"); + + // When... + InternalServletException thrown = catchThrowableOfType(() -> { + secretProcessor.processResource(secretJson, APPLY); + }, InternalServletException.class); + + // Then... + assertThat(thrown).isNotNull(); + checkErrorStructure(thrown.getMessage(), 5069, "GAL5069E", + "Invalid request body provided. The following mandatory fields are missing from the request body", + "[metadata]"); + } + + @Test + public void testApplySecretWithMissingApiVersionThrowsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = "a-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // Remove the apiVersion from the JSON payload to force an error + secretJson.remove("apiVersion"); + + // When... + InternalServletException thrown = catchThrowableOfType(() -> { + secretProcessor.processResource(secretJson, APPLY); + }, InternalServletException.class); + + // Then... + assertThat(thrown).isNotNull(); + checkErrorStructure(thrown.getMessage(), 5069, "GAL5069E", + "Invalid request body provided. The following mandatory fields are missing from the request body", + "[apiVersion]"); + } + + @Test + public void testApplySecretWithMissingUsernamePasswordFieldsReturnsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = null; + String password = null; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // When... + List errors = secretProcessor.processResource(secretJson, APPLY); + + // Then... + assertThat(errors).hasSize(1); + checkErrorStructure(errors.get(0), 5072, + "The 'UsernamePassword' type was provided but the following fields are missing from the 'data' field:", + "[username, password]"); + } + + @Test + public void testApplySecretWithUnsupportedEncodingReturnsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = "UNKNOWN!!!"; + String username = "a-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // When... + List errors = secretProcessor.processResource(secretJson, APPLY); + + // Then... + assertThat(errors).hasSize(1); + checkErrorStructure(errors.get(0), 5073, + "GAL5073E: Unsupported data encoding scheme provided"); + } + + @Test + public void testApplySecretWithUnknownSecretTypeReturnsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UNKNOWN TYPE!"; + String encoding = null; + String username = "a-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // When... + List errors = secretProcessor.processResource(secretJson, APPLY); + + // Then... + assertThat(errors).hasSize(1); + checkErrorStructure(errors.get(0), 5074, + "GAL5074E: Unknown GalasaSecret type provided"); + } + + @Test + public void testApplySecretWithNoNameAndUnknownSecretTypeAndUnknownEncodingReturnsMultipleErrors() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UNKNOWN TYPE!"; + String encoding = "UNKNOWN ENCODING!"; + String username = "a-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // Remove the secret's type from the JSON payload to force an error + secretJson.get("metadata").getAsJsonObject().remove("name"); + + // When... + List errors = secretProcessor.processResource(secretJson, APPLY); + + // Then... + assertThat(errors).hasSize(3); + checkErrorStructure(errors.get(0), 5070, + "GAL5070E: Invalid GalasaSecret provided", + "One or more of the following mandatory fields are missing from the 'metadata' field: [name, type]."); + checkErrorStructure(errors.get(1), 5074, + "GAL5074E: Unknown GalasaSecret type provided"); + checkErrorStructure(errors.get(2), 5073, + "GAL5073E: Unsupported data encoding scheme provided"); + } + + @Test + public void testCreateUsernamePasswordSecretSetsCredentialsOk() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = "my-username"; + String password = "a-password"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); + + // When... + List errors = secretProcessor.processResource(secretJson, CREATE); + + // Then... + assertThat(errors).isEmpty(); + + CredentialsUsernamePassword credentials = (CredentialsUsernamePassword) mockCreds.getCredentials(secretName); + assertThat(credentials).isNotNull(); + assertThat(credentials.getUsername()).isEqualTo(username); + assertThat(credentials.getPassword()).isEqualTo(password); + } + + @Test + public void testCreateEncodedUsernamePasswordSecretSetsCredentialsOk() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernamePassword"; + + // Base64-encode the credentials + Encoder encoder = Base64.getEncoder(); + String encoding = "base64"; + String username = "abc"; + String password = "123"; + String encodedUsername = encoder.encodeToString(username.getBytes()); + String encodedPassword = encoder.encodeToString(password.getBytes()); + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, encodedPassword); + + // When... + List errors = secretProcessor.processResource(secretJson, CREATE); + + // Then... + assertThat(errors).isEmpty(); + + // The credentials should have been decoded, so that they can be encrypted by the creds store + CredentialsUsernamePassword credentials = (CredentialsUsernamePassword) mockCreds.getCredentials(secretName); + assertThat(credentials).isNotNull(); + assertThat(credentials.getUsername()).isEqualTo(username); + assertThat(credentials.getPassword()).isEqualTo(password); + } + + @Test + public void testCreateEncodedTokenSecretSetsCredentialsOk() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "Token"; + + // Base64-encode the credentials + Encoder encoder = Base64.getEncoder(); + String encoding = "base64"; + String token = "my-token"; + String encodedToken = encoder.encodeToString(token.getBytes()); + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, encodedToken); + + // When... + List errors = secretProcessor.processResource(secretJson, CREATE); + + // Then... + assertThat(errors).isEmpty(); + + // The credentials should have been decoded, so that they can be encrypted by the creds store + CredentialsToken credentials = (CredentialsToken) mockCreds.getCredentials(secretName); + assertThat(credentials).isNotNull(); + assertThat(credentials.getToken()).isEqualTo(token.getBytes()); + } + + @Test + public void testCreateEncodedUsernameTokenSecretSetsCredentialsOk() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "UsernameToken"; + + // Base64-encode the credentials + Encoder encoder = Base64.getEncoder(); + String encoding = "base64"; + String username = "my-username"; + String token = "my-token"; + String encodedUsername = encoder.encodeToString(username.getBytes()); + String encodedToken = encoder.encodeToString(token.getBytes()); + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, null, encodedToken); + + // When... + List errors = secretProcessor.processResource(secretJson, CREATE); + + // Then... + assertThat(errors).isEmpty(); + + // The credentials should have been decoded, so that they can be encrypted by the creds store + CredentialsUsernameToken credentials = (CredentialsUsernameToken) mockCreds.getCredentials(secretName); + assertThat(credentials).isNotNull(); + assertThat(credentials.getUsername()).isEqualTo(username); + assertThat(credentials.getToken()).isEqualTo(token.getBytes()); + } + + @Test + public void testCreateEncodedUsernameSecretSetsCredentialsOk() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "Username"; + + // Base64-encode the credentials + Encoder encoder = Base64.getEncoder(); + String encoding = "base64"; + String username = "my-username"; + String encodedUsername = encoder.encodeToString(username.getBytes()); + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, null, null); + + // When... + List errors = secretProcessor.processResource(secretJson, CREATE); + + // Then... + assertThat(errors).isEmpty(); + + // The credentials should have been decoded, so that they can be encrypted by the creds store + CredentialsUsername credentials = (CredentialsUsername) mockCreds.getCredentials(secretName); + assertThat(credentials).isNotNull(); + assertThat(credentials.getUsername()).isEqualTo(username); + } + + @Test + public void testDeleteSecretDeletesCredentialsOk() throws Exception { + // Given... + String secretName = "ABC"; + String username = "my-username"; + Map existingCreds = new HashMap<>(); + existingCreds.put(secretName, new CredentialsUsername(username)); + existingCreds.put("another-secret", new CredentialsUsername("another-username")); + + MockCredentialsService mockCreds = new MockCredentialsService(existingCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String type = "Username"; + String encoding = null; + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, null, null); + + // When... + assertThat(mockCreds.getAllCredentials()).hasSize(2); + assertThat(mockCreds.getCredentials(secretName)).isNotNull(); + List errors = secretProcessor.processResource(secretJson, DELETE); + + // Then... + assertThat(errors).isEmpty(); + assertThat(mockCreds.getAllCredentials()).hasSize(1); + assertThat(mockCreds.getCredentials(secretName)).isNull(); + } + + @Test + public void testDeleteSecretDoesNotInsistOnData() throws Exception { + // Given... + String secretName = "ABC"; + String username = "my-username"; + Map existingCreds = new HashMap<>(); + existingCreds.put(secretName, new CredentialsUsername(username)); + existingCreds.put("another-secret", new CredentialsUsername("another-username")); + + MockCredentialsService mockCreds = new MockCredentialsService(existingCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String type = "Username"; + String encoding = null; + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, null, null); + + // The data section is not required for deleting secrets + secretJson.remove("data"); + + // When... + assertThat(mockCreds.getAllCredentials()).hasSize(2); + assertThat(mockCreds.getCredentials(secretName)).isNotNull(); + List errors = secretProcessor.processResource(secretJson, DELETE); + + // Then... + assertThat(errors).isEmpty(); + assertThat(mockCreds.getAllCredentials()).hasSize(1); + assertThat(mockCreds.getCredentials(secretName)).isNull(); + } + + @Test + public void testCreateSecretThatAlreadyExistsThrowsError() throws Exception { + // Given... + Map credsMap = new HashMap<>(); + credsMap.put("ABC", new CredentialsUsername("my-username")); + + MockCredentialsService mockCreds = new MockCredentialsService(credsMap); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "Username"; + String encoding = null; + String username = "another-username"; + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, null, null); + + // When... + InternalServletException thrown = catchThrowableOfType(() -> { + secretProcessor.processResource(secretJson, CREATE); + }, InternalServletException.class); + + // Then... + assertThat(thrown).isNotNull(); + checkErrorStructure(thrown.getMessage(), 5075, "GAL5075E", + "A secret with the provided ID already exists."); + } + + @Test + public void testUpdateSecretThatDoesNotExistThrowsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "Token"; + String encoding = null; + String token = "another-token"; + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token); + + // When... + InternalServletException thrown = catchThrowableOfType(() -> { + secretProcessor.processResource(secretJson, UPDATE); + }, InternalServletException.class); + + // Then... + assertThat(thrown).isNotNull(); + checkErrorStructure(thrown.getMessage(), 5076, "GAL5076E", + "A secret with the provided ID does not exist"); + } + + @Test + public void testApplySecretWithFailingCredsServiceThrowsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + mockCreds.setThrowError(true); + + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "Token"; + String encoding = null; + String token = "a-token"; + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token); + + // When... + InternalServletException thrown = catchThrowableOfType(() -> { + secretProcessor.processResource(secretJson, APPLY); + }, InternalServletException.class); + + // Then... + assertThat(thrown).isNotNull(); + checkErrorStructure(thrown.getMessage(), 5079, "GAL5079E", + "Failed to retrieve the secret with the given ID from the credentials store"); + } + + @Test + public void testDeleteSecretWithFailingCredsServiceThrowsError() throws Exception { + // Given... + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + mockCreds.setThrowError(true); + + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + String secretName = "ABC"; + String type = "Token"; + String encoding = null; + String token = "a-token"; + + JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token); + + // When... + InternalServletException thrown = catchThrowableOfType(() -> { + secretProcessor.processResource(secretJson, DELETE); + }, InternalServletException.class); + + // Then... + assertThat(thrown).isNotNull(); + checkErrorStructure(thrown.getMessage(), 5078, "GAL5078E", + "Failed to delete a secret with the given ID from the credentials store"); + } +} diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/routes/TestResourcesRoute.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/routes/TestResourcesRoute.java index 3e2ddf261..81994a37e 100644 --- a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/routes/TestResourcesRoute.java +++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/routes/TestResourcesRoute.java @@ -6,6 +6,7 @@ package dev.galasa.framework.api.resources.routes; import static org.assertj.core.api.Assertions.*; +import static dev.galasa.framework.api.common.resources.ResourceAction.*; import java.util.HashMap; import java.util.ArrayList; @@ -115,376 +116,6 @@ public void TestPathRegexMultipleForwardSlashPathReturnsFalse(){ /* * Internal Functions */ - - @Test - public void TestProcessGalasaPropertyValidPropertyReturnsOK() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = "property.name"; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - - //Then... - checkPropertyInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyPropertyWithNewNamespaceReturnsOK() throws Exception{ - //Given... - String namespace = "newnamespace"; - String propertyname = "property.name"; - String value = "myvalue"; - setServlet("framework"); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - - //Then... - checkPropertyInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyInvalidPropertyNameReturnsError() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = "property1!"; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5043E: Invalid property name. Property name 'property1!' much have at least two parts separated by a . (dot)."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyPropertyNameWithTrailingDotReturnsError() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = "property.name."; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5044E: Invalid property name. Property name 'property.name.' must not end with a . (dot) separator."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyPropertyNameWithLeadingDotReturnsError() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = ".property.name"; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5041E: Invalid property name. '.property.name' must not start with the '.' character. Allowable first characters are a-z or A-Z."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyBadPropertyNameReturnsError() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = "property"; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5043E: Invalid property name. Property name 'property' much have at least two parts separated by a . (dot)."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyMissingPropertyNameReturnsError() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = ""; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5040E: Invalid property name. Property name is missing or empty."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyMissingPropertyNamespaceReturnsError() throws Exception{ - //Given... - String namespace = ""; - String propertyname = "property.name"; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5031E: Invalid namespace. Namespace is empty."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyBadNamespaceReturnsError() throws Exception{ - //Given... - String namespace = "namespace@"; - String propertyname = "property.name"; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5033E: Invalid namespace name. 'namespace@' must not contain the '@' character. Allowable characters after the first character are a-z, A-Z, 0-9."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyNamespaceWithTrailingDotReturnsError() throws Exception{ - //Given... - String namespace = "namespace."; - String propertyname = "property.name"; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5033E: Invalid namespace name. 'namespace.' must not contain the '.' character. Allowable characters after the first character are a-z, A-Z, 0-9."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyNamespaceWithLeadingDotReturnsError() throws Exception{ - //Given... - String namespace = ".namespace"; - String propertyname = "property.name"; - String value = "myvalue"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5032E: Invalid namespace name. '.namespace' must not start with the '.' character. Allowable first characters are a-z or A-Z."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyMissingPropertyValueReturnsError() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = "property.name"; - String value = ""; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5024E: Error occurred because the Galasa Property is invalid.", - "The 'value' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyEmptyFieldsReturnsError() throws Exception{ - //Given... - String namespace = ""; - String propertyname = ""; - String value = ""; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(3); - assertThat(errors.get(0)).contains("GAL5040E: Invalid property name. Property name is missing or empty."); - assertThat(errors.get(1)).contains("GAL5031E: Invalid namespace. Namespace is empty."); - assertThat(errors.get(2)).contains("GAL5024E: Error occurred because the Galasa Property is invalid. 'The 'value' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty.'"); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyNoMetadataOrDataReturnsError() throws Exception{ - //Given... - String namespace = ""; - String propertyname = ""; - String value = ""; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - String jsonString = "{\"apiVersion\": \"galasa-dev/v1alpha1\",\n\"kind\": \"GalasaProperty\",\"metadata\": {},\"data\": {}}"; - JsonObject propertyJson = JsonParser.parseString(jsonString).getAsJsonObject(); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(2); - assertThat(errors.get(0)).contains("GAL5024E: Error occurred because the Galasa Property is invalid.", - "The 'metadata' field cannot be empty. The fields 'name' and 'namespace' are mandatory for the type GalasaProperty."); - assertThat(errors.get(1)).contains("GAL5024E: Error occurred because the Galasa Property is invalid.", - "The 'data' field cannot be empty. The field 'value' is mandatory for the type GalasaProperty."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyMissingApiVersionReturnsError() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = "property.name"; - String value = "value"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, ""); - - //When... - Throwable thrown = catchThrowable(() -> { - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - }); - - //Then... - assertThat(thrown).isNotNull(); - assertThat(thrown.getMessage()).contains("GAL5027E: Error occurred. The field apiVersion in the request body is invalid. The value '' is not a supported version." + - " Currently the ecosystem accepts the 'galasa-dev/v1alpha1' api version. This could indicate a mis-match between client and server levels." + - " Please check with your Ecosystem administrator the level. You may have to upgrade/downgrade your client program."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } - - @Test - public void TestProcessGalasaPropertyBadJsonReturnsError() throws Exception{ - //Given... - String namespace = "framework"; - String propertyname = "property.name"; - String value = "value"; - setServlet(namespace); - MockResourcesServlet servlet = getServlet(); - CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); - String jsonString = "{\"apiVersion\":\"galasa-dev/v1alpha1\","+namespace+"."+propertyname+":"+value+"}"; - JsonObject propertyJson = JsonParser.parseString(jsonString).getAsJsonObject(); - - //When... - resourcesRoute.processGalasaProperty(propertyJson, "apply"); - List errors = resourcesRoute.errors; - - //Then... - assertThat(errors).isNotNull(); - assertThat(errors.size()).isEqualTo(1); - assertThat(errors.get(0)).contains("GAL5400E: Error occurred when trying to execute request ",". Please check your request parameters or report the problem to your Galasa Ecosystem owner."); - checkPropertyNotInNamespace(namespace,propertyname,value); - } @Test public void TestProcessDataArrayBadJsonArrayReturnsError() throws Exception{ @@ -495,12 +126,12 @@ public void TestProcessDataArrayBadJsonArrayReturnsError() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString = "[{},{},{}]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "apply"); + resourcesRoute.processDataArray(propertyJson, APPLY); List errors = resourcesRoute.errors; //Then... @@ -518,17 +149,17 @@ public void TestProcessDataArrayBadJsonReturnsError() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString = "[{\"kind\":\"GalasaProperty\",\"apiVersion\":\"galasa-dev/v1alpha1\","+namespace+"."+propertyname+":"+value+"}]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "apply"); + resourcesRoute.processDataArray(propertyJson, APPLY); List errors = resourcesRoute.errors; //Then... assertThat(errors.size()).isEqualTo(1); - checkErrorListContainsError(errors,"GAL5400E: Error occurred when trying to execute request "); + checkErrorListContainsError(errors,"GAL5069E: Invalid request body provided. The following mandatory fields are missing from the request body"); checkPropertyNotInNamespace(namespace,propertyname,value); } @@ -541,19 +172,17 @@ public void TestProcessDataArrayBadKindReturnsError() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString = "[{\"kind\":\"GalasaProperly\",\"apiVersion\":\"v1alpha1\","+namespace+"."+propertyname+":"+value+"}]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "apply"); + resourcesRoute.processDataArray(propertyJson, APPLY); List errors = resourcesRoute.errors; //Then... assertThat(errors.size()).isEqualTo(1); - checkErrorListContainsError(errors,"GAL5026E: Error occurred. The field kind in the request body is invalid. The value 'GalasaProperly' is not supported." + - " This could indicate a mis-match between client and server levels. Please check with your Ecosystem administrator the level." + - " You may have to upgrade/downgrade your client program."); + checkErrorListContainsError(errors,"GAL5026E: Error occurred. The field 'kind' in the request body is invalid"); checkPropertyNotInNamespace(namespace,propertyname,value); } @@ -564,12 +193,12 @@ public void TestProcessDataArrayNullJsonObjectReturnsError() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString = "[null]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "apply"); + resourcesRoute.processDataArray(propertyJson, APPLY); List errors = resourcesRoute.errors; //Then... @@ -586,11 +215,11 @@ public void TestProcessDataArrayCorrectJSONReturnsOK() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); JsonArray propertyJson = generatePropertyArrayJson(namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... - resourcesRoute.processDataArray(propertyJson, "apply"); + resourcesRoute.processDataArray(propertyJson, APPLY); List errors = resourcesRoute.errors; //Then... @@ -607,22 +236,20 @@ public void TestProcessDataArrayThreeBadJsonReturnsErrors() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString = "[null, {\"kind\":\"GalasaProperty\",\"apiVersion\":\"galasa-dev/v1alpha1\","+namespace+"."+propertyname+":"+value+"},"+ "{\"kind\":\"GalasaProperly\",\"apiVersion\":\"v1alpha1\","+namespace+"."+propertyname+":"+value+"},{}]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "apply"); + resourcesRoute.processDataArray(propertyJson, APPLY); List errors = resourcesRoute.errors; //Then... assertThat(errors.size()).isEqualTo(4); checkErrorListContainsError(errors,"GAL5067E: Error occurred. A 'NULL' value is not a valid resource. Please check the request format, or check with your Ecosystem administrator."); - checkErrorListContainsError(errors,"GAL5400E: Error occurred when trying to execute request "); - checkErrorListContainsError(errors,"GAL5026E: Error occurred. The field kind in the request body is invalid. The value 'GalasaProperly' is not supported." + - " This could indicate a mis-match between client and server levels. Please check with your Ecosystem administrator the level." + - " You may have to upgrade/downgrade your client program."); + checkErrorListContainsError(errors,"GAL5069E: Invalid request body provided. The following mandatory fields are missing from the request body"); + checkErrorListContainsError(errors,"GAL5026E: Error occurred. The field 'kind' in the request body is invalid."); checkErrorListContainsError(errors,"GAL5068E: Error occurred. The JSON element for a resource can not be empty. Please check the request format, or check with your Ecosystem administrator."); checkPropertyNotInNamespace(namespace,propertyname,value); } @@ -638,13 +265,13 @@ public void TestProcessDataArrayCreateWithOneExistingRecordJSONReturnsOneError() setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString ="["+ generatePropertyJson(namespace,propertyname,value,"galasa-dev/v1alpha1"); jsonString = jsonString+","+ generatePropertyJson(namespace,propertyNameTwo,valueTwo,"galasa-dev/v1alpha1") +"]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "create"); + resourcesRoute.processDataArray(propertyJson, CREATE); List errors = resourcesRoute.errors; //Then... @@ -666,13 +293,13 @@ public void TestProcessDataArrayCreateWithTwoExistingRecordsJSONReturnsTwoErrors setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString ="["+ generatePropertyJson(namespace,propertyname,value,"galasa-dev/v1alpha1"); jsonString = jsonString+","+ generatePropertyJson(namespace,propertyNameTwo,valueTwo,"galasa-dev/v1alpha1") +"]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "create"); + resourcesRoute.processDataArray(propertyJson, CREATE); List errors = resourcesRoute.errors; //Then... @@ -696,13 +323,13 @@ public void TestProcessDataArrayUpdateWithOneNewRecordJSONReturnsOneError() thro setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString ="["+ generatePropertyJson(namespace,propertyname,value,"galasa-dev/v1alpha1"); jsonString = jsonString+","+ generatePropertyJson(namespace,propertyNameTwo,valueTwo,"galasa-dev/v1alpha1") +"]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "update"); + resourcesRoute.processDataArray(propertyJson, UPDATE); List errors = resourcesRoute.errors; //Then... @@ -723,13 +350,13 @@ public void TestProcessDataArrayUpdateWithTwoNewRecordsJSONReturnsTwoError() thr setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); String jsonString ="["+ generatePropertyJson(namespace,propertyname,value,"galasa-dev/v1alpha1"); jsonString = jsonString+","+ generatePropertyJson(namespace,propertyNameTwo,valueTwo,"galasa-dev/v1alpha1") +"]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, "update"); + resourcesRoute.processDataArray(propertyJson, UPDATE); List errors = resourcesRoute.errors; //Then... @@ -754,7 +381,7 @@ public void TestProcessRequestApplyActionReturnsOK() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); JsonObject requestJson = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... @@ -776,7 +403,7 @@ public void TestProcessRequestCreateActionReturnsOK() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... @@ -798,7 +425,7 @@ public void TestProcessRequestUpdateActionReturnsOK() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... @@ -820,7 +447,7 @@ public void TestProcessRequestBadActionReturnsError() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... @@ -833,9 +460,7 @@ public void TestProcessRequestBadActionReturnsError() throws Exception{ String message = thrown.getMessage(); checkErrorStructure(message, 5025, - "GAL5025E: Error occurred. The field action in the request body is invalid. The action value'badaction' supplied is not supported." + - " Supported actions are: create, apply and update. This could indicate a mis-match between client and server levels." + - " Please check with your Ecosystem administrator the level. You may have to upgrade/downgrade your client program."); + "GAL5025E: Error occurred. The field 'action' in the request body is invalid."); checkPropertyNotInNamespace(namespace,propertyname,value); } @@ -1348,7 +973,7 @@ public void TestGetErrorsAsJsonReturnsJsonString() throws Exception{ setServlet("framework"); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); // When... String json = resourcesRoute.getErrorsAsJson(errors); diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/internal/creds/FileCredentialsStore.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/internal/creds/FileCredentialsStore.java index 492a65f57..88dee3aa0 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/internal/creds/FileCredentialsStore.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/internal/creds/FileCredentialsStore.java @@ -106,4 +106,13 @@ public void shutdown() throws CredentialsException { } } + @Override + public void setCredentials(String credsId, ICredentials credentials) throws CredentialsException { + // Not implemented for local credentials... + } + + @Override + public void deleteCredentials(String credsId) throws CredentialsException { + // Not implemented for local credentials... + } } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/internal/creds/FrameworkCredentialsService.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/internal/creds/FrameworkCredentialsService.java index 89d98ca4c..6ce610cc5 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/internal/creds/FrameworkCredentialsService.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/internal/creds/FrameworkCredentialsService.java @@ -95,4 +95,14 @@ public ICredentials getCredentials(@NotNull String credsId) throws CredentialsEx return creds; } + + @Override + public void setCredentials(String credentialsId, ICredentials credentials) throws CredentialsException { + credsStore.setCredentials(credentialsId, credentials); + } + + @Override + public void deleteCredentials(String credentialsId) throws CredentialsException { + credsStore.deleteCredentials(credentialsId); + } } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java index eaf0238ea..ccc96ecf1 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/Credentials.java @@ -21,6 +21,10 @@ public abstract class Credentials { private IEncryptionService encryptionService; private final SecretKeySpec key; + public Credentials() { + this.key = null; + } + public Credentials(SecretKeySpec key) throws CredentialsException { this(key, new FileSystem(), new SystemEnvironment()); } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java index fca9f3137..a5a41850f 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsToken.java @@ -5,6 +5,8 @@ */ package dev.galasa.framework.spi.creds; +import java.util.Properties; + import javax.crypto.spec.SecretKeySpec; import dev.galasa.ICredentialsToken; @@ -12,6 +14,10 @@ public class CredentialsToken extends Credentials implements ICredentialsToken { private final byte[] token; + public CredentialsToken(String plainTextToken) { + this.token = plainTextToken.getBytes(); + } + public CredentialsToken(SecretKeySpec key, String stoken) throws CredentialsException { super(key); @@ -27,4 +33,11 @@ public byte[] getToken() { return token; } + @Override + public Properties toProperties(String credentialsId) { + Properties credsProperties = new Properties(); + credsProperties.setProperty("secure.credentials." + credentialsId + ".token" , new String(this.token)); + return credsProperties; + } + } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java index 74ff1314f..9e232ffc5 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsername.java @@ -6,6 +6,7 @@ package dev.galasa.framework.spi.creds; import java.nio.charset.StandardCharsets; +import java.util.Properties; import javax.crypto.spec.SecretKeySpec; @@ -14,6 +15,10 @@ public class CredentialsUsername extends Credentials implements ICredentialsUsername { private String username; + public CredentialsUsername(String plainTextUsername) { + this.username = plainTextUsername; + } + public CredentialsUsername(SecretKeySpec key, String username) throws CredentialsException { super(key); @@ -28,4 +33,11 @@ public String getUsername() { return username; } + @Override + public Properties toProperties(String credentialsId) { + Properties credsProperties = new Properties(); + credsProperties.setProperty("secure.credentials." + credentialsId + ".username" , this.username); + return credsProperties; + } + } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java index bf922fbd9..811509ed3 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernamePassword.java @@ -6,6 +6,7 @@ package dev.galasa.framework.spi.creds; import java.nio.charset.StandardCharsets; +import java.util.Properties; import javax.crypto.spec.SecretKeySpec; @@ -15,6 +16,11 @@ public class CredentialsUsernamePassword extends Credentials implements ICredent private String username; private String password; + public CredentialsUsernamePassword(String plainTextUsername, String plainTextPassword) { + this.username = plainTextUsername; + this.password = plainTextPassword; + } + public CredentialsUsernamePassword(SecretKeySpec key, String username, String password) throws CredentialsException { super(key); @@ -38,4 +44,12 @@ public String getUsername() { public String getPassword() { return password; } + + @Override + public Properties toProperties(String credentialsId) { + Properties credsProperties = new Properties(); + credsProperties.setProperty("secure.credentials." + credentialsId + ".username" , this.username); + credsProperties.setProperty("secure.credentials." + credentialsId + ".password" , this.password); + return credsProperties; + } } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java index b4e74b837..37fb3c3cd 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/CredentialsUsernameToken.java @@ -6,6 +6,7 @@ package dev.galasa.framework.spi.creds; import java.nio.charset.StandardCharsets; +import java.util.Properties; import javax.crypto.spec.SecretKeySpec; @@ -15,6 +16,11 @@ public class CredentialsUsernameToken extends Credentials implements ICredential private String username; private byte[] token; + public CredentialsUsernameToken(String plainTextUsername, String encryptedToken) { + this.username = plainTextUsername; + this.token = encryptedToken.getBytes(); + } + public CredentialsUsernameToken(SecretKeySpec key, String username, String token) throws CredentialsException { super(key); @@ -38,4 +44,12 @@ public String getUsername() { public byte[] getToken() { return token; } + + @Override + public Properties toProperties(String credentialsId) { + Properties credsProperties = new Properties(); + credsProperties.setProperty("secure.credentials." + credentialsId + ".username" , this.username); + credsProperties.setProperty("secure.credentials." + credentialsId + ".token" , new String(this.token)); + return credsProperties; + } } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/FrameworkEncryptionService.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/FrameworkEncryptionService.java index 3e613fdb5..f7e3689e4 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/FrameworkEncryptionService.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/FrameworkEncryptionService.java @@ -21,7 +21,6 @@ import dev.galasa.framework.spi.SystemEnvironment; public class FrameworkEncryptionService implements IEncryptionService { - public static final String ENCRYPTION_KEYS_PATH_ENV = "GALASA_ENCRYPTION_KEYS_PATH"; private static final String KEY_ALGORITHM = "AES"; diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/ICredentialsService.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/ICredentialsService.java index 92efaa64d..044de9e27 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/ICredentialsService.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/ICredentialsService.java @@ -11,5 +11,25 @@ public interface ICredentialsService { + /** + * Gets the credentials with the given ID and returns them without decrypting their values + * + * @param credentialsId the ID of the credentials to retrieve from the credentials store + * @return the encrypted credentials in the credentials store, or null if no such credentials exist + * @throws CredentialsException if there was an issue accessing the credentials store + */ + // ICredentials getEncryptedCredentials(@NotNull String credentialsId) throws CredentialsException; + + /** + * Gets the credentials with the given ID and returns them after attempting to decrypt their values + * + * @param credentialsId the ID of the credentials to retrieve from the credentials store + * @return the decrypted credentials in the credentials store, or null if no such credentials exist + * @throws CredentialsException if there was an issue accessing the credentials store + */ ICredentials getCredentials(@NotNull String credentialsId) throws CredentialsException; + + void setCredentials(String credentialsId, ICredentials credentials) throws CredentialsException; + + void deleteCredentials(String credentialsId) throws CredentialsException; } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/ICredentialsStore.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/ICredentialsStore.java index 64619731c..c7a3bd96c 100644 --- a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/ICredentialsStore.java +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/ICredentialsStore.java @@ -11,5 +11,9 @@ public interface ICredentialsStore { ICredentials getCredentials(String credsId) throws CredentialsException; + void setCredentials(String credsId, ICredentials credentials) throws CredentialsException; + + void deleteCredentials(String credsId) throws CredentialsException; + void shutdown() throws CredentialsException; } diff --git a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java index e83250e8b..1abc94cbb 100644 --- a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java +++ b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java @@ -5,7 +5,14 @@ */ package dev.galasa.framework.mocks; +import java.util.Properties; + import dev.galasa.ICredentials; class MockCredentials implements ICredentials { + + @Override + public Properties toProperties(String credentialsId) { + throw new UnsupportedOperationException("Unimplemented method 'toProperties'"); + } }; \ No newline at end of file diff --git a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentialsStore.java b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentialsStore.java index 3309522b9..b0edfd22d 100644 --- a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentialsStore.java +++ b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentialsStore.java @@ -32,4 +32,13 @@ public void shutdown() throws CredentialsException { throw new UnsupportedOperationException("Unimplemented method 'shutdown'"); } + @Override + public void setCredentials(String credsId, ICredentials credentials) throws CredentialsException { + throw new UnsupportedOperationException("Unimplemented method 'setCredentials'"); + } + + @Override + public void deleteCredentials(String credsId) throws CredentialsException { + throw new UnsupportedOperationException("Unimplemented method 'deleteCredentials'"); + } } diff --git a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/spi/creds/FrameworkEncryptionServiceTest.java b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/spi/creds/FrameworkEncryptionServiceTest.java index 171e33ba3..0f532c6f7 100644 --- a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/spi/creds/FrameworkEncryptionServiceTest.java +++ b/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/spi/creds/FrameworkEncryptionServiceTest.java @@ -116,7 +116,6 @@ public void testCanLoadAndUseEncryptionKeysFromFileSystemOk() throws Exception { String mockEncryptionKeysFilePath = "/encryption-keys.yaml"; mockEnvironment.setenv(FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV, mockEncryptionKeysFilePath); - List oldDecryptionKeys = new ArrayList<>(); String encodedEncryptionkey = generateEncodedEncryptionKeyString(); String yaml = createEncryptionKeysYaml(encodedEncryptionkey, oldDecryptionKeys); @@ -143,7 +142,6 @@ public void testDecryptTextWithWrongKeyReturnsNullText() throws Exception { String mockEncryptionKeysFilePath = "/encryption-keys.yaml"; mockEnvironment.setenv(FrameworkEncryptionService.ENCRYPTION_KEYS_PATH_ENV, mockEncryptionKeysFilePath); - List oldDecryptionKeys = List.of( generateEncodedEncryptionKeyString(), generateEncodedEncryptionKeyString() diff --git a/galasa-parent/dev.galasa/src/main/java/dev/galasa/ICredentials.java b/galasa-parent/dev.galasa/src/main/java/dev/galasa/ICredentials.java index 0cfa04b7c..4369db949 100644 --- a/galasa-parent/dev.galasa/src/main/java/dev/galasa/ICredentials.java +++ b/galasa-parent/dev.galasa/src/main/java/dev/galasa/ICredentials.java @@ -5,6 +5,8 @@ */ package dev.galasa; -public interface ICredentials { +import java.util.Properties; +public interface ICredentials { + Properties toProperties(String credentialsId); } diff --git a/galasa-parent/galasa-testharness/build.gradle b/galasa-parent/galasa-testharness/build.gradle index e3e3e4b38..cb9d551ef 100644 --- a/galasa-parent/galasa-testharness/build.gradle +++ b/galasa-parent/galasa-testharness/build.gradle @@ -4,7 +4,7 @@ plugins { description = 'Galasa Testharness' -version = "0.34.0" +version = "0.38.0" dependencies { diff --git a/galasa-parent/galasa-testharness/src/main/java/dev/galasa/testharness/InMemoryCreds.java b/galasa-parent/galasa-testharness/src/main/java/dev/galasa/testharness/InMemoryCreds.java index cea6a44e7..a008ec951 100644 --- a/galasa-parent/galasa-testharness/src/main/java/dev/galasa/testharness/InMemoryCreds.java +++ b/galasa-parent/galasa-testharness/src/main/java/dev/galasa/testharness/InMemoryCreds.java @@ -22,4 +22,13 @@ public ICredentials getCredentials(@NotNull String credentialsId) throws Credent return credentials.get(credentialsId); } + @Override + public void setCredentials(String credentialsId, ICredentials credentials) throws CredentialsException { + throw new UnsupportedOperationException("Unimplemented method 'setCredentials'"); + } + + @Override + public void deleteCredentials(String credentialsId) throws CredentialsException { + throw new UnsupportedOperationException("Unimplemented method 'deleteCredentials'"); + } } diff --git a/release.yaml b/release.yaml index 863147bae..28771e4fd 100644 --- a/release.yaml +++ b/release.yaml @@ -114,7 +114,7 @@ framework: codecoverage: true - artifact: galasa-testharness - version: 0.34.0 + version: 0.38.0 obr: false mvp: false bom: false @@ -163,7 +163,7 @@ api: codecoverage: true - artifact: dev.galasa.framework.api.common - version: 0.37.0 + version: 0.38.0 obr: true mvp: false bom: false @@ -226,7 +226,7 @@ api: codecoverage: true - artifact: dev.galasa.framework.api.resources - version: 0.37.0 + version: 0.38.0 obr: true mvp: false bom: false