Skip to content

Commit eae1325

Browse files
authored
Update FK constraints to enable cascading deletes (#1046)
1 parent 6c597cb commit eae1325

21 files changed

+991
-497
lines changed

src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import alpine.resources.AlpineRequest;
2525
import com.github.packageurl.MalformedPackageURLException;
2626
import com.github.packageurl.PackageURL;
27+
import jakarta.json.Json;
28+
import jakarta.json.JsonArray;
29+
import jakarta.json.JsonValue;
2730
import org.apache.commons.lang3.tuple.Pair;
2831
import org.dependencytrack.model.Component;
2932
import org.dependencytrack.model.ComponentIdentity;
@@ -35,12 +38,8 @@
3538
import org.dependencytrack.resources.v1.vo.DependencyGraphResponse;
3639
import org.dependencytrack.tasks.IntegrityMetaInitializerTask;
3740

38-
import jakarta.json.Json;
39-
import jakarta.json.JsonArray;
40-
import jakarta.json.JsonValue;
4141
import javax.jdo.PersistenceManager;
4242
import javax.jdo.Query;
43-
import javax.jdo.Transaction;
4443
import java.io.StringReader;
4544
import java.util.ArrayList;
4645
import java.util.HashMap;
@@ -609,55 +608,6 @@ protected void deleteComponents(Project project) {
609608
}
610609
}
611610

612-
/**
613-
* Deletes a Component and all objects dependant on the component.
614-
*
615-
* @param component the Component to delete
616-
* @param commitIndex specifies if the search index should be committed (an expensive operation)
617-
*/
618-
public void recursivelyDelete(Component component, boolean commitIndex) {
619-
final Transaction trx = pm.currentTransaction();
620-
final boolean isJoiningExistingTrx = trx.isActive();
621-
try {
622-
if (!isJoiningExistingTrx) {
623-
trx.begin();
624-
}
625-
626-
for (final Component child : component.getChildren()) {
627-
recursivelyDelete(child, false);
628-
pm.flush();
629-
}
630-
631-
// Use bulk DELETE queries to avoid having to fetch every single object from the database first.
632-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.AnalysisComment WHERE analysis.component == :component"), component);
633-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Analysis WHERE component == :component"), component);
634-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysisComment WHERE violationAnalysis.component == :component"), component);
635-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysis WHERE component == :component"), component);
636-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.DependencyMetrics WHERE component == :component"), component);
637-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.FindingAttribution WHERE component == :component"), component);
638-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.PolicyViolation WHERE component == :component"), component);
639-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.IntegrityAnalysis WHERE component == :component"), component);
640-
641-
642-
// The component itself must be deleted via deletePersistentAll, otherwise relationships
643-
// (e.g. with Vulnerability via COMPONENTS_VULNERABILITIES table) will not be cleaned up properly.
644-
final Query<Component> componentQuery = pm.newQuery(Component.class, "this == :component");
645-
try {
646-
componentQuery.deletePersistentAll(component);
647-
} finally {
648-
componentQuery.closeAll();
649-
}
650-
651-
if (!isJoiningExistingTrx) {
652-
trx.commit();
653-
}
654-
} finally {
655-
if (!isJoiningExistingTrx && trx.isActive()) {
656-
trx.rollback();
657-
}
658-
}
659-
}
660-
661611
/**
662612
* Returns a list of components by matching its identity information.
663613
*

src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -818,54 +818,6 @@ private static Set<UUID> parseDirectDependenciesUuids(
818818
return uuids;
819819
}
820820

821-
/**
822-
* Deletes a Project and all objects dependent on the project.
823-
*
824-
* @param project the Project to delete
825-
* @param commitIndex specifies if the search index should be committed (an expensive operation)
826-
*/
827-
@Override
828-
public void recursivelyDelete(final Project project, final boolean commitIndex) {
829-
runInTransaction(() -> {
830-
for (final Project child : project.getChildren()) {
831-
// Note: This could be refactored such that each project is deleted
832-
// in its own transaction. That would break semantics when it comes
833-
// to joining an existing transaction though, so needs a bit more thought.
834-
recursivelyDelete(child, false);
835-
pm.flush();
836-
}
837-
838-
// Use bulk DELETE queries to avoid having to fetch every single object from the database first.
839-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.AnalysisComment WHERE analysis.project == :project"), project);
840-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Analysis WHERE project == :project"), project);
841-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysisComment WHERE violationAnalysis.project == :project"), project);
842-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ViolationAnalysis WHERE project == :project"), project);
843-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.DependencyMetrics WHERE project == :project"), project);
844-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectMetrics WHERE project == :project"), project);
845-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.FindingAttribution WHERE project == :project"), project);
846-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.PolicyViolation WHERE project == :project"), project);
847-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.IntegrityAnalysis WHERE component.project == :project"), project);
848-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Bom WHERE project == :project"), project);
849-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.Vex WHERE project == :project"), project);
850-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectMetadata WHERE project == :project"), project);
851-
executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.ProjectProperty WHERE project == :project"), project);
852-
853-
// Projects, Components, and ServiceComponents must be deleted via deletePersistentAll, otherwise relationships
854-
// (e.g. with Vulnerability via COMPONENTS_VULNERABILITIES table) will not be cleaned up properly.
855-
deleteComponents(project);
856-
deleteServiceComponents(project);
857-
removeProjectFromNotificationRules(project);
858-
removeProjectFromPolicies(project);
859-
860-
final Query<Project> projectQuery = pm.newQuery(Project.class, "this == :project");
861-
try {
862-
projectQuery.deletePersistentAll(project);
863-
} finally {
864-
projectQuery.closeAll();
865-
}
866-
});
867-
}
868-
869821
/**
870822
* Creates a key/value pair (ProjectProperty) for the specified Project.
871823
*

src/main/java/org/dependencytrack/persistence/QueryManager.java

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import alpine.persistence.NotSortableException;
3333
import alpine.persistence.OrderDirection;
3434
import alpine.persistence.PaginatedResult;
35-
import alpine.persistence.ScopedCustomization;
3635
import alpine.resources.AlpineRequest;
3736
import alpine.server.util.DbUtil;
3837
import com.github.packageurl.PackageURL;
@@ -128,7 +127,6 @@
128127
import java.util.concurrent.Callable;
129128
import java.util.function.Predicate;
130129

131-
import static org.datanucleus.PropertyNames.PROPERTY_QUERY_SQL_ALLOWALL;
132130
import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED;
133131
import static org.dependencytrack.proto.vulnanalysis.v1.ScanStatus.SCAN_STATUS_FAILED;
134132

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

656-
public void recursivelyDelete(final Project project, final boolean commitIndex) {
657-
getProjectQueryManager().recursivelyDelete(project, commitIndex);
658-
}
659-
660654
public ProjectProperty createProjectProperty(final Project project, final String groupName, final String propertyName,
661655
final String propertyValue, final ProjectProperty.PropertyType propertyType,
662656
final String description) {
@@ -743,10 +737,6 @@ void deleteComponents(Project project) {
743737
getComponentQueryManager().deleteComponents(project);
744738
}
745739

746-
public void recursivelyDelete(Component component, boolean commitIndex) {
747-
getComponentQueryManager().recursivelyDelete(component, commitIndex);
748-
}
749-
750740
public Map<String, Component> getDependencyGraphForComponents(Project project, List<Component> components) {
751741
return getComponentQueryManager().getDependencyGraphForComponents(project, components);
752742
}
@@ -1015,10 +1005,6 @@ public void deleteAffectedVersionAttribution(final Vulnerability vulnerability,
10151005
getVulnerabilityQueryManager().deleteAffectedVersionAttribution(vulnerability, vulnerableSoftware, source);
10161006
}
10171007

1018-
public void deleteAffectedVersionAttributions(final Vulnerability vulnerability) {
1019-
getVulnerabilityQueryManager().deleteAffectedVersionAttributions(vulnerability);
1020-
}
1021-
10221008
public boolean contains(Vulnerability vulnerability, Component component) {
10231009
return getVulnerabilityQueryManager().contains(vulnerability, component);
10241010
}
@@ -1119,14 +1105,6 @@ public ServiceComponent updateServiceComponent(ServiceComponent transientService
11191105
return getServiceComponentQueryManager().updateServiceComponent(transientServiceComponent, commitIndex);
11201106
}
11211107

1122-
public void deleteServiceComponents(final Project project) {
1123-
getServiceComponentQueryManager().deleteServiceComponents(project);
1124-
}
1125-
1126-
public void recursivelyDelete(ServiceComponent service, boolean commitIndex) {
1127-
getServiceComponentQueryManager().recursivelyDelete(service, commitIndex);
1128-
}
1129-
11301108
public PaginatedResult getVulnerabilities() {
11311109
return getVulnerabilityQueryManager().getVulnerabilities();
11321110
}
@@ -1618,20 +1596,6 @@ public <T> T runInRetryableTransaction(final Callable<T> supplier, final Predica
16181596
.executeSupplier(() -> callInTransaction(supplier));
16191597
}
16201598

1621-
public void recursivelyDeleteTeam(Team team) {
1622-
runInTransaction(() -> {
1623-
pm.deletePersistentAll(team.getApiKeys());
1624-
1625-
try (var ignored = new ScopedCustomization(pm).withProperty(PROPERTY_QUERY_SQL_ALLOWALL, "true")) {
1626-
final Query<?> aclDeleteQuery = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, """
1627-
DELETE FROM "PROJECT_ACCESS_TEAMS" WHERE "PROJECT_ACCESS_TEAMS"."TEAM_ID" = ?""");
1628-
executeAndCloseWithArray(aclDeleteQuery, team.getId());
1629-
}
1630-
1631-
pm.deletePersistent(team);
1632-
});
1633-
}
1634-
16351599
/**
16361600
* Returns a list of all {@link DependencyGraphResponse} objects by {@link Component} UUID.
16371601
*

src/main/java/org/dependencytrack/persistence/ServiceComponentQueryManager.java

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
import javax.jdo.PersistenceManager;
2929
import javax.jdo.Query;
30-
import javax.jdo.Transaction;
3130
import java.util.List;
3231
import java.util.UUID;
3332

@@ -194,63 +193,6 @@ public ServiceComponent updateServiceComponent(ServiceComponent transientService
194193
return result;
195194
}
196195

197-
/**
198-
* Deletes all services for the specified Project.
199-
* @param project the Project to delete services of
200-
*/
201-
public void deleteServiceComponents(Project project) {
202-
final Query<ServiceComponent> query = pm.newQuery(ServiceComponent.class, "project == :project");
203-
try {
204-
query.deletePersistentAll(project);
205-
} finally {
206-
query.closeAll();
207-
}
208-
}
209-
210-
/**
211-
* Deletes a ServiceComponent and all objects dependant on the service.
212-
* @param service the ServiceComponent to delete
213-
* @param commitIndex specifies if the search index should be committed (an expensive operation)
214-
*/
215-
public void recursivelyDelete(ServiceComponent service, boolean commitIndex) {
216-
final Transaction trx = pm.currentTransaction();
217-
final boolean isJoiningExistingTrx = trx.isActive();
218-
try {
219-
if (!isJoiningExistingTrx) {
220-
trx.begin();
221-
}
222-
223-
for (final ServiceComponent child : service.getChildren()) {
224-
recursivelyDelete(child, false);
225-
pm.flush();
226-
}
227-
228-
// TODO: Add these in when these features are supported by service components
229-
//deleteAnalysisTrail(service);
230-
//deleteViolationAnalysisTrail(service);
231-
//deleteMetrics(service);
232-
//deleteFindingAttributions(service);
233-
//deletePolicyViolations(service);
234-
235-
final Query<ServiceComponent> query = pm.newQuery(ServiceComponent.class);
236-
query.setFilter("this == :service");
237-
try {
238-
query.deletePersistentAll(service);
239-
} finally {
240-
query.closeAll();
241-
}
242-
243-
if (!isJoiningExistingTrx) {
244-
trx.commit();
245-
}
246-
247-
} finally {
248-
if (!isJoiningExistingTrx && trx.isActive()) {
249-
trx.rollback();
250-
}
251-
}
252-
}
253-
254196
/**
255197
* Returns a list of all {@link DependencyGraphResponse} objects by {@link ServiceComponent} UUID.
256198
* @param uuids a list of {@link ServiceComponent} UUIDs

src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,20 +1013,6 @@ public void deleteAffectedVersionAttribution(final Vulnerability vulnerability,
10131013
query.deletePersistentAll();
10141014
}
10151015

1016-
/**
1017-
* Delete all {@link AffectedVersionAttribution}s associated with a given {@link Vulnerability}.
1018-
*
1019-
* @param vulnerability The {@link Vulnerability} to delete {@link AffectedVersionAttribution}s for
1020-
* @since 4.7.0
1021-
*/
1022-
@Override
1023-
public void deleteAffectedVersionAttributions(final Vulnerability vulnerability) {
1024-
final Query<AffectedVersionAttribution> query = pm.newQuery(AffectedVersionAttribution.class);
1025-
query.setFilter("vulnerability == :vulnerability");
1026-
query.setParameters(vulnerability);
1027-
query.deletePersistentAll();
1028-
}
1029-
10301016
/**
10311017
* Binds the two objects together in a corresponding join table.
10321018
*
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* This file is part of Dependency-Track.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
package org.dependencytrack.persistence.jdbi;
20+
21+
import org.jdbi.v3.sqlobject.customizer.Bind;
22+
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
23+
24+
import java.util.UUID;
25+
26+
public interface ComponentDao {
27+
28+
@SqlUpdate("""
29+
DELETE
30+
FROM "COMPONENT"
31+
WHERE "UUID" = :componentUuid
32+
""")
33+
int deleteComponent(@Bind final UUID componentUuid);
34+
}

src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ private static Jdbi createJdbi(final PersistenceManager pm) {
153153
* @throws IllegalStateException When the given {@link QueryManager} is not participating
154154
* in an active {@link javax.jdo.Transaction}
155155
*/
156-
static Jdbi createLocalJdbi(final QueryManager qm) {
156+
public static Jdbi createLocalJdbi(final QueryManager qm) {
157157
return createLocalJdbi(qm.getPersistenceManager());
158158
}
159159

src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.jdbi.v3.sqlobject.customizer.Define;
2727
import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings;
2828
import org.jdbi.v3.sqlobject.statement.SqlQuery;
29+
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
2930

3031
import java.time.Instant;
3132
import java.util.List;
@@ -220,4 +221,10 @@ record ConciseProjectMetricsRow(
220221
) {
221222
}
222223

224+
@SqlUpdate("""
225+
DELETE
226+
FROM "PROJECT"
227+
WHERE "UUID" = :projectUuid
228+
""")
229+
int deleteProject(@Bind final UUID projectUuid);
223230
}

0 commit comments

Comments
 (0)