From 67285e02df91e1abdd648e6971dcc7ebfbfe0657 Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 19 Oct 2023 15:28:22 +0200 Subject: [PATCH] Added transient List of ProjectVersions and set Metrics in Project Ported from https://github.com/DependencyTrack/dependency-track/pull/2581 Co-authored-by: Walter de Boer Signed-off-by: nscuro --- .../org/dependencytrack/model/Project.java | 10 +++ .../dependencytrack/model/ProjectVersion.java | 64 +++++++++++++++ .../persistence/ProjectQueryManager.java | 81 ++++++++++++++++--- .../persistence/QueryManager.java | 6 +- .../resources/v1/ProjectResource.java | 9 +-- .../resources/v1/ProjectResourceTest.java | 30 ++++++- 6 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/ProjectVersion.java diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 5068a8ce4..184b5492c 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -277,6 +277,8 @@ public enum FetchGroup { private transient ProjectMetrics metrics; + private transient List versions; + private transient List dependencyGraph; public long getId() { @@ -476,6 +478,14 @@ public void setMetrics(ProjectMetrics metrics) { this.metrics = metrics; } + public List getVersions() { + return versions; + } + + public void setVersions(List versions) { + this.versions = versions; + } + public List getAccessTeams() { return accessTeams; } diff --git a/src/main/java/org/dependencytrack/model/ProjectVersion.java b/src/main/java/org/dependencytrack/model/ProjectVersion.java new file mode 100644 index 000000000..bf83558e1 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/ProjectVersion.java @@ -0,0 +1,64 @@ +/* + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.io.Serializable; +import java.util.UUID; + +/** + * Value object holding UUID and version for a project + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProjectVersion implements Serializable { + + private static final long serialVersionUID = 1L; + + private UUID uuid; + + private String version; + + public ProjectVersion() { + this.uuid = null; + this.version = null; + } + + public ProjectVersion(UUID uuid, String version) { + this.uuid = uuid; + this.version = version; + + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + public UUID getUuid() { + return uuid; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 81b280bd7..62b69a2a5 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -38,6 +38,7 @@ import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.model.ProjectVersion; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.notification.NotificationConstants; @@ -84,6 +85,7 @@ final class ProjectQueryManager extends QueryManager implements IQueryManager { * * @return a List of Projects */ + @Override public PaginatedResult getProjects(final boolean includeMetrics, final boolean excludeInactive, final boolean onlyRoot) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); @@ -131,6 +133,7 @@ public PaginatedResult getProjects(final boolean includeMetrics, final boolean e * * @return a List of Projects */ + @Override public PaginatedResult getProjects(final boolean includeMetrics) { return getProjects(includeMetrics, false, false); } @@ -140,6 +143,7 @@ public PaginatedResult getProjects(final boolean includeMetrics) { * * @return a List of Projects */ + @Override public PaginatedResult getProjects() { return getProjects(false); } @@ -150,6 +154,7 @@ public PaginatedResult getProjects() { * * @return a List of Projects */ + @Override public List getAllProjects() { return getAllProjects(false); } @@ -160,6 +165,7 @@ public List getAllProjects() { * * @return a List of Projects */ + @Override public List getAllProjects(boolean excludeInactive) { final Query query = pm.newQuery(Project.class); if (excludeInactive) { @@ -175,6 +181,7 @@ public List getAllProjects(boolean excludeInactive) { * @param name the name of the Projects (required) * @return a List of Project objects */ + @Override public PaginatedResult getProjects(final String name, final boolean excludeInactive, final boolean onlyRoot) { final Query query = pm.newQuery(Project.class); if (orderBy == null) { @@ -197,6 +204,23 @@ public PaginatedResult getProjects(final String name, final boolean excludeInact return execute(query, params); } + /** + * Returns a project by its uuid. + * @param uuid the uuid of the Project (required) + * @return a Project object, or null if not found + */ + @Override + public Project getProject(final String uuid) { + final Project project = getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); + if (project != null) { + // set Metrics to minimize the number of round trips a client needs to make + project.setMetrics(getMostRecentProjectMetrics(project)); + // set ProjectVersions to minimize the number of round trips a client needs to make + project.setVersions(getProjectVersions(project)); + } + return project; + } + /** * Returns a project by its name and version. * @@ -204,6 +228,7 @@ public PaginatedResult getProjects(final String name, final boolean excludeInact * @param version the version of the Project (or null) * @return a Project object, or null if not found */ + @Override public Project getProject(final String name, final String version) { final Query query = pm.newQuery(Project.class); @@ -217,7 +242,14 @@ public Project getProject(final String name, final String version) { preprocessACLs(query, queryFilter, params, false); query.setFilter(queryFilter); query.setRange(0, 1); - return singleResult(query.executeWithMap(params)); + final Project project = singleResult(query.executeWithMap(params)); + if (project != null) { + // set Metrics to prevent extra round trip + project.setMetrics(getMostRecentProjectMetrics(project)); + // set ProjectVersions to prevent extra round trip + project.setVersions(getProjectVersions(project)); + } + return project; } /** @@ -226,6 +258,7 @@ public Project getProject(final String name, final String version) { * @param team the team the has access to Projects * @return a List of Project objects */ + @Override public PaginatedResult getProjects(final Team team, final boolean excludeInactive, final boolean bypass, final boolean onlyRoot) { final Query query = pm.newQuery(Project.class); if (orderBy == null) { @@ -254,6 +287,7 @@ public PaginatedResult getProjects(final Team team, final boolean excludeInactiv * @param tag the tag associated with the Project * @return a List of Projects that contain the tag */ + @Override public PaginatedResult getProjects(final Tag tag, final boolean includeMetrics, final boolean excludeInactive, final boolean onlyRoot) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); @@ -296,6 +330,7 @@ public PaginatedResult getProjects(final Tag tag, final boolean includeMetrics, * @param classifier the classifier of the Project * @return a List of Projects of the specified classifier */ + @Override public PaginatedResult getProjects(final Classifier classifier, final boolean includeMetrics, final boolean excludeInactive, final boolean onlyRoot) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); @@ -333,6 +368,7 @@ public PaginatedResult getProjects(final Classifier classifier, final boolean in * @param tag the tag associated with the Project * @return a List of Projects that contain the tag */ + @Override public PaginatedResult getProjects(final Tag tag) { return getProjects(tag, false, false, false); } @@ -373,6 +409,7 @@ private synchronized List resolveTags(final List tags) { * @param name the name of the Tag * @return a Tag object */ + @Override public Tag getTagByName(final String name) { final String loweredTrimmedTag = StringUtils.lowerCase(StringUtils.trimToNull(name)); final Query query = pm.newQuery(Tag.class, "name == :name"); @@ -386,6 +423,7 @@ public Tag getTagByName(final String name) { * @param name the name of the Tag to create * @return the created Tag object */ + @Override public Tag createTag(final String name) { final String loweredTrimmedTag = StringUtils.lowerCase(StringUtils.trimToNull(name)); final Tag resolvedTag = getTagByName(loweredTrimmedTag); @@ -429,6 +467,7 @@ private List createTags(final List names) { * @param commitIndex specifies if the search index should be committed (an expensive operation) * @return the created Project */ + @Override public Project createProject(String name, String description, String version, List tags, Project parent, PackageURL purl, boolean active, boolean commitIndex) { final Project project = new Project(); project.setName(name); @@ -468,6 +507,7 @@ public Project createProject(String name, String description, String version, Li * @param commitIndex specifies if the search index should be committed (an expensive operation) * @return the created Project */ + @Override public Project createProject(final Project project, List tags, boolean commitIndex) { if (project.getParent() != null && !Boolean.TRUE.equals(project.getParent().isActive())) { throw new IllegalArgumentException("An inactive Parent cannot be selected as parent"); @@ -503,6 +543,7 @@ public Project createProject(final Project project, List tags, boolean comm * @param commitIndex specifies if the search index should be committed (an expensive operation) * @return the updated Project */ + @Override public Project updateProject(UUID uuid, String name, String description, String version, List tags, PackageURL purl, boolean active, boolean commitIndex) { final Project project = getObjectByUuid(Project.class, uuid); project.setName(name); @@ -531,6 +572,7 @@ public Project updateProject(UUID uuid, String name, String description, String * @param commitIndex specifies if the search index should be committed (an expensive operation) * @return the updated Project */ + @Override public Project updateProject(Project transientProject, boolean commitIndex) { final Project project = getObjectByUuid(Project.class, transientProject.getUuid()); project.setAuthor(transientProject.getAuthor()); @@ -576,6 +618,7 @@ public Project updateProject(Project transientProject, boolean commitIndex) { return result; } + @Override public Project clone(UUID from, String newVersion, boolean includeTags, boolean includeProperties, boolean includeComponents, boolean includeServices, boolean includeAuditHistory, boolean includeACL) { @@ -688,6 +731,7 @@ public Project clone(UUID from, String newVersion, boolean includeTags, boolean * @param project the Project to delete * @param commitIndex specifies if the search index should be committed (an expensive operation) */ + @Override public void recursivelyDelete(final Project project, final boolean commitIndex) { final Transaction trx = pm.currentTransaction(); final boolean isJoiningExistingTrx = trx.isActive(); @@ -755,6 +799,7 @@ public void recursivelyDelete(final Project project, final boolean commitIndex) * @param description a description of the property * @return the created ProjectProperty object */ + @Override public ProjectProperty createProjectProperty(final Project project, final String groupName, final String propertyName, final String propertyValue, final ProjectProperty.PropertyType propertyType, final String description) { @@ -776,6 +821,7 @@ public ProjectProperty createProjectProperty(final Project project, final String * @param propertyName the name of the property * @return a ProjectProperty object */ + @Override public ProjectProperty getProjectProperty(final Project project, final String groupName, final String propertyName) { final Query query = this.pm.newQuery(ProjectProperty.class, "project == :project && groupName == :groupName && propertyName == :propertyName"); query.setRange(0, 1); @@ -788,6 +834,7 @@ public ProjectProperty getProjectProperty(final Project project, final String gr * @param project the project the property belongs to * @return a List ProjectProperty objects */ + @Override @SuppressWarnings("unchecked") public List getProjectProperties(final Project project) { final Query query = this.pm.newQuery(ProjectProperty.class, "project == :project"); @@ -829,6 +876,7 @@ public void bind(Project project, List tags) { * @param bomFormat the format and version of the bom format * @return the updated Project */ + @Override public Project updateLastBomImport(Project p, Date date, String bomFormat) { final Project project = getObjectById(Project.class, p.getId()); project.setLastBomImport(date); @@ -836,10 +884,10 @@ public Project updateLastBomImport(Project p, Date date, String bomFormat) { return persist(project); } + @Override public boolean hasAccess(final Principal principal, final Project project) { if (isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED)) { - if (principal instanceof UserPrincipal) { - final UserPrincipal userPrincipal = (UserPrincipal) principal; + if (principal instanceof UserPrincipal userPrincipal) { if (super.hasAccessManagementPermission(userPrincipal)) { return true; } @@ -852,8 +900,7 @@ public boolean hasAccess(final Principal principal, final Project project) { } } } - } else if (principal instanceof ApiKey) { - final ApiKey apiKey = (ApiKey) principal; + } else if (principal instanceof ApiKey apiKey) { if (super.hasAccessManagementPermission(apiKey)) { return true; } @@ -882,8 +929,7 @@ public boolean hasAccess(final Principal principal, final Project project) { private void preprocessACLs(final Query query, final String inputFilter, final Map params, final boolean bypass) { if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && !bypass) { final List teams; - if (super.principal instanceof UserPrincipal) { - final UserPrincipal userPrincipal = ((UserPrincipal) super.principal); + if (super.principal instanceof UserPrincipal userPrincipal) { teams = userPrincipal.getTeams(); if (super.hasAccessManagementPermission(userPrincipal)) { query.setFilter(inputFilter); @@ -928,9 +974,9 @@ private void preprocessACLs(final Query query, final String inputFilter * @param principal * @return True if ACL was updated */ + @Override public boolean updateNewProjectACL(Project project, Principal principal) { - if (isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && principal instanceof ApiKey) { - ApiKey apiKey = (ApiKey) principal; + if (isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && principal instanceof ApiKey apiKey) { final var apiTeam = apiKey.getTeams().stream().findFirst(); if (apiTeam.isPresent()) { LOGGER.debug("adding Team to ACL of newly created project"); @@ -945,6 +991,7 @@ public boolean updateNewProjectACL(Project project, Principal principal) { return false; } + @Override public boolean hasAccessManagementPermission(final UserPrincipal userPrincipal) { for (Permission permission : getEffectivePermissions(userPrincipal)) { if (Permissions.ACCESS_MANAGEMENT.name().equals(permission.getName())) { @@ -954,11 +1001,12 @@ public boolean hasAccessManagementPermission(final UserPrincipal userPrincipal) return false; } + @Override public boolean hasAccessManagementPermission(final ApiKey apiKey) { return hasPermission(apiKey, Permissions.ACCESS_MANAGEMENT.name()); } - + @Override public PaginatedResult getChildrenProjects(final UUID uuid, final boolean includeMetrics, final boolean excludeInactive) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); @@ -998,6 +1046,7 @@ public PaginatedResult getChildrenProjects(final UUID uuid, final boolean includ return result; } + @Override public PaginatedResult getChildrenProjects(final Classifier classifier, final UUID uuid, final boolean includeMetrics, final boolean excludeInactive) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); @@ -1026,6 +1075,7 @@ public PaginatedResult getChildrenProjects(final Classifier classifier, final UU return result; } + @Override public PaginatedResult getChildrenProjects(final Tag tag, final UUID uuid, final boolean includeMetrics, final boolean excludeInactive) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); @@ -1058,6 +1108,7 @@ public PaginatedResult getChildrenProjects(final Tag tag, final UUID uuid, final return result; } + @Override public PaginatedResult getProjectsWithoutDescendantsOf(final boolean exludeInactive, final Project project) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); @@ -1092,6 +1143,7 @@ public PaginatedResult getProjectsWithoutDescendantsOf(final boolean exludeInact return result; } + @Override public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final boolean excludeInactive, Project project) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); @@ -1133,6 +1185,7 @@ public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final * @param project The {@link Project} to fetch the parent {@link UUID}s for * @return A {@link List} of {@link UUID}s */ + @Override public List getParents(final Project project) { return getParents(project.getUuid(), new ArrayList<>()); } @@ -1182,4 +1235,12 @@ private static boolean hasActiveChild(Project project) { } return hasActiveChild; } + + private List getProjectVersions(Project project) { + final Query query = pm.newQuery(Project.class); + query.setFilter("name == :name"); + query.setParameters(project.getName()); + query.setResult("uuid, version"); + return query.executeResultList(ProjectVersion.class); + } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 4a5a7439e..3349930f5 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -439,6 +439,10 @@ public PaginatedResult getProjects(final String name, final boolean excludeInact return getProjectQueryManager().getProjects(name, excludeInactive, onlyRoot); } + public Project getProject(final String uuid) { + return getProjectQueryManager().getProject(uuid); + } + public Project getProject(final String name, final String version) { return getProjectQueryManager().getProject(name, version); } @@ -1465,7 +1469,7 @@ public void recursivelyDeleteTeam(Team team) { pm.currentTransaction().begin(); pm.deletePersistentAll(team.getApiKeys()); String aclDeleteQuery = """ - DELETE FROM PROJECT_ACCESS_TEAMS WHERE \"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = ? + DELETE FROM PROJECT_ACCESS_TEAMS WHERE \"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = ? """; final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, aclDeleteQuery); query.executeWithArray(team.getId()); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 55620857a..6b3827e1c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -40,16 +40,13 @@ import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.CloneProjectRequest; +import javax.jdo.FetchGroup; import javax.validation.Validator; -import java.util.Collection; -import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.PATCH; import javax.ws.rs.POST; - -import javax.jdo.FetchGroup; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -58,6 +55,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.security.Principal; +import java.util.Collection; +import java.util.List; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; @@ -115,7 +114,7 @@ public Response getProject( @ApiParam(value = "The UUID of the project to retrieve", required = true) @PathParam("uuid") String uuid) { try (QueryManager qm = new QueryManager()) { - final Project project = qm.getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); + final Project project = qm.getProject(uuid); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(project).build(); diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 85b1bba11..58bb50daf 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -19,7 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.common.util.UuidUtil; -import alpine.notification.Notification; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import org.dependencytrack.ResourceTest; @@ -48,7 +47,6 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -70,8 +68,6 @@ protected DeploymentContext configureDeployment() { .build(); } - private static final ConcurrentLinkedQueue NOTIFICATIONS = new ConcurrentLinkedQueue<>(); - @Test public void getProjectsDefaultRequestTest() { for (int i = 0; i < 1000; i++) { @@ -179,6 +175,29 @@ public void getProjectsByNameActiveOnlyRequestTest() { Assert.assertEquals(100, json.size()); } + @Test + public void getProjectLookupTest() { + for (int i=0; i<500; i++) { + qm.createProject("Acme Example", null, String.valueOf(i), null, null, null, false, false); + } + Response response = target(V1_PROJECT+"/lookup") + .queryParam("name", "Acme Example") + .queryParam("version", "10") + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("Acme Example", json.getString("name")); + Assert.assertEquals("10", json.getString("version")); + Assert.assertEquals(500, json.getJsonArray("versions").size()); + Assert.assertNotNull(json.getJsonArray("versions").getJsonObject(100).getString("uuid")); + Assert.assertNotEquals("", json.getJsonArray("versions").getJsonObject(100).getString("uuid")); + Assert.assertEquals("100", json.getJsonArray("versions").getJsonObject(100).getString("version")); + } + @Test public void getProjectsAscOrderedRequestTest() { qm.createProject("ABC", null, "1.0", null, null, null, true, false); @@ -225,6 +244,9 @@ public void getProjectByUuidTest() { JsonObject json = parseJsonObject(response); Assert.assertNotNull(json); Assert.assertEquals("ABC", json.getString("name")); + Assert.assertEquals(1, json.getJsonArray("versions").size()); + Assert.assertEquals(project.getUuid().toString(), json.getJsonArray("versions").getJsonObject(0).getJsonString("uuid").getString()); + Assert.assertEquals("1.0", json.getJsonArray("versions").getJsonObject(0).getJsonString("version").getString()); } @Test