diff --git a/alpine/alpine-executable-war/src/main/java/alpine/embedded/EmbeddedJettyServer.java b/alpine/alpine-executable-war/src/main/java/alpine/embedded/EmbeddedJettyServer.java
index b5c693dce1..32979e87d8 100644
--- a/alpine/alpine-executable-war/src/main/java/alpine/embedded/EmbeddedJettyServer.java
+++ b/alpine/alpine-executable-war/src/main/java/alpine/embedded/EmbeddedJettyServer.java
@@ -43,6 +43,7 @@
/**
* The primary class that starts an embedded Jetty server
+ *
* @author Steve Springett
* @since 1.0.0
*/
@@ -73,7 +74,7 @@ public static void main(final String[] args) throws Exception {
final Server server = new Server();
final HttpConfiguration httpConfig = new HttpConfiguration();
- httpConfig.addCustomizer( new org.eclipse.jetty.server.ForwardedRequestCustomizer() ); // Add support for X-Forwarded headers
+ httpConfig.addCustomizer(new org.eclipse.jetty.server.ForwardedRequestCustomizer()); // Add support for X-Forwarded headers
// Enable legacy (mimicking Jetty 9) URI compliance.
// This is required to allow URL encoding in path segments, e.g. "/foo/bar%2Fbaz".
@@ -89,7 +90,7 @@ public static void main(final String[] args) throws Exception {
// here, the only viable long-term solution is to adapt REST APIs to follow Servlet API 6 spec.
httpConfig.setUriCompliance(UriCompliance.LEGACY);
- final HttpConnectionFactory connectionFactory = new HttpConnectionFactory( httpConfig );
+ final HttpConnectionFactory connectionFactory = new HttpConnectionFactory(httpConfig);
final ServerConnector connector = new ServerConnector(server, connectionFactory);
connector.setHost(host);
connector.setPort(port);
@@ -102,6 +103,7 @@ public static void main(final String[] args) throws Exception {
context.setErrorHandler(new ErrorHandler());
context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
context.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/[^/]*taglibs.*\\.jar$");
+ context.setThrowUnavailableOnStartupException(true);
// Prevent loading of logging classes
context.getProtectedClassMatcher().add("org.apache.log4j.");
diff --git a/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java b/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java
index eedd78a431..a11bbdbdac 100644
--- a/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java
+++ b/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java
@@ -52,12 +52,10 @@ public enum ConfigKey implements Config.Key {
VULNERABILITY_POLICY_S3_BUCKET_NAME("vulnerability.policy.s3.bucket.name", null),
VULNERABILITY_POLICY_S3_BUNDLE_NAME("vulnerability.policy.s3.bundle.name", null),
VULNERABILITY_POLICY_S3_REGION("vulnerability.policy.s3.region", null),
- DATABASE_MIGRATION_URL("database.migration.url", null),
- DATABASE_MIGRATION_USERNAME("database.migration.username", null),
- DATABASE_MIGRATION_PASSWORD("database.migration.password", null),
- DATABASE_RUN_MIGRATIONS("database.run.migrations", true),
- DATABASE_RUN_MIGRATIONS_ONLY("database.run.migrations.only", false),
INIT_TASKS_ENABLED("init.tasks.enabled", true),
+ INIT_TASKS_DATABASE_URL("init.tasks.database.url", null),
+ INIT_TASKS_DATABASE_USERNAME("init.tasks.database.username", null),
+ INIT_TASKS_DATABASE_PASSWORD("init.tasks.database.password", null),
INIT_AND_EXIT("init.and.exit", false),
DEV_SERVICES_ENABLED("dev.services.enabled", false),
diff --git a/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java b/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java
index 331b604bee..acb9fb7724 100644
--- a/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java
+++ b/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java
@@ -18,11 +18,9 @@
*/
package org.dependencytrack.health;
-import alpine.Config;
import alpine.common.logging.Logger;
import alpine.server.health.HealthCheckRegistry;
import alpine.server.health.checks.DatabaseHealthCheck;
-import org.dependencytrack.common.ConfigKey;
import org.dependencytrack.event.kafka.processor.ProcessorsHealthCheck;
import jakarta.servlet.ServletContextEvent;
@@ -34,12 +32,6 @@ public class HealthCheckInitializer implements ServletContextListener {
@Override
public void contextInitialized(final ServletContextEvent event) {
- if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- LOGGER.debug("Not registering health checks because %s is enabled"
- .formatted(ConfigKey.INIT_AND_EXIT.getPropertyName()));
- return;
- }
-
LOGGER.info("Registering health checks");
HealthCheckRegistry.getInstance().register("database", new DatabaseHealthCheck());
HealthCheckRegistry.getInstance().register("kafka-processors", new ProcessorsHealthCheck());
diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTask.java b/apiserver/src/main/java/org/dependencytrack/init/InitTask.java
new file mode 100644
index 0000000000..cea6327f2c
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/init/InitTask.java
@@ -0,0 +1,51 @@
+/*
+ * 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.init;
+
+/**
+ * A task to be run on application startup.
+ *
+ * @since 5.6.0
+ */
+public interface InitTask {
+
+ int PRIORITY_HIGHEST = 100;
+ int PRIORITY_LOWEST = 0;
+
+ /**
+ * @return Priority of the task.
+ * @see #PRIORITY_HIGHEST
+ * @see #PRIORITY_LOWEST
+ */
+ int priority();
+
+ /**
+ * @return Name of the task. Must be globally unique.
+ */
+ String name();
+
+ /**
+ * Execute the task.
+ *
+ * @param ctx Context in which the task is executed.
+ * @throws Exception When the task execution failed.
+ */
+ void execute(InitTaskContext ctx) throws Exception;
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java
similarity index 57%
rename from apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java
rename to apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java
index f2971a7ff9..be01f297fe 100644
--- a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java
+++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java
@@ -16,14 +16,21 @@
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
-package org.dependencytrack.persistence.defaults;
+package org.dependencytrack.init;
-import java.io.IOException;
+import alpine.Config;
-public interface IDefaultObjectImporter {
-
- boolean shouldImport();
-
- void loadDefaults() throws IOException;
+import javax.sql.DataSource;
+/**
+ * Context available to {@link InitTask}s.
+ *
+ * TODO: Introduce a tiny abstraction over {@link Config} such that
+ * Alpine specifics don't bleed through to {@link InitTask}s.
+ *
+ * @param config A {@link Config} instance to read application configuration.
+ * @param dataSource A {@link DataSource} which may be used for database interactions.
+ * @since 5.6.0
+ */
+public record InitTaskContext(Config config, DataSource dataSource) {
}
diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java
new file mode 100644
index 0000000000..2ee6c5837a
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java
@@ -0,0 +1,180 @@
+/*
+ * 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.init;
+
+import alpine.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.servlet.ServletContextListener;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import static java.util.Comparator.comparing;
+import static java.util.Comparator.reverseOrder;
+import static java.util.Objects.requireNonNull;
+import static org.dependencytrack.util.ConfigUtil.getPassThroughProperties;
+
+/**
+ * @since 5.6.0
+ */
+final class InitTaskExecutor implements ServletContextListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(InitTaskExecutor.class);
+ private static final long ADVISORY_LOCK_KEY = "dependency-track-init-tasks".hashCode();
+
+ private final Config config;
+ private final DataSource dataSource;
+ private final List tasks;
+
+ InitTaskExecutor(final Config config, final DataSource dataSource) {
+ this(config, dataSource, loadInitTasks());
+ }
+
+ InitTaskExecutor(final Config config, final DataSource dataSource, final List tasks) {
+ this.config = requireNonNull(config, "config must not be null");
+ this.dataSource = requireNonNull(dataSource, "dataSource must not be null");
+ this.tasks = requireNonNull(tasks, "tasks must not be null");
+ }
+
+ public void execute() {
+ final List orderedTasks = this.tasks.stream()
+ .peek(requireUniqueName())
+ .peek(requireValidPriority())
+ .filter(isTaskEnabled())
+ .sorted(comparing(InitTask::priority, reverseOrder())
+ .thenComparing(InitTask::name))
+ .toList();
+
+ final long startTimeNanos = System.nanoTime();
+
+ // We're using session-level advisory locks here,
+ // which won't work when using PgBouncer in "transaction" mode.
+ // We can't use transaction-level locking because that would
+ // block some DDL statements executed by database migrations,
+ // such as "CREATE INDEX CONCURRENTLY".
+ //
+ // This GitLab issue describes the problem well:
+ // https://gitlab.com/gitlab-com/support/support-training/-/issues/3823#locks-block-a-gitlab-database-migration
+ //
+ // The intended workaround is to use a separate set of connection
+ // details specifically for init tasks, which bypasses PgBouncer.
+ try (final Connection connection = dataSource.getConnection();
+ final PreparedStatement lockStatement = connection.prepareStatement("""
+ SELECT PG_ADVISORY_LOCK(?)
+ """);
+ final PreparedStatement unlockStatement = connection.prepareStatement("""
+ SELECT PG_ADVISORY_UNLOCK(?)
+ """)) {
+ LOGGER.debug("Trying to acquire lock {}", ADVISORY_LOCK_KEY);
+ lockStatement.setLong(1, ADVISORY_LOCK_KEY);
+ lockStatement.execute();
+ LOGGER.debug(
+ "Lock {} acquired after {}ms",
+ ADVISORY_LOCK_KEY,
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos));
+
+ final var taskContext = new InitTaskContext(config, dataSource);
+
+ try {
+ long taskStartTimeNanos;
+ for (final InitTask task : orderedTasks) {
+ taskStartTimeNanos = System.nanoTime();
+ LOGGER.info("Executing init task {}", task.name());
+ try {
+ task.execute(taskContext);
+ LOGGER.info(
+ "Completed init task {} in {}ms",
+ task.name(),
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - taskStartTimeNanos));
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to execute init task " + task.name(), e);
+ }
+ }
+ } finally {
+ LOGGER.debug("Releasing lock {}", ADVISORY_LOCK_KEY);
+ unlockStatement.setLong(1, ADVISORY_LOCK_KEY);
+ final ResultSet rs = unlockStatement.executeQuery();
+ if (!rs.next() || !rs.getBoolean(1)) {
+ LOGGER.warn("""
+ Lock {} could not be released, likely because a connection pooler \
+ in "transaction" mode is being used. Ensure that a direct database connection \
+ is provided when executing init tasks.""", ADVISORY_LOCK_KEY);
+ }
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Failed to acquire or release lock " + ADVISORY_LOCK_KEY, e);
+ }
+
+ LOGGER.info(
+ "All init tasks completed in {}ms",
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos));
+ }
+
+ private static List loadInitTasks() {
+ return ServiceLoader.load(InitTask.class).stream()
+ .map(ServiceLoader.Provider::get)
+ .toList();
+ }
+
+ private Consumer requireUniqueName() {
+ final var seenTaskClassesByTaskName =
+ new HashMap>(this.tasks.size());
+
+ return task -> {
+ final Class extends InitTask> previousClass =
+ seenTaskClassesByTaskName.put(task.name(), task.getClass());
+ if (previousClass != null) {
+ throw new IllegalStateException(
+ "Duplicate task name %s: Registered by %s and %s".formatted(
+ task.name(), previousClass.getName(), task.getClass().getName()));
+ }
+ };
+ }
+
+ private Consumer requireValidPriority() {
+ return task -> {
+ if (task.priority() < InitTask.PRIORITY_LOWEST
+ || task.priority() > InitTask.PRIORITY_HIGHEST) {
+ throw new IllegalStateException(
+ "Invalid priority of task %s: Must be within [%d..%d] but is %d".formatted(
+ task.name(), InitTask.PRIORITY_LOWEST, InitTask.PRIORITY_HIGHEST, task.priority()));
+ }
+ };
+ }
+
+ private Predicate isTaskEnabled() {
+ return task -> {
+ final String propertyPrefix = "init.task." + task.name();
+ final Map properties = getPassThroughProperties(config, propertyPrefix);
+ return !"false".equals(properties.get(propertyPrefix + ".enabled"));
+ };
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java
new file mode 100644
index 0000000000..5686412dfc
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java
@@ -0,0 +1,99 @@
+/*
+ * 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.init;
+
+import alpine.Config;
+import org.dependencytrack.common.ConfigKey;
+import org.postgresql.ds.PGSimpleDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import javax.sql.DataSource;
+
+import static alpine.Config.AlpineKey.DATABASE_PASSWORD;
+import static alpine.Config.AlpineKey.DATABASE_URL;
+import static alpine.Config.AlpineKey.DATABASE_USERNAME;
+import static java.util.Objects.requireNonNullElseGet;
+import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_PASSWORD;
+import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_URL;
+import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_USERNAME;
+
+/**
+ * @since 5.6.0
+ */
+public final class InitTaskServletContextListener implements ServletContextListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(InitTaskServletContextListener.class);
+
+ private final Config config;
+
+ @SuppressWarnings("unused")
+ public InitTaskServletContextListener() {
+ this(Config.getInstance());
+ }
+
+ InitTaskServletContextListener(final Config config) {
+ this.config = config;
+ }
+
+ @Override
+ public void contextInitialized(final ServletContextEvent event) {
+ if (!config.getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) {
+ LOGGER.debug(
+ "Not executing init tasks because {} is disabled",
+ ConfigKey.INIT_TASKS_ENABLED.getPropertyName());
+ return;
+ }
+
+ final DataSource dataSource;
+ try {
+ dataSource = createDataSource(config);
+ } catch (RuntimeException e) {
+ throw new IllegalStateException("Failed to create data source", e);
+ }
+
+ final var taskExecutor = new InitTaskExecutor(config, dataSource);
+ taskExecutor.execute();
+
+ if (config.getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
+ LOGGER.info(
+ "Exiting because {} is enabled",
+ ConfigKey.INIT_AND_EXIT.getPropertyName());
+ System.exit(0);
+ }
+ }
+
+ private DataSource createDataSource(final Config config) {
+ final var dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(requireNonNullElseGet(
+ config.getProperty(INIT_TASKS_DATABASE_URL),
+ () -> config.getProperty(DATABASE_URL)));
+ dataSource.setUser(requireNonNullElseGet(
+ config.getProperty(INIT_TASKS_DATABASE_USERNAME),
+ () -> config.getProperty(DATABASE_USERNAME)));
+ dataSource.setPassword(requireNonNullElseGet(
+ config.getProperty(INIT_TASKS_DATABASE_PASSWORD),
+ () -> config.getPropertyOrFile(DATABASE_PASSWORD)));
+
+ return dataSource;
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java b/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java
new file mode 100644
index 0000000000..6a7df63fbb
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java
@@ -0,0 +1,72 @@
+/*
+ * 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.model;
+
+/**
+ * @since 5.6.0
+ */
+public enum DefaultRepository {
+
+ CPAN_PUBLIC_REGISTRY(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", 1),
+ GEM_RUBYGEMS(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", 1),
+ HEX_HEX_PM(RepositoryType.HEX, "hex.pm", "https://hex.pm/", 1),
+ MAVEN_CENTRAL(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", 1),
+ MAVEN_ATLASSIAN_PUBLIC(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", 2),
+ MAVEN_JBOSS_RELEASES(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", 3),
+ MAVEN_CLOJARS(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", 4),
+ MAVEN_GOOGLE_ANDROID(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", 5),
+ NPM_PUBLIC_REGISTRY(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", 1),
+ PYPI_PYPI_ORG(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", 1),
+ NUGET_GALLERY(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", 1),
+ COMPOSER_PACKAGIST(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", 1),
+ CARGO_CRATES_IO(RepositoryType.CARGO, "crates.io", "https://crates.io", 1),
+ GO_PROXY_GOLANG_ORG(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", 1),
+ GITHUB(RepositoryType.GITHUB, "github", "https://github.com", 1),
+ HACKAGE(RepositoryType.HACKAGE, "hackage.haskell", "https://hackage.haskell.org/", 1),
+ NIXPKGS_NIXOS_ORG(RepositoryType.NIXPKGS, "nixos.org", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", 1);
+
+ private final RepositoryType type;
+ private final String identifier;
+ private final String url;
+ private final int resolutionOrder;
+
+ DefaultRepository(final RepositoryType type, final String identifier, final String url, final int resolutionOrder) {
+ this.type = type;
+ this.identifier = identifier;
+ this.url = url;
+ this.resolutionOrder = resolutionOrder;
+ }
+
+ public RepositoryType getType() {
+ return type;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public int getResolutionOrder() {
+ return resolutionOrder;
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java
new file mode 100644
index 0000000000..83c10e87c8
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+import alpine.server.util.DbUtil;
+import org.dependencytrack.init.InitTask;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.support.liquibase.MigrationExecutor;
+
+import java.sql.Connection;
+
+/**
+ * @since 5.6.0
+ */
+public final class DatabaseMigrationInitTask implements InitTask {
+
+ @Override
+ public int priority() {
+ return PRIORITY_HIGHEST;
+ }
+
+ @Override
+ public String name() {
+ return "database.migration";
+ }
+
+ @Override
+ public void execute(InitTaskContext ctx) throws Exception {
+ try (final Connection connection = ctx.dataSource().getConnection()) {
+ // Ensure that DbUtil#isPostgreSQL will work as expected.
+ // Some legacy code ported over from v4 still uses this.
+ //
+ // NB: This was previously done in alpine.server.upgrade.UpgradeExecutor.
+ //
+ // TODO: Remove once DbUtil#isPostgreSQL is no longer used.
+ DbUtil.initPlatformName(connection);
+ }
+
+ new MigrationExecutor(ctx.dataSource(), "migration/changelog-main.xml").executeMigration();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java
new file mode 100644
index 0000000000..f391fafdfb
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import org.dependencytrack.init.InitTask;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.persistence.jdbi.JdbiFactory;
+import org.dependencytrack.persistence.jdbi.MetricsDao;
+
+import java.time.LocalDate;
+
+/**
+ * @since 5.6.0
+ */
+public final class DatabasePartitionMaintenanceInitTask implements InitTask {
+
+ @Override
+ public int priority() {
+ return PRIORITY_HIGHEST - 10;
+ }
+
+ @Override
+ public String name() {
+ return "database.partition.maintenance";
+ }
+
+ @Override
+ public void execute(final InitTaskContext ctx) throws Exception {
+ final var jdbi = JdbiFactory.createLocalJdbi(ctx.dataSource());
+
+ jdbi.useTransaction(handle -> {
+ var metricsDao = handle.attach(MetricsDao.class);
+ metricsDao.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString());
+ metricsDao.createMetricsPartitionsForDate(LocalDate.now().plusDays(1).toString(), LocalDate.now().plusDays(2).toString());
+ });
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java
new file mode 100644
index 0000000000..35d243e0b9
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java
@@ -0,0 +1,494 @@
+/*
+ * 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;
+
+import alpine.server.auth.PasswordService;
+import org.apache.commons.lang3.SerializationUtils;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.init.InitTask;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.model.ConfigPropertyConstants;
+import org.dependencytrack.model.DefaultRepository;
+import org.dependencytrack.model.License;
+import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
+import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser;
+import org.dependencytrack.persistence.jdbi.ConfigPropertyDao;
+import org.dependencytrack.persistence.jdbi.JdbiFactory;
+import org.jdbi.v3.core.Handle;
+import org.jdbi.v3.core.statement.PreparedBatch;
+import org.jdbi.v3.core.statement.Update;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.dependencytrack.model.ConfigPropertyConstants.INTERNAL_DEFAULT_OBJECTS_VERSION;
+import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR;
+import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED;
+
+/**
+ * @since 5.6.0
+ */
+public final class DatabaseSeedingInitTask implements InitTask {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseSeedingInitTask.class);
+
+ private static final Map> DEFAULT_TEAM_PERMISSIONS = Map.of(
+ "Administrators", Stream.of(Permissions.values()).map(Permissions::name).toList(),
+ "Portfolio Managers", List.of(Permissions.Constants.PORTFOLIO_MANAGEMENT),
+ "Automation", List.of(Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.BOM_CREATE),
+ "Badge Viewers", List.of(Permissions.Constants.BADGES_READ));
+
+ private static final Map> DEFAULT_ROLE_PERMISSIONS = Map.of(
+ "Project Admin", List.of(
+ Permissions.Constants.BADGES_READ,
+ Permissions.Constants.BOM_READ,
+ Permissions.Constants.BOM_CREATE,
+ Permissions.Constants.FINDING_CREATE,
+ Permissions.Constants.FINDING_READ,
+ Permissions.Constants.FINDING_UPDATE,
+ Permissions.Constants.POLICY_VIOLATION_CREATE,
+ Permissions.Constants.POLICY_VIOLATION_READ,
+ Permissions.Constants.POLICY_VIOLATION_UPDATE,
+ Permissions.Constants.PROJECT_READ,
+ Permissions.Constants.PROJECT_UPDATE,
+ Permissions.Constants.PROJECT_DELETE),
+ "Project Auditor", List.of(
+ Permissions.Constants.BADGES_READ,
+ Permissions.Constants.BOM_READ,
+ Permissions.Constants.FINDING_READ,
+ Permissions.Constants.POLICY_VIOLATION_READ,
+ Permissions.Constants.PROJECT_READ),
+ "Project Editor", List.of(
+ Permissions.Constants.BOM_CREATE,
+ Permissions.Constants.BOM_READ,
+ Permissions.Constants.FINDING_READ,
+ Permissions.Constants.FINDING_UPDATE,
+ Permissions.Constants.POLICY_VIOLATION_CREATE,
+ Permissions.Constants.POLICY_VIOLATION_READ,
+ Permissions.Constants.POLICY_VIOLATION_UPDATE,
+ Permissions.Constants.PROJECT_READ,
+ Permissions.Constants.PROJECT_UPDATE),
+ "Project Viewer", List.of(
+ Permissions.Constants.BADGES_READ,
+ Permissions.Constants.BOM_READ,
+ Permissions.Constants.PROJECT_READ));
+
+ @Override
+ public int priority() {
+ return PRIORITY_HIGHEST - 10;
+ }
+
+ @Override
+ public String name() {
+ return "database.seeding";
+ }
+
+ @Override
+ public void execute(final InitTaskContext ctx) throws Exception {
+ final var jdbi = JdbiFactory.createLocalJdbi(ctx.dataSource());
+
+ jdbi.useTransaction(handle -> {
+ final var configPropertyDao = handle.attach(ConfigPropertyDao.class);
+
+ final String defaultObjectsVersion = configPropertyDao
+ .getOptionalValue(INTERNAL_DEFAULT_OBJECTS_VERSION)
+ .orElse(null);
+ if (ctx.config().getApplicationBuildUuid().equals(defaultObjectsVersion)) {
+ LOGGER.info(
+ "Default objects already populated for build {} (timestamp: {}); Skipping",
+ ctx.config().getApplicationBuildUuid(),
+ ctx.config().getApplicationBuildTimestamp());
+ return;
+ }
+
+ seedDefaultConfigProperties(handle);
+ seedDefaultPermissions(handle);
+ seedDefaultLicenses(handle);
+ seedDefaultNotificationPublishers(handle);
+ seedDefaultRepositories(handle);
+
+ final boolean isFirstExecution = defaultObjectsVersion == null;
+ if (isFirstExecution) {
+ seedDefaultTeams(handle);
+ seedDefaultRoles(handle);
+ seedDefaultUsers(handle);
+ seedDefaultLicenseGroups(handle);
+ }
+
+ configPropertyDao.setValue(
+ INTERNAL_DEFAULT_OBJECTS_VERSION,
+ ctx.config().getApplicationBuildUuid());
+ });
+ }
+
+ public static void seedDefaultConfigProperties(final Handle jdbiHandle) {
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "CONFIGPROPERTY" ("GROUPNAME", "PROPERTYNAME", "PROPERTYTYPE", "PROPERTYVALUE", "DESCRIPTION")
+ VALUES (:groupName, :propertyName, :propertyType, :defaultPropertyValue, :description)
+ ON CONFLICT ("GROUPNAME", "PROPERTYNAME") DO NOTHING
+ """);
+
+ for (final ConfigPropertyConstants configProperty : ConfigPropertyConstants.values()) {
+ preparedBatch.bindBean(configProperty);
+ preparedBatch.add();
+ }
+
+ final int configPropertiesCreated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created {} config properties", configPropertiesCreated);
+ }
+
+ public static void seedDefaultPermissions(final Handle jdbiHandle) {
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "PERMISSION" ("NAME", "DESCRIPTION")
+ VALUES (:name, :description)
+ ON CONFLICT ("NAME") DO NOTHING
+ """);
+
+ for (final Permissions permission : Permissions.values()) {
+ preparedBatch.bind("name", permission.name());
+ preparedBatch.bind("description", permission.getDescription());
+ preparedBatch.add();
+ }
+
+ final int permissionsCreated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created {} permissions", permissionsCreated);
+ }
+
+ public static void seedDefaultTeams(final Handle jdbiHandle) {
+ final Update update = jdbiHandle.createUpdate("""
+ WITH cte_team_permission AS (
+ SELECT *
+ FROM UNNEST(:teamNames, :permissionNames) AS t(team_name, permission_name)
+ ),
+ cte_created_team AS (
+ INSERT INTO "TEAM" ("NAME", "UUID")
+ SELECT DISTINCT ON (team_name)
+ team_name
+ , GEN_RANDOM_UUID()
+ FROM cte_team_permission
+ RETURNING "ID" AS id
+ , "NAME" AS name
+ )
+ INSERT INTO "TEAMS_PERMISSIONS" ("TEAM_ID", "PERMISSION_ID")
+ SELECT cte_created_team.id
+ , (SELECT "ID" FROM "PERMISSION" WHERE "NAME" = cte_team_permission.permission_name)
+ FROM cte_team_permission
+ INNER JOIN cte_created_team
+ ON cte_created_team.name = cte_team_permission.team_name
+ """);
+
+ final var teamNames = new ArrayList();
+ final var permissionNames = new ArrayList();
+
+ for (final Map.Entry> entry : DEFAULT_TEAM_PERMISSIONS.entrySet()) {
+ for (final String permissionName : entry.getValue()) {
+ teamNames.add(entry.getKey());
+ permissionNames.add(permissionName);
+ }
+ }
+
+ update
+ .bindArray("teamNames", String.class, teamNames)
+ .bindArray("permissionNames", String.class, permissionNames)
+ .execute();
+ }
+
+ public static void seedDefaultRoles(final Handle jdbiHandle) {
+ final Update update = jdbiHandle.createUpdate("""
+ WITH cte_role_permission AS (
+ SELECT *
+ FROM UNNEST(:roleNames, :permissionNames) AS t(role_name, permission_name)
+ ),
+ cte_created_role AS (
+ INSERT INTO "ROLE" ("NAME", "UUID")
+ SELECT DISTINCT ON (role_name)
+ role_name
+ , GEN_RANDOM_UUID()
+ FROM cte_role_permission
+ RETURNING "ID" AS id
+ , "NAME" AS name
+ )
+ INSERT INTO "ROLES_PERMISSIONS" ("ROLE_ID", "PERMISSION_ID")
+ SELECT cte_created_role.id
+ , (SELECT "ID" FROM "PERMISSION" WHERE "NAME" = cte_role_permission.permission_name)
+ FROM cte_role_permission
+ INNER JOIN cte_created_role
+ ON cte_created_role.name = cte_role_permission.role_name
+ """);
+
+ final var roleNames = new ArrayList();
+ final var permissionNames = new ArrayList();
+
+ for (final Map.Entry> entry : DEFAULT_ROLE_PERMISSIONS.entrySet()) {
+ for (final String permissionName : entry.getValue()) {
+ roleNames.add(entry.getKey());
+ permissionNames.add(permissionName);
+ }
+ }
+
+ update
+ .bindArray("roleNames", String.class, roleNames)
+ .bindArray("permissionNames", String.class, permissionNames)
+ .execute();
+ }
+
+ public static void seedDefaultUsers(final Handle jdbiHandle) {
+ final long adminUserId = jdbiHandle.createUpdate("""
+ INSERT INTO "USER" (
+ "TYPE", "USERNAME", "EMAIL", "PASSWORD", "LAST_PASSWORD_CHANGE"
+ , "FORCE_PASSWORD_CHANGE", "NON_EXPIRY_PASSWORD", "SUSPENDED")
+ VALUES ('MANAGED', 'admin', 'admin@localhost', :password, NOW(), TRUE, TRUE, FALSE)
+ RETURNING "ID"
+ """)
+ .bind("password", new String(PasswordService.createHash("admin".toCharArray())))
+ .executeAndReturnGeneratedKeys()
+ .mapTo(Long.class)
+ .one();
+
+ jdbiHandle.createUpdate("""
+ INSERT INTO "USERS_TEAMS" ("USER_ID", "TEAM_ID")
+ SELECT :adminUserId, (SELECT "ID" FROM "TEAM" WHERE "NAME" = 'Administrators')
+ """)
+ .bind("adminUserId", adminUserId)
+ .execute();
+
+ jdbiHandle.createUpdate("""
+ INSERT INTO "USERS_PERMISSIONS" ("USER_ID", "PERMISSION_ID")
+ SELECT :adminUserId, "PERMISSION"."ID" FROM "PERMISSION"
+ """)
+ .bind("adminUserId", adminUserId)
+ .execute();
+ }
+
+ public static void seedDefaultLicenses(final Handle jdbiHandle) {
+ final List licenses;
+ try {
+ licenses = new SpdxLicenseDetailParser().getLicenseDefinitions();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to load license details", e);
+ }
+
+ // We have hundreds of licenses, the majority of which is *very* unlikely to change between executions
+ // of this init task. In the future, we should store the version of the SPDX license list,
+ // and then only sync licenses when that version has changed.
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "LICENSE" (
+ "LICENSEID", "NAME", "HEADER", "TEXT", "TEMPLATE", "ISDEPRECATED"
+ , "FSFLIBRE", "ISOSIAPPROVED", "COMMENT", "SEEALSO", "UUID"
+ )
+ VALUES (
+ :licenseId, :name, :header, :text, :template, :deprecatedLicenseId
+ , :fsfLibre, :osiApproved, :comment, :seeAlsoSerialized, GEN_RANDOM_UUID()
+ )
+ ON CONFLICT ("LICENSEID") DO UPDATE
+ SET "NAME" = EXCLUDED."NAME"
+ , "HEADER" = EXCLUDED."HEADER"
+ , "TEXT" = EXCLUDED."TEXT"
+ , "TEMPLATE" = EXCLUDED."TEMPLATE"
+ , "ISDEPRECATED" = EXCLUDED."ISDEPRECATED"
+ , "FSFLIBRE" = EXCLUDED."FSFLIBRE"
+ , "ISOSIAPPROVED" = EXCLUDED."ISOSIAPPROVED"
+ , "COMMENT" = EXCLUDED."COMMENT"
+ , "SEEALSO" = EXCLUDED."SEEALSO"
+ -- Only update when at least one relevant field has changed.
+ WHERE "LICENSE"."NAME" IS DISTINCT FROM EXCLUDED."NAME"
+ OR "LICENSE"."HEADER" IS DISTINCT FROM EXCLUDED."HEADER"
+ OR "LICENSE"."TEXT" IS DISTINCT FROM EXCLUDED."TEXT"
+ OR "LICENSE"."TEMPLATE" IS DISTINCT FROM EXCLUDED."TEMPLATE"
+ OR "LICENSE"."ISDEPRECATED" IS DISTINCT FROM EXCLUDED."ISDEPRECATED"
+ OR "LICENSE"."FSFLIBRE" IS DISTINCT FROM EXCLUDED."FSFLIBRE"
+ OR "LICENSE"."ISOSIAPPROVED" IS DISTINCT FROM EXCLUDED."ISOSIAPPROVED"
+ OR "LICENSE"."COMMENT" IS DISTINCT FROM EXCLUDED."COMMENT"
+ OR "LICENSE"."SEEALSO" IS DISTINCT FROM EXCLUDED."SEEALSO"
+ """);
+
+ for (final License license : licenses) {
+ preparedBatch.bindBean(license);
+ preparedBatch.bind(
+ "seeAlsoSerialized",
+ license.getSeeAlso() != null
+ ? SerializationUtils.serialize(license.getSeeAlso())
+ : null);
+ preparedBatch.add();
+ }
+
+ int licensesCreatedOrUpdated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created or updated {} licenses", licensesCreatedOrUpdated);
+ }
+
+ public static void seedDefaultLicenseGroups(final Handle jdbiHandle) {
+ final Update update = jdbiHandle.createUpdate("""
+ WITH cte_group_license AS (
+ SELECT *
+ FROM UNNEST(:groupNames, :groupRiskWeights, :licenseIds) AS t(group_name, group_risk_weight, license_id)
+ ),
+ cte_created_group AS (
+ INSERT INTO "LICENSEGROUP" ("NAME", "RISKWEIGHT", "UUID")
+ SELECT DISTINCT ON (group_name)
+ group_name
+ , group_risk_weight
+ , GEN_RANDOM_UUID()
+ FROM cte_group_license
+ RETURNING "ID" AS id, "NAME" AS name
+ )
+ INSERT INTO "LICENSEGROUP_LICENSE" ("LICENSEGROUP_ID", "LICENSE_ID")
+ SELECT cte_created_group.id
+ , (SELECT "ID" FROM "LICENSE" WHERE "LICENSEID" = cte_group_license.license_id)
+ FROM cte_group_license
+ INNER JOIN cte_created_group
+ ON cte_created_group.name = cte_group_license.group_name
+ """);
+
+ final JsonArray groupDefsJson;
+ try (final InputStream inputStream = DatabaseSeedingInitTask.class.getResourceAsStream("/default-objects/licenseGroups.json");
+ final JsonReader jsonReader = Json.createReader(inputStream)) {
+ groupDefsJson = jsonReader.readArray();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to parse license group definition", e);
+ }
+
+ final var groupNames = new ArrayList();
+ final var groupRiskWeights = new ArrayList();
+ final var licenseIds = new ArrayList();
+
+ for (int i = 0; i < groupDefsJson.size(); i++) {
+ final JsonObject groupDefJson = groupDefsJson.getJsonObject(i);
+ final String groupName = groupDefJson.getString("name");
+ final int riskWeight = groupDefJson.getInt("riskWeight");
+
+ final JsonArray licenseIdsJson = groupDefJson.getJsonArray("licenses");
+ for (int j = 0; j < licenseIdsJson.size(); j++) {
+ groupNames.add(groupName);
+ groupRiskWeights.add(riskWeight);
+ licenseIds.add(licenseIdsJson.getString(j));
+ }
+ }
+
+ update
+ .bindArray("groupNames", String.class, groupNames)
+ .bindArray("groupRiskWeights", Integer.class, groupRiskWeights)
+ .bindArray("licenseIds", String.class, licenseIds)
+ .execute();
+ }
+
+ public static void seedDefaultNotificationPublishers(final Handle jdbiHandle) {
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "NOTIFICATIONPUBLISHER" (
+ "NAME", "PUBLISHER_CLASS", "DEFAULT_PUBLISHER", "DESCRIPTION"
+ , "TEMPLATE", "TEMPLATE_MIME_TYPE", "UUID")
+ VALUES (
+ :publisherName, :publisherClass, TRUE, :publisherDescription
+ , :templateContent, :templateMimeType, GEN_RANDOM_UUID())
+ ON CONFLICT ("NAME") DO UPDATE
+ SET "PUBLISHER_CLASS" = EXCLUDED."PUBLISHER_CLASS"
+ , "DESCRIPTION" = EXCLUDED."DESCRIPTION"
+ , "TEMPLATE" = EXCLUDED."TEMPLATE"
+ , "TEMPLATE_MIME_TYPE" = EXCLUDED."TEMPLATE_MIME_TYPE"
+ -- Only update when at least one relevant field has changed.
+ WHERE "NOTIFICATIONPUBLISHER"."PUBLISHER_CLASS" IS DISTINCT FROM EXCLUDED."PUBLISHER_CLASS"
+ OR "NOTIFICATIONPUBLISHER"."DESCRIPTION" IS DISTINCT FROM EXCLUDED."DESCRIPTION"
+ OR "NOTIFICATIONPUBLISHER"."TEMPLATE" IS DISTINCT FROM EXCLUDED."TEMPLATE"
+ OR "NOTIFICATIONPUBLISHER"."TEMPLATE_MIME_TYPE" IS DISTINCT FROM EXCLUDED."TEMPLATE_MIME_TYPE"
+ """);
+
+ final var configPropertyDao = jdbiHandle.attach(ConfigPropertyDao.class);
+ final var templateOverrideEnabled = configPropertyDao.getOptionalValue(
+ NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED, Boolean.class).orElse(false);
+ final var templateOverrideBaseDir = configPropertyDao.getOptionalValue(
+ NOTIFICATION_TEMPLATE_BASE_DIR).orElse(null);
+ if (templateOverrideEnabled && templateOverrideBaseDir == null) {
+ throw new IllegalStateException("%s is enabled but %s is not configured".formatted(
+ NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyName(),
+ NOTIFICATION_TEMPLATE_BASE_DIR.getPropertyName()));
+ }
+
+ for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) {
+ final URL templateFileUrl = DatabaseSeedingInitTask.class.getResource(publisher.getPublisherTemplateFile());
+ if (templateFileUrl == null) {
+ throw new IllegalStateException("Template file %s of default publisher %s does not exist".formatted(
+ publisher.getPublisherTemplateFile(), publisher.getPublisherName()));
+ }
+
+ Path templateFilePath;
+ try {
+ templateFilePath = Paths.get(templateFileUrl.toURI());
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException("Failed to construct path for template file: " + templateFileUrl, e);
+ }
+
+ if (templateOverrideEnabled) {
+ final Path customTemplateFilePath = Paths.get(templateOverrideBaseDir, publisher.getPublisherTemplateFile());
+ if (Files.exists(customTemplateFilePath)) {
+ templateFilePath = customTemplateFilePath;
+ }
+ }
+
+ final String templateContent;
+ try {
+ templateContent = Files.readString(templateFilePath);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to read template file: " + templateFilePath, e);
+ }
+
+ preparedBatch.bindBean(publisher);
+ preparedBatch.bind("templateContent", templateContent);
+ preparedBatch.add();
+ }
+
+ final int publishersCreatedOrUpdated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created or updated {} publishers", publishersCreatedOrUpdated);
+ }
+
+ public static void seedDefaultRepositories(final Handle jdbiHandle) {
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "REPOSITORY"(
+ "TYPE", "IDENTIFIER", "URL", "INTERNAL", "RESOLUTION_ORDER"
+ , "ENABLED", "AUTHENTICATIONREQUIRED", "UUID")
+ VALUES (
+ :type, :identifier, :url, FALSE, :resolutionOrder
+ , TRUE, FALSE, GEN_RANDOM_UUID())
+ ON CONFLICT ("TYPE", "IDENTIFIER") DO NOTHING
+ """);
+
+ for (final DefaultRepository repository : DefaultRepository.values()) {
+ preparedBatch.bindBean(repository);
+ preparedBatch.add();
+ }
+
+ final int reposCreated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created {} repositories", reposCreated);
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java
deleted file mode 100644
index 3c194031d3..0000000000
--- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java
+++ /dev/null
@@ -1,431 +0,0 @@
-/*
- * 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;
-
-import alpine.Config;
-import alpine.common.logging.Logger;
-import alpine.model.ConfigProperty;
-import alpine.model.ManagedUser;
-import alpine.model.Permission;
-import alpine.server.auth.PasswordService;
-
-import jakarta.servlet.ServletContextEvent;
-import jakarta.servlet.ServletContextListener;
-
-import org.dependencytrack.auth.Permissions;
-import org.dependencytrack.common.ConfigKey;
-import org.dependencytrack.model.ConfigPropertyConstants;
-import org.dependencytrack.model.License;
-import org.dependencytrack.model.RepositoryType;
-import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser;
-import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter;
-import org.dependencytrack.persistence.jdbi.MetricsDao;
-import org.dependencytrack.util.NotificationUtil;
-import org.dependencytrack.util.WaitingLockConfiguration;
-
-import java.io.IOException;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static net.javacrumbs.shedlock.core.LockAssert.assertLocked;
-import static org.dependencytrack.model.ConfigPropertyConstants.INTERNAL_DEFAULT_OBJECTS_VERSION;
-import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle;
-import static org.dependencytrack.util.LockProvider.executeWithLockWaiting;
-
-/**
- * Creates default objects on an empty database.
- *
- * @author Steve Springett
- * @since 3.0.0
- */
-public class DefaultObjectGenerator implements ServletContextListener {
-
- private static final Logger LOGGER = Logger.getLogger(DefaultObjectGenerator.class);
-
- private static final Map> DEFAULT_TEAM_PERMISSIONS = Map.of(
- "Administrators", Stream.of(Permissions.values()).map(Permissions::name).toList(),
- "Portfolio Managers", List.of(Permissions.Constants.PORTFOLIO_MANAGEMENT),
- "Automation", List.of(Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.BOM_CREATE),
- "Badge Viewers", List.of(Permissions.Constants.BADGES_READ));
-
- private static final Map> DEFAULT_ROLE_PERMISSIONS = Map.of(
- "Project Admin", List.of(
- Permissions.Constants.BADGES_READ,
- Permissions.Constants.BOM_READ,
- Permissions.Constants.BOM_CREATE,
- Permissions.Constants.FINDING_CREATE,
- Permissions.Constants.FINDING_READ,
- Permissions.Constants.FINDING_UPDATE,
- Permissions.Constants.POLICY_VIOLATION_CREATE,
- Permissions.Constants.POLICY_VIOLATION_READ,
- Permissions.Constants.POLICY_VIOLATION_UPDATE,
- Permissions.Constants.PROJECT_READ,
- Permissions.Constants.PROJECT_UPDATE,
- Permissions.Constants.PROJECT_DELETE),
- "Project Auditor", List.of(
- Permissions.Constants.BADGES_READ,
- Permissions.Constants.BOM_READ,
- Permissions.Constants.FINDING_READ,
- Permissions.Constants.POLICY_VIOLATION_READ,
- Permissions.Constants.PROJECT_READ),
- "Project Editor", List.of(
- Permissions.Constants.BOM_CREATE,
- Permissions.Constants.BOM_READ,
- Permissions.Constants.FINDING_READ,
- Permissions.Constants.FINDING_UPDATE,
- Permissions.Constants.POLICY_VIOLATION_CREATE,
- Permissions.Constants.POLICY_VIOLATION_READ,
- Permissions.Constants.POLICY_VIOLATION_UPDATE,
- Permissions.Constants.PROJECT_READ,
- Permissions.Constants.PROJECT_UPDATE),
- "Project Viewer", List.of(
- Permissions.Constants.BADGES_READ,
- Permissions.Constants.BOM_READ,
- Permissions.Constants.PROJECT_READ));
-
- private final Map persistentPermissionByName = new HashMap<>();
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void contextInitialized(final ServletContextEvent event) {
- if (!Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) {
- LOGGER.info("Not populating database with default objects because %s is disabled"
- .formatted(ConfigKey.INIT_TASKS_ENABLED.getPropertyName()));
- return;
- }
-
- // Ensure that this task is only executed by a single instance at once.
- // Wait for lock acquisition rather than simply skipping execution,
- // since application logic may depend on default objects being present.
- final var lockConfig = new WaitingLockConfiguration(
- /* createdAt */ Instant.now(),
- /* name */ getClass().getName(),
- /* lockAtMostFor */ Duration.ofMinutes(5),
- /* lockAtLeastFor */ Duration.ZERO,
- /* pollInterval */ Duration.ofSeconds(1),
- /* waitTimeout */ Duration.ofMinutes(5));
-
- try {
- executeWithLockWaiting(lockConfig, this::executeLocked);
- } catch (Throwable t) {
- if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- // Make absolutely sure that we exit with non-zero code so
- // the container orchestrator knows to restart the container.
- LOGGER.error("Failed to populate database with default objects", t);
- System.exit(1);
- }
-
- throw new RuntimeException("Failed to populate database with default objects", t);
- }
-
- if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- LOGGER.info("Exiting because %s is enabled".formatted(ConfigKey.INIT_AND_EXIT.getPropertyName()));
- System.exit(0);
- }
- }
-
- private void executeLocked() {
- assertLocked();
-
- if (!shouldExecute()) {
- LOGGER.info("Default objects already populated for build %s (timestamp: %s); Skipping".formatted(
- Config.getInstance().getApplicationBuildUuid(),
- Config.getInstance().getApplicationBuildTimestamp()));
- return;
- }
-
- // TODO: Make population transactional with recordDefaultObjectsVersion().
-
- LOGGER.info("Initializing default object generator");
- try (final var qm = new QueryManager()) {
- loadDefaultPermissions(qm);
- loadDefaultPersonas(qm);
- loadDefaultLicenses(qm);
- loadDefaultLicenseGroups(qm);
- loadDefaultRepositories(qm);
- loadDefaultRoles(qm);
- loadDefaultConfigProperties(qm);
- loadDefaultNotificationPublishers(qm);
- recordDefaultObjectsVersion(qm);
- }
-
- LOGGER.info("Ensuring the metrics partitions for today and tomorrow exist.");
- ensureMetricsPartitions();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void contextDestroyed(final ServletContextEvent event) {
- /* Intentionally blank to satisfy interface */
- }
-
- private boolean shouldExecute() {
- try (final var qm = new QueryManager()) {
- final ConfigProperty configProperty = qm.getConfigProperty(
- INTERNAL_DEFAULT_OBJECTS_VERSION.getGroupName(),
- INTERNAL_DEFAULT_OBJECTS_VERSION.getPropertyName());
-
- return configProperty == null
- || configProperty.getPropertyValue() == null
- || !Config.getInstance().getApplicationBuildUuid().equals(configProperty.getPropertyValue());
- }
- }
-
- private void recordDefaultObjectsVersion(final QueryManager qm) {
- qm.runInTransaction(() -> {
- final ConfigProperty configProperty = qm.getConfigProperty(
- INTERNAL_DEFAULT_OBJECTS_VERSION.getGroupName(),
- INTERNAL_DEFAULT_OBJECTS_VERSION.getPropertyName());
-
- configProperty.setPropertyValue(Config.getInstance().getApplicationBuildUuid());
- });
- }
-
- public static void loadDefaultLicenses() {
- try (final var qm = new QueryManager()) {
- loadDefaultLicenses(qm);
- }
- }
-
- /**
- * Loads the default licenses into the database if no license data exists.
- */
- private static void loadDefaultLicenses(final QueryManager qm) {
- LOGGER.info("Synchronizing SPDX license definitions to datastore");
-
- final SpdxLicenseDetailParser parser = new SpdxLicenseDetailParser();
- try {
- final List licenses = parser.getLicenseDefinitions();
- for (final License license : licenses) {
- LOGGER.debug("Synchronizing: " + license.getName());
- qm.synchronizeLicense(license, false);
- }
- } catch (IOException e) {
- LOGGER.error("An error occurred during the parsing SPDX license definitions");
- LOGGER.error(e.getMessage());
- }
- }
-
- /**
- * Loads the default license groups into the database if no license groups exists.
- */
- private void loadDefaultLicenseGroups(final QueryManager qm) {
- final DefaultLicenseGroupImporter importer = new DefaultLicenseGroupImporter(qm);
- if (!importer.shouldImport()) {
- return;
- }
- LOGGER.info("Adding default license group definitions to datastore");
- try {
- importer.loadDefaults();
- } catch (IOException e) {
- LOGGER.error("An error occurred loading default license group definitions");
- LOGGER.error(e.getMessage());
- }
- }
-
- public void loadDefaultPermissions() {
- try (final var qm = new QueryManager()) {
- loadDefaultPermissions(qm);
- }
- }
-
- /**
- * Loads the default permissions
- */
- private void loadDefaultPermissions(final QueryManager qm) {
- LOGGER.info("Synchronizing permissions to datastore");
-
- final List allPermissions = Objects.requireNonNullElse(qm.getPermissions(), Collections.emptyList());
- final Map existing = allPermissions.stream().collect(
- Collectors.toMap(Permission::getName, Function.identity()));
-
- for (final Permissions value : Permissions.values()) {
- final String name = value.name();
-
- LOGGER.debug("Creating permission: " + name);
- persistentPermissionByName.put(name,
- existing.getOrDefault(name, qm.createPermission(name, value.getDescription())));
- }
- }
-
- @SuppressWarnings("unused")
- void loadDefaultPersonas() {
- try (final var qm = new QueryManager()) {
- loadDefaultPersonas(qm);
- }
- }
-
- /**
- * Loads the default users and teams
- */
- private void loadDefaultPersonas(final QueryManager qm) {
- if (!qm.getManagedUsers().isEmpty() && qm.getTeams().getTotal() != 0)
- return;
-
- LOGGER.info("Adding default users and teams to datastore");
-
- LOGGER.debug("Creating user: admin");
- ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost",
- new String(PasswordService.createHash("admin".toCharArray())), true, true, false);
-
- for (var name : DEFAULT_TEAM_PERMISSIONS.keySet()) {
- LOGGER.debug("Creating team: " + name);
- var team = qm.createTeam(name);
-
- LOGGER.debug("Assigning default permissions for team: " + name);
- team.setPermissions(getPermissionsByName(DEFAULT_TEAM_PERMISSIONS.get(name)));
-
- qm.persist(team);
- }
-
- LOGGER.debug("Adding admin user to System Administrators");
- qm.addUserToTeam(admin, qm.getTeam("Administrators"));
-
- admin = qm.getObjectById(ManagedUser.class, admin.getId());
- admin.setPermissions(qm.getPermissions());
- qm.persist(admin);
- }
-
- /**
- * Perform a lookup of {@link Permission}s for specified name(s).
- *
- * @param names permission names
- * @return list of {@link Permission}s
- */
- private List getPermissionsByName(List names) {
- return names.stream().map(persistentPermissionByName::get).filter(Objects::nonNull).toList();
- }
-
- public void loadDefaultRoles() {
- try (final var qm = new QueryManager()) {
- loadDefaultRoles(qm);
- }
- }
-
- /**
- * Loads the default Roles
- */
- private void loadDefaultRoles(final QueryManager qm) {
- if (!qm.getRoles().isEmpty())
- return;
-
- LOGGER.info("Adding default roles to datastore");
-
- for (var name : DEFAULT_ROLE_PERMISSIONS.keySet()) {
- LOGGER.debug("Creating role: " + name);
- qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name)));
- }
- }
-
- public void loadDefaultRepositories() {
- try (final var qm = new QueryManager()) {
- loadDefaultRepositories(qm);
- }
- }
-
- /**
- * Loads the default repositories
- */
- private void loadDefaultRepositories(final QueryManager qm) {
- LOGGER.info("Synchronizing default repositories to datastore");
- // @formatter:off
- qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null);
- qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false, false,null, null);
- qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false, false, null, null);
- qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.CARGO, "crates.io", "https://crates.io", true, false, false, null, null);
- qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", true, false, false, null, null);
- qm.createRepository(RepositoryType.GITHUB, "github.com", "https://github.com", true, false, false, null, null);
- qm.createRepository(RepositoryType.HACKAGE, "hackage.haskell", "https://hackage.haskell.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.NIXPKGS, "nixos.org", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", true, false, false, null, null);
- // @formatter:on
- }
-
- @SuppressWarnings("unused")
- void loadDefaultConfigProperties() {
- try (final var qm = new QueryManager()) {
- loadDefaultConfigProperties(qm);
- }
- }
-
- /**
- * Loads the default ConfigProperty objects
- */
- private void loadDefaultConfigProperties(final QueryManager qm) {
- LOGGER.info("Synchronizing config properties to datastore");
- for (final ConfigPropertyConstants cpc : ConfigPropertyConstants.values()) {
- LOGGER.debug("Creating config property: " + cpc.getGroupName() + " / " + cpc.getPropertyName());
- if (qm.getConfigProperty(cpc.getGroupName(), cpc.getPropertyName()) == null) {
- qm.createConfigProperty(cpc.getGroupName(), cpc.getPropertyName(), cpc.getDefaultPropertyValue(),
- cpc.getPropertyType(), cpc.getDescription());
- }
- }
- }
-
- public void loadDefaultNotificationPublishers() {
- try (final var qm = new QueryManager()) {
- loadDefaultNotificationPublishers(qm);
- }
- }
-
- /**
- * Loads the default notification publishers
- */
- private void loadDefaultNotificationPublishers(final QueryManager qm) {
- LOGGER.info("Synchronizing notification publishers to datastore");
- try {
- NotificationUtil.loadDefaultNotificationPublishers(qm);
- } catch (IOException e) {
- LOGGER.error("An error occurred while synchronizing a default notification publisher", e);
- }
- }
-
- /**
- * Create metrics partitions for today and tomorrow if they don't exist.
- */
- void ensureMetricsPartitions() {
- useJdbiHandle(handle -> {
- var metricsHandle = handle.attach(MetricsDao.class);
- metricsHandle.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString());
- metricsHandle.createMetricsPartitionsForDate(LocalDate.now().plusDays(1).toString(), LocalDate.now().plusDays(2).toString());
- });
- }
-}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java b/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java
deleted file mode 100644
index 05347dcb2c..0000000000
--- a/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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;
-
-import alpine.Config;
-import alpine.common.logging.Logger;
-import alpine.server.util.DbUtil;
-import com.zaxxer.hikari.HikariConfig;
-import com.zaxxer.hikari.HikariDataSource;
-import org.dependencytrack.common.ConfigKey;
-import org.dependencytrack.support.liquibase.MigrationExecutor;
-
-import jakarta.servlet.ServletContextEvent;
-import jakarta.servlet.ServletContextListener;
-import java.sql.Connection;
-import java.util.Optional;
-
-public class MigrationInitializer implements ServletContextListener {
-
- private static final Logger LOGGER = Logger.getLogger(MigrationInitializer.class);
-
- private final Config config;
-
- @SuppressWarnings("unused")
- public MigrationInitializer() {
- this(Config.getInstance());
- }
-
- MigrationInitializer(final Config config) {
- this.config = config;
- }
-
- @Override
- public void contextInitialized(final ServletContextEvent event) {
- if (!config.getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) {
- LOGGER.debug("Not running migrations because %s is disabled"
- .formatted(ConfigKey.INIT_TASKS_ENABLED.getPropertyName()));
- return;
- }
- if (!config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS)) {
- LOGGER.debug("Not running migrations because %s is disabled"
- .formatted(ConfigKey.DATABASE_RUN_MIGRATIONS.getPropertyName()));
- return;
- }
-
- LOGGER.info("Running migrations");
- try (final HikariDataSource dataSource = createDataSource()) {
- try (final Connection connection = dataSource.getConnection()) {
- // Ensure that DbUtil#isPostgreSQL will work as expected.
- // Some legacy code ported over from v4 still uses this.
- //
- // NB: This was previously done in alpine.server.upgrade.UpgradeExecutor.
- //
- // TODO: Remove once DbUtil#isPostgreSQL is no longer used.
- DbUtil.initPlatformName(connection);
- }
-
- new MigrationExecutor(dataSource, "migration/changelog-main.xml").executeMigration();
- } catch (Exception e) {
- if (config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS_ONLY)
- || config.getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- // Make absolutely sure that we exit with non-zero code so
- // the container orchestrator knows to restart the container.
- LOGGER.error("Failed to execute migrations", e);
- System.exit(1);
- }
-
- throw new RuntimeException("Failed to execute migrations", e);
- }
-
- if (config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS_ONLY)) {
- LOGGER.info("Exiting because %s is enabled".formatted(ConfigKey.DATABASE_RUN_MIGRATIONS.getPropertyName()));
- System.exit(0);
- }
- }
-
- private HikariDataSource createDataSource() {
- final String jdbcUrl = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_URL))
- .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_URL));
- final String username = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_USERNAME))
- .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_USERNAME));
- final String password = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_PASSWORD))
- .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_PASSWORD));
-
- final var hikariCfg = new HikariConfig();
- hikariCfg.setJdbcUrl(jdbcUrl);
- hikariCfg.setDriverClassName(config.getProperty(Config.AlpineKey.DATABASE_DRIVER));
- hikariCfg.setUsername(username);
- hikariCfg.setPassword(password);
- hikariCfg.setMaximumPoolSize(1);
- hikariCfg.setMinimumIdle(1);
-
- return new HikariDataSource(hikariCfg);
- }
-}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java b/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java
deleted file mode 100644
index a52727c63c..0000000000
--- a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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.defaults;
-
-import alpine.common.logging.Logger;
-import org.dependencytrack.model.License;
-import org.dependencytrack.model.LicenseGroup;
-import org.dependencytrack.persistence.QueryManager;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URLDecoder;
-import java.util.ArrayList;
-import java.util.List;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * Imports default LicenseGroup objects into the datastore.
- *
- * @author Steve Springett
- * @since 4.0.0
- */
-public class DefaultLicenseGroupImporter implements IDefaultObjectImporter {
-
- private static final Logger LOGGER = Logger.getLogger(DefaultLicenseGroupImporter.class);
-
- private QueryManager qm;
-
- public DefaultLicenseGroupImporter(final QueryManager qm) {
- this.qm = qm;
- }
-
- public boolean shouldImport() {
- if (qm.getLicenseGroups().getTotal() > 0) {
- return false;
- }
- return true;
- }
-
- public void loadDefaults() throws IOException {
- final File defaultsFile = new File(URLDecoder.decode(getClass().getProtectionDomain().getCodeSource().getLocation().getPath(), UTF_8.name()) + "default-objects/licenseGroups.json");
- final JSONArray licenseGroups = readFile(defaultsFile);
- for (int i = 0; i < licenseGroups.length(); i++) {
- final JSONObject json = licenseGroups.getJSONObject(i);
- final LicenseGroup licenseGroup = new LicenseGroup();
- licenseGroup.setName(json.getString("name"));
- licenseGroup.setRiskWeight(json.getInt("riskWeight"));
- LOGGER.debug("Adding " + licenseGroup.getName());
- final List licenses = new ArrayList<>();
- for (int k=0; k T getValue(final ConfigPropertyConstants property, final Class cl
return getOptionalValue(property, clazz).orElseThrow(NoSuchElementException::new);
}
+ @SqlUpdate("""
+ UPDATE "CONFIGPROPERTY"
+ SET "PROPERTYVALUE" = :value
+ WHERE "GROUPNAME" = :groupName
+ AND "PROPERTYNAME" = :propertyName
+ """)
+ void setValue(@BindBean ConfigPropertyConstants property, @Bind String value);
+
}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java
index 25b387ed53..916eff5952 100644
--- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java
@@ -154,6 +154,10 @@ public static Jdbi createLocalJdbi(final QueryManager qm) {
return createLocalJdbi(qm.getPersistenceManager());
}
+ public static Jdbi createLocalJdbi(final DataSource dataSource) {
+ return customizeJdbi(Jdbi.create(dataSource));
+ }
+
private static Jdbi createLocalJdbi(final PersistenceManager pm) {
if (!pm.currentTransaction().isActive()) {
throw new IllegalStateException("""
diff --git a/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java b/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java
index 5449f8f313..5751f325ff 100644
--- a/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java
+++ b/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java
@@ -18,9 +18,7 @@
*/
package org.dependencytrack.plugin;
-import alpine.Config;
import alpine.common.logging.Logger;
-import org.dependencytrack.common.ConfigKey;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
@@ -36,12 +34,6 @@ public class PluginInitializer implements ServletContextListener {
@Override
public void contextInitialized(final ServletContextEvent event) {
- if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- LOGGER.debug("Not loading plugins because %s is enabled"
- .formatted(ConfigKey.INIT_AND_EXIT.getPropertyName()));
- return;
- }
-
LOGGER.info("Loading plugins");
pluginManager.loadPlugins();
}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java
index 172f8aa14f..cdca6a24b2 100644
--- a/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java
@@ -19,7 +19,6 @@
package org.dependencytrack.resources.v1;
import alpine.common.logging.Logger;
-import alpine.model.ConfigProperty;
import alpine.notification.Notification;
import alpine.server.auth.PermissionRequired;
import alpine.server.resources.AlpineResource;
@@ -33,32 +32,36 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.validation.Validator;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.DELETE;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.PUT;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
-import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.model.NotificationPublisher;
import org.dependencytrack.model.NotificationRule;
import org.dependencytrack.model.validation.ValidUuid;
import org.dependencytrack.notification.NotificationConstants;
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.publisher.PublisherClass;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.dependencytrack.persistence.QueryManager;
+import org.dependencytrack.persistence.jdbi.ConfigPropertyDao;
import org.dependencytrack.util.NotificationUtil;
+import jakarta.validation.Validator;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.List;
+import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
/**
* JAX-RS resources for processing notification publishers.
*
@@ -262,21 +265,14 @@ public Response deleteNotificationPublisher(@Parameter(description = "The UUID o
})
@PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION)
public Response restoreDefaultTemplates() {
- try (QueryManager qm = new QueryManager()) {
- return qm.callInTransaction(() -> {
- final ConfigProperty property = qm.getConfigProperty(
- ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getGroupName(),
- ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyName()
- );
- property.setPropertyValue("false");
- qm.persist(property);
- NotificationUtil.loadDefaultNotificationPublishers(qm);
- return Response.ok().build();
- });
- } catch (Exception exception) {
- LOGGER.error(exception.getMessage(), exception);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Exception occured while restoring default notification publisher templates.").build();
- }
+ useJdbiTransaction(handle -> {
+ handle.attach(ConfigPropertyDao.class).setValue(
+ NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED, "false");
+
+ DatabaseSeedingInitTask.seedDefaultNotificationPublishers(handle);
+ });
+
+ return Response.ok().build();
}
@POST
diff --git a/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java b/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java
new file mode 100644
index 0000000000..ebd3dc067f
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java
@@ -0,0 +1,48 @@
+/*
+ * 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.security;
+
+import alpine.security.crypto.KeyManager;
+import org.dependencytrack.init.InitTask;
+import org.dependencytrack.init.InitTaskContext;
+
+/**
+ * @since 5.6.0
+ */
+public class KeyGenerationInitTask implements InitTask {
+
+ @Override
+ public int priority() {
+ return PRIORITY_HIGHEST - 5;
+ }
+
+ @Override
+ public String name() {
+ return "key.generation";
+ }
+
+ @Override
+ public void execute(final InitTaskContext ctx) throws Exception {
+ // Force initialization of KeyManager, which will cause
+ // the secret, as well as the public-private key pair
+ // to be generated if necessary.
+ final var ignored = KeyManager.getInstance();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java b/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java
index 6d5bd22c2d..ee5c4d7731 100644
--- a/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java
+++ b/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java
@@ -18,18 +18,14 @@
*/
package org.dependencytrack.util;
-import alpine.model.ConfigProperty;
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
-import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
import org.dependencytrack.model.Analysis;
import org.dependencytrack.model.AnalysisState;
import org.dependencytrack.model.Bom;
import org.dependencytrack.model.Component;
-import org.dependencytrack.model.ConfigPropertyConstants;
-import org.dependencytrack.model.NotificationPublisher;
import org.dependencytrack.model.Policy;
import org.dependencytrack.model.PolicyCondition;
import org.dependencytrack.model.PolicyViolation;
@@ -44,7 +40,6 @@
import org.dependencytrack.notification.NotificationConstants;
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.NotificationScope;
-import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
import org.dependencytrack.notification.vo.AnalysisDecisionChange;
import org.dependencytrack.notification.vo.BomConsumedOrProcessed;
import org.dependencytrack.notification.vo.BomProcessingFailed;
@@ -58,10 +53,6 @@
import javax.jdo.FetchPlan;
import javax.jdo.Query;
-import java.io.File;
-import java.io.IOException;
-import java.net.URLDecoder;
-import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
@@ -72,8 +63,6 @@
import java.util.UUID;
import java.util.stream.Collectors;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
public final class NotificationUtil {
/**
@@ -319,39 +308,6 @@ public static void analyzeNotificationCriteria(final QueryManager qm, final Long
.subject(new PolicyViolationIdentified(violation, component, project)));
}
- public static void loadDefaultNotificationPublishers(QueryManager qm) throws IOException {
- for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) {
- File templateFile = new File(URLDecoder.decode(NotificationUtil.class.getResource(publisher.getPublisherTemplateFile()).getFile(), UTF_8.name()));
- if (qm.isEnabled(ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED)) {
- ConfigProperty templateBaseDir = qm.getConfigProperty(
- ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR.getGroupName(),
- ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR.getPropertyName()
- );
- File userProvidedTemplateFile = new File(Path.of(templateBaseDir.getPropertyValue(), publisher.getPublisherTemplateFile()).toUri());
- if (userProvidedTemplateFile.exists()) {
- templateFile = userProvidedTemplateFile;
- }
- }
- final String templateContent = FileUtils.readFileToString(templateFile, UTF_8);
- final NotificationPublisher existingPublisher = qm.getDefaultNotificationPublisherByName(publisher.getPublisherName());
- if (existingPublisher == null) {
- qm.createNotificationPublisher(
- publisher.getPublisherName(), publisher.getPublisherDescription(),
- publisher.getPublisherClass().name(), templateContent, publisher.getTemplateMimeType(),
- publisher.isDefaultPublisher()
- );
- } else {
- existingPublisher.setName(publisher.getPublisherName());
- existingPublisher.setDescription(publisher.getPublisherDescription());
- existingPublisher.setPublisherClass(publisher.getPublisherClass().name());
- existingPublisher.setTemplate(templateContent);
- existingPublisher.setTemplateMimeType(publisher.getTemplateMimeType());
- existingPublisher.setDefaultPublisher(publisher.isDefaultPublisher());
- qm.updateNotificationPublisher(existingPublisher);
- }
- }
- }
-
public static String generateNotificationContent(final org.dependencytrack.proto.notification.v1.Vulnerability vulnerability) {
final String content;
if (vulnerability.hasDescription()) {
diff --git a/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask b/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask
new file mode 100644
index 0000000000..c7826402b6
--- /dev/null
+++ b/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask
@@ -0,0 +1,4 @@
+org.dependencytrack.persistence.DatabaseMigrationInitTask
+org.dependencytrack.persistence.DatabasePartitionMaintenanceInitTask
+org.dependencytrack.persistence.DatabaseSeedingInitTask
+org.dependencytrack.security.KeyGenerationInitTask
\ No newline at end of file
diff --git a/apiserver/src/main/resources/application.properties b/apiserver/src/main/resources/application.properties
index 096ec7e77a..79cca2a77a 100644
--- a/apiserver/src/main/resources/application.properties
+++ b/apiserver/src/main/resources/application.properties
@@ -156,27 +156,7 @@ alpine.datanucleus.executioncontext.maxidle=0
# @hidden
alpine.datanucleus.deletionpolicy=DataNucleus
-# Defines whether database migrations should be executed on startup.
-#
-# From v5.6.0 onwards, migrations are considered part of the initialization tasks.
-# Setting init.tasks.enabled to `false` will disable migrations,
-# even if database.run.migrations is enabled.
-#
-# @category: Database
-# @type: boolean
-database.run.migrations=true
-
-# Defines whether the application should exit upon successful execution of database migrations.
-# Enabling this option makes the application suitable for running as k8s init container.
-# Has no effect unless database.run.migrations is `true`.
-#
-# From v5.6.0 onwards, usage of init.and.exit should be preferred.
-#
-# @category: Database
-# @type: boolean
-# database.run.migrations.only=false
-
-# Defines the database JDBC URL to use when executing migrations.
+# Defines the database JDBC URL to use when executing init tasks.
# If not set, the value of alpine.database.url will be used.
# Should generally not be set, unless TLS authentication is used,
# and custom connection variables are required.
@@ -184,23 +164,23 @@ database.run.migrations=true
# @category: Database
# @default: ${alpine.database.url}
# @type: string
-# database.migration.url=
+# init.tasks.database.url=
-# Defines the database user for executing migrations.
+# Defines the database user for executing init tasks.
# If not set, the value of alpine.database.username will be used.
#
# @category: Database
# @default: ${alpine.database.username}
# @type: string
-# database.migration.username=
+# init.tasks.database.username=
-# Defines the database password for executing migrations.
+# Defines the database password for executing init tasks.
# If not set, the value of alpine.database.password will be used.
#
# @category: Database
# @default: ${alpine.database.password}
# @type: string
-# database.migration.password=
+# init.tasks.database.password=
# Specifies the number of bcrypt rounds to use when hashing a user's password.
# The higher the number the more secure the password, at the expense of
@@ -1228,16 +1208,46 @@ vulnerability.policy.s3.bundle.name=
vulnerability.policy.s3.region=
# Whether to execute initialization tasks on startup.
-# Initialization tasks include:
-#
-# - Execution of database migrations
-# - Populating the database with default objects (permissions, users, licenses, etc.)
-#
#
# @category: General
# @type: boolean
init.tasks.enabled=true
+# Whether to enable the database migration init task.
+#
+# Has no effect unless init.tasks.enabled is `true`.
+#
+# @category: General
+# @type: boolean
+init.task.database.migration.enabled=true
+
+# Whether to enable the database partition maintenance init task.
+#
+# Has no effect unless init.tasks.enabled is `true`.
+#
+# @category: General
+# @type: boolean
+init.task.database.partition.maintenance.enabled=true
+
+# Whether to enable the database seeding init task.
+#
+# Seeding involves populating the database with default objects,
+# such as permissions, users, licenses, etc.
+#
+# Has no effect unless init.tasks.enabled is `true`.
+#
+# @category: General
+# @type: boolean
+init.task.database.seeding.enabled=true
+
+# Whether to enable the key generation init task.
+#
+# Has no effect unless init.tasks.enabled is `true`.
+#
+# @category: General
+# @type: boolean
+init.task.key.generation.enabled=true
+
# Whether to only execute initialization tasks and exit.
#
# @category: General
diff --git a/apiserver/src/main/webapp/WEB-INF/web.xml b/apiserver/src/main/webapp/WEB-INF/web.xml
index 052fe67a42..ed671bd089 100644
--- a/apiserver/src/main/webapp/WEB-INF/web.xml
+++ b/apiserver/src/main/webapp/WEB-INF/web.xml
@@ -30,10 +30,7 @@
alpine.server.metrics.MetricsInitializer
- org.dependencytrack.common.KeyManagerInitializer
-
-
- org.dependencytrack.persistence.MigrationInitializer
+ org.dependencytrack.init.InitTaskServletContextListener
alpine.server.persistence.PersistenceManagerFactory
@@ -44,9 +41,6 @@
org.dependencytrack.health.HealthCheckInitializer
-
- org.dependencytrack.persistence.DefaultObjectGenerator
-
org.dependencytrack.event.kafka.KafkaProducerInitializer
diff --git a/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java b/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java
new file mode 100644
index 0000000000..0ac943fae0
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.init;
+
+import alpine.Config;
+import org.dependencytrack.PersistenceCapableTest;
+import org.junit.Before;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.Mockito.mock;
+
+public class InitTaskExecutorTest extends PersistenceCapableTest {
+
+ private Config configMock;
+ private PGSimpleDataSource dataSource;
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ configMock = mock(Config.class);
+
+ dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgresContainer.getJdbcUrl());
+ dataSource.setUser(postgresContainer.getUsername());
+ dataSource.setPassword(postgresContainer.getPassword());
+ }
+
+ @Test
+ public void shouldExecuteTasksInPriorityOrder() {
+ final var executedTaskNames = new ArrayList(3);
+
+ final var executor = new InitTaskExecutor(configMock, dataSource, List.of(
+ new TestInitTask(1, "a", () -> executedTaskNames.add("a")),
+ new TestInitTask(5, "b", () -> executedTaskNames.add("b")),
+ new TestInitTask(3, "c", () -> executedTaskNames.add("c"))));
+ executor.execute();
+
+ assertThat(executedTaskNames).containsExactly("b", "c", "a");
+ }
+
+ @Test
+ public void shouldThrowWhenTaskExecutionFails() {
+ final var executor = new InitTaskExecutor(configMock, dataSource, List.of(
+ new TestInitTask(1, "test", () -> {
+ throw new IllegalStateException("boom");
+ })));
+
+ assertThatExceptionOfType(IllegalStateException.class)
+ .isThrownBy(executor::execute)
+ .withMessage("Failed to execute init task test")
+ .withCauseInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void shouldThrowOnDuplicateTaskName() {
+ final var executor = new InitTaskExecutor(configMock, dataSource, List.of(
+ new TestInitTask(1, "test"),
+ new TestInitTask(2, "test")));
+
+ assertThatExceptionOfType(IllegalStateException.class)
+ .isThrownBy(executor::execute)
+ // NB: In a real-world scenario, the class names would be different.
+ .withMessage("""
+ Duplicate task name test: Registered by \
+ org.dependencytrack.init.InitTaskExecutorTest$TestInitTask and \
+ org.dependencytrack.init.InitTaskExecutorTest$TestInitTask""");
+ }
+
+ @Test
+ public void shouldThrowOnInvalidTaskPriority() {
+ final var executor = new InitTaskExecutor(configMock, dataSource, List.of(
+ new TestInitTask(-1, "test")));
+
+ assertThatExceptionOfType(IllegalStateException.class)
+ .isThrownBy(executor::execute)
+ .withMessage("Invalid priority of task test: Must be within [0..100] but is -1");
+ }
+
+ private static final class TestInitTask implements InitTask {
+
+ private final int priority;
+ private final String name;
+ private final Runnable runnable;
+
+ private TestInitTask(final int priority, final String name) {
+ this(priority, name, null);
+ }
+
+ private TestInitTask(final int priority, final String name, final Runnable runnable) {
+ this.priority = priority;
+ this.name = name;
+ this.runnable = runnable;
+ }
+
+ @Override
+ public int priority() {
+ return priority;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public void execute(final InitTaskContext ctx) {
+ if (runnable != null) {
+ runnable.run();
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java b/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java
new file mode 100644
index 0000000000..aab5ea215a
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.model;
+
+import org.assertj.core.api.SoftAssertions;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class DefaultRepositoryTest {
+
+ @Test
+ public void shouldNotHaveDuplicateResolutionOrdersPerType() {
+ final Map> defaultRepoByType =
+ Arrays.stream(DefaultRepository.values()).collect(
+ Collectors.groupingBy(
+ DefaultRepository::getType,
+ Collectors.mapping(
+ DefaultRepository::getResolutionOrder,
+ Collectors.toList())));
+
+ final var softAsserts = new SoftAssertions();
+ for (final RepositoryType type : RepositoryType.values()) {
+ final List defaultRepos = defaultRepoByType.get(type);
+ if (defaultRepos != null) {
+ softAsserts.assertThat(defaultRepos).doesNotHaveDuplicates();
+ }
+ }
+
+ softAsserts.assertAll();
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java
similarity index 56%
rename from apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java
rename to apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java
index d0636b9d84..526859115a 100644
--- a/apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java
@@ -20,10 +20,12 @@
import alpine.Config;
import org.dependencytrack.common.ConfigKey;
+import org.dependencytrack.init.InitTaskContext;
import org.jdbi.v3.core.Jdbi;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
@@ -34,9 +36,10 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-public class MigrationInitializerTest {
+public class DatabaseMigrationInitTaskTest {
private PostgreSQLContainer> postgresContainer;
+ private PGSimpleDataSource dataSource;
private Jdbi jdbi;
@Before
@@ -44,11 +47,12 @@ public void setUp() {
postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:13-alpine"));
postgresContainer.start();
- jdbi = Jdbi.create(
- postgresContainer.getJdbcUrl(),
- postgresContainer.getUsername(),
- postgresContainer.getPassword()
- );
+ dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgresContainer.getJdbcUrl());
+ dataSource.setUser(postgresContainer.getUsername());
+ dataSource.setPassword(postgresContainer.getPassword());
+
+ jdbi = Jdbi.create(dataSource);
}
@After
@@ -59,67 +63,35 @@ public void tearDown() {
}
@Test
- public void test() {
+ public void test() throws Exception {
final var configMock = mock(Config.class);
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword());
when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true);
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true);
- new MigrationInitializer(configMock).contextInitialized(null);
+ new DatabaseMigrationInitTask().execute(new InitTaskContext(configMock, dataSource));
assertMigrationExecuted(/* expectExecuted */ true);
}
@Test
- public void testWithMigrationCredentials() {
+ public void testWithMigrationCredentials() throws Exception {
final var configMock = mock(Config.class);
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn("username");
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn("password");
when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true);
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true);
- when(configMock.getProperty(eq(ConfigKey.DATABASE_MIGRATION_USERNAME))).thenReturn(postgresContainer.getUsername());
- when(configMock.getProperty(eq(ConfigKey.DATABASE_MIGRATION_PASSWORD))).thenReturn(postgresContainer.getPassword());
+ when(configMock.getProperty(eq(ConfigKey.INIT_TASKS_DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername());
+ when(configMock.getProperty(eq(ConfigKey.INIT_TASKS_DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword());
- new MigrationInitializer(configMock).contextInitialized(null);
+ new DatabaseMigrationInitTask().execute(new InitTaskContext(configMock, dataSource));
assertMigrationExecuted(/* expectExecuted */ true);
}
- @Test
- public void testWithRunMigrationsDisabled() {
- final var configMock = mock(Config.class);
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword());
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true);
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(false);
-
- new MigrationInitializer(configMock).contextInitialized(null);
-
- assertMigrationExecuted(/* expectExecuted */ false);
- }
-
- @Test
- public void testWithInitTasksDisabled() {
- final var configMock = mock(Config.class);
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword());
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(false);
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true);
-
- new MigrationInitializer(configMock).contextInitialized(null);
-
- assertMigrationExecuted(/* expectExecuted */ false);
- }
-
private void assertMigrationExecuted(final boolean expectExecuted) {
final List tableNames = jdbi.withHandle(handle -> handle.createQuery("""
SELECT "table_name"
@@ -136,4 +108,4 @@ private void assertMigrationExecuted(final boolean expectExecuted) {
}
}
-}
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java
new file mode 100644
index 0000000000..47fd057387
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import alpine.Config;
+import org.dependencytrack.PersistenceCapableTest;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.persistence.jdbi.MetricsDao;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle;
+import static org.mockito.Mockito.mock;
+
+public class DatabasePartitionMaintenanceInitTaskTest extends PersistenceCapableTest {
+
+ @Test
+ public void testMetricsPartitionsForTodayAndTomorrow() throws Exception {
+ final var dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgresContainer.getJdbcUrl());
+ dataSource.setUser(postgresContainer.getUsername());
+ dataSource.setPassword(postgresContainer.getPassword());
+
+ new DatabasePartitionMaintenanceInitTask().execute(new InitTaskContext(mock(Config.class), dataSource));
+
+ var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
+ var tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE);
+
+ useJdbiHandle(handle -> {
+ var metricsDao = handle.attach(MetricsDao.class);
+ assertThat(Collections.frequency(metricsDao.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(today))).isEqualTo(1);
+ assertThat(Collections.frequency(metricsDao.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
+ assertThat(Collections.frequency(metricsDao.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(today))).isEqualTo(1);
+ assertThat(Collections.frequency(metricsDao.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java
new file mode 100644
index 0000000000..8a7b23edf2
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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;
+
+import alpine.Config;
+import alpine.model.ConfigProperty;
+import alpine.model.ManagedUser;
+import alpine.model.Permission;
+import alpine.model.Team;
+import org.dependencytrack.PersistenceCapableTest;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.model.ConfigPropertyConstants;
+import org.dependencytrack.model.DefaultRepository;
+import org.dependencytrack.model.License;
+import org.dependencytrack.model.LicenseGroup;
+import org.dependencytrack.model.NotificationPublisher;
+import org.dependencytrack.model.Repository;
+import org.dependencytrack.model.Role;
+import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
+import org.junit.Before;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class DatabaseSeedingInitTaskTest extends PersistenceCapableTest {
+
+ private Config configMock;
+ private PGSimpleDataSource dataSource;
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ configMock = mock(Config.class);
+ doReturn("25b88cc9-ff96-4ec5-9921-6212e954a46f").when(configMock).getApplicationBuildUuid();
+ doReturn("1970-01-01 00:00:00").when(configMock).getApplicationBuildTimestamp();
+
+ dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgresContainer.getJdbcUrl());
+ dataSource.setUser(postgresContainer.getUsername());
+ dataSource.setPassword(postgresContainer.getPassword());
+ }
+
+ @Test
+ public void test() throws Exception {
+ new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource));
+
+ final List configProperties = qm.getConfigProperties();
+ assertThat(configProperties).hasSize(ConfigPropertyConstants.values().length);
+ assertThat(configProperties).allSatisfy(property -> {
+ assertThat(property.getGroupName()).isNotBlank();
+ assertThat(property.getPropertyName()).isNotBlank();
+ assertThat(property.getPropertyType()).isNotNull();
+ assertThat(property.getDescription()).isNotNull();
+ });
+ assertThat(configProperties).anySatisfy(property -> assertThat(property.getPropertyValue()).isNotBlank());
+
+ final List permissions = qm.getPermissions();
+ assertThat(permissions).hasSize(Permissions.values().length);
+ assertThat(permissions).allSatisfy(permission -> {
+ assertThat(permission.getName()).isNotBlank();
+ assertThat(permission.getDescription()).isNotBlank();
+ });
+
+ final List teams = qm.getTeams().getList(Team.class);
+ assertThat(teams).isNotEmpty();
+ assertThat(teams).allSatisfy(team -> {
+ assertThat(team.getName()).isNotBlank();
+ assertThat(team.getUuid()).isNotNull();
+ assertThat(team.getPermissions()).isNotEmpty();
+ });
+
+ final List roles = qm.getRoles();
+ assertThat(roles).isNotEmpty();
+ assertThat(roles).allSatisfy(role -> {
+ assertThat(role.getName()).isNotBlank();
+ assertThat(role.getUuid()).isNotNull();
+ assertThat(role.getPermissions()).isNotEmpty();
+ });
+
+ final List users = qm.getManagedUsers();
+ assertThat(users).satisfiesExactly(user -> {
+ assertThat(user.getUsername()).isEqualTo("admin");
+ assertThat(user.getEmail()).isEqualTo("admin@localhost");
+ assertThat(user.getPassword()).isNotBlank();
+ assertThat(user.getLastPasswordChange()).isNotNull();
+ assertThat(user.isForcePasswordChange()).isTrue();
+ assertThat(user.isNonExpiryPassword()).isTrue();
+ assertThat(user.isSuspended()).isFalse();
+ assertThat(user.getPermissions()).hasSize(Permissions.values().length);
+ assertThat(user.getTeams()).extracting(Team::getName).containsOnly("Administrators");
+ });
+
+ final List licenses = qm.getLicenses().getList(License.class);
+ assertThat(licenses).isNotEmpty();
+ assertThat(licenses).allSatisfy(license -> {
+ assertThat(license.getLicenseId()).isNotBlank();
+ assertThat(license.getName()).isNotBlank();
+ assertThat(license.getUuid()).isNotNull();
+ });
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getHeader()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getHeader()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getText()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getTemplate()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getComment()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getSeeAlso()).isNotEmpty());
+
+ final List licenseGroups = qm.getLicenseGroups().getList(LicenseGroup.class);
+ assertThat(licenseGroups).isNotEmpty();
+ assertThat(licenseGroups).allSatisfy(licenseGroup -> {
+ assertThat(licenseGroup.getName()).isNotBlank();
+ assertThat(licenseGroup.getUuid()).isNotNull();
+ assertThat(licenseGroup.getLicenses()).isNotEmpty();
+ });
+
+ final List notificationPublishers = qm.getAllNotificationPublishers();
+ assertThat(notificationPublishers).hasSize(DefaultNotificationPublishers.values().length);
+ assertThat(notificationPublishers).allSatisfy(notificationPublisher -> {
+ assertThat(notificationPublisher.getName()).isNotBlank();
+ assertThat(notificationPublisher.getPublisherClass()).isNotBlank();
+ assertThat(notificationPublisher.getDescription()).isNotBlank();
+ assertThat(notificationPublisher.getTemplate()).isNotBlank();
+ assertThat(notificationPublisher.getTemplateMimeType()).isNotBlank();
+ assertThat(notificationPublisher.isDefaultPublisher()).isTrue();
+ });
+
+ final List repositories = qm.getRepositories().getList(Repository.class);
+ assertThat(repositories).hasSize(DefaultRepository.values().length);
+ assertThat(repositories).allSatisfy(repository -> {
+ assertThat(repository.getType()).isNotNull();
+ assertThat(repository.getIdentifier()).isNotBlank();
+ assertThat(repository.getUrl()).isNotBlank();
+ assertThat(repository.getResolutionOrder()).isNotZero();
+ assertThat(repository.isEnabled()).isTrue();
+ assertThat(repository.getUuid()).isNotNull();
+ });
+ }
+
+ @Test
+ public void testWithDefaultObjectsAlreadyPopulated() throws Exception {
+ new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource));
+
+ List licenses = qm.getLicenses().getList(License.class);
+ assertThat(licenses).isNotEmpty();
+
+ qm.delete(licenses);
+
+ new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource));
+
+ // Default objects must not have been populated again, since their
+ // version is already current for this application build.
+ licenses = qm.getLicenses().getList(License.class);
+ assertThat(licenses).isEmpty();
+ }
+
+ @Test
+ public void testLoadDefaultLicensesUpdatesExistingLicenses() throws Exception {
+ final var license = new License();
+ license.setLicenseId("LGPL-2.1+");
+ license.setName("name");
+ license.setComment("comment");
+ license.setHeader("header");
+ license.setSeeAlso("seeAlso");
+ license.setTemplate("template");
+ license.setText("text");
+ qm.persist(license);
+
+ new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource));
+
+ qm.getPersistenceManager().refresh(license);
+ assertThat(license.getLicenseId()).isEqualTo("LGPL-2.1+");
+ assertThat(license.getName()).isEqualTo("GNU Lesser General Public License v2.1 or later");
+ assertThat(license.getComment()).isNotEqualTo("comment");
+ assertThat(license.getHeader()).isNotEqualTo("header");
+ assertThat(license.getSeeAlso()).isNotEqualTo(new String[]{"seeAlso"});
+ assertThat(license.getTemplate()).isNotEqualTo("template");
+ assertThat(license.getText()).isNotEqualTo("text");
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java
deleted file mode 100644
index e88e737466..0000000000
--- a/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * 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;
-
-import org.dependencytrack.PersistenceCapableTest;
-import org.dependencytrack.auth.Permissions;
-import org.dependencytrack.common.ConfigKey;
-import org.dependencytrack.model.ConfigPropertyConstants;
-import org.dependencytrack.model.License;
-import org.dependencytrack.model.Repository;
-import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
-import org.dependencytrack.persistence.jdbi.MetricsDao;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.contrib.java.lang.system.EnvironmentVariables;
-
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.Collections;
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle;
-
-public class DefaultObjectGeneratorTest extends PersistenceCapableTest {
-
- @Rule
- public EnvironmentVariables environmentVariables = new EnvironmentVariables();
-
- @Test
- public void testContextInitialized() throws Exception {
- testLoadDefaultPermissions();
- testLoadDefaultPersonas();
- testLoadDefaultLicenses();
- testLoadDefaultRepositories();
- testLoadDefaultConfigProperties();
- testLoadDefaultNotificationPublishers();
- }
-
- @Test
- public void testWithInitTasksDisabled() {
- environmentVariables.set(ConfigKey.INIT_TASKS_ENABLED.name(), "false");
-
- new DefaultObjectGenerator().contextInitialized(null);
-
- assertThat(qm.getPermissions()).isEmpty();
- assertThat(qm.getManagedUsers()).isEmpty();
- assertThat(qm.getLicenses().getList(License.class)).isEmpty();
- assertThat(qm.getRepositories().getList(Repository.class)).isEmpty();
- assertThat(qm.getConfigProperties()).isEmpty();
- assertThat(qm.getAllNotificationPublishers()).isEmpty();
- }
-
- @Test
- public void testWithDefaultObjectsAlreadyPopulated() {
- new DefaultObjectGenerator().contextInitialized(null);
-
- List licenses = qm.getLicenses().getList(License.class);
- assertThat(licenses).isNotEmpty();
-
- qm.delete(licenses);
-
- new DefaultObjectGenerator().contextInitialized(null);
-
- // Default objects must not have been populated again, since their
- // version is already current for this application build.
- licenses = qm.getLicenses().getList(License.class);
- assertThat(licenses).isEmpty();
- }
-
- @Test
- public void testLoadDefaultLicenses() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultLicenses();
- Assert.assertEquals(738, qm.getAllLicensesConcise().size());
- }
-
- @Test
- public void testLoadDefaultLicensesUpdatesExistingLicenses() throws Exception {
- final var license = new License();
- license.setLicenseId("LGPL-2.1+");
- license.setName("name");
- license.setComment("comment");
- license.setHeader("header");
- license.setSeeAlso("seeAlso");
- license.setTemplate("template");
- license.setText("text");
- qm.persist(license);
-
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultLicenses();
-
- qm.getPersistenceManager().refresh(license);
- assertThat(license.getLicenseId()).isEqualTo("LGPL-2.1+");
- assertThat(license.getName()).isEqualTo("GNU Lesser General Public License v2.1 or later");
- assertThat(license.getComment()).isNotEqualTo("comment");
- assertThat(license.getHeader()).isNotEqualTo("header");
- assertThat(license.getSeeAlso()).isNotEqualTo(new String[]{"seeAlso"});
- assertThat(license.getTemplate()).isNotEqualTo("template");
- assertThat(license.getText()).isNotEqualTo("text");
- }
-
- @Test
- public void testLoadDefaultPermissions() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
- Assert.assertEquals(Permissions.values().length, qm.getPermissions().size());
- }
-
- @Test
- public void testLoadDefaultPersonas() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultPersonas();
- Assert.assertEquals(4, qm.getTeams().getTotal());
- }
-
- @Test
- public void testLoadDefaultRepositories() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultRepositories();
- Assert.assertEquals(17, qm.getAllRepositories().size());
- }
-
- @Test
- public void testLoadDefaultConfigProperties() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultConfigProperties();
- Assert.assertEquals(ConfigPropertyConstants.values().length, qm.getConfigProperties().size());
- }
-
- @Test
- public void testLoadDefaultNotificationPublishers() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultNotificationPublishers();
- Assert.assertEquals(DefaultNotificationPublishers.values().length, qm.getAllNotificationPublishers().size());
- }
-
- @Test
- public void testMetricsPartitionsForTodayAndTomorrow() {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.ensureMetricsPartitions();
- var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
- var tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE);
- withJdbiHandle(handle -> {
- var metricsHandle = handle.attach(MetricsDao.class);
- assertThat(Collections.frequency(metricsHandle.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(today))).isEqualTo(1);
- assertThat(Collections.frequency(metricsHandle.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
- assertThat(Collections.frequency(metricsHandle.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(today))).isEqualTo(1);
- assertThat(Collections.frequency(metricsHandle.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
- return null;
- });
- }
-}
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java
index c0dbfe64f7..7e92a5a370 100644
--- a/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java
@@ -23,20 +23,22 @@
import org.junit.Assert;
import org.junit.Test;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
public class NotificationQueryManagerTest extends PersistenceCapableTest {
@Test
public void testGetNotificationPublisher() {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.contextInitialized(null);
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers);
+
var publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName());
Assert.assertEquals("SlackPublisher", publisher.getPublisherClass());
}
@Test
public void testGetDefaultNotificationPublisher() {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.contextInitialized(null);
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers);
+
var publisher = qm.getDefaultNotificationPublisherByName(DefaultNotificationPublishers.SLACK.getPublisherName());
Assert.assertEquals("Slack", publisher.getName());
Assert.assertEquals("SlackPublisher", publisher.getPublisherClass());
diff --git a/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java b/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java
index 65f589d6b1..45777105fe 100644
--- a/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java
@@ -48,7 +48,7 @@
import org.dependencytrack.model.ViolationAnalysisState;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerabilityAlias;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.dependencytrack.plugin.PluginManager;
import org.dependencytrack.proto.filestorage.v1.FileMetadata;
import org.dependencytrack.tasks.BomUploadProcessingTask;
@@ -73,6 +73,7 @@
import static org.apache.commons.io.IOUtils.resourceToURL;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
public class CelPolicyEngineTest extends PersistenceCapableTest {
@@ -1949,8 +1950,10 @@ public void testEvaluateProjectWithNoLongerApplicableViolationWithAnalysis() {
@Test
@Ignore // Un-ignore for manual profiling purposes.
public void testWithBloatedBom() throws Exception {
- // Import all default objects (includes licenses and license groups).
- new DefaultObjectGenerator().contextInitialized(null);
+ useJdbiTransaction(handle -> {
+ DatabaseSeedingInitTask.seedDefaultLicenses(handle);
+ DatabaseSeedingInitTask.seedDefaultLicenseGroups(handle);
+ });
final var project = new Project();
project.setName("acme-app");
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java
index dc90233938..8397c070a1 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java
@@ -24,7 +24,7 @@
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.model.License;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.ClassRule;
@@ -36,6 +36,8 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
public class LicenseResourceTest extends ResourceTest {
@ClassRule
@@ -47,8 +49,8 @@ public class LicenseResourceTest extends ResourceTest {
@Override
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultLicenses();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java
index 479fa5e6a4..7506111f2f 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java
@@ -22,11 +22,6 @@
import alpine.notification.NotificationLevel;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFeature;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonObject;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.model.ConfigPropertyConstants;
@@ -35,19 +30,25 @@
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.NotificationScope;
import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.dependencytrack.notification.publisher.PublisherClass.SendMailPublisher;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
public class NotificationPublisherResourceTest extends ResourceTest {
@@ -60,8 +61,8 @@ public class NotificationPublisherResourceTest extends ResourceTest {
@Before
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultNotificationPublishers();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java
index ab703e8902..a8e9e56cc0 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java
@@ -24,11 +24,6 @@
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFeature;
import alpine.server.filters.AuthorizationFeature;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonObject;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import net.javacrumbs.jsonunit.core.Option;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
@@ -39,7 +34,7 @@
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.NotificationScope;
import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
@@ -47,6 +42,11 @@
import org.junit.ClassRule;
import org.junit.Test;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -58,6 +58,7 @@
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.dependencytrack.notification.publisher.PublisherClass.SendMailPublisher;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
import static org.hamcrest.Matchers.equalTo;
public class NotificationRuleResourceTest extends ResourceTest {
@@ -72,8 +73,8 @@ public class NotificationRuleResourceTest extends ResourceTest {
@Before
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultNotificationPublishers();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java
index 32dd32b7c7..a530e47e35 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java
@@ -29,7 +29,7 @@
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.Role;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.Before;
@@ -42,12 +42,13 @@
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
public class PermissionResourceTest extends ResourceTest {
@ClassRule
@@ -59,8 +60,8 @@ public class PermissionResourceTest extends ResourceTest {
@Before
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java
index 0b14692f5a..dbea1ec7df 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java
@@ -25,7 +25,7 @@
import org.dependencytrack.model.Repository;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.dependencytrack.persistence.QueryManager;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
@@ -41,6 +41,8 @@
import java.util.Date;
import java.util.List;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
public class RepositoryResourceTest extends ResourceTest {
@ClassRule
@@ -53,8 +55,8 @@ public class RepositoryResourceTest extends ResourceTest {
@Override
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultRepositories();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultRepositories);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java
index d2db8c73dd..89ef76678a 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java
@@ -18,33 +18,34 @@
*/
package org.dependencytrack.resources.v1;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
+import alpine.common.util.UuidUtil;
+import alpine.model.ManagedUser;
+import alpine.model.Permission;
+import alpine.server.filters.ApiFilter;
+import alpine.server.filters.AuthenticationFeature;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.Role;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
-import alpine.common.util.UuidUtil;
-import alpine.model.ManagedUser;
-import alpine.model.Permission;
-import alpine.server.filters.ApiFilter;
-import alpine.server.filters.AuthenticationFeature;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
public class RoleResourceTest extends ResourceTest {
@@ -58,9 +59,11 @@ public class RoleResourceTest extends ResourceTest {
@Override
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
- generator.loadDefaultRoles();
+
+ useJdbiTransaction(handle -> {
+ DatabaseSeedingInitTask.seedDefaultPermissions(handle);
+ DatabaseSeedingInitTask.seedDefaultRoles(handle);
+ });
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java
index e91041c8f7..ea723abddb 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java
@@ -27,30 +27,31 @@
import alpine.server.auth.JsonWebToken;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFeature;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonObject;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.assertj.core.api.Assertions;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.model.Project;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
import static org.hamcrest.CoreMatchers.equalTo;
public class TeamResourceTest extends ResourceTest {
@@ -70,8 +71,7 @@ public void setUpUser(boolean isAdmin) {
qm.addUserToTeam(testUser, team);
userNotPartof = qm.createTeam("UserNotPartof");
if (isAdmin) {
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions);
List permissionsList = new ArrayList<>();
final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT");
permissionsList.add(adminPermission);
@@ -521,8 +521,7 @@ public void getVisibleNonApiKeyTeams() {
@Test
public void getVisibleAdminApiKeyTeams() {
userNotPartof = qm.createTeam("UserNotPartof");
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions);
List permissionsList = new ArrayList<>();
final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT");
permissionsList.add(adminPermission);
diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java
index c362035c5f..17010c4d2f 100644
--- a/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java
@@ -47,7 +47,7 @@
import org.dependencytrack.model.Project;
import org.dependencytrack.model.VulnerabilityScan;
import org.dependencytrack.model.WorkflowStep;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.dependencytrack.plugin.PluginManager;
import org.dependencytrack.proto.filestorage.v1.FileMetadata;
import org.dependencytrack.proto.notification.v1.BomProcessingFailedSubject;
@@ -99,6 +99,7 @@
import static org.dependencytrack.model.WorkflowStep.METRICS_UPDATE;
import static org.dependencytrack.model.WorkflowStep.POLICY_EVALUATION;
import static org.dependencytrack.model.WorkflowStep.VULN_ANALYSIS;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_PROCESSED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_PROCESSING_FAILED;
import static org.dependencytrack.proto.notification.v1.Level.LEVEL_ERROR;
@@ -127,7 +128,7 @@ public void before() throws Exception {
@Test
public void informTest() throws Exception {
// Required for license resolution.
- DefaultObjectGenerator.loadDefaultLicenses();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses);
Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false);
@@ -283,7 +284,7 @@ public void informTest() throws Exception {
@Test
public void informTestWithComponentAlreadyExistsForIntegrityCheck() throws Exception {
// Required for license resolution.
- DefaultObjectGenerator.loadDefaultLicenses();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses);
Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false);
@@ -1747,7 +1748,7 @@ public void informWithEmptyComponentAndServiceNameTest() throws Exception {
@Test
public void informBomWithProtobufFormat() throws Exception {
- DefaultObjectGenerator.loadDefaultLicenses();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses);
Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false);
diff --git a/persistence-migration/src/main/resources/migration/changelog-v5.6.0.xml b/persistence-migration/src/main/resources/migration/changelog-v5.6.0.xml
index 732abe3fb2..08fb5fc298 100644
--- a/persistence-migration/src/main/resources/migration/changelog-v5.6.0.xml
+++ b/persistence-migration/src/main/resources/migration/changelog-v5.6.0.xml
@@ -2656,7 +2656,13 @@
-
+
+
+
+
+
+
+
@@ -2934,6 +2940,10 @@
ALTER TABLE "ROLES_PERMISSIONS" RENAME CONSTRAINT "ROLES_PERMISSIONS_NEW_PERMISSION_FK" TO "ROLES_PERMISSIONS_PERMISSION_FK";
+
+
+
+
DROP TRIGGER IF EXISTS trigger_effective_permissions_mx_on_teams_permissions_insert ON "TEAMS_PERMISSIONS";