Skip to content

Commit

Permalink
Add option to deactivate old project versions on BOM upload
Browse files Browse the repository at this point in the history
Fixes #4532

Signed-off-by: Christoffer Rumohr <[email protected]>
  • Loading branch information
crumohr committed Jan 7, 2025
1 parent bcd2651 commit 7e6488e
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 1 deletion.
22 changes: 21 additions & 1 deletion src/main/java/org/dependencytrack/resources/v1/BomResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ public Response uploadBom(
@FormDataParam("parentVersion") String parentVersion,
@FormDataParam("parentUUID") String parentUUID,
@DefaultValue("false") @FormDataParam("isLatest") boolean isLatest,
@DefaultValue("false") @FormDataParam("isActiveExclusively") boolean isActiveExclusively,
@Parameter(schema = @Schema(type = "string")) @FormDataParam("bom") final List<FormDataBodyPart> artifactParts
) {
if (projectUuid != null) { // behavior in v3.0.0
Expand Down Expand Up @@ -454,7 +455,26 @@ public Response uploadBom(
return Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build();
}
}
return process(qm, project, artifactParts);
final var response = process(qm, project, artifactParts);
if (response.getStatusInfo() == Response.Status.OK && isActiveExclusively && trimmedProjectName != null) {
LOGGER.info("Deactivating old versions for project: " + trimmedProjectName);
if (!isLatest) {
var message = "Value \"isLatest=true\" required when \"isActiveExclusively=true\".";
LOGGER.error(message);
return Response.status(Response.Status.NOT_ACCEPTABLE).entity(message).build();
}
qm.getProjects(trimmedProjectName, true, false, null).getList(Project.class).forEach(p -> {
if (p.isLatest()) {
return;
} else if (!qm.hasAccess(super.getPrincipal(), p)) {
LOGGER.warn("Could not deactivate project, no access: " + p.getUuid() + " / " + p.getName());
return;
}
p.setActive(false);
qm.updateProject(p, true);
});
}
return response;
}
}
}
Expand Down
109 changes: 109 additions & 0 deletions src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,115 @@ public void uploadBomAutoCreateWithTagsMultipartTest() throws Exception {
.containsExactlyInAnyOrder("tag1", "tag2");
}

@Test
public void uploadBomAutoCreateIsLatestMultipartTest() 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")
.field("isLatest", "true");

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");
assertThat(project.isLatest()).isTrue();
}

@Test
public void uploadBomAutoCreateIsLatestIsActiveExclusivelyMultipartTest() throws Exception {
initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD);

final var multipartV1 = 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("autoCreate", "true")
.field("isLatest", "true")
.field("isActiveExclusively", "true");

final var multipartV2 = new FormDataMultiPart()
.field("bom", resourceToString("/unit/bom-1.xml", StandardCharsets.UTF_8), MediaType.APPLICATION_XML_TYPE)
.field("projectName", "Acme Example")
.field("projectVersion", "2.0")
.field("autoCreate", "true")
.field("isLatest", "true")
.field("isActiveExclusively", "true");

final var client = ClientBuilder.newClient(new ClientConfig()
.connectorProvider(new HttpUrlConnectorProvider()));

final Response responseV1 = client.target(jersey.target(V1_BOM).getUri()).request()
.header(X_API_KEY, apiKey)
.post(Entity.entity(multipartV1, multipartV1.getMediaType()));
assertThat(responseV1.getStatus()).isEqualTo(200);
assertThatJson(getPlainTextBody(responseV1)).isEqualTo("""
{
"token": "${json-unit.any-string}"
}
""");

final Response responseV2 = client.target(jersey.target(V1_BOM).getUri()).request()
.header(X_API_KEY, apiKey)
.post(Entity.entity(multipartV2, multipartV2.getMediaType()));
assertThat(responseV2.getStatus()).isEqualTo(200);
assertThatJson(getPlainTextBody(responseV2)).isEqualTo("""
{
"token": "${json-unit.any-string}"
}
""");

final Project projectV1 = qm.getProject("Acme Example", "1.0");
assertThat(projectV1).isNotNull();
assertThat(projectV1.isLatest()).isFalse();
assertThat(projectV1.isActive()).isFalse();

final Project projectV2 = qm.getProject("Acme Example", "2.0");
assertThat(projectV2).isNotNull();
assertThat(projectV2.isLatest()).isTrue();
assertThat(projectV2.isActive()).isTrue();
}

@Test
public void uploadBomAutoCreateIsNotLatestIsActiveExclusivelyMultipartTest() 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("autoCreate", "true")
//.field("isLatest", "false") // this is implied with @DefaultValue("false")
.field("isActiveExclusively", "true");

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(406);
assertThat(getPlainTextBody(response)).isEqualTo("Value \"isLatest=true\" required when \"isActiveExclusively=true\".");
}

@Test
public void uploadBomUnauthorizedTest() throws Exception {
String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml"));
Expand Down

0 comments on commit 7e6488e

Please sign in to comment.