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 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: -#

# # @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";