Skip to content

Commit

Permalink
Fix JDOUserException when multiple licenses match a component's lic…
Browse files Browse the repository at this point in the history
…ense name

Fixes #3957

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Jul 9, 2024
1 parent bb7020f commit 3bfbd1d
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import java.util.List;
import java.util.Map;

final class LicenseQueryManager extends QueryManager implements IQueryManager {

Expand Down Expand Up @@ -93,6 +94,20 @@ public License getLicense(String licenseId) {
return singleResult(query.execute(licenseId));
}

/**
* @since 4.12.0
*/
@Override
public License getLicenseByIdOrName(final String licenseIdOrName) {
final Query<License> query = pm.newQuery(License.class);
query.setFilter("licenseId == :licenseIdOrName || name == :licenseIdOrName");
query.setNamedParameters(Map.of("licenseIdOrName", licenseIdOrName));
query.setOrdering("licenseId asc"); // Ensure result is consistent.
query.setRange(0, 1); // Multiple licenses can have the same name; Pick the first one.
final License license = executeAndCloseUnique(query);
return license != null ? license : License.UNRESOLVED;
}

/**
* Returns a Custom License object from the specified name
* @param licenseName license name of custom license
Expand All @@ -105,6 +120,20 @@ public License getCustomLicense(String licenseName) {
return singleResult(query.execute(licenseName));
}

/**
* @since 4.12.0
*/
@Override
public License getCustomLicenseByName(final String licenseName) {
final Query<License> query = pm.newQuery(License.class);
query.setFilter("name == :name && customLicense == true");
query.setParameters(licenseName);
query.setOrdering("licenseId asc"); // Ensure result is consistent.
query.setRange(0, 1); // Multiple licenses can have the same name; Pick the first one.
final License license = executeAndCloseUnique(query);
return license != null ? license : License.UNRESOLVED;
}

/**
* Creates a new License.
* @param license the License object to create
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/org/dependencytrack/persistence/QueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import alpine.server.util.DbUtil;
import com.github.packageurl.PackageURL;
import com.google.common.collect.Lists;
import jakarta.json.JsonObject;
import org.apache.commons.lang3.ClassUtils;
import org.datanucleus.PropertyNames;
import org.datanucleus.api.jdo.JDOQuery;
Expand Down Expand Up @@ -84,7 +85,6 @@
import org.dependencytrack.resources.v1.vo.DependencyGraphResponse;
import org.dependencytrack.tasks.scanners.AnalyzerIdentity;

import jakarta.json.JsonObject;
import javax.jdo.FetchPlan;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
Expand Down Expand Up @@ -629,10 +629,18 @@ public License getLicense(String licenseId) {
return getLicenseQueryManager().getLicense(licenseId);
}

public License getLicenseByIdOrName(final String licenseIdOrName) {
return getLicenseQueryManager().getLicenseByIdOrName(licenseIdOrName);
}

public License getCustomLicense(String licenseName) {
return getLicenseQueryManager().getCustomLicense(licenseName);
}

public License getCustomLicenseByName(final String licenseName) {
return getLicenseQueryManager().getCustomLicenseByName(licenseName);
}

public License synchronizeLicense(License license, boolean commitIndex) {
return getLicenseQueryManager().synchronizeLicense(license, commitIndex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,7 @@ private static void resolveAndApplyLicense(final QueryManager qm,
// by priority, and simply take the first resolvable candidate.
for (final org.cyclonedx.model.License licenseCandidate : component.getLicenseCandidates()) {
if (isNotBlank(licenseCandidate.getId())) {
final License resolvedLicense = licenseCache.computeIfAbsent(licenseCandidate.getId(),
licenseId -> resolveLicense(qm, licenseId));
final License resolvedLicense = licenseCache.computeIfAbsent(licenseCandidate.getId(), qm::getLicenseByIdOrName);
if (resolvedLicense != License.UNRESOLVED) {
component.setResolvedLicense(resolvedLicense);
component.setLicenseUrl(trimToNull(licenseCandidate.getUrl()));
Expand All @@ -673,16 +672,14 @@ private static void resolveAndApplyLicense(final QueryManager qm,
}

if (isNotBlank(licenseCandidate.getName())) {
final License resolvedLicense = licenseCache.computeIfAbsent(licenseCandidate.getName(),
licenseName -> resolveLicense(qm, licenseName));
final License resolvedLicense = licenseCache.computeIfAbsent(licenseCandidate.getName(), qm::getLicenseByIdOrName);
if (resolvedLicense != License.UNRESOLVED) {
component.setResolvedLicense(resolvedLicense);
component.setLicenseUrl(trimToNull(licenseCandidate.getUrl()));
break;
}

final License resolvedCustomLicense = customLicenseCache.computeIfAbsent(licenseCandidate.getName(),
licenseName -> resolveCustomLicense(qm, licenseName));
final License resolvedCustomLicense = customLicenseCache.computeIfAbsent(licenseCandidate.getName(), qm::getCustomLicenseByName);
if (resolvedCustomLicense != License.UNRESOLVED) {
component.setResolvedLicense(resolvedCustomLicense);
component.setLicenseUrl(trimToNull(licenseCandidate.getUrl()));
Expand All @@ -704,18 +701,6 @@ private static void resolveAndApplyLicense(final QueryManager qm,
}
}

private static License resolveLicense(final QueryManager qm, final String licenseIdOrName) {
final Query<License> query = qm.getPersistenceManager().newQuery(License.class);
query.setFilter("licenseId == :licenseIdOrName || name == :licenseIdOrName");
query.setNamedParameters(Map.of("licenseIdOrName", licenseIdOrName));
try {
final License license = query.executeUnique();
return license != null ? license : License.UNRESOLVED;
} finally {
query.closeAll();
}
}

private static License resolveCustomLicense(final QueryManager qm, final String licenseName) {
final Query<License> query = qm.getPersistenceManager().newQuery(License.class);
query.setFilter("name == :name && customLicense == true");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,55 @@ public void informIssue3371Test() throws Exception {
}
}

@Test // https://github.com/DependencyTrack/dependency-track/issues/3957
public void informIssue3957Test() {
final var licenseA = new License();
licenseA.setLicenseId("GPL-1.0");
licenseA.setName("GNU General Public License v1.0 only");
qm.persist(licenseA);

final var licenseB = new License();
licenseB.setLicenseId("GPL-1.0-only");
licenseB.setName("GNU General Public License v1.0 only");
qm.persist(licenseB);

final var project = new Project();
project.setName("acme-license-app");
qm.persist(project);

final byte[] bomBytes = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b80",
"version": 1,
"components": [
{
"type": "library",
"name": "acme-lib-x",
"licenses": [
{
"license": {
"name": "GNU General Public License v1.0 only"
}
}
]
}
]
}
""".getBytes(StandardCharsets.UTF_8);

final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomBytes);
new BomUploadProcessingTask().inform(bomUploadEvent);
awaitBomProcessedNotification(bomUploadEvent);

qm.getPersistenceManager().evictAll();
assertThat(qm.getAllComponents(project)).satisfiesExactly(component -> {
assertThat(component.getResolvedLicense()).isNotNull();
assertThat(component.getResolvedLicense().getLicenseId()).isEqualTo("GPL-1.0");
});
}

private void awaitBomProcessedNotification(final BomUploadEvent bomUploadEvent) {
try {
await("BOM Processed Notification")
Expand Down

0 comments on commit 3bfbd1d

Please sign in to comment.