diff --git a/src/main/java/org/dependencytrack/model/Tag.java b/src/main/java/org/dependencytrack/model/Tag.java index f1d1b1fe42..5e7367cfae 100644 --- a/src/main/java/org/dependencytrack/model/Tag.java +++ b/src/main/java/org/dependencytrack/model/Tag.java @@ -73,6 +73,13 @@ public class Tag implements Serializable { @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) private List projects; + public Tag() { + } + + public Tag(final String name) { + this.name = name; + } + public long getId() { return id; } diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index c681a43d0c..0da5464604 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -32,7 +32,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; -import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.StringUtils; @@ -45,6 +44,7 @@ import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; +import org.dependencytrack.model.Tag; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.notification.NotificationConstants.Title; import org.dependencytrack.notification.NotificationGroup; @@ -80,11 +80,14 @@ import java.io.IOException; import java.io.InputStream; import java.security.Principal; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.UUID; +import static java.util.function.Predicate.not; + /** * JAX-RS resources for processing bill-of-material (bom) documents. * @@ -92,7 +95,7 @@ * @since 3.0.0 */ @Path("/v1/bom") -@Tag(name = "bom") +@io.swagger.v3.oas.annotations.tags.Tag(name = "bom") @SecurityRequirements({ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") @@ -367,14 +370,17 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.BOM_UPLOAD) - public Response uploadBom(@FormDataParam("project") String projectUuid, - @DefaultValue("false") @FormDataParam("autoCreate") boolean autoCreate, - @FormDataParam("projectName") String projectName, - @FormDataParam("projectVersion") String projectVersion, - @FormDataParam("parentName") String parentName, - @FormDataParam("parentVersion") String parentVersion, - @FormDataParam("parentUUID") String parentUUID, - @Parameter(schema = @Schema(type = "string")) @FormDataParam("bom") final List artifactParts) { + public Response uploadBom( + @FormDataParam("project") String projectUuid, + @DefaultValue("false") @FormDataParam("autoCreate") boolean autoCreate, + @FormDataParam("projectName") String projectName, + @FormDataParam("projectVersion") String projectVersion, + @FormDataParam("projectTags") String projectTags, + @FormDataParam("parentName") String parentName, + @FormDataParam("parentVersion") String parentVersion, + @FormDataParam("parentUUID") String parentUUID, + @Parameter(schema = @Schema(type = "string")) @FormDataParam("bom") final List artifactParts + ) { if (projectUuid != null) { // behavior in v3.0.0 try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, projectUuid); @@ -404,7 +410,10 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified parent project is forbidden").build(); } } - project = qm.createProject(trimmedProjectName, null, trimmedProjectVersion, null, parent, null, true, true); + final List tags = (projectTags != null && !projectTags.isBlank()) + ? Arrays.stream(projectTags.split(",")).map(String::trim).filter(not(String::isEmpty)).map(Tag::new).toList() + : null; + project = qm.createProject(trimmedProjectName, null, trimmedProjectVersion, tags, parent, null, true, true); Principal principal = getPrincipal(); qm.updateNewProjectACL(project, principal); } else { diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 88b8711d68..425763ca78 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -44,6 +44,9 @@ import org.dependencytrack.resources.v1.exception.JsonMappingExceptionMapper; import org.dependencytrack.resources.v1.vo.BomSubmitRequest; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; @@ -51,9 +54,11 @@ import org.junit.Test; import jakarta.json.JsonObject; +import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.UUID; @@ -63,6 +68,7 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.apache.commons.io.IOUtils.resourceToByteArray; +import static org.apache.commons.io.IOUtils.resourceToString; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_ENABLED; @@ -871,6 +877,39 @@ public void uploadBomAutoCreateWithTagsTest() throws Exception { .containsExactlyInAnyOrder("tag1", "tag2"); } + @Test + public void uploadBomAutoCreateWithTagsMultipartTest() throws Exception { + initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); + + final var multiPart = new FormDataMultiPart() + .field("bom", resourceToString("/unit/bom-1.xml", StandardCharsets.UTF_8), MediaType.APPLICATION_XML_TYPE) + .field("projectName", "Acme Example") + .field("projectVersion", "1.0") + .field("projectTags", "tag1,tag2") + .field("autoCreate", "true"); + + // NB: The GrizzlyConnectorProvider doesn't work with MultiPart requests. + // https://github.com/eclipse-ee4j/jersey/issues/5094 + final var client = ClientBuilder.newClient(new ClientConfig() + .connectorProvider(new HttpUrlConnectorProvider())); + + final Response response = client.target(jersey.target(V1_BOM).getUri()).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(multiPart, multiPart.getMediaType())); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + { + "token": "${json-unit.any-string}" + } + """); + + final Project project = qm.getProject("Acme Example", "1.0"); + assertThat(project).isNotNull(); + assertThat(project.getTags()) + .extracting(Tag::getName) + .containsExactlyInAnyOrder("tag1", "tag2"); + } + @Test public void uploadBomUnauthorizedTest() throws Exception { String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml"));