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
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE})
|
||||
@Constraint(validatedBy = {})
|
||||
@Retention(RUNTIME)
|
||||
@ReportAsSingleViolation
|
||||
|
|
|
@ -20,10 +20,7 @@ package org.dependencytrack.persistence;
|
|||
|
||||
import alpine.common.logging.Logger;
|
||||
import alpine.event.framework.Event;
|
||||
import alpine.model.ApiKey;
|
||||
import alpine.model.IConfigProperty;
|
||||
import alpine.model.Team;
|
||||
import alpine.model.UserPrincipal;
|
||||
import alpine.persistence.PaginatedResult;
|
||||
import alpine.resources.AlpineRequest;
|
||||
import com.github.packageurl.MalformedPackageURLException;
|
||||
|
@ -33,7 +30,6 @@ import org.dependencytrack.event.IndexEvent;
|
|||
import org.dependencytrack.model.Component;
|
||||
import org.dependencytrack.model.ComponentIdentity;
|
||||
import org.dependencytrack.model.ComponentProperty;
|
||||
import org.dependencytrack.model.ConfigPropertyConstants;
|
||||
import org.dependencytrack.model.Project;
|
||||
import org.dependencytrack.model.RepositoryMetaComponent;
|
||||
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) {
|
||||
Map<String, Component> dependencyGraph = new HashMap<>();
|
||||
if (project.getDirectDependencies() == null || project.getDirectDependencies().isBlank()) {
|
||||
|
|
|
@ -31,6 +31,7 @@ import alpine.resources.AlpineRequest;
|
|||
import com.github.packageurl.PackageURL;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.datanucleus.api.jdo.JDOQuery;
|
||||
import org.dependencytrack.auth.Permissions;
|
||||
import org.dependencytrack.event.IndexEvent;
|
||||
import org.dependencytrack.model.Analysis;
|
||||
|
@ -55,6 +56,8 @@ import org.dependencytrack.util.NotificationUtil;
|
|||
import javax.jdo.FetchPlan;
|
||||
import javax.jdo.PersistenceManager;
|
||||
import javax.jdo.Query;
|
||||
import javax.jdo.metadata.MemberMetadata;
|
||||
import javax.jdo.metadata.TypeMetadata;
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
@ -894,7 +897,36 @@ final class ProjectQueryManager extends QueryManager implements IQueryManager {
|
|||
/**
|
||||
* 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) {
|
||||
final List<Team> teams;
|
||||
if (super.principal instanceof final UserPrincipal userPrincipal) {
|
||||
|
@ -911,10 +943,14 @@ final class ProjectQueryManager extends QueryManager implements IQueryManager {
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (teams != null && teams.size() > 0) {
|
||||
if (teams != null && !teams.isEmpty()) {
|
||||
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(" ");
|
||||
if (projectMemberFieldName != null) {
|
||||
sb.append(projectMemberFieldName).append(".");
|
||||
}
|
||||
sb.append(" accessTeams.contains(:team").append(i).append(") ");
|
||||
params.put("team" + i, team);
|
||||
if (i < teamsSize-1) {
|
||||
|
@ -922,7 +958,7 @@ final class ProjectQueryManager extends QueryManager implements IQueryManager {
|
|||
}
|
||||
}
|
||||
if (inputFilter != null && !inputFilter.isBlank()) {
|
||||
query.setFilter(inputFilter + " && (" + sb.toString() + ")");
|
||||
query.setFilter(inputFilter + " && (" + sb + ")");
|
||||
} else {
|
||||
query.setFilter(sb.toString());
|
||||
}
|
||||
|
|
|
@ -433,6 +433,10 @@ public class QueryManager extends AlpineQueryManager {
|
|||
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) {
|
||||
return getProjectQueryManager().getProjects(tag, includeMetrics, excludeInactive, onlyRoot);
|
||||
}
|
||||
|
@ -1336,6 +1340,14 @@ public class QueryManager extends AlpineQueryManager {
|
|||
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) {
|
||||
return getTagQueryManager().getTaggedPolicies(tagName);
|
||||
}
|
||||
|
|
|
@ -29,10 +29,12 @@ import org.dependencytrack.model.Tag;
|
|||
import javax.jdo.PersistenceManager;
|
||||
import javax.jdo.Query;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -39,17 +39,24 @@ import org.dependencytrack.persistence.TagQueryManager.TagListRow;
|
|||
import org.dependencytrack.persistence.TagQueryManager.TaggedPolicyRow;
|
||||
import org.dependencytrack.persistence.TagQueryManager.TaggedProjectRow;
|
||||
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.TaggedPolicyListResponseItem;
|
||||
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.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Path("/v1/tag")
|
||||
|
@ -126,6 +133,102 @@ public class TagResource extends AlpineResource {
|
|||
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
|
||||
@Path("/{name}/policy")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -69,6 +69,15 @@ public class ProblemDetails {
|
|||
)
|
||||
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() {
|
||||
return type;
|
||||
}
|
||||
|
|
|
@ -8,14 +8,20 @@ import org.dependencytrack.model.Policy;
|
|||
import org.dependencytrack.model.Project;
|
||||
import org.dependencytrack.model.Tag;
|
||||
import org.dependencytrack.resources.v1.exception.ConstraintViolationExceptionMapper;
|
||||
import org.glassfish.jersey.client.ClientProperties;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import jakarta.json.JsonArray;
|
||||
import jakarta.ws.rs.HttpMethod;
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
@ -351,6 +357,275 @@ public class TagResourceTest extends ResourceTest {
|
|||
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
|
||||
public void getTaggedPoliciesTest() {
|
||||
final Tag tagFoo = qm.createTag("foo");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue