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 3ddf109e..7d9c489a 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,13 +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.BaseResourceValidator; +import dev.galasa.framework.api.common.resources.AbstractValidator; import static dev.galasa.framework.api.common.ServletErrorMessage.*; import javax.servlet.http.HttpServletResponse; -public class TokenPayloadValidator extends BaseResourceValidator implements IBeanValidator { +public class TokenPayloadValidator extends AbstractValidator implements IBeanValidator { @Override public void validate(TokenPayload tokenPayload) throws InternalServletException { 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 86e6b4fe..4f66f722 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 @@ -128,7 +128,7 @@ public enum ServletErrorMessage { 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 or contain only spaces or tabs, and must only contain characters in the Latin-1 character set. Check your request payload and try again."), + 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."), diff --git a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/BaseResourceValidator.java b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/AbstractValidator.java similarity index 96% rename from galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/BaseResourceValidator.java rename to galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/AbstractValidator.java index cab2bc07..90858a11 100644 --- a/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/BaseResourceValidator.java +++ b/galasa-parent/dev.galasa.framework.api.common/src/main/java/dev/galasa/framework/api/common/resources/AbstractValidator.java @@ -8,7 +8,7 @@ /** * A base validator class that contains commonly-used validation methods */ -public class BaseResourceValidator { +public abstract class AbstractValidator { /** * Checks whether a given string is in valid Latin-1 format (e.g. characters in the range 0 - 255) 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 00000000..bd1e7bb5 --- /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 6bf90933..5676cede 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 @@ -5,17 +5,12 @@ */ package dev.galasa.framework.api.common.resources; -import java.util.List; - public enum GalasaSecretType { USERNAME_PASSWORD("UsernamePassword", "username", "password"), USERNAME_TOKEN("UsernameToken", "username", "token"), USERNAME("Username", "username"), TOKEN("Token", "token"); - public static final String DEFAULT_API_VERSION = "galasa-dev/v1alpha1"; - public static final List SUPPORTED_ENCODING_SCHEMES = List.of("base64"); - private String name; private String[] requiredDataFields; 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 00000000..3171915b --- /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.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 9d4aca52..4d1d2554 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 17bfe23b..a8ed74da 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,13 +21,12 @@ 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; @@ -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 11a36c8d..0fb836d2 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; @@ -29,10 +26,10 @@ 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.BaseResourceValidator; 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; @@ -47,13 +44,9 @@ public class GalasaSecretProcessor extends AbstractGalasaResourceProcessor imple private final Log logger = LogFactory.getLog(getClass()); - private static final List SUPPORTED_ENCODING_SCHEMES = List.of("base64"); - private ICredentialsService credentialsService; private ITimeService timeService; - private BaseResourceValidator validator = new BaseResourceValidator(); - public GalasaSecretProcessor(ICredentialsService credentialsService, ITimeService timeService) { this.credentialsService = credentialsService; this.timeService = timeService; @@ -128,24 +121,15 @@ 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, GalasaSecretType.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( @@ -176,56 +160,4 @@ private ICredentials getCredentialsFromSecret( } 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()); - } - - // If a description is provided, check that it is valid - if (metadata.has("description")) { - String description = metadata.get("description").getAsString(); - if (description.isBlank() || !validator.isLatin1(description)) { - ServletError error = new ServletError(GAL5102_INVALID_SECRET_DESCRIPTION_PROVIDED); - validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); - } - } - - // Check if the given secret type is a valid type - if (metadata.has("type")) { - GalasaSecretType secretType = GalasaSecretType.getFromString(metadata.get("type").getAsString()); - if (secretType == null) { - String supportedSecretTypes = Arrays.stream(GalasaSecretType.values()) - .map(GalasaSecretType::toString) - .collect(Collectors.joining(", ")); - - ServletError error = new ServletError(GAL5074_UNKNOWN_GALASA_SECRET_TYPE, supportedSecretTypes); - validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); - } - } - - // Check if the given encoding scheme is supported - if (metadata.has("encoding") && !SUPPORTED_ENCODING_SCHEMES.contains(metadata.get("encoding").getAsString())) { - ServletError error = new ServletError(GAL5073_UNSUPPORTED_GALASA_SECRET_ENCODING, String.join(", ", SUPPORTED_ENCODING_SCHEMES)); - validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); - } - } - - private void validateSecretData(JsonObject secretJson, List validationErrors) { - JsonObject metadata = secretJson.get("metadata").getAsJsonObject(); - JsonObject data = secretJson.get("data").getAsJsonObject(); - - GalasaSecretType secretType = GalasaSecretType.getFromString(metadata.get("type").getAsString()); - String[] requiredTypeFields = secretType.getRequiredDataFields(); - List missingFields = getMissingResourceFields(data, Arrays.asList(requiredTypeFields)); - - if (!missingFields.isEmpty()) { - ServletError error = new ServletError(GAL5072_INVALID_GALASA_SECRET_MISSING_TYPE_DATA, secretType.toString(), String.join(", ", missingFields)); - validationErrors.add(new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST).getMessage()); - } - } } diff --git a/galasa-parent/dev.galasa.framework.api.resources/src/main/java/dev/galasa/framework/api/resources/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 00000000..b53131b9 --- /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 00000000..1d269ccc --- /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.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 index 2ee427c3..a7a0496c 100644 --- 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 @@ -6,7 +6,6 @@ package dev.galasa.framework.api.secrets.internal; import static dev.galasa.framework.api.common.ServletErrorMessage.*; -import static dev.galasa.framework.api.common.resources.GalasaSecretType.*; import javax.servlet.http.HttpServletResponse; @@ -14,12 +13,11 @@ 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.IBeanValidator; import dev.galasa.framework.api.common.InternalServletException; import dev.galasa.framework.api.common.ServletError; -import dev.galasa.framework.api.common.resources.BaseResourceValidator; +import dev.galasa.framework.api.common.resources.SecretValidator; -public class SecretRequestValidator extends BaseResourceValidator implements IBeanValidator { +public class SecretRequestValidator extends SecretValidator { @Override public void validate(SecretRequest secretRequest) throws InternalServletException { @@ -28,11 +26,7 @@ public void validate(SecretRequest secretRequest) throws InternalServletExceptio SecretRequesttoken token = secretRequest.gettoken(); // Check that the secret has been given a name - String secretName = secretRequest.getname(); - if (secretName == null || secretName.isBlank() || !isLatin1(secretName)) { - ServletError error = new ServletError(GAL5092_INVALID_SECRET_NAME_PROVIDED); - throw new InternalServletException(error, HttpServletResponse.SC_BAD_REQUEST); - } + validateSecretName(secretRequest.getname()); validateDescription(secretRequest.getdescription()); @@ -80,12 +74,4 @@ private void validateField(String value, String encoding) throws InternalServlet 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.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 index 55b29dc6..ae777ea4 100644 --- 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 @@ -33,6 +33,7 @@ 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; @@ -69,7 +70,7 @@ protected GalasaSecret createGalasaSecretFromCredentials(String secretName, ICre setSecretTypeValuesFromCredentials(metadata, data, credentials); setSecretMetadata(metadata, credentials.getDescription(), credentials.getLastUpdatedByUser(), credentials.getLastUpdatedTime()); GalasaSecret secret = new GalasaSecret(); - secret.setApiVersion(GalasaSecretType.DEFAULT_API_VERSION); + secret.setApiVersion(GalasaResourceValidator.DEFAULT_API_VERSION); secret.setdata(data); secret.setmetadata(metadata); 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 index 3673d762..ee28146b 100644 --- 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 @@ -46,9 +46,8 @@ public class SecretDetailsRoute extends AbstractSecretsRoute { // where {secret-id} can consist of the following characters: // - Alphanumeric characters (a-zA-Z0-9) // - Underscores (_) - // - Dots (.) // - Dashes (-) - private static final String PATH_PATTERN = "\\/([a-zA-Z0-9_.-]+)\\/?"; + private static final String PATH_PATTERN = "\\/([a-zA-Z0-9_-]+)\\/?"; private ICredentialsService credentialsService; 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 index df075a05..153ef433 100644 --- 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 @@ -48,11 +48,11 @@ public void testSecretDetailsRouteRegexMatchesExpectedPaths() throws Exception { assertThat(routePattern.matcher("/MYSECRET/").matches()).isTrue(); assertThat(routePattern.matcher("/mysecret").matches()).isTrue(); assertThat(routePattern.matcher("/myS3cret123").matches()).isTrue(); - assertThat(routePattern.matcher("/123My.Secret.456").matches()).isTrue(); - assertThat(routePattern.matcher("/My-Secret.456").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(); 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 index 428e7449..dc660731 100644 --- 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 @@ -374,7 +374,7 @@ public void testCreateSecretWithMissingSecretNameThrowsError() throws Exception String output = outStream.toString(); assertThat(servletResponse.getStatus()).isEqualTo(400); checkErrorStructure(output, 5092, "GAL5092E", - "The name of a Galasa secret cannot be empty or contain only spaces or tabs"); + "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 @@ -410,7 +410,7 @@ public void testCreateSecretWithBlankSecretNameThrowsError() throws Exception { String output = outStream.toString(); assertThat(servletResponse.getStatus()).isEqualTo(400); checkErrorStructure(output, 5092, "GAL5092E", - "The name of a Galasa secret cannot be empty or contain only spaces or tabs"); + "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 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 index ec45ba01..02873e64 100644 --- 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 @@ -13,7 +13,7 @@ import com.google.gson.JsonObject; import dev.galasa.framework.api.common.BaseServletTest; -import dev.galasa.framework.api.common.resources.GalasaSecretType; +import dev.galasa.framework.api.common.resources.GalasaResourceValidator; public class SecretsServletTest extends BaseServletTest { @@ -57,7 +57,7 @@ protected JsonObject generateSecretJson( Instant lastUpdatedTime ) { JsonObject secretJson = new JsonObject(); - secretJson.addProperty("apiVersion", GalasaSecretType.DEFAULT_API_VERSION); + secretJson.addProperty("apiVersion", GalasaResourceValidator.DEFAULT_API_VERSION); secretJson.add("metadata", generateExpectedMetadata(secretName, type, description, lastUpdatedUser, lastUpdatedTime)); secretJson.add("data", generateExpectedData(username, password, token));