From 3ce6c5a1d243fe59c440c0a0527581abee55554b Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 25 Sep 2024 13:23:12 +0200 Subject: [PATCH] Ensure no unique constraint violation for `ProjectMetadata` Ports https://github.com/DependencyTrack/dependency-track/pull/3982 from Dependency-Track v4.12.0 Signed-off-by: nscuro --- .../org/dependencytrack/common/MdcKeys.java | 1 + .../persistence/ProjectQueryManager.java | 250 +++++++++--------- .../tasks/CloneProjectTask.java | 36 ++- .../tasks/BomUploadProcessingTaskTest.java | 78 ++++++ .../tasks/CloneProjectTaskTest.java | 4 +- 5 files changed, 236 insertions(+), 133 deletions(-) diff --git a/src/main/java/org/dependencytrack/common/MdcKeys.java b/src/main/java/org/dependencytrack/common/MdcKeys.java index d9c300f8c..ff88c6531 100644 --- a/src/main/java/org/dependencytrack/common/MdcKeys.java +++ b/src/main/java/org/dependencytrack/common/MdcKeys.java @@ -29,6 +29,7 @@ public final class MdcKeys { public static final String MDC_BOM_UPLOAD_TOKEN = "bomUploadToken"; public static final String MDC_BOM_VERSION = "bomVersion"; public static final String MDC_COMPONENT_UUID = "componentUuid"; + public static final String MDC_EVENT_TOKEN = "eventToken"; public static final String MDC_EXTENSION = "extension"; public static final String MDC_EXTENSION_NAME = "extensionName"; public static final String MDC_EXTENSION_POINT = "extensionPoint"; diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index dd68d3c11..826d9f4a3 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -512,147 +512,157 @@ public Project updateProject(Project transientProject, boolean commitIndex) { } @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) { - throw new IllegalStateException("Project with UUID %s was supposed to be cloned, but it does not exist anymore".formatted(from)); - } - 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. - throw new IllegalStateException("Project %s was supposed to be cloned to version %s, but that version already exists" - .formatted(source, newVersion)); - } - Project project = new Project(); - project.setAuthors(source.getAuthors()); - 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); - } + public Project clone( + final UUID from, + final String newVersion, + final boolean includeTags, + final boolean includeProperties, + final boolean includeComponents, + final boolean includeServices, + final boolean includeAuditHistory, + final boolean includeACL, + final boolean includePolicyViolations + ) { + return callInTransaction(() -> { + final Project source = getObjectByUuid(Project.class, from, Project.FetchGroup.ALL.name()); + if (source == null) { + throw new IllegalStateException("Project was supposed to be cloned, but it does not exist anymore"); + } + 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. + throw new IllegalStateException(""" + Project was supposed to be cloned to version %s, \ + but that version already exists""".formatted(newVersion)); + } + Project project = new Project(); + project.setAuthors(source.getAuthors()); + 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 (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); + 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(), sourceAttribution.getAttributedOn()); + 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(), sourceAttribution.getAttributedOn()); + } + clonedComponents.put(sourceComponent.getId(), clonedComponent); } - 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 (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.setVulnerabilityPolicyId(sourceAnalysis.getVulnerabilityPolicyId()); - 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 (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.setVulnerabilityPolicyId(sourceAnalysis.getVulnerabilityPolicyId()); + 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 (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); - } + 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()); - return project; + return project; + }); } /** diff --git a/src/main/java/org/dependencytrack/tasks/CloneProjectTask.java b/src/main/java/org/dependencytrack/tasks/CloneProjectTask.java index ea42181ad..d6ee19e6a 100644 --- a/src/main/java/org/dependencytrack/tasks/CloneProjectTask.java +++ b/src/main/java/org/dependencytrack/tasks/CloneProjectTask.java @@ -26,10 +26,13 @@ import org.dependencytrack.model.WorkflowState; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.CloneProjectRequest; +import org.slf4j.MDC; import java.util.Date; import java.util.UUID; +import static org.dependencytrack.common.MdcKeys.MDC_EVENT_TOKEN; +import static org.dependencytrack.common.MdcKeys.MDC_PROJECT_UUID; import static org.dependencytrack.model.WorkflowStatus.PENDING; import static org.dependencytrack.model.WorkflowStep.PROJECT_CLONE; @@ -41,11 +44,12 @@ public class CloneProjectTask implements Subscriber { * {@inheritDoc} */ public void inform(final Event e) { - if (e instanceof CloneProjectEvent) { - final CloneProjectEvent event = (CloneProjectEvent)e; + if (e instanceof final CloneProjectEvent event) { final CloneProjectRequest request = event.getRequest(); - final UUID chainIdentifier = ((CloneProjectEvent) e).getChainIdentifier(); - try (QueryManager qm = new QueryManager()) { + final UUID chainIdentifier = event.getChainIdentifier(); + try (var ignoredMdcProjectUuid = MDC.putCloseable(MDC_PROJECT_UUID, request.getProject()); + var ignoredMdcEventToken = MDC.putCloseable(MDC_EVENT_TOKEN, event.getChainIdentifier().toString()); + final var qm = new QueryManager()) { WorkflowState workflowState = qm.updateStartTimeIfWorkflowStateExists(chainIdentifier, PROJECT_CLONE); if (workflowState == null) { final var now = new Date(); @@ -55,17 +59,27 @@ public void inform(final Event e) { workflowState.setToken(chainIdentifier); workflowState.setStartedAt(now); workflowState.setUpdatedAt(now); - qm.getPersistenceManager().makePersistent(workflowState); + qm.persist(workflowState); } + try { - LOGGER.info("Cloning project: " + request.getProject()); - final Project project = qm.clone(UUID.fromString(request.getProject()), - request.getVersion(), request.includeTags(), request.includeProperties(), - request.includeComponents(), request.includeServices(), request.includeAuditHistory(), request.includeACL(), request.includePolicyViolations()); + LOGGER.info("Cloning project for version %s".formatted(request.getVersion())); + final Project project = qm.clone( + UUID.fromString(request.getProject()), + request.getVersion(), + request.includeTags(), + request.includeProperties(), + request.includeComponents(), + request.includeServices(), + request.includeAuditHistory(), + request.includeACL(), + request.includePolicyViolations() + ); + qm.updateWorkflowStateToComplete(workflowState); - LOGGER.info("Cloned project: " + request.getProject() + " to " + project.getUuid()); + LOGGER.info("Cloned project for version %s into project %s".formatted(project.getVersion(), project.getUuid())); } catch (Exception ex) { - LOGGER.error("An error occurred while cloning project %s".formatted(request.getProject()), ex); + LOGGER.error("Failed to clone project", ex); qm.updateWorkflowStateToFailed(workflowState, ex.getMessage()); } } diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index 5813aead0..b02edd317 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -1455,6 +1455,84 @@ public void informIssue3957Test() throws Exception { }); } + @Test + public void informIssue3981Test() throws Exception { + final var project = new Project(); + project.setName("acme-license-app"); + project.setVersion("1.2.3"); + qm.persist(project); + + byte[] bomBytes = """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b80", + "version": 1, + "metadata": { + "authors": [ + { + "name": "foo", + "email": "foo@example.com" + } + ] + }, + "components": [ + { + "type": "library", + "name": "acme-lib-x" + } + ] + } + """.getBytes(StandardCharsets.UTF_8); + + var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), createTempBomFile(bomBytes)); + qm.createWorkflowSteps(bomUploadEvent.getChainIdentifier()); + new BomUploadProcessingTask().inform(bomUploadEvent); + assertBomProcessedNotification(); + + final Project clonedProject = qm.clone(project.getUuid(), "3.2.1", true, true, true, true, true, true, true); + + bomBytes = """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b80", + "version": 1, + "metadata": { + "authors": [ + { + "name": "bar", + "email": "bar@example.com" + } + ] + }, + "components": [ + { + "type": "library", + "name": "acme-lib-x" + } + ] + } + """.getBytes(StandardCharsets.UTF_8); + + bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, clonedProject.getId()), createTempBomFile(bomBytes)); + qm.createWorkflowSteps(bomUploadEvent.getChainIdentifier()); + new BomUploadProcessingTask().inform(bomUploadEvent); + assertBomProcessedNotification(); + + qm.getPersistenceManager().evictAll(); + + assertThat(project.getMetadata().getAuthors()).satisfiesExactly(author -> { + assertThat(author.getName()).isEqualTo("foo"); + assertThat(author.getEmail()).isEqualTo("foo@example.com"); + }); + + assertThat(clonedProject.getMetadata().getAuthors()).satisfiesExactly(author -> { + assertThat(author.getName()).isEqualTo("bar"); + assertThat(author.getEmail()).isEqualTo("bar@example.com"); + }); + } + @Test public void informIssue3936Test() throws Exception{ diff --git a/src/test/java/org/dependencytrack/tasks/CloneProjectTaskTest.java b/src/test/java/org/dependencytrack/tasks/CloneProjectTaskTest.java index d67340fcc..08f867393 100644 --- a/src/test/java/org/dependencytrack/tasks/CloneProjectTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/CloneProjectTaskTest.java @@ -67,7 +67,7 @@ public void testCloneProjectDoesNotExist() { assertThat(state.getStatus()).isEqualTo(FAILED); assertThat(state.getStartedAt()).isNotNull(); assertThat(state.getUpdatedAt()).isBefore(Date.from(Instant.now())); - assertThat(state.getFailureReason()).contains("Project with UUID " + uuid + " was supposed to be cloned, but it does not exist anymore"); + assertThat(state.getFailureReason()).contains("Project was supposed to be cloned, but it does not exist anymore"); }); } @@ -84,7 +84,7 @@ public void testCloneProjectVersionExist() { assertThat(state.getStatus()).isEqualTo(FAILED); assertThat(state.getStartedAt()).isNotNull(); assertThat(state.getUpdatedAt()).isBefore(Date.from(Instant.now())); - assertThat(state.getFailureReason()).contains("Project Acme Example : 1.0 was supposed to be cloned to version 1.0, but that version already exists"); + assertThat(state.getFailureReason()).contains("Project was supposed to be cloned to version 1.0, but that version already exists"); }); } }