diff --git a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java index c6c495787a..b03be7a40d 100644 --- a/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/FindingsSearchQueryManager.java @@ -25,7 +25,6 @@ import com.github.packageurl.PackageURL; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.Component; -import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Finding; import org.dependencytrack.model.GroupedFinding; import org.dependencytrack.model.RepositoryMetaComponent; @@ -343,37 +342,14 @@ private void processInputFilter(StringBuilder queryFilter, Map p } private void preprocessACLs(StringBuilder queryFilter, final Map params) { - if (!isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) - || hasAccessManagementPermission(this.principal)) { - return; - } - if (queryFilter.isEmpty()) { queryFilter.append(" WHERE "); } else { queryFilter.append(" AND "); } - final var teamIds = new ArrayList<>(getTeamIds(principal)); - if (teamIds.isEmpty()) { - queryFilter.append(":false"); - params.put("false", false); - return; - } - - // NB: Need to work around the fact that the RDBMSes can't agree on how to do member checks. Oh joy! :))) - final var teamIdChecks = new ArrayList(); - for (int i = 0; i < teamIds.size(); i++) { - teamIdChecks.add("\"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = :teamId" + i); - params.put("teamId" + i, teamIds.get(i)); - } - - queryFilter.append(""" - EXISTS ( - SELECT 1 - FROM "PROJECT_ACCESS_TEAMS" - WHERE "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = "PROJECT"."ID" - AND (%s) - )""".formatted(String.join(" OR ", teamIdChecks))); + final Map.Entry> projectAclConditionAndParams = getProjectAclSqlCondition(); + queryFilter.append(projectAclConditionAndParams.getKey()).append(" "); + params.putAll(projectAclConditionAndParams.getValue()); } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index a999274996..0c726bd6bd 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -91,6 +91,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -98,6 +99,7 @@ import java.util.UUID; import static org.datanucleus.PropertyNames.PROPERTY_QUERY_SQL_ALLOWALL; +import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED; /** * This QueryManager provides a concrete extension of {@link AlpineQueryManager} by @@ -1324,6 +1326,18 @@ public boolean hasAccessManagementPermission(final ApiKey apiKey) { return getProjectQueryManager().hasAccessManagementPermission(apiKey); } + public List getTags() { + return getTagQueryManager().getTags(); + } + + public List getTaggedProjects(final String tagName) { + return getTagQueryManager().getTaggedProjects(tagName); + } + + public List getTaggedPolicies(final String tagName) { + return getTagQueryManager().getTaggedPolicies(tagName); + } + public PaginatedResult getTags(String policyUuid) { return getTagQueryManager().getTags(policyUuid); } @@ -1478,4 +1492,50 @@ public List getRepositoryMetaComponentsBatch(final List public List getRepositoryMetaComponents(final List list) { return getRepositoryQueryManager().getRepositoryMetaComponents(list); } + + /** + * @see #getProjectAclSqlCondition(String) + * @since 4.12.0 + */ + public Map.Entry> getProjectAclSqlCondition() { + return getProjectAclSqlCondition("PROJECT"); + } + + /** + * @param projectTableAlias Name or alias of the {@code PROJECT} table to use in the condition. + * @return A SQL condition that may be used to check if the {@link #principal} has access to a project + * @since 4.12.0 + */ + public Map.Entry> getProjectAclSqlCondition(final String projectTableAlias) { + if (request == null) { + return Map.entry(":true", Map.of("true", true)); + } + + if (principal == null || !isEnabled(ACCESS_MANAGEMENT_ACL_ENABLED) || hasAccessManagementPermission(principal)) { + return Map.entry(":true", Map.of("true", true)); + } + + final var teamIds = new ArrayList<>(getTeamIds(principal)); + if (teamIds.isEmpty()) { + return Map.entry(":false", Map.of("false", false)); + } + + + // NB: Need to work around the fact that the RDBMSes can't agree on how to do member checks. Oh joy! :))) + final var params = new HashMap(); + final var teamIdChecks = new ArrayList(); + for (int i = 0; i < teamIds.size(); i++) { + teamIdChecks.add("\"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = :teamId" + i); + params.put("teamId" + i, teamIds.get(i)); + } + + return Map.entry(""" + EXISTS ( + SELECT 1 + FROM "PROJECT_ACCESS_TEAMS" + WHERE "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = "%s"."ID" + AND (%s) + )""".formatted(projectTableAlias, String.join(" OR ", teamIdChecks)), params); + } + } \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/persistence/TagQueryManager.java b/src/main/java/org/dependencytrack/persistence/TagQueryManager.java index 5234917a04..b8dbcab053 100644 --- a/src/main/java/org/dependencytrack/persistence/TagQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/TagQueryManager.java @@ -19,6 +19,7 @@ package org.dependencytrack.persistence; import alpine.common.logging.Logger; +import alpine.persistence.OrderDirection; import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; import org.dependencytrack.model.Policy; @@ -26,8 +27,12 @@ import org.dependencytrack.model.Tag; import javax.jdo.PersistenceManager; +import javax.jdo.Query; +import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Stream; public class TagQueryManager extends QueryManager implements IQueryManager { @@ -54,6 +59,163 @@ public class TagQueryManager extends QueryManager implements IQueryManager { super(pm, request); } + /** + * @since 4.12.0 + */ + public record TagListRow(String name, long projectCount, long policyCount, long totalCount) { + } + + /** + * @since 4.12.0 + */ + @Override + public List getTags() { + final Map.Entry> projectAclConditionAndParams = getProjectAclSqlCondition(); + final String projectAclCondition = projectAclConditionAndParams.getKey(); + final Map projectAclConditionParams = projectAclConditionAndParams.getValue(); + + // language=SQL + var sqlQuery = """ + SELECT "NAME" AS "name" + , (SELECT COUNT(*) + FROM "PROJECTS_TAGS" + INNER JOIN "PROJECT" + ON "PROJECT"."ID" = "PROJECTS_TAGS"."PROJECT_ID" + WHERE "PROJECTS_TAGS"."TAG_ID" = "TAG"."ID" + AND %s + ) AS "projectCount" + , (SELECT COUNT(*) + FROM "POLICY_TAGS" + WHERE "POLICY_TAGS"."TAG_ID" = "TAG"."ID" + ) AS "policyCount" + , COUNT(*) OVER() AS "totalCount" + FROM "TAG" + """.formatted(projectAclCondition); + + final var params = new HashMap<>(projectAclConditionParams); + + if (filter != null) { + sqlQuery += " WHERE \"NAME\" LIKE :nameFilter"; + params.put("nameFilter", "%" + filter + "%"); + } + + if (orderBy == null) { + sqlQuery += " ORDER BY \"name\" ASC"; + } else if ("name".equals(orderBy) || "projectCount".equals(orderBy) || "policyCount".equals(orderBy)) { + sqlQuery += " ORDER BY \"%s\" %s, \"ID\" ASC".formatted(orderBy, + orderDirection == OrderDirection.DESCENDING ? "DESC" : "ASC"); + } else { + throw new IllegalArgumentException("Cannot sort by " + orderBy); + } + + if (pagination.isPaginated()) { + sqlQuery += " OFFSET %d FETCH NEXT %d ROWS ONLY".formatted(pagination.getOffset(), pagination.getLimit()); + } + + final Query query = pm.newQuery(Query.SQL, sqlQuery); + query.setNamedParameters(params); + try { + return new ArrayList<>(query.executeResultList(TagListRow.class)); + } finally { + query.closeAll(); + } + } + + /** + * @since 4.12.0 + */ + public record TaggedProjectRow(String uuid, String name, String version, long totalCount) { + } + + /** + * @since 4.12.0 + */ + @Override + public List getTaggedProjects(final String tagName) { + final Map.Entry> projectAclConditionAndParams = getProjectAclSqlCondition(); + final String projectAclCondition = projectAclConditionAndParams.getKey(); + final Map projectAclConditionParams = projectAclConditionAndParams.getValue(); + + // language=SQL + var sqlQuery = """ + SELECT "PROJECT"."UUID" AS "uuid" + , "PROJECT"."NAME" AS "name" + , "PROJECT"."VERSION" AS "version" + , COUNT(*) OVER() AS "totalCount" + FROM "PROJECT" + INNER JOIN "PROJECTS_TAGS" + ON "PROJECTS_TAGS"."PROJECT_ID" = "PROJECT"."ID" + INNER JOIN "TAG" + ON "TAG"."ID" = "PROJECTS_TAGS"."TAG_ID" + WHERE "TAG"."NAME" = :tag + AND %s + """.formatted(projectAclCondition); + + final var params = new HashMap<>(projectAclConditionParams); + + if (filter != null) { + sqlQuery += " WHERE \"PROJECT\".\"NAME\" LIKE :nameFilter"; + params.put("nameFilter", "%" + filter + "%"); + } + + if (orderBy == null) { + sqlQuery += " ORDER BY \"name\" ASC, \"version\" DESC"; + } else if ("name".equals(orderBy) || "version".equals(orderBy)) { + sqlQuery += " ORDER BY \"%s\" %s, \"ID\" ASC".formatted(orderBy, + orderDirection == OrderDirection.DESCENDING ? "DESC" : "ASC"); + } else { + throw new IllegalArgumentException("Cannot sort by " + orderBy); + } + + if (pagination.isPaginated()) { + sqlQuery += " OFFSET %d FETCH NEXT %d ROWS ONLY".formatted(pagination.getOffset(), pagination.getLimit()); + } + + final Query query = pm.newQuery(Query.SQL, sqlQuery); + query.setNamedParameters(params); + try { + return new ArrayList<>(query.executeResultList(TaggedProjectRow.class)); + } finally { + query.closeAll(); + } + } + + /** + * @since 4.12.0 + */ + public record TaggedPolicyRow(String uuid, String name, long totalCount) { + } + + /** + * @since 4.12.0 + */ + @Override + public List getTaggedPolicies(final String tagName) { + final Query query = pm.newQuery(Query.SQL, /* language=SQL */ """ + SELECT "POLICY"."UUID" AS "uuid" + , "POLICY"."NAME" AS "name" + , COUNT(*) OVER() AS "totalCount" + FROM "POLICY" + INNER JOIN "POLICY_TAGS" + ON "POLICY_TAGS"."POLICY_ID" = "POLICY"."ID" + INNER JOIN "TAG" + ON "TAG"."ID" = "POLICY_TAGS"."TAG_ID" + WHERE "TAG"."NAME" = :name + """); + + // TODO: Filter on policy name + // TODO: Ordering + // TODO: Pagination + + query.setParameters(tagName); + try { + return new ArrayList<>(query.executeResultList(TaggedPolicyRow.class)); + } finally { + query.closeAll(); + } + } + + @Override public PaginatedResult getTags(String policyUuid) { LOGGER.debug("Retrieving tags under policy " + policyUuid); diff --git a/src/main/java/org/dependencytrack/resources/v1/TagResource.java b/src/main/java/org/dependencytrack/resources/v1/TagResource.java index 8606d073d0..d4f54a50ab 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TagResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TagResource.java @@ -35,7 +35,13 @@ import org.dependencytrack.model.Tag; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +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.vo.TagListResponseItem; +import org.dependencytrack.resources.v1.vo.TaggedPolicyListResponseItem; +import org.dependencytrack.resources.v1.vo.TaggedProjectListResponseItem; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -43,6 +49,8 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.UUID; @Path("/v1/tag") @io.swagger.v3.oas.annotations.tags.Tag(name = "tag") @@ -52,6 +60,95 @@ }) public class TagResource extends AlpineResource { + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of all tags", + description = "

Requires permission VIEW_PORTFOLIO

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of all tags", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of tags", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagListResponseItem.class))) + ) + }) + @PaginatedApi + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) + public Response getAllTags() { + final List tagListRows; + try (final var qm = new QueryManager(getAlpineRequest())) { + tagListRows = qm.getTags(); + } + + final List tags = tagListRows.stream() + .map(row -> new TagListResponseItem(row.name(), row.projectCount(), row.policyCount())) + .toList(); + final long totalCount = tagListRows.isEmpty() ? 0 : tagListRows.getFirst().totalCount(); + return Response.ok(tags).header(TOTAL_COUNT_HEADER, totalCount).build(); + } + + @GET + @Path("/{name}/project") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of all projects assigned to the given tag.", + description = "

Requires permission VIEW_PORTFOLIO

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of all projects assigned to the given tag", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = TaggedProjectListResponseItem.class))) + ) + }) + @PaginatedApi + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) + public Response getTaggedProjects(@PathParam("name") final String tagName) { + final List taggedProjectListRows; + try (final var qm = new QueryManager(getAlpineRequest())) { + taggedProjectListRows = qm.getTaggedProjects(tagName); + } + + final List tags = taggedProjectListRows.stream() + .map(row -> new TaggedProjectListResponseItem(UUID.fromString(row.uuid()), row.name(), row.version())) + .toList(); + final long totalCount = taggedProjectListRows.isEmpty() ? 0 : taggedProjectListRows.getFirst().totalCount(); + return Response.ok(tags).header(TOTAL_COUNT_HEADER, totalCount).build(); + } + + @GET + @Path("/{name}/policy") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of all policies assigned to the given tag.", + description = "

Requires permission VIEW_PORTFOLIO

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of all policies assigned to the given tag", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of policies", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = TaggedProjectListResponseItem.class))) + ) + }) + @PaginatedApi + @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) + public Response getTaggedPolicies(@PathParam("name") final String tagName) { + final List taggedPolicyListRows; + try (final var qm = new QueryManager(getAlpineRequest())) { + taggedPolicyListRows = qm.getTaggedPolicies(tagName); + } + + final List tags = taggedPolicyListRows.stream() + .map(row -> new TaggedPolicyListResponseItem(UUID.fromString(row.uuid()), row.name())) + .toList(); + final long totalCount = taggedPolicyListRows.isEmpty() ? 0 : taggedPolicyListRows.getFirst().totalCount(); + return Response.ok(tags).header(TOTAL_COUNT_HEADER, totalCount).build(); + } + @GET @Path("/{policyUuid}") @Produces(MediaType.APPLICATION_JSON) @@ -71,7 +168,7 @@ public class TagResource extends AlpineResource { }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getTags(@Parameter(description = "The UUID of the policy", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("policyUuid") @ValidUuid String policyUuid){ + @PathParam("policyUuid") @ValidUuid String policyUuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final PaginatedResult result = qm.getTags(policyUuid); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/TagListResponseItem.java b/src/main/java/org/dependencytrack/resources/v1/vo/TagListResponseItem.java new file mode 100644 index 0000000000..1200757c95 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/vo/TagListResponseItem.java @@ -0,0 +1,31 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.vo; + +import io.swagger.v3.oas.annotations.Parameter; + +/** + * @since 4.12.0 + */ +public record TagListResponseItem( + @Parameter(description = "Name of the tag") String name, + @Parameter(description = "Number of projects assigned to this tag") long projectCount, + @Parameter(description = "Number of policies assigned to this tag") long policyCount +) { +} diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/TaggedPolicyListResponseItem.java b/src/main/java/org/dependencytrack/resources/v1/vo/TaggedPolicyListResponseItem.java new file mode 100644 index 0000000000..3d8322cda8 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/vo/TaggedPolicyListResponseItem.java @@ -0,0 +1,32 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.vo; + +import io.swagger.v3.oas.annotations.Parameter; + +import java.util.UUID; + +/** + * @since 4.12.0 + */ +public record TaggedPolicyListResponseItem( + @Parameter(description = "UUID of the policy") UUID uuid, + @Parameter(description = "Name of the policy") String name +) { +} diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/TaggedProjectListResponseItem.java b/src/main/java/org/dependencytrack/resources/v1/vo/TaggedProjectListResponseItem.java new file mode 100644 index 0000000000..fe21a2b901 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/vo/TaggedProjectListResponseItem.java @@ -0,0 +1,33 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.vo; + +import io.swagger.v3.oas.annotations.Parameter; + +import java.util.UUID; + +/** + * @since 4.12.0 + */ +public record TaggedProjectListResponseItem( + @Parameter(description = "UUID of the project") UUID uuid, + @Parameter(description = "Name of the project") String name, + @Parameter(description = "Version of the project") String version +) { +} diff --git a/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java index e447ce26a8..1c01e1797b 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java @@ -5,6 +5,8 @@ import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.Policy; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Tag; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; import org.junit.ClassRule; @@ -14,6 +16,10 @@ import jakarta.ws.rs.core.Response; import java.util.List; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED; + public class TagResourceTest extends ResourceTest { @ClassRule @@ -22,10 +28,193 @@ public class TagResourceTest extends ResourceTest { .register(ApiFilter.class) .register(AuthenticationFilter.class)); + @Test + public void getTagsTest() { + 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 var projectC = new Project(); + projectC.setName("acme-app-c"); + qm.persist(projectC); + + final Tag tagFoo = qm.createTag("foo"); + final Tag tagBar = qm.createTag("bar"); + + qm.bind(projectA, List.of(tagFoo, tagBar)); + qm.bind(projectB, List.of(tagFoo)); + qm.bind(projectC, List.of(tagFoo)); + + projectA.addAccessTeam(team); + projectB.addAccessTeam(team); + // NB: Not assigning projectC + + final var policy = new Policy(); + policy.setName("policy"); + policy.setOperator(Policy.Operator.ALL); + policy.setViolationState(Policy.ViolationState.INFO); + policy.setTags(List.of(tagBar)); + qm.persist(policy); + + final Response response = jersey.target(V1_TAG) + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("2"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + [ + { + "name": "bar", + "projectCount": 1, + "policyCount": 1 + }, + { + "name": "foo", + "projectCount": 2, + "policyCount": 0 + } + ] + """); + } + + @Test + public void getTagsWithPaginationTest() { + for (int i = 0; i < 5; i++) { + qm.createTag("tag-" + (i + 1)); + } + + Response response = jersey.target(V1_TAG) + .queryParam("pageNumber", "1") + .queryParam("pageSize", "3") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("5"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + [ + { + "name": "tag-1", + "projectCount": 0, + "policyCount": 0 + }, + { + "name": "tag-2", + "projectCount": 0, + "policyCount": 0 + }, + { + "name": "tag-3", + "projectCount": 0, + "policyCount": 0 + } + ] + """); + + response = jersey.target(V1_TAG) + .queryParam("pageNumber", "2") + .queryParam("pageSize", "3") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("5"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + [ + { + "name": "tag-4", + "projectCount": 0, + "policyCount": 0 + }, + { + "name": "tag-5", + "projectCount": 0, + "policyCount": 0 + } + ] + """); + } + + @Test + public void getTagsWithFilterTest() { + qm.createTag("foo"); + qm.createTag("bar"); + + final Response response = jersey.target(V1_TAG) + .queryParam("filter", "o") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + [ + { + "name": "foo", + "projectCount": 0, + "policyCount": 0 + } + ] + """); + } + + @Test + public void getTagsSortByProjectCountTest() { + 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 tagFoo = qm.createTag("foo"); + final Tag tagBar = qm.createTag("bar"); + + qm.bind(projectA, List.of(tagFoo, tagBar)); + qm.bind(projectB, List.of(tagFoo)); + + final Response response = jersey.target(V1_TAG) + .queryParam("sortName", "projectCount") + .queryParam("sortOrder", "desc") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("2"); + assertThatJson(getPlainTextBody(response)).isEqualTo(""" + [ + { + "name": "foo", + "projectCount": 2, + "policyCount": 0 + }, + { + "name": "bar", + "projectCount": 1, + "policyCount": 0 + } + ] + """); + } + @Test public void getAllTagsWithOrderingTest() { - for (int i=1; i<5; i++) { - qm.createTag("Tag "+i); + for (int i = 1; i < 5; i++) { + qm.createTag("Tag " + i); } qm.createProject("Project A", null, "1", List.of(qm.getTagByName("Tag 1"), qm.getTagByName("Tag 2")), null, null, true, false); qm.createProject("Project B", null, "1", List.of(qm.getTagByName("Tag 2"), qm.getTagByName("Tag 3"), qm.getTagByName("Tag 4")), null, null, true, false); @@ -46,8 +235,8 @@ public void getAllTagsWithOrderingTest() { @Test public void getTagsWithPolicyProjectsFilterTest() { - for (int i=1; i<5; i++) { - qm.createTag("Tag "+i); + for (int i = 1; i < 5; i++) { + qm.createTag("Tag " + i); } qm.createProject("Project A", null, "1", List.of(qm.getTagByName("Tag 1"), qm.getTagByName("Tag 2")), null, null, true, false); qm.createProject("Project B", null, "1", List.of(qm.getTagByName("Tag 1"), qm.getTagByName("Tag 3")), null, null, true, false);