Skip to content
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 REST endpoints to tag and untag policies in bulk #817

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
/**
* @since 4.11.0
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Target({ElementType.TYPE_USE, ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = {})
@Retention(RUNTIME)
@ReportAsSingleViolation
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/dependencytrack/persistence/QueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,10 @@ public void bind(Project project, List<Tag> tags) {
getProjectQueryManager().bind(project, tags);
}

public void bind(Policy policy, List<Tag> tags) {
getProjectQueryManager().bind(policy, tags);
}

public boolean hasAccessManagementPermission(final Object principal) {
if (principal instanceof final UserPrincipal userPrincipal) {
return hasAccessManagementPermission(userPrincipal);
Expand Down Expand Up @@ -1945,4 +1949,12 @@ public long deleteComponentPropertyByUuid(final Component component, final UUID
public void synchronizeComponentProperties(final Component component, final List<ComponentProperty> properties) {
getComponentQueryManager().synchronizeComponentProperties(component, properties);
}

public void tagPolicies(final String tagName, final Collection<String> policyUuids) {
getTagQueryManager().tagPolicies(tagName, policyUuids);
}

public void untagPolicies(final String tagName, final Collection<String> policyUuids) {
getTagQueryManager().untagPolicies(tagName, policyUuids);
}
}
49 changes: 49 additions & 0 deletions src/main/java/org/dependencytrack/persistence/TagQueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Stream;

public class TagQueryManager extends QueryManager implements IQueryManager {
Expand Down Expand Up @@ -167,4 +169,51 @@ private List<Tag> createTags(final List<String> names) {
}
return new ArrayList<>(persist(newTags));
}

/**
* @since 4.12.0
*/
@Override
public void tagPolicies(final String tagName, final Collection<String> policyUuids) {
runInTransaction(() -> {
final Tag tag = getTagByName(tagName);
if (tag == null) {
throw new NoSuchElementException("A tag with name %s does not exist".formatted(tagName));
}

final Query<Policy> policiesQuery = pm.newQuery(Policy.class);
policiesQuery.setFilter(":uuids.contains(uuid)");
policiesQuery.setParameters(policyUuids);
final List<Policy> policies = executeAndCloseList(policiesQuery);

for (final Policy policy : policies) {
bind(policy, List.of(tag));
}
});
}

/**
* @since 4.12.0
*/
@Override
public void untagPolicies(final String tagName, final Collection<String> policyUuids) {
runInTransaction(() -> {
final Tag tag = getTagByName(tagName);
if (tag == null) {
throw new NoSuchElementException("A tag with name %s does not exist".formatted(tagName));
}

final Query<Policy> policiesQuery = pm.newQuery(Policy.class);
policiesQuery.setFilter(":uuids.contains(uuid)");
policiesQuery.setParameters(policyUuids);
final List<Policy> policies = executeAndCloseList(policiesQuery);

for (final Policy policy : policies) {
if (policy.getTags() == null || policy.getTags().isEmpty()) {
continue;
}
policy.getTags().remove(tag);
}
});
}
}
12 changes: 10 additions & 2 deletions src/main/java/org/dependencytrack/resources/v1/PolicyResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,10 @@ public Response removeProjectFromPolicy(
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Adds a tag to a policy",
description = "<p>Requires permission <strong>POLICY_MANAGEMENT</strong> or <strong>POLICY_MANAGEMENT_UPDATE</strong></p>"
description = """
<p><strong>Deprecated</strong>. Use <code>POST /api/v1/tag/{name}/policy</code> instead.</p>
<p>Requires permission <strong>POLICY_MANAGEMENT</strong> or <strong>POLICY_MANAGEMENT_UPDATE</strong></p>
"""
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Policy.class))),
Expand All @@ -310,6 +313,7 @@ public Response removeProjectFromPolicy(
@ApiResponse(responseCode = "404", description = "The policy or tag could not be found")
})
@PermissionRequired({Permissions.Constants.POLICY_MANAGEMENT, Permissions.Constants.POLICY_MANAGEMENT_UPDATE})
@Deprecated(forRemoval = true)
public Response addTagToPolicy(
@Parameter(description = "The UUID of the policy to add a project to", schema = @Schema(type = "string", format = "uuid"), required = true)
@PathParam("policyUuid") @ValidUuid String policyUuid,
Expand Down Expand Up @@ -341,7 +345,10 @@ public Response addTagToPolicy(
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Removes a tag from a policy",
description = "<p>Requires permission <strong>POLICY_MANAGEMENT</strong> or <strong>POLICY_MANAGEMENT_DELETE</strong></p>"
description = """
<p><strong>Deprecated</strong>. Use <code>DELETE /api/v1/tag/{name}/policy</code> instead.</p>
<p>Requires permission <strong>POLICY_MANAGEMENT</strong> or <strong>POLICY_MANAGEMENT_DELETE</strong></p>
"""
)
@ApiResponses(value = {
@ApiResponse(responseCode = "204", content = @Content(schema = @Schema(implementation = Policy.class))),
Expand All @@ -350,6 +357,7 @@ public Response addTagToPolicy(
@ApiResponse(responseCode = "404", description = "The policy or tag could not be found")
})
@PermissionRequired({Permissions.Constants.POLICY_MANAGEMENT, Permissions.Constants.POLICY_MANAGEMENT_DELETE})
@Deprecated(forRemoval = true)
public Response removeTagFromPolicy(
@Parameter(description = "The UUID of the policy to remove the tag from", schema = @Schema(type = "string", format = "uuid"), required = true)
@PathParam("policyUuid") @ValidUuid String policyUuid,
Expand Down
100 changes: 100 additions & 0 deletions src/main/java/org/dependencytrack/resources/v1/TagResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
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 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;
Expand All @@ -41,6 +45,10 @@
import org.dependencytrack.model.Tag;
import org.dependencytrack.model.validation.ValidUuid;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.resources.v1.problems.ProblemDetails;

import java.util.NoSuchElementException;
import java.util.Set;

@Path("/v1/tag")
@io.swagger.v3.oas.annotations.tags.Tag(name = "tag")
Expand Down Expand Up @@ -73,4 +81,96 @@ public Response getTags(@Parameter(description = "The UUID of the policy", schem
return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build();
}
}

@POST
@Path("/{name}/policy")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Tags one or more policies.",
description = "<p>Requires permission <strong>POLICY_MANAGEMENT</strong></p>"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "204",
description = "Policies 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.POLICY_MANAGEMENT)
public Response tagPolicies(
@Parameter(description = "Name of the tag to assign", required = true)
@PathParam("name") final String tagName,
@Parameter(
description = "UUIDs of policies to tag",
required = true,
array = @ArraySchema(schema = @Schema(type = "string", format = "uuid"))
)
@Size(min = 1, max = 100) final Set<@ValidUuid String> policyUuids
) {
try (final var qm = new QueryManager(getAlpineRequest())) {
qm.tagPolicies(tagName, policyUuids);
} 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}/policy")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Untags one or more policies.",
description = "<p>Requires permission <strong>POLICY_MANAGEMENT</strong></p>"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "204",
description = "Policies 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.POLICY_MANAGEMENT)
public Response untagPolicies(
@Parameter(description = "Name of the tag", required = true)
@PathParam("name") final String tagName,
@Parameter(
description = "UUIDs of policies to untag",
required = true,
array = @ArraySchema(schema = @Schema(type = "string", format = "uuid"))
)
@Size(min = 1, max = 100) final Set<@ValidUuid String> policyUuids
) {
try (final var qm = new QueryManager(getAlpineRequest())) {
qm.untagPolicies(tagName, policyUuids);
} 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;

import java.net.URI;

Expand Down Expand Up @@ -69,6 +71,27 @@ 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;
}

/**
* @since 4.12.0
*/
public Response toResponse() {
return Response
.status(status)
.header(HttpHeaders.CONTENT_TYPE, MEDIA_TYPE_JSON)
.entity(this)
.build();
}


public URI getType() {
return type;
}
Expand Down
Loading
Loading