diff --git a/.github/workflows/kapua-ci.yaml b/.github/workflows/kapua-ci.yaml index 466bd0b6431..e17f3c0fb84 100755 --- a/.github/workflows/kapua-ci.yaml +++ b/.github/workflows/kapua-ci.yaml @@ -329,6 +329,18 @@ jobs: tag: '@rest_cors' needs-docker-images: 'true' needs-api-docker-image: 'true' + test-api-parsing: + needs: test-api-auth + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Clones Kapua repo inside the runner + uses: actions/checkout@v4 + - uses: ./.github/actions/runTestsTaggedAs + with: + tag: '@rest_parsing' + needs-docker-images: 'true' + needs-api-docker-image: 'true' junit-tests: needs: build runs-on: ubuntu-latest diff --git a/commons-rest/errors/src/main/java/org/eclipse/kapua/commons/rest/errors/EclipseLinkExceptionMapper.java b/commons-rest/errors/src/main/java/org/eclipse/kapua/commons/rest/errors/EclipseLinkExceptionMapper.java new file mode 100644 index 00000000000..be9b18b81b3 --- /dev/null +++ b/commons-rest/errors/src/main/java/org/eclipse/kapua/commons/rest/errors/EclipseLinkExceptionMapper.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2021, 2022 Eurotech and/or its affiliates and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eurotech - initial API and implementation + *******************************************************************************/ +package org.eclipse.kapua.commons.rest.errors; + +import org.eclipse.kapua.commons.rest.model.errors.ThrowableInfo; +import org.eclipse.persistence.exceptions.ConversionException; +import org.eclipse.persistence.exceptions.DescriptorException; +import org.eclipse.persistence.exceptions.EclipseLinkException; +import org.eclipse.persistence.exceptions.XMLConversionException; + +import org.eclipse.persistence.exceptions.XMLMarshalException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class EclipseLinkExceptionMapper implements ExceptionMapper { + + private static final Logger LOG = LoggerFactory.getLogger(EclipseLinkExceptionMapper.class); + + private final boolean showStackTrace; + + @Inject + public EclipseLinkExceptionMapper(ExceptionConfigurationProvider exceptionConfigurationProvider) { + this.showStackTrace = exceptionConfigurationProvider.showStackTrace(); + } + + @Override + public Response toResponse(EclipseLinkException eclipseException) { + LOG.error(eclipseException.getMessage(), eclipseException); + + if (eclipseException instanceof ConversionException || + eclipseException instanceof DescriptorException || + eclipseException instanceof XMLMarshalException || + eclipseException instanceof XMLConversionException) { // These are subset of EclipseLinkExceptions thrown by MOXy, so we have a problem with JAXB parsing of the XML/JSON + ThrowableInfo responseError = new ThrowableInfo(Response.Status.BAD_REQUEST.getStatusCode(), eclipseException, showStackTrace); + responseError.setMessage("An error occurred during the parsing of the XML/JSON. Check the correctness of its format. Details of the exception thrown: " + responseError.getMessage()); + return Response + .status(Response.Status.BAD_REQUEST) + .entity(responseError) + .build(); + } else { + //Generic error message for other EclipseLinkExceptions + return Response + .serverError() + .entity(new ThrowableInfo(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), eclipseException, showStackTrace)) + .build(); + } + } + +} diff --git a/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/RestClientSteps.java b/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/RestClientSteps.java index 45b94721881..6bf3288b914 100644 --- a/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/RestClientSteps.java +++ b/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/RestClientSteps.java @@ -182,6 +182,9 @@ public void restCallInternal(String method, String resource, String json, boolea baseBuilder.POST(HttpRequest.BodyPublishers.ofString(json)); } else if (method.equals("GET")) { baseBuilder.GET(); + } else if (method.equals("PUT")) { + baseBuilder.setHeader("Content-Type", "application/json"); + baseBuilder.PUT(HttpRequest.BodyPublishers.ofString(json)); } if (additionalHeaders != null) { diff --git a/qa/integration/src/test/java/org/eclipse/kapua/integration/rest/RunRestParsingRequestTest.java b/qa/integration/src/test/java/org/eclipse/kapua/integration/rest/RunRestParsingRequestTest.java new file mode 100644 index 00000000000..91333b92b05 --- /dev/null +++ b/qa/integration/src/test/java/org/eclipse/kapua/integration/rest/RunRestParsingRequestTest.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2018, 2022 Eurotech and/or its affiliates and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eurotech + *******************************************************************************/ +package org.eclipse.kapua.integration.rest; + +import io.cucumber.junit.Cucumber; +import io.cucumber.junit.CucumberOptions; +import org.junit.runner.RunWith; + +@RunWith(Cucumber.class) +@CucumberOptions( + features = "classpath:features/rest/parsingRequests/RestParsingRequestsContents.feature", + glue = {"org.eclipse.kapua.qa.common", + "org.eclipse.kapua.qa.integration.steps", + "org.eclipse.kapua.service.endpoint.steps", + "org.eclipse.kapua.service.account.steps", + "org.eclipse.kapua.service.user.steps" + }, + plugin = { "pretty", + "html:target/cucumber/RestCors", + "json:target/RestCors_cucumber.json" + }, + monochrome = true) +public class RunRestParsingRequestTest { +} diff --git a/qa/integration/src/test/resources/features/rest/parsingRequests/RestParsingRequestsContents.feature b/qa/integration/src/test/resources/features/rest/parsingRequests/RestParsingRequestsContents.feature new file mode 100644 index 00000000000..6223347d3b4 --- /dev/null +++ b/qa/integration/src/test/resources/features/rest/parsingRequests/RestParsingRequestsContents.feature @@ -0,0 +1,64 @@ +############################################################################### +# Copyright (c) 2018, 2022 Eurotech and/or its affiliates and others +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# Eurotech - initial API and implementation +############################################################################### +@env_docker_base +@rest_parsing + +Feature: REST API tests for parsing of requests + REST API tests to verify correct responses upon parsing of wrong format fields in requests + + @setup + Scenario: Initialize security context, then start rest-api container and dependencies + Given Init Security Context + And start rest-API container and dependencies with auth token TTL "10000"ms and refresh token TTL "20000"ms and cors endpoint refresh interval 5s + + Scenario: Creation of a device with a wrong format "status" field + api back-end parser (MOXy) should spot error on the format and the mico-service should reply with 400 error code + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When REST "POST" call at "/v1/_/devices" with JSON "{\"clientId\": \"wrongStatusDevice\", \"status\": \"FOOOO\"}" + Then REST response code is 400 + And REST response containing text "An error occurred during the parsing of the XML/JSON" + + Scenario: Update of "credentialService" configuration missing to include the "type" field but providing the "value" field + api back-end parser (MOXy) should spot error on the format and the mico-service should reply with 400 error code + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When REST "PUT" call at "/v1/_/serviceConfigurations/org.eclipse.kapua.service.authentication.credential.CredentialService" with JSON "{\"id\": \"org.eclipse.kapua.service.authentication.credential.CredentialService\", \"properties\": {\"property\": [{\"name\": \"password.minLength\", \"array\": false, \"encrypted\": false, \"value\": [\"13\"]}]}}" + Then REST response code is 400 + And REST response containing text "An error occurred during the parsing of the XML/JSON" + And REST response containing text "Illegal 'null' value for 'property.type' for parameter: password.minLength" + + Scenario: Update of an user setting a wrong format "expirationDate" field + api back-end parser (MOXy) should spot error on the format and the micro-service should reply with 400 error code + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + #now let's try to update kapua-broker with a not parsable expiration date + When REST "PUT" call at "/v1/_/users/Ag" with JSON "{\"optlock\": 1, \"name\": \"kapua-broker\", \"expirationDate\": \"randomwrongvaluenotparsabledate\"}" + Then REST response code is 400 + And REST response containing text "An error occurred during the parsing of the XML/JSON" + + Scenario: Update of "credentialService" configuration missing to include the "type" field AND the "value" field + api back-end parser (MOXy) should NOT spot error because we are trying to update a property with a "default" value + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + #the json is a huge string but basically it's a complete set of properties for the CredentialService, where the "password.minLength" property we want to set has missing "type" and "value" fields (we want to set an unlimited value for it) + When REST "PUT" call at "/v1/_/serviceConfigurations/org.eclipse.kapua.service.authentication.credential.CredentialService" with JSON "{\"id\": \"org.eclipse.kapua.service.authentication.credential.CredentialService\", \"properties\": {\"property\": [{\"name\": \"lockoutPolicy.resetAfter\", \"array\": false, \"encrypted\": false, \"type\": \"Integer\", \"value\": [\"3800\"]}, {\"name\": \"password.minLength\", \"array\": false, \"encrypted\": false}, {\"name\": \"lockoutPolicy.lockDuration\", \"array\": false, \"encrypted\": false, \"type\": \"Integer\", \"value\": [\"10800\"]}, {\"name\": \"lockoutPolicy.enabled\", \"array\": false, \"encrypted\": false, \"type\": \"Boolean\", \"value\": [\"true\"]}, {\"name\": \"lockoutPolicy.maxFailures\", \"array\": false, \"encrypted\": false, \"type\": \"Integer\", \"value\": [\"3\"]}]}}" + Then REST response code is 204 + + @teardown + Scenario: Stop full docker environment + Given Stop full docker environment + And Clean Locator Instance + + diff --git a/rest-api/web/src/main/java/org/eclipse/kapua/app/api/web/MoxyJsonFeatureCustomJsonProvider.java b/rest-api/web/src/main/java/org/eclipse/kapua/app/api/web/MoxyJsonFeatureCustomJsonProvider.java new file mode 100644 index 00000000000..13853559347 --- /dev/null +++ b/rest-api/web/src/main/java/org/eclipse/kapua/app/api/web/MoxyJsonFeatureCustomJsonProvider.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2016, 2022 Eurotech and/or its affiliates and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eurotech - initial API and implementation + *******************************************************************************/ +package org.eclipse.kapua.app.api.web; + +import org.glassfish.jersey.internal.InternalProperties; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.Provider; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.helpers.DefaultValidationEventHandler; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +public class MoxyJsonFeatureCustomJsonProvider implements Feature { + //A custom feature used to set a custom moxyJsonProvider + + private static final String JSON_FEATURE = MoxyJsonFeatureCustomJsonProvider.class.getSimpleName(); + + @Override + public boolean configure(final FeatureContext context) { + final Configuration config = context.getConfiguration(); + // Disable other JSON providers. In this way the org.glassfish.jersey.moxy.json.MoxyJsonFeature (registered as default by MOXy) will skip the registration of the default provider + context.property(PropertiesHelper.getPropertyNameForRuntime(InternalProperties.JSON_FEATURE, config.getRuntimeType()), + JSON_FEATURE); + context.register(CustomMoxyJsonProvider.class); + return true; + } + + @Provider + public static class CustomMoxyJsonProvider extends org.glassfish.jersey.moxy.json.internal.ConfigurableMoxyJsonProvider { + //A custom moxyJsonProvider that sets the unmarshaller validationEventHandler to the default one. This one allows to propagate exceptions to the stack when an error is found (for example, when an exception has been thrown from one of our custom "xmlAdapters") + @Override + protected void preReadFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, Unmarshaller unmarshaller) throws JAXBException { + super.preReadFrom(type, genericType, annotations, mediaType, httpHeaders, unmarshaller); + unmarshaller.setEventHandler(new DefaultValidationEventHandler()); + } + } +} diff --git a/rest-api/web/src/main/java/org/eclipse/kapua/app/api/web/RestApisApplication.java b/rest-api/web/src/main/java/org/eclipse/kapua/app/api/web/RestApisApplication.java index 99db488f73f..ff0fc5a5e62 100644 --- a/rest-api/web/src/main/java/org/eclipse/kapua/app/api/web/RestApisApplication.java +++ b/rest-api/web/src/main/java/org/eclipse/kapua/app/api/web/RestApisApplication.java @@ -60,12 +60,12 @@ protected void configure() { property(ServerProperties.WADL_FEATURE_DISABLE, true); //Manually adding MOXyJSONFeature - register(org.glassfish.jersey.moxy.json.MoxyJsonFeature.class); register(MoxyJsonConfigContextResolver.class); register(UriConnegFilter.class); register(JaxbContextResolver.class); register(KapuaSerializableBodyWriter.class); register(ListBodyWriter.class); + register(MoxyJsonFeatureCustomJsonProvider.class); register(new ContainerLifecycleListener() { diff --git a/service/api/src/main/java/org/eclipse/kapua/model/xml/adapters/XmlPropertiesAdapter.java b/service/api/src/main/java/org/eclipse/kapua/model/xml/adapters/XmlPropertiesAdapter.java index 87cebc3e434..4b2a8d52765 100644 --- a/service/api/src/main/java/org/eclipse/kapua/model/xml/adapters/XmlPropertiesAdapter.java +++ b/service/api/src/main/java/org/eclipse/kapua/model/xml/adapters/XmlPropertiesAdapter.java @@ -43,7 +43,7 @@ public Map unmarshal(V[] properties) { .stream() .peek(adaptedProp -> { if (adaptedProp.getType() == null && adaptedProp.getValues() != null) { - throw new InternalError("null value for property.type parameter"); + throw new IllegalArgumentException("Illegal 'null' value for 'property.type' for parameter: " + adaptedProp.getName()); } }) .filter(adaptedProp -> xmlPropertyAdapters.containsKey((adaptedProp.getType()))) diff --git a/service/api/src/test/java/org/eclipse/kapua/model/xml/adapters/XmlPropertiesAdapterTest.java b/service/api/src/test/java/org/eclipse/kapua/model/xml/adapters/XmlPropertiesAdapterTest.java index 278a0e17c09..a0776ecadf5 100644 --- a/service/api/src/test/java/org/eclipse/kapua/model/xml/adapters/XmlPropertiesAdapterTest.java +++ b/service/api/src/test/java/org/eclipse/kapua/model/xml/adapters/XmlPropertiesAdapterTest.java @@ -254,7 +254,7 @@ public void testUnmarshallingMissingType() { //and an instance final XmlPropertiesAdapter instance = new TestPropertiesAdapter(adapters); //When I unmarshal - Assert.assertThrows(InternalError.class, () -> instance.unmarshal(new TestPropertyAdapted[]{ + Assert.assertThrows(IllegalArgumentException.class, () -> instance.unmarshal(new TestPropertyAdapted[]{ new TestPropertyAdapted("aString", TestTypes.First, "TheString"), new TestPropertyAdapted("aBoolean", TestTypes.Second, "false", "true"), new TestPropertyAdapted("anotherValue", null, "42")