Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#455 validate version on provider and module upload #458

Merged
merged 3 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[semantic version

=== fixes

* https://github.com/PacoVK/tapir/pull/455[#455 - Missing Version Validation on Module Upload Allows Unsupported Formats]
* https://github.com/PacoVK/tapir/pull/433[#433 - Validation of module version]

=== added

=== changed
Expand Down
4 changes: 2 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ When you publish a Terraform module, you need a [DeployKey](#deploy-keys) giving
##### Prerequisites:
* The package name and version must be unique in the top-level namespace.
* You need to specify a module namespace, a module name and the modules corresponding provider. For example `myorg/vpc/aws`.
* Versioning must follow [Semantic Versioning](https://semver.org) specs
* Versioning must follow [Semantic Versioning](https://semver.org) specs, can have an optional `v` prefix. (e.g.`1.0.0` or `v1.0.0`)
* Currently only `.zip` is supported.

**NOTE**: The zipped module directory layout should follow the [Terraform module structure](https://www.terraform.io/docs/language/modules/develop/structure.html).
Expand Down Expand Up @@ -122,7 +122,7 @@ To create and build the provider it is highly recommended to use the [official H
##### Prerequisites:
* The provider name (aka. type) must be unique in the top-level namespace.
* You need to specify a provider namespace, a provider type. For example `myorg/my-provider`.
* Versioning must follow [Semantic Versioning](https://semver.org) specs
* Versioning must follow [Semantic Versioning](https://semver.org) specs and **must** have a `v` prefix. (e.g. `v1.0.0`)
* Currently only `.zip` is supported.
* The `.zip` must contain all files that are described in [how to prepare release](https://developer.hashicorp.com/terraform/registry/providers/publishing#preparing-your-provider).

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/api/Modules.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/api/Providers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/api/mapper/exceptions/TapirExceptionMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/core/exceptions/InvalidVersionException.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package core.exceptions;

public interface RegistryComplianceException {
}
20 changes: 20 additions & 0 deletions src/main/java/core/service/VersionService.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
12 changes: 12 additions & 0 deletions src/test/java/api/mapper/exceptions/TapirExceptionMapperTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,6 +15,7 @@
class TapirExceptionMapperTest {

TapirExceptionMapper mapper = new TapirExceptionMapper();

@Test
void getCorrectStatusWhenModuleNotFoundExceptionOccurs() {
TapirException notFoundException = new ModuleNotFoundException("fake-id-version");
Expand Down Expand Up @@ -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<ErrorResponse.ErrorMessage> 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");
}
}
29 changes: 29 additions & 0 deletions src/test/java/core/service/VersionServiceTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}