mirror of
https://github.com/DependencyTrack/dependency-track.git
synced 2025-10-19 16:03:19 +00:00
Add REST endpoints for bulk tagging & un-tagging of projects
Signed-off-by: nscuro <nscuro@protonmail.com>
This commit is contained in:
parent
a0407b46af
commit
c41717f515
8 changed files with 498 additions and 50 deletions
|
@ -31,7 +31,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
/**
|
/**
|
||||||
* @since 4.11.0
|
* @since 4.11.0
|
||||||
*/
|
*/
|
||||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||||
@Constraint(validatedBy = {})
|
@Constraint(validatedBy = {})
|
||||||
@Retention(RUNTIME)
|
@Retention(RUNTIME)
|
||||||
@ReportAsSingleViolation
|
@ReportAsSingleViolation
|
||||||
|
|
|
@ -20,10 +20,7 @@ package org.dependencytrack.persistence;
|
||||||
|
|
||||||
import alpine.common.logging.Logger;
|
import alpine.common.logging.Logger;
|
||||||
import alpine.event.framework.Event;
|
import alpine.event.framework.Event;
|
||||||
import alpine.model.ApiKey;
|
|
||||||
import alpine.model.IConfigProperty;
|
import alpine.model.IConfigProperty;
|
||||||
import alpine.model.Team;
|
|
||||||
import alpine.model.UserPrincipal;
|
|
||||||
import alpine.persistence.PaginatedResult;
|
import alpine.persistence.PaginatedResult;
|
||||||
import alpine.resources.AlpineRequest;
|
import alpine.resources.AlpineRequest;
|
||||||
import com.github.packageurl.MalformedPackageURLException;
|
import com.github.packageurl.MalformedPackageURLException;
|
||||||
|
@ -33,7 +30,6 @@ import org.dependencytrack.event.IndexEvent;
|
||||||
import org.dependencytrack.model.Component;
|
import org.dependencytrack.model.Component;
|
||||||
import org.dependencytrack.model.ComponentIdentity;
|
import org.dependencytrack.model.ComponentIdentity;
|
||||||
import org.dependencytrack.model.ComponentProperty;
|
import org.dependencytrack.model.ComponentProperty;
|
||||||
import org.dependencytrack.model.ConfigPropertyConstants;
|
|
||||||
import org.dependencytrack.model.Project;
|
import org.dependencytrack.model.Project;
|
||||||
import org.dependencytrack.model.RepositoryMetaComponent;
|
import org.dependencytrack.model.RepositoryMetaComponent;
|
||||||
import org.dependencytrack.model.RepositoryType;
|
import org.dependencytrack.model.RepositoryType;
|
||||||
|
@ -680,48 +676,6 @@ final class ComponentQueryManager extends QueryManager implements IQueryManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A similar method exists in ProjectQueryManager
|
|
||||||
*/
|
|
||||||
private void preprocessACLs(final Query<Component> query, final String inputFilter, final Map<String, Object> params, final boolean bypass) {
|
|
||||||
if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && !bypass) {
|
|
||||||
final List<Team> teams;
|
|
||||||
if (super.principal instanceof UserPrincipal) {
|
|
||||||
final UserPrincipal userPrincipal = ((UserPrincipal) super.principal);
|
|
||||||
teams = userPrincipal.getTeams();
|
|
||||||
if (super.hasAccessManagementPermission(userPrincipal)) {
|
|
||||||
query.setFilter(inputFilter);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final ApiKey apiKey = ((ApiKey) super.principal);
|
|
||||||
teams = apiKey.getTeams();
|
|
||||||
if (super.hasAccessManagementPermission(apiKey)) {
|
|
||||||
query.setFilter(inputFilter);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (teams != null && teams.size() > 0) {
|
|
||||||
final StringBuilder sb = new StringBuilder();
|
|
||||||
for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) {
|
|
||||||
final Team team = super.getObjectById(Team.class, teams.get(i).getId());
|
|
||||||
sb.append(" project.accessTeams.contains(:team").append(i).append(") ");
|
|
||||||
params.put("team" + i, team);
|
|
||||||
if (i < teamsSize-1) {
|
|
||||||
sb.append(" || ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inputFilter != null) {
|
|
||||||
query.setFilter(inputFilter + " && (" + sb.toString() + ")");
|
|
||||||
} else {
|
|
||||||
query.setFilter(sb.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query.setFilter(inputFilter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Component> getDependencyGraphForComponents(Project project, List<Component> components) {
|
public Map<String, Component> getDependencyGraphForComponents(Project project, List<Component> components) {
|
||||||
Map<String, Component> dependencyGraph = new HashMap<>();
|
Map<String, Component> dependencyGraph = new HashMap<>();
|
||||||
if (project.getDirectDependencies() == null || project.getDirectDependencies().isBlank()) {
|
if (project.getDirectDependencies() == null || project.getDirectDependencies().isBlank()) {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import alpine.resources.AlpineRequest;
|
||||||
import com.github.packageurl.PackageURL;
|
import com.github.packageurl.PackageURL;
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.datanucleus.api.jdo.JDOQuery;
|
||||||
import org.dependencytrack.auth.Permissions;
|
import org.dependencytrack.auth.Permissions;
|
||||||
import org.dependencytrack.event.IndexEvent;
|
import org.dependencytrack.event.IndexEvent;
|
||||||
import org.dependencytrack.model.Analysis;
|
import org.dependencytrack.model.Analysis;
|
||||||
|
@ -55,6 +56,8 @@ import org.dependencytrack.util.NotificationUtil;
|
||||||
import javax.jdo.FetchPlan;
|
import javax.jdo.FetchPlan;
|
||||||
import javax.jdo.PersistenceManager;
|
import javax.jdo.PersistenceManager;
|
||||||
import javax.jdo.Query;
|
import javax.jdo.Query;
|
||||||
|
import javax.jdo.metadata.MemberMetadata;
|
||||||
|
import javax.jdo.metadata.TypeMetadata;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -894,7 +897,36 @@ final class ProjectQueryManager extends QueryManager implements IQueryManager {
|
||||||
/**
|
/**
|
||||||
* A similar method exists in ComponentQueryManager
|
* A similar method exists in ComponentQueryManager
|
||||||
*/
|
*/
|
||||||
private void preprocessACLs(final Query<Project> query, final String inputFilter, final Map<String, Object> params, final boolean bypass) {
|
@Override
|
||||||
|
void preprocessACLs(final Query<?> query, final String inputFilter, final Map<String, Object> params, final boolean bypass) {
|
||||||
|
String projectMemberFieldName = null;
|
||||||
|
final org.datanucleus.store.query.Query<?> internalQuery = ((JDOQuery<?>)query).getInternalQuery();
|
||||||
|
if (!Project.class.equals(internalQuery.getCandidateClass())) {
|
||||||
|
// NB: The query does not directly target Project, but if it has a relationship
|
||||||
|
// with Project we can still make the ACL check work. If the query candidate
|
||||||
|
// has EXACTLY one persistent field of type Project, we'll use that.
|
||||||
|
// If there are more than one, or none at all, we fail to avoid unintentional behavior.
|
||||||
|
final TypeMetadata candidateTypeMetadata = pm.getPersistenceManagerFactory().getMetadata(internalQuery.getCandidateClassName());
|
||||||
|
|
||||||
|
for (final MemberMetadata memberMetadata : candidateTypeMetadata.getMembers()) {
|
||||||
|
if (!Project.class.getName().equals(memberMetadata.getFieldType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectMemberFieldName != null) {
|
||||||
|
throw new IllegalArgumentException("Query candidate class %s has multiple members of type %s"
|
||||||
|
.formatted(internalQuery.getCandidateClassName(), Project.class.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
projectMemberFieldName = memberMetadata.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectMemberFieldName == null) {
|
||||||
|
throw new IllegalArgumentException("Query candidate class %s has no member of type %s"
|
||||||
|
.formatted(internalQuery.getCandidateClassName(), Project.class.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && !bypass) {
|
if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && !bypass) {
|
||||||
final List<Team> teams;
|
final List<Team> teams;
|
||||||
if (super.principal instanceof final UserPrincipal userPrincipal) {
|
if (super.principal instanceof final UserPrincipal userPrincipal) {
|
||||||
|
@ -911,10 +943,14 @@ final class ProjectQueryManager extends QueryManager implements IQueryManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (teams != null && teams.size() > 0) {
|
if (teams != null && !teams.isEmpty()) {
|
||||||
final StringBuilder sb = new StringBuilder();
|
final StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) {
|
for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) {
|
||||||
final Team team = super.getObjectById(Team.class, teams.get(i).getId());
|
final Team team = super.getObjectById(Team.class, teams.get(i).getId());
|
||||||
|
sb.append(" ");
|
||||||
|
if (projectMemberFieldName != null) {
|
||||||
|
sb.append(projectMemberFieldName).append(".");
|
||||||
|
}
|
||||||
sb.append(" accessTeams.contains(:team").append(i).append(") ");
|
sb.append(" accessTeams.contains(:team").append(i).append(") ");
|
||||||
params.put("team" + i, team);
|
params.put("team" + i, team);
|
||||||
if (i < teamsSize-1) {
|
if (i < teamsSize-1) {
|
||||||
|
@ -922,7 +958,7 @@ final class ProjectQueryManager extends QueryManager implements IQueryManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (inputFilter != null && !inputFilter.isBlank()) {
|
if (inputFilter != null && !inputFilter.isBlank()) {
|
||||||
query.setFilter(inputFilter + " && (" + sb.toString() + ")");
|
query.setFilter(inputFilter + " && (" + sb + ")");
|
||||||
} else {
|
} else {
|
||||||
query.setFilter(sb.toString());
|
query.setFilter(sb.toString());
|
||||||
}
|
}
|
||||||
|
|
|
@ -433,6 +433,10 @@ public class QueryManager extends AlpineQueryManager {
|
||||||
return getProjectQueryManager().hasAccess(principal, project);
|
return getProjectQueryManager().hasAccess(principal, project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void preprocessACLs(final Query<?> query, final String inputFilter, final Map<String, Object> params, final boolean bypass) {
|
||||||
|
getProjectQueryManager().preprocessACLs(query, inputFilter, params, bypass);
|
||||||
|
}
|
||||||
|
|
||||||
public PaginatedResult getProjects(final Tag tag, final boolean includeMetrics, final boolean excludeInactive, final boolean onlyRoot) {
|
public PaginatedResult getProjects(final Tag tag, final boolean includeMetrics, final boolean excludeInactive, final boolean onlyRoot) {
|
||||||
return getProjectQueryManager().getProjects(tag, includeMetrics, excludeInactive, onlyRoot);
|
return getProjectQueryManager().getProjects(tag, includeMetrics, excludeInactive, onlyRoot);
|
||||||
}
|
}
|
||||||
|
@ -1336,6 +1340,14 @@ public class QueryManager extends AlpineQueryManager {
|
||||||
return getTagQueryManager().getTaggedProjects(tagName);
|
return getTagQueryManager().getTaggedProjects(tagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void tagProjects(final String tagName, final Collection<String> projectUuids) {
|
||||||
|
getTagQueryManager().tagProjects(tagName, projectUuids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void untagProjects(final String tagName, final Collection<String> projectUuids) {
|
||||||
|
getTagQueryManager().untagProjects(tagName, projectUuids);
|
||||||
|
}
|
||||||
|
|
||||||
public List<TagQueryManager.TaggedPolicyRow> getTaggedPolicies(final String tagName) {
|
public List<TagQueryManager.TaggedPolicyRow> getTaggedPolicies(final String tagName) {
|
||||||
return getTagQueryManager().getTaggedPolicies(tagName);
|
return getTagQueryManager().getTaggedPolicies(tagName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,12 @@ import org.dependencytrack.model.Tag;
|
||||||
import javax.jdo.PersistenceManager;
|
import javax.jdo.PersistenceManager;
|
||||||
import javax.jdo.Query;
|
import javax.jdo.Query;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class TagQueryManager extends QueryManager implements IQueryManager {
|
public class TagQueryManager extends QueryManager implements IQueryManager {
|
||||||
|
@ -191,6 +193,63 @@ public class TagQueryManager extends QueryManager implements IQueryManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.12.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void tagProjects(final String tagName, final Collection<String> projectUuids) {
|
||||||
|
runInTransaction(() -> {
|
||||||
|
final Tag tag = getTagByName(tagName);
|
||||||
|
if (tag == null) {
|
||||||
|
throw new NoSuchElementException("A tag with name %s does not exist".formatted(tagName));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Query<Project> projectsQuery = pm.newQuery(Project.class);
|
||||||
|
final var params = new HashMap<String, Object>(Map.of("uuids", projectUuids));
|
||||||
|
preprocessACLs(projectsQuery, ":uuids.contains(uuid)", params, /* bypass */ false);
|
||||||
|
projectsQuery.setNamedParameters(params);
|
||||||
|
final List<Project> projects = executeAndCloseList(projectsQuery);
|
||||||
|
|
||||||
|
for (final Project project : projects) {
|
||||||
|
if (project.getTags() == null || project.getTags().isEmpty()) {
|
||||||
|
project.setTags(List.of(tag));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!project.getTags().contains(tag)) {
|
||||||
|
project.getTags().add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.12.0
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void untagProjects(final String tagName, final Collection<String> projectUuids) {
|
||||||
|
runInTransaction(() -> {
|
||||||
|
final Tag tag = getTagByName(tagName);
|
||||||
|
if (tag == null) {
|
||||||
|
throw new NoSuchElementException("A tag with name %s does not exist".formatted(tagName));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Query<Project> projectsQuery = pm.newQuery(Project.class);
|
||||||
|
final var params = new HashMap<String, Object>(Map.of("uuids", projectUuids));
|
||||||
|
preprocessACLs(projectsQuery, ":uuids.contains(uuid)", params, /* bypass */ false);
|
||||||
|
projectsQuery.setNamedParameters(params);
|
||||||
|
final List<Project> projects = executeAndCloseList(projectsQuery);
|
||||||
|
|
||||||
|
for (final Project project : projects) {
|
||||||
|
if (project.getTags() == null || project.getTags().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
project.getTags().remove(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 4.12.0
|
* @since 4.12.0
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -39,17 +39,24 @@ import org.dependencytrack.persistence.TagQueryManager.TagListRow;
|
||||||
import org.dependencytrack.persistence.TagQueryManager.TaggedPolicyRow;
|
import org.dependencytrack.persistence.TagQueryManager.TaggedPolicyRow;
|
||||||
import org.dependencytrack.persistence.TagQueryManager.TaggedProjectRow;
|
import org.dependencytrack.persistence.TagQueryManager.TaggedProjectRow;
|
||||||
import org.dependencytrack.resources.v1.openapi.PaginatedApi;
|
import org.dependencytrack.resources.v1.openapi.PaginatedApi;
|
||||||
|
import org.dependencytrack.resources.v1.problems.ProblemDetails;
|
||||||
import org.dependencytrack.resources.v1.vo.TagListResponseItem;
|
import org.dependencytrack.resources.v1.vo.TagListResponseItem;
|
||||||
import org.dependencytrack.resources.v1.vo.TaggedPolicyListResponseItem;
|
import org.dependencytrack.resources.v1.vo.TaggedPolicyListResponseItem;
|
||||||
import org.dependencytrack.resources.v1.vo.TaggedProjectListResponseItem;
|
import org.dependencytrack.resources.v1.vo.TaggedProjectListResponseItem;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DELETE;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.PathParam;
|
import jakarta.ws.rs.PathParam;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Path("/v1/tag")
|
@Path("/v1/tag")
|
||||||
|
@ -126,6 +133,102 @@ public class TagResource extends AlpineResource {
|
||||||
return Response.ok(tags).header(TOTAL_COUNT_HEADER, totalCount).build();
|
return Response.ok(tags).header(TOTAL_COUNT_HEADER, totalCount).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{name}/project")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(
|
||||||
|
summary = "Tags one or more projects.",
|
||||||
|
description = "<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong></p>"
|
||||||
|
)
|
||||||
|
@ApiResponses(value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "204",
|
||||||
|
description = "Projects tagged successfully."
|
||||||
|
),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "A tag with the provided name does not exist.",
|
||||||
|
content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT)
|
||||||
|
public Response tagProjects(
|
||||||
|
@Parameter(description = "Name of the tag to assign", required = true)
|
||||||
|
@PathParam("name") final String tagName,
|
||||||
|
@Parameter(
|
||||||
|
description = "UUIDs of projects to tag",
|
||||||
|
required = true,
|
||||||
|
array = @ArraySchema(schema = @Schema(type = "string", format = "uuid"))
|
||||||
|
)
|
||||||
|
@Size(min = 1, max = 100) final Set<@ValidUuid String> projectUuids
|
||||||
|
) {
|
||||||
|
try (final var qm = new QueryManager(getAlpineRequest())) {
|
||||||
|
qm.tagProjects(tagName, projectUuids);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// TODO: Move this to an ExceptionMapper once https://github.com/stevespringett/Alpine/pull/588 is available.
|
||||||
|
if (e.getCause() instanceof final NoSuchElementException nseException) {
|
||||||
|
return Response
|
||||||
|
.status(404)
|
||||||
|
.header("Content-Type", ProblemDetails.MEDIA_TYPE_JSON)
|
||||||
|
.entity(new ProblemDetails(404, "Resource does not exist", nseException.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/{name}/project")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(
|
||||||
|
summary = "Untags one or more projects.",
|
||||||
|
description = "<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong></p>"
|
||||||
|
)
|
||||||
|
@ApiResponses(value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "204",
|
||||||
|
description = "Projects untagged successfully."
|
||||||
|
),
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "A tag with the provided name does not exist.",
|
||||||
|
content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT)
|
||||||
|
public Response untagProjects(
|
||||||
|
@Parameter(description = "Name of the tag", required = true)
|
||||||
|
@PathParam("name") final String tagName,
|
||||||
|
@Parameter(
|
||||||
|
description = "UUIDs of projects to untag",
|
||||||
|
required = true,
|
||||||
|
array = @ArraySchema(schema = @Schema(type = "string", format = "uuid"))
|
||||||
|
)
|
||||||
|
@Size(min = 1, max = 100) final Set<@ValidUuid String> projectUuids
|
||||||
|
) {
|
||||||
|
try (final var qm = new QueryManager(getAlpineRequest())) {
|
||||||
|
qm.untagProjects(tagName, projectUuids);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// TODO: Move this to an ExceptionMapper once https://github.com/stevespringett/Alpine/pull/588 is available.
|
||||||
|
if (e.getCause() instanceof final NoSuchElementException nseException) {
|
||||||
|
return Response
|
||||||
|
.status(404)
|
||||||
|
.header("Content-Type", ProblemDetails.MEDIA_TYPE_JSON)
|
||||||
|
.entity(new ProblemDetails(404, "Resource does not exist", nseException.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{name}/policy")
|
@Path("/{name}/policy")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -69,6 +69,15 @@ public class ProblemDetails {
|
||||||
)
|
)
|
||||||
private URI instance;
|
private URI instance;
|
||||||
|
|
||||||
|
public ProblemDetails() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProblemDetails(final int status, final String title, final String detail) {
|
||||||
|
this.status = status;
|
||||||
|
this.title = title;
|
||||||
|
this.detail = detail;
|
||||||
|
}
|
||||||
|
|
||||||
public URI getType() {
|
public URI getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,20 @@ import org.dependencytrack.model.Policy;
|
||||||
import org.dependencytrack.model.Project;
|
import org.dependencytrack.model.Project;
|
||||||
import org.dependencytrack.model.Tag;
|
import org.dependencytrack.model.Tag;
|
||||||
import org.dependencytrack.resources.v1.exception.ConstraintViolationExceptionMapper;
|
import org.dependencytrack.resources.v1.exception.ConstraintViolationExceptionMapper;
|
||||||
|
import org.glassfish.jersey.client.ClientProperties;
|
||||||
import org.glassfish.jersey.server.ResourceConfig;
|
import org.glassfish.jersey.server.ResourceConfig;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import jakarta.json.JsonArray;
|
import jakarta.json.JsonArray;
|
||||||
|
import jakarta.ws.rs.HttpMethod;
|
||||||
|
import jakarta.ws.rs.client.Entity;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
@ -351,6 +357,275 @@ public class TagResourceTest extends ResourceTest {
|
||||||
assertThat(getPlainTextBody(response)).isEqualTo("[]");
|
assertThat(getPlainTextBody(response)).isEqualTo("[]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tagProjectsTest() {
|
||||||
|
final var projectA = new Project();
|
||||||
|
projectA.setName("acme-app-a");
|
||||||
|
qm.persist(projectA);
|
||||||
|
|
||||||
|
final var projectB = new Project();
|
||||||
|
projectB.setName("acme-app-b");
|
||||||
|
qm.persist(projectB);
|
||||||
|
|
||||||
|
qm.createTag("foo");
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.post(Entity.json(List.of(projectA.getUuid(), projectB.getUuid())));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
qm.getPersistenceManager().evictAll();
|
||||||
|
assertThat(projectA.getTags()).satisfiesExactly(projectTag -> assertThat(projectTag.getName()).isEqualTo("foo"));
|
||||||
|
assertThat(projectB.getTags()).satisfiesExactly(projectTag -> assertThat(projectTag.getName()).isEqualTo("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tagProjectsWithTagNotExistsTest() {
|
||||||
|
final var projectA = new Project();
|
||||||
|
projectA.setName("acme-app-a");
|
||||||
|
qm.persist(projectA);
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.post(Entity.json(List.of(projectA.getUuid())));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(404);
|
||||||
|
assertThat(response.getHeaderString("Content-Type")).isEqualTo("application/problem+json");
|
||||||
|
assertThatJson(getPlainTextBody(response)).isEqualTo("""
|
||||||
|
{
|
||||||
|
"status": 404,
|
||||||
|
"title": "Resource does not exist",
|
||||||
|
"detail": "A tag with name foo does not exist"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tagProjectsWithNoProjectUuidsTest() {
|
||||||
|
qm.createTag("foo");
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.post(Entity.json(Collections.emptyList()));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(400);
|
||||||
|
assertThatJson(getPlainTextBody(response)).isEqualTo("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"message": "size must be between 1 and 100",
|
||||||
|
"messageTemplate": "{jakarta.validation.constraints.Size.message}",
|
||||||
|
"path": "tagProjects.arg1",
|
||||||
|
"invalidValue": "[]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tagProjectsWithAclTest() {
|
||||||
|
qm.createConfigProperty(
|
||||||
|
ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(),
|
||||||
|
ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(),
|
||||||
|
"true",
|
||||||
|
ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(),
|
||||||
|
ACCESS_MANAGEMENT_ACL_ENABLED.getDescription()
|
||||||
|
);
|
||||||
|
|
||||||
|
final var projectA = new Project();
|
||||||
|
projectA.setName("acme-app-a");
|
||||||
|
qm.persist(projectA);
|
||||||
|
|
||||||
|
final var projectB = new Project();
|
||||||
|
projectB.setName("acme-app-b");
|
||||||
|
qm.persist(projectB);
|
||||||
|
|
||||||
|
qm.createTag("foo");
|
||||||
|
|
||||||
|
projectA.addAccessTeam(team);
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.post(Entity.json(List.of(projectA.getUuid(), projectB.getUuid())));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
qm.getPersistenceManager().evictAll();
|
||||||
|
assertThat(projectA.getTags()).satisfiesExactly(projectTag -> assertThat(projectTag.getName()).isEqualTo("foo"));
|
||||||
|
assertThat(projectB.getTags()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tagProjectsWhenAlreadyTaggedTest() {
|
||||||
|
final var projectA = new Project();
|
||||||
|
projectA.setName("acme-app-a");
|
||||||
|
qm.persist(projectA);
|
||||||
|
|
||||||
|
final Tag tag = qm.createTag("foo");
|
||||||
|
qm.bind(projectA, List.of(tag));
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.post(Entity.json(List.of(projectA.getUuid())));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
qm.getPersistenceManager().evictAll();
|
||||||
|
assertThat(projectA.getTags()).satisfiesExactly(projectTag -> assertThat(projectTag.getName()).isEqualTo("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void untagProjectsTest() {
|
||||||
|
final var projectA = new Project();
|
||||||
|
projectA.setName("acme-app-a");
|
||||||
|
qm.persist(projectA);
|
||||||
|
|
||||||
|
final var projectB = new Project();
|
||||||
|
projectB.setName("acme-app-b");
|
||||||
|
qm.persist(projectB);
|
||||||
|
|
||||||
|
final Tag tag = qm.createTag("foo");
|
||||||
|
qm.bind(projectA, List.of(tag));
|
||||||
|
qm.bind(projectB, List.of(tag));
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
|
||||||
|
.method(HttpMethod.DELETE, Entity.json(List.of(projectA.getUuid(), projectB.getUuid())));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
qm.getPersistenceManager().evictAll();
|
||||||
|
assertThat(projectA.getTags()).isEmpty();
|
||||||
|
assertThat(projectB.getTags()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void untagProjectsWithAclTest() {
|
||||||
|
qm.createConfigProperty(
|
||||||
|
ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(),
|
||||||
|
ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(),
|
||||||
|
"true",
|
||||||
|
ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(),
|
||||||
|
ACCESS_MANAGEMENT_ACL_ENABLED.getDescription()
|
||||||
|
);
|
||||||
|
|
||||||
|
final var projectA = new Project();
|
||||||
|
projectA.setName("acme-app-a");
|
||||||
|
qm.persist(projectA);
|
||||||
|
|
||||||
|
final var projectB = new Project();
|
||||||
|
projectB.setName("acme-app-b");
|
||||||
|
qm.persist(projectB);
|
||||||
|
|
||||||
|
final Tag tag = qm.createTag("foo");
|
||||||
|
qm.bind(projectA, List.of(tag));
|
||||||
|
qm.bind(projectB, List.of(tag));
|
||||||
|
|
||||||
|
projectA.addAccessTeam(team);
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
|
||||||
|
.method(HttpMethod.DELETE, Entity.json(List.of(projectA.getUuid(), projectB.getUuid())));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
qm.getPersistenceManager().evictAll();
|
||||||
|
assertThat(projectA.getTags()).isEmpty();
|
||||||
|
assertThat(projectB.getTags()).satisfiesExactly(projectTag -> assertThat(projectTag.getName()).isEqualTo("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void untagProjectsWithTagNotExistsTest() {
|
||||||
|
final var projectA = new Project();
|
||||||
|
projectA.setName("acme-app-a");
|
||||||
|
qm.persist(projectA);
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
|
||||||
|
.method(HttpMethod.DELETE, Entity.json(List.of(projectA.getUuid())));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(404);
|
||||||
|
assertThat(response.getHeaderString("Content-Type")).isEqualTo("application/problem+json");
|
||||||
|
assertThatJson(getPlainTextBody(response)).isEqualTo("""
|
||||||
|
{
|
||||||
|
"status": 404,
|
||||||
|
"title": "Resource does not exist",
|
||||||
|
"detail": "A tag with name foo does not exist"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void untagProjectsWithNoProjectUuidsTest() {
|
||||||
|
qm.createTag("foo");
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
|
||||||
|
.method(HttpMethod.DELETE, Entity.json(Collections.emptyList()));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(400);
|
||||||
|
assertThatJson(getPlainTextBody(response)).isEqualTo("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"message": "size must be between 1 and 100",
|
||||||
|
"messageTemplate": "{jakarta.validation.constraints.Size.message}",
|
||||||
|
"path": "untagProjects.arg1",
|
||||||
|
"invalidValue": "[]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void untagProjectsWithTooManyProjectUuidsTest() {
|
||||||
|
qm.createTag("foo");
|
||||||
|
|
||||||
|
final List<String> projectUuids = IntStream.range(0, 101)
|
||||||
|
.mapToObj(ignored -> UUID.randomUUID())
|
||||||
|
.map(UUID::toString)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
|
||||||
|
.method(HttpMethod.DELETE, Entity.json(projectUuids));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(400);
|
||||||
|
assertThatJson(getPlainTextBody(response)).isEqualTo("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"message": "size must be between 1 and 100",
|
||||||
|
"messageTemplate": "{jakarta.validation.constraints.Size.message}",
|
||||||
|
"path": "untagProjects.arg1",
|
||||||
|
"invalidValue": "${json-unit.any-string}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void untagProjectsWhenNotTaggedTest() {
|
||||||
|
final var projectA = new Project();
|
||||||
|
projectA.setName("acme-app-a");
|
||||||
|
qm.persist(projectA);
|
||||||
|
|
||||||
|
qm.createTag("foo");
|
||||||
|
|
||||||
|
final Response response = jersey.target(V1_TAG + "/foo/project")
|
||||||
|
.request()
|
||||||
|
.header(X_API_KEY, apiKey)
|
||||||
|
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
|
||||||
|
.method(HttpMethod.DELETE, Entity.json(List.of(projectA.getUuid())));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
qm.getPersistenceManager().evictAll();
|
||||||
|
assertThat(projectA.getTags()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getTaggedPoliciesTest() {
|
public void getTaggedPoliciesTest() {
|
||||||
final Tag tagFoo = qm.createTag("foo");
|
final Tag tagFoo = qm.createTag("foo");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue