From fcc9e585f9d7a48ae3fffabea32cec3c485de6a3 Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:47:50 +0000 Subject: [PATCH] Add REST API endpoints for secrets (#662) * WIP: Start implementing Secrets APIs Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Add GET /secrets and PUT /secrets/{secret-name} endpoints Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Update secrets endpoints in openapi spec Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * fix: Add secretName path parameter to /secrets/{secretName} endpoints Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: PUT /secrets/{secretName} now supports changing secret types and updating existing secrets Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * fix: Throw error if unexpected fields are given to update secrets Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * review: Replaced secret ID with secret name Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Add description, lastUpdatedTime, and lastUpdatedBy to secrets Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * refactor: Separate validation from resource processors Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * fix: Retain secret description when updating a secret Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --------- Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .secrets.baseline | 42 +- .../internal/TokenPayloadValidator.java | 20 +- .../api/common/ServletErrorMessage.java | 21 +- .../common/resources/AbstractValidator.java | 46 + .../resources/GalasaResourceValidator.java | 79 ++ .../common/resources/GalasaSecretType.java | 2 +- .../api/common/resources/Secret.java | 9 +- .../api/common/resources/SecretValidator.java | 40 + .../framework/api/common/BaseServletTest.java | 7 +- .../api/common/mocks/MockFramework.java | 4 + .../common/mocks/MockHttpServletRequest.java | 4 + .../src/main/resources/openapi.yaml | 245 +++- .../api/resources/ResourcesServlet.java | 9 +- .../AbstractGalasaResourceProcessor.java | 46 +- .../processors/GalasaPropertyProcessor.java | 65 +- .../processors/GalasaSecretProcessor.java | 90 +- .../processors/IGalasaResourceProcessor.java | 3 +- .../api/resources/routes/ResourcesRoute.java | 28 +- .../validators/GalasaPropertyValidator.java | 88 ++ .../validators/GalasaSecretValidator.java | 103 ++ .../api/resources/ResourcesServletTest.java | 30 +- .../resources/mocks/MockResourcesServlet.java | 8 + .../GalasaPropertyProcessorTest.java | 48 +- .../processors/GalasaSecretProcessorTest.java | 222 +++- .../resources/routes/TestResourcesRoute.java | 72 +- .../dev.galasa.framework.api.secrets/bnd.bnd | 7 + .../build.gradle | 29 + .../settings.gradle | 1 + .../framework/api/secrets/SecretsServlet.java | 58 + .../internal/SecretRequestValidator.java | 77 ++ .../UpdateSecretRequestValidator.java | 130 ++ .../internal/routes/AbstractSecretsRoute.java | 198 +++ .../internal/routes/SecretDetailsRoute.java | 254 ++++ .../secrets/internal/routes/SecretsRoute.java | 110 ++ .../api/secrets/internal/MockCredentials.java | 54 + .../internal/SecretDetailsRouteTest.java | 1061 +++++++++++++++++ .../secrets/internal/SecretsRouteTest.java | 808 +++++++++++++ .../secrets/internal/SecretsServletTest.java | 115 ++ .../api/secrets/mocks/MockSecretsServlet.java | 30 + .../internal/creds/FileCredentialsStore.java | 7 + .../creds/FrameworkCredentialsService.java | 7 + .../spi/creds/AbstractCredentials.java | 80 ++ .../framework/spi/creds/Credentials.java | 1 - .../framework/spi/creds/CredentialsToken.java | 5 +- .../spi/creds/CredentialsUsername.java | 4 +- .../creds/CredentialsUsernamePassword.java | 6 +- .../spi/creds/CredentialsUsernameToken.java | 6 +- .../spi/creds/ICredentialsService.java | 16 +- .../spi/creds/ICredentialsStore.java | 4 + .../framework/mocks/MockCredentials.java | 18 - .../framework/mocks/MockCredentialsStore.java | 5 + .../main/java/dev/galasa/ICredentials.java | 10 + .../dev/galasa/testharness/InMemoryCreds.java | 6 + galasa-parent/settings.gradle | 1 + release.yaml | 9 + 55 files changed, 4080 insertions(+), 368 deletions(-) create mode 100644 galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/AbstractValidator.java create mode 100644 galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaResourceValidator.java create mode 100644 galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/SecretValidator.java create mode 100644 galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/validators/GalasaPropertyValidator.java create mode 100644 galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/validators/GalasaSecretValidator.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/bnd.bnd create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/build.gradle create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/settings.gradle create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/SecretsServlet.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/SecretRequestValidator.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/UpdateSecretRequestValidator.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/AbstractSecretsRoute.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretDetailsRoute.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretsRoute.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/MockCredentials.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsServletTest.java create mode 100644 galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/mocks/MockSecretsServlet.java create mode 100644 galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/AbstractCredentials.java delete mode 100644 galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java diff --git a/.secrets.baseline b/.secrets.baseline index 8d7a76a24..2882e431f 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -94,12 +94,22 @@ "verified_result": null } ], + "galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/ServletErrorMessage.java": [ + { + "hashed_secret": "a5c1ad5f1dc7d24152e39cb14dfa99775fa1884d", + "is_secret": false, + "is_verified": false, + "line_number": 139, + "type": "Secret Keyword", + "verified_result": null + } + ], "galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java": [ { "hashed_secret": "ef04cf5107d3be11fceddd045cc585d2bda031bb", "is_secret": false, "is_verified": false, - "line_number": 28, + "line_number": 30, "type": "JSON Web Token", "verified_result": null } @@ -109,7 +119,35 @@ "hashed_secret": "0ea7458942ab65e0a340cf4fd28ca00d93c494f3", "is_secret": false, "is_verified": false, - "line_number": 321, + "line_number": 710, + "type": "Secret Keyword", + "verified_result": null + } + ], + "galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java": [ + { + "hashed_secret": "1beb7496ebbe82c61151be093956d83dac625c13", + "is_secret": false, + "is_verified": false, + "line_number": 293, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "89e7fc0c50091804bfeb26cddefc0e701dd60fab", + "is_secret": false, + "is_verified": false, + "line_number": 732, + "type": "Secret Keyword", + "verified_result": null + } + ], + "galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java": [ + { + "hashed_secret": "1beb7496ebbe82c61151be093956d83dac625c13", + "is_secret": false, + "is_verified": false, + "line_number": 670, "type": "Secret Keyword", "verified_result": null } diff --git a/galasa-parent/dev.galasa.framework.api.authentication/src/main/java/dev/galasa/framework/api/authentication/internal/TokenPayloadValidator.java b/galasa-parent/dev.galasa.framework.api.authentication/src/main/java/dev/galasa/framework/api/authentication/internal/TokenPayloadValidator.java index 1b0fb6763..7d9c489a7 100644 --- a/galasa-parent/dev.galasa.framework.api.authentication/src/main/java/dev/galasa/framework/api/authentication/internal/TokenPayloadValidator.java +++ b/galasa-parent/dev.galasa.framework.api.authentication/src/main/java/dev/galasa/framework/api/authentication/internal/TokenPayloadValidator.java @@ -9,12 +9,13 @@ import dev.galasa.framework.api.common.IBeanValidator; import dev.galasa.framework.api.common.InternalServletException; import dev.galasa.framework.api.common.ServletError; +import dev.galasa.framework.api.common.resources.AbstractValidator; import static dev.galasa.framework.api.common.ServletErrorMessage.*; import javax.servlet.http.HttpServletResponse; -public class TokenPayloadValidator implements IBeanValidator { +public class TokenPayloadValidator extends AbstractValidator implements IBeanValidator { @Override public void validate(TokenPayload tokenPayload) throws InternalServletException { @@ -36,21 +37,4 @@ public void validate(TokenPayload tokenPayload) throws InternalServletException throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); } } - - /** - * Checks whether a given string contains only alphanumeric characters, '-', and '_' - * - * @param str the string to validate - * @return true if the string contains only alphanumeric characters, '-', and '_', or false otherwise - */ - private boolean isAlphanumWithDashes(String str) { - boolean isValid = true; - for (char c : str.toCharArray()) { - if (!Character.isLetterOrDigit(c) && c != '-' && c != '_') { - isValid = false; - break; - } - } - return isValid; - } } 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 2b6357784..4f66f722f 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 @@ -95,11 +95,11 @@ public enum ServletErrorMessage { 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."), + GAL5075_ERROR_SECRET_ALREADY_EXISTS (5075, "E: Error occurred when trying to create a secret with the given name. A secret with the provided name already exists."), + GAL5076_ERROR_SECRET_DOES_NOT_EXIST (5076, "E: Error occurred. A secret with the provided name does not exist. Check that your provided secret name is correct and try again."), 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."), + 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 name 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."), @@ -125,7 +125,20 @@ public enum ServletErrorMessage { // User APIs... GAL5081_INVALID_QUERY_PARAM_VALUE (5081, "E: A request to get the user details for a particular user failed. The query parameter provided is not valid. Supported values for the ‘loginId’ query parameter are : ‘me’. This problem is caused by the client program sending a bad request. Please report this problem to the owner of your client program."), - GAL5082_NO_LOGINID_PARAM_PROVIDED (5082, "E: A request to get the user details failed. The request did not supply a ‘loginId’ filter. A ‘loginId’ query parameter with a value of : ‘me’ was expected. This problem is caused by the client program sending a bad request. Please report this problem to the owner of your client program.") + GAL5082_NO_LOGINID_PARAM_PROVIDED (5082, "E: A request to get the user details failed. The request did not supply a ‘loginId’ filter. A ‘loginId’ query parameter with a value of : ‘me’ was expected. This problem is caused by the client program sending a bad request. Please report this problem to the owner of your client program."), + + // Secrets APIs... + GAL5092_INVALID_SECRET_NAME_PROVIDED (5092, "E: Invalid secret name provided. The name of a Galasa secret cannot be empty, contain only spaces or tabs, or contain dots ('.'), and must only contain characters in the Latin-1 character set. Check your request payload and try again."), + GAL5093_ERROR_SECRET_NOT_FOUND (5093, "E: Unable to retrieve a secret with the given name. No such secret exists. Check your request query parameters and try again."), + GAL5094_FAILED_TO_GET_SECRET_FROM_CREDS (5094, "E: Failed to retrieve a secret with the given name from the credentials store. The credentials store might be badly configured or could be experiencing a temporary issue. Report the problem to your Galasa Ecosystem owner."), + GAL5095_ERROR_PASSWORD_AND_TOKEN_PROVIDED (5095, "E: Invalid secret payload provided. The ''password'' and ''token'' fields are mutually exclusive and cannot be provided in the same secret. Check your request payload and try again."), + GAL5096_ERROR_MISSING_SECRET_VALUE (5096, "E: Invalid secret payload provided. One or more secret fields in your request payload are missing a ''value''. Check your request payload and try again."), + GAL5097_FAILED_TO_DECODE_SECRET_VALUE (5097, "E: Failed to decode a provided secret value. Expected the value to be encoded in ''{0}'' format but it was not. Check your request values are properly encoded and try again."), + GAL5098_ERROR_PASSWORD_MISSING_USERNAME (5098, "E: Invalid secret payload provided. A ''password'' field was provided but the ''username'' field was missing. Check your request payload and try again."), + GAL5099_ERROR_MISSING_REQUIRED_SECRET_FIELD (5099, "E: Invalid secret payload provided. The ''{0}'' type was provided but the required ''{1}'' field was missing. Check your request payload and try again."), + GAL5100_ERROR_UNEXPECTED_SECRET_FIELD_PROVIDED (5100, "E: Invalid secret payload provided. An unexpected field was given to update a ''{0}'' secret. Only the following fields can be provided to update this secret: ''{1}''. Check your request payload and try again."), + GAL5101_ERROR_UNEXPECTED_SECRET_TYPE_DETECTED (5101, "E: Unknown secret type detected. A secret retrieved from the credentials store is in an unknown or unsupported format. Report the problem to your Galasa Ecosystem owner."), + GAL5102_INVALID_SECRET_DESCRIPTION_PROVIDED (5102, "E: Invalid secret description provided. The description should not only contain spaces or tabs. When provided, it must contain characters in the Latin-1 character set. Report the problem to your Galasa Ecosystem owner."), ; diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/AbstractValidator.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/AbstractValidator.java new file mode 100644 index 000000000..90858a111 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/AbstractValidator.java @@ -0,0 +1,46 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.common.resources; + +/** + * A base validator class that contains commonly-used validation methods + */ +public abstract class AbstractValidator { + + /** + * Checks whether a given string is in valid Latin-1 format (e.g. characters in the range 0 - 255) + * + * @param str the string to validate + * @return true if the string is in valid Latin-1 format, or false otherwise + */ + public boolean isLatin1(String str) { + boolean isValidLatin1 = true; + for (char i = 0; i < str.length(); i++) { + if (str.charAt(i) > 255) { + isValidLatin1 = false; + break; + } + } + return isValidLatin1; + } + + /** + * Checks whether a given string contains only alphanumeric characters, '-', and '_' + * + * @param str the string to validate + * @return true if the string contains only alphanumeric characters, '-', and '_', or false otherwise + */ + public boolean isAlphanumWithDashes(String str) { + boolean isValid = true; + for (char c : str.toCharArray()) { + if (!Character.isLetterOrDigit(c) && c != '-' && c != '_') { + isValid = false; + break; + } + } + return isValid; + } +} diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaResourceValidator.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaResourceValidator.java new file mode 100644 index 000000000..bd1e7bb51 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/GalasaResourceValidator.java @@ -0,0 +1,79 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.common.resources; + +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.JsonObject; + +import dev.galasa.framework.api.common.IBeanValidator; +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.ServletError; + +/** + * An abstract class containing the base methods used to validate Galasa resources. + */ +public abstract class GalasaResourceValidator extends AbstractValidator implements IBeanValidator { + + public static final String DEFAULT_API_VERSION = "galasa-dev/v1alpha1"; + + protected List validationErrors = new ArrayList<>(); + protected ResourceAction action; + + public GalasaResourceValidator() {} + + public GalasaResourceValidator(ResourceAction action) { + this.action = action; + } + + public List getValidationErrors() { + return validationErrors; + } + + private List getRequiredResourceFields() { + List requiredFields = new ArrayList<>(); + requiredFields.add("apiVersion"); + requiredFields.add("metadata"); + if (action != DELETE) { + requiredFields.add("data"); + } + return requiredFields; + } + + protected List getMissingResourceFields(JsonObject resourceJson, List requiredFields) { + List missingFields = new ArrayList<>(); + for (String field : requiredFields) { + if (!resourceJson.has(field)) { + missingFields.add(field); + } + } + return missingFields; + } + + protected void checkResourceHasRequiredFields( + JsonObject resourceJson, + String expectedApiVersion + ) throws InternalServletException { + List requiredFields = getRequiredResourceFields(); + 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); + } + } +} 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 index 6ce4bf7bd..5676cede5 100644 --- 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 @@ -4,7 +4,7 @@ * 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"), 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 index 90f7d5888..272e1e8fe 100644 --- 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 @@ -10,6 +10,7 @@ import dev.galasa.framework.api.common.ServletError; import dev.galasa.framework.spi.creds.CredentialsException; import dev.galasa.framework.spi.creds.ICredentialsService; +import dev.galasa.framework.spi.utils.ITimeService; import static dev.galasa.framework.api.common.ServletErrorMessage.*; @@ -19,11 +20,13 @@ public class Secret { private String secretId; private ICredentialsService credentialsService; + private ITimeService timeService; private ICredentials value; - public Secret(ICredentialsService credentialsService, String secretName) { + public Secret(ICredentialsService credentialsService, String secretName, ITimeService timeService) { this.secretId = secretName; this.credentialsService = credentialsService; + this.timeService = timeService; } public boolean existsInCredentialsStore() { @@ -39,8 +42,10 @@ public void loadValueFromCredentialsStore() throws InternalServletException { } } - public void setSecretToCredentialsStore(ICredentials newValue) throws InternalServletException { + public void setSecretToCredentialsStore(ICredentials newValue, String username) throws InternalServletException { try { + newValue.setLastUpdatedTime(timeService.now()); + newValue.setLastUpdatedByUser(username); credentialsService.setCredentials(secretId, newValue); } catch (CredentialsException e) { ServletError error = new ServletError(GAL5077_FAILED_TO_SET_SECRET); diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/SecretValidator.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/SecretValidator.java new file mode 100644 index 000000000..3171915b8 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/SecretValidator.java @@ -0,0 +1,40 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.common.resources; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; + +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.ServletError; + +public abstract class SecretValidator extends GalasaResourceValidator { + + public static final List SUPPORTED_ENCODING_SCHEMES = List.of("base64"); + + public SecretValidator() {} + + public SecretValidator(ResourceAction action) { + super(action); + } + + protected void validateSecretName(String secretName) throws InternalServletException { + if (secretName == null || secretName.isBlank() || secretName.contains(".") || !isLatin1(secretName)) { + ServletError error = new ServletError(GAL5092_INVALID_SECRET_NAME_PROVIDED); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + } + + protected void validateDescription(String description) throws InternalServletException { + if (description != null && (description.isBlank() || !isLatin1(description))) { + ServletError error = new ServletError(GAL5102_INVALID_SECRET_DESCRIPTION_PROVIDED); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + } +} diff --git a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java index a9b205f61..b37ba0e26 100644 --- a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java +++ b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/BaseServletTest.java @@ -10,6 +10,8 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import dev.galasa.framework.spi.utils.GalasaGson; + import static org.assertj.core.api.Assertions.*; import java.util.Map; @@ -25,7 +27,10 @@ public class BaseServletTest { // "name": "Jack Skellington", // "iat": 1516239022 // } - public final static String DUMMY_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0UmVxdWVzdG9yIiwic3ViIjoicmVxdWVzdG9ySWQiLCJuYW1lIjoiSmFjayBTa2VsbGluZ3RvbiIsImlhdCI6MTUxNjIzOTAyMn0.kW1arFknbywrtRrxsLjB2MiXcM6oSgnUrOpuAlE5dhk"; //Dummy JWT + public static final String DUMMY_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0UmVxdWVzdG9yIiwic3ViIjoicmVxdWVzdG9ySWQiLCJuYW1lIjoiSmFjayBTa2VsbGluZ3RvbiIsImlhdCI6MTUxNjIzOTAyMn0.kW1arFknbywrtRrxsLjB2MiXcM6oSgnUrOpuAlE5dhk"; //Dummy JWT + public static final String JWT_USERNAME = "testRequestor"; + + protected static final GalasaGson gson = new GalasaGson(); protected void checkErrorStructure(String jsonString , int expectedErrorCode , String... expectedErrorMessageParts ) throws Exception { 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 edcd8608e..f8ca35377 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 @@ -42,6 +42,10 @@ public MockFramework() { // Do nothing... } + public MockFramework(MockCredentialsService credsService) { + this.creds = credsService; + } + public MockFramework(IAuthStoreService authStoreService) { this.authStoreService = authStoreService; } diff --git a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockHttpServletRequest.java b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockHttpServletRequest.java index 3d8a471d6..541a6e51b 100644 --- a/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockHttpServletRequest.java +++ b/galasa-parent/dev.galasa.framework.api.common/src/testFixtures/java/dev/galasa/framework/api/common/mocks/MockHttpServletRequest.java @@ -186,6 +186,10 @@ public void setContentType(String contentType) { this.contentType = contentType; } + public void setQueryParameter(String parameter, String value) { + this.parameterMap.put(parameter, new String[] { value }); + } + public void setHeader(String header, String value) { this.headerMap.put(header, value); } 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 5359cb81b..82b6ceb9d 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 @@ -292,19 +292,6 @@ paths: '500': $ref: '#/components/responses/InternalServerError' - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/JsonError' - examples: - genericerror: - value: - error_code: 5000 - error_message: "GAL5000E: Error occurred when trying to access the endpoint. Report the problem to your Galasa Ecosystem owner." - summary: An Error occurred when trying to access the endpoint - - ################################################################################## # Users API ################################################################################## @@ -1725,6 +1712,148 @@ paths: '401': $ref: "#/components/responses/Unauthorized" ################################################################################## +# Secrets API +################################################################################## + /secrets: + parameters: + - $ref: '#/components/parameters/ClientApiVersion' + get: + summary: Get a list of secrets from the Credentials Store + description: | + Returns a list of secrets stored in the Credentials Store. + + Requests to this endpoint require a valid bearer token in JWT format to be provided + in the 'Authorization' header (e.g. 'Authorization: Bearer '). + operationId: getSecrets + tags: + - Secrets API + parameters: + - name: name + in: query + description: The name of a specific secret to be retrieved + required: false + schema: + type: string + responses: + '200': + description: A list of secrets + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/GalasaSecret' + '401': + $ref: "#/components/responses/Unauthorized" + '500': + $ref: '#/components/responses/InternalServerError' + post: + summary: Create a new secret + description: | + Creates a new secret and stores it in the Credentials Store. + + Requests to this endpoint require a valid bearer token in JWT format to be provided + in the 'Authorization' header (e.g. 'Authorization: Bearer '). + operationId: createSecret + tags: + - Secrets API + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SecretRequest' + responses: + '201': + description: A new secret was created successfully + '409': + $ref: "#/components/responses/Conflict" + '401': + $ref: "#/components/responses/Unauthorized" + '400': + $ref: "#/components/responses/BadRequest" + '500': + $ref: '#/components/responses/InternalServerError' + /secrets/{secretName}: + parameters: + - $ref: '#/components/parameters/ClientApiVersion' + - name: secretName + in: path + description: The name of the secret resource to manage + required: true + schema: + type: string + get: + summary: Gets a secret with the given name from the Credentials Store + description: | + Returns a secret with the given name from the Credentials Store. + + Requests to this endpoint require a valid bearer token in JWT format to be provided + in the 'Authorization' header (e.g. 'Authorization: Bearer '). + operationId: getSecret + tags: + - Secrets API + responses: + '200': + description: The secret with the provided name + content: + application/json: + schema: + $ref: '#/components/schemas/GalasaSecret' + '404': + $ref: '#/components/responses/NotFound' + '401': + $ref: "#/components/responses/Unauthorized" + '500': + $ref: '#/components/responses/InternalServerError' + put: + summary: Create or update a secret + description: | + Updates an existing secret with the given name in the Credentials Store. + If no such secret exists, a new secret will be created. + + Requests to this endpoint require a valid bearer token in JWT format to be provided + in the 'Authorization' header (e.g. 'Authorization: Bearer '). + operationId: updateSecret + tags: + - Secrets API + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SecretRequest' + responses: + '204': + description: The existing secret was updated successfully + '201': + description: A new secret was created successfully + '401': + $ref: "#/components/responses/Unauthorized" + '400': + $ref: "#/components/responses/BadRequest" + '500': + $ref: '#/components/responses/InternalServerError' + delete: + operationId: deleteSecret + summary: Deletes a secret from the Credentials Store + description: | + Deletes a secret with the given name from the Credentials Store. + + Requests to this endpoint require a valid bearer token in JWT format to be provided + in the 'Authorization' header (e.g. 'Authorization: Bearer '). + tags: + - Secrets API + responses: + '204': + description: No content response indicating that the given secret has been deleted + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' +################################################################################## # Bootstrap API ################################################################################## /bootstrap: @@ -1751,6 +1880,18 @@ paths: ################################################################################## components: responses: + BadRequest: + description: The provided request payload was not valid + content: + application/json: + schema: + $ref: '#/components/schemas/APIError' + Conflict: + description: The requested resource already exists + content: + application/json: + schema: + $ref: '#/components/schemas/APIError' InternalServerError: description: An internal server error occurred content: @@ -1763,6 +1904,12 @@ components: error_code: 5000 error_message: "GAL5000E: Error occurred when trying to access the endpoint. Report the problem to your Galasa Ecosystem owner." summary: An Error occurred when trying to access the endpoint + NotFound: + description: The requested resource could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/APIError' Unauthorized: description: Unauthorized as a valid bearer token has not been provided in the "Authorization" header content: @@ -1895,15 +2042,27 @@ components: name: type: string description: | - The name of the Galasa Secret to perform a resource action on. + The name that identifies the Galasa Secret. + description: + type: string + description: | + The description to be associated with the Galasa Secret. + lastUpdatedTime: + type: string + format: date-time + description: | + The timestamp at which the Galasa Secret was last updated. + lastUpdatedBy: + type: string + description: | + The ID of the last user that updated the Galasa Secret. 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] + $ref: '#/components/schemas/GalasaSecretType' description: | The type of the Galasa Secret resource. Supported types are 'UsernamePassword', 'Username', 'UsernameToken', 'Token'. @@ -1928,6 +2087,10 @@ components: 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). + GalasaSecretType: + type: string + description: The type of a Galasa Secret + enum: [UsernamePassword, Username, UsernameToken, Token] APIErrorArray: type: array description: An array of API Errors relating to the resources that failed to complete as expected @@ -1952,6 +2115,56 @@ components: items: anyOf: - $ref: '#/components/schemas/GalasaProperty' + - $ref: '#/components/schemas/GalasaSecret' + SecretRequest: + type: object + properties: + name: + type: string + description: The name of the secret to create or update + description: + type: string + description: The description to associate with the secret to create or update + type: + type: string + description: The type of the secret to create or update + $ref: '#/components/schemas/GalasaSecretType' + username: + type: object + description: The username to create or update in the Credentials Store. + properties: + value: + type: string + description: The username value to set into the secret + encoding: + type: string + description: | + The character encoding scheme that has been applied to the username. + Currently, base64 is the only supported encoding scheme + password: + type: object + description: The password to create or update in the Credentials Store. Cannot be used alongside 'token' + properties: + value: + type: string + description: The password value to set into the secret + encoding: + type: string + description: | + The character encoding scheme that has been applied to the password. + Currently, base64 is the only supported encoding scheme + token: + type: object + description: The token to create or update in the Credentials Store. Cannot be used alongside 'password' + properties: + value: + type: string + description: The token value to set into the secret + encoding: + type: string + description: | + The character encoding scheme that has been applied to the token. + Currently, base64 is the only supported encoding scheme TestRuns: type: object properties: 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 645687562..c22bc2833 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 @@ -18,11 +18,15 @@ import dev.galasa.framework.FileSystem; import dev.galasa.framework.IFileSystem; import dev.galasa.framework.api.common.BaseServlet; +import dev.galasa.framework.api.common.Environment; +import dev.galasa.framework.api.common.SystemEnvironment; import dev.galasa.framework.api.common.resources.CPSFacade; 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; +import dev.galasa.framework.spi.utils.ITimeService; +import dev.galasa.framework.spi.utils.SystemTimeService; /* * Proxy Servlet for the /resources/* endpoints */ @@ -38,6 +42,9 @@ public class ResourcesServlet extends BaseServlet { protected Log logger = LogFactory.getLog(this.getClass()); protected IFileSystem fileSystem = new FileSystem(); + + protected ITimeService timeService = new SystemTimeService(); + protected Environment env = new SystemEnvironment(); protected IFramework getFramework() { return this.framework; @@ -54,7 +61,7 @@ public void init() throws ServletException { super.init(); try { - addRoute(new ResourcesRoute(getResponseBuilder(), new CPSFacade(framework), framework.getCredentialsService())); + addRoute(new ResourcesRoute(getResponseBuilder(), new CPSFacade(framework), framework.getCredentialsService(), timeService, env)); } 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 index 9d4aca528..4d1d2554f 100644 --- 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 @@ -5,19 +5,15 @@ */ 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.GalasaResourceValidator; import dev.galasa.framework.api.common.resources.ResourceAction; import dev.galasa.framework.spi.utils.GalasaGson; @@ -25,42 +21,10 @@ 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; - } + protected List checkGalasaResourceJsonStructure(GalasaResourceValidator validator, JsonObject propertyJson) throws InternalServletException { + validator.validate(propertyJson); - private List getRequiredResourceFields(ResourceAction action) { - List requiredFields = new ArrayList<>(); - requiredFields.add("apiVersion"); - requiredFields.add("metadata"); - if (action != DELETE) { - requiredFields.add("data"); - } - return requiredFields; + List validationErrors = validator.getValidationErrors(); + return validationErrors; } } 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 index ea93ab5ce..a8ed74dab 100644 --- 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 @@ -8,12 +8,10 @@ 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; @@ -23,20 +21,19 @@ 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.api.resources.validators.GalasaPropertyValidator; 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 { + public List processResource(JsonObject resource, ResourceAction action, String username) throws InternalServletException { List errors = checkGalasaPropertyJsonStructure(resource, action); try { if (errors.isEmpty()) { @@ -75,61 +72,7 @@ public List processResource(JsonObject resource, ResourceAction action) } 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()); - } + GalasaPropertyValidator validator = new GalasaPropertyValidator(action); + return checkGalasaResourceJsonStructure(validator, propertyJson); } } 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 index 2ab2485f5..0fb836d22 100644 --- 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 @@ -9,12 +9,9 @@ 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; @@ -26,16 +23,19 @@ 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.beans.generated.GalasaSecretmetadata; 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.api.resources.validators.GalasaSecretValidator; 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; +import dev.galasa.framework.spi.utils.ITimeService; /** * Processor class to handle creating, updating, and deleting GalasaSecret resources @@ -44,24 +44,23 @@ public class GalasaSecretProcessor extends AbstractGalasaResourceProcessor imple 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; + private ITimeService timeService; - public GalasaSecretProcessor(ICredentialsService credentialsService) { + public GalasaSecretProcessor(ICredentialsService credentialsService, ITimeService timeService) { this.credentialsService = credentialsService; + this.timeService = timeService; } @Override - public List processResource(JsonObject resourceJson, ResourceAction action) throws InternalServletException { + public List processResource(JsonObject resourceJson, ResourceAction action, String username) 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); + Secret secret = new Secret(credentialsService, credentialsId, timeService); if (action == DELETE) { logger.info("Deleting secret from credentials store"); @@ -78,12 +77,13 @@ public List processResource(JsonObject resourceJson, ResourceAction acti throw new InternalServletException(error, HttpServletResponse.SC_NOT_FOUND); } - GalasaSecretType secretType = GalasaSecretType.getFromString(galasaSecret.getmetadata().gettype().toString()); + GalasaSecretmetadata metadata = galasaSecret.getmetadata(); + GalasaSecretType secretType = GalasaSecretType.getFromString(metadata.gettype().toString()); GalasaSecretdata decodedData = decodeSecretData(galasaSecret); - ICredentials credentials = getCredentialsFromSecret(secretType, decodedData); + ICredentials credentials = getCredentialsFromSecret(secretType, decodedData, metadata); logger.info("Setting secret in credentials store"); - secret.setSecretToCredentialsStore(credentials); + secret.setSecretToCredentialsStore(credentials, username); logger.info("Secret set in credentials store OK"); } logger.info("Processed GalasaSecret resource OK"); @@ -121,27 +121,22 @@ private GalasaSecretdata decodeSecretData(GalasaSecret galasaSecret) throws Inte 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)); + ServletError error = new ServletError(GAL5073_UNSUPPORTED_GALASA_SECRET_ENCODING, String.join(", ", GalasaSecretValidator.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; + GalasaSecretValidator validator = new GalasaSecretValidator(action); + return checkGalasaResourceJsonStructure(validator, secretJson); } - private ICredentials getCredentialsFromSecret(GalasaSecretType secretType, GalasaSecretdata decodedData) { + private ICredentials getCredentialsFromSecret( + GalasaSecretType secretType, + GalasaSecretdata decodedData, + GalasaSecretmetadata metadata + ) { ICredentials credentials = null; switch (secretType) { case USERNAME: @@ -159,49 +154,10 @@ private ICredentials getCredentialsFromSecret(GalasaSecretType secretType, Galas 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()); + if (credentials != null) { + credentials.setDescription(metadata.getdescription()); } + return credentials; } } 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 index 5679b1c34..58a0b401e 100644 --- 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 @@ -19,8 +19,9 @@ public interface IGalasaResourceProcessor { * * @param resourceJson the resource to perform an action on * @param action the action to perform + * @param username the username of the user performing the action * @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; + List processResource(JsonObject resourceJson, ResourceAction action, String username) 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 ee5e6ebb5..89442da82 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 @@ -23,7 +23,9 @@ import com.google.gson.JsonObject; import dev.galasa.framework.api.common.BaseRoute; +import dev.galasa.framework.api.common.Environment; import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.JwtWrapper; import dev.galasa.framework.api.common.QueryParameters; import dev.galasa.framework.api.common.ResponseBuilder; import dev.galasa.framework.api.common.ServletError; @@ -37,6 +39,7 @@ import dev.galasa.framework.spi.FrameworkException; import dev.galasa.framework.spi.creds.ICredentialsService; import dev.galasa.framework.spi.utils.GalasaGson; +import dev.galasa.framework.spi.utils.ITimeService; public class ResourcesRoute extends BaseRoute{ @@ -50,11 +53,20 @@ public class ResourcesRoute extends BaseRoute{ protected List errors = new ArrayList(); - public ResourcesRoute(ResponseBuilder responseBuilder, CPSFacade cps, ICredentialsService credentialsService) { + private Environment env; + + public ResourcesRoute( + ResponseBuilder responseBuilder, + CPSFacade cps, + ICredentialsService credentialsService, + ITimeService timeService, + Environment env + ) { super(responseBuilder, path); + this.env = env; resourceProcessors.put(GALASA_PROPERTY, new GalasaPropertyProcessor(cps)); - resourceProcessors.put(GALASA_SECRET, new GalasaSecretProcessor(credentialsService)); + resourceProcessors.put(GALASA_SECRET, new GalasaSecretProcessor(credentialsService, timeService)); } @Override @@ -63,7 +75,9 @@ public HttpServletResponse handlePostRequest(String pathInfo, QueryParameters qu logger.info("ResourcesRoute - handlePostRequest() entered"); JsonObject jsonBody = parseRequestBody(request, JsonObject.class); - List errorsList = processRequest(jsonBody); + + String requestUsername = new JwtWrapper(request, env).getUsername(); + List errorsList = processRequest(jsonBody, requestUsername); if (errorsList.size() >0){ response = getResponseBuilder().buildResponse(request, response, "application/json", getErrorsAsJson(errorsList), HttpServletResponse.SC_BAD_REQUEST); } else { @@ -76,12 +90,12 @@ public HttpServletResponse handlePostRequest(String pathInfo, QueryParameters qu } - protected List processRequest(JsonObject body) throws InternalServletException{ + protected List processRequest(JsonObject body, String username) throws InternalServletException{ 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); + processDataArray(jsonArray, action, username); } else { ServletError error = new ServletError(GAL5025_UNSUPPORTED_ACTION); throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); @@ -105,7 +119,7 @@ protected String getErrorsAsJson(List errorsList){ return gson.toJson(json); } - protected void processDataArray(JsonArray jsonArray, ResourceAction action) throws InternalServletException{ + protected void processDataArray(JsonArray jsonArray, ResourceAction action, String username) throws InternalServletException{ for (JsonElement element: jsonArray) { try { checkJsonElementIsValidJSON(element); @@ -118,7 +132,7 @@ protected void processDataArray(JsonArray jsonArray, ResourceAction action) thro throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); } - errors.addAll(resourceProcessors.get(kind).processResource(resource, action)); + errors.addAll(resourceProcessors.get(kind).processResource(resource, action, username)); } catch (InternalServletException s) { errors.add(s.getMessage()); diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/validators/GalasaPropertyValidator.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/validators/GalasaPropertyValidator.java new file mode 100644 index 000000000..b53131b9e --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/validators/GalasaPropertyValidator.java @@ -0,0 +1,88 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.resources.validators; + +import static dev.galasa.framework.api.common.ServletErrorMessage.GAL5024_INVALID_GALASAPROPERTY; +import static dev.galasa.framework.api.common.resources.ResourceAction.*; + +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.GalasaResourceValidator; +import dev.galasa.framework.api.common.resources.ResourceAction; +import dev.galasa.framework.api.common.resources.ResourceNameValidator; + +public class GalasaPropertyValidator extends GalasaResourceValidator { + + private ResourceNameValidator nameValidator = new ResourceNameValidator(); + + public GalasaPropertyValidator(ResourceAction action) { + super(action); + } + + @Override + public void validate(JsonObject propertyJson) throws InternalServletException { + checkResourceHasRequiredFields(propertyJson, GalasaProperty.DEFAULTAPIVERSION); + + validatePropertyMetadata(propertyJson); + + // Delete operations shouldn't require a 'data' section, just the metadata to identify + // the property to delete + if (action != DELETE) { + validatePropertyData(propertyJson); + } + } + + private void validatePropertyMetadata(JsonObject propertyJson) { + //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) { + //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/validators/GalasaSecretValidator.java b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/validators/GalasaSecretValidator.java new file mode 100644 index 000000000..1d269cccb --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/validators/GalasaSecretValidator.java @@ -0,0 +1,103 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.resources.validators; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; +import static dev.galasa.framework.api.common.resources.ResourceAction.*; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +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.GalasaSecretType; +import dev.galasa.framework.api.common.resources.ResourceAction; +import dev.galasa.framework.api.common.resources.SecretValidator; + +public class GalasaSecretValidator extends SecretValidator { + + public GalasaSecretValidator(ResourceAction action) { + super(action); + } + + @Override + public void validate(JsonObject secretJson) throws InternalServletException { + checkResourceHasRequiredFields(secretJson, DEFAULT_API_VERSION); + + validateSecretMetadata(secretJson); + + // 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); + } + } + + private void validateSecretMetadata(JsonObject secretJson) { + 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()); + } + + if (metadata.has("name")) { + try { + validateSecretName(metadata.get("name").getAsString()); + } catch (InternalServletException e) { + validationErrors.add(e.getMessage()); + } + } + + // If a description is provided, check that it is valid + if (metadata.has("description")) { + try { + validateDescription(metadata.get("description").getAsString()); + } catch (InternalServletException e) { + validationErrors.add(e.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) { + 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/test/java/dev/galasa/framework/api/resources/ResourcesServletTest.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/ResourcesServletTest.java index 3d306a87c..a267ce0bf 100644 --- a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/ResourcesServletTest.java +++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/ResourcesServletTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.*; import java.io.PrintWriter; +import java.time.Instant; import java.util.List; import java.util.Map; @@ -21,6 +22,7 @@ import com.google.gson.JsonObject; import dev.galasa.framework.api.common.BaseServletTest; +import dev.galasa.framework.api.common.EnvironmentVariables; import dev.galasa.framework.api.common.ResponseBuilder; import dev.galasa.framework.api.common.mocks.MockEnvironment; import dev.galasa.framework.api.common.mocks.MockFramework; @@ -28,7 +30,7 @@ import dev.galasa.framework.api.common.mocks.MockHttpServletResponse; import dev.galasa.framework.api.common.mocks.MockIConfigurationPropertyStoreService; import dev.galasa.framework.api.common.mocks.MockServletOutputStream; - +import dev.galasa.framework.api.common.mocks.MockTimeService; import dev.galasa.framework.api.resources.mocks.MockResourcesServlet; import dev.galasa.framework.spi.ConfigurationPropertyStoreException; import dev.galasa.framework.spi.IConfigurationPropertyStoreService; @@ -43,6 +45,8 @@ public class ResourcesServletTest extends BaseServletTest { HttpServletRequest req; HttpServletResponse resp; + private Map headers = Map.of("Authorization", "Bearer " + BaseServletTest.DUMMY_JWT); + private class MockICPSServiceWithError extends MockIConfigurationPropertyStoreService { protected MockICPSServiceWithError(String namespace){ super.namespaceInput= namespace; @@ -55,13 +59,6 @@ public void deleteProperty(@NotNull String name) throws ConfigurationPropertySto } protected void setServlet(String namespace){ - this.servlet = new MockResourcesServlet(); - servlet.setResponseBuilder(new ResponseBuilder(new MockEnvironment())); - - ServletOutputStream outStream = new MockServletOutputStream(); - PrintWriter writer = new PrintWriter(outStream); - this.resp = new MockHttpServletResponse(writer, outStream); - IConfigurationPropertyStoreService cpsstore; if (namespace != null){ cpsstore = new MockIConfigurationPropertyStoreService(namespace); @@ -69,17 +66,28 @@ protected void setServlet(String namespace){ cpsstore = new MockICPSServiceWithError("framework"); } IFramework framework = new MockFramework(cpsstore); - this.servlet.setFramework(framework); + + MockEnvironment env = new MockEnvironment(); + env.setenv(EnvironmentVariables.GALASA_USERNAME_CLAIMS, "preferred_username"); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + this.servlet = new MockResourcesServlet(framework, env, timeService); + + servlet.setResponseBuilder(new ResponseBuilder(env)); + + ServletOutputStream outStream = new MockServletOutputStream(); + PrintWriter writer = new PrintWriter(outStream); + this.resp = new MockHttpServletResponse(writer, outStream); } protected void setServlet(String path,String namespace, Map parameterMap){ setServlet(namespace); - this.req = new MockHttpServletRequest(parameterMap,path); + this.req = new MockHttpServletRequest(parameterMap, path, headers); } protected void setServlet( String path,String namespace, JsonObject requestBody, String method){ setServlet(namespace); - this.req = new MockHttpServletRequest(path, gson.toJson(requestBody), method); + this.req = new MockHttpServletRequest(path, gson.toJson(requestBody), method, headers); } protected void setServlet( String path,String namespace, JsonObject requestBody, String method, Map headerMap) { diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/mocks/MockResourcesServlet.java b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/mocks/MockResourcesServlet.java index 89f5c63f5..29e1a5ff0 100644 --- a/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/mocks/MockResourcesServlet.java +++ b/galasa-parent/dev.galasa.framework.api.resources/src/test/java/dev/galasa/framework/api/resources/mocks/MockResourcesServlet.java @@ -7,11 +7,19 @@ import dev.galasa.framework.IFileSystem; import dev.galasa.framework.api.common.mocks.IServletUnderTest; +import dev.galasa.framework.api.common.mocks.MockEnvironment; +import dev.galasa.framework.api.common.mocks.MockTimeService; import dev.galasa.framework.api.resources.ResourcesServlet; import dev.galasa.framework.spi.IFramework; public class MockResourcesServlet extends ResourcesServlet implements IServletUnderTest{ + public MockResourcesServlet(IFramework framework, MockEnvironment env, MockTimeService timeService) { + this.framework = framework; + this.env = env; + this.timeService = timeService; + } + @Override public void setFramework(IFramework framework) { super.setFramework(framework); 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 index 363d33989..8105f0e9d 100644 --- 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 @@ -24,6 +24,7 @@ public class GalasaPropertyProcessorTest extends ResourcesServletTest { @Test public void testProcessGalasaPropertyValidPropertyReturnsOK() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "myvalue"; @@ -34,7 +35,7 @@ public void testProcessGalasaPropertyValidPropertyReturnsOK() throws Exception { JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - propertyProcessor.processResource(propertyJson, APPLY); + propertyProcessor.processResource(propertyJson, APPLY, username); //Then... checkPropertyInNamespace(namespace,propertyname,value); @@ -43,6 +44,7 @@ public void testProcessGalasaPropertyValidPropertyReturnsOK() throws Exception { @Test public void testProcessGalasaPropertyPropertyWithNewNamespaceReturnsOK() throws Exception { //Given... + String username = "myuser"; String namespace = "newnamespace"; String propertyname = "property.name"; String value = "myvalue"; @@ -53,7 +55,7 @@ public void testProcessGalasaPropertyPropertyWithNewNamespaceReturnsOK() throws JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - propertyProcessor.processResource(propertyJson, APPLY); + propertyProcessor.processResource(propertyJson, APPLY, username); //Then... checkPropertyInNamespace(namespace,propertyname,value); @@ -62,6 +64,7 @@ public void testProcessGalasaPropertyPropertyWithNewNamespaceReturnsOK() throws @Test public void testProcessGalasaPropertyInvalidPropertyNameReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property1!"; String value = "myvalue"; @@ -72,7 +75,7 @@ public void testProcessGalasaPropertyInvalidPropertyNameReturnsError() throws Ex JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... @@ -85,6 +88,7 @@ public void testProcessGalasaPropertyInvalidPropertyNameReturnsError() throws Ex @Test public void testProcessGalasaPropertyPropertyNameWithTrailingDotReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name."; String value = "myvalue"; @@ -95,7 +99,7 @@ public void testProcessGalasaPropertyPropertyNameWithTrailingDotReturnsError() t JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -107,6 +111,7 @@ public void testProcessGalasaPropertyPropertyNameWithTrailingDotReturnsError() t @Test public void testProcessGalasaPropertyPropertyNameWithLeadingDotReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = ".property.name"; String value = "myvalue"; @@ -117,7 +122,7 @@ public void testProcessGalasaPropertyPropertyNameWithLeadingDotReturnsError() th JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -129,6 +134,7 @@ public void testProcessGalasaPropertyPropertyNameWithLeadingDotReturnsError() th @Test public void testProcessGalasaPropertyBadPropertyNameReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property"; String value = "myvalue"; @@ -139,7 +145,7 @@ public void testProcessGalasaPropertyBadPropertyNameReturnsError() throws Except JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -151,6 +157,7 @@ public void testProcessGalasaPropertyBadPropertyNameReturnsError() throws Except @Test public void testProcessGalasaPropertyMissingPropertyNameReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = ""; String value = "myvalue"; @@ -161,7 +168,7 @@ public void testProcessGalasaPropertyMissingPropertyNameReturnsError() throws Ex JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -173,6 +180,7 @@ public void testProcessGalasaPropertyMissingPropertyNameReturnsError() throws Ex @Test public void testProcessGalasaPropertyMissingPropertyNamespaceReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = ""; String propertyname = "property.name"; String value = "myvalue"; @@ -183,7 +191,7 @@ public void testProcessGalasaPropertyMissingPropertyNamespaceReturnsError() thro JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -195,6 +203,7 @@ public void testProcessGalasaPropertyMissingPropertyNamespaceReturnsError() thro @Test public void testProcessGalasaPropertyBadNamespaceReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "namespace@"; String propertyname = "property.name"; String value = "myvalue"; @@ -205,7 +214,7 @@ public void testProcessGalasaPropertyBadNamespaceReturnsError() throws Exception JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -217,6 +226,7 @@ public void testProcessGalasaPropertyBadNamespaceReturnsError() throws Exception @Test public void testProcessGalasaPropertyNamespaceWithTrailingDotReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "namespace."; String propertyname = "property.name"; String value = "myvalue"; @@ -227,7 +237,7 @@ public void testProcessGalasaPropertyNamespaceWithTrailingDotReturnsError() thro JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -239,6 +249,7 @@ public void testProcessGalasaPropertyNamespaceWithTrailingDotReturnsError() thro @Test public void testProcessGalasaPropertyNamespaceWithLeadingDotReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = ".namespace"; String propertyname = "property.name"; String value = "myvalue"; @@ -249,7 +260,7 @@ public void testProcessGalasaPropertyNamespaceWithLeadingDotReturnsError() throw JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -261,6 +272,7 @@ public void testProcessGalasaPropertyNamespaceWithLeadingDotReturnsError() throw @Test public void testProcessGalasaPropertyMissingPropertyValueReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = ""; @@ -271,7 +283,7 @@ public void testProcessGalasaPropertyMissingPropertyValueReturnsError() throws E JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -284,6 +296,7 @@ public void testProcessGalasaPropertyMissingPropertyValueReturnsError() throws E @Test public void testProcessGalasaPropertyEmptyFieldsReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = ""; String propertyname = ""; String value = ""; @@ -294,7 +307,7 @@ public void testProcessGalasaPropertyEmptyFieldsReturnsError() throws Exception JsonObject propertyJson = generatePropertyJson(namespace, propertyname, value, "galasa-dev/v1alpha1"); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -308,6 +321,7 @@ public void testProcessGalasaPropertyEmptyFieldsReturnsError() throws Exception @Test public void testProcessGalasaPropertyNoMetadataOrDataReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = ""; String propertyname = ""; String value = ""; @@ -319,7 +333,7 @@ public void testProcessGalasaPropertyNoMetadataOrDataReturnsError() throws Excep JsonObject propertyJson = JsonParser.parseString(jsonString).getAsJsonObject(); //When... - List errors = propertyProcessor.processResource(propertyJson, APPLY); + List errors = propertyProcessor.processResource(propertyJson, APPLY, username); //Then... assertThat(errors).isNotNull(); @@ -334,6 +348,7 @@ public void testProcessGalasaPropertyNoMetadataOrDataReturnsError() throws Excep @Test public void testProcessGalasaPropertyMissingApiVersionReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "value"; @@ -345,7 +360,7 @@ public void testProcessGalasaPropertyMissingApiVersionReturnsError() throws Exce //When... Throwable thrown = catchThrowable(() -> { - propertyProcessor.processResource(propertyJson, APPLY); + propertyProcessor.processResource(propertyJson, APPLY, username); }); //Then... @@ -357,6 +372,7 @@ public void testProcessGalasaPropertyMissingApiVersionReturnsError() throws Exce @Test public void testProcessGalasaPropertyBadJsonReturnsError() throws Exception { //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "value"; @@ -369,7 +385,7 @@ public void testProcessGalasaPropertyBadJsonReturnsError() throws Exception { //When... Throwable thrown = catchThrowable(() -> { - propertyProcessor.processResource(propertyJson, APPLY); + propertyProcessor.processResource(propertyJson, APPLY, username); }); //Then... 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 index 8f1a16c12..4bbe382d3 100644 --- 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 @@ -8,6 +8,7 @@ import static org.assertj.core.api.Assertions.*; import static dev.galasa.framework.api.common.resources.ResourceAction.*; +import java.time.Instant; import java.util.Base64; import java.util.HashMap; import java.util.List; @@ -21,6 +22,7 @@ import dev.galasa.ICredentials; import dev.galasa.framework.api.common.InternalServletException; import dev.galasa.framework.api.common.mocks.MockCredentialsService; +import dev.galasa.framework.api.common.mocks.MockTimeService; import dev.galasa.framework.api.resources.ResourcesServletTest; import dev.galasa.framework.spi.creds.CredentialsToken; import dev.galasa.framework.spi.creds.CredentialsUsername; @@ -29,17 +31,29 @@ public class GalasaSecretProcessorTest extends ResourcesServletTest { - private JsonObject generateSecretJson(String secretName, String type, String encoding, String username, String password) { + 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 description + ) { + return generateSecretJson(secretName, type, encoding, username, password, null, description); + } - private JsonObject generateSecretJson( + private JsonObject generateSecretJson( String secretName, String type, String encoding, String username, String password, - String token + String token, + String description ) { JsonObject secretJson = new JsonObject(); secretJson.addProperty("apiVersion", "galasa-dev/v1alpha1"); @@ -49,6 +63,10 @@ private JsonObject generateSecretJson( secretMetadata.addProperty("name", secretName); secretMetadata.addProperty("type", type); + if (description != null) { + secretMetadata.addProperty("description", description); + } + if (encoding != null) { secretMetadata.addProperty("encoding", encoding); } @@ -82,14 +100,16 @@ private JsonObject generateSecretJson( // "username": "a-username" // } // } - return secretJson; - } + return secretJson; + } @Test public void testApplySecretWithMissingNameReturnsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernamePassword"; String encoding = null; @@ -101,7 +121,7 @@ public void testApplySecretWithMissingNameReturnsError() throws Exception { secretJson.get("metadata").getAsJsonObject().remove("name"); // When... - List errors = secretProcessor.processResource(secretJson, APPLY); + List errors = secretProcessor.processResource(secretJson, APPLY, requestUsername); // Then... assertThat(errors).hasSize(1); @@ -113,8 +133,10 @@ public void testApplySecretWithMissingNameReturnsError() throws Exception { @Test public void testApplySecretWithMissingSecretTypeReturnsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernamePassword"; String encoding = null; @@ -126,7 +148,7 @@ public void testApplySecretWithMissingSecretTypeReturnsError() throws Exception secretJson.get("metadata").getAsJsonObject().remove("type"); // When... - List errors = secretProcessor.processResource(secretJson, APPLY); + List errors = secretProcessor.processResource(secretJson, APPLY, requestUsername); // Then... assertThat(errors).hasSize(1); @@ -138,8 +160,10 @@ public void testApplySecretWithMissingSecretTypeReturnsError() throws Exception @Test public void testApplySecretWithMissingDataThrowsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernamePassword"; String encoding = null; @@ -152,7 +176,7 @@ public void testApplySecretWithMissingDataThrowsError() throws Exception { // When... InternalServletException thrown = catchThrowableOfType(() -> { - secretProcessor.processResource(secretJson, APPLY); + secretProcessor.processResource(secretJson, APPLY, requestUsername); }, InternalServletException.class); // Then... @@ -165,8 +189,10 @@ public void testApplySecretWithMissingDataThrowsError() throws Exception { @Test public void testApplySecretWithMissingMetadataThrowsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernamePassword"; String encoding = null; @@ -179,7 +205,7 @@ public void testApplySecretWithMissingMetadataThrowsError() throws Exception { // When... InternalServletException thrown = catchThrowableOfType(() -> { - secretProcessor.processResource(secretJson, APPLY); + secretProcessor.processResource(secretJson, APPLY, requestUsername); }, InternalServletException.class); // Then... @@ -192,8 +218,10 @@ public void testApplySecretWithMissingMetadataThrowsError() throws Exception { @Test public void testApplySecretWithMissingApiVersionThrowsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernamePassword"; String encoding = null; @@ -206,7 +234,7 @@ public void testApplySecretWithMissingApiVersionThrowsError() throws Exception { // When... InternalServletException thrown = catchThrowableOfType(() -> { - secretProcessor.processResource(secretJson, APPLY); + secretProcessor.processResource(secretJson, APPLY, requestUsername); }, InternalServletException.class); // Then... @@ -219,8 +247,10 @@ public void testApplySecretWithMissingApiVersionThrowsError() throws Exception { @Test public void testApplySecretWithMissingUsernamePasswordFieldsReturnsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernamePassword"; String encoding = null; @@ -229,7 +259,7 @@ public void testApplySecretWithMissingUsernamePasswordFieldsReturnsError() throw JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); // When... - List errors = secretProcessor.processResource(secretJson, APPLY); + List errors = secretProcessor.processResource(secretJson, APPLY, requestUsername); // Then... assertThat(errors).hasSize(1); @@ -241,8 +271,10 @@ public void testApplySecretWithMissingUsernamePasswordFieldsReturnsError() throw @Test public void testApplySecretWithUnsupportedEncodingReturnsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernamePassword"; String encoding = "UNKNOWN!!!"; @@ -251,7 +283,7 @@ public void testApplySecretWithUnsupportedEncodingReturnsError() throws Exceptio JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); // When... - List errors = secretProcessor.processResource(secretJson, APPLY); + List errors = secretProcessor.processResource(secretJson, APPLY, requestUsername); // Then... assertThat(errors).hasSize(1); @@ -262,8 +294,10 @@ public void testApplySecretWithUnsupportedEncodingReturnsError() throws Exceptio @Test public void testApplySecretWithUnknownSecretTypeReturnsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UNKNOWN TYPE!"; String encoding = null; @@ -272,7 +306,7 @@ public void testApplySecretWithUnknownSecretTypeReturnsError() throws Exception JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password); // When... - List errors = secretProcessor.processResource(secretJson, APPLY); + List errors = secretProcessor.processResource(secretJson, APPLY, requestUsername); // Then... assertThat(errors).hasSize(1); @@ -283,8 +317,10 @@ public void testApplySecretWithUnknownSecretTypeReturnsError() throws Exception @Test public void testApplySecretWithNoNameAndUnknownSecretTypeAndUnknownEncodingReturnsMultipleErrors() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UNKNOWN TYPE!"; String encoding = "UNKNOWN ENCODING!"; @@ -296,7 +332,7 @@ public void testApplySecretWithNoNameAndUnknownSecretTypeAndUnknownEncodingRetur secretJson.get("metadata").getAsJsonObject().remove("name"); // When... - List errors = secretProcessor.processResource(secretJson, APPLY); + List errors = secretProcessor.processResource(secretJson, APPLY, requestUsername); // Then... assertThat(errors).hasSize(3); @@ -312,17 +348,21 @@ public void testApplySecretWithNoNameAndUnknownSecretTypeAndUnknownEncodingRetur @Test public void testCreateUsernamePasswordSecretSetsCredentialsOk() throws Exception { // Given... + Instant lastUpdatedTime = Instant.EPOCH; + MockTimeService mockTimeService = new MockTimeService(lastUpdatedTime); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; 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); + String description = "my new credentials"; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password, description); // When... - List errors = secretProcessor.processResource(secretJson, CREATE); + List errors = secretProcessor.processResource(secretJson, CREATE, requestUsername); // Then... assertThat(errors).isEmpty(); @@ -331,13 +371,18 @@ public void testCreateUsernamePasswordSecretSetsCredentialsOk() throws Exception assertThat(credentials).isNotNull(); assertThat(credentials.getUsername()).isEqualTo(username); assertThat(credentials.getPassword()).isEqualTo(password); + assertThat(credentials.getDescription()).isEqualTo(description); + assertThat(credentials.getLastUpdatedTime()).isEqualTo(lastUpdatedTime); + assertThat(credentials.getLastUpdatedByUser()).isEqualTo(requestUsername); } @Test public void testCreateEncodedUsernamePasswordSecretSetsCredentialsOk() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernamePassword"; @@ -352,7 +397,7 @@ public void testCreateEncodedUsernamePasswordSecretSetsCredentialsOk() throws Ex JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, encodedPassword); // When... - List errors = secretProcessor.processResource(secretJson, CREATE); + List errors = secretProcessor.processResource(secretJson, CREATE, requestUsername); // Then... assertThat(errors).isEmpty(); @@ -367,8 +412,10 @@ public void testCreateEncodedUsernamePasswordSecretSetsCredentialsOk() throws Ex @Test public void testCreateEncodedTokenSecretSetsCredentialsOk() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "Token"; @@ -378,10 +425,10 @@ public void testCreateEncodedTokenSecretSetsCredentialsOk() throws Exception { String token = "my-token"; String encodedToken = encoder.encodeToString(token.getBytes()); - JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, encodedToken); + JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, encodedToken, null); // When... - List errors = secretProcessor.processResource(secretJson, CREATE); + List errors = secretProcessor.processResource(secretJson, CREATE, requestUsername); // Then... assertThat(errors).isEmpty(); @@ -395,8 +442,10 @@ public void testCreateEncodedTokenSecretSetsCredentialsOk() throws Exception { @Test public void testCreateEncodedUsernameTokenSecretSetsCredentialsOk() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "UsernameToken"; @@ -408,10 +457,10 @@ public void testCreateEncodedUsernameTokenSecretSetsCredentialsOk() throws Excep String encodedUsername = encoder.encodeToString(username.getBytes()); String encodedToken = encoder.encodeToString(token.getBytes()); - JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, null, encodedToken); + JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, null, encodedToken, null); // When... - List errors = secretProcessor.processResource(secretJson, CREATE); + List errors = secretProcessor.processResource(secretJson, CREATE, requestUsername); // Then... assertThat(errors).isEmpty(); @@ -426,8 +475,10 @@ public void testCreateEncodedUsernameTokenSecretSetsCredentialsOk() throws Excep @Test public void testCreateEncodedUsernameSecretSetsCredentialsOk() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "Username"; @@ -440,7 +491,7 @@ public void testCreateEncodedUsernameSecretSetsCredentialsOk() throws Exception JsonObject secretJson = generateSecretJson(secretName, type, encoding, encodedUsername, null, null); // When... - List errors = secretProcessor.processResource(secretJson, CREATE); + List errors = secretProcessor.processResource(secretJson, CREATE, requestUsername); // Then... assertThat(errors).isEmpty(); @@ -454,6 +505,7 @@ public void testCreateEncodedUsernameSecretSetsCredentialsOk() throws Exception @Test public void testDeleteSecretDeletesCredentialsOk() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); String secretName = "ABC"; String username = "my-username"; Map existingCreds = new HashMap<>(); @@ -461,7 +513,8 @@ public void testDeleteSecretDeletesCredentialsOk() throws Exception { existingCreds.put("another-secret", new CredentialsUsername("another-username")); MockCredentialsService mockCreds = new MockCredentialsService(existingCreds); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String type = "Username"; String encoding = null; @@ -470,7 +523,7 @@ public void testDeleteSecretDeletesCredentialsOk() throws Exception { // When... assertThat(mockCreds.getAllCredentials()).hasSize(2); assertThat(mockCreds.getCredentials(secretName)).isNotNull(); - List errors = secretProcessor.processResource(secretJson, DELETE); + List errors = secretProcessor.processResource(secretJson, DELETE, requestUsername); // Then... assertThat(errors).isEmpty(); @@ -481,6 +534,7 @@ public void testDeleteSecretDeletesCredentialsOk() throws Exception { @Test public void testDeleteSecretDoesNotInsistOnData() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); String secretName = "ABC"; String username = "my-username"; Map existingCreds = new HashMap<>(); @@ -488,7 +542,8 @@ public void testDeleteSecretDoesNotInsistOnData() throws Exception { existingCreds.put("another-secret", new CredentialsUsername("another-username")); MockCredentialsService mockCreds = new MockCredentialsService(existingCreds); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String type = "Username"; String encoding = null; @@ -500,7 +555,7 @@ public void testDeleteSecretDoesNotInsistOnData() throws Exception { // When... assertThat(mockCreds.getAllCredentials()).hasSize(2); assertThat(mockCreds.getCredentials(secretName)).isNotNull(); - List errors = secretProcessor.processResource(secretJson, DELETE); + List errors = secretProcessor.processResource(secretJson, DELETE, requestUsername); // Then... assertThat(errors).isEmpty(); @@ -511,11 +566,13 @@ public void testDeleteSecretDoesNotInsistOnData() throws Exception { @Test public void testCreateSecretThatAlreadyExistsThrowsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); Map credsMap = new HashMap<>(); credsMap.put("ABC", new CredentialsUsername("my-username")); MockCredentialsService mockCreds = new MockCredentialsService(credsMap); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "Username"; String encoding = null; @@ -525,55 +582,59 @@ public void testCreateSecretThatAlreadyExistsThrowsError() throws Exception { // When... InternalServletException thrown = catchThrowableOfType(() -> { - secretProcessor.processResource(secretJson, CREATE); + secretProcessor.processResource(secretJson, CREATE, requestUsername); }, InternalServletException.class); // Then... assertThat(thrown).isNotNull(); checkErrorStructure(thrown.getMessage(), 5075, "GAL5075E", - "A secret with the provided ID already exists."); + "A secret with the provided name already exists."); } @Test public void testUpdateSecretThatDoesNotExistThrowsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "Token"; String encoding = null; String token = "another-token"; - JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token); + JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token, null); // When... InternalServletException thrown = catchThrowableOfType(() -> { - secretProcessor.processResource(secretJson, UPDATE); + secretProcessor.processResource(secretJson, UPDATE, requestUsername); }, InternalServletException.class); // Then... assertThat(thrown).isNotNull(); checkErrorStructure(thrown.getMessage(), 5076, "GAL5076E", - "A secret with the provided ID does not exist"); + "A secret with the provided name does not exist"); } @Test public void testApplySecretWithFailingCredsServiceThrowsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); mockCreds.setThrowError(true); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "Token"; String encoding = null; String token = "a-token"; - JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token); + JsonObject secretJson = generateSecretJson(secretName, type, encoding, null, null, token, null); // When... InternalServletException thrown = catchThrowableOfType(() -> { - secretProcessor.processResource(secretJson, APPLY); + secretProcessor.processResource(secretJson, APPLY, requestUsername); }, InternalServletException.class); // Then... @@ -585,10 +646,12 @@ public void testApplySecretWithFailingCredsServiceThrowsError() throws Exception @Test public void testDeleteSecretWithFailingCredsServiceThrowsError() throws Exception { // Given... + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); mockCreds.setThrowError(true); - GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; String secretName = "ABC"; String type = "Token"; String encoding = null; @@ -598,7 +661,7 @@ public void testDeleteSecretWithFailingCredsServiceThrowsError() throws Exceptio // When... InternalServletException thrown = catchThrowableOfType(() -> { - secretProcessor.processResource(secretJson, DELETE); + secretProcessor.processResource(secretJson, DELETE, requestUsername); }, InternalServletException.class); // Then... @@ -606,4 +669,57 @@ public void testDeleteSecretWithFailingCredsServiceThrowsError() throws Exceptio checkErrorStructure(thrown.getMessage(), 5078, "GAL5078E", "Failed to delete a secret with the given ID from the credentials store"); } + + @Test + public void testCreateUsernamePasswordSecretWithBlankDescriptionThrowsError() throws Exception { + // Given... + Instant lastUpdatedTime = Instant.EPOCH; + MockTimeService mockTimeService = new MockTimeService(lastUpdatedTime); + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = "my-username"; + String password = "a-password"; + String description = " "; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password, description); + + // When... + List errors = secretProcessor.processResource(secretJson, CREATE, requestUsername); + + // Then... + assertThat(errors).hasSize(1); + checkErrorStructure(errors.get(0), 5102, "GAL5102E", + "Invalid secret description provided"); + } + + @Test + public void testCreateUsernamePasswordSecretWithNonLatin1DescriptionThrowsError() throws Exception { + // Given... + Instant lastUpdatedTime = Instant.EPOCH; + MockTimeService mockTimeService = new MockTimeService(lastUpdatedTime); + MockCredentialsService mockCreds = new MockCredentialsService(new HashMap<>()); + GalasaSecretProcessor secretProcessor = new GalasaSecretProcessor(mockCreds, mockTimeService); + String requestUsername = "myuser"; + String secretName = "ABC"; + String type = "UsernamePassword"; + String encoding = null; + String username = "my-username"; + String password = "a-password"; + + // Latin-1 characters are in the 0-255 range, so set one that is outside this range + char nonLatin1Character = (char) 300; + String description = "this is my bad description " + nonLatin1Character; + JsonObject secretJson = generateSecretJson(secretName, type, encoding, username, password, description); + + // When... + List errors = secretProcessor.processResource(secretJson, CREATE, requestUsername); + + // Then... + assertThat(errors).hasSize(1); + checkErrorStructure(errors.get(0), 5102, "GAL5102E", + "Invalid secret description provided"); + } } 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 81994a37e..446f293fc 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 @@ -120,18 +120,19 @@ public void TestPathRegexMultipleForwardSlashPathReturnsFalse(){ @Test public void TestProcessDataArrayBadJsonArrayReturnsError() throws Exception{ //Given... + String username = "myuser"; 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, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null); String jsonString = "[{},{},{}]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, APPLY); + resourcesRoute.processDataArray(propertyJson, APPLY, username); List errors = resourcesRoute.errors; //Then... @@ -143,18 +144,19 @@ public void TestProcessDataArrayBadJsonArrayReturnsError() throws Exception{ @Test public void TestProcessDataArrayBadJsonReturnsError() throws Exception{ //Given... + String username = "myuser"; 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, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, 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, username); List errors = resourcesRoute.errors; //Then... @@ -166,18 +168,19 @@ public void TestProcessDataArrayBadJsonReturnsError() throws Exception{ @Test public void TestProcessDataArrayBadKindReturnsError() throws Exception{ //Given... + String username = "myuser"; 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, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, 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, username); List errors = resourcesRoute.errors; //Then... @@ -189,16 +192,17 @@ public void TestProcessDataArrayBadKindReturnsError() throws Exception{ @Test public void TestProcessDataArrayNullJsonObjectReturnsError() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null); String jsonString = "[null]"; JsonArray propertyJson = JsonParser.parseString(jsonString).getAsJsonArray(); //When... - resourcesRoute.processDataArray(propertyJson, APPLY); + resourcesRoute.processDataArray(propertyJson, APPLY, username); List errors = resourcesRoute.errors; //Then... @@ -209,17 +213,18 @@ public void TestProcessDataArrayNullJsonObjectReturnsError() throws Exception{ @Test public void TestProcessDataArrayCorrectJSONReturnsOK() throws Exception{ //Given... + String username = "myuser"; 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, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null); JsonArray propertyJson = generatePropertyArrayJson(namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... - resourcesRoute.processDataArray(propertyJson, APPLY); + resourcesRoute.processDataArray(propertyJson, APPLY, username); List errors = resourcesRoute.errors; //Then... @@ -230,19 +235,20 @@ public void TestProcessDataArrayCorrectJSONReturnsOK() throws Exception{ @Test public void TestProcessDataArrayThreeBadJsonReturnsErrors() throws Exception{ //Given... + String username = "myuser"; 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, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, 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, username); List errors = resourcesRoute.errors; //Then... @@ -257,6 +263,7 @@ public void TestProcessDataArrayThreeBadJsonReturnsErrors() throws Exception{ @Test public void TestProcessDataArrayCreateWithOneExistingRecordJSONReturnsOneError() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "value"; @@ -265,13 +272,13 @@ public void TestProcessDataArrayCreateWithOneExistingRecordJSONReturnsOneError() setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, 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, username); List errors = resourcesRoute.errors; //Then... @@ -285,6 +292,7 @@ public void TestProcessDataArrayCreateWithOneExistingRecordJSONReturnsOneError() @Test public void TestProcessDataArrayCreateWithTwoExistingRecordsJSONReturnsTwoErrors() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.1"; String value = "value"; @@ -293,13 +301,13 @@ public void TestProcessDataArrayCreateWithTwoExistingRecordsJSONReturnsTwoErrors setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, 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, username); List errors = resourcesRoute.errors; //Then... @@ -315,6 +323,7 @@ public void TestProcessDataArrayCreateWithTwoExistingRecordsJSONReturnsTwoErrors @Test public void TestProcessDataArrayUpdateWithOneNewRecordJSONReturnsOneError() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "value"; @@ -323,13 +332,13 @@ public void TestProcessDataArrayUpdateWithOneNewRecordJSONReturnsOneError() thro setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, 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, username); List errors = resourcesRoute.errors; //Then... @@ -342,6 +351,7 @@ public void TestProcessDataArrayUpdateWithOneNewRecordJSONReturnsOneError() thro @Test public void TestProcessDataArrayUpdateWithTwoNewRecordsJSONReturnsTwoError() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "value"; @@ -350,13 +360,13 @@ public void TestProcessDataArrayUpdateWithTwoNewRecordsJSONReturnsTwoError() thr setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, 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, username); List errors = resourcesRoute.errors; //Then... @@ -374,6 +384,7 @@ public void TestProcessDataArrayUpdateWithTwoNewRecordsJSONReturnsTwoError() thr @Test public void TestProcessRequestApplyActionReturnsOK() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "value"; @@ -381,11 +392,11 @@ public void TestProcessRequestApplyActionReturnsOK() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null); JsonObject requestJson = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... - resourcesRoute.processRequest(requestJson); + resourcesRoute.processRequest(requestJson, username); List errors = resourcesRoute.errors; //Then... @@ -396,6 +407,7 @@ public void TestProcessRequestApplyActionReturnsOK() throws Exception{ @Test public void TestProcessRequestCreateActionReturnsOK() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "value"; @@ -403,11 +415,11 @@ public void TestProcessRequestCreateActionReturnsOK() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null); JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... - resourcesRoute.processRequest(jsonString); + resourcesRoute.processRequest(jsonString, username); List errors = resourcesRoute.errors; //Then... @@ -418,6 +430,7 @@ public void TestProcessRequestCreateActionReturnsOK() throws Exception{ @Test public void TestProcessRequestUpdateActionReturnsOK() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.1"; String value = "value"; @@ -425,11 +438,11 @@ public void TestProcessRequestUpdateActionReturnsOK() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null); JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... - resourcesRoute.processRequest(jsonString); + resourcesRoute.processRequest(jsonString, username); List errors = resourcesRoute.errors; //Then... @@ -440,6 +453,7 @@ public void TestProcessRequestUpdateActionReturnsOK() throws Exception{ @Test public void TestProcessRequestBadActionReturnsError() throws Exception{ //Given... + String username = "myuser"; String namespace = "framework"; String propertyname = "property.name"; String value = "value"; @@ -447,12 +461,12 @@ public void TestProcessRequestBadActionReturnsError() throws Exception{ setServlet(namespace); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null); JsonObject jsonString = generateRequestJson(action, namespace,propertyname,value,"galasa-dev/v1alpha1"); //When... Throwable thrown = catchThrowable(() -> { - resourcesRoute.processRequest(jsonString); + resourcesRoute.processRequest(jsonString, username); }); //Then... @@ -973,7 +987,7 @@ public void TestGetErrorsAsJsonReturnsJsonString() throws Exception{ setServlet("framework"); MockResourcesServlet servlet = getServlet(); CPSFacade cps = new CPSFacade(servlet.getFramework()); - ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null); + ResourcesRoute resourcesRoute = new ResourcesRoute(null, cps, null, null, null); // When... String json = resourcesRoute.getErrorsAsJson(errors); diff --git a/galasa-parent/dev.galasa.framework.api.secrets/bnd.bnd b/galasa-parent/dev.galasa.framework.api.secrets/bnd.bnd new file mode 100644 index 000000000..7647aade3 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/bnd.bnd @@ -0,0 +1,7 @@ +-snapshot ${tstamp} +Bundle-Name: Galasa API Secrets microservices +Export-Package: dev.galasa.framework.api.secrets*; +Import-Package: dev.galasa.framework.api.common*,\ + dev.galasa.framework.api.beans,\ + * + diff --git a/galasa-parent/dev.galasa.framework.api.secrets/build.gradle b/galasa-parent/dev.galasa.framework.api.secrets/build.gradle new file mode 100644 index 000000000..9211f2b6f --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'biz.aQute.bnd.builder' + id 'galasa.api.server' +} + +description = 'Galasa API - Secrets' + +version = '0.38.0' + +dependencies { + implementation project(':dev.galasa.framework') + implementation project(':dev.galasa.framework.api.beans') + implementation project(':dev.galasa.framework.api.common') + testImplementation(testFixtures(project(':dev.galasa.framework.api.common'))) +} + +// Note: These values are consumed by the parent build process +// They indicate which packages of functionality this OSGi bundle should be delivered inside, +// or referenced from. +// The settings here are gathered together by the build process to create a release.yaml file +// which gathers-up all the packaging metadata about all the OSGi bundles in this component. +ext.projectName=project.name +ext.includeInOBR = true +ext.includeInMVP = false +ext.includeInIsolated = true +ext.includeInBOM = false +ext.includeInCodeCoverage = true +ext.includeInJavadoc = false + diff --git a/galasa-parent/dev.galasa.framework.api.secrets/settings.gradle b/galasa-parent/dev.galasa.framework.api.secrets/settings.gradle new file mode 100644 index 000000000..afaa62a7c --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "dev.galasa.framework.api.secrets" diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/SecretsServlet.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/SecretsServlet.java new file mode 100644 index 000000000..151883711 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/SecretsServlet.java @@ -0,0 +1,58 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets; + +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import dev.galasa.framework.api.common.BaseServlet; +import dev.galasa.framework.api.common.Environment; +import dev.galasa.framework.api.common.SystemEnvironment; +import dev.galasa.framework.api.secrets.internal.routes.SecretDetailsRoute; +import dev.galasa.framework.api.secrets.internal.routes.SecretsRoute; +import dev.galasa.framework.spi.IFramework; +import dev.galasa.framework.spi.creds.CredentialsException; +import dev.galasa.framework.spi.creds.ICredentialsService; +import dev.galasa.framework.spi.utils.ITimeService; +import dev.galasa.framework.spi.utils.SystemTimeService; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; + +/* + * REST API Servlet for the /secrets/* endpoints + */ +@Component(service = Servlet.class, scope = ServiceScope.PROTOTYPE, property = { +"osgi.http.whiteboard.servlet.pattern=/secrets/*" }, name = "Galasa Secrets microservice") +public class SecretsServlet extends BaseServlet { + + @Reference + protected IFramework framework; + + protected Environment env = new SystemEnvironment(); + protected ITimeService timeService = new SystemTimeService(); + + private static final long serialVersionUID = 1L; + + private Log logger = LogFactory.getLog(this.getClass()); + + @Override + public void init() throws ServletException { + logger.info("Secrets servlet initialising"); + + try { + ICredentialsService credentialsService = framework.getCredentialsService(); + addRoute(new SecretsRoute(getResponseBuilder(), credentialsService, env, timeService)); + addRoute(new SecretDetailsRoute(getResponseBuilder(), credentialsService, env, timeService)); + } catch (CredentialsException e) { + throw new ServletException("Failed to initialise the Secrets servlet"); + } + logger.info("Secrets servlet initialised"); + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/SecretRequestValidator.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/SecretRequestValidator.java new file mode 100644 index 000000000..a7a0496ce --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/SecretRequestValidator.java @@ -0,0 +1,77 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; + +import javax.servlet.http.HttpServletResponse; + +import dev.galasa.framework.api.beans.generated.SecretRequest; +import dev.galasa.framework.api.beans.generated.SecretRequestpassword; +import dev.galasa.framework.api.beans.generated.SecretRequesttoken; +import dev.galasa.framework.api.beans.generated.SecretRequestusername; +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.ServletError; +import dev.galasa.framework.api.common.resources.SecretValidator; + +public class SecretRequestValidator extends SecretValidator { + + @Override + public void validate(SecretRequest secretRequest) throws InternalServletException { + SecretRequestusername username = secretRequest.getusername(); + SecretRequestpassword password = secretRequest.getpassword(); + SecretRequesttoken token = secretRequest.gettoken(); + + // Check that the secret has been given a name + validateSecretName(secretRequest.getname()); + + validateDescription(secretRequest.getdescription()); + + // Password and token are mutually exclusive, so error if both are provided + if (password != null && token != null) { + ServletError error = new ServletError(GAL5095_ERROR_PASSWORD_AND_TOKEN_PROVIDED); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + + // Password cannot be specified on its own + if (username == null && password != null) { + ServletError error = new ServletError(GAL5098_ERROR_PASSWORD_MISSING_USERNAME); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + + validateSecretRequestFields(username, password, token); + } + + protected void validateSecretRequestFields( + SecretRequestusername username, + SecretRequestpassword password, + SecretRequesttoken token + ) throws InternalServletException { + if (username != null) { + validateField(username.getvalue(), username.getencoding()); + } + + if (password != null) { + validateField(password.getvalue(), password.getencoding()); + } + + if (token != null) { + validateField(token.getvalue(), token.getencoding()); + } + } + + private void validateField(String value, String encoding) throws InternalServletException { + if (encoding != null && !SUPPORTED_ENCODING_SCHEMES.contains(encoding)) { + ServletError error = new ServletError(GAL5073_UNSUPPORTED_GALASA_SECRET_ENCODING, String.join(", ", SUPPORTED_ENCODING_SCHEMES)); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + + if (value == null || value.isBlank()) { + ServletError error = new ServletError(GAL5096_ERROR_MISSING_SECRET_VALUE); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/UpdateSecretRequestValidator.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/UpdateSecretRequestValidator.java new file mode 100644 index 000000000..ba600f2b6 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/UpdateSecretRequestValidator.java @@ -0,0 +1,130 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletResponse; + +import com.google.gson.JsonObject; + +import dev.galasa.framework.api.beans.generated.SecretRequest; +import dev.galasa.framework.api.beans.generated.SecretRequestpassword; +import dev.galasa.framework.api.beans.generated.SecretRequesttoken; +import dev.galasa.framework.api.beans.generated.SecretRequestusername; +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.spi.utils.GalasaGson; + +public class UpdateSecretRequestValidator extends SecretRequestValidator { + + private static final GalasaGson gson = new GalasaGson(); + + private GalasaSecretType existingSecretType; + + public UpdateSecretRequestValidator(GalasaSecretType existingSecretType) { + this.existingSecretType = existingSecretType; + } + + @Override + public void validate(SecretRequest secretRequest) throws InternalServletException { + if (existingSecretType == null) { + validateCreateSecretRequest(secretRequest); + } else { + validateUpdateSecretRequest(secretRequest); + } + } + + private void validateCreateSecretRequest(SecretRequest secretRequest) throws InternalServletException { + SecretRequestusername username = secretRequest.getusername(); + SecretRequestpassword password = secretRequest.getpassword(); + SecretRequesttoken token = secretRequest.gettoken(); + + validateDescription(secretRequest.getdescription()); + + // Password and token are mutually exclusive, so error if both are provided + if (password != null && token != null) { + ServletError error = new ServletError(GAL5095_ERROR_PASSWORD_AND_TOKEN_PROVIDED); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + + // Password cannot be specified on its own + if (username == null && password != null) { + ServletError error = new ServletError(GAL5098_ERROR_PASSWORD_MISSING_USERNAME); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + + validateSecretRequestFields(username, password, token); + } + + private void validateUpdateSecretRequest(SecretRequest secretRequest) throws InternalServletException { + SecretRequestusername username = secretRequest.getusername(); + SecretRequestpassword password = secretRequest.getpassword(); + SecretRequesttoken token = secretRequest.gettoken(); + + validateDescription(secretRequest.getdescription()); + + // Password and token are mutually exclusive, so error if both are provided + if (password != null && token != null) { + ServletError error = new ServletError(GAL5095_ERROR_PASSWORD_AND_TOKEN_PROVIDED); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + + String requestedType = secretRequest.gettype(); + if (existingSecretType != null && requestedType == null) { + // The user intends to update an existing secret without changing its type + // so make sure that the relevant fields have been supplied + checkProvidedSecretFieldsAreRelevant(existingSecretType, secretRequest); + } else if (requestedType != null) { + GalasaSecretType secretType = GalasaSecretType.getFromString(requestedType.toString()); + if (secretType == null) { + // An unknown type was provided, so throw an error + String supportedSecretTypes = Arrays.stream(GalasaSecretType.values()) + .map(GalasaSecretType::toString) + .collect(Collectors.joining(", ")); + ServletError error = new ServletError(GAL5074_UNKNOWN_GALASA_SECRET_TYPE, supportedSecretTypes); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + // A specific type of secret was given, so make sure that all of the required fields + // for the given type have been provided + validateSecretTypeFields(secretType, secretRequest); + checkProvidedSecretFieldsAreRelevant(secretType, secretRequest); + } + validateSecretRequestFields(username, password, token); + } + + private void validateSecretTypeFields(GalasaSecretType secretType, SecretRequest secretRequest) throws InternalServletException { + JsonObject secretRequestJson = gson.toJsonTree(secretRequest).getAsJsonObject(); + for (String requiredField : secretType.getRequiredDataFields()) { + if (!secretRequestJson.has(requiredField)) { + ServletError error = new ServletError(GAL5099_ERROR_MISSING_REQUIRED_SECRET_FIELD, secretType.toString(), requiredField); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + } + } + + private void checkProvidedSecretFieldsAreRelevant(GalasaSecretType secretType, SecretRequest secretRequest) throws InternalServletException { + JsonObject secretRequestJson = gson.toJsonTree(secretRequest).getAsJsonObject(); + Set secretRequestFields = secretRequestJson.keySet() + .stream() + .filter(key -> !key.equals("name") && !key.equals("type") && !key.equals("description")) + .collect(Collectors.toSet()); + + List requiredTypeFields = Arrays.asList(secretType.getRequiredDataFields()); + for (String field : secretRequestFields) { + if (!requiredTypeFields.contains(field)) { + ServletError error = new ServletError(GAL5100_ERROR_UNEXPECTED_SECRET_FIELD_PROVIDED, secretType.toString(), String.join(", ", requiredTypeFields)); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + } + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/AbstractSecretsRoute.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/AbstractSecretsRoute.java new file mode 100644 index 000000000..ae777ea47 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/AbstractSecretsRoute.java @@ -0,0 +1,198 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal.routes; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; +import static dev.galasa.framework.api.beans.generated.GalasaSecretType.*; + +import java.time.Instant; +import java.util.Base64; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import dev.galasa.ICredentials; +import dev.galasa.ICredentialsToken; +import dev.galasa.ICredentialsUsername; +import dev.galasa.ICredentialsUsernamePassword; +import dev.galasa.ICredentialsUsernameToken; +import dev.galasa.framework.api.beans.generated.GalasaSecret; +import dev.galasa.framework.api.beans.generated.GalasaSecretdata; +import dev.galasa.framework.api.beans.generated.GalasaSecretmetadata; +import dev.galasa.framework.api.beans.generated.SecretRequest; +import dev.galasa.framework.api.beans.generated.SecretRequestpassword; +import dev.galasa.framework.api.beans.generated.SecretRequesttoken; +import dev.galasa.framework.api.beans.generated.SecretRequestusername; +import dev.galasa.framework.api.common.BaseRoute; +import dev.galasa.framework.api.common.Environment; +import dev.galasa.framework.api.common.InternalServletException; +import dev.galasa.framework.api.common.JwtWrapper; +import dev.galasa.framework.api.common.ResponseBuilder; +import dev.galasa.framework.api.common.ServletError; +import dev.galasa.framework.api.common.resources.GalasaResourceValidator; +import dev.galasa.framework.api.common.resources.GalasaSecretType; +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.utils.ITimeService; + +public abstract class AbstractSecretsRoute extends BaseRoute { + + private static final String DEFAULT_RESPONSE_ENCODING = "base64"; + + private Environment env; + protected ITimeService timeService; + + private static final Map, GalasaSecretType> credentialsToSecretTypes = Map.of( + CredentialsUsername.class, GalasaSecretType.USERNAME, + CredentialsToken.class, GalasaSecretType.TOKEN, + CredentialsUsernamePassword.class, GalasaSecretType.USERNAME_PASSWORD, + CredentialsUsernameToken.class, GalasaSecretType.USERNAME_TOKEN + ); + + public AbstractSecretsRoute(ResponseBuilder responseBuilder, String path, Environment env, ITimeService timeService) { + super(responseBuilder, path); + this.env = env; + this.timeService = timeService; + } + + protected GalasaSecret createGalasaSecretFromCredentials(String secretName, ICredentials credentials) throws InternalServletException { + GalasaSecretmetadata metadata = new GalasaSecretmetadata(null); + GalasaSecretdata data = new GalasaSecretdata(); + + metadata.setname(secretName); + metadata.setencoding(DEFAULT_RESPONSE_ENCODING); + setSecretTypeValuesFromCredentials(metadata, data, credentials); + setSecretMetadata(metadata, credentials.getDescription(), credentials.getLastUpdatedByUser(), credentials.getLastUpdatedTime()); + GalasaSecret secret = new GalasaSecret(); + secret.setApiVersion(GalasaResourceValidator.DEFAULT_API_VERSION); + secret.setdata(data); + secret.setmetadata(metadata); + + return secret; + } + + protected ICredentials buildDecodedCredentialsToSet(SecretRequest secretRequest, String lastUpdatedByUser) throws InternalServletException { + ICredentials decodedSecret = decodeCredentialsFromSecretPayload(secretRequest); + setSecretMetadataProperties(decodedSecret, secretRequest.getdescription(), lastUpdatedByUser); + return decodedSecret; + } + + private ICredentials decodeCredentialsFromSecretPayload(SecretRequest secretRequest) throws InternalServletException { + ICredentials credentials = null; + SecretRequestusername username = secretRequest.getusername(); + SecretRequestpassword password = secretRequest.getpassword(); + SecretRequesttoken token = secretRequest.gettoken(); + + if (username != null) { + String decodedUsername = decodeSecretValue(username.getvalue(), username.getencoding()); + if (password != null) { + // We have a username and password + String decodedPassword = decodeSecretValue(password.getvalue(), password.getencoding()); + credentials = new CredentialsUsernamePassword(decodedUsername, decodedPassword); + + } else if (token != null) { + // We have a username and token + String decodedToken = decodeSecretValue(token.getvalue(), token.getencoding()); + credentials = new CredentialsUsernameToken(decodedUsername, decodedToken); + } else { + // We have a username + credentials = new CredentialsUsername(decodedUsername); + } + } else if (token != null) { + // We have a token + String decodedToken = decodeSecretValue(token.getvalue(), token.getencoding()); + credentials = new CredentialsToken(decodedToken); + } + return credentials; + } + + protected String decodeSecretValue(String possiblyEncodedValue, String encoding) throws InternalServletException { + String decodedValue = possiblyEncodedValue; + if (encoding != null && possiblyEncodedValue != null) { + try { + if (encoding.equalsIgnoreCase(DEFAULT_RESPONSE_ENCODING)) { + byte[] decodedBytes = Base64.getDecoder().decode(possiblyEncodedValue); + decodedValue = new String(decodedBytes); + } + } catch (IllegalArgumentException e) { + ServletError error = new ServletError(GAL5097_FAILED_TO_DECODE_SECRET_VALUE, DEFAULT_RESPONSE_ENCODING); + throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); + } + } + return decodedValue; + } + + private void setSecretTypeValuesFromCredentials(GalasaSecretmetadata metadata, GalasaSecretdata data, ICredentials credentials) throws InternalServletException { + GalasaSecretType secretType = getSecretType(credentials); + if (secretType == GalasaSecretType.USERNAME) { + ICredentialsUsername usernameCredentials = (ICredentialsUsername) credentials; + data.setusername(encodeValue(usernameCredentials.getUsername())); + + metadata.settype(Username); + } else if (secretType == GalasaSecretType.USERNAME_PASSWORD) { + ICredentialsUsernamePassword usernamePasswordCredentials = (ICredentialsUsernamePassword) credentials; + data.setusername(encodeValue(usernamePasswordCredentials.getUsername())); + data.setpassword(encodeValue(usernamePasswordCredentials.getPassword())); + + metadata.settype(USERNAME_PASSWORD); + } else if (secretType == GalasaSecretType.USERNAME_TOKEN) { + ICredentialsUsernameToken usernameTokenCredentials = (ICredentialsUsernameToken) credentials; + data.setusername(encodeValue(usernameTokenCredentials.getUsername())); + data.settoken(encodeValue(new String(usernameTokenCredentials.getToken()))); + + metadata.settype(USERNAME_TOKEN); + } else if (secretType == GalasaSecretType.TOKEN) { + ICredentialsToken tokenCredentials = (ICredentialsToken) credentials; + data.settoken(encodeValue(new String(tokenCredentials.getToken()))); + + metadata.settype(Token); + } else { + // The credentials are in an unknown format, throw an error + ServletError error = new ServletError(GAL5101_ERROR_UNEXPECTED_SECRET_TYPE_DETECTED); + throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + private void setSecretMetadata(GalasaSecretmetadata metadata, String description, String username, Instant timestamp) { + metadata.setdescription(description); + metadata.setLastUpdatedBy(username); + + if (timestamp != null) { + metadata.setLastUpdatedTime(timestamp.toString()); + } + } + + private String encodeValue(String value) { + String encodedValue = value; + if (DEFAULT_RESPONSE_ENCODING.equals("base64")) { + encodedValue = Base64.getEncoder().encodeToString(value.getBytes()); + } + return encodedValue; + } + + protected GalasaSecretType getSecretType(ICredentials existingSecret) { + GalasaSecretType existingSecretType = null; + if (existingSecret != null) { + existingSecretType = credentialsToSecretTypes.get(existingSecret.getClass()); + } + return existingSecretType; + } + + protected String getUsernameFromRequestJwt(HttpServletRequest request) throws InternalServletException { + return new JwtWrapper(request, env).getUsername(); + } + + protected void setSecretMetadataProperties(ICredentials secret, String description, String lastUpdatedByUser) { + if (description != null && !description.isBlank()) { + secret.setDescription(description); + } + secret.setLastUpdatedByUser(lastUpdatedByUser); + secret.setLastUpdatedTime(timeService.now()); + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretDetailsRoute.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretDetailsRoute.java new file mode 100644 index 000000000..3707223ae --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretDetailsRoute.java @@ -0,0 +1,254 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal.routes; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; +import static dev.galasa.framework.api.common.resources.GalasaSecretType.*; + +import java.io.IOException; +import java.util.regex.Matcher; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import dev.galasa.ICredentials; +import dev.galasa.framework.api.beans.generated.GalasaSecret; +import dev.galasa.framework.api.beans.generated.SecretRequest; +import dev.galasa.framework.api.beans.generated.SecretRequestpassword; +import dev.galasa.framework.api.beans.generated.SecretRequesttoken; +import dev.galasa.framework.api.beans.generated.SecretRequestusername; +import dev.galasa.framework.api.common.Environment; +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.GalasaSecretType; +import dev.galasa.framework.api.secrets.internal.SecretRequestValidator; +import dev.galasa.framework.api.secrets.internal.UpdateSecretRequestValidator; +import dev.galasa.framework.spi.FrameworkException; +import dev.galasa.framework.spi.creds.CredentialsException; +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; +import dev.galasa.framework.spi.utils.ITimeService; + +public class SecretDetailsRoute extends AbstractSecretsRoute { + + // Regex to match /secrets/{secret-id} and /secrets/{secret-id}/ + // where {secret-id} can consist of the following characters: + // - Alphanumeric characters (a-zA-Z0-9) + // - Underscores (_) + // - Dashes (-) + private static final String PATH_PATTERN = "\\/([a-zA-Z0-9_-]+)\\/?"; + + private ICredentialsService credentialsService; + + private Log logger = LogFactory.getLog(getClass()); + + public SecretDetailsRoute( + ResponseBuilder responseBuilder, + ICredentialsService credentialsService, + Environment env, + ITimeService timeService + ) { + super(responseBuilder, PATH_PATTERN, env, timeService); + this.credentialsService = credentialsService; + } + + @Override + public HttpServletResponse handleGetRequest( + String pathInfo, + QueryParameters queryParams, + HttpServletRequest request, + HttpServletResponse response + ) throws FrameworkException { + logger.info("handleGetRequest() entered. Getting secret with the given name"); + String secretName = getSecretNameFromPath(pathInfo); + GalasaSecret secret = getSecretByName(secretName); + + logger.info("handleGetRequest() exiting"); + return getResponseBuilder().buildResponse(request, response, "application/json", + gson.toJson(secret), HttpServletResponse.SC_OK); + } + + + @Override + public HttpServletResponse handlePutRequest( + String pathInfo, + QueryParameters queryParams, + HttpServletRequest request, + HttpServletResponse response + ) throws FrameworkException, IOException { + logger.info("handlePutRequest() entered. Validating request payload"); + checkRequestHasContent(request); + + String secretName = getSecretNameFromPath(pathInfo); + String lastUpdatedByUser = getUsernameFromRequestJwt(request); + SecretRequest secretPayload = parseRequestBody(request, SecretRequest.class); + + ICredentials existingSecret = credentialsService.getCredentials(secretName); + GalasaSecretType existingSecretType = getSecretType(existingSecret); + validateUpdateRequest(existingSecretType, secretPayload, existingSecret); + + ICredentials decodedSecret = null; + int responseCode = HttpServletResponse.SC_NO_CONTENT; + if (existingSecret == null) { + // No secret with the given name exists, so create a new one + decodedSecret = buildDecodedCredentialsToSet(secretPayload, lastUpdatedByUser); + responseCode = HttpServletResponse.SC_CREATED; + } else if (secretPayload.gettype() != null) { + // When a secret type is given, all relevant fields for that type are required, + // so overwrite the existing secret to change its type + decodedSecret = buildDecodedCredentialsToSet(secretPayload, lastUpdatedByUser); + } else { + // A secret already exists and no type was given, so just update the secret by + // overriding its existing values with the values provided in the request + decodedSecret = getOverriddenSecret(existingSecretType, existingSecret, secretPayload); + String description = getOverriddenValue(existingSecret.getDescription(), secretPayload.getdescription()); + setSecretMetadataProperties(decodedSecret, description, lastUpdatedByUser); + } + credentialsService.setCredentials(secretName, decodedSecret); + + logger.info("handlePutRequest() exiting"); + return getResponseBuilder().buildResponse(request, response, responseCode); + } + + @Override + public HttpServletResponse handleDeleteRequest( + String pathInfo, + QueryParameters queryParams, + HttpServletRequest request, + HttpServletResponse response + ) throws FrameworkException { + logger.info("handleDeleteRequest() entered"); + // The provided name is implicitly validated by the route's regex pattern + String secretName = getSecretNameFromPath(pathInfo); + deleteSecret(secretName); + + logger.info("handleDeleteRequest() exiting"); + return getResponseBuilder().buildResponse(request, response, HttpServletResponse.SC_NO_CONTENT); + } + + private void validateUpdateRequest(GalasaSecretType existingSecretType, SecretRequest secretPayload, ICredentials existingSecret) throws InternalServletException { + SecretRequestValidator updateSecretValidator = new UpdateSecretRequestValidator(existingSecretType); + updateSecretValidator.validate(secretPayload); + + logger.info("Request payload validated OK"); + } + + private String getSecretNameFromPath(String pathInfo) throws InternalServletException { + Matcher matcher = this.getPath().matcher(pathInfo); + matcher.matches(); + return matcher.group(1); + } + + private GalasaSecret getSecretByName(String secretName) throws InternalServletException { + GalasaSecret secret = null; + try { + ICredentials credentials = credentialsService.getCredentials(secretName); + + if (credentials == null) { + ServletError error = new ServletError(GAL5093_ERROR_SECRET_NOT_FOUND); + throw new InternalServletException(error, HttpServletResponse.SC_NOT_FOUND); + } + + logger.info("A secret with the given name was found OK"); + + secret = createGalasaSecretFromCredentials(secretName, credentials); + } catch (CredentialsException e) { + ServletError error = new ServletError(GAL5094_FAILED_TO_GET_SECRET_FROM_CREDS); + throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + return secret; + } + + private void deleteSecret(String secretName) throws InternalServletException { + try { + if (credentialsService.getCredentials(secretName) == null) { + ServletError error = new ServletError(GAL5076_ERROR_SECRET_DOES_NOT_EXIST); + throw new InternalServletException(error, HttpServletResponse.SC_NOT_FOUND); + } + logger.info("Attempting to delete the secret with the given name"); + + credentialsService.deleteCredentials(secretName); + + logger.info("The secret with the given name was deleted OK"); + + } catch (CredentialsException e) { + ServletError error = new ServletError(GAL5078_FAILED_TO_DELETE_SECRET); + throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + private ICredentials getOverriddenSecret(GalasaSecretType existingSecretType, ICredentials existingSecret, SecretRequest secretRequest) throws InternalServletException { + ICredentials overriddenSecret = existingSecret; + + if (existingSecretType == USERNAME) { + CredentialsUsername usernameSecret = (CredentialsUsername) existingSecret; + String overriddenUsername = getOverriddenUsername(usernameSecret.getUsername(), secretRequest.getusername()); + overriddenSecret = new CredentialsUsername(overriddenUsername); + } else if (existingSecretType == TOKEN) { + CredentialsToken tokenSecret = (CredentialsToken) existingSecret; + String overriddenToken = getOverriddenToken(new String(tokenSecret.getToken()), secretRequest.gettoken()); + overriddenSecret = new CredentialsToken(overriddenToken); + } else if (existingSecretType == USERNAME_PASSWORD) { + CredentialsUsernamePassword usernamePasswordSecret = (CredentialsUsernamePassword) existingSecret; + String overriddenUsername = getOverriddenUsername(usernamePasswordSecret.getUsername(), secretRequest.getusername()); + String overriddenPassword = getOverriddenPassword(usernamePasswordSecret.getPassword(), secretRequest.getpassword()); + overriddenSecret = new CredentialsUsernamePassword(overriddenUsername, overriddenPassword); + } else if (existingSecretType == USERNAME_TOKEN) { + CredentialsUsernameToken usernameTokenSecret = (CredentialsUsernameToken) existingSecret; + String overriddenUsername = getOverriddenUsername(usernameTokenSecret.getUsername(), secretRequest.getusername()); + String overriddenToken = getOverriddenToken(new String(usernameTokenSecret.getToken()), secretRequest.gettoken()); + overriddenSecret = new CredentialsUsernameToken(overriddenUsername, overriddenToken); + } else { + // The credentials are in an unknown format, throw an error + ServletError error = new ServletError(GAL5101_ERROR_UNEXPECTED_SECRET_TYPE_DETECTED); + throw new InternalServletException(error, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + return overriddenSecret; + } + + private String getOverriddenValue(String originalValue, String possibleOverride) { + String newValue = originalValue; + if (possibleOverride != null) { + newValue = possibleOverride; + } + return newValue; + } + + private String getOverriddenUsername(String existingUsername, SecretRequestusername requestUsername) throws InternalServletException { + String overriddenUsername = existingUsername; + if (requestUsername != null) { + String possiblyDecodedUsername = decodeSecretValue(requestUsername.getvalue(), requestUsername.getencoding()); + overriddenUsername = getOverriddenValue(existingUsername, possiblyDecodedUsername); + } + return overriddenUsername; + } + + private String getOverriddenPassword(String existingPassword, SecretRequestpassword requestPassword) throws InternalServletException { + String overriddenPassword = existingPassword; + if (requestPassword != null) { + String possiblyDecodedPassword = decodeSecretValue(requestPassword.getvalue(), requestPassword.getencoding()); + overriddenPassword = getOverriddenValue(existingPassword, possiblyDecodedPassword); + } + return overriddenPassword; + } + + private String getOverriddenToken(String existingToken, SecretRequesttoken requestToken) throws InternalServletException { + String overriddenToken = existingToken; + if (requestToken != null) { + String possiblyDecodedToken = decodeSecretValue(requestToken.getvalue(), requestToken.getencoding()); + overriddenToken = getOverriddenValue(existingToken, possiblyDecodedToken); + } + return overriddenToken; + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretsRoute.java b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretsRoute.java new file mode 100644 index 000000000..4aa45227d --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/main/java/dev/galasa/framework/api/secrets/internal/routes/SecretsRoute.java @@ -0,0 +1,110 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal.routes; + +import static dev.galasa.framework.api.common.ServletErrorMessage.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import dev.galasa.ICredentials; +import dev.galasa.framework.api.beans.generated.GalasaSecret; +import dev.galasa.framework.api.beans.generated.SecretRequest; +import dev.galasa.framework.api.common.Environment; +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.secrets.internal.SecretRequestValidator; +import dev.galasa.framework.spi.FrameworkException; +import dev.galasa.framework.spi.creds.ICredentialsService; +import dev.galasa.framework.spi.utils.ITimeService; + +public class SecretsRoute extends AbstractSecretsRoute { + + // Regex to match /secrets and /secrets/ only + private static final String PATH_PATTERN = "\\/?"; + + private ICredentialsService credentialsService; + private SecretRequestValidator createSecretValidator = new SecretRequestValidator(); + + private Log logger = LogFactory.getLog(getClass()); + + public SecretsRoute( + ResponseBuilder responseBuilder, + ICredentialsService credentialsService, + Environment env, + ITimeService timeService) { + super(responseBuilder, PATH_PATTERN, env, timeService); + this.credentialsService = credentialsService; + } + + @Override + public HttpServletResponse handleGetRequest( + String pathInfo, + QueryParameters queryParams, + HttpServletRequest request, + HttpServletResponse response + ) throws FrameworkException { + logger.info("handleGetRequest() entered. Getting secrets from the credentials store"); + List secrets = new ArrayList<>(); + Map retrievedCredentials = credentialsService.getAllCredentials(); + + if (!retrievedCredentials.isEmpty()) { + for (Entry entry : retrievedCredentials.entrySet()) { + GalasaSecret secret = createGalasaSecretFromCredentials(entry.getKey(), entry.getValue()); + secrets.add(secret); + } + logger.info("Secrets retrieved from credentials store OK"); + } else { + logger.info("No secrets found in the credentials store"); + } + + logger.info("handleGetRequest() exiting"); + return getResponseBuilder().buildResponse(request, response, "application/json", + gson.toJson(secrets), HttpServletResponse.SC_OK); + } + + @Override + public HttpServletResponse handlePostRequest( + String pathInfo, + QueryParameters queryParams, + HttpServletRequest request, + HttpServletResponse response + ) throws FrameworkException, IOException { + logger.info("handlePostRequest() entered. Validating request payload"); + checkRequestHasContent(request); + SecretRequest secretPayload = parseRequestBody(request, SecretRequest.class); + createSecretValidator.validate(secretPayload); + + logger.info("Request payload validated"); + + // Check if a secret with the given name already exists, throwing an error if so + String secretName = secretPayload.getname(); + if (credentialsService.getCredentials(secretName) != null) { + ServletError error = new ServletError(GAL5075_ERROR_SECRET_ALREADY_EXISTS); + throw new InternalServletException(error, HttpServletResponse.SC_CONFLICT); + } + + logger.info("Setting secret in credentials store"); + String lastUpdatedByUser = getUsernameFromRequestJwt(request); + ICredentials decodedSecret = buildDecodedCredentialsToSet(secretPayload, lastUpdatedByUser); + credentialsService.setCredentials(secretName, decodedSecret); + + logger.info("Secret set in credentials store OK"); + logger.info("handlePostRequest() exiting"); + return getResponseBuilder().buildResponse(request, response, HttpServletResponse.SC_CREATED); + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/MockCredentials.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/MockCredentials.java new file mode 100644 index 000000000..0f7925f5b --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/MockCredentials.java @@ -0,0 +1,54 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal; + +import java.time.Instant; +import java.util.Properties; + +import dev.galasa.ICredentials; + +class MockCredentials implements ICredentials { + + @Override + public Properties toProperties(String credentialsId) { + throw new UnsupportedOperationException("Unimplemented method 'toProperties'"); + } + + @Override + public void setDescription(String description) { + throw new UnsupportedOperationException("Unimplemented method 'setDescription'"); + } + + @Override + public void setLastUpdatedByUser(String username) { + throw new UnsupportedOperationException("Unimplemented method 'setLastUpdatedByUser'"); + } + + @Override + public void setLastUpdatedTime(Instant time) { + throw new UnsupportedOperationException("Unimplemented method 'setLastUpdatedTime'"); + } + + @Override + public String getDescription() { + throw new UnsupportedOperationException("Unimplemented method 'getDescription'"); + } + + @Override + public String getLastUpdatedByUser() { + throw new UnsupportedOperationException("Unimplemented method 'getLastUpdatedByUser'"); + } + + @Override + public Instant getLastUpdatedTime() { + throw new UnsupportedOperationException("Unimplemented method 'getLastUpdatedTime'"); + } + + @Override + public Properties getMetadataProperties(String credentialsId) { + throw new UnsupportedOperationException("Unimplemented method 'getMetadataProperties'"); + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java new file mode 100644 index 000000000..153ef4339 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretDetailsRouteTest.java @@ -0,0 +1,1061 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal; + +import static org.assertj.core.api.Assertions.*; +import static dev.galasa.framework.api.common.resources.GalasaSecretType.*; + +import java.time.Instant; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.servlet.ServletOutputStream; + +import org.junit.Test; + +import com.google.gson.JsonObject; + +import dev.galasa.ICredentials; +import dev.galasa.framework.api.common.HttpMethod; +import dev.galasa.framework.api.common.mocks.MockCredentialsService; +import dev.galasa.framework.api.common.mocks.MockFramework; +import dev.galasa.framework.api.common.mocks.MockHttpServletRequest; +import dev.galasa.framework.api.common.mocks.MockHttpServletResponse; +import dev.galasa.framework.api.common.mocks.MockTimeService; +import dev.galasa.framework.api.secrets.internal.routes.SecretDetailsRoute; +import dev.galasa.framework.api.secrets.mocks.MockSecretsServlet; +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 SecretDetailsRouteTest extends SecretsServletTest { + + @Test + public void testSecretDetailsRouteRegexMatchesExpectedPaths() throws Exception { + // Given... + Pattern routePattern = new SecretDetailsRoute(null, null, null, null).getPath(); + + // Then... + // The servlet's whiteboard pattern will match /secrets, so this route should + // allow the name of a secret to be provided + assertThat(routePattern.matcher("/MYSECRET").matches()).isTrue(); + assertThat(routePattern.matcher("/MYSECRET/").matches()).isTrue(); + assertThat(routePattern.matcher("/mysecret").matches()).isTrue(); + assertThat(routePattern.matcher("/myS3cret123").matches()).isTrue(); + assertThat(routePattern.matcher("/My-Secret_456").matches()).isTrue(); + + // The route should not accept the following + assertThat(routePattern.matcher("/My-Secret.456").matches()).isFalse(); + assertThat(routePattern.matcher("/123My.Secret.456").matches()).isFalse(); + assertThat(routePattern.matcher("////").matches()).isFalse(); + assertThat(routePattern.matcher("").matches()).isFalse(); + assertThat(routePattern.matcher("/my secret").matches()).isFalse(); + assertThat(routePattern.matcher("/thisisbad").matches()).isFalse(); + assertThat(routePattern.matcher("/javascript:thisisbad;").matches()).isFalse(); + } + + @Test + public void testGetSecretByNameReturnsSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String username = "my-user"; + creds.put(secretName, new CredentialsUsername(username)); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doGet(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(200); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + + String expectedJson = gson.toJson(generateSecretJson(secretName, "Username", username, null, null)); + assertThat(outStream.toString()).isEqualTo(expectedJson); + } + + @Test + public void testGetNonExistantSecretByNameReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "NON_EXISTANT_SECRET"; + creds.put("BOB", new CredentialsUsername("my-user")); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doGet(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(404); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5093, "GAL5093E", + "Unable to retrieve a secret with the given name"); + } + + @Test + public void testGetSecretByNameWithFailingCredsStoreReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + MockCredentialsService credsService = new MockCredentialsService(creds); + + // Force a server error from the creds service + credsService.setThrowError(true); + + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS); + mockRequest.setQueryParameter("name", secretName); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doGet(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(500); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5094, "GAL5094E", + "Failed to retrieve a secret with the given name from the credentials store"); + } + + @Test + public void testDeleteSecretDeletesSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + creds.put(secretName, new CredentialsUsername("my-user")); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS); + mockRequest.setMethod(HttpMethod.DELETE.toString()); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doDelete(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + } + + @Test + public void testDeleteNonExistantSecretReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "NON_EXISTANT_SECRET"; + creds.put("BOB", new CredentialsUsername("my-user")); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS); + mockRequest.setMethod(HttpMethod.DELETE.toString()); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doDelete(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(404); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5076, "GAL5076E", + "Error occurred. A secret with the provided name does not exist."); + } + + @Test + public void testDeleteSecretWithFailingCredsStoreReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + MockCredentialsService credsService = new MockCredentialsService(creds); + + // Force a server error from the creds service + credsService.setThrowError(true); + + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, REQUEST_HEADERS); + mockRequest.setMethod(HttpMethod.DELETE.toString()); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doDelete(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(500); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5078, "GAL5078E", + "Failed to delete a secret with the given ID from the credentials store"); + } + + @Test + public void testUpdateSecretUsernameUpdatesSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-username"; + String oldPassword = "not-a-password"; + String newUsername = "my-new-username"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsUsernamePassword(oldUsername, oldPassword)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("username", createSecretJson(newUsername)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsUsernamePassword updatedCredentials = (CredentialsUsernamePassword) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getUsername()).isEqualTo(newUsername); + assertThat(updatedCredentials.getPassword()).isEqualTo(oldPassword); + } + + @Test + public void testUpdateSecretUsernamePasswordUpdatesSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-username"; + String oldPassword = "not-a-password"; + String newUsername = "my-new-username"; + String newPassword = "my-new-password"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsUsernamePassword(oldUsername, oldPassword)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("username", createSecretJson(newUsername)); + secretJson.add("password", createSecretJson(newPassword)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsUsernamePassword updatedCredentials = (CredentialsUsernamePassword) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getUsername()).isEqualTo(newUsername); + assertThat(updatedCredentials.getPassword()).isEqualTo(newPassword); + } + + @Test + public void testUpdateTokenSecretUpdatesValueOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldToken = "my-old-token"; + String newToken = "my-new-token"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsToken(oldToken)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsToken updatedCredentials = (CredentialsToken) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getToken()).isEqualTo(newToken.getBytes()); + } + + @Test + public void testUpdateUsernameTokenSecretUpdatesValueOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-old-username"; + String oldToken = "my-old-token"; + String newToken = "my-new-token"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsUsernameToken(oldUsername, oldToken)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsUsernameToken updatedCredentials = (CredentialsUsernameToken) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getUsername()).isEqualTo(oldUsername); + assertThat(updatedCredentials.getToken()).isEqualTo(newToken.getBytes()); + } + + @Test + public void testUpdateUsernameSecretUpdatesValueOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-old-username"; + String newUsername = "my-new-username"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsUsername(oldUsername)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("username", createSecretJson(newUsername)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsUsername updatedCredentials = (CredentialsUsername) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getUsername()).isEqualTo(newUsername); + } + + @Test + public void testUpdateSecretToTokenChangesSecretTypeOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-username"; + String newToken = "my-new-token"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsUsername(oldUsername)); + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("type", TOKEN.toString()); + secretJson.add("token", createSecretJson(newToken)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsToken updatedCredentials = (CredentialsToken) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getToken()).isEqualTo(newToken.getBytes()); + } + + @Test + public void testUpdateSecretWithUnknownTypeReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-username"; + String newUsername = "my-new-username"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsUsername(oldUsername)); + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("type", "UNKNOWN TYPE"); + secretJson.add("username", createSecretJson(newUsername)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5074, "GAL5074E", + "Unknown GalasaSecret type provided"); + } + + @Test + public void testUpdateSecretWithTypeAndMissingFieldsReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-username"; + String newUsername = "my-new-username"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsUsername(oldUsername)); + + // Create a request to change a secret into a UsernameToken, but is missing a token + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("type", USERNAME_TOKEN.toString()); + secretJson.add("username", createSecretJson(newUsername)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5099, "GAL5099E", + "The 'UsernameToken' type was provided but the required 'token' field was missing"); + } + + @Test + public void testUpdateSecretWithPasswordAndTokenPayloadReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldToken = "my-old-token"; + String newToken = "my-new-token"; + String newPassword = "my-new-password"; + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsToken(oldToken)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("password", createSecretJson(newPassword)); + secretJson.add("token", createSecretJson(newToken)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5095, "GAL5095E", + "The 'password' and 'token' fields are mutually exclusive"); + } + + @Test + public void testUpdateSecretWithMixedEncodingUpdatesSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-username"; + String newUsername = "my-new-username"; + String newToken = "my-new-token"; + String newTokenEncoded = Base64.getEncoder().encodeToString(newToken.getBytes()); + + // Put an existing secret into the credentials store + creds.put(secretName, new CredentialsUsername(oldUsername)); + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("type", USERNAME_TOKEN.toString()); + secretJson.add("username", createSecretJson(newUsername)); + secretJson.add("token", createSecretJson(newTokenEncoded, "base64")); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsUsernameToken updatedCredentials = (CredentialsUsernameToken) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getUsername()).isEqualTo(newUsername); + assertThat(updatedCredentials.getToken()).isEqualTo(newToken.getBytes()); + } + + @Test + public void testUpdateNonExistantSecretCreatesSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String newToken = "my-new-token"; + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + // Expect a 201 Created + assertThat(servletResponse.getStatus()).isEqualTo(201); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsToken updatedCredentials = (CredentialsToken) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getToken()).isEqualTo(newToken.getBytes()); + } + + @Test + public void testUpdateNonExistantSecretWithPasswordAndTokenPayloadReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String newToken = "my-new-token"; + String newPassword = "my-new-password"; + + JsonObject secretJson = new JsonObject(); + secretJson.add("password", createSecretJson(newPassword)); + secretJson.add("token", createSecretJson(newToken)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5095, "GAL5095E", + "The 'password' and 'token' fields are mutually exclusive"); + } + + @Test + public void testUpdateNonExistantSecretWithPasswordOnlyPayloadReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String newPassword = "my-new-password"; + + JsonObject secretJson = new JsonObject(); + secretJson.add("password", createSecretJson(newPassword)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + assertThat(servletResponse.getContentType()).isEqualTo("application/json"); + checkErrorStructure(outStream.toString(), 5098, "GAL5098E", + "A 'password' field was provided but the 'username' field was missing"); + } + + @Test + public void testUpdateSecretWithUnknownTokenEncodingReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String newToken = "my-new-token"; + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken, "UNKNOWN ENCODING!")); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(outStream.toString(), 5073, "GAL5073E", + "Unsupported data encoding scheme provided"); + } + + @Test + public void testUpdateSecretWithUnknownUsernameEncodingReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String newUsername = "my-new-username"; + + JsonObject secretJson = new JsonObject(); + secretJson.add("username", createSecretJson(newUsername, "UNKNOWN ENCODING!")); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(outStream.toString(), 5073, "GAL5073E", + "Unsupported data encoding scheme provided"); + } + + @Test + public void testUpdateSecretWithUnexpectedFieldsReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldUsername = "my-old-username"; + creds.put(secretName, new CredentialsUsername(oldUsername)); + + JsonObject secretJson = new JsonObject(); + + // "password" isn't a valid field in the Username type, so this should throw an error + secretJson.add("password", createSecretJson("bad")); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(outStream.toString(), 5100, "GAL5100E", + "An unexpected field was given to update a 'Username' secret"); + } + + @Test + public void testUpdateSecretWithTooManyFieldsReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldToken = "my-old-token"; + String newToken = "my-new-token"; + creds.put(secretName, new CredentialsToken(oldToken)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken)); + + // "username" isn't a valid field in the Token type, so this should throw an error + secretJson.add("username", createSecretJson("my-username")); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(outStream.toString(), 5100, "GAL5100E", + "An unexpected field was given to update a 'Token' secret"); + } + + @Test + public void testUpdateSecretWithUnsupportedTypeReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String newToken = "my-new-token"; + + // The mock credentials type is not a supported type, so this should cause an error + creds.put(secretName, new MockCredentials()); + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken)); + + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(500); + checkErrorStructure(outStream.toString(), 5101, "GAL5101E", + "Unknown secret type detected"); + } + + @Test + public void testUpdateSecretWithBlankDescriptionReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldToken = "my-old-token"; + String newToken = "my-new-token"; + String newDescription = " "; + + creds.put(secretName, new CredentialsToken(oldToken)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken)); + secretJson.addProperty("description", newDescription); + + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(outStream.toString(), 5102, "GAL5102E", + "Invalid secret description provided"); + } + + @Test + public void testUpdateSecretWithNonLatin1DescriptionReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldToken = "my-old-token"; + String newToken = "my-new-token"; + + // Latin-1 characters are in the range 0-255, so get one that is outside this range + char nonLatin1Character = (char)300; + String description = Character.toString(nonLatin1Character) + " more text here!"; + + creds.put(secretName, new CredentialsToken(oldToken)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken)); + secretJson.addProperty("description", description); + + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(outStream.toString(), 5102, "GAL5102E", + "Invalid secret description provided"); + } + + @Test + public void testUpdateSecretWithValidLatin1DescriptionUpdatesSecret() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String oldToken = "my-old-token"; + String newToken = "my-new-token"; + + Instant lastUpdatedTime = Instant.EPOCH; + + // Latin-1 characters are in the range 0-255 + char latin1Character = (char)255; + String description = Character.toString(latin1Character) + " more text here!"; + + creds.put(secretName, new CredentialsToken(oldToken)); + + JsonObject secretJson = new JsonObject(); + secretJson.add("token", createSecretJson(newToken)); + secretJson.addProperty("description", description); + + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(lastUpdatedTime); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/" + secretName, secretJsonStr, HttpMethod.PUT.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPut(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(204); + assertThat(outStream.toString()).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsToken updatedCredentials = (CredentialsToken) credsService.getCredentials(secretName); + assertThat(updatedCredentials).isNotNull(); + assertThat(updatedCredentials.getToken()).isEqualTo(newToken.getBytes()); + assertThat(updatedCredentials.getDescription()).isEqualTo(description); + assertThat(updatedCredentials.getLastUpdatedTime()).isEqualTo(lastUpdatedTime); + assertThat(updatedCredentials.getLastUpdatedByUser()).isEqualTo(JWT_USERNAME); + } +} + diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java new file mode 100644 index 000000000..dc6607310 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsRouteTest.java @@ -0,0 +1,808 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Instant; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.servlet.ServletOutputStream; + +import org.junit.Test; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import dev.galasa.ICredentials; +import dev.galasa.framework.api.common.HttpMethod; +import dev.galasa.framework.api.common.mocks.MockCredentialsService; +import dev.galasa.framework.api.common.mocks.MockFramework; +import dev.galasa.framework.api.common.mocks.MockHttpServletRequest; +import dev.galasa.framework.api.common.mocks.MockHttpServletResponse; +import dev.galasa.framework.api.common.mocks.MockTimeService; +import dev.galasa.framework.api.secrets.internal.routes.SecretsRoute; +import dev.galasa.framework.api.secrets.mocks.MockSecretsServlet; +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 SecretsRouteTest extends SecretsServletTest { + + @Test + public void testSecretsRouteRegexMatchesExpectedPaths() throws Exception { + // Given... + Pattern routePattern = new SecretsRoute(null, null, null, null).getPath(); + + // Then... + // The servlet's whiteboard pattern will match /secrets, so the secrets route + // should only allow an optional / or an empty string (no suffix after "/secrets") + assertThat(routePattern.matcher("/").matches()).isTrue(); + assertThat(routePattern.matcher("").matches()).isTrue(); + + // The route should not accept the following + assertThat(routePattern.matcher("////").matches()).isFalse(); + assertThat(routePattern.matcher("/wrongpath!").matches()).isFalse(); + } + + @Test + public void testGetSecretsReturnsAllSecretsOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName1 = "BOB"; + String username1 = "my-username"; + String password1 = "not-a-password"; + String description1 = "this is my first secret"; + String lastUser1 = "user1"; + Instant lastUpdated1 = Instant.EPOCH; + + String secretName2 = "ITS_BOB_AGAIN"; + String username2 = "another-username"; + String description2 = "this is my second secret"; + String lastUser2 = "user2"; + Instant lastUpdated2 = Instant.EPOCH.plusMillis(1); + + String secretName3 = "not-b0b"; + String token3 = "this-is-a-token"; + + String secretName4 = "new-bob"; + String username4 = "this-is-yet-another-username"; + String token4 = "this-is-another-token"; + String lastUser4 = "user4"; + Instant lastUpdated4 = Instant.EPOCH.plusMillis(4); + + ICredentials secret1 = new CredentialsUsernamePassword(username1, password1); + secret1.setDescription(description1); + secret1.setLastUpdatedByUser(lastUser1); + secret1.setLastUpdatedTime(lastUpdated1); + + ICredentials secret2 = new CredentialsUsername(username2); + secret2.setDescription(description2); + secret2.setLastUpdatedByUser(lastUser2); + secret2.setLastUpdatedTime(lastUpdated2); + + ICredentials secret4 = new CredentialsUsernameToken(username4, token4); + secret4.setLastUpdatedByUser(username4); + secret4.setLastUpdatedByUser(lastUser4); + secret4.setLastUpdatedTime(lastUpdated4); + + creds.put(secretName1, secret1); + creds.put(secretName2, secret2); + creds.put(secretName3, new CredentialsToken(token3)); + creds.put(secretName4, secret4); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doGet(mockRequest, servletResponse); + + // Then... + JsonArray expectedJson = new JsonArray(); + expectedJson.add(generateSecretJson(secretName4, "UsernameToken", username4, null, token4, null, lastUser4, lastUpdated4)); + expectedJson.add(generateSecretJson(secretName2, "Username", username2, null, null, description2, lastUser2, lastUpdated2)); + expectedJson.add(generateSecretJson(secretName1, "UsernamePassword", username1, password1, null, description1, lastUser1, lastUpdated1)); + expectedJson.add(generateSecretJson(secretName3, "Token", null, null, token3)); + + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(200); + assertThat(output).isEqualTo(gson.toJson(expectedJson)); + } + + @Test + public void testGetSecretsWithUnknownSecretTypeReturnsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName1 = "BOB"; + creds.put(secretName1, new MockCredentials()); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doGet(mockRequest, servletResponse); + + // Then... + assertThat(servletResponse.getStatus()).isEqualTo(500); + checkErrorStructure(outStream.toString(), 5101, "GAL5101E", + "Unknown secret type detected"); + } + + @Test + public void testCreateUsernamePasswordSecretCreatesSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String username = "my-username"; + String password = "not-a-password"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(username)); + secretJson.add("password", createSecretJson(password)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(201); + assertThat(output).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsUsernamePassword createdCredentials = (CredentialsUsernamePassword) credsService.getCredentials(secretName); + assertThat(createdCredentials).isNotNull(); + assertThat(createdCredentials.getUsername()).isEqualTo(username); + assertThat(createdCredentials.getPassword()).isEqualTo(password); + } + + @Test + public void testCreateTokenSecretCreatesSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB_TOKEN"; + String token = "my-token"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("token", createSecretJson(token)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(201); + assertThat(output).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsToken createdCredentials = (CredentialsToken) credsService.getCredentials(secretName); + assertThat(createdCredentials).isNotNull(); + assertThat(createdCredentials.getToken()).isEqualTo(token.getBytes()); + } + + @Test + public void testCreateBase64EncodedSecretCreatesSecretOk() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String username = "my-username"; + String encoding = "base64"; + String encodedUsername = Base64.getEncoder().encodeToString(username.getBytes()); + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(encodedUsername, encoding)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(201); + assertThat(output).isEmpty(); + + assertThat(credsService.getAllCredentials()).hasSize(1); + CredentialsUsername createdCredentials = (CredentialsUsername) credsService.getCredentials(secretName); + assertThat(createdCredentials).isNotNull(); + assertThat(createdCredentials.getUsername()).isEqualTo(username); + } + + @Test + public void testCreateBase64EncodedSecretWithBadlyEncodedDataThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String username = "this isn't base64 encoded!"; + String encoding = "base64"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(username, encoding)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5097, "GAL5097E", + "Expected the value to be encoded in 'base64' format but it was not."); + } + + @Test + public void testCreateSecretWithPasswordAndTokenThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "BOB"; + String token = "my-token"; + String password = "not-a-password"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("token", createSecretJson(token)); + secretJson.add("password", createSecretJson(password)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5095, "GAL5095E", + "The 'password' and 'token' fields are mutually exclusive"); + } + + @Test + public void testCreateSecretWithMissingSecretNameThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String username = "my-username"; + String password = "not-a-password"; + + JsonObject secretJson = new JsonObject(); + secretJson.add("username", createSecretJson(username)); + secretJson.add("password", createSecretJson(password)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5092, "GAL5092E", + "The name of a Galasa secret cannot be empty, contain only spaces or tabs, or contain dots (.), and must only contain characters in the Latin-1 character set"); + } + + @Test + public void testCreateSecretWithBlankSecretNameThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = " "; + String username = "my-username"; + String password = "not-a-password"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(username)); + secretJson.add("password", createSecretJson(password)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5092, "GAL5092E", + "The name of a Galasa secret cannot be empty, contain only spaces or tabs, or contain dots (.), and must only contain characters in the Latin-1 character set"); + } + + @Test + public void testCreateSecretWithMissingUsernameValueThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-SECRET"; + String username = null; + String password = "not-a-password"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(username)); + secretJson.add("password", createSecretJson(password)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5096, "GAL5096E", + "One or more secret fields in your request payload are missing a 'value'"); + } + + @Test + public void testCreateSecretWithMissingPasswordValueThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-SECRET"; + String username = "my-username"; + String password = null; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(username)); + secretJson.add("password", createSecretJson(password)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5096, "GAL5096E", + "One or more secret fields in your request payload are missing a 'value'"); + } + + @Test + public void testCreateSecretWithMissingTokenValueThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-SECRET"; + String username = "my-username"; + String token = null; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(username)); + secretJson.add("token", createSecretJson(token)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5096, "GAL5096E", + "One or more secret fields in your request payload are missing a 'value'"); + } + + @Test + public void testCreateSecretWithBlankUsernameValueThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-SECRET"; + String username = " "; + String token = "my-token"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(username)); + secretJson.add("token", createSecretJson(token)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5096, "GAL5096E", + "One or more secret fields in your request payload are missing a 'value'"); + } + + @Test + public void testCreateSecretWithBlankPasswordValueThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-SECRET"; + String username = "my-username"; + String password = " "; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson(username)); + secretJson.add("password", createSecretJson(password)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5096, "GAL5096E", + "One or more secret fields in your request payload are missing a 'value'"); + } + + @Test + public void testCreateSecretWithBlankTokenValueThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-SECRET"; + String token = " "; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("token", createSecretJson(token)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5096, "GAL5096E", + "One or more secret fields in your request payload are missing a 'value'"); + } + + @Test + public void testCreateSecretWithUnknownEncodingValueThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-SECRET"; + String token = "my-token"; + String encoding = "UNKNOWN ENCODING!"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("token", createSecretJson(token, encoding)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5073, "GAL5073E", + "Unsupported data encoding scheme provided"); + } + + @Test + public void testCreateSecretWithPasswordAndMissingUsernameThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-SECRET"; + String password = "not-a-password"; + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("password", createSecretJson(password)); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5098, "GAL5098E", + "A 'password' field was provided but the 'username' field was missing"); + } + + @Test + public void testCreateSecretWithExistingSecretNameThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + String secretName = "MY-EXISTING-SECRET"; + creds.put(secretName, new CredentialsUsername("my-username")); + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson("my-new-username")); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(409); + checkErrorStructure(output, 5075, "GAL5075E", + "A secret with the provided name already exists"); + } + + @Test + public void testCreateSecretWithNonLatin1SecretNameThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + + // Latin-1 characters are in the range 0-255, so get one that is outside this range + char nonLatin1Character = (char)300; + String secretName = "MY-EXISTING-SECRET" + nonLatin1Character; + + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.add("username", createSecretJson("my-new-username")); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5092, "GAL5092E", + "Invalid secret name provided"); + } + + @Test + public void testCreateSecretWithNonLatin1DescriptionThrowsError() throws Exception { + // Given... + Map creds = new HashMap<>(); + + // Latin-1 characters are in the range 0-255, so get one that is outside this range + char nonLatin1Character = (char)300; + String description = Character.toString(nonLatin1Character) + " more text here!"; + String secretName = "MY-EXISTING-SECRET"; + + + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("name", secretName); + secretJson.addProperty("description", description); + secretJson.add("username", createSecretJson("my-new-username")); + String secretJsonStr = gson.toJson(secretJson); + + MockCredentialsService credsService = new MockCredentialsService(creds); + MockFramework mockFramework = new MockFramework(credsService); + + MockTimeService timeService = new MockTimeService(Instant.EPOCH); + MockSecretsServlet servlet = new MockSecretsServlet(mockFramework, timeService); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest("/", secretJsonStr, HttpMethod.POST.toString(), REQUEST_HEADERS); + + MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + ServletOutputStream outStream = servletResponse.getOutputStream(); + + // When... + servlet.init(); + servlet.doPost(mockRequest, servletResponse); + + // Then... + String output = outStream.toString(); + assertThat(servletResponse.getStatus()).isEqualTo(400); + checkErrorStructure(output, 5102, "GAL5102E", + "Invalid secret description provided"); + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsServletTest.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsServletTest.java new file mode 100644 index 000000000..02873e647 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/internal/SecretsServletTest.java @@ -0,0 +1,115 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.internal; + +import java.time.Instant; +import java.util.Base64; +import java.util.Base64.Encoder; +import java.util.Map; + +import com.google.gson.JsonObject; + +import dev.galasa.framework.api.common.BaseServletTest; +import dev.galasa.framework.api.common.resources.GalasaResourceValidator; + +public class SecretsServletTest extends BaseServletTest { + + protected static final Map REQUEST_HEADERS = Map.of("Authorization", "Bearer " + BaseServletTest.DUMMY_JWT); + + protected JsonObject createSecretJson(String value, String encoding) { + JsonObject secretJson = new JsonObject(); + if (value != null) { + secretJson.addProperty("value", value); + } + + if (encoding != null) { + secretJson.addProperty("encoding", encoding); + } + + return secretJson; + } + + protected JsonObject createSecretJson(String value) { + return createSecretJson(value, null); + } + + protected JsonObject generateSecretJson( + String secretName, + String type, + String username, + String password, + String token + ) { + return generateSecretJson(secretName, type, username, password, token, null, null, null); + } + + protected JsonObject generateSecretJson( + String secretName, + String type, + String username, + String password, + String token, + String description, + String lastUpdatedUser, + Instant lastUpdatedTime + ) { + JsonObject secretJson = new JsonObject(); + secretJson.addProperty("apiVersion", GalasaResourceValidator.DEFAULT_API_VERSION); + + secretJson.add("metadata", generateExpectedMetadata(secretName, type, description, lastUpdatedUser, lastUpdatedTime)); + secretJson.add("data", generateExpectedData(username, password, token)); + + secretJson.addProperty("kind", "GalasaSecret"); + + return secretJson; + } + + private JsonObject generateExpectedMetadata( + String secretName, + String type, + String description, + String lastUpdatedUser, + Instant lastUpdatedTime + ) { + JsonObject metadata = new JsonObject(); + metadata.addProperty("name", secretName); + if (lastUpdatedTime != null) { + metadata.addProperty("lastUpdatedTime", lastUpdatedTime.toString()); + } + + if (lastUpdatedUser != null) { + metadata.addProperty("lastUpdatedBy", lastUpdatedUser); + } + + metadata.addProperty("encoding", "base64"); + + if (description != null) { + metadata.addProperty("description", description); + } + + metadata.addProperty("type", type); + + return metadata; + } + + private JsonObject generateExpectedData(String username, String password, String token) { + JsonObject data = new JsonObject(); + Encoder encoder = Base64.getEncoder(); + if (username != null) { + data.addProperty("username", encoder.encodeToString(username.getBytes())); + } + + if (password != null) { + data.addProperty("password", encoder.encodeToString(password.getBytes())); + } + + if (token != null) { + data.addProperty("token", encoder.encodeToString(token.getBytes())); + } + + return data; + } +} diff --git a/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/mocks/MockSecretsServlet.java b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/mocks/MockSecretsServlet.java new file mode 100644 index 000000000..ed3108cb9 --- /dev/null +++ b/galasa-parent/dev.galasa.framework.api.secrets/src/test/java/dev/galasa/framework/api/secrets/mocks/MockSecretsServlet.java @@ -0,0 +1,30 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.framework.api.secrets.mocks; + +import dev.galasa.framework.api.common.EnvironmentVariables; +import dev.galasa.framework.api.common.ResponseBuilder; +import dev.galasa.framework.api.common.mocks.MockEnvironment; +import dev.galasa.framework.api.common.mocks.MockFramework; +import dev.galasa.framework.api.common.mocks.MockTimeService; +import dev.galasa.framework.api.secrets.SecretsServlet; +import dev.galasa.framework.spi.utils.ITimeService; + +public class MockSecretsServlet extends SecretsServlet { + + public MockSecretsServlet(MockFramework framework, MockTimeService mockTimeService) { + this(framework, new MockEnvironment(), mockTimeService); + } + + public MockSecretsServlet(MockFramework framework, MockEnvironment env, ITimeService timeService) { + env.setenv(EnvironmentVariables.GALASA_USERNAME_CLAIMS, "preferred_username"); + + this.framework = framework; + this.env = env; + this.timeService = timeService; + setResponseBuilder(new ResponseBuilder(env)); + } +} 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 88dee3aa0..daf042e70 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 @@ -9,6 +9,7 @@ import java.net.URI; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Map; import javax.crypto.spec.SecretKeySpec; @@ -115,4 +116,10 @@ public void setCredentials(String credsId, ICredentials credentials) throws Cred public void deleteCredentials(String credsId) throws CredentialsException { // Not implemented for local credentials... } + + @Override + public Map getAllCredentials() throws CredentialsException { + // Not implemented for local credentials... + throw new UnsupportedOperationException("Unimplemented method 'getAllCredentials'"); + } } 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 6ce610cc5..f20b885b4 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 @@ -5,6 +5,8 @@ */ package dev.galasa.framework.internal.creds; +import java.util.Map; + import javax.validation.constraints.NotNull; import dev.galasa.framework.spi.IConfidentialTextService; @@ -105,4 +107,9 @@ public void setCredentials(String credentialsId, ICredentials credentials) throw public void deleteCredentials(String credentialsId) throws CredentialsException { credsStore.deleteCredentials(credentialsId); } + + @Override + public Map getAllCredentials() throws CredentialsException { + return credsStore.getAllCredentials(); + } } diff --git a/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/AbstractCredentials.java b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/AbstractCredentials.java new file mode 100644 index 000000000..636269bee --- /dev/null +++ b/galasa-parent/dev.galasa.framework/src/main/java/dev/galasa/framework/spi/creds/AbstractCredentials.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.time.Instant; +import java.util.Properties; + +import javax.crypto.spec.SecretKeySpec; + +import dev.galasa.ICredentials; + +/** + * An abstract class where common credential-related details are stored. + */ +public abstract class AbstractCredentials extends Credentials implements ICredentials { + + protected static final String CREDS_PROPERTY_PREFIX = "secure.credentials."; + + private String description; + private String lastUpdatedByUser; + private Instant lastUpdatedTime; + + public AbstractCredentials(SecretKeySpec key) throws CredentialsException { + super(key); + } + + public AbstractCredentials() { + super(); + } + + @Override + public void setDescription(String description) { + this.description = description; + } + + @Override + public void setLastUpdatedByUser(String username) { + this.lastUpdatedByUser = username; + } + + @Override + public void setLastUpdatedTime(Instant time) { + this.lastUpdatedTime = time; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String getLastUpdatedByUser() { + return lastUpdatedByUser; + } + + @Override + public Instant getLastUpdatedTime() { + return lastUpdatedTime; + } + + @Override + public Properties getMetadataProperties(String credentialsId) { + Properties properties = new Properties(); + if (description != null) { + properties.put(CREDS_PROPERTY_PREFIX + credentialsId + ".description", description); + } + + if (lastUpdatedTime != null) { + properties.put(CREDS_PROPERTY_PREFIX + credentialsId + ".lastUpdated.time", lastUpdatedTime.toString()); + } + + if (lastUpdatedByUser != null) { + properties.put(CREDS_PROPERTY_PREFIX + credentialsId + ".lastUpdated.user", lastUpdatedByUser); + } + return properties; + } +} 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 ccc96ecf1..f57df24a8 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 @@ -64,5 +64,4 @@ protected byte[] decrypt(String encrypted) throws CredentialsException { throw new CredentialsException("Unable to decrypt credentials", e); } } - } 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 a5a41850f..40f76c47f 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 @@ -11,7 +11,7 @@ import dev.galasa.ICredentialsToken; -public class CredentialsToken extends Credentials implements ICredentialsToken { +public class CredentialsToken extends AbstractCredentials implements ICredentialsToken { private final byte[] token; public CredentialsToken(String plainTextToken) { @@ -36,8 +36,7 @@ public byte[] getToken() { @Override public Properties toProperties(String credentialsId) { Properties credsProperties = new Properties(); - credsProperties.setProperty("secure.credentials." + credentialsId + ".token" , new String(this.token)); + credsProperties.setProperty(CREDS_PROPERTY_PREFIX + 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 9e232ffc5..c9e1ad438 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 @@ -12,7 +12,7 @@ import dev.galasa.ICredentialsUsername; -public class CredentialsUsername extends Credentials implements ICredentialsUsername { +public class CredentialsUsername extends AbstractCredentials implements ICredentialsUsername { private String username; public CredentialsUsername(String plainTextUsername) { @@ -36,7 +36,7 @@ public String getUsername() { @Override public Properties toProperties(String credentialsId) { Properties credsProperties = new Properties(); - credsProperties.setProperty("secure.credentials." + credentialsId + ".username" , this.username); + credsProperties.setProperty(CREDS_PROPERTY_PREFIX + 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 811509ed3..efa4eb03a 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 @@ -12,7 +12,7 @@ import dev.galasa.ICredentialsUsernamePassword; -public class CredentialsUsernamePassword extends Credentials implements ICredentialsUsernamePassword { +public class CredentialsUsernamePassword extends AbstractCredentials implements ICredentialsUsernamePassword { private String username; private String password; @@ -48,8 +48,8 @@ public String getPassword() { @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); + credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".username" , this.username); + credsProperties.setProperty(CREDS_PROPERTY_PREFIX + 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 37fb3c3cd..e11678409 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 @@ -12,7 +12,7 @@ import dev.galasa.ICredentialsUsernameToken; -public class CredentialsUsernameToken extends Credentials implements ICredentialsUsernameToken { +public class CredentialsUsernameToken extends AbstractCredentials implements ICredentialsUsernameToken { private String username; private byte[] token; @@ -48,8 +48,8 @@ public byte[] getToken() { @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)); + credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".username" , this.username); + credsProperties.setProperty(CREDS_PROPERTY_PREFIX + credentialsId + ".token" , new String(this.token)); return credsProperties; } } 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 044de9e27..70b9ac863 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 @@ -5,6 +5,8 @@ */ package dev.galasa.framework.spi.creds; +import java.util.Map; + import javax.validation.constraints.NotNull; import dev.galasa.ICredentials; @@ -12,22 +14,22 @@ public interface ICredentialsService { /** - * Gets the credentials with the given ID and returns them without decrypting their values + * 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 encrypted credentials in the credentials store, or null if no such credentials exist + * @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 getEncryptedCredentials(@NotNull String credentialsId) throws CredentialsException; + ICredentials getCredentials(@NotNull String credentialsId) throws CredentialsException; /** - * Gets the credentials with the given ID and returns them after attempting to decrypt their values + * Gets all the credentials stored in the credentials store * - * @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 + * @return a map of credentials, where keys correspond to credentials IDs and + * values correspond to the credentials * @throws CredentialsException if there was an issue accessing the credentials store */ - ICredentials getCredentials(@NotNull String credentialsId) throws CredentialsException; + Map getAllCredentials() throws CredentialsException; void setCredentials(String credentialsId, ICredentials credentials) 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 c7a3bd96c..8b6aebfb3 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 @@ -5,12 +5,16 @@ */ package dev.galasa.framework.spi.creds; +import java.util.Map; + import dev.galasa.ICredentials; public interface ICredentialsStore { ICredentials getCredentials(String credsId) throws CredentialsException; + Map getAllCredentials() throws CredentialsException; + void setCredentials(String credsId, ICredentials credentials) throws CredentialsException; void deleteCredentials(String credsId) 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 deleted file mode 100644 index 1abc94cbb..000000000 --- a/galasa-parent/dev.galasa.framework/src/test/java/dev/galasa/framework/mocks/MockCredentials.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright contributors to the Galasa project - * - * SPDX-License-Identifier: EPL-2.0 - */ -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 b0edfd22d..9d1de3e88 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 @@ -41,4 +41,9 @@ public void setCredentials(String credsId, ICredentials credentials) throws Cred public void deleteCredentials(String credsId) throws CredentialsException { throw new UnsupportedOperationException("Unimplemented method 'deleteCredentials'"); } + + @Override + public Map getAllCredentials() throws CredentialsException { + throw new UnsupportedOperationException("Unimplemented method 'getAllCredentials'"); + } } 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 4369db949..35db1ec2b 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,8 +5,18 @@ */ package dev.galasa; +import java.time.Instant; import java.util.Properties; public interface ICredentials { Properties toProperties(String credentialsId); + Properties getMetadataProperties(String credentialsId); + + void setDescription(String description); + void setLastUpdatedByUser(String username); + void setLastUpdatedTime(Instant time); + + String getDescription(); + String getLastUpdatedByUser(); + Instant getLastUpdatedTime(); } 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 a008ec951..2478a7ec2 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 @@ -6,6 +6,7 @@ package dev.galasa.testharness; import java.util.HashMap; +import java.util.Map; import javax.validation.constraints.NotNull; @@ -31,4 +32,9 @@ public void setCredentials(String credentialsId, ICredentials credentials) throw public void deleteCredentials(String credentialsId) throws CredentialsException { throw new UnsupportedOperationException("Unimplemented method 'deleteCredentials'"); } + + @Override + public Map getAllCredentials() throws CredentialsException { + throw new UnsupportedOperationException("Unimplemented method 'getAllCredentials'"); + } } diff --git a/galasa-parent/settings.gradle b/galasa-parent/settings.gradle index 04611cf19..b1f51ad51 100644 --- a/galasa-parent/settings.gradle +++ b/galasa-parent/settings.gradle @@ -32,6 +32,7 @@ include 'dev.galasa.framework.api.openapi.servlet' include 'dev.galasa.framework.api.ras' include 'dev.galasa.framework.api.resources' include 'dev.galasa.framework.api.runs' +include 'dev.galasa.framework.api.secrets' include 'dev.galasa.framework.api.testcatalog' include 'dev.galasa.framework.docker.controller' include 'dev.galasa.framework.k8s.controller' diff --git a/release.yaml b/release.yaml index 28771e4fd..dc981c045 100644 --- a/release.yaml +++ b/release.yaml @@ -243,6 +243,15 @@ api: isolated: true codecoverage: true + - artifact: dev.galasa.framework.api.secrets + version: 0.38.0 + obr: true + mvp: false + bom: false + javadoc: false + isolated: true + codecoverage: true + - artifact: dev.galasa.framework.api.testcatalog version: 0.34.0 obr: true