Skip to content

Commit

Permalink
Update FK constraints to enable cascading deletes (#1046)
Browse files Browse the repository at this point in the history
  • Loading branch information
sahibamittal authored Jan 31, 2025
1 parent 6c597cb commit eae1325
Show file tree
Hide file tree
Showing 21 changed files with 991 additions and 497 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import alpine.resources.AlpineRequest;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonValue;
import org.apache.commons.lang3.tuple.Pair;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
Expand All @@ -35,12 +38,8 @@
import org.dependencytrack.resources.v1.vo.DependencyGraphResponse;
import org.dependencytrack.tasks.IntegrityMetaInitializerTask;

import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonValue;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -609,55 +608,6 @@ protected void deleteComponents(Project project) {
}
}

/**
* Deletes a Component and all objects dependant on the component.
*
* @param component the Component to delete
* @param commitIndex specifies if the search index should be committed (an expensive operation)
*/
public void recursivelyDelete(Component component, boolean commitIndex) {
final Transaction trx = pm.currentTransaction();
final boolean isJoiningExistingTrx = trx.isActive();
try {
if (!isJoiningExistingTrx) {
trx.begin();
}

for (final Component child : component.getChildren()) {
recursivelyDelete(child, false);
pm.flush();
}

// Use bulk DELETE queries to avoid having to fetch every single object from the database first.
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.AnalysisComment WHERE analysis.component == :component"), component);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Analysis WHERE component == :component"), component);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysisComment WHERE violationAnalysis.component == :component"), component);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysis WHERE component == :component"), component);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.DependencyMetrics WHERE component == :component"), component);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.FindingAttribution WHERE component == :component"), component);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.PolicyViolation WHERE component == :component"), component);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.IntegrityAnalysis WHERE component == :component"), component);


// The component itself must be deleted via deletePersistentAll, otherwise relationships
// (e.g. with Vulnerability via COMPONENTS_VULNERABILITIES table) will not be cleaned up properly.
final Query<Component> componentQuery = pm.newQuery(Component.class, "this == :component");
try {
componentQuery.deletePersistentAll(component);
} finally {
componentQuery.closeAll();
}

if (!isJoiningExistingTrx) {
trx.commit();
}
} finally {
if (!isJoiningExistingTrx && trx.isActive()) {
trx.rollback();
}
}
}

/**
* Returns a list of components by matching its identity information.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -818,54 +818,6 @@ private static Set<UUID> parseDirectDependenciesUuids(
return uuids;
}

/**
* Deletes a Project and all objects dependent 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) {
runInTransaction(() -> {
for (final Project child : project.getChildren()) {
// Note: This could be refactored such that each project is deleted
// in its own transaction. That would break semantics when it comes
// to joining an existing transaction though, so needs a bit more thought.
recursivelyDelete(child, false);
pm.flush();
}

// Use bulk DELETE queries to avoid having to fetch every single object from the database first.
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.AnalysisComment WHERE analysis.project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Analysis WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysisComment WHERE violationAnalysis.project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysis WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.DependencyMetrics WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectMetrics WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.FindingAttribution WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.PolicyViolation WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.IntegrityAnalysis WHERE component.project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Bom WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Vex WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectMetadata WHERE project == :project"), project);
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectProperty WHERE project == :project"), project);

// Projects, Components, and ServiceComponents must be deleted via deletePersistentAll, otherwise relationships
// (e.g. with Vulnerability via COMPONENTS_VULNERABILITIES table) will not be cleaned up properly.
deleteComponents(project);
deleteServiceComponents(project);
removeProjectFromNotificationRules(project);
removeProjectFromPolicies(project);

final Query<Project> projectQuery = pm.newQuery(Project.class, "this == :project");
try {
projectQuery.deletePersistentAll(project);
} finally {
projectQuery.closeAll();
}
});
}

/**
* Creates a key/value pair (ProjectProperty) for the specified Project.
*
Expand Down
36 changes: 0 additions & 36 deletions src/main/java/org/dependencytrack/persistence/QueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import alpine.persistence.NotSortableException;
import alpine.persistence.OrderDirection;
import alpine.persistence.PaginatedResult;
import alpine.persistence.ScopedCustomization;
import alpine.resources.AlpineRequest;
import alpine.server.util.DbUtil;
import com.github.packageurl.PackageURL;
Expand Down Expand Up @@ -128,7 +127,6 @@
import java.util.concurrent.Callable;
import java.util.function.Predicate;

import static org.datanucleus.PropertyNames.PROPERTY_QUERY_SQL_ALLOWALL;
import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED;
import static org.dependencytrack.proto.vulnanalysis.v1.ScanStatus.SCAN_STATUS_FAILED;

Expand Down Expand Up @@ -653,10 +651,6 @@ public Project updateLastBomImport(Project p, Date date, String bomFormat) {
return getProjectQueryManager().updateLastBomImport(p, date, bomFormat);
}

public void recursivelyDelete(final Project project, final boolean commitIndex) {
getProjectQueryManager().recursivelyDelete(project, commitIndex);
}

public ProjectProperty createProjectProperty(final Project project, final String groupName, final String propertyName,
final String propertyValue, final ProjectProperty.PropertyType propertyType,
final String description) {
Expand Down Expand Up @@ -743,10 +737,6 @@ void deleteComponents(Project project) {
getComponentQueryManager().deleteComponents(project);
}

public void recursivelyDelete(Component component, boolean commitIndex) {
getComponentQueryManager().recursivelyDelete(component, commitIndex);
}

public Map<String, Component> getDependencyGraphForComponents(Project project, List<Component> components) {
return getComponentQueryManager().getDependencyGraphForComponents(project, components);
}
Expand Down Expand Up @@ -1015,10 +1005,6 @@ public void deleteAffectedVersionAttribution(final Vulnerability vulnerability,
getVulnerabilityQueryManager().deleteAffectedVersionAttribution(vulnerability, vulnerableSoftware, source);
}

public void deleteAffectedVersionAttributions(final Vulnerability vulnerability) {
getVulnerabilityQueryManager().deleteAffectedVersionAttributions(vulnerability);
}

public boolean contains(Vulnerability vulnerability, Component component) {
return getVulnerabilityQueryManager().contains(vulnerability, component);
}
Expand Down Expand Up @@ -1119,14 +1105,6 @@ public ServiceComponent updateServiceComponent(ServiceComponent transientService
return getServiceComponentQueryManager().updateServiceComponent(transientServiceComponent, commitIndex);
}

public void deleteServiceComponents(final Project project) {
getServiceComponentQueryManager().deleteServiceComponents(project);
}

public void recursivelyDelete(ServiceComponent service, boolean commitIndex) {
getServiceComponentQueryManager().recursivelyDelete(service, commitIndex);
}

public PaginatedResult getVulnerabilities() {
return getVulnerabilityQueryManager().getVulnerabilities();
}
Expand Down Expand Up @@ -1618,20 +1596,6 @@ public <T> T runInRetryableTransaction(final Callable<T> supplier, final Predica
.executeSupplier(() -> callInTransaction(supplier));
}

public void recursivelyDeleteTeam(Team team) {
runInTransaction(() -> {
pm.deletePersistentAll(team.getApiKeys());

try (var ignored = new ScopedCustomization(pm).withProperty(PROPERTY_QUERY_SQL_ALLOWALL, "true")) {
final Query<?> aclDeleteQuery = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, """
DELETE FROM "PROJECT_ACCESS_TEAMS" WHERE "PROJECT_ACCESS_TEAMS"."TEAM_ID" = ?""");
executeAndCloseWithArray(aclDeleteQuery, team.getId());
}

pm.deletePersistent(team);
});
}

/**
* Returns a list of all {@link DependencyGraphResponse} objects by {@link Component} UUID.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import java.util.List;
import java.util.UUID;

Expand Down Expand Up @@ -194,63 +193,6 @@ public ServiceComponent updateServiceComponent(ServiceComponent transientService
return result;
}

/**
* Deletes all services for the specified Project.
* @param project the Project to delete services of
*/
public void deleteServiceComponents(Project project) {
final Query<ServiceComponent> query = pm.newQuery(ServiceComponent.class, "project == :project");
try {
query.deletePersistentAll(project);
} finally {
query.closeAll();
}
}

/**
* Deletes a ServiceComponent and all objects dependant on the service.
* @param service the ServiceComponent to delete
* @param commitIndex specifies if the search index should be committed (an expensive operation)
*/
public void recursivelyDelete(ServiceComponent service, boolean commitIndex) {
final Transaction trx = pm.currentTransaction();
final boolean isJoiningExistingTrx = trx.isActive();
try {
if (!isJoiningExistingTrx) {
trx.begin();
}

for (final ServiceComponent child : service.getChildren()) {
recursivelyDelete(child, false);
pm.flush();
}

// TODO: Add these in when these features are supported by service components
//deleteAnalysisTrail(service);
//deleteViolationAnalysisTrail(service);
//deleteMetrics(service);
//deleteFindingAttributions(service);
//deletePolicyViolations(service);

final Query<ServiceComponent> query = pm.newQuery(ServiceComponent.class);
query.setFilter("this == :service");
try {
query.deletePersistentAll(service);
} finally {
query.closeAll();
}

if (!isJoiningExistingTrx) {
trx.commit();
}

} finally {
if (!isJoiningExistingTrx && trx.isActive()) {
trx.rollback();
}
}
}

/**
* Returns a list of all {@link DependencyGraphResponse} objects by {@link ServiceComponent} UUID.
* @param uuids a list of {@link ServiceComponent} UUIDs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1013,20 +1013,6 @@ public void deleteAffectedVersionAttribution(final Vulnerability vulnerability,
query.deletePersistentAll();
}

/**
* Delete all {@link AffectedVersionAttribution}s associated with a given {@link Vulnerability}.
*
* @param vulnerability The {@link Vulnerability} to delete {@link AffectedVersionAttribution}s for
* @since 4.7.0
*/
@Override
public void deleteAffectedVersionAttributions(final Vulnerability vulnerability) {
final Query<AffectedVersionAttribution> query = pm.newQuery(AffectedVersionAttribution.class);
query.setFilter("vulnerability == :vulnerability");
query.setParameters(vulnerability);
query.deletePersistentAll();
}

/**
* Binds the two objects together in a corresponding join table.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.persistence.jdbi;

import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;

import java.util.UUID;

public interface ComponentDao {

@SqlUpdate("""
DELETE
FROM "COMPONENT"
WHERE "UUID" = :componentUuid
""")
int deleteComponent(@Bind final UUID componentUuid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private static Jdbi createJdbi(final PersistenceManager pm) {
* @throws IllegalStateException When the given {@link QueryManager} is not participating
* in an active {@link javax.jdo.Transaction}
*/
static Jdbi createLocalJdbi(final QueryManager qm) {
public static Jdbi createLocalJdbi(final QueryManager qm) {
return createLocalJdbi(qm.getPersistenceManager());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.jdbi.v3.sqlobject.customizer.Define;
import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;

import java.time.Instant;
import java.util.List;
Expand Down Expand Up @@ -220,4 +221,10 @@ record ConciseProjectMetricsRow(
) {
}

@SqlUpdate("""
DELETE
FROM "PROJECT"
WHERE "UUID" = :projectUuid
""")
int deleteProject(@Bind final UUID projectUuid);
}
Loading

0 comments on commit eae1325

Please sign in to comment.