-
-
Notifications
You must be signed in to change notification settings - Fork 580
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
Add /v1/project/batchDelete API method that deletes with SQL #4383
base: master
Are you sure you want to change the base?
Changes from all commits
2a0a7e5
7035a8c
58eff60
3450de7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -66,8 +66,10 @@ | |||||||||||||||||||||||||||||||||||||||||||
import jakarta.ws.rs.core.Response; | ||||||||||||||||||||||||||||||||||||||||||||
import javax.jdo.FetchGroup; | ||||||||||||||||||||||||||||||||||||||||||||
import java.security.Principal; | ||||||||||||||||||||||||||||||||||||||||||||
import java.util.ArrayList; | ||||||||||||||||||||||||||||||||||||||||||||
import java.util.Collection; | ||||||||||||||||||||||||||||||||||||||||||||
import java.util.HashMap; | ||||||||||||||||||||||||||||||||||||||||||||
import java.util.Iterator; | ||||||||||||||||||||||||||||||||||||||||||||
import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||
import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||||||
import java.util.Set; | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -804,6 +806,56 @@ public Response deleteProject( | |||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
@POST | ||||||||||||||||||||||||||||||||||||||||||||
@Path("/batchDelete") | ||||||||||||||||||||||||||||||||||||||||||||
@Consumes(MediaType.APPLICATION_JSON) | ||||||||||||||||||||||||||||||||||||||||||||
@Produces(MediaType.APPLICATION_JSON) | ||||||||||||||||||||||||||||||||||||||||||||
@Operation( | ||||||||||||||||||||||||||||||||||||||||||||
summary = "Deletes a list of projects specified by their UUIDs", | ||||||||||||||||||||||||||||||||||||||||||||
description = "<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong></p>" | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
@ApiResponses(value = { | ||||||||||||||||||||||||||||||||||||||||||||
@ApiResponse(responseCode = "204", description = "Projects removed successfully"), | ||||||||||||||||||||||||||||||||||||||||||||
@ApiResponse(responseCode = "207", description = "Access is forbidden to the projects listed in the response"), | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sold on this status code. Some surface-level research leads me to believe using this outside of WebDAV is not intended: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/207 Wouldn't it be better to atomically fail the request if deletion of one or more projects failed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point, the 207 indeed seems to be reserved for WebDAV. The reason for implementing it this way was based on the outcome of this discussion, where the suggestion by sebD was to return a 204 with a body containing the list of inaccessible projects. The 207 was my suggestion, and since I got a thumbs up I thought I would do it the same way in this PR. I see a value in letting the client know which projects that are inaccessible, so I'm leaning towards going with sebD's suggestion instead of just returning a 401. Would that be an acceptable solution? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think sebD's original comment was geared towards the situation where we proceed with deleting accessible projects when there have been inaccessible projects in the provided list. In that case you do need to communicate the partial success somehow. My point is that no projects should be deleted at all, if at least one of them is inaccessible. You can then still respond with We do this for tag endpoints already:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'll look into using ProblemDetails since that will make it consistent with how failed tag deletion is handled. Thanks for the tip! |
||||||||||||||||||||||||||||||||||||||||||||
@ApiResponse(responseCode = "401", description = "Unauthorized") | ||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||
@PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) | ||||||||||||||||||||||||||||||||||||||||||||
public Response deleteProjects(List<UUID> uuids) { | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using I think there should be some upper limit to the number of UUIDs being submitted in one go. We can always extend it later if people need it, but we can't reduce it later if larger batches end up causing problems. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good idea. I have tested it with up to 1000 UUIDs, so maybe that could be a reasonable max size to start with? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that sounds reasonable. |
||||||||||||||||||||||||||||||||||||||||||||
List<UUID> inaccessibleProjects = new ArrayList<>(); | ||||||||||||||||||||||||||||||||||||||||||||
try (QueryManager qm = new QueryManager()) { | ||||||||||||||||||||||||||||||||||||||||||||
for (Iterator<UUID> it = uuids.iterator(); it.hasNext();) { | ||||||||||||||||||||||||||||||||||||||||||||
UUID uuid = it.next(); | ||||||||||||||||||||||||||||||||||||||||||||
final Project project = qm.getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, that doesn't seem to be needed since Project.accessTeams is part of the default fetch group. ProjectQueryManager.hasAccess only needs that property from the project, so I'll change to the getObjectByUuid(Class, UUID) signature instead. |
||||||||||||||||||||||||||||||||||||||||||||
if (project != null) { | ||||||||||||||||||||||||||||||||||||||||||||
if (!qm.hasAccess(super.getPrincipal(), project)) { | ||||||||||||||||||||||||||||||||||||||||||||
LOGGER.warn(super.getPrincipal().getName() + " lacks access to project " + project | ||||||||||||||||||||||||||||||||||||||||||||
+ ", delete request denied" | ||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||
inaccessibleProjects.add(uuid); | ||||||||||||||||||||||||||||||||||||||||||||
it.remove(); | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||
LOGGER.warn("No project found by UUID " + uuid); | ||||||||||||||||||||||||||||||||||||||||||||
it.remove(); | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
if (uuids.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||
return Response.status(Response.Status.FORBIDDEN) | ||||||||||||||||||||||||||||||||||||||||||||
.entity(inaccessibleProjects) | ||||||||||||||||||||||||||||||||||||||||||||
.build(); | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
qm.deleteProjectsByUUIDs(uuids); | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
if (inaccessibleProjects.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||
return Response.status(Response.Status.NO_CONTENT).build(); | ||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||
return Response.status(207).entity(inaccessibleProjects).build(); | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
@PUT | ||||||||||||||||||||||||||||||||||||||||||||
@Path("/clone") | ||||||||||||||||||||||||||||||||||||||||||||
@Consumes(MediaType.APPLICATION_JSON) | ||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For completeness, how many RDBMSes have you tested with this? Having tested at least H2, MSSQL, and PostgreSQL would be good. We have a Docker Compose file for MSSQL here: https://github.com/DependencyTrack/dependency-track/blob/master/dev/docker-compose.mssql.yml
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've only tested it with H2 and PostgreSQL so far (H2 in unit tests, PostgreSQL in system tests) but I can try migrating my test data to MSSQL and test that as well. I'll get back to you with the results when done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to do a full load test or anything. We just need assurance that the queries work as expected. Sadly the small differences in SQL syntax between the different RDBMSes turn out to be tripwires more often than not. :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, true, and true :) But I'll need some data in the schema to pass the condition that the UUID list is not empty, so I'll try to migrate the relatively small exported sample that I created with pg_sample.