Commit 42afcc30 authored by Kostis Trantzas's avatar Kostis Trantzas
Browse files

Merge branch 'range-interval-type-validation' into 'develop'

Implemented range interval validation and type validation for Service...

See merge request !81
parents 5c3d12ee 8e416561
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -5,3 +5,4 @@
/.classpath
/.settings
/org.etsi.osl.tmf.api.iml
/.idea/
+19 −10
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@@ -18,15 +19,23 @@ public class RestExceptionHandler extends ResponseEntityExceptionHandler {
	@Override
	protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
			HttpHeaders headers, HttpStatusCode status, WebRequest request) {
		String error = "Malformed JSON request";
		ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, error, ex);
		return new ResponseEntity<Object>(apiError, apiError.getStatus());
		return buildResponseEntity(ex);
	}

	// other exception handlers below

	private ResponseEntity<Object> buildResponseEntity(ApiError apiError) {
		return new ResponseEntity<>(apiError, apiError.getStatus());
	@Override
	protected ResponseEntity<Object> handleMethodArgumentNotValid(
			MethodArgumentNotValidException ex,
			HttpHeaders headers,
			HttpStatusCode status,
			WebRequest request) {
		return buildResponseEntity(ex);
	}
	// other exception handlers below

	private ResponseEntity<Object> buildResponseEntity(Throwable ex) {
		String error = "Malformed JSON request";
		ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, error, ex);
		return new ResponseEntity<Object>(apiError, apiError.getStatus());
	}
}
+12 −8
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.commons.io.IOUtils;
import org.etsi.osl.centrallog.client.CLevel;
import org.etsi.osl.centrallog.client.CentralLogger;
import org.etsi.osl.sd.model.ServiceDescriptor;
import org.etsi.osl.tmf.common.model.Attachment;
@@ -42,6 +41,7 @@ import org.etsi.osl.tmf.scm633.model.ServiceSpecificationCreate;
import org.etsi.osl.tmf.scm633.model.ServiceSpecificationUpdate;
import org.etsi.osl.tmf.scm633.reposervices.ServiceSpecificationRepoService;
import org.etsi.osl.tmf.util.AddUserAsOwnerToRelatedParties;
import org.etsi.osl.tmf.util.ServiceSpecificationValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -52,15 +52,10 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -94,12 +89,21 @@ public class ServiceSpecificationApiController implements ServiceSpecificationAp
	@Autowired
	private CentralLogger centralLogger;

	@Autowired
	private ServiceSpecificationValidator serviceSpecificationValidator;

	@org.springframework.beans.factory.annotation.Autowired
	public ServiceSpecificationApiController(ObjectMapper objectMapper, HttpServletRequest request) {
		this.objectMapper = objectMapper;
		this.request = request;
	}

	// Custom validation resulting from ServiceSpecCharacteristicValue range interval and type validation (https://labs.etsi.org/rep/groups/osl/code/-/epics/30)
	@InitBinder
	protected void initBinder(WebDataBinder binder) {
		binder.addValidators(serviceSpecificationValidator);
	}

	@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')" )
	public ResponseEntity<ServiceSpecification> createServiceSpecification(
			@Parameter(description = "The ServiceSpecification to be created", required = true) @Valid @RequestBody ServiceSpecificationCreate serviceSpecification) {
+108 −0
Original line number Diff line number Diff line
package org.etsi.osl.tmf.util;

import org.etsi.osl.tmf.common.model.Any;
import org.etsi.osl.tmf.common.model.ERangeInterval;
import org.etsi.osl.tmf.common.model.EValueType;
import org.etsi.osl.tmf.scm633.model.ServiceSpecCharacteristicValue;
import org.etsi.osl.tmf.scm633.model.ServiceSpecificationUpdate;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Objects;

@Component
public class ServiceSpecificationValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return ServiceSpecificationUpdate.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ServiceSpecificationUpdate update = (ServiceSpecificationUpdate) target;
        if (update.getServiceSpecCharacteristic() == null) {
            return;
        }
        boolean invalid = update.getServiceSpecCharacteristic().stream()
                .flatMap(serviceSpecCharacteristic ->
                        serviceSpecCharacteristic.getServiceSpecCharacteristicValue().stream())
                .anyMatch(serviceSpecCharacteristicValue ->
                        !validateType(serviceSpecCharacteristicValue) || !isWithinRangeInterval(serviceSpecCharacteristicValue));
        if (invalid) {
            errors.reject("invalid.request");
        }
    }

    private boolean validateType(ServiceSpecCharacteristicValue serviceSpecCharacteristicValue) {
        final String INTEGER_REGEX = "[-+]?\\d+";
        final String FLOAT_REGEX = "[-+]?\\d*([.,]\\d+)?([eE][-+]?\\d+)?";
        final String BOOLEAN_REGEX = "(?i)true|false";
        final DateTimeFormatter ISO_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
        if (serviceSpecCharacteristicValue.getValueType() == null) {
            return true;
        }
        Any value = serviceSpecCharacteristicValue.getValue();
        if (value == null) {
            return true;
        }
        String stringValue = value.getValue();
        if (stringValue == null || stringValue.isBlank()) {
            return true;
        }
        try {
            return switch (EValueType.getEnum(serviceSpecCharacteristicValue.getValueType())) {
                case INTEGER, SMALLINT, lONGINT -> stringValue.matches(INTEGER_REGEX);
                case FLOAT -> stringValue.matches(FLOAT_REGEX);
                case BOOLEAN -> stringValue.matches(BOOLEAN_REGEX) || stringValue.matches(INTEGER_REGEX);
                case TIMESTAMP -> {
                    try {
                        LocalDateTime.parse(stringValue, ISO_DATE_TIME);
                        yield true;
                    } catch (DateTimeParseException e) {
                        yield false;
                    }
                }
                default -> true;
            };
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

    private boolean isWithinRangeInterval(ServiceSpecCharacteristicValue serviceSpecCharacteristicValue) {
        if (serviceSpecCharacteristicValue.getRangeInterval() == null) {
            return true;
        }
        if (!Objects.equals(serviceSpecCharacteristicValue.getValueType(), EValueType.INTEGER.getValue()) &&
                !Objects.equals(serviceSpecCharacteristicValue.getValueType(), EValueType.SMALLINT.getValue()) &&
                !Objects.equals(serviceSpecCharacteristicValue.getValueType(), EValueType.lONGINT.getValue())) {
            return true;
        }
        Any value = serviceSpecCharacteristicValue.getValue();
        if (value == null) {
            return true;
        }
        String stringValue = value.getValue();
        if (stringValue == null || stringValue.isBlank()) {
            return true;
        }
        int valueFrom = serviceSpecCharacteristicValue.getValueFrom() != null ? serviceSpecCharacteristicValue.getValueFrom() : Integer.MIN_VALUE;
        int valueTo = serviceSpecCharacteristicValue.getValueTo() != null ? serviceSpecCharacteristicValue.getValueTo() : Integer.MAX_VALUE;
        try {
            int intValue = Integer.parseInt(stringValue);
            return switch (ERangeInterval.getEnum(serviceSpecCharacteristicValue.getRangeInterval())) {
                case OPEN -> intValue > valueFrom && intValue < valueTo;
                case CLOSED -> intValue >= valueFrom && intValue <= valueTo;
                case CLOSED_BOTTOM -> intValue >= valueFrom && intValue < valueTo;
                case CLOSED_TOP -> intValue > valueFrom && intValue <= valueTo;
            };
        } catch (IllegalArgumentException e) {
            return false;
        }
    }
}
+71 −0
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.etsi.osl.services.api.BaseIT;
@@ -433,4 +434,74 @@ public class ServiceSpecificationApiControllerTest extends BaseIT {

        return response;
    }

    @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" })
    @Test
    public void testServiceSpecInvalidRangeIntervalIsBadRequest() throws Exception {
        File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecInvalidRangeInterval.json");
        InputStream in = new FileInputStream(serviceSpec);
        String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8);
        ServiceSpecificationCreate serviceSpecificationCreate = JsonUtils.toJsonObj(serviceSpecText, ServiceSpecificationCreate.class);
        mvc.perform(MockMvcRequestBuilders.post("/serviceCatalogManagement/v4/serviceSpecification")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(serviceSpecificationCreate)))
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(status().isBadRequest());
    }

    @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" })
    @Test
    public void testServiceSpecInvalidTypesIsBadRequest() throws Exception {
        File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecInvalidTypes.json");
        InputStream in = new FileInputStream(serviceSpec);
        String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8);
        ServiceSpecificationCreate serviceSpecificationCreate = JsonUtils.toJsonObj(serviceSpecText, ServiceSpecificationCreate.class);
        mvc.perform(MockMvcRequestBuilders.post("/serviceCatalogManagement/v4/serviceSpecification")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(serviceSpecificationCreate)))
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(status().isBadRequest());
    }

    @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" })
    @Test
    public void testServiceSpecValidRangeIntervalIsOk() throws Exception {
        final int existingServiceSpecs = specRepoService.findAll().size();
        File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecValidRangeInterval.json");
        InputStream in = new FileInputStream(serviceSpec);
        String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8);
        ServiceSpecificationCreate serviceSpecificationCreate = JsonUtils.toJsonObj(serviceSpecText, ServiceSpecificationCreate.class);
        String response = mvc.perform(MockMvcRequestBuilders.post("/serviceCatalogManagement/v4/serviceSpecification")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(serviceSpecificationCreate)))
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();
        assertThat(specRepoService.findAll().size()).isEqualTo(existingServiceSpecs + 1);
        ServiceSpecification responseSpec = JsonUtils.toJsonObj(response, ServiceSpecification.class);
        assertThat(responseSpec.getName()).isEqualTo("Test Spec");
    }

    @WithMockUser(username = "osadmin", roles = { "ADMIN","USER" })
    @Test
    public void testServiceSpecValidTypesIsOk() throws Exception {
        final int existingServiceSpecs = specRepoService.findAll().size();
        File serviceSpec = new File("src/test/resources/reposervices/scm633/testServiceSpecValidTypes.json");
        InputStream in = new FileInputStream(serviceSpec);
        String serviceSpecText = IOUtils.toString(in, StandardCharsets.UTF_8);
        ServiceSpecificationCreate serviceSpecificationCreate = JsonUtils.toJsonObj(serviceSpecText, ServiceSpecificationCreate.class);
        String response = mvc.perform(MockMvcRequestBuilders.post("/serviceCatalogManagement/v4/serviceSpecification")
                        .with(SecurityMockMvcRequestPostProcessors.csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(JsonUtils.toJson(serviceSpecificationCreate)))
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();
        assertThat(specRepoService.findAll().size()).isEqualTo(existingServiceSpecs + 1);
        ServiceSpecification responseSpec = JsonUtils.toJsonObj(response, ServiceSpecification.class);
        assertThat(responseSpec.getName()).isEqualTo("Test Spec");
    }
}
Loading