/* * 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.persistence; import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.model.ApiKey; import alpine.model.Permission; import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.notification.Notification; import alpine.notification.NotificationLevel; import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; import com.github.packageurl.PackageURL; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.IndexEvent; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalysisComment; import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.PolicyViolation; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetadata; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ProjectVersion; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.util.NotificationUtil; import javax.jdo.FetchPlan; import javax.jdo.PersistenceManager; import javax.jdo.Query; import java.security.Principal; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; final class ProjectQueryManager extends QueryManager implements IQueryManager { private static final Logger LOGGER = Logger.getLogger(ProjectQueryManager.class); /** * Constructs a new QueryManager. * @param pm a PersistenceManager object */ ProjectQueryManager(final PersistenceManager pm) { super(pm); } /** * Constructs a new QueryManager. * @param pm a PersistenceManager object * @param request an AlpineRequest object */ ProjectQueryManager(final PersistenceManager pm, final AlpineRequest request) { super(pm, request); } /** * Returns a list of all projects. * @return a List of Projects */ @Override public PaginatedResult getProjects(final boolean includeMetrics, final boolean excludeInactive, final boolean onlyRoot, final Team notAssignedToTeam) { final PaginatedResult result; final Query query = pm.newQuery(Project.class); if (orderBy == null) { query.setOrdering("name asc, version desc"); } var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive); if (onlyRoot){ filterBuilder.excludeChildProjects(); query.getFetchPlan().addGroup(Project.FetchGroup.ALL.name()); } if(notAssignedToTeam != null) { filterBuilder.notWithTeam(notAssignedToTeam); } if (filter != null) { final String filterString = ".*" + filter.toLowerCase() + ".*"; final Tag tag = getTagByName(filter.trim()); if (tag != null) { filterBuilder = filterBuilder.withFuzzyNameOrExactTag(filterString, tag); } else { filterBuilder = filterBuilder.withFuzzyName(filterString); } } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); result = execute(query, params); if (includeMetrics) { // Populate each Project object in the paginated result with transitive related // data to minimize the number of round trips a client needs to make, process, and render. for (Project project : result.getList(Project.class)) { project.setMetrics(getMostRecentProjectMetrics(project)); } } return result; } /** * Returns a list of all projects. * @return a List of Projects */ @Override public PaginatedResult getProjects(final boolean includeMetrics) { return getProjects(includeMetrics, false, false, null); } /** * Returns a list of all projects. * @return a List of Projects */ @Override public PaginatedResult getProjects() { return getProjects(false); } /** * Returns a list of all projects. * This method if designed NOT to provide paginated results. * @return a List of Projects */ @Override public List getAllProjects() { return getAllProjects(false); } /** * Returns a list of all projects. * This method if designed NOT to provide paginated results. * @return a List of Projects */ @Override public List getAllProjects(boolean excludeInactive) { final Query query = pm.newQuery(Project.class); if (excludeInactive) { query.setFilter("active == true || active == null"); } query.setOrdering("id asc"); return query.executeList(); } /** * Returns a list of projects by their name. * @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 Team notAssignedToTeam) { final Query query = pm.newQuery(Project.class); if (orderBy == null) { query.setOrdering("version desc"); } final var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive) .withName(name); if (onlyRoot) { filterBuilder.excludeChildProjects(); query.getFetchPlan().addGroup(Project.FetchGroup.ALL.name()); } if(notAssignedToTeam != null) { filterBuilder.notWithTeam(notAssignedToTeam); } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); 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. * @param name the name of the Project (required) * @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); final var filterBuilder = new ProjectQueryFilterBuilder() .withName(name) .withVersion(version); final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); query.setFilter(queryFilter); query.setRange(0, 1); 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; } /** * Returns a list of projects that are accessible by the specified team. * @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) { query.setOrdering("name asc, version desc, id asc"); } final var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive) .withTeam(team); if (onlyRoot){ filterBuilder.excludeChildProjects(); query.getFetchPlan().addGroup(Project.FetchGroup.ALL.name()); } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, bypass); return execute(query, params); } /** * Returns a paginated result of projects by tag. * @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); if (orderBy == null) { query.setOrdering("name asc"); } var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive) .withTag(tag); if (onlyRoot){ filterBuilder.excludeChildProjects(); query.getFetchPlan().addGroup(Project.FetchGroup.ALL.name()); } if (filter != null) { final String filterString = ".*" + filter.toLowerCase() + ".*"; filterBuilder = filterBuilder.withFuzzyName(filterString); } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); result = execute(query, params); if (includeMetrics) { // Populate each Project object in the paginated result with transitive related // data to minimize the number of round trips a client needs to make, process, and render. for (Project project : result.getList(Project.class)) { project.setMetrics(getMostRecentProjectMetrics(project)); } } return result; } /** * Returns a paginated result of projects by classifier. * @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); if (orderBy == null) { query.setOrdering("name asc"); } final var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive) .withClassifier(classifier); if (onlyRoot){ filterBuilder.excludeChildProjects(); query.getFetchPlan().addGroup(Project.FetchGroup.ALL.name()); } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); result = execute(query, params); if (includeMetrics) { // Populate each Project object in the paginated result with transitive related // data to minimize the number of round trips a client needs to make, process, and render. for (Project project : result.getList(Project.class)) { project.setMetrics(getMostRecentProjectMetrics(project)); } } return result; } /** * Returns a paginated result of projects by tag. * @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); } /** * Returns a list of Tag objects what have been resolved. It resolved * tags by querying the database to retrieve the tag. If the tag does * not exist, the tag will be created and returned with other resolved * tags. * @param tags a List of Tags to resolve * @return List of resolved Tags */ private synchronized List resolveTags(final List tags) { if (tags == null) { return new ArrayList<>(); } final List resolvedTags = new ArrayList<>(); final List unresolvedTags = new ArrayList<>(); for (final Tag tag: tags) { final String trimmedTag = StringUtils.trimToNull(tag.getName()); if (trimmedTag != null) { final Tag resolvedTag = getTagByName(trimmedTag); if (resolvedTag != null) { resolvedTags.add(resolvedTag); } else { unresolvedTags.add(trimmedTag); } } } resolvedTags.addAll(createTags(unresolvedTags)); return resolvedTags; } /** * Returns a list of Tag objects by name. * @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"); query.setRange(0, 1); return singleResult(query.execute(loweredTrimmedTag)); } /** * Creates a new Tag object with the specified 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); if (resolvedTag != null) { return resolvedTag; } final Tag tag = new Tag(); tag.setName(loweredTrimmedTag); return persist(tag); } /** * Creates one or more Tag objects from the specified name(s). * @param names the name(s) of the Tag(s) to create * @return the created Tag object(s) */ private List createTags(final List names) { final List newTags = new ArrayList<>(); for (final String name: names) { final String loweredTrimmedTag = StringUtils.lowerCase(StringUtils.trimToNull(name)); if (getTagByName(loweredTrimmedTag) == null) { final Tag tag = new Tag(); tag.setName(loweredTrimmedTag); newTags.add(tag); } } return new ArrayList<>(persist(newTags)); } /** * Creates a new Project. * @param name the name of the project to create * @param description a description of the project * @param version the project version * @param tags a List of Tags - these will be resolved if necessary * @param parent an optional parent Project * @param purl an optional Package URL * @param active specified if the project is active * @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); project.setDescription(description); project.setVersion(version); if (parent != null ) { if (!Boolean.TRUE.equals(parent.isActive())){ throw new IllegalArgumentException("An inactive Parent cannot be selected as parent"); } project.setParent(parent); } project.setPurl(purl); project.setActive(active); final Project result = persist(project); final List resolvedTags = resolveTags(tags); bind(project, resolvedTags); Event.dispatch(new IndexEvent(IndexEvent.Action.CREATE, result)); Notification.dispatch(new Notification() .scope(NotificationScope.PORTFOLIO) .group(NotificationGroup.PROJECT_CREATED) .title(NotificationConstants.Title.PROJECT_CREATED) .level(NotificationLevel.INFORMATIONAL) .content(result.getName() + " was created") .subject(NotificationUtil.toJson(pm.detachCopy(result))) ); commitSearchIndex(commitIndex, Project.class); return result; } /** * Creates a new Project. * @param project the project to create * @param tags a List of Tags - these will be resolved if necessary * @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"); } if (project.isActive() == null) { project.setActive(Boolean.TRUE); } final Project result = persist(project); final List resolvedTags = resolveTags(tags); bind(project, resolvedTags); Event.dispatch(new IndexEvent(IndexEvent.Action.CREATE, result)); commitSearchIndex(commitIndex, Project.class); return result; } /** * Updates an existing Project. * @param transientProject the project to update * @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()); project.setPublisher(transientProject.getPublisher()); project.setManufacturer(transientProject.getManufacturer()); project.setSupplier(transientProject.getSupplier()); project.setGroup(transientProject.getGroup()); project.setName(transientProject.getName()); project.setDescription(transientProject.getDescription()); project.setVersion(transientProject.getVersion()); project.setClassifier(transientProject.getClassifier()); project.setCpe(transientProject.getCpe()); project.setPurl(transientProject.getPurl()); project.setSwidTagId(transientProject.getSwidTagId()); project.setExternalReferences(transientProject.getExternalReferences()); if (Boolean.TRUE.equals(project.isActive()) && !Boolean.TRUE.equals(transientProject.isActive()) && hasActiveChild(project)){ throw new IllegalArgumentException("Project cannot be set to inactive if active children are present."); } project.setActive(transientProject.isActive()); if (transientProject.getParent() != null && transientProject.getParent().getUuid() != null) { if (project.getUuid().equals(transientProject.getParent().getUuid())){ throw new IllegalArgumentException("A project cannot select itself as a parent"); } Project parent = getObjectByUuid(Project.class, transientProject.getParent().getUuid()); if (!Boolean.TRUE.equals(parent.isActive())){ throw new IllegalArgumentException("An inactive project cannot be selected as a parent"); } else if (isChildOf(parent, transientProject.getUuid())){ throw new IllegalArgumentException("The new parent project cannot be a child of the current project."); } else { project.setParent(parent); } project.setParent(parent); }else { project.setParent(null); } final List resolvedTags = resolveTags(transientProject.getTags()); bind(project, resolvedTags); final Project result = persist(project); Event.dispatch(new IndexEvent(IndexEvent.Action.UPDATE, result)); commitSearchIndex(commitIndex, Project.class); return result; } @Override public Project clone(UUID from, String newVersion, boolean includeTags, boolean includeProperties, boolean includeComponents, boolean includeServices, boolean includeAuditHistory, boolean includeACL, boolean includePolicyViolations) { final Project source = getObjectByUuid(Project.class, from, Project.FetchGroup.ALL.name()); if (source == null) { LOGGER.warn("Project with UUID %s was supposed to be cloned, but it does not exist anymore".formatted(from)); return null; } if (doesProjectExist(source.getName(), newVersion)) { // Project cloning is an asynchronous process. When receiving the clone request, we already perform // this check. It is possible though that a project with the new version is created synchronously // between the clone event being dispatched, and it being processed. LOGGER.warn("Project %s was supposed to be cloned to version %s, but that version already exists" .formatted(source, newVersion)); return null; } Project project = new Project(); project.setAuthor(source.getAuthor()); project.setManufacturer(source.getManufacturer()); project.setSupplier(source.getSupplier()); project.setPublisher(source.getPublisher()); project.setGroup(source.getGroup()); project.setName(source.getName()); project.setDescription(source.getDescription()); project.setVersion(newVersion); project.setClassifier(source.getClassifier()); project.setActive(source.isActive()); project.setCpe(source.getCpe()); project.setPurl(source.getPurl()); project.setSwidTagId(source.getSwidTagId()); if (includeComponents && includeServices) { project.setDirectDependencies(source.getDirectDependencies()); } project.setParent(source.getParent()); project = persist(project); if (source.getMetadata() != null) { final var metadata = new ProjectMetadata(); metadata.setProject(project); metadata.setAuthors(source.getMetadata().getAuthors()); metadata.setSupplier(source.getMetadata().getSupplier()); persist(metadata); } if (includeTags) { for (final Tag tag: source.getTags()) { tag.getProjects().add(project); persist(tag); } } if (includeProperties && source.getProperties() != null) { for (final ProjectProperty sourceProperty: source.getProperties()) { final ProjectProperty property = new ProjectProperty(); property.setProject(project); property.setPropertyType(sourceProperty.getPropertyType()); property.setGroupName(sourceProperty.getGroupName()); property.setPropertyName(sourceProperty.getPropertyName()); property.setPropertyValue(sourceProperty.getPropertyValue()); property.setDescription(sourceProperty.getDescription()); persist(property); } } final Map clonedComponents = new HashMap<>(); if (includeComponents) { final List sourceComponents = getAllComponents(source); if (sourceComponents != null) { for (final Component sourceComponent: sourceComponents) { final Component clonedComponent = cloneComponent(sourceComponent, project, false); // Add vulnerabilties and finding attribution from the source component to the cloned component for (Vulnerability vuln: sourceComponent.getVulnerabilities()) { final FindingAttribution sourceAttribution = this.getFindingAttribution(vuln, sourceComponent); this.addVulnerability(vuln, clonedComponent, sourceAttribution.getAnalyzerIdentity(), sourceAttribution.getAlternateIdentifier(), sourceAttribution.getReferenceUrl()); } clonedComponents.put(sourceComponent.getId(), clonedComponent); } } } if (includeServices) { final List sourceServices = getAllServiceComponents(source); if (sourceServices != null) { for (final ServiceComponent sourceService : sourceServices) { cloneServiceComponent(sourceService, project, false); } } } if (includeAuditHistory && includeComponents) { final List analyses = super.getAnalyses(source); if (analyses != null) { for (final Analysis sourceAnalysis: analyses) { Analysis analysis = new Analysis(); analysis.setAnalysisState(sourceAnalysis.getAnalysisState()); final Component clonedComponent = clonedComponents.get(sourceAnalysis.getComponent().getId()); if (clonedComponent == null) { break; } analysis.setComponent(clonedComponent); analysis.setVulnerability(sourceAnalysis.getVulnerability()); analysis.setSuppressed(sourceAnalysis.isSuppressed()); analysis.setAnalysisResponse(sourceAnalysis.getAnalysisResponse()); analysis.setAnalysisJustification(sourceAnalysis.getAnalysisJustification()); analysis.setAnalysisState(sourceAnalysis.getAnalysisState()); analysis.setAnalysisDetails(sourceAnalysis.getAnalysisDetails()); analysis = persist(analysis); if (sourceAnalysis.getAnalysisComments() != null) { for (final AnalysisComment sourceComment: sourceAnalysis.getAnalysisComments()) { final AnalysisComment analysisComment = new AnalysisComment(); analysisComment.setAnalysis(analysis); analysisComment.setTimestamp(sourceComment.getTimestamp()); analysisComment.setComment(sourceComment.getComment()); analysisComment.setCommenter(sourceComment.getCommenter()); persist(analysisComment); } } } } } if (includeACL) { List accessTeams = source.getAccessTeams(); if (!CollectionUtils.isEmpty(accessTeams)) { project.setAccessTeams(new ArrayList<>(accessTeams)); } } if(includeComponents && includePolicyViolations){ final List sourcePolicyViolations = getAllPolicyViolations(source); if(sourcePolicyViolations != null){ for(final PolicyViolation policyViolation: sourcePolicyViolations){ final Component destinationComponent = clonedComponents.get(policyViolation.getComponent().getId()); final PolicyViolation clonedPolicyViolation = clonePolicyViolation(policyViolation, destinationComponent); persist(clonedPolicyViolation); } } } project = getObjectById(Project.class, project.getId()); Event.dispatch(new IndexEvent(IndexEvent.Action.CREATE, project)); commitSearchIndex(true, Project.class); return project; } /** * Deletes a Project and all objects dependant on the project. * @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) { if (project.getChildren() != null) { for (final Project child: project.getChildren()) { recursivelyDelete(child, false); } } pm.getFetchPlan().setDetachmentOptions(FetchPlan.DETACH_LOAD_FIELDS); final Project result = pm.getObjectById(Project.class, project.getId()); Event.dispatch(new IndexEvent(IndexEvent.Action.DELETE, result)); commitSearchIndex(commitIndex, Project.class); deleteAnalysisTrail(project); deleteViolationAnalysisTrail(project); deleteMetrics(project); deleteFindingAttributions(project); deletePolicyViolations(project); deleteComponents(project); for (final ServiceComponent s: getAllServiceComponents(project)) { recursivelyDelete(s, false); } deleteBoms(project); deleteVexs(project); removeProjectFromNotificationRules(project); removeProjectFromPolicies(project); delete(project.getMetadata()); delete(project.getProperties()); delete(getAllBoms(project)); delete(project.getChildren()); delete(project); } /** * Creates a key/value pair (ProjectProperty) for the specified Project. * @param project the Project to create the property for * @param groupName the group name of the property * @param propertyName the name of the property * @param propertyValue the value of the property * @param propertyType the type of property * @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) { final ProjectProperty property = new ProjectProperty(); property.setProject(project); property.setGroupName(groupName); property.setPropertyName(propertyName); property.setPropertyValue(propertyValue); property.setPropertyType(propertyType); property.setDescription(description); return persist(property); } /** * Returns a ProjectProperty with the specified groupName and propertyName. * @param project the project the property belongs to * @param groupName the group name of the config property * @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); return singleResult(query.execute(project, groupName, propertyName)); } /** * Returns a List of ProjectProperty's for the specified project. * @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"); query.setOrdering("groupName asc, propertyName asc"); return (List)query.execute(project); } /** * Binds the two objects together in a corresponding join table. * @param project a Project object * @param tags a List of Tag objects */ @SuppressWarnings("unchecked") @Override public void bind(Project project, List tags) { final Query query = pm.newQuery(Tag.class, "projects.contains(:project)"); final List currentProjectTags = (List)query.execute(project); pm.currentTransaction().begin(); for (final Tag tag: currentProjectTags) { if (!tags.contains(tag)) { tag.getProjects().remove(project); } } project.setTags(tags); for (final Tag tag: tags) { final List projects = tag.getProjects(); if (!projects.contains(project)) { projects.add(project); } } pm.currentTransaction().commit(); } /** * Updates the last time a bom was imported. * @param date the date of the last bom import * @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); project.setLastBomImportFormat(bomFormat); return persist(project); } @Override public boolean hasAccess(final Principal principal, final Project project) { if (isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED)) { if (principal instanceof final UserPrincipal userPrincipal) { if (super.hasAccessManagementPermission(userPrincipal)) { return true; } if (userPrincipal.getTeams() != null) { for (final Team userInTeam : userPrincipal.getTeams()) { for (final Team accessTeam : project.getAccessTeams()) { if (userInTeam.getId() == accessTeam.getId()) { return true; } } } } } else if (principal instanceof final ApiKey apiKey ){ if (super.hasAccessManagementPermission(apiKey)) { return true; } if (apiKey.getTeams() != null) { for (final Team userInTeam : apiKey.getTeams()) { for (final Team accessTeam : project.getAccessTeams()) { if (userInTeam.getId() == accessTeam.getId()) { return true; } } } } } else if (principal == null) { // This is a system request being made (e.g. MetricsUpdateTask, etc) where there isn't a principal return true; } return false; } else { return true; } } /** * A similar method exists in ComponentQueryManager */ 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 final UserPrincipal userPrincipal) { 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(" accessTeams.contains(:team").append(i).append(") "); params.put("team" + i, team); if (i < teamsSize-1) { sb.append(" || "); } } if (inputFilter != null && !inputFilter.isBlank()) { query.setFilter(inputFilter + " && (" + sb.toString() + ")"); } else { query.setFilter(sb.toString()); } } } else if (StringUtils.trimToNull(inputFilter) != null) { query.setFilter(inputFilter); } } /** * Updates a Project ACL to add the principals Team to the AccessTeams * This only happens if Portfolio Access Control is enabled and the @param principal is an ApyKey * For a UserPrincipal we don't know which Team(s) to add to the ACL, * See https://github.com/DependencyTrack/dependency-track/issues/1435 * @param project * @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) { final var apiTeam = apiKey.getTeams().stream().findFirst(); if (apiTeam.isPresent()) { LOGGER.debug("adding Team to ACL of newly created project"); final Team team = getObjectByUuid(Team.class, apiTeam.get().getUuid()); project.addAccessTeam(team); persist(project); return true; } else { LOGGER.warn("API Key without a Team, unable to assign team ACL to project."); } } return false; } @Override public boolean hasAccessManagementPermission(final UserPrincipal userPrincipal) { for (Permission permission: getEffectivePermissions(userPrincipal)) { if (Permissions.ACCESS_MANAGEMENT.name().equals(permission.getName())) { return true; } } 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); if (orderBy == null) { query.setOrdering("name asc, version desc"); } var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive) .withParent(uuid); if (filter != null) { final String filterString = ".*" + filter.toLowerCase() + ".*"; final Tag tag = getTagByName(filter.trim()); if (tag != null) { filterBuilder = filterBuilder.withFuzzyNameOrExactTag(filterString, tag); } else { filterBuilder = filterBuilder.withFuzzyName(filterString); } } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); query.getFetchPlan().addGroup(Project.FetchGroup.ALL.name()); result = execute(query, params); if (includeMetrics) { // Populate each Project object in the paginated result with transitive related // data to minimize the number of round trips a client needs to make, process, and render. for (Project project : result.getList(Project.class)) { project.setMetrics(getMostRecentProjectMetrics(project)); } } 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); if (orderBy == null) { query.setOrdering("name asc"); } final var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive) .withParent(uuid) .withClassifier(classifier); final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); query.getFetchPlan().addGroup(Project.FetchGroup.ALL.name()); result = execute(query, params); if (includeMetrics) { // Populate each Project object in the paginated result with transitive related // data to minimize the number of round trips a client needs to make, process, and render. for (Project project : result.getList(Project.class)) { project.setMetrics(getMostRecentProjectMetrics(project)); } } 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); if (orderBy == null) { query.setOrdering("name asc"); } var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive) .withParent(uuid) .withTag(tag); if (filter != null) { final String filterString = ".*" + filter.toLowerCase() + ".*"; filterBuilder = filterBuilder.withFuzzyName(filterString); } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); result = execute(query, params); if (includeMetrics) { // Populate each Project object in the paginated result with transitive related // data to minimize the number of round trips a client needs to make, process, and render. for (Project project : result.getList(Project.class)) { project.setMetrics(getMostRecentProjectMetrics(project)); } } return result; } @Override public PaginatedResult getProjectsWithoutDescendantsOf(final boolean exludeInactive, final Project project){ final PaginatedResult result; final Query query = pm.newQuery(Project.class); if (orderBy == null) { query.setOrdering("name asc, version desc"); } var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(exludeInactive); if (filter != null) { final String filterString = ".*" + filter.toLowerCase() + ".*"; final Tag tag = getTagByName(filter.trim()); if (tag != null) { filterBuilder = filterBuilder.withFuzzyNameOrExactTag(filterString, tag); } else { filterBuilder = filterBuilder.withFuzzyName(filterString); } } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); result = execute(query, params); result.setObjects(result.getList(Project.class).stream().filter(p -> !isChildOf(p, project.getUuid()) && !p.getUuid().equals(project.getUuid())).toList()); result.setTotal(result.getObjects().size()); return result; } @Override public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final boolean excludeInactive, Project project){ final PaginatedResult result; final Query query = pm.newQuery(Project.class); if (orderBy == null) { query.setOrdering("name asc, version desc"); } var filterBuilder = new ProjectQueryFilterBuilder() .excludeInactive(excludeInactive) .withName(name); if (filter != null) { final String filterString = ".*" + filter.toLowerCase() + ".*"; final Tag tag = getTagByName(filter.trim()); if (tag != null) { filterBuilder = filterBuilder.withFuzzyNameOrExactTag(filterString, tag); } else { filterBuilder = filterBuilder.withFuzzyName(filterString); } } final String queryFilter = filterBuilder.buildFilter(); final Map params = filterBuilder.getParams(); preprocessACLs(query, queryFilter, params, false); result = execute(query, params); result.setObjects(result.getList(Project.class).stream().filter(p -> !isChildOf(p, project.getUuid()) && !p.getUuid().equals(project.getUuid())).toList()); result.setTotal(result.getObjects().size()); return result; } /** * Check whether a {@link Project} with a given {@code name} and {@code version} exists. * * @param name Name of the {@link Project} to check for * @param version Version of the {@link Project} to check for * @return {@code true} when a matching {@link Project} exists, otherwise {@code false} * @since 4.9.0 */ @Override public boolean doesProjectExist(final String name, final String version) { final Query query = pm.newQuery(Project.class); if (version != null) { query.setFilter("name == :name && version == :version"); query.setNamedParameters(Map.of( "name", name, "version", version )); } else { // Version is optional for projects, but using null // for parameter values bypasses the query compilation cache. // https://github.com/DependencyTrack/dependency-track/issues/2540 query.setFilter("name == :name && version == null"); query.setNamedParameters(Map.of( "name", name )); } query.setResult("count(this)"); try { return query.executeResultUnique(Long.class) > 0; } finally { query.closeAll(); } } private static boolean isChildOf(Project project, UUID uuid) { boolean isChild = false; if (project.getParent() != null){ if (project.getParent().getUuid().equals(uuid)){ return true; } else { isChild = isChildOf(project.getParent(), uuid); } } return isChild; } private static boolean hasActiveChild(Project project) { boolean hasActiveChild = false; if (project.getChildren() != null){ for (Project child: project.getChildren()) { if (Boolean.TRUE.equals(child.isActive()) || hasActiveChild) { return true; } else { hasActiveChild = hasActiveChild(child); } } } 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); } }