Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix project.active being nullable #4415

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions dev/docker-compose.mysql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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.
services:
apiserver:
depends_on:
- mysql
environment:
ALPINE_DATABASE_MODE: "external"
ALPINE_DATABASE_URL: "jdbc:mysql://mysql:3306/dtrack?autoReconnect=true&useSSL=false&sessionVariables=sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'"
ALPINE_DATABASE_DRIVER: "com.mysql.cj.jdbc.Driver"
ALPINE_DATABASE_USERNAME: "dtrack"
ALPINE_DATABASE_PASSWORD: "dtrack"

mysql:
image: mysql:5.7
platform: "linux/amd64" # arm64 is not supported
environment:
MYSQL_DATABASE: "dtrack"
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
MYSQL_USER: "dtrack"
MYSQL_PASSWORD: "dtrack"
ports:
- "127.0.0.1:3306:3306"
volumes:
- "mysql-data:/var/lib/mysql"
restart: unless-stopped

volumes:
mysql-data: { }
8 changes: 4 additions & 4 deletions src/main/java/org/dependencytrack/model/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,9 @@ public enum FetchGroup {
private Double lastInheritedRiskScore;

@Persistent
@Column(name = "ACTIVE")
@Column(name = "ACTIVE", defaultValue = "true")
@JsonSerialize(nullsUsing = BooleanDefaultTrueSerializer.class)
private Boolean active; // Added in v3.6. Existing records need to be nullable on upgrade.
private boolean active = true;

@Persistent
@Index(name = "PROJECT_IS_LATEST_IDX")
Expand Down Expand Up @@ -512,11 +512,11 @@ public void setExternalReferences(List<ExternalReference> externalReferences) {
this.externalReferences = externalReferences;
}

public Boolean isActive() {
public boolean isActive() {
return active;
}

public void setActive(Boolean active) {
public void setActive(boolean active) {
this.active = active;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@
* Value object holding UUID and version for a project
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ProjectVersion(UUID uuid, String version, Boolean active) implements Serializable {
public record ProjectVersion(UUID uuid, String version, boolean active) implements Serializable {
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ private void limitToProject(
}

if (isLimitedToTags) {
final Predicate<Project> tagMatchPredicate = project -> (project.isActive() == null || project.isActive())
final Predicate<Project> tagMatchPredicate = project -> project.isActive()
&& project.getTags() != null
&& project.getTags().stream().anyMatch(rule.getTags()::contains);

Expand Down Expand Up @@ -340,7 +340,7 @@ private boolean checkIfChildrenAreAffected(Project parent, UUID uuid) {
return false;
}
for (Project child : parent.getChildren()) {
final boolean isChildActive = child.isActive() == null || child.isActive();
final boolean isChildActive = child.isActive();
if ((child.getUuid().equals(uuid) && isChildActive) || isChild) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ public PaginatedResult getPolicyViolations(boolean includeSuppressed, boolean sh
filterCriteria.add("(analysis.suppressed == false || analysis.suppressed == null)");
}
if (!showInactive) {
filterCriteria.add("(project.active == true || project.active == null)");
filterCriteria.add("project.active");
}
processViolationsFilters(filters, params, filterCriteria);
if (orderBy == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ProjectQueryFilterBuilder {

ProjectQueryFilterBuilder excludeInactive(boolean excludeInactive) {
if (excludeInactive) {
filterCriteria.add("(active == true || active == null)");
filterCriteria.add("active");
}
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public List<Project> getAllProjects() {
public List<Project> getAllProjects(boolean excludeInactive) {
final Query<Project> query = pm.newQuery(Project.class);
if (excludeInactive) {
query.setFilter("active == true || active == null");
query.setFilter("active");
}
query.setOrdering("id asc");
return query.executeList();
Expand Down Expand Up @@ -473,9 +473,6 @@ public Project createProject(final Project project, List<Tag> tags, boolean comm
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 oldLatestProject = project.isLatest() ? getLatestProjectVersion(project.getName()) : null;
final Project result = callInTransaction(() -> {
// Remove isLatest flag from current latest project version, if the new project will be the latest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private static List<ComponentDocument> fetchNext(final QueryManager qm, final Lo
final Query<Component> query = qm.getPersistenceManager().newQuery(Component.class);
var filterParts = new ArrayList<String>();
var params = new HashMap<String, Object>();
filterParts.add("(project.active == null || project.active)");
filterParts.add("project.active");
if (lastId != null) {
filterParts.add("id > :lastId");
params.put("lastId", lastId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private static List<ProjectDocument> fetchNext(final QueryManager qm, final Long
final Query<Project> query = qm.getPersistenceManager().newQuery(Project.class);
var filterParts = new ArrayList<String>();
var params = new HashMap<String, Object>();
filterParts.add("(active == null || active)");
filterParts.add("active");
if (lastId != null) {
filterParts.add("id > :lastId");
params.put("lastId", lastId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ private void updateMetrics() throws Exception {
private List<Project> fetchNextActiveProjectsBatch(final PersistenceManager pm, final Long lastId) {
final Query<Project> query = pm.newQuery(Project.class);
if (lastId == null) {
query.setFilter("(active == null || active == true)");
query.setFilter("active");
} else {
query.setFilter("(active == null || active == true) && id < :lastId");
query.setFilter("active && id < :lastId");
query.setParameters(lastId);
}
query.setOrdering("id DESC");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ protected boolean isCacheCurrent(ComponentAnalysisCache cac, String target) {

private List<Component> fetchNextComponentBatch(final QueryManager qm, final Long lastId) {
final var filterConditions = new ArrayList<>(List.of(
"(project.active == null || project.active)",
"project.active",
"purl != null"));
final var filterParams = new HashMap<String, Object>();
if (lastId != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class UpgradeItems {
UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4100.v4100Updater.class);
UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4110.v4110Updater.class);
UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4120.v4120Updater.class);
UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4122.v4122Updater.class);
}

static List<Class<? extends UpgradeItem>> getUpgradeItems() {
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/org/dependencytrack/upgrade/v4122/v4122Updater.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.upgrade.v4122;

import alpine.common.logging.Logger;
import alpine.persistence.AlpineQueryManager;
import alpine.server.upgrade.AbstractUpgradeItem;
import alpine.server.util.DbUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;

public class v4122Updater extends AbstractUpgradeItem {

private static final Logger LOGGER = Logger.getLogger(v4122Updater.class);

@Override
public String getSchemaVersion() {
return "4.12.2";
}

@Override
public void executeUpgrade(final AlpineQueryManager qm, final Connection connection) throws Exception {
fixProjectActiveNullValues(connection);
}

private static void fixProjectActiveNullValues(final Connection connection) throws SQLException {
LOGGER.info("Setting active flag to true for projects where it's currently null");
try (final PreparedStatement ps = connection.prepareStatement("""
UPDATE "PROJECT"
SET "ACTIVE" = ?
WHERE "ACTIVE" IS NULL;
""")) {
ps.setBoolean(1, true);

final int modifiedProjects = ps.executeUpdate();
LOGGER.info("Updated active flag of %d projects".formatted(modifiedProjects));
}

LOGGER.info("Setting default value of the project active flag to true");
try (final Statement stmt = connection.createStatement()) {
if (DbUtil.isMssql()) {
stmt.executeUpdate("""
ALTER TABLE "PROJECT"
ADD DEFAULT 'true'
FOR "ACTIVE";
""");
} else if (DbUtil.isMysql()) {
stmt.executeUpdate("""
ALTER TABLE "PROJECT"
MODIFY COLUMN "ACTIVE" BIT(1) DEFAULT 1;
""");
} else if (DbUtil.isPostgreSQL() || DbUtil.isH2()) {
stmt.executeUpdate("""
ALTER TABLE "PROJECT"
ALTER COLUMN "ACTIVE"
SET DEFAULT TRUE;
""");
} else {
throw new IllegalStateException(
"Unsupported database: " + connection.getMetaData().getDatabaseProductName());
}
}
}

}
2 changes: 0 additions & 2 deletions src/test/java/org/dependencytrack/model/ProjectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,9 @@ public void testProjectPersistActiveFieldDefaultsToTrue() {
project.setName("Example Project 1");
project.setDescription("Description 1");
project.setVersion("1.0");
project.setActive(null);

Project persistedProject = qm.createProject(project, null, false);

Assert.assertNotNull(persistedProject.isActive());
Assert.assertTrue(persistedProject.isActive());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -698,43 +698,6 @@ public void testAffectedInactiveChild() {
Assert.assertEquals(0, rules.size());
}

@Test
public void testAffectedActiveNullChild() {
NotificationPublisher publisher = createSlackPublisher();
// Creates a new rule and defines when the rule should be triggered (notifyOn)
NotificationRule rule = qm.createNotificationRule("Matching Test Rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher);
Set<NotificationGroup> notifyOn = new HashSet<>();
notifyOn.add(NotificationGroup.NEW_VULNERABILITY);
rule.setNotifyOn(notifyOn);
// Creates a project which will later be matched on
List<Project> projects = new ArrayList<>();
Project grandParent = qm.createProject("Test Project Grandparent", null, "1.0", null, null, null, true, false);
Project parent = qm.createProject("Test Project Parent", null, "1.0", null, grandParent, null, true, false);
Project child = qm.createProject("Test Project Child", null, "1.0", null, parent, null, true, false);
Project grandChild = qm.createProject("Test Project Grandchild", null, "1.0", null, child, null, true, false);
grandChild.setActive(null); // https://github.com/DependencyTrack/dependency-track/issues/3296
projects.add(grandParent);
rule.setProjects(projects);
// Creates a new component
Component component = new Component();
component.setProject(grandChild);
// Creates a new notification
Notification notification = new Notification();
notification.setScope(NotificationScope.PORTFOLIO.name());
notification.setGroup(NotificationGroup.NEW_VULNERABILITY.name());
notification.setLevel(NotificationLevel.INFORMATIONAL);
// Notification should be limited to only specific projects - Set the projects which are affected by the notification event
Set<Project> affectedProjects = new HashSet<>();
affectedProjects.add(grandChild);
NewVulnerabilityIdentified subject = new NewVulnerabilityIdentified(new Vulnerability(), component, affectedProjects, null);
notification.setSubject(subject);
// Ok, let's test this
NotificationRouter router = new NotificationRouter();
List<NotificationRule> rules = router.resolveRules(PublishContext.from(notification), notification);
Assert.assertTrue(rule.isNotifyChildren());
Assert.assertEquals(1, rules.size());
}

@Test
public void testValidMatchingTagLimitingRule() {
NotificationPublisher publisher = createSlackPublisher();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ public void getProjectByUuidTest() {
.withMatcher("projectUuid", equalTo(project.getUuid().toString()))
.withMatcher("parentUuid", equalTo(parentProject.getUuid().toString()))
.withMatcher("childUuid", equalTo(childProject.getUuid().toString()))
.isEqualTo("""
.isEqualTo(/* language=JSON */ """
{
"name": "acme-app",
"version": "1.0.0",
Expand All @@ -344,7 +344,8 @@ public void getProjectByUuidTest() {
"versions": [
{
"uuid": "${json-unit.matches:projectUuid}",
"version": "1.0.0"
"version": "1.0.0",
"active": true
}
]
}
Expand Down Expand Up @@ -1012,20 +1013,25 @@ public void updateProjectNotPermittedTest() {

@Test
public void updateProjectTestIsActiveEqualsNull() {
Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false);
project.setDescription("Test project");
project.setActive(null);
Assert.assertNull(project.isActive());
Response response = jersey.target(V1_PROJECT)
final Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false);
final Response response = jersey.target(V1_PROJECT)
.request()
.header(X_API_KEY, apiKey)
.post(Entity.entity(project, MediaType.APPLICATION_JSON));
.post(Entity.json(/* language=JSON */ """
{
"uuid": "%s",
"name": "ABC",
"version": "1.0",
"description": "Test project"
}
""".formatted(project.getUuid())));
Assert.assertEquals(200, response.getStatus(), 0);
JsonObject json = parseJsonObject(response);
Assert.assertNotNull(json);
Assert.assertEquals("ABC", json.getString("name"));
Assert.assertEquals("1.0", json.getString("version"));
Assert.assertEquals("Test project", json.getString("description"));
Assert.assertTrue(json.getBoolean("active"));
}

@Test
Expand Down
Loading