diff --git a/src/main/java/api/Modules.java b/src/main/java/api/Modules.java index d4036b3e..7f522f77 100644 --- a/src/main/java/api/Modules.java +++ b/src/main/java/api/Modules.java @@ -1,8 +1,10 @@ package api; +import core.exceptions.InvalidVersionException; import core.exceptions.StorageException; import core.service.ModuleService; import core.service.StorageService; +import core.service.VersionService; import core.storage.util.StorageUtil; import core.terraform.ArtifactVersion; import core.terraform.Module; @@ -54,6 +56,9 @@ public Response getModuleByName(String namespace, String name, String provider) public Response uploadModule( String namespace, String name, String provider, String version, FormData archive) throws Exception { + if(!VersionService.isValidModuleVersion(version)) { + throw new InvalidVersionException(version); + } Module module = new Module(namespace, name, provider, version); module.setPublished_at(Instant.now()); archive.setEntity(module); diff --git a/src/main/java/api/Providers.java b/src/main/java/api/Providers.java index f5612703..45e58725 100644 --- a/src/main/java/api/Providers.java +++ b/src/main/java/api/Providers.java @@ -2,7 +2,9 @@ import api.dto.ProviderTerraformDto; import api.dto.ProviderVersionsDto; +import core.exceptions.InvalidVersionException; import core.service.ProviderService; +import core.service.VersionService; import core.terraform.Provider; import core.upload.FormData; import core.upload.service.UploadService; @@ -67,6 +69,9 @@ public Response getDownloadUrl( @Path("{namespace}/{type}/{version}") public Response uploadProvider(String namespace, String type, String version, FormData archive) throws Exception { + if(!VersionService.isValidProviderVersion(version)) { + throw new InvalidVersionException(version); + } Provider provider = new Provider(namespace, type); provider.setPublished_at(Instant.now()); archive.setEntity(provider); diff --git a/src/main/java/api/mapper/exceptions/TapirExceptionMapper.java b/src/main/java/api/mapper/exceptions/TapirExceptionMapper.java index 7c2c2e7f..4ee2713e 100644 --- a/src/main/java/api/mapper/exceptions/TapirExceptionMapper.java +++ b/src/main/java/api/mapper/exceptions/TapirExceptionMapper.java @@ -2,6 +2,7 @@ import api.mapper.exceptions.response.ErrorResponse; import core.exceptions.NotFoundException; +import core.exceptions.RegistryComplianceException; import core.exceptions.TapirException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -26,6 +27,9 @@ public Response toResponse(TapirException e) { if (e instanceof NotFoundException) { status = Response.Status.NOT_FOUND; } + if(e instanceof RegistryComplianceException) { + status = Response.Status.BAD_REQUEST; + } return Response.status(status) .entity(new ErrorResponse(errorId, errorMessage)) .header("Content-Type", MediaType.APPLICATION_JSON).build(); diff --git a/src/main/java/core/exceptions/InvalidVersionException.java b/src/main/java/core/exceptions/InvalidVersionException.java new file mode 100644 index 00000000..b9c457d4 --- /dev/null +++ b/src/main/java/core/exceptions/InvalidVersionException.java @@ -0,0 +1,14 @@ +package core.exceptions; + +public class InvalidVersionException extends TapirException implements RegistryComplianceException { + + private static final String errorMessage = "Version %s is invalid and does not comply with the Terraform registry versioning specification"; + + public InvalidVersionException(String version) { + super(String.format(errorMessage, version)); + } + + public InvalidVersionException(String version, Throwable cause) { + super(String.format(errorMessage, version), cause); + } +} diff --git a/src/main/java/core/exceptions/RegistryComplianceException.java b/src/main/java/core/exceptions/RegistryComplianceException.java new file mode 100644 index 00000000..c1ce3d19 --- /dev/null +++ b/src/main/java/core/exceptions/RegistryComplianceException.java @@ -0,0 +1,4 @@ +package core.exceptions; + +public interface RegistryComplianceException { +} diff --git a/src/main/java/core/service/VersionService.java b/src/main/java/core/service/VersionService.java new file mode 100644 index 00000000..2903f06d --- /dev/null +++ b/src/main/java/core/service/VersionService.java @@ -0,0 +1,20 @@ +package core.service; + +import java.util.regex.Pattern; + +public class VersionService { + + // see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + private static final String SEMVER_PATTERN_TEMPLATE = "^%s(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"; + + private static final Pattern moduleSemVerPattern = Pattern.compile(String.format(SEMVER_PATTERN_TEMPLATE, "(v?)")); + private static final Pattern providerSemVerPattern = Pattern.compile(String.format(SEMVER_PATTERN_TEMPLATE, "v")); + + public static boolean isValidModuleVersion(String version) { + return moduleSemVerPattern.matcher(version).matches(); + } + + public static boolean isValidProviderVersion(String version) { + return providerSemVerPattern.matcher(version).matches(); + } +} diff --git a/src/test/java/api/mapper/exceptions/TapirExceptionMapperTest.java b/src/test/java/api/mapper/exceptions/TapirExceptionMapperTest.java index 70b04c12..e4989571 100644 --- a/src/test/java/api/mapper/exceptions/TapirExceptionMapperTest.java +++ b/src/test/java/api/mapper/exceptions/TapirExceptionMapperTest.java @@ -1,5 +1,6 @@ package api.mapper.exceptions; +import core.exceptions.InvalidVersionException; import static org.junit.jupiter.api.Assertions.assertEquals; import api.mapper.exceptions.response.ErrorResponse; @@ -14,6 +15,7 @@ class TapirExceptionMapperTest { TapirExceptionMapper mapper = new TapirExceptionMapper(); + @Test void getCorrectStatusWhenModuleNotFoundExceptionOccurs() { TapirException notFoundException = new ModuleNotFoundException("fake-id-version"); @@ -43,4 +45,14 @@ void getCorrectStatusWhenRuntimeExceptionOccurs() { assertEquals(errors.size(), 1); assertEquals(errors.get(0).getMessage(), "Module/ Provider with id fake-id-version could not be found"); } + + @Test + void getCorrectStatusWhenRegistryComplianceExceptionOccurs() { + TapirException invalidVersionException = new InvalidVersionException("wrong-version"); + Response badRequestResponse = mapper.toResponse(invalidVersionException); + List errors = ((ErrorResponse) badRequestResponse.getEntity()).getErrors(); + assertEquals(badRequestResponse.getStatus(), 400); + assertEquals(errors.size(), 1); + assertEquals(errors.get(0).getMessage(), "Version wrong-version is invalid and does not comply with the Terraform registry versioning specification"); + } } \ No newline at end of file diff --git a/src/test/java/core/service/VersionServiceTest.java b/src/test/java/core/service/VersionServiceTest.java new file mode 100644 index 00000000..63660a46 --- /dev/null +++ b/src/test/java/core/service/VersionServiceTest.java @@ -0,0 +1,29 @@ +package core.service; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + +class VersionServiceTest { + + @Test + void isValidModuleVersion() { + assertTrue(VersionService.isValidModuleVersion("1.0.0")); + assertTrue(VersionService.isValidModuleVersion("v1.0.0")); + assertTrue(VersionService.isValidModuleVersion("2.1.3-alpha")); + assertTrue(VersionService.isValidModuleVersion("0.0.1-beta+exp.sha.5114f85")); + assertTrue(VersionService.isValidModuleVersion("1.0.0+20130313144700")); + assertFalse(VersionService.isValidModuleVersion("1.0")); + assertFalse(VersionService.isValidModuleVersion("1.Foo.0")); + } + + @Test + void isValidProviderVersion() { + assertTrue(VersionService.isValidProviderVersion("v1.0.0")); + assertTrue(VersionService.isValidProviderVersion("v2.1.3-alpha")); + assertTrue(VersionService.isValidProviderVersion("v0.0.1-beta+exp.sha.5114f85")); + assertFalse(VersionService.isValidProviderVersion("1.0.0")); + assertFalse(VersionService.isValidProviderVersion("1.0.0+20130313144700")); + assertFalse(VersionService.isValidProviderVersion("v1.0")); + assertFalse(VersionService.isValidProviderVersion("v1.Foo.0")); + } +} \ No newline at end of file