Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ private void checkFlywayHead(final List<String> 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<String> failures) {
final String[] mustBeEmpty = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,61 @@ 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())
.as("default preflight should pass after bootstrap; failures: %s", after.failures())
.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();
Expand Down
Loading
Loading