From 3562f9df803fb4e5e197358ffb795667a17edc9f Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 1 Jun 2026 13:59:54 +0200 Subject: [PATCH] v4-migrator: Populate PERMISSION table in bootstrap phase Signed-off-by: nscuro --- .../v4migrator/PermissionCatalog.java | 95 +++++++++++++++++++ .../v4migrator/TableRegistry.java | 66 ++----------- .../v4migrator/cli/BootstrapCommand.java | 9 ++ .../v4migrator/preflight/Preflight.java | 4 +- .../v4migrator/BootstrapIT.java | 48 ++++++++++ .../v4migrator/UsersPermissionsIT.java | 56 +++++++++++ .../testsupport/V5TargetContainer.java | 19 +--- 7 files changed, 223 insertions(+), 74 deletions(-) create mode 100644 support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/PermissionCatalog.java diff --git a/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/PermissionCatalog.java b/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/PermissionCatalog.java new file mode 100644 index 0000000000..a94d36a9a1 --- /dev/null +++ b/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/PermissionCatalog.java @@ -0,0 +1,95 @@ +/* + * 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.v4migrator; + +import org.jdbi.v3.core.Jdbi; + +/** + * Seeds the v5 {@code PERMISSION} catalog from the apiserver's + * {@code org.dependencytrack.auth.Permissions} enum at Flyway head + * {@code 202605111028}. Keep this in sync with the enum. + * + *

The migrator owns v5 PERMISSION seeding because downstream join-table loads + * ({@code USERS_PERMISSIONS}, {@code TEAMS_PERMISSIONS}) need to FK-resolve v5 + * permission IDs (including v5-only entries such as + * {@code PORTFOLIO_ACCESS_CONTROL_BYPASS}) before the apiserver runs its own + * seeding step on first post-migration boot. + * + *

Seeding runs in {@code bootstrap} rather than {@code transform} so that a + * documented "drop v5 schema, re-bootstrap, re-run load" recovery (see ADR-023 + * §Resumability and the user-facing migration guide) leaves PERMISSION populated + * without re-running {@code transform}. The INSERT uses + * {@code ON CONFLICT ("NAME") DO NOTHING} and is safe to invoke repeatedly. + */ +public final class PermissionCatalog { + + private static final String SEED_SQL = """ + INSERT INTO "PERMISSION" ("NAME", "DESCRIPTION") VALUES + ('BOM_UPLOAD', 'Allows the ability to upload CycloneDX Software Bill of Materials (SBOM)'), + ('VIEW_PORTFOLIO', 'Provides the ability to view the portfolio of projects, components, and licenses'), + ('PORTFOLIO_ACCESS_CONTROL_BYPASS', 'Provides the ability to bypass portfolio access control, granting access to all projects'), + ('PORTFOLIO_MANAGEMENT', 'Allows the creation, modification, and deletion of data in the portfolio'), + ('PORTFOLIO_MANAGEMENT_CREATE', 'Allows the creation of data in the portfolio'), + ('PORTFOLIO_MANAGEMENT_READ', 'Allows the reading of data in the portfolio'), + ('PORTFOLIO_MANAGEMENT_UPDATE', 'Allows the updating of data in the portfolio'), + ('PORTFOLIO_MANAGEMENT_DELETE', 'Allows the deletion of data in the portfolio'), + ('VIEW_VULNERABILITY', 'Provides the ability to view the vulnerabilities projects are affected by'), + ('VULNERABILITY_ANALYSIS', 'Provides all abilities to make analysis decisions on vulnerabilities'), + ('VULNERABILITY_ANALYSIS_CREATE', 'Provides the ability to upload supported VEX documents to a project'), + ('VULNERABILITY_ANALYSIS_READ', 'Provides the ability read the VEX document for a project'), + ('VULNERABILITY_ANALYSIS_UPDATE', 'Provides the ability to make analysis decisions on vulnerabilities and upload supported VEX documents for a project'), + ('VIEW_POLICY_VIOLATION', 'Provides the ability to view policy violations'), + ('VULNERABILITY_MANAGEMENT', 'Allows all management permissions of internally-defined vulnerabilities'), + ('VULNERABILITY_MANAGEMENT_CREATE', 'Allows creation of internally-defined vulnerabilities'), + ('VULNERABILITY_MANAGEMENT_READ', 'Allows reading internally-defined vulnerabilities'), + ('VULNERABILITY_MANAGEMENT_UPDATE', 'Allows updating internally-defined vulnerabilities and vulnerability tags'), + ('VULNERABILITY_MANAGEMENT_DELETE', 'Allows management of internally-defined vulnerabilities'), + ('POLICY_VIOLATION_ANALYSIS', 'Provides the ability to make analysis decisions on policy violations'), + ('ACCESS_MANAGEMENT', 'Allows the management of users, teams, and API keys'), + ('ACCESS_MANAGEMENT_CREATE', 'Allows create permissions of users, teams, and API keys'), + ('ACCESS_MANAGEMENT_READ', 'Allows read permissions of users, teams, and API keys'), + ('ACCESS_MANAGEMENT_UPDATE', 'Allows update permissions of users, teams, and API keys'), + ('ACCESS_MANAGEMENT_DELETE', 'Allows delete permissions of users, teams, and API keys'), + ('SECRET_MANAGEMENT', 'Grants full secret management access'), + ('SECRET_MANAGEMENT_CREATE', 'Grants the ability to create secrets'), + ('SECRET_MANAGEMENT_UPDATE', 'Grants the ability to update secrets'), + ('SECRET_MANAGEMENT_DELETE', 'Grants the ability to delete secrets'), + ('SYSTEM_CONFIGURATION', 'Allows all access to configuration of the system including notifications, repositories, and email settings'), + ('SYSTEM_CONFIGURATION_CREATE', 'Allows creating configuration of the system including notifications, repositories, and email settings'), + ('SYSTEM_CONFIGURATION_READ', 'Allows reading the configuration of the system including notifications, repositories, and email settings'), + ('SYSTEM_CONFIGURATION_UPDATE', 'Allows updating the configuration of the system including notifications, repositories, and email settings'), + ('SYSTEM_CONFIGURATION_DELETE', 'Allows deleting the configuration of the system including notifications, repositories, and email settings'), + ('PROJECT_CREATION_UPLOAD', 'Provides the ability to optionally create project (if non-existent) on BOM or scan upload'), + ('POLICY_MANAGEMENT', 'Allows the creation, modification, and deletion of policy'), + ('POLICY_MANAGEMENT_CREATE', 'Allows the creation of a policy'), + ('POLICY_MANAGEMENT_READ', 'Allows reading of policies'), + ('POLICY_MANAGEMENT_UPDATE', 'Allows the modification of a policy'), + ('POLICY_MANAGEMENT_DELETE', 'Allows the deletion of a policy'), + ('TAG_MANAGEMENT', 'Allows the modification and deletion of tags'), + ('TAG_MANAGEMENT_DELETE', 'Allows the deletion of a tag') + ON CONFLICT ("NAME") DO NOTHING + """; + + private PermissionCatalog() { + } + + public static int seed(final Jdbi target) { + return target.withHandle(h -> h.execute(SEED_SQL)); + } +} diff --git a/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/TableRegistry.java b/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/TableRegistry.java index 2ec05bf3f0..0a0877997a 100644 --- a/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/TableRegistry.java +++ b/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/TableRegistry.java @@ -595,14 +595,14 @@ ON CONFLICT ("USERNAME") DO NOTHING ); /** - * Source-only mirror of v4 {@code PERMISSION}. The migrator seeds v5 {@code PERMISSION} - * with the full v5 catalog (ON CONFLICT DO NOTHING) and builds - * {@code permission_name_map} by inner-joining v4 NAME against v5 PERMISSION. - * v4 permission names that no longer exist in v5 (e.g. {@code VIEW_BADGES}) drop out - * of the map. Implication fan-out (v4 {@code ACCESS_MANAGEMENT} -> - * v5 {@code PORTFOLIO_ACCESS_CONTROL_BYPASS}) is applied on the join-table - * {@code tgt_*} tables. See {@code TEAMS_PERMISSIONS} and the consolidated - * {@code USERS_PERMISSIONS} transforms. + * Source-only mirror of v4 {@code PERMISSION}. The v5 {@code PERMISSION} catalog is + * seeded during {@code bootstrap} (see {@link PermissionCatalog}); transform here + * just builds {@code permission_name_map} by inner-joining v4 NAME against the + * already-seeded v5 PERMISSION table. v4 permission names that no longer exist in + * v5 (e.g. {@code VIEW_BADGES}) drop out of the map. Implication fan-out (v4 + * {@code ACCESS_MANAGEMENT} -> v5 {@code PORTFOLIO_ACCESS_CONTROL_BYPASS}) is + * applied on the join-table {@code tgt_*} tables. See {@code TEAMS_PERMISSIONS} and + * the consolidated {@code USERS_PERMISSIONS} transforms. */ private static final TableMigration PERMISSION = new TableMigration( "PERMISSION", @@ -620,56 +620,6 @@ ON CONFLICT ("USERNAME") DO NOTHING """, List.of("ID", "DESCRIPTION", "NAME"), """ - -- Seed v5 PERMISSION with the full catalog from apiserver's - -- org.dependencytrack.auth.Permissions enum at Flyway head 202605111028. - -- Keep this in sync with the enum; the migrator owns v5 PERMISSION seeding so - -- that join-table loads further down can resolve v5-only permission IDs (e.g. - -- PORTFOLIO_ACCESS_CONTROL_BYPASS) before the apiserver runs its own seeding - -- step on first post-migration boot. - INSERT INTO "PERMISSION" ("NAME", "DESCRIPTION") VALUES - ('BOM_UPLOAD', 'Allows the ability to upload CycloneDX Software Bill of Materials (SBOM)'), - ('VIEW_PORTFOLIO', 'Provides the ability to view the portfolio of projects, components, and licenses'), - ('PORTFOLIO_ACCESS_CONTROL_BYPASS', 'Provides the ability to bypass portfolio access control, granting access to all projects'), - ('PORTFOLIO_MANAGEMENT', 'Allows the creation, modification, and deletion of data in the portfolio'), - ('PORTFOLIO_MANAGEMENT_CREATE', 'Allows the creation of data in the portfolio'), - ('PORTFOLIO_MANAGEMENT_READ', 'Allows the reading of data in the portfolio'), - ('PORTFOLIO_MANAGEMENT_UPDATE', 'Allows the updating of data in the portfolio'), - ('PORTFOLIO_MANAGEMENT_DELETE', 'Allows the deletion of data in the portfolio'), - ('VIEW_VULNERABILITY', 'Provides the ability to view the vulnerabilities projects are affected by'), - ('VULNERABILITY_ANALYSIS', 'Provides all abilities to make analysis decisions on vulnerabilities'), - ('VULNERABILITY_ANALYSIS_CREATE', 'Provides the ability to upload supported VEX documents to a project'), - ('VULNERABILITY_ANALYSIS_READ', 'Provides the ability read the VEX document for a project'), - ('VULNERABILITY_ANALYSIS_UPDATE', 'Provides the ability to make analysis decisions on vulnerabilities and upload supported VEX documents for a project'), - ('VIEW_POLICY_VIOLATION', 'Provides the ability to view policy violations'), - ('VULNERABILITY_MANAGEMENT', 'Allows all management permissions of internally-defined vulnerabilities'), - ('VULNERABILITY_MANAGEMENT_CREATE', 'Allows creation of internally-defined vulnerabilities'), - ('VULNERABILITY_MANAGEMENT_READ', 'Allows reading internally-defined vulnerabilities'), - ('VULNERABILITY_MANAGEMENT_UPDATE', 'Allows updating internally-defined vulnerabilities and vulnerability tags'), - ('VULNERABILITY_MANAGEMENT_DELETE', 'Allows management of internally-defined vulnerabilities'), - ('POLICY_VIOLATION_ANALYSIS', 'Provides the ability to make analysis decisions on policy violations'), - ('ACCESS_MANAGEMENT', 'Allows the management of users, teams, and API keys'), - ('ACCESS_MANAGEMENT_CREATE', 'Allows create permissions of users, teams, and API keys'), - ('ACCESS_MANAGEMENT_READ', 'Allows read permissions of users, teams, and API keys'), - ('ACCESS_MANAGEMENT_UPDATE', 'Allows update permissions of users, teams, and API keys'), - ('ACCESS_MANAGEMENT_DELETE', 'Allows delete permissions of users, teams, and API keys'), - ('SECRET_MANAGEMENT', 'Grants full secret management access'), - ('SECRET_MANAGEMENT_CREATE', 'Grants the ability to create secrets'), - ('SECRET_MANAGEMENT_UPDATE', 'Grants the ability to update secrets'), - ('SECRET_MANAGEMENT_DELETE', 'Grants the ability to delete secrets'), - ('SYSTEM_CONFIGURATION', 'Allows all access to configuration of the system including notifications, repositories, and email settings'), - ('SYSTEM_CONFIGURATION_CREATE', 'Allows creating configuration of the system including notifications, repositories, and email settings'), - ('SYSTEM_CONFIGURATION_READ', 'Allows reading the configuration of the system including notifications, repositories, and email settings'), - ('SYSTEM_CONFIGURATION_UPDATE', 'Allows updating the configuration of the system including notifications, repositories, and email settings'), - ('SYSTEM_CONFIGURATION_DELETE', 'Allows deleting the configuration of the system including notifications, repositories, and email settings'), - ('PROJECT_CREATION_UPLOAD', 'Provides the ability to optionally create project (if non-existent) on BOM or scan upload'), - ('POLICY_MANAGEMENT', 'Allows the creation, modification, and deletion of policy'), - ('POLICY_MANAGEMENT_CREATE', 'Allows the creation of a policy'), - ('POLICY_MANAGEMENT_READ', 'Allows reading of policies'), - ('POLICY_MANAGEMENT_UPDATE', 'Allows the modification of a policy'), - ('POLICY_MANAGEMENT_DELETE', 'Allows the deletion of a policy'), - ('TAG_MANAGEMENT', 'Allows the modification and deletion of tags'), - ('TAG_MANAGEMENT_DELETE', 'Allows the deletion of a tag') - ON CONFLICT ("NAME") DO NOTHING; DROP TABLE IF EXISTS "%1$s".permission_name_map; CREATE UNLOGGED TABLE "%1$s".permission_name_map ( orig_id BIGINT NOT NULL PRIMARY KEY diff --git a/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/cli/BootstrapCommand.java b/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/cli/BootstrapCommand.java index 3485c54b42..345984412f 100644 --- a/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/cli/BootstrapCommand.java +++ b/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/cli/BootstrapCommand.java @@ -20,6 +20,7 @@ import org.dependencytrack.migration.MigrationExecutor; import org.dependencytrack.v4migrator.ExitCode; +import org.dependencytrack.v4migrator.PermissionCatalog; import org.dependencytrack.v4migrator.config.Connections; import org.dependencytrack.v4migrator.preflight.Preflight; import org.dependencytrack.v4migrator.preflight.Preflight.Mode; @@ -59,6 +60,13 @@ protected int execute(final Jdbi target) { head, Preflight.EXPECTED_FLYWAY_HEAD); return ExitCode.SCHEMA_VERSION_MISMATCH; } + + // Seed the v5 PERMISSION catalog now so that downstream load phases can FK-resolve + // into it without depending on transform having run in the same invocation. See + // PermissionCatalog for context. + LOGGER.info("Seeding v5 PERMISSION catalog"); + PermissionCatalog.seed(target); + LOGGER.info("Bootstrap complete. Flyway head = {}. Run 'extract' or 'run' next.", head); return ExitCode.OK; } @@ -67,5 +75,6 @@ protected int execute(final Jdbi target) { protected void printPlan() { System.out.println(" Phase: bootstrap"); System.out.println(" Target Flyway head to apply: " + Preflight.EXPECTED_FLYWAY_HEAD); + System.out.println(" Seed v5 PERMISSION catalog: yes (idempotent)"); } } diff --git a/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/preflight/Preflight.java b/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/preflight/Preflight.java index aef0fec835..d57eac3ced 100644 --- a/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/preflight/Preflight.java +++ b/support/v4-migrator/src/main/java/org/dependencytrack/v4migrator/preflight/Preflight.java @@ -196,8 +196,8 @@ private void checkFlywayHead(final List failures) { /** * Refuse to operate against a v5 cluster that already has user data. - * Checks a small set of high-trust tables. Flyway-installed seed data - * (e.g. default permissions) is permitted. + * Checks a small set of high-trust tables. Seed data populated by bootstrap + * (default permissions) is permitted. */ private void checkTargetEmpty(final List failures) { final String[] mustBeEmpty = { diff --git a/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/BootstrapIT.java b/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/BootstrapIT.java index c44dbc46f0..6261407c7c 100644 --- a/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/BootstrapIT.java +++ b/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/BootstrapIT.java @@ -124,6 +124,21 @@ void bootstrapAppliesFlywayUpToExpectedHead() { .one()); assertThat(head).isEqualTo(Preflight.EXPECTED_FLYWAY_HEAD); + // PERMISSION catalog must be seeded by bootstrap so that downstream load phases + // can FK-resolve permission IDs even if the operator follows the documented + // "drop v5 schema, re-bootstrap, re-run load" recovery (issue #6217). + final long permissionCount = jdbi().withHandle(h -> + h.createQuery("SELECT count(*) FROM \"PERMISSION\"").mapTo(Long.class).one()); + assertThat(permissionCount).isEqualTo(42L); + final boolean hasV5OnlyPermission = jdbi().withHandle(h -> + h.createQuery(""" + SELECT EXISTS ( + SELECT 1 FROM "PERMISSION" + WHERE "NAME" = 'PORTFOLIO_ACCESS_CONTROL_BYPASS') + """) + .mapTo(Boolean.class).one()); + assertThat(hasV5OnlyPermission).isTrue(); + // Default-mode preflight now passes (schema applied, no user data, no PERMISSION pre-seed needed). final PreflightResult after = new Preflight(jdbi(), null, opts, Mode.DEFAULT).run(); assertThat(after.ok()) @@ -131,6 +146,39 @@ void bootstrapAppliesFlywayUpToExpectedHead() { .isTrue(); } + @Test + @Order(4) + void shouldRemainIdempotentWhenBootstrapInvokedTwice() { + final GlobalOptions opts = optsForContainer(); + opts.stagingSchema = "dt_v4_migration_bootstrap"; + + final long countBefore = jdbi().withHandle(h -> + h.createQuery("SELECT count(*) FROM \"PERMISSION\"").mapTo(Long.class).one()); + + final ByteArrayOutputStream capture = new ByteArrayOutputStream(); + final PrintStream origOut = System.out; + final PrintStream origErr = System.err; + System.setOut(new PrintStream(capture, true)); + System.setErr(new PrintStream(capture, true)); + final int exit; + try { + exit = new CommandLine(new V4Migrator()).execute( + "bootstrap", + "--target-url", container.getJdbcUrl(), + "--target-user", container.getUsername(), + "--target-pass", container.getPassword(), + "--staging-schema", opts.stagingSchema); + } finally { + System.setOut(origOut); + System.setErr(origErr); + } + assertThat(exit).as("second bootstrap output: %s", capture).isEqualTo(ExitCode.OK); + + final long countAfter = jdbi().withHandle(h -> + h.createQuery("SELECT count(*) FROM \"PERMISSION\"").mapTo(Long.class).one()); + assertThat(countAfter).isEqualTo(countBefore); + } + private GlobalOptions optsForContainer() { final GlobalOptions opts = new GlobalOptions(); opts.targetUrl = container.getJdbcUrl(); diff --git a/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/UsersPermissionsIT.java b/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/UsersPermissionsIT.java index c53a79f824..3872ad3715 100644 --- a/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/UsersPermissionsIT.java +++ b/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/UsersPermissionsIT.java @@ -27,8 +27,11 @@ import org.dependencytrack.v4migrator.transform.TransformPhase; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; import java.util.List; import java.util.Map; @@ -44,6 +47,7 @@ * to preserve v4's implicit portfolio-access-control bypass. */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class UsersPermissionsIT { private V4PostgresSource source; @@ -66,6 +70,7 @@ void stop() { } @Test + @Order(1) void mapsPermissionsByNameAndRewritesJoins() throws Exception { // Seed v4: a TEAM, two permissions (one carried over to v5, one v4-only), a MANAGED // user "alice" with both, an LDAP user "bob" with only the v4-only one, and an OIDC @@ -168,6 +173,57 @@ void mapsPermissionsByNameAndRewritesJoins() throws Exception { "Engineering:PORTFOLIO_ACCESS_CONTROL_BYPASS"); } + /** + * Regression for #6217: + * the doc-guided recovery from a failed {@code load} drops and re-creates the v5 schema, + * then re-runs {@code bootstrap} and {@code load} (without {@code transform}). Bootstrap + * must therefore leave the v5 {@code PERMISSION} catalog populated so that the + * {@code USERS_PERMISSIONS_PERMISSION_FK} on the subsequent join-table load resolves. + */ + @Test + @Order(2) + void shouldSucceedWhenLoadResumedAfterPermissionReset() { + // Simulate the operator's recovery: drop everything that hangs off PERMISSION, + // including PERMISSION itself, and restart the identity sequence so the re-seed + // produces identical IDs to the originals captured in permission_name_map. + target.jdbi().useHandle(h -> + h.execute("TRUNCATE TABLE \"PERMISSION\" RESTART IDENTITY CASCADE")); + + // Re-bootstrap: only the PERMISSION seed step (Flyway is already at head). + PermissionCatalog.seed(target.jdbi()); + + // Re-run just the join-table loads. The permission_name_map and tgt_*_permissions + // staging tables are still in place from the first pipeline run. + final TableMigration teamsPerms = TableRegistry.loaded().stream() + .filter(t -> t.name().equals("TEAMS_PERMISSIONS")) + .findFirst().orElseThrow(); + final TableMigration usersPerms = TableRegistry.loaded().stream() + .filter(t -> t.name().equals("USERS_PERMISSIONS")) + .findFirst().orElseThrow(); + + target.jdbi().useHandle(h -> { + h.execute(teamsPerms.loadSql().formatted("dt_v4_migration")); + h.execute(usersPerms.loadSql().formatted("dt_v4_migration")); + }); + + final List> userPerms = target.jdbi().withHandle(h -> + h.createQuery(""" + SELECT u."USERNAME" AS username, p."NAME" AS permission_name + FROM "USERS_PERMISSIONS" up + JOIN "USER" u ON u."ID" = up."USER_ID" + JOIN "PERMISSION" p ON p."ID" = up."PERMISSION_ID" + ORDER BY u."USERNAME", p."NAME" + """) + .mapToMap() + .list()); + + assertThat(userPerms).extracting(m -> m.get("username") + ":" + m.get("permission_name")) + .containsExactlyInAnyOrder( + "alice:VIEW_PORTFOLIO", + "carol:ACCESS_MANAGEMENT", + "carol:PORTFOLIO_ACCESS_CONTROL_BYPASS"); + } + private void runPipeline() throws Exception { final GlobalOptions global = new GlobalOptions(); global.targetUrl = target.jdbcUrl(); diff --git a/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/testsupport/V5TargetContainer.java b/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/testsupport/V5TargetContainer.java index b9a165b18a..40ba242c5c 100644 --- a/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/testsupport/V5TargetContainer.java +++ b/support/v4-migrator/src/test/java/org/dependencytrack/v4migrator/testsupport/V5TargetContainer.java @@ -19,6 +19,7 @@ package org.dependencytrack.v4migrator.testsupport; import org.dependencytrack.migration.MigrationExecutor; +import org.dependencytrack.v4migrator.PermissionCatalog; import org.dependencytrack.v4migrator.preflight.Preflight; import org.jdbi.v3.core.Jdbi; import org.testcontainers.containers.PostgreSQLContainer; @@ -45,23 +46,13 @@ public V5TargetContainer() { public V5TargetContainer start() { container.start(); new MigrationExecutor(dataSource(), Preflight.EXPECTED_FLYWAY_HEAD).execute(); - seedPermissions(); + // Mimic the bootstrap step. PermissionCatalog.seed is what the BootstrapCommand + // invokes immediately after applying Flyway, so downstream phases see the full + // v5 PERMISSION catalog. + PermissionCatalog.seed(jdbi()); return this; } - /** - * Mimic the v5 apiserver's {@code DatabaseSeedingInitTask}, which populates {@code PERMISSION} - * on first boot. The migrator's preflight refuses to run against an empty PERMISSION table, - * so tests must reproduce that initial state. A minimal seed is enough for preflight. - */ - private void seedPermissions() { - jdbi().useHandle(h -> h.execute(""" - INSERT INTO "PERMISSION" ("NAME", "DESCRIPTION") - VALUES ('VIEW_PORTFOLIO', 'View projects, components, vulnerabilities') - ON CONFLICT ("NAME") DO NOTHING - """)); - } - public String jdbcUrl() { return container.getJdbcUrl(); }