From 9da6f4e953d6a327edd5c2d82f9e3ab48631edc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:12:34 +0000 Subject: [PATCH 001/181] Bump debian from `b5ace51` to `5724d31` in /src/main/docker Bumps debian from `b5ace51` to `5724d31`. --- updated-dependencies: - dependency-name: debian dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index 157928f0aa..b6057471e6 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -16,7 +16,7 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. FROM eclipse-temurin:21.0.5_11-jre-jammy@sha256:5f8358c9d5615c18e95728e8b8528bda7ff40a7a5da2ac9a35b7a01f5d9b231a AS jre-build -FROM debian:stable-slim@sha256:b5ace515e78743215a1b101a6f17e59ed74b17132139ca3af3c37e605205e973 +FROM debian:stable-slim@sha256:5724d31208341cef9af6ae2be86be9cda6a87271f362a03481a522c9c19d401b # Arguments that can be passed at build time # Directory names must end with / to avoid errors when ADDing and COPYing From 570dad14f415497faf6df057d628d7de5d5c0257 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 12 Feb 2025 17:09:37 +0100 Subject: [PATCH 002/181] Fix `DbUtil` not being initialized Some legacy code ported over from v4 still uses `DbUtil#isPostgreSQL` checks to determine if certain SQL syntax can be used. Since our move to Liquibase, `DbUtil` has not been initialized anymore, and hence always returned `false` for the aforementioned check. Ultimately, usages of `DbUtil` should be removed entirely. Signed-off-by: nscuro Signed-off-by: Allen Shearin --- .../persistence/migration/MigrationInitializer.java | 12 ++++++++++++ .../org/dependencytrack/PostgresTestContainer.java | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/org/dependencytrack/persistence/migration/MigrationInitializer.java b/src/main/java/org/dependencytrack/persistence/migration/MigrationInitializer.java index f9912dcda6..b5a6c0d8d4 100644 --- a/src/main/java/org/dependencytrack/persistence/migration/MigrationInitializer.java +++ b/src/main/java/org/dependencytrack/persistence/migration/MigrationInitializer.java @@ -20,6 +20,7 @@ import alpine.Config; import alpine.common.logging.Logger; +import alpine.server.util.DbUtil; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import liquibase.Liquibase; @@ -40,6 +41,7 @@ import jakarta.servlet.ServletContextEvent; import jakarta.servlet.ServletContextListener; import javax.sql.DataSource; +import java.sql.Connection; import java.util.HashMap; import java.util.Optional; @@ -73,6 +75,16 @@ public void contextInitialized(final ServletContextEvent event) { 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); + } + runMigration(dataSource); } catch (Exception e) { if (config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS_ONLY) diff --git a/src/test/java/org/dependencytrack/PostgresTestContainer.java b/src/test/java/org/dependencytrack/PostgresTestContainer.java index 9ebf678117..deb3a3dff1 100644 --- a/src/test/java/org/dependencytrack/PostgresTestContainer.java +++ b/src/test/java/org/dependencytrack/PostgresTestContainer.java @@ -18,6 +18,7 @@ */ package org.dependencytrack; +import alpine.server.util.DbUtil; import com.github.dockerjava.api.command.InspectContainerResponse; import org.dependencytrack.persistence.migration.MigrationInitializer; import org.postgresql.ds.PGSimpleDataSource; @@ -25,6 +26,8 @@ import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.TestcontainersConfiguration; +import java.sql.Connection; + public class PostgresTestContainer extends PostgreSQLContainer { @SuppressWarnings("resource") @@ -61,6 +64,13 @@ protected void containerIsStarted(final InspectContainerResponse containerInfo, dataSource.setPassword(getPassword()); try { + try (final Connection connection = dataSource.getConnection()) { + // Ensure that DbUtil#isPostgreSQL will work as expected. + // Some legacy code ported over from v4 still uses this. + // TODO: Remove once DbUtil#isPostgreSQL is no longer used. + DbUtil.initPlatformName(connection); + } + MigrationInitializer.runMigration(dataSource); } catch (Exception e) { throw new RuntimeException("Failed to execute migrations", e); From 9624f57c728f5549cdfea887fecf029438a31112 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:23:49 +0000 Subject: [PATCH 003/181] Bump lib.net.javacrumbs.shedlock.version from 6.2.0 to 6.3.0 Bumps `lib.net.javacrumbs.shedlock.version` from 6.2.0 to 6.3.0. Updates `net.javacrumbs.shedlock:shedlock-provider-jdbc` from 6.2.0 to 6.3.0 Updates `net.javacrumbs.shedlock:shedlock-provider-jdbc-internal` from 6.2.0 to 6.3.0 --- updated-dependencies: - dependency-name: net.javacrumbs.shedlock:shedlock-provider-jdbc dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: net.javacrumbs.shedlock:shedlock-provider-jdbc-internal dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e5a781b4d4..64cd5ef9ec 100644 --- a/pom.xml +++ b/pom.xml @@ -128,7 +128,7 @@ 1.1.1 2.0.16 4.5.14 - 6.2.0 + 6.3.0 1.4.0 42.7.4 From 7007af153ea7b06f6d71a49705dce5ebef1fb493 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 17 Feb 2025 12:55:45 -0600 Subject: [PATCH 004/181] refactor: add changeset for new tables Signed-off-by: Jonathan Howard Signed-off-by: Allen Shearin --- .../resources/migration/changelog-v5.6.0.xml | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index ef25e3fc6f..ee0801e9df 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -636,4 +636,98 @@ onDelete="CASCADE" onUpdate="NO ACTION" referencedColumnNames="ID" referencedTableName="WORKFLOW_STATE" validate="true"/> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 687bce6d80ccbf6cd8615d5cc2e2d5819c4c5388 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Thu, 20 Feb 2025 10:49:30 -0600 Subject: [PATCH 005/181] fix: changeset unique constraints Signed-off-by: Jonathan Howard Signed-off-by: Allen Shearin --- src/main/resources/META-INF/persistence.xml | 2 + .../resources/migration/changelog-v5.6.0.xml | 92 +++++++++++++------ 2 files changed, 66 insertions(+), 28 deletions(-) diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index f09e8a9e04..cf0d7ac966 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -33,6 +33,7 @@ org.dependencytrack.model.FindingAttribution org.dependencytrack.model.License org.dependencytrack.model.LicenseGroup + org.dependencytrack.model.MappedRole org.dependencytrack.model.NotificationPublisher org.dependencytrack.model.NotificationRule org.dependencytrack.model.Policy @@ -45,6 +46,7 @@ org.dependencytrack.model.ProjectProperty org.dependencytrack.model.Repository org.dependencytrack.model.RepositoryMetaComponent + org.dependencytrack.model.Role org.dependencytrack.model.ServiceComponent org.dependencytrack.model.Tag org.dependencytrack.model.Vex diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index ee0801e9df..cbb6c44e28 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -642,62 +642,80 @@ - + + + + + + + + + + + + + + + - - + + + + + + + + + + - - + referencedTableName="ROLE" referencedColumnNames="ID" /> + + referencedTableName="PERMISSION" referencedColumnNames="ID" /> - - - + + + + + referencedTableName="PROJECT" referencedColumnNames="ID" /> + + referencedTableName="ROLE" referencedColumnNames="ID" /> + referencedTableName="LDAPUSER" referencedColumnNames="ID" /> + + referencedTableName="PROJECT_ACCESS_ROLES" referencedColumnNames="ID" /> @@ -705,29 +723,47 @@ + referencedTableName="MANAGEDUSER" referencedColumnNames="ID" /> + + referencedTableName="PROJECT_ACCESS_ROLES" referencedColumnNames="ID" /> + referencedTableName="OIDCUSER" referencedColumnNames="ID" /> + + referencedTableName="PROJECT_ACCESS_ROLES" referencedColumnNames="ID" /> + + + + + + + + + + From af04fb24a1c7e78ec5f002ce14ef35d8dc851413 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 18 Feb 2025 15:21:05 +0100 Subject: [PATCH 006/181] Mirror container images to Docker Hub Analogue to https://github.com/DependencyTrack/hyades/pull/1672 Signed-off-by: nscuro Signed-off-by: Allen Shearin --- .github/workflows/ci-build.yaml | 2 +- .github/workflows/ci-publish.yaml | 2 +- .github/workflows/mirror-container-image.yml | 54 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/mirror-container-image.yml diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index 352a568cf9..f20f4457e0 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -48,4 +48,4 @@ jobs: security-events: write # Required to upload trivy's SARIF output secrets: registry-0-usr: ${{ github.repository_owner }} - registry-0-psw: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + registry-0-psw: ${{ github.repository_owner == 'DependencyTrack' && secrets.BOT_IMAGE_PUSH_TOKEN || secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index b4b04526f1..86b24e2a1e 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -58,7 +58,7 @@ jobs: security-events: write # Required to upload trivy's SARIF output secrets: registry-0-usr: ${{ github.repository_owner }} - registry-0-psw: ${{ secrets.GITHUB_TOKEN }} + registry-0-psw: ${{ github.repository_owner == 'DependencyTrack' && secrets.BOT_IMAGE_PUSH_TOKEN || secrets.GITHUB_TOKEN }} update-github-release: runs-on: ubuntu-latest diff --git a/.github/workflows/mirror-container-image.yml b/.github/workflows/mirror-container-image.yml new file mode 100644 index 0000000000..06ef5ac531 --- /dev/null +++ b/.github/workflows/mirror-container-image.yml @@ -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. +name: Mirror Container Image + +on: + registry_package: + types: + - published + +permissions: { } + +jobs: + mirror-to-dockerhub: + name: Mirror to Docker Hub + runs-on: ubuntu-latest + # * Only mirror images to Docker Hub for the official repository. + # * Ensure only container packages are considered for this workflow. + # * Deal with multiplatform images where only the final manifest has a tag name. + # amd64 and arm64 image variants are published without tag name. + if: | + github.repository_owner == 'DependencyTrack' + && github.event.registry_package.package_type == 'CONTAINER' + && github.event.registry_package.package_version.container_metadata.tag.name != '' + steps: + - name: Login to Docker Hub + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # tag=v3.3.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - name: Setup Crane + uses: imjasonh/setup-crane@31b88efe9de28ae0ffa220711af4b60be9435f6e # tag=v0.4 + - name: Mirror image to Docker Hub + run: | + IMAGE_REPOSITORY="${{ github.event.registry_package.namespace }}/${{ github.event.registry_package.name }}" + IMAGE_TAG="${{ github.event.registry_package.package_version.container_metadata.tag.name }}" + + SRC_IMAGE="ghcr.io/${IMAGE_REPOSITORY}:${IMAGE_TAG}" + DST_IMAGE="docker.io/${IMAGE_REPOSITORY}:${IMAGE_TAG}" + + crane copy "${SRC_IMAGE}" "${DST_IMAGE}" \ No newline at end of file From 391a155aaba6c6457f4648a17e4141cede6689b8 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 19 Feb 2025 12:04:38 +0100 Subject: [PATCH 007/181] Fix erroneous over-suppression of Snyk findings A mix-up of `"V"."VULNID" != ANY(:vulnIdsToExclude)` and `"V"."VULNID" != ALL(:vulnIdsToExclude)` caused all but one Snyk vulnerability to be suppressed for a component. https://stackoverflow.com/a/11730845 Signed-off-by: nscuro Signed-off-by: Allen Shearin --- .../VulnerabilityScanResultProcessor.java | 2 +- .../VulnerabilityScanResultProcessorTest.java | 34 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java b/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java index 6d1c472242..2b9bd9fa62 100644 --- a/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java +++ b/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java @@ -952,7 +952,7 @@ ON CONFLICT ("VULNERABILITY_ID", "COMPONENT_ID", "PROJECT_ID") DO UPDATE AND "V"."SOURCE" = 'SNYK' AND ("A"."SUPPRESSED" IS NULL OR NOT "A"."SUPPRESSED") <#if vulnIdsToExclude> - AND "V"."VULNID" != ANY(:vulnIdsToExclude) + AND "V"."VULNID" != ALL(:vulnIdsToExclude) ) -- Try to create suppression analyses for all Snyk vulns identified above. diff --git a/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java b/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java index 3727247a81..607a261c03 100644 --- a/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java +++ b/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java @@ -1539,6 +1539,13 @@ public void processSuccessfulScanResultWithSnykVulnerabilityTest() { qm.persist(vulnerabilityS3); qm.addVulnerability(vulnerabilityS3, component, AnalyzerIdentity.SNYK_ANALYZER); + // Another SNYK vulnerability to NOT be suppressed, that doesn't yet have an analysis. + final var vulnerabilityS4 = new Vulnerability(); + vulnerabilityS4.setVulnId("SNYK-004"); + vulnerabilityS4.setSource(Vulnerability.Source.SNYK); + qm.persist(vulnerabilityS4); + qm.addVulnerability(vulnerabilityS4, component, AnalyzerIdentity.SNYK_ANALYZER); + final var scanToken = UUID.randomUUID().toString(); final var scanKey = ScanKey.newBuilder().setScanToken(scanToken).setComponentUuid(component.getUuid().toString()).build(); final var scanResult = ScanResult.newBuilder() @@ -1546,11 +1553,34 @@ public void processSuccessfulScanResultWithSnykVulnerabilityTest() { .addScannerResults(ScannerResult.newBuilder() .setScanner(SCANNER_SNYK) .setStatus(SCAN_STATUS_SUCCESSFUL) - .setBom(Bom.newBuilder().addVulnerabilities(createVuln("SNYK-001", "SNYK")))) + .setBom(Bom.newBuilder() + .addVulnerabilities(createVuln("SNYK-001", "SNYK")) + .addVulnerabilities(createVuln("SNYK-004", "SNYK")))) .build(); processor.process(aConsumerRecord(scanKey, scanResult).build()); + // Existing analyses are still in L1 cache. Wipe it. + qm.getPersistenceManager().evictAll(); + + assertThat(qm.getAllVulnerabilities(component, true)).satisfiesExactlyInAnyOrder( + vuln -> { + assertThat(vuln.getVulnId()).isEqualTo("SNYK-001"); + assertThat(qm.getAnalysis(component, vuln).isSuppressed()).isFalse(); + }, + vuln -> { + assertThat(vuln.getVulnId()).isEqualTo("SNYK-002"); + assertThat(qm.getAnalysis(component, vuln).isSuppressed()).isTrue(); + }, + vuln -> { + assertThat(vuln.getVulnId()).isEqualTo("SNYK-003"); + assertThat(qm.getAnalysis(component, vuln).isSuppressed()).isTrue(); + }, + vuln -> { + assertThat(vuln.getVulnId()).isEqualTo("SNYK-004"); + assertThat(qm.getAnalysis(component, vuln)).isNull(); + }); + // Project audit change notification must be sent. assertThat(kafkaMockProducer.history()).satisfiesExactly(record -> { assertThat(record.topic()).isEqualTo(KafkaTopics.VULN_ANALYSIS_RESULT_PROCESSED.name()); @@ -1581,7 +1611,7 @@ public void processSuccessfulScanResultWithSnykVulnerabilityTest() { assertThat(notification.getTitle()).isEqualTo(NotificationUtil.generateNotificationTitle(NotificationConstants.Title.ANALYSIS_DECISION_FALSE_POSITIVE, project)); assertThat(notification.getContent()).isEqualTo("An analysis decision was made to a finding affecting a project"); }); - } + } private org.cyclonedx.proto.v1_6.Vulnerability createVuln(final String id, final String source) { return org.cyclonedx.proto.v1_6.Vulnerability.newBuilder() From 8afb4be8404c9f01d16ab529dd041f4c50f7a994 Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 19 Feb 2025 12:24:42 +0100 Subject: [PATCH 008/181] Populate audit trail when auto-suppressing withdrawn Snyk findings Signed-off-by: nscuro Signed-off-by: Allen Shearin --- .../VulnerabilityScanResultProcessor.java | 29 ++++++++++---- .../VulnerabilityScanResultProcessorTest.java | 38 +++++++++++++++++-- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java b/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java index 2b9bd9fa62..7c1687a4cd 100644 --- a/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java +++ b/src/main/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessor.java @@ -25,7 +25,6 @@ import com.google.protobuf.Any; import com.google.protobuf.Timestamp; import com.google.protobuf.util.Timestamps; -import jakarta.ws.rs.core.MultivaluedHashMap; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.dependencytrack.event.PortfolioVulnerabilityAnalysisEvent; import org.dependencytrack.event.kafka.KafkaEvent; @@ -76,6 +75,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.slf4j.MDC; +import jakarta.ws.rs.core.MultivaluedHashMap; import javax.jdo.Query; import javax.jdo.Transaction; import java.util.ArrayList; @@ -92,6 +92,7 @@ import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.datanucleus.PropertyNames.PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT; @@ -102,7 +103,6 @@ import static org.dependencytrack.parser.dependencytrack.ModelConverterCdxToVuln.convert; import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction; import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle; -import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction; import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; import static org.dependencytrack.proto.notification.v1.Group.GROUP_NEW_VULNERABILITY; import static org.dependencytrack.proto.notification.v1.Group.GROUP_NEW_VULNERABLE_DEPENDENCY; @@ -294,14 +294,29 @@ private Set syncVulnerabilities(final QueryManager qm, final Comp } private void compareAndSuppressVulnBySnyk(QueryManager qm, Component component, List vulnList) { - final var analysisIds = new ArrayList(); - useJdbiTransaction(handle -> { + final List analysisIds = inJdbiTransaction(handle -> { final var dao = handle.attach(Dao.class); - analysisIds.addAll(dao.suppressSnykAnalyses(component.id, - vulnList.isEmpty() ? null : vulnList.stream().map(vuln -> vuln.getId()).toList(), - component.projectId)); + + final List modifiedAnalysisIds = dao.suppressSnykAnalyses( + component.id, + !vulnList.isEmpty() + ? vulnList.stream().map(org.cyclonedx.proto.v1_6.Vulnerability::getId).toList() + : null, + component.projectId); + + final List analysisComments = modifiedAnalysisIds.stream() + .flatMap(analysisId -> Stream.of( + new AnalysisComment(analysisId, formatComment(AnalysisCommentField.SUPPRESSED, null, true), /* commenter */ null), + new AnalysisComment(analysisId, "The vulnerability is no longer reported by any analyzer.", /* commenter */ null))) + .toList(); + if (!analysisComments.isEmpty()) { + dao.createAnalysisComments(analysisComments); + } + + return modifiedAnalysisIds; }); for (var analysisId : analysisIds) { + // TODO: Use this#maybeQueueProjectAuditChangeNotification instead. var analysis = qm.getObjectById(org.dependencytrack.model.Analysis.class, analysisId); var notification = NotificationUtil.generateAnalysisNotification(qm, analysis, true, true); final var event = KafkaEventConverter.convert(notification); diff --git a/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java b/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java index 607a261c03..f2a60b7ead 100644 --- a/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java +++ b/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java @@ -1566,15 +1566,47 @@ public void processSuccessfulScanResultWithSnykVulnerabilityTest() { assertThat(qm.getAllVulnerabilities(component, true)).satisfiesExactlyInAnyOrder( vuln -> { assertThat(vuln.getVulnId()).isEqualTo("SNYK-001"); - assertThat(qm.getAnalysis(component, vuln).isSuppressed()).isFalse(); + + final Analysis vulnAnalysis = qm.getAnalysis(component, vuln); + assertThat(vulnAnalysis).isNotNull(); + assertThat(vulnAnalysis.isSuppressed()).isFalse(); + assertThat(vulnAnalysis.getAnalysisComments()).isEmpty(); }, vuln -> { assertThat(vuln.getVulnId()).isEqualTo("SNYK-002"); - assertThat(qm.getAnalysis(component, vuln).isSuppressed()).isTrue(); + + final Analysis vulnAnalysis = qm.getAnalysis(component, vuln); + assertThat(vulnAnalysis).isNotNull(); + assertThat(vulnAnalysis.isSuppressed()).isTrue(); + assertThat(vulnAnalysis.getAnalysisComments()).satisfiesExactly( + comment -> { + assertThat(comment.getComment()).isEqualTo("Suppressed"); + assertThat(comment.getCommenter()).isNull(); + assertThat(comment.getTimestamp()).isNotNull(); + }, + comment -> { + assertThat(comment.getComment()).isEqualTo("The vulnerability is no longer reported by any analyzer."); + assertThat(comment.getCommenter()).isNull(); + assertThat(comment.getTimestamp()).isNotNull(); + }); }, vuln -> { assertThat(vuln.getVulnId()).isEqualTo("SNYK-003"); - assertThat(qm.getAnalysis(component, vuln).isSuppressed()).isTrue(); + + final Analysis vulnAnalysis = qm.getAnalysis(component, vuln); + assertThat(vulnAnalysis).isNotNull(); + assertThat(vulnAnalysis.isSuppressed()).isTrue(); + assertThat(vulnAnalysis.getAnalysisComments()).satisfiesExactly( + comment -> { + assertThat(comment.getComment()).isEqualTo("Suppressed"); + assertThat(comment.getCommenter()).isNull(); + assertThat(comment.getTimestamp()).isNotNull(); + }, + comment -> { + assertThat(comment.getComment()).isEqualTo("The vulnerability is no longer reported by any analyzer."); + assertThat(comment.getCommenter()).isNull(); + assertThat(comment.getTimestamp()).isNotNull(); + }); }, vuln -> { assertThat(vuln.getVulnId()).isEqualTo("SNYK-004"); From 71f2ccbce2057909f6870e9ce4cc91e211eba95a Mon Sep 17 00:00:00 2001 From: Sahiba Mittal Date: Wed, 19 Feb 2025 16:01:51 +0000 Subject: [PATCH 009/181] Log deleted project info + fix nullPointerException (#1064) Signed-off-by: Allen Shearin --- .../persistence/jdbi/ProjectDao.java | 19 ++++++++++++++---- .../maintenance/ProjectMaintenanceTask.java | 18 +++++++++++------ .../ProjectMaintenanceTaskTest.java | 20 +++++++++++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java index ea775ca2be..456c9a2257 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonAlias; import jakarta.annotation.Nullable; +import org.jdbi.v3.core.mapper.reflect.ColumnName; import org.jdbi.v3.json.Json; import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper; import org.jdbi.v3.sqlobject.customizer.Bind; @@ -228,7 +229,7 @@ record ConciseProjectMetricsRow( """) int deleteProject(@Bind final UUID projectUuid); - @SqlUpdate(""" + @SqlQuery(""" WITH "CTE" AS ( SELECT "ID" FROM "PROJECT" @@ -239,10 +240,18 @@ record ConciseProjectMetricsRow( DELETE FROM "PROJECT" WHERE "ID" IN (SELECT "ID" FROM "CTE") + RETURNING "NAME", "VERSION", "INACTIVE_SINCE", "UUID" """) - int deleteInactiveProjectsForRetentionDuration(@Bind final Instant retentionCutOff, @Bind final int batchSize); + @RegisterConstructorMapper(DeletedProject.class) + List deleteInactiveProjectsForRetentionDuration(@Bind final Instant retentionCutOff, @Bind final int batchSize); - @SqlUpdate(""" + record DeletedProject(@ColumnName("NAME") String name, + @ColumnName("VERSION") String version, + @ColumnName("INACTIVE_SINCE") Instant inactiveSince, + @ColumnName("UUID") UUID uuid) { + } + + @SqlQuery(""" DELETE FROM "PROJECT" WHERE "PROJECT"."INACTIVE_SINCE" IS NOT NULL @@ -255,8 +264,10 @@ record ConciseProjectMetricsRow( ORDER BY "PROJECT"."INACTIVE_SINCE" DESC LIMIT :versionCountThreshold ) + RETURNING "NAME", "VERSION", "INACTIVE_SINCE", "UUID" """) - int retainLastXInactiveProjects(@Bind final String projectName, @Bind final int versionCountThreshold); + @RegisterConstructorMapper(DeletedProject.class) + List retainLastXInactiveProjects(@Bind final String projectName, @Bind final int versionCountThreshold); @SqlQuery(""" SELECT "PROJECT"."NAME" diff --git a/src/main/java/org/dependencytrack/tasks/maintenance/ProjectMaintenanceTask.java b/src/main/java/org/dependencytrack/tasks/maintenance/ProjectMaintenanceTask.java index 8e87c3690e..0669928057 100644 --- a/src/main/java/org/dependencytrack/tasks/maintenance/ProjectMaintenanceTask.java +++ b/src/main/java/org/dependencytrack/tasks/maintenance/ProjectMaintenanceTask.java @@ -75,12 +75,12 @@ private Statistics informLocked() { assertLocked(); AtomicInteger numDeletedTotal = new AtomicInteger(0); - final String retentionType = withJdbiHandle(handle -> - handle.attach(ConfigPropertyDao.class).getValue(MAINTENANCE_PROJECTS_RETENTION_TYPE, String.class)); + final var retentionType = withJdbiHandle(handle -> + handle.attach(ConfigPropertyDao.class).getOptionalValue(MAINTENANCE_PROJECTS_RETENTION_TYPE, String.class)); - if (retentionType != null) { + if (!retentionType.isEmpty() && !retentionType.get().isEmpty()) { int batchSize = 100; - if (retentionType.equals("AGE")) { + if (retentionType.get().equals("AGE")) { final int retentionDays = withJdbiHandle(handle -> handle.attach(ConfigPropertyDao.class).getValue(MAINTENANCE_PROJECTS_RETENTION_DAYS, Integer.class)); @@ -88,12 +88,15 @@ private Statistics informLocked() { Instant retentionCutOff = Instant.now().minus(retentionDuration); Integer numDeletedLastBatch = null; while (numDeletedLastBatch == null || numDeletedLastBatch > 0) { - numDeletedLastBatch = withJdbiHandle( + final var deletedProjectsBatch = withJdbiHandle( batchHandle -> { final var projectDao = batchHandle.attach(ProjectDao.class); return projectDao.deleteInactiveProjectsForRetentionDuration(retentionCutOff, batchSize); }); + numDeletedLastBatch = deletedProjectsBatch.size(); numDeletedTotal.addAndGet(numDeletedLastBatch); + deletedProjectsBatch.forEach(deletedProject -> + LOGGER.info("Inactive project deleted: [name:%s, version:%s, inactive since:%s, uuid:%s]".formatted(deletedProject.name(), deletedProject.version(), deletedProject.inactiveSince(), deletedProject.uuid()))); } } else { final int versionCountThreshold = withJdbiHandle(handle -> @@ -105,7 +108,10 @@ private Statistics informLocked() { final var projectDao = batchHandle.attach(ProjectDao.class); List projectBatch = projectDao.getDistinctProjects(versionCountThreshold, batchSize); for (var projectName : projectBatch) { - numDeletedTotal.addAndGet(projectDao.retainLastXInactiveProjects(projectName, versionCountThreshold)); + final var deletedProjects = projectDao.retainLastXInactiveProjects(projectName, versionCountThreshold); + numDeletedTotal.addAndGet(deletedProjects.size()); + deletedProjects.forEach(deletedProject -> + LOGGER.info("Inactive project deleted: [name:%s, version:%s, inactive since:%s, uuid:%s]".formatted(deletedProject.name(), deletedProject.version(), deletedProject.inactiveSince(), deletedProject.uuid()))); } return projectBatch.size(); }); diff --git a/src/test/java/org/dependencytrack/tasks/maintenance/ProjectMaintenanceTaskTest.java b/src/test/java/org/dependencytrack/tasks/maintenance/ProjectMaintenanceTaskTest.java index 129b57dfc0..f78cee6a4e 100644 --- a/src/test/java/org/dependencytrack/tasks/maintenance/ProjectMaintenanceTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/maintenance/ProjectMaintenanceTaskTest.java @@ -210,4 +210,24 @@ public void testWithProjectRetentionDisabled() { assertThatNoException().isThrownBy(() -> task.inform(new ProjectMaintenanceEvent())); assertThat(qm.getProjects()).isNotNull(); } + + @Test + public void testWithProjectRetentionDisabledWithEmptyValue() { + qm.createConfigProperty( + MAINTENANCE_PROJECTS_RETENTION_TYPE.getGroupName(), + MAINTENANCE_PROJECTS_RETENTION_TYPE.getPropertyName(), + "", + MAINTENANCE_PROJECTS_RETENTION_TYPE.getPropertyType(), + MAINTENANCE_PROJECTS_RETENTION_TYPE.getDescription()); + + var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + project.setInactiveSince(DateUtil.parseShortDate("20100109")); + qm.persist(project); + + final var task = new ProjectMaintenanceTask(); + assertThatNoException().isThrownBy(() -> task.inform(new ProjectMaintenanceEvent())); + assertThat(qm.getProjects()).isNotNull(); + } } \ No newline at end of file From 28906c8cc9f84254286e7e59f2aab42192ece07d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:32:29 +0000 Subject: [PATCH 010/181] Bump lib.testcontainers.version from 1.20.4 to 1.20.5 Bumps `lib.testcontainers.version` from 1.20.4 to 1.20.5. Updates `org.testcontainers:kafka` from 1.20.4 to 1.20.5 - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.4...1.20.5) Updates `org.testcontainers:postgresql` from 1.20.4 to 1.20.5 - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.4...1.20.5) --- updated-dependencies: - dependency-name: org.testcontainers:kafka dependency-type: direct:development update-type: version-update:semver-patch - dependency-name: org.testcontainers:postgresql dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 64cd5ef9ec..aa79526bf4 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,7 @@ 0.5.3.2 3.2.2 4.29.3 - 1.20.4 + 1.20.5 2.2.0 + + + + + \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java b/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java new file mode 100644 index 0000000000..3b6b17ccca --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java @@ -0,0 +1,239 @@ +/* + * 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.datanucleus.method; + +import alpine.model.Team; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.junit.Test; + +import javax.jdo.Query; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class ProjectIsAccessibleByMethodTest extends PersistenceCapableTest { + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToTrueWhenProjectIsAccessible() { + final var teamA = new Team(); + teamA.setName("team-a"); + qm.persist(teamA); + + final var teamB = new Team(); + teamB.setName("team-b"); + qm.persist(teamB); + + final var project = new Project(); + project.setName("acme-app"); + project.setAccessTeams(List.of(teamA, teamB)); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{teamA.getId(), teamB.getId()})); + + final List projects = query.executeList(); + assertThat(projects).hasSize(1); + } + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToTrueWhenProjectParentIsAccessible() { + final var team = new Team(); + team.setName("team"); + qm.persist(team); + + final var parentProject = new Project(); + parentProject.setName("acme-app-parent"); + parentProject.setAccessTeams(List.of(team)); + qm.persist(parentProject); + + final var project = new Project(); + project.setParent(parentProject); + project.setName("acme-app"); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("name == 'acme-app' && this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); + + final List projects = query.executeList(); + assertThat(projects).hasSize(1); + } + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToTrueWhenProjectGrandParentIsAccessible() { + final var team = new Team(); + team.setName("team"); + qm.persist(team); + + final var grandParentProject = new Project(); + grandParentProject.setName("acme-app-grand-parent"); + grandParentProject.setAccessTeams(List.of(team)); + qm.persist(grandParentProject); + + final var parentProject = new Project(); + parentProject.setParent(grandParentProject); + parentProject.setName("acme-app-parent"); + qm.persist(parentProject); + + final var project = new Project(); + project.setParent(parentProject); + project.setName("acme-app"); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("name == 'acme-app' && this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); + + final List projects = query.executeList(); + assertThat(projects).hasSize(1); + } + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToFalseWhenProjectIsNotAccessible() { + final var teamA = new Team(); + teamA.setName("team-a"); + qm.persist(teamA); + + final var teamB = new Team(); + teamB.setName("team-b"); + qm.persist(teamB); + + final var project = new Project(); + project.setName("acme-app"); + project.setAccessTeams(List.of(teamA)); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{teamB.getId()})); + + final List projects = query.executeList(); + assertThat(projects).isEmpty(); + } + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToFalseWhenOnlyChildProjectIsAccessible() { + final var team = new Team(); + team.setName("team"); + qm.persist(team); + + final var parentProject = new Project(); + parentProject.setName("acme-app-parent"); + qm.persist(parentProject); + + final var project = new Project(); + project.setParent(parentProject); + project.setName("acme-app"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("name == 'acme-app-parent' && this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); + + final List projects = query.executeList(); + assertThat(projects).hasSize(0); + } + + @Test + @SuppressWarnings("resource") + public void shouldBeAllowedOnProjectMembersOfNonProjectObjects() { + final var team = new Team(); + team.setName("team"); + qm.persist(team); + + final var project = new Project(); + project.setName("acme-app"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Query query = qm.getPersistenceManager().newQuery(Component.class); + query.setFilter("project.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); + + final List components = query.executeList(); + assertThat(components).hasSize(1); + } + + @Test + @SuppressWarnings("resource") + public void shouldThrowWhenCalledOnNonProjectObject() { + final Query query = qm.getPersistenceManager().newQuery(Component.class); + query.setFilter("this.isAccessibleBy(:teamIds)"); + query.setParameters(Arrays.asList(1L, 2L, 3L)); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(query::execute) + .withMessage(""" + isAccessibleBy is only allowed for objects of type org.dependencytrack.model.Project, \ + but was called on org.dependencytrack.model.Component"""); + } + + @Test + @SuppressWarnings("resource") + public void shouldThrowWhenNoArgs() { + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy()"); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(query::execute) + .withMessage("Expected exactly one argument, but got 0"); + } + + @Test + @SuppressWarnings("resource") + public void shouldThrowWhenArgIsOfUnexpectedType() { + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy(:teamIdsString)"); + query.setParameters("1, 2, 3"); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(query::execute) + .withMessage(""" + Expected argument to be of type org.datanucleus.store.rdbms.sql.expression.ArrayLiteral, \ + but got org.datanucleus.store.rdbms.sql.expression.ParameterLiteral"""); + } + + @Test + @SuppressWarnings("resource") + public void shouldThrowWhenArgIsOfUnexpectedArrayType() { + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy(:teamIdStrings)"); + query.setNamedParameters(Map.of("teamIdsStrings", new String[]{"1", "2", "3"})); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(query::execute) + .withMessage(""" + Expected argument to be of type org.datanucleus.store.rdbms.sql.expression.ArrayLiteral, \ + but got org.datanucleus.store.rdbms.sql.expression.ParameterLiteral"""); + } + +} \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/persistence/jdbi/ApiRequestStatementCustomizerTest.java b/src/test/java/org/dependencytrack/persistence/jdbi/ApiRequestStatementCustomizerTest.java index 11476d09b1..8aeabf4796 100644 --- a/src/test/java/org/dependencytrack/persistence/jdbi/ApiRequestStatementCustomizerTest.java +++ b/src/test/java/org/dependencytrack/persistence/jdbi/ApiRequestStatementCustomizerTest.java @@ -566,11 +566,7 @@ public void testWithPortfolioAclEnabledWithTeams() { , 2 AS "valueB" FROM "PROJECT" WHERE TRUE - AND EXISTS (SELECT 1 - FROM "PROJECT_ACCESS_TEAMS" - WHERE "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = "PROJECT"."ID" - AND "PROJECT_ACCESS_TEAMS"."TEAM_ID" = ANY(:projectAclTeamIds) - ) + AND HAS_PROJECT_ACCESS("PROJECT"."ID", :projectAclTeamIds) """); assertThat(ctx.getBinding()).hasToString("{named:{projectAclTeamIds:[%s]}}".formatted(team.getId())); diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index 143872aa42..c4979c744b 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -53,7 +53,6 @@ import java.util.UUID; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.assertj.core.api.Assertions.assertThat; import static org.dependencytrack.model.WorkflowStatus.PENDING; import static org.dependencytrack.resources.v1.FindingResource.MEDIA_TYPE_SARIF_JSON; @@ -516,10 +515,10 @@ public void getAllFindingsWithAclEnabled() { .header(X_API_KEY, team.getApiKeys().get(0).getKey()) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); - Assert.assertEquals(String.valueOf(3), response.getHeaderString(TOTAL_COUNT_HEADER)); + Assert.assertEquals(String.valueOf(4), response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(3, json.size()); + Assert.assertEquals(4, json.size()); Assert.assertEquals(date.getTime() ,json.getJsonObject(0).getJsonObject("vulnerability").getJsonNumber("published").longValue()); Assert.assertEquals(p1.getName() ,json.getJsonObject(0).getJsonObject("component").getString("projectName")); Assert.assertEquals(p1.getVersion() ,json.getJsonObject(0).getJsonObject("component").getString("projectVersion")); @@ -532,6 +531,12 @@ public void getAllFindingsWithAclEnabled() { Assert.assertEquals(p1.getName() ,json.getJsonObject(2).getJsonObject("component").getString("projectName")); Assert.assertEquals(p1.getVersion() ,json.getJsonObject(2).getJsonObject("component").getString("projectVersion")); Assert.assertEquals(p1.getUuid().toString(), json.getJsonObject(2).getJsonObject("component").getString("project")); + + // Findings of p1_child are returned because team was given access to its parent project p1. + Assert.assertEquals(date.getTime(), json.getJsonObject(3).getJsonObject("vulnerability").getJsonNumber("published").longValue()); + Assert.assertEquals(p1_child.getName(), json.getJsonObject(3).getJsonObject("component").getString("projectName")); + Assert.assertEquals(p1_child.getVersion(), json.getJsonObject(3).getJsonObject("component").getString("projectVersion")); + Assert.assertEquals(p1_child.getUuid().toString(), json.getJsonObject(3).getJsonObject("component").getString("project")); } @Test @@ -672,7 +677,7 @@ public void getAllFindingsGroupedByVulnerabilityWithAclEnabled() { Assert.assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").size()); Assert.assertEquals(80, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(0).getInt("cweId")); Assert.assertEquals(666, json.getJsonObject(1).getJsonObject("vulnerability").getJsonArray("cwes").getJsonObject(1).getInt("cweId")); - Assert.assertEquals(1, json.getJsonObject(1).getJsonObject("vulnerability").getInt("affectedProjectCount")); + Assert.assertEquals(2, json.getJsonObject(1).getJsonObject("vulnerability").getInt("affectedProjectCount")); // p1 and p1_child. Assert.assertEquals("INTERNAL", json.getJsonObject(2).getJsonObject("vulnerability").getString("source")); Assert.assertEquals("Vuln-3", json.getJsonObject(2).getJsonObject("vulnerability").getString("vulnId")); diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 82535e3de4..9a29e71b1c 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -906,8 +906,25 @@ public void getProjectChildrenConciseWithAclTest() { .header(X_API_KEY, apiKey) .get(); assertThat(response.getStatus()).isEqualTo(200); - assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("0"); - assertThat(getPlainTextBody(response)).isEqualTo("[]"); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("2"); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + [ + { + "active": true, + "hasChildren": false, + "isLatest": false, + "name": "acme-child-app-a", + "uuid": "${json-unit.any-string}" + }, + { + "active": true, + "hasChildren": false, + "isLatest": false, + "name": "acme-child-app-b", + "uuid": "${json-unit.any-string}" + } + ] + """); // Additionally grant access to acme-child-app-a. childProjectA.addAccessTeam(team); @@ -917,8 +934,8 @@ public void getProjectChildrenConciseWithAclTest() { .header(X_API_KEY, apiKey) .get(); assertThat(response.getStatus()).isEqualTo(200); - assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1"); - assertThatJson(getPlainTextBody(response)).isEqualTo(""" + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("2"); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ [ { "uuid": "${json-unit.any-string}", @@ -931,6 +948,13 @@ public void getProjectChildrenConciseWithAclTest() { } ], "hasChildren": false + }, + { + "uuid": "${json-unit.any-string}", + "name": "acme-child-app-b", + "active": true, + "isLatest": false, + "hasChildren": false } ] """.formatted(team.getName())); From bdaf5ee6ba63882f1f5407a9fc8030135d83b7da Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 24 Feb 2025 10:52:58 -0700 Subject: [PATCH 014/181] feat: add role and mappedrole classes (#2) Signed-off-by: Allen Shearin --- .../org/dependencytrack/model/MappedRole.java | 200 ++++++++++++++++++ .../java/org/dependencytrack/model/Role.java | 161 ++++++++++++++ .../resources/migration/changelog-v5.6.0.xml | 53 ++++- 3 files changed, 408 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/MappedRole.java create mode 100644 src/main/java/org/dependencytrack/model/Role.java diff --git a/src/main/java/org/dependencytrack/model/MappedRole.java b/src/main/java/org/dependencytrack/model/MappedRole.java new file mode 100644 index 0000000000..6480e4f9a2 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/MappedRole.java @@ -0,0 +1,200 @@ +/* + * 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 alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.FetchGroup; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; +import javax.jdo.annotations.Unique; + +/** + * Model for associating a role on a given project with users. + * + * @author Allen Shearin + * @since 5.6.0 + */ +@PersistenceCapable(table = "PROJECT_ACCESS_ROLES") +@Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "LDAPUSERS_PROJECTS_ROLES", + members = { "ldapUsers", "project", "role" }, + deferred = "true") +@Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "MANAGEDUSERS_PROJECTS_ROLES", + members = { "managedUsers", "project", "role" }, + deferred = "true") +@Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "OIDCUSERS_PROJECTS_ROLES", + members = { "oidcUsers", "project", "role" }, + deferred = "true") +@FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "ldapUsers"), + @Persistent(name = "managedUsers"), + @Persistent(name = "oidcUsers") +}) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MappedRole implements Serializable { + + private static final long serialVersionUID = 1982348710987098723L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Column(name = "ROLE_ID", allowsNull = "false") + @JsonIgnore + private Role role; + + @Persistent + @Column(name = "PROJECT_ID", allowsNull = "false") + @JsonIgnore + private Project project; + + @Persistent(table = "LDAPUSERS_PROJECTS_ROLES", defaultFetchGroup = "true") + @Join(column = "PROJECT_ACCESS_ROLE_ID") + @Element(column = "LDAPUSER_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List ldapUsers; + + @Persistent(table = "MANAGEDUSERS_PROJECTS_ROLES") + @Join(column = "PROJECT_ACCESS_ROLE_ID") + @Element(column = "MANAGEDUSER_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List managedUsers; + + @Persistent(table = "OIDCUSERS_PROJECTS_ROLES") + @Join(column = "PROJECT_ACCESS_ROLE_ID") + @Element(column = "OIDCUSER_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List oidcUsers; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public Project getProject() { + return project; + } + + public void setProject(Project project) { + this.project = project; + } + + public List getLdapUsers() { + return ldapUsers; + } + + public void setLdapUsers(List ldapUsers) { + this.ldapUsers = ldapUsers; + } + + public void addLdapUsers(LdapUser... ldapUsers) { + if (this.ldapUsers == null) { + this.ldapUsers = new ArrayList<>(Arrays.asList(ldapUsers)); + + return; + } + + for (var user : ldapUsers) + if (!this.ldapUsers.contains(user)) + this.ldapUsers.add(user); + } + + public List getManagedUsers() { + return managedUsers; + } + + public void setManagedUsers(List managedUsers) { + this.managedUsers = managedUsers; + } + + public void addManagedUsers(ManagedUser... managedUsers) { + if (this.managedUsers == null) { + this.managedUsers = new ArrayList<>(Arrays.asList(managedUsers)); + + return; + } + + for (var user : managedUsers) + if (!this.managedUsers.contains(user)) + this.managedUsers.add(user); + } + + public List getOidcUsers() { + return oidcUsers; + } + + public void setOidcUsers(List oidcUsers) { + this.oidcUsers = oidcUsers; + } + + public void addOidcUsers(OidcUser... oidcUsers) { + if (this.oidcUsers == null) { + this.oidcUsers = new ArrayList<>(Arrays.asList(oidcUsers)); + + return; + } + + for (var user : oidcUsers) + if (!this.oidcUsers.contains(user)) + this.oidcUsers.add(user); + } + +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/model/Role.java b/src/main/java/org/dependencytrack/model/Role.java new file mode 100644 index 0000000000..1b2ed3b042 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/Role.java @@ -0,0 +1,161 @@ +/* + * 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 alpine.common.validation.RegexSequence; +import alpine.model.Permission; +import alpine.server.json.TrimmedStringDeserializer; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.FetchGroup; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; +import javax.jdo.annotations.Unique; + +/** + * Model for tracking roles. Roles define static sets of permissions + * that can be applied to a user with the scope of a project. + * + * @author Allen Shearin + * @since 5.6.0 + */ +@PersistenceCapable +@FetchGroup(name = "ALL", members = { + @Persistent(name = "name"), + @Persistent(name = "description"), + @Persistent(name = "permissions"), +}) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Role implements Serializable { + + private static final long serialVersionUID = -7592438796591673355L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Unique(name = "ROLE_NAME_IDX", deferred = "true") + @Column(name = "NAME", jdbcType = "VARCHAR", allowsNull = "false") + @NotBlank + @Size(min = 1, max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, + message = "The name may only contain printable characters") + private String name; + + @Persistent + @Column(name = "DESCRIPTION", jdbcType = "VARCHAR") + @Size(max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, + message = "The description may only contain printable characters") + private String description; + + @Persistent(table = "ROLES_PERMISSIONS", defaultFetchGroup = "true") + @Unique(name = "ROLES_PERMISSIONS_IDX") + @Join(column = "ROLE_ID") + @Element(column = "PERMISSION_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) + private List permissions; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } + + public void addPermissions(Permission... permissions) { + if (this.permissions == null) { + this.permissions = new ArrayList<>(Arrays.asList(permissions)); + + return; + } + + for (var permission : permissions) + if (!this.permissions.contains(permission)) + this.permissions.add(permission); + } + + @Override + public String toString() { + var permissionStrings = permissions.stream() + .map(permission -> permission.getName()) + .toList(); + + return "%s{id=%d, name='%s', description='%s', permissions=%s}".formatted( + getClass().getSimpleName(), + id, + name, + description != null ? description : "", + permissionStrings); + } +} \ No newline at end of file diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index 6449865364..c33d445e0d 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -683,23 +683,31 @@ + + + + + + + + @@ -710,7 +718,7 @@ uniqueConstraintName="PROJECT_ACCESS_ROLES_COMPOSITE_IDX" foreignKeyName="PROJECT_ACCESS_ROLES_PROJECT_FK" referencedTableName="PROJECT" referencedColumnNames="ID" - deferrable="true" initiallyDeferred="true" deleteCascade="false" + deferrable="true" initiallyDeferred="true" deleteCascade="true" validateUnique="true" validateForeignKey="true" /> @@ -719,11 +727,19 @@ uniqueConstraintName="PROJECT_ACCESS_ROLES_COMPOSITE_IDX" foreignKeyName="PROJECT_ACCESS_ROLES_ROLE_FK" referencedTableName="ROLE" referencedColumnNames="ID" - deferrable="true" initiallyDeferred="true" deleteCascade="false" + deferrable="true" initiallyDeferred="true" deleteCascade="true" validateUnique="true" validateForeignKey="true" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 70a30fc9556eae08b68988cffd8ddd528f1447b8 Mon Sep 17 00:00:00 2001 From: jmayer-lm Date: Tue, 25 Feb 2025 15:02:10 -0500 Subject: [PATCH 015/181] Define new Role permissisons & Initial creation of RoleResource.java (#5) * Defining new role permissions Signed-off-by: Johnny Mayer * Defining new role permissions Signed-off-by: Johnny Mayer * Initial creation of RolesResource Signed-off-by: Johnny Mayer * address comments Signed-off-by: Johnny Mayer * Adding logger statement, removing getRoles() stub. Signed-off-by: Johnny Mayer * update @since, update permissions, added log statement Signed-off-by: Johnny Mayer --------- Signed-off-by: Johnny Mayer Signed-off-by: Allen Shearin --- .../org/dependencytrack/auth/Permissions.java | 10 + .../resources/v1/RoleResource.java | 181 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 src/main/java/org/dependencytrack/resources/v1/RoleResource.java diff --git a/src/main/java/org/dependencytrack/auth/Permissions.java b/src/main/java/org/dependencytrack/auth/Permissions.java index 982eefe1a7..a8e124a6b8 100644 --- a/src/main/java/org/dependencytrack/auth/Permissions.java +++ b/src/main/java/org/dependencytrack/auth/Permissions.java @@ -61,6 +61,11 @@ public enum Permissions { 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"), + ROLE_MANAGEMENT("Allows the creation, modification, and deletion of roles"), + ROLE_MANAGEMENT_CREATE("Allows the creation of roles"), + ROLE_MANAGEMENT_READ("Allows reading of roles"), + ROLE_MANAGEMENT_UPDATE("Allows update of roles"), + ROLE_MANAGEMENT_DELETE("Allows the deletion of roles"), TAG_MANAGEMENT("Allows the modification and deletion of tags"), TAG_MANAGEMENT_DELETE("Allows the deletion of a tag"), VIEW_BADGES("Provides the ability to view badges"); @@ -111,6 +116,11 @@ public static class Constants { public static final String POLICY_MANAGEMENT_READ = "POLICY_MANAGEMENT_READ"; public static final String POLICY_MANAGEMENT_UPDATE = "POLICY_MANAGEMENT_UPDATE"; public static final String POLICY_MANAGEMENT_DELETE = "POLICY_MANAGEMENT_DELETE"; + public static final String ROLE_MANAGEMENT = "ROLE_MANAGEMENT"; + public static final String ROLE_MANAGEMENT_CREATE = "ROLE_MANAGEMENT_CREATE"; + public static final String ROLE_MANAGEMENT_READ = "ROLE_MANAGEMENT_READ"; + public static final String ROLE_MANAGEMENT_UPDATE = "ROLE_MANAGEMENT_UPDATE"; + public static final String ROLE_MANAGEMENT_DELETE = "ROLE_MANAGEMENT_DELETE"; public static final String TAG_MANAGEMENT = "TAG_MANAGEMENT"; public static final String TAG_MANAGEMENT_DELETE = "TAG_MANAGEMENT_DELETE"; public static final String VIEW_BADGES = "VIEW_BADGES"; diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java new file mode 100644 index 0000000000..175531de0a --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -0,0 +1,181 @@ +/* + * 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.resources.v1; + +import alpine.common.logging.Logger; +import alpine.model.Permission; +import alpine.model.Role; +import alpine.model.UserPrincipal; +import alpine.server.auth.PermissionRequired; +import alpine.server.resources.AlpineResource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +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.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.model.validation.ValidUuid; +import org.dependencytrack.persistence.QueryManager; +import org.owasp.security.logging.SecurityMarkers; + +import java.util.List; + +/** + * JAX-RS resources for processing roles. + * + * @author Johnny Mayer + * @since 5.6.0 + */ +@Path("/v1/role") +@Tag(name = "role") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) +public class RoleResource extends AlpineResource { + + private static final Logger LOGGER = Logger.getLogger(RoleResource.class); + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of all roles", + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of all roles", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of roles", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Role.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ}) + public Response getRoles() { + return Response.ok(roles).header(TOTAL_COUNT_HEADER, totalCount).build(); + } + + @GET + @Path("/{uuid}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a specific role", + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A specific role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ}) + public Response getRole( + @Parameter(description = "The UUID of the role to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Returned role: " + role.getName()); + return Response.ok(role).build(); + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Creates a new role", + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "The created role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE}) + public Response createRole(Role jsonRole) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + role.getName()); + return Response.ok(role).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Updates a role's fields", + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_UPDATE

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "The updated role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE}) + public Response updateRole(Role jsonRole) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Updated role: " + role.getName()); + return Response.ok(role).build(); + } + + @DELETE + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Deletes a role", + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_DELETE

" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Role removed successfully"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE}) + public Response deleteRole(Role jsonRole) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Delete role: " + role.getName()); + return Response.ok(role).build(); + } + + + +} From 4c61f7ca2c23f7487d4aa2379d484bbb51b12709 Mon Sep 17 00:00:00 2001 From: lmphil <126618132+lmphil@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:20:36 -0400 Subject: [PATCH 016/181] add roleQueryManager method stubs (#6) Signed-off-by: Philippe Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 18 +++++++++ .../persistence/RoleQueryManager.java | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/main/java/org/dependencytrack/persistence/RoleQueryManager.java diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 80d56b286e..5501cd0891 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -24,6 +24,7 @@ import alpine.model.ApiKey; import alpine.model.ConfigProperty; import alpine.model.IConfigProperty.PropertyType; +import alpine.model.Permission; import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.notification.NotificationLevel; @@ -76,6 +77,7 @@ import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.Role; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vex; @@ -153,6 +155,7 @@ public class QueryManager extends AlpineQueryManager { private PolicyQueryManager policyQueryManager; private ProjectQueryManager projectQueryManager; private RepositoryQueryManager repositoryQueryManager; + private RoleQueryManager roleQueryManager; private ServiceComponentQueryManager serviceComponentQueryManager; private VexQueryManager vexQueryManager; private VulnerabilityQueryManager vulnerabilityQueryManager; @@ -442,6 +445,13 @@ private RepositoryQueryManager getRepositoryQueryManager() { return repositoryQueryManager; } + private RoleQueryManager getRoleQueryManager(){ + if (roleQueryManager == null) { + roleQueryManager = (request ==null) ? new RoleQueryManager(getPersistenceManager()) : new RoleQueryManager(getPersistenceManager(), request); + } + return roleQueryManager; + } + /** * Lazy instantiation of NotificationQueryManager. * @@ -1323,6 +1333,14 @@ public synchronized RepositoryMetaComponent synchronizeRepositoryMetaComponent(f return getRepositoryQueryManager().synchronizeRepositoryMetaComponent(transientRepositoryMetaComponent); } + public boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ + return getRoleQueryManager().addRoleToUser(principal, role, roleName, projectName); + } + + public boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName){ + return getRoleQueryManager().removeRoleFromUser(principal, role, roleName, projectName); + } + public NotificationRule createNotificationRule(String name, NotificationScope scope, NotificationLevel level, NotificationPublisher publisher) { return getNotificationQueryManager().createNotificationRule(name, scope, level, publisher); } diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java new file mode 100644 index 0000000000..dc7924137a --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -0,0 +1,40 @@ +package org.dependencytrack.persistence; + +import java.nio.file.attribute.UserPrincipal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jdo.PersistenceManager; +import javax.jdo.Query; + +import org.dependencytrack.model.Role; + +import alpine.common.logging.Logger; +import alpine.model.Permission; +import alpine.resources.AlpineRequest; + +final class RoleQueryManager extends QueryManager implements IQueryManager { + + private static final Logger LOGGER = Logger.getLogger(ProjectQueryManager.class); + + RoleQueryManager(final PersistenceManager pm) { + super(pm); + } + + RoleQueryManager(final PersistenceManager pm, final AlpineRequest request) { + super(pm, request); + } + + boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ + //WARNING: This method is a stub. + //TODO: Implement addRoleToUser + return true; + } + + boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName){ + //WARNING: This method is a stub. + //TODO: Implement removeRoleFromUser + return true; + } +} From 0bcefd8983916fb74848e92d9b48e3f921d346ce Mon Sep 17 00:00:00 2001 From: jmayer-lm Date: Thu, 27 Feb 2025 12:16:22 -0500 Subject: [PATCH 017/181] Add /v1/user Endpoints & CRUD Method Stubs (#7) * Add POST and DELETE role endpoints to UserResource Signed-off-by: Johnny Mayer * apply code style suggestions Signed-off-by: Johnny Mayer * Add CRUD method stubs to RoleQueryManager and QueryManager Signed-off-by: Johnny Mayer * Apply suggestions from code review Co-authored-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Signed-off-by: Allen Shearin --------- Signed-off-by: Johnny Mayer Signed-off-by: Allen Shearin Co-authored-by: Allen Shearin Co-authored-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 20 +++ .../persistence/RoleQueryManager.java | 30 +++- .../resources/v1/UserResource.java | 129 ++++++++++++++++-- 3 files changed, 161 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 5501cd0891..fbcc7dfab8 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -924,6 +924,26 @@ public void deletePolicyCondition(PolicyCondition policyCondition) { getPolicyQueryManager().deletePolicyCondition(policyCondition); } + public Role createRole(Role role) { + return getRoleQueryManager().createRole(role); + } + + public List getRoles() { + return getRoleQueryManager().getRoles(); + } + + public Role getRole(String uuid) { + return getRoleQueryManager().getRole(null); + } + + public Role updateRole(Role role) { + return getRoleQueryManager().updateRole(role); + } + + public boolean deleteRole(String uuid, boolean value) { + return getRoleQueryManager().deleteRole(uuid, value); + } + public Vulnerability createVulnerability(Vulnerability vulnerability, boolean commitIndex) { return getVulnerabilityQueryManager().createVulnerability(vulnerability, commitIndex); } diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index dc7924137a..94dae4f731 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -1,17 +1,14 @@ package org.dependencytrack.persistence; import java.nio.file.attribute.UserPrincipal; -import java.util.HashMap; +import java.util.Collections; import java.util.List; -import java.util.Map; import javax.jdo.PersistenceManager; -import javax.jdo.Query; import org.dependencytrack.model.Role; import alpine.common.logging.Logger; -import alpine.model.Permission; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { @@ -26,6 +23,31 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { super(pm, request); } + public Role createRole(Role role) { + // TODO:Implement role creation logic + return role; + } + + public List getRoles() { + // TODO:Implement role retrieval logic + return Collections.emptyList(); + } + + public Role getRole(String uuid) { + // TODO:Implement role retrieval logic + return null; + } + + public Role updateRole(Role role) { + // TODO:Implement role update logic + return role; + } + + public boolean deleteRole(String uuid, boolean value) { + // TODO:Implement role deletion logic + return false; + } + boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ //WARNING: This method is a stub. //TODO: Implement addRoleToUser diff --git a/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 249cb68d00..327c828362 100644 --- a/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -18,6 +18,22 @@ */ package org.dependencytrack.resources.v1; +import java.security.Principal; +import java.util.List; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.event.kafka.KafkaEventDispatcher; +import org.dependencytrack.model.IdentifiableObject; +import org.dependencytrack.model.Role; +import org.dependencytrack.notification.NotificationConstants; +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.proto.notification.v1.UserSubject; +import org.owasp.security.logging.SecurityMarkers; + import alpine.Config; import alpine.common.logging.Logger; import alpine.model.LdapUser; @@ -57,22 +73,9 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.event.kafka.KafkaEventDispatcher; -import org.dependencytrack.model.IdentifiableObject; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.proto.notification.v1.UserSubject; -import org.owasp.security.logging.SecurityMarkers; - -import java.security.Principal; -import java.util.List; -import java.util.Optional; /** * JAX-RS resources for processing users. @@ -775,4 +778,102 @@ private UserSubject buildUserSubject(final String username, final String email) Optional.ofNullable(email).ifPresent(userBuilder::setEmail); return userBuilder.build(); } + + @POST + @Path("/{username}/role") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Adds role to specific user.", + description = "

Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Updated user with a specific role", + content = @Content(schema = @Schema(implementation = UserPrincipal.class)) + ), + @ApiResponse(responseCode = "304", description = "The user has already been assigned to this role."), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user or role could not be found") + }) + @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) + public Response addRoleToUser( + @Parameter(description = "A valid username", required = true) + @PathParam("username") String username, + @Parameter(description = "The UUID of the role to associate username with", required = true) + IdentifiableObject identifiableObject, + @Parameter(description = "The name of the role", required = true) + @QueryParam("roleName") String roleName, + @Parameter(description = "The name of the project", required = true) + @QueryParam("projectName") String projectName) { + try (QueryManager qm = new QueryManager()) { + final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) + return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); + + final boolean modified = qm.addRoleToUser(principal, role, roleName, projectName); + principal = qm.getObjectById(principal.getClass(), principal.getId()); + if (modified) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s".formatted(principal.getName(), role.getName(), projectName)); + return Response.ok(principal).build(); + } + + return Response.status(Response.Status.NOT_MODIFIED).entity("The user is already a member of the specified role.").build(); + } + } + + @DELETE + @Path("/{username}/role") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Removes role from specific user.", + description = "

Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

" +) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Updated user with a specific role removed", + content = @Content(schema = @Schema(implementation = UserPrincipal.class)) + ), + @ApiResponse(responseCode = "204", description = "The role has been successfully removed from the user"), + @ApiResponse(responseCode = "304", description = "The user is not a member of the specified role"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user or role could not be found") +}) + @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) + public Response removeRoleFromUser( + @Parameter(description = "A valid username", required = true) + @PathParam("username") String username, + @Parameter(description = "The UUID of the role to remove from the username", required = true) + IdentifiableObject identifiableObject, + @Parameter(description = "The name of the role", required = true) + @QueryParam("roleName") String roleName, + @Parameter(description = "The name of the project", required = true) + @QueryParam("projectName") String projectName) { + try (QueryManager qm = new QueryManager()) { + final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) + return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); + + final boolean modified = qm.removeRoleFromUser(principal, role, roleName, projectName); + + principal = qm.getObjectById(principal.getClass(), principal.getId()); + if (modified) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed role membership for: %s / role: %s / project: %s".formatted(principal.getName(), role.getName(), projectName)); + return Response.noContent().build(); + } + + return Response.status(Response.Status.NOT_MODIFIED).entity("The user is not a member of the specified role.").build(); + } + } } From df11a6c22c254bd15bc5b73913e1c2dda54d42c6 Mon Sep 17 00:00:00 2001 From: EphraimEM Date: Thu, 27 Feb 2025 11:19:37 -0600 Subject: [PATCH 018/181] Add default roles and permissions for project management (#8) Signed-off-by: Ephraim Mensah Signed-off-by: Allen Shearin --- .../persistence/DefaultObjectGenerator.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index a226735182..fa2b4c030c 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -292,6 +292,103 @@ private List getBadgesPermissions(final List fullList) { } return permissions; } + + /** + * Loads the default Roles + */ + private void loadDefaultRoles() { + try (QueryManager qm = new QueryManager()) { + if (!qm.getRoles().isEmpty()) { + return; + } + LOGGER.info("Adding default roles to datastore"); + LOGGER.debug("Creating role: Project Admin"); + final Role projectAdmin = qm.createRole("Project Admin", false); + LOGGER.debug("Creating role: Project Auditor"); + final Role projectAuditor = qm.createRole("Project Auditor", false); + LOGGER.debug("Creating role: Project Editor"); + final Role projectEditor = qm.createRole("Project Editor", false); + LOGGER.debug("Creating role: Project Viewer"); + final Role projectViewer = qm.createRole("Project Viewer", true); + + final List fullList = qm.getPermissions(); + + LOGGER.debug("Assigning default permissions to roles"); + projectAdmin.setPermissions(getProjectAdminPermissions(fullList)); + projectAuditor.setPermissions(getProjectAuditorPermissions(fullList)); + projectEditor.setPermissions(getProjectEditorPermissions(fullList)); + projectViewer.setPermissions(getProjectViewerPermissions(fullList)); + + qm.persist(projectAdmin); + qm.persist(projectAuditor); + qm.persist(projectEditor); + qm.persist(projectViewer); + } + } + + private List getProjectAdminPermissions(final List fullList) { + final List permissions = new ArrayList<>(); + for (final Permission permission : fullList) { + if (permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT.name()) || + permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_CREATE.name()) || + permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_READ.name()) || + permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_UPDATE.name()) || + permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_DELETE.name()) || + permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS.name()) || + permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_CREATE.name()) || + permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_READ.name()) || + permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_UPDATE.name()) || + permission.getName().equals(Permissions.POLICY_MANAGEMENT.name()) || + permission.getName().equals(Permissions.POLICY_MANAGEMENT_CREATE.name()) || + permission.getName().equals(Permissions.POLICY_MANAGEMENT_READ.name()) || + permission.getName().equals(Permissions.POLICY_MANAGEMENT_UPDATE.name()) || + permission.getName().equals(Permissions.POLICY_MANAGEMENT_DELETE.name())) { + permissions.add(permission); + } + } + return permissions; + } + + private List getProjectAuditorPermissions(final List fullList) { + final List permissions = new ArrayList<>(); + for (final Permission permission : fullList) { + if (permission.getName().equals(Permissions.VIEW_PORTFOLIO.name()) || + permission.getName().equals(Permissions.VIEW_VULNERABILITY.name()) || + permission.getName().equals(Permissions.VIEW_POLICY_VIOLATION.name()) || + permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_READ.name())) { + permissions.add(permission); + } + } + return permissions; + } + + private List getProjectEditorPermissions(final List fullList) { + final List permissions = new ArrayList<>(); + for (final Permission permission : fullList) { + if (permission.getName().equals(Permissions.BOM_UPLOAD.name()) || + permission.getName().equals(Permissions.VIEW_PORTFOLIO.name()) || + permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_READ.name()) || + permission.getName().equals(Permissions.VIEW_VULNERABILITY.name()) || + permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_READ.name()) || + permission.getName().equals(Permissions.PROJECT_CREATION_UPLOAD.name())) { + permissions.add(permission); + } + } + return permissions; + } + + private List getProjectViewerPermissions(final List fullList) { + final List permissions = new ArrayList<>(); + for (final Permission permission : fullList) { + if (permission.getName().equals(Permissions.VIEW_PORTFOLIO.name()) || + permission.getName().equals(Permissions.VIEW_VULNERABILITY.name()) || + permission.getName().equals(Permissions.VIEW_BADGES.name())) { + permissions.add(permission); + } + } + return permissions; + } + /** * Loads the default repositories From 781656c6db862a1f15d11c8d37aaa03e37b4b0d9 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 27 Feb 2025 12:27:06 -0700 Subject: [PATCH 019/181] cleaup createRoles and loadDefaultRoles Signed-off-by: Allen Shearin --- .../persistence/DefaultObjectGenerator.java | 34 ++++++------- .../persistence/QueryManager.java | 6 +-- .../persistence/RoleQueryManager.java | 41 ++++++++++++---- .../resources/v1/RoleResource.java | 48 ++++++++++--------- 4 files changed, 75 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index fa2b4c030c..34e14000f3 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -31,6 +31,7 @@ import org.dependencytrack.common.ConfigKey; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; +import org.dependencytrack.model.Role; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter; @@ -301,31 +302,26 @@ private void loadDefaultRoles() { if (!qm.getRoles().isEmpty()) { return; } + LOGGER.debug("Assigning default permissions to roles"); + final List fullList = qm.getPermissions(); + LOGGER.info("Adding default roles to datastore"); LOGGER.debug("Creating role: Project Admin"); - final Role projectAdmin = qm.createRole("Project Admin", false); + final Role projectAdmin = qm.createRole("Project Admin", null, getProjectAdminPermissions(fullList)); LOGGER.debug("Creating role: Project Auditor"); - final Role projectAuditor = qm.createRole("Project Auditor", false); + final Role projectAuditor = qm.createRole("Project Auditor", null, getProjectAuditorPermissions(fullList)); LOGGER.debug("Creating role: Project Editor"); - final Role projectEditor = qm.createRole("Project Editor", false); + final Role projectEditor = qm.createRole("Project Editor", null, getProjectEditorPermissions(fullList)); LOGGER.debug("Creating role: Project Viewer"); - final Role projectViewer = qm.createRole("Project Viewer", true); - - final List fullList = qm.getPermissions(); - - LOGGER.debug("Assigning default permissions to roles"); - projectAdmin.setPermissions(getProjectAdminPermissions(fullList)); - projectAuditor.setPermissions(getProjectAuditorPermissions(fullList)); - projectEditor.setPermissions(getProjectEditorPermissions(fullList)); - projectViewer.setPermissions(getProjectViewerPermissions(fullList)); - + final Role projectViewer = qm.createRole("Project Viewer", null, getProjectViewerPermissions(fullList)); + qm.persist(projectAdmin); qm.persist(projectAuditor); qm.persist(projectEditor); qm.persist(projectViewer); } } - + private List getProjectAdminPermissions(final List fullList) { final List permissions = new ArrayList<>(); for (final Permission permission : fullList) { @@ -348,7 +344,7 @@ private List getProjectAdminPermissions(final List fullL } return permissions; } - + private List getProjectAuditorPermissions(final List fullList) { final List permissions = new ArrayList<>(); for (final Permission permission : fullList) { @@ -361,7 +357,7 @@ private List getProjectAuditorPermissions(final List ful } return permissions; } - + private List getProjectEditorPermissions(final List fullList) { final List permissions = new ArrayList<>(); for (final Permission permission : fullList) { @@ -376,7 +372,7 @@ private List getProjectEditorPermissions(final List full } return permissions; } - + private List getProjectViewerPermissions(final List fullList) { final List permissions = new ArrayList<>(); for (final Permission permission : fullList) { @@ -389,7 +385,6 @@ private List getProjectViewerPermissions(final List full return permissions; } - /** * Loads the default repositories */ @@ -425,7 +420,8 @@ private void loadDefaultConfigProperties() { 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()); + qm.createConfigProperty(cpc.getGroupName(), cpc.getPropertyName(), cpc.getDefaultPropertyValue(), + cpc.getPropertyType(), cpc.getDescription()); } } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index fbcc7dfab8..9910b232ae 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -924,11 +924,11 @@ public void deletePolicyCondition(PolicyCondition policyCondition) { getPolicyQueryManager().deletePolicyCondition(policyCondition); } - public Role createRole(Role role) { - return getRoleQueryManager().createRole(role); + public Role createRole(final String name, final String description, final List permissions) { + return getRoleQueryManager().createRole(name, description, permissions); } - public List getRoles() { + public List getRoles() { return getRoleQueryManager().getRoles(); } diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 94dae4f731..fe03019757 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -1,3 +1,21 @@ +/* + * 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 java.nio.file.attribute.UserPrincipal; @@ -9,6 +27,7 @@ import org.dependencytrack.model.Role; import alpine.common.logging.Logger; +import alpine.model.Permission; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { @@ -23,9 +42,13 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { super(pm, request); } - public Role createRole(Role role) { - // TODO:Implement role creation logic - return role; + public Role createRole(final String name, final String description, final List permissions) { + Role role = new Role(); + role.setName(name); + role.setDescription(description); + role.setPermissions(permissions); + + return persist(role); } public List getRoles() { @@ -48,15 +71,15 @@ public boolean deleteRole(String uuid, boolean value) { return false; } - boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ - //WARNING: This method is a stub. - //TODO: Implement addRoleToUser + boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName) { + // WARNING: This method is a stub. + // TODO: Implement addRoleToUser return true; } - boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName){ - //WARNING: This method is a stub. - //TODO: Implement removeRoleFromUser + boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName) { + // WARNING: This method is a stub. + // TODO: Implement removeRoleFromUser return true; } } diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 175531de0a..0d88ba23f4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -19,9 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; -import alpine.model.Permission; -import alpine.model.Role; -import alpine.model.UserPrincipal; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -50,10 +47,11 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.model.Role; import org.dependencytrack.persistence.QueryManager; import org.owasp.security.logging.SecurityMarkers; -import java.util.List; +import java.util.Collections; /** * JAX-RS resources for processing roles. @@ -86,10 +84,10 @@ public class RoleResource extends AlpineResource { ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRoles() { - return Response.ok(roles).header(TOTAL_COUNT_HEADER, totalCount).build(); - } + return Response.ok(Collections.emptyList()).header(TOTAL_COUNT_HEADER, 0).build(); + } @GET @Path("/{uuid}") @@ -107,12 +105,18 @@ public Response getRoles() { @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRole( - @Parameter(description = "The UUID of the role to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Returned role: " + role.getName()); - return Response.ok(role).build(); + @Parameter(description = "The UUID of the role to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { + try (QueryManager qm = new QueryManager()) { + Role role = qm.getObjectByUuid(Role.class, uuid); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); + + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Returned role: " + role.getName()); + return Response.ok(role).build(); + } } @PUT @@ -130,10 +134,10 @@ public Response getRole( ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE }) public Response createRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + role.getName()); - return Response.ok(role).build(); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + jsonRole.getName()); + return Response.ok(jsonRole).build(); } @POST @@ -152,10 +156,10 @@ public Response createRole(Role jsonRole) { @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE }) public Response updateRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Updated role: " + role.getName()); - return Response.ok(role).build(); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Updated role: " + jsonRole.getName()); + return Response.ok(jsonRole).build(); } @DELETE @@ -170,12 +174,10 @@ public Response updateRole(Role jsonRole) { @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE }) public Response deleteRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Delete role: " + role.getName()); - return Response.ok(role).build(); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Delete role: " + jsonRole.getName()); + return Response.ok(jsonRole).build(); } - - } From 594ca9c0155743da29036b22776876a6f5220eea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:12:42 +0000 Subject: [PATCH 020/181] Bump org.slf4j:log4j-over-slf4j from 2.0.16 to 2.0.17 Bumps org.slf4j:log4j-over-slf4j from 2.0.16 to 2.0.17. --- updated-dependencies: - dependency-name: org.slf4j:log4j-over-slf4j dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bf735e3363..dbefafee4b 100644 --- a/pom.xml +++ b/pom.xml @@ -126,7 +126,7 @@ 0.7.0 7.1.0 1.1.1 - 2.0.16 + 2.0.17 4.5.14 6.3.0 1.4.0 From 16610e21b53bce9dfcf37556807a55c5e93820af Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 27 Feb 2025 13:56:16 -0700 Subject: [PATCH 021/181] Flesh out stubbed api endpoints, call createDefaultRoles Signed-off-by: Allen Shearin Signed-off-by: Allen Shearin --- .../persistence/DefaultObjectGenerator.java | 1 + .../persistence/RoleQueryManager.java | 22 +++-- .../resources/v1/PermissionResource.java | 89 +++++++++++++++++++ .../resources/v1/RoleResource.java | 4 +- 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 34e14000f3..b8c02a22ab 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -119,6 +119,7 @@ private void executeLocked() { loadDefaultLicenses(); loadDefaultLicenseGroups(); loadDefaultRepositories(); + loadDefaultRoles(); loadDefaultConfigProperties(); loadDefaultNotificationPublishers(); diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index fe03019757..5628ee4fb3 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,16 +18,16 @@ */ package org.dependencytrack.persistence; -import java.nio.file.attribute.UserPrincipal; -import java.util.Collections; import java.util.List; import javax.jdo.PersistenceManager; +import javax.jdo.Query; import org.dependencytrack.model.Role; import alpine.common.logging.Logger; import alpine.model.Permission; +import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { @@ -42,6 +42,7 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { super(pm, request); } + @Override public Role createRole(final String name, final String description, final List permissions) { Role role = new Role(); role.setName(name); @@ -51,33 +52,42 @@ public Role createRole(final String name, final String description, final List

getRoles() { - // TODO:Implement role retrieval logic - return Collections.emptyList(); + final Query query = pm.newQuery(Role.class); + if (orderBy == null) + query.setOrdering("name asc"); + + return query.executeList(); } + @Override public Role getRole(String uuid) { // TODO:Implement role retrieval logic return null; } + @Override public Role updateRole(Role role) { // TODO:Implement role update logic return role; } + @Override public boolean deleteRole(String uuid, boolean value) { // TODO:Implement role deletion logic return false; } - boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName) { + @Override + public boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName) { // WARNING: This method is a stub. // TODO: Implement addRoleToUser return true; } - boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName) { + @Override + public boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName) { // WARNING: This method is a stub. // TODO: Implement removeRoleFromUser return true; diff --git a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 19d305a1d1..3a714c09d3 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -43,6 +43,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Role; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.owasp.security.logging.SecurityMarkers; @@ -221,6 +222,94 @@ public Response addPermissionToTeam( } } + @DELETE + @Path("/{permission}/role/{uuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + description = "

Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_DELETE

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "The updated role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "304", description = "The role already has the specified permission assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_DELETE}) + public Response removePermissionFromRole( + @Parameter(description = "A valid role uuid", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "A valid permission", required = true) + @PathParam("permission") String permissionName) { + try (QueryManager qm = new QueryManager()) { + Role role = qm.getObjectByUuid(Role.class, uuid); + if (role == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + } + final Permission permission = qm.getPermission(permissionName); + if (permission == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); + } + final List permissions = role.getPermissions(); + if (permissions != null && permissions.contains(permission)) { + permissions.remove(permission); + role.setPermissions(permissions); + role = qm.persist(role); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed permission for role: " + role.getName() + " / permission: " + permission.getName()); + return Response.ok(role).build(); + } + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + } + + @POST + @Path("/{permission}/role/{uuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + description = "

Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "The updated role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "304", description = "The role already has the specified permission assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) + public Response addPermissionToRole( + @Parameter(description = "A valid role uuid", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "A valid permission", required = true) + @PathParam("permission") String permissionName) { + try (QueryManager qm = new QueryManager()) { + Role role = qm.getObjectByUuid(Role.class, uuid); + if (role == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + } + final Permission permission = qm.getPermission(permissionName); + if (permission == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); + } + final List permissions = role.getPermissions(); + if (permissions != null && !permissions.contains(permission)) { + permissions.add(permission); + role.setPermissions(permissions); + role = qm.persist(role); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added permission for role: " + role.getName() + " / permission: " + permission.getName()); + return Response.ok(role).build(); + } + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + } + @DELETE @Path("/{permission}/team/{uuid}") @Consumes(MediaType.APPLICATION_JSON) diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 0d88ba23f4..2630f99293 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -137,7 +137,9 @@ public Response getRole( @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE }) public Response createRole(Role jsonRole) { super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + jsonRole.getName()); - return Response.ok(jsonRole).build(); + try (QueryManager qm = new QueryManager()) { + return Response.ok(qm.persist(jsonRole)).build(); + } } @POST From dd808a3e61b57e92e36a673b76bf82ac4e37d1a5 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 27 Feb 2025 16:31:26 -0700 Subject: [PATCH 022/181] update getRoles Signed-off-by: Allen Shearin --- .../org/dependencytrack/resources/v1/RoleResource.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 2630f99293..3072d65d0d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -51,8 +51,6 @@ import org.dependencytrack.persistence.QueryManager; import org.owasp.security.logging.SecurityMarkers; -import java.util.Collections; - /** * JAX-RS resources for processing roles. * @@ -86,8 +84,11 @@ public class RoleResource extends AlpineResource { }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRoles() { - return Response.ok(Collections.emptyList()).header(TOTAL_COUNT_HEADER, 0).build(); + try (QueryManager qm = new QueryManager()) { + return Response.ok(qm.getRoles()).header(TOTAL_COUNT_HEADER, 0).build(); + } } + @GET @Path("/{uuid}") From 7b301be3eff764e8daef3752e5cdbb34ea16ce47 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:33:54 -0600 Subject: [PATCH 023/181] fix: add role UUID field (#9) * fix: add role UUID field Signed-off-by: Jonathan Howard * fix: add uuid field to fetch group Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard Signed-off-by: Allen Shearin --- .../java/org/dependencytrack/model/Role.java | 20 ++++++++++++++++++- .../resources/migration/changelog-v5.6.0.xml | 6 ++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/Role.java b/src/main/java/org/dependencytrack/model/Role.java index 1b2ed3b042..47f8516575 100644 --- a/src/main/java/org/dependencytrack/model/Role.java +++ b/src/main/java/org/dependencytrack/model/Role.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @@ -34,6 +35,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; + import javax.jdo.annotations.Column; import javax.jdo.annotations.Element; import javax.jdo.annotations.Extension; @@ -58,6 +61,7 @@ @Persistent(name = "name"), @Persistent(name = "description"), @Persistent(name = "permissions"), + @Persistent(name = "uuid"), }) @JsonInclude(JsonInclude.Include.NON_NULL) public class Role implements Serializable { @@ -101,6 +105,12 @@ public enum FetchGroup { @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) private List permissions; + @Persistent(customValueStrategy = "uuid") + @Unique(name = "ROLE_UUID_IDX") + @Column(name = "UUID", sqlType = "UUID", allowsNull = "false") + @NotNull + private UUID uuid; + public long getId() { return id; } @@ -145,6 +155,14 @@ public void addPermissions(Permission... permissions) { this.permissions.add(permission); } + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + @Override public String toString() { var permissionStrings = permissions.stream() @@ -158,4 +176,4 @@ public String toString() { description != null ? description : "", permissionStrings); } -} \ No newline at end of file +} diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index c33d445e0d..58a35e1f17 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -678,6 +678,12 @@ + + + + From 5731b27e9708d36b7eafbc8b6c1586a2f8d10c79 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:52:25 -0600 Subject: [PATCH 024/181] refactor: implement role endpoint methods (#10) * refactor: implement role endpoint methods Signed-off-by: Jonathan Howard * style: restore original method order Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 16 ++-- .../persistence/RoleQueryManager.java | 44 ++++++---- .../persistence/jdbi/RoleDao.java | 36 ++++++++ .../resources/v1/RoleResource.java | 84 ++++++++++++------- .../resources/v1/UserResource.java | 71 +++++++++++----- 5 files changed, 173 insertions(+), 78 deletions(-) create mode 100644 src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 9910b232ae..e21cb836c3 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -936,12 +936,8 @@ public Role getRole(String uuid) { return getRoleQueryManager().getRole(null); } - public Role updateRole(Role role) { - return getRoleQueryManager().updateRole(role); - } - - public boolean deleteRole(String uuid, boolean value) { - return getRoleQueryManager().deleteRole(uuid, value); + public Role updateRole(Role transientRole) { + return getRoleQueryManager().updateRole(transientRole); } public Vulnerability createVulnerability(Vulnerability vulnerability, boolean commitIndex) { @@ -1353,12 +1349,12 @@ public synchronized RepositoryMetaComponent synchronizeRepositoryMetaComponent(f return getRepositoryQueryManager().synchronizeRepositoryMetaComponent(transientRepositoryMetaComponent); } - public boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ - return getRoleQueryManager().addRoleToUser(principal, role, roleName, projectName); + public boolean addRoleToUser(UserPrincipal principal, Role role, Project project){ + return getRoleQueryManager().addRoleToUser(principal, role, project); } - public boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName){ - return getRoleQueryManager().removeRoleFromUser(principal, role, roleName, projectName); + public boolean removeRoleFromUser(UserPrincipal principal, Role role, Project project){ + return getRoleQueryManager().removeRoleFromUser(principal, role, project); } public NotificationRule createNotificationRule(String name, NotificationScope scope, NotificationLevel level, NotificationPublisher publisher) { diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 5628ee4fb3..9681746ad2 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,11 +18,13 @@ */ package org.dependencytrack.persistence; +import java.util.Collections; import java.util.List; import javax.jdo.PersistenceManager; import javax.jdo.Query; +import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; import alpine.common.logging.Logger; @@ -32,7 +34,7 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { - private static final Logger LOGGER = Logger.getLogger(ProjectQueryManager.class); + private static final Logger LOGGER = Logger.getLogger(RoleQueryManager.class); RoleQueryManager(final PersistenceManager pm) { super(pm); @@ -63,33 +65,47 @@ public List getRoles() { @Override public Role getRole(String uuid) { - // TODO:Implement role retrieval logic - return null; + final Query query = pm.newQuery(Role.class, "uuid == :uuid"); + + return query.executeUnique(); } - @Override - public Role updateRole(Role role) { - // TODO:Implement role update logic - return role; + public List getUnassignedProjects(final String username) { + return getUnassignedProjects(getUserPrincipal(username)); + } + + public List getUnassignedProjects(final UserPrincipal principal) { + // TODO: Implement getUnassignedProjects + return Collections.emptyList(); + } + + public List getUnassignedRolePermissions(final Role role) { + // TODO: Implement getUnassignedRolePermissions + return Collections.emptyList(); } @Override - public boolean deleteRole(String uuid, boolean value) { - // TODO:Implement role deletion logic - return false; + public Role updateRole(Role transientRole) { + final Role role = getObjectByUuid(Role.class, transientRole.getUuid()); + if (role == null) + return null; + + role.setName(transientRole.getName()); + role.setDescription(transientRole.getDescription()); + + return persist(role); } @Override - public boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName) { - // WARNING: This method is a stub. + public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { // TODO: Implement addRoleToUser return true; } @Override - public boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName) { - // WARNING: This method is a stub. + public boolean removeRoleFromUser(UserPrincipal principal, Role role, Project project) { // TODO: Implement removeRoleFromUser return true; } + } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java new file mode 100644 index 0000000000..c64a25de4e --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -0,0 +1,36 @@ +/* + * 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.jdbi; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** + * @since 5.6.0 + */ +public interface RoleDao { + + @SqlUpdate(""" + DELETE + FROM "ROLE" + WHERE "ID" = :roleId + """) + int deleteRole(@Bind final long roleId); + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 3072d65d0d..c1445ef64c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -49,8 +49,12 @@ import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.model.Role; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.RoleDao; +import org.jdbi.v3.core.Handle; import org.owasp.security.logging.SecurityMarkers; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; + /** * JAX-RS resources for processing roles. * @@ -71,51 +75,50 @@ public class RoleResource extends AlpineResource { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Returns a list of all roles", - description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

" - ) + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "A list of all roles", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of roles", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Role.class))) - ), + headers = @Header( + name = TOTAL_COUNT_HEADER, + description = "The total number of roles", + schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Role.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRoles() { try (QueryManager qm = new QueryManager()) { - return Response.ok(qm.getRoles()).header(TOTAL_COUNT_HEADER, 0).build(); + return Response.ok(qm.getRoles()).header(TOTAL_COUNT_HEADER, qm.getCount(Role.class)).build(); } } - @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Returns a specific role", - description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

" - ) + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "A specific role", - content = @Content(schema = @Schema(implementation = Role.class)) - ), + content = @Content(schema = @Schema(implementation = Role.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRole( - @Parameter(description = "The UUID of the role to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { + @Parameter( + description = "The UUID of the role to retrieve", + schema = @Schema(type = "string", format = "uuid"), + required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { Role role = qm.getObjectByUuid(Role.class, uuid); if (role == null) - return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, - "Returned role: " + role.getName()); return Response.ok(role).build(); } } @@ -125,21 +128,23 @@ public Response getRole( @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Creates a new role", - description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

" - ) + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

") @ApiResponses(value = { @ApiResponse( responseCode = "201", description = "The created role", - content = @Content(schema = @Schema(implementation = Role.class)) - ), + content = @Content(schema = @Schema(implementation = Role.class))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE }) public Response createRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + jsonRole.getName()); + failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); + try (QueryManager qm = new QueryManager()) { - return Response.ok(qm.persist(jsonRole)).build(); + final Role role = qm.createRole(jsonRole.getName(), jsonRole.getDescription(), jsonRole.getPermissions()); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role created: " + role.getName()); + + return Response.status(Response.Status.CREATED).entity(role).build(); } } @@ -148,21 +153,28 @@ public Response createRole(Role jsonRole) { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Updates a role's fields", - description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_UPDATE

" - ) + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_UPDATE

") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "The updated role", - content = @Content(schema = @Schema(implementation = Role.class)) - ), + content = @Content(schema = @Schema(implementation = Role.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE }) public Response updateRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Updated role: " + jsonRole.getName()); - return Response.ok(jsonRole).build(); + failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); + + try (QueryManager qm = new QueryManager()) { + Role role = qm.updateRole(jsonRole); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role updated: " + role.getName()); + + return Response.ok(role).build(); + } } @DELETE @@ -170,8 +182,7 @@ public Response updateRole(Role jsonRole) { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Deletes a role", - description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_DELETE

" - ) + description = "

Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_DELETE

") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Role removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -179,8 +190,19 @@ public Response updateRole(Role jsonRole) { }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE }) public Response deleteRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Delete role: " + jsonRole.getName()); - return Response.ok(jsonRole).build(); + try (QueryManager qm = new QueryManager()) { + final Role role = qm.getObjectByUuid(Role.class, jsonRole.getUuid(), Role.FetchGroup.ALL.name()); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + try (final Handle jdbiHandle = openJdbiHandle()) { + jdbiHandle.attach(RoleDao.class).deleteRole(role.getId()); + } + + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role deleted: " + role.getName()); + + return Response.noContent().build(); + } } } diff --git a/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 327c828362..347bf46a53 100644 --- a/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -26,6 +26,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.model.IdentifiableObject; +import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; @@ -800,13 +801,19 @@ private UserSubject buildUserSubject(final String username, final String email) @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) public Response addRoleToUser( @Parameter(description = "A valid username", required = true) - @PathParam("username") String username, + @PathParam("username") + String username, + @Parameter(description = "The UUID of the role to associate username with", required = true) - IdentifiableObject identifiableObject, - @Parameter(description = "The name of the role", required = true) - @QueryParam("roleName") String roleName, + IdentifiableObject identifiableObject, + @Parameter(description = "The name of the project", required = true) - @QueryParam("projectName") String projectName) { + @QueryParam("projectName") + String projectName, + + @Parameter(description = "The version of the project") + @QueryParam("projectVersion") + String projectVersion) { try (QueryManager qm = new QueryManager()) { final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); if (role == null) @@ -816,14 +823,20 @@ public Response addRoleToUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - final boolean modified = qm.addRoleToUser(principal, role, roleName, projectName); + Project project = qm.getProject(projectName, projectVersion); + if (project == null) + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + + final boolean modified = qm.addRoleToUser(principal, role, project); + if (!modified) + return Response.notModified().entity("The user is already a member of the specified role.").build(); + principal = qm.getObjectById(principal.getClass(), principal.getId()); - if (modified) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s".formatted(principal.getName(), role.getName(), projectName)); - return Response.ok(principal).build(); - } + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Added role membership for: %s / role: %s / project: %s" + .formatted(principal.getName(), role.getName(), projectName)); - return Response.status(Response.Status.NOT_MODIFIED).entity("The user is already a member of the specified role.").build(); + return Response.ok(principal).build(); } } @@ -849,13 +862,19 @@ public Response addRoleToUser( @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) public Response removeRoleFromUser( @Parameter(description = "A valid username", required = true) - @PathParam("username") String username, - @Parameter(description = "The UUID of the role to remove from the username", required = true) - IdentifiableObject identifiableObject, - @Parameter(description = "The name of the role", required = true) - @QueryParam("roleName") String roleName, + @PathParam("username") + String username, + + @Parameter(description = "The UUID of the role to associate username with", required = true) + IdentifiableObject identifiableObject, + @Parameter(description = "The name of the project", required = true) - @QueryParam("projectName") String projectName) { + @QueryParam("projectName") + String projectName, + + @Parameter(description = "The version of the project") + @QueryParam("projectVersion") + String projectVersion) { try (QueryManager qm = new QueryManager()) { final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); if (role == null) @@ -865,15 +884,21 @@ public Response removeRoleFromUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - final boolean modified = qm.removeRoleFromUser(principal, role, roleName, projectName); + Project project = qm.getProject(projectName, projectVersion); + if (project == null) + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + + final boolean modified = qm.removeRoleFromUser(principal, role, project); + if (!modified) + return Response.notModified().entity("The user is not a member of the specified role.").build(); principal = qm.getObjectById(principal.getClass(), principal.getId()); - if (modified) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed role membership for: %s / role: %s / project: %s".formatted(principal.getName(), role.getName(), projectName)); - return Response.noContent().build(); - } + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Removed role membership for: %s / role: %s / project: %s" + .formatted(principal.getName(), role.getName(), projectName)); - return Response.status(Response.Status.NOT_MODIFIED).entity("The user is not a member of the specified role.").build(); + return Response.noContent().build(); } } + } From d856857902b8d3cfdf8e1e17bd3c43883a0203c0 Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 20 Feb 2025 16:32:14 +0100 Subject: [PATCH 025/181] Improve coverage of portfolio ACL checks in REST API endpoints Signed-off-by: nscuro Signed-off-by: Allen Shearin --- .../org/dependencytrack/common/MdcScope.java | 49 +++ .../ProjectAccessDeniedException.java | 30 ++ .../persistence/PolicyQueryManager.java | 43 --- .../persistence/QueryManager.java | 5 - .../VulnerabilityQueryManager.java | 165 +-------- .../persistence/jdbi/VulnerabilityDao.java | 18 +- .../resources/v1/AbstractApiResource.java | 78 +++++ .../v1/AbstractConfigPropertyResource.java | 13 +- .../resources/v1/AnalysisResource.java | 35 +- .../resources/v1/BadgeResource.java | 52 +-- .../resources/v1/BomResource.java | 97 +++--- .../v1/ComponentPropertyResource.java | 105 +++--- .../resources/v1/ComponentResource.java | 182 +++++----- .../resources/v1/DependencyGraphResource.java | 42 +-- .../resources/v1/FindingResource.java | 104 +++--- .../resources/v1/MetricsResource.java | 112 +++--- .../v1/NotificationRuleResource.java | 34 +- .../resources/v1/PolicyResource.java | 30 +- .../resources/v1/PolicyViolationResource.java | 54 +-- .../resources/v1/ProjectPropertyResource.java | 126 +++---- .../resources/v1/ProjectResource.java | 222 ++++++------ .../resources/v1/ServiceResource.java | 79 ++--- .../resources/v1/VexResource.java | 55 +-- .../v1/ViolationAnalysisResource.java | 33 +- .../resources/v1/VulnerabilityResource.java | 117 +++---- .../ProjectAccessDeniedExceptionMapper.java | 43 +++ .../org/dependencytrack/JerseyTestRule.java | 5 +- .../resources/v1/AnalysisResourceTest.java | 115 ++++++- .../resources/v1/BadgeResourceTest.java | 2 +- .../resources/v1/BomResourceTest.java | 86 ++++- .../v1/ComponentPropertyResourceTest.java | 120 +++++++ .../resources/v1/ComponentResourceTest.java | 322 ++++++++++++++---- .../v1/DependencyGraphResourceTest.java | 69 ++++ .../resources/v1/FindingResourceTest.java | 59 ++++ .../resources/v1/MetricsResourceTest.java | 316 +++++++++++++++++ .../v1/NotificationRuleResourceTest.java | 80 ++++- .../resources/v1/PolicyResourceTest.java | 87 ++++- .../v1/PolicyViolationResourceTest.java | 93 ++++- .../v1/ProjectPropertyResourceTest.java | 193 ++++++++++- .../resources/v1/ProjectResourceTest.java | 297 +++++++++++++++- .../resources/v1/TagResourceTest.java | 16 +- .../resources/v1/VexResourceTest.java | 84 ++++- .../v1/ViolationAnalysisResourceTest.java | 84 +++++ .../v1/VulnerabilityResourceTest.java | 175 +++++++++- 44 files changed, 3066 insertions(+), 1060 deletions(-) create mode 100644 src/main/java/org/dependencytrack/common/MdcScope.java create mode 100644 src/main/java/org/dependencytrack/exception/ProjectAccessDeniedException.java create mode 100644 src/main/java/org/dependencytrack/resources/v1/AbstractApiResource.java create mode 100644 src/main/java/org/dependencytrack/resources/v1/exception/ProjectAccessDeniedExceptionMapper.java create mode 100644 src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java diff --git a/src/main/java/org/dependencytrack/common/MdcScope.java b/src/main/java/org/dependencytrack/common/MdcScope.java new file mode 100644 index 0000000000..9a990d9f8b --- /dev/null +++ b/src/main/java/org/dependencytrack/common/MdcScope.java @@ -0,0 +1,49 @@ +/* + * 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.common; + +import org.slf4j.MDC; +import org.slf4j.MDC.MDCCloseable; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @since 5.6.0 + */ +public class MdcScope implements Closeable { + + private final List mdcCloseables = new ArrayList<>(); + + public MdcScope(final Map variables) { + for (final Map.Entry entry : variables.entrySet()) { + if (MDC.get(entry.getKey()) == null) { + mdcCloseables.add(MDC.putCloseable(entry.getKey(), entry.getValue())); + } + } + } + + @Override + public void close() { + mdcCloseables.forEach(MDCCloseable::close); + } + +} diff --git a/src/main/java/org/dependencytrack/exception/ProjectAccessDeniedException.java b/src/main/java/org/dependencytrack/exception/ProjectAccessDeniedException.java new file mode 100644 index 0000000000..d063939f28 --- /dev/null +++ b/src/main/java/org/dependencytrack/exception/ProjectAccessDeniedException.java @@ -0,0 +1,30 @@ +/* + * 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.exception; + +/** + * @since 5.6.0 + */ +public class ProjectAccessDeniedException extends RuntimeException { + + public ProjectAccessDeniedException(final String message) { + super(message); + } + +} diff --git a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java index 55b806d35a..9194658502 100644 --- a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java @@ -18,13 +18,9 @@ */ package org.dependencytrack.persistence; -import alpine.model.ApiKey; -import alpine.model.Team; -import alpine.model.UserPrincipal; import alpine.persistence.PaginatedResult; import alpine.resources.AlpineRequest; import org.dependencytrack.model.Component; -import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; import org.dependencytrack.model.Policy; @@ -804,43 +800,4 @@ private void processInputFilter(Map params, List filterC } } - @Override - void preprocessACLs(final Query query, final String inputFilter, final Map params, final boolean bypass) { - if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && !bypass) { - final List teams; - if (super.principal instanceof UserPrincipal) { - final UserPrincipal userPrincipal = ((UserPrincipal) super.principal); - teams = userPrincipal.getTeams(); - if (super.hasAccessManagementPermission(userPrincipal)) { - query.setFilter(inputFilter); - return; - } - } else { - final ApiKey apiKey = ((ApiKey) super.principal); - teams = apiKey.getTeams(); - if (super.hasAccessManagementPermission(apiKey)) { - query.setFilter(inputFilter); - return; - } - } - if (teams != null && teams.size() > 0) { - final StringBuilder sb = new StringBuilder(); - for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) { - final Team team = super.getObjectById(Team.class, teams.get(i).getId()); - sb.append(" project.accessTeams.contains(:team").append(i).append(") "); - params.put("team" + i, team); - if (i < teamsSize-1) { - sb.append(" || "); - } - } - if (inputFilter != null) { - query.setFilter(inputFilter + " && (" + sb.toString() + ")"); - } else { - query.setFilter(sb.toString()); - } - } - } else { - query.setFilter(inputFilter); - } - } } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index e21cb836c3..29ad2e03c9 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -99,7 +99,6 @@ import org.dependencytrack.proto.vulnanalysis.v1.ScanResult; import org.dependencytrack.proto.vulnanalysis.v1.ScanStatus; import org.dependencytrack.proto.vulnanalysis.v1.ScannerResult; -import org.dependencytrack.resources.v1.vo.AffectedProject; import org.dependencytrack.resources.v1.vo.DependencyGraphResponse; import org.dependencytrack.tasks.IntegrityMetaInitializerTask; @@ -1195,10 +1194,6 @@ public long getSuppressedCount(Project project, Component component) { return getFindingsQueryManager().getSuppressedCount(project, component); } - public List getAffectedProjects(Vulnerability vulnerability) { - return getVulnerabilityQueryManager().getAffectedProjects(vulnerability); - } - public VulnerabilityAlias synchronizeVulnerabilityAlias(VulnerabilityAlias alias) { return getVulnerabilityQueryManager().synchronizeVulnerabilityAlias(alias); } diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index b1e35a3acf..0b56851020 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -25,7 +25,6 @@ import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalyzerIdentity; import org.dependencytrack.model.Component; -import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Epss; import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.Project; @@ -36,7 +35,6 @@ import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.persistence.jdbi.VulnerabilityDao; import org.dependencytrack.persistence.jdbi.VulnerabilityDao.AffectedProjectCountRow; -import org.dependencytrack.resources.v1.vo.AffectedProject; import org.jdbi.v3.core.Handle; import javax.jdo.PersistenceManager; @@ -49,13 +47,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; -import static org.dependencytrack.util.PrincipalUtil.getPrincipalTeamIds; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; final class VulnerabilityQueryManager extends QueryManager implements IQueryManager { @@ -316,9 +312,19 @@ public PaginatedResult getVulnerabilities() { result = execute(query); } Map matchedEpssList = getEpssForCveIds( - result.getList(Vulnerability.class).stream().map(vuln -> vuln.getVulnId()).distinct().toList()); + result.getList(Vulnerability.class).stream().map(Vulnerability::getVulnId).distinct().toList()); + final Map affectedProjectCountRows = withJdbiHandle( + this.request, + jdbiHandle -> jdbiHandle.attach(VulnerabilityDao.class).getAffectedProjectCount( + result.getList(Vulnerability.class).stream().map(Vulnerability::getId).toList(), + /* includeSuppressed */ false)).stream() + .collect(Collectors.toMap(AffectedProjectCountRow::id, Function.identity())); for (final Vulnerability vulnerability : result.getList(Vulnerability.class)) { - vulnerability.setAffectedProjectCount(this.getAffectedProjectCount(vulnerability)); + final AffectedProjectCountRow affectedProjects = affectedProjectCountRows.get(vulnerability.getId()); + if (affectedProjects != null) { + vulnerability.setAffectedProjectCount(affectedProjects.totalProjectCount()); + vulnerability.setAffectedInactiveProjectCount(affectedProjects.totalProjectCount() - affectedProjects.activeProjectCount()); + } vulnerability.setAliases(getVulnerabilityAliases(vulnerability)); vulnerability.setEpss(matchedEpssList.get(vulnerability.getVulnId())); } @@ -343,10 +349,12 @@ public PaginatedResult getVulnerabilities(Component component, boolean includeSu List componentVulnerabilities; List affectedProjectCounts; - try (final Handle jdbiHandle = openJdbiHandle((this.request))) { + try (final Handle jdbiHandle = openJdbiHandle(this.request)) { final var dao = jdbiHandle.attach(VulnerabilityDao.class); componentVulnerabilities = dao.getVulnerabilitiesByComponent(component.getId(), includeSuppressed); - affectedProjectCounts = dao.getAffectedProjectCount(componentVulnerabilities.stream().map(Vulnerability::getId).toList()); + affectedProjectCounts = dao.getAffectedProjectCount( + componentVulnerabilities.stream().map(Vulnerability::getId).toList(), + includeSuppressed); } Map vulnById = componentVulnerabilities.stream().collect(Collectors.toMap(Vulnerability::getId, vulnerability -> vulnerability)); for (var vulnerabilityProjectCount : affectedProjectCounts) { @@ -476,145 +484,6 @@ private String generateExcludeSuppressed(Project project, Component component) { return excludeClause; } - /** - * Generates partial JDOQL statement excluding suppressed vulnerabilities for this project. - * @param project the project to query on - * @return a partial where clause - */ - private String generateExcludeSuppressed(Project project) { - return generateExcludeSuppressed(project, null); - } - - /** - * Returns a {@link List} of {@link Project}s affected by a given {@link Vulnerability}. - * - * @param vulnerability The {@link Vulnerability} to query on - * @param fetchGroups The fetch groups to use - * @return A {@link List} of {@link Project}s - */ - public List getProjects(final Vulnerability vulnerability, final Set fetchGroups) { - // It is not possible to select distinct objects, so select IDs instead - // and fetch objects in a second step. This is still a lot more efficient - // than fetching objects first and then de-duplicating them in memory. - var queryStr = """ - SELECT DISTINCT this.project.id - FROM org.dependencytrack.model.Component - WHERE this.vulnerabilities.contains(:vuln) - && (SELECT FROM org.dependencytrack.model.Analysis a - WHERE a.component == this - && a.vulnerability == :vuln - && a.suppressed == true).isEmpty() - """; - final var params = new HashMap(); - params.put("vuln", vulnerability); - - if (isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) - && !hasAccessManagementPermission(principal)) { - queryStr += """ - && this.project.accessTeams.contains(team) - && :teamIds.contains(team.id) - VARIABLES alpine.model.Team team - """; - params.put("teamIds", getPrincipalTeamIds(super.principal)); - } - - // TODO: This query should support pagination - - final Query query = pm.newQuery(Query.JDOQL, queryStr); - final List affectedProjectIds; - try { - query.setNamedParameters(params); - affectedProjectIds = List.copyOf(query.executeResultList(Long.class)); - } finally { - query.closeAll(); - } - - return getObjectsById(Project.class, affectedProjectIds, fetchGroups); - } - - /** - * Returns a List of Projects affected by a specific vulnerability. - * @param vulnerability the vulnerability to query on - * @return a List of AffectedProjects - */ - public List getAffectedProjects(Vulnerability vulnerability) { - // Fetch all projects affected by the given vulnerability. - // Group them by their UUID to ease additional enrichment. - final Map affectedProjects = getProjects(vulnerability, Set.of(Project.FetchGroup.IDENTIFIERS.name())).stream() - .map(project -> new AffectedProject( - project.getUuid(), - project.getDirectDependencies() != null, - project.getName(), - project.getVersion(), project.isActive(), - null - )) - .collect(Collectors.toMap(affectedProject -> affectedProject.getUuid().toString(), Function.identity())); - - // Fetch UUIDs of components that are part of the affected projects, - // and affected by the given vulnerability. Return both the component's UUID, - // but also the UUID of the project it belongs to, so we can correlate. - final Query query = pm.newQuery(Component.class); - query.setFilter(":projectUuids.contains(project.uuid) && vulnerabilities.contains(:vulnerability)"); - query.setParameters(affectedProjects.keySet(), vulnerability); - query.setResult("uuid, project.uuid"); - final List resultRows; - try { - resultRows = List.copyOf(query.executeResultList(Object[].class)); - } finally { - query.closeAll(); - } - - // "Enrich" affectedProjects with component UUIDs. - for (final Object[] resultRow : resultRows) { - final String componentUuid = String.valueOf(resultRow[0]); - final String projectUuid = String.valueOf(resultRow[1]); - affectedProjects.get(projectUuid).getAffectedComponentUuids().add(UUID.fromString(componentUuid)); - } - - return new ArrayList<>(affectedProjects.values()); - } - - /** - * Returns the number of {@link Project}s affected by a given {@link Vulnerability}. - * - * @param vulnerability The {@link Vulnerability} to query on - * @return Number of affected {@link Project}s - */ - public int getAffectedProjectCount(final Vulnerability vulnerability) { - var queryStr = """ - SELECT COUNT(DISTINCT this.project.id) - FROM org.dependencytrack.model.Component - WHERE this.vulnerabilities.contains(:vuln) - && (SELECT FROM org.dependencytrack.model.Analysis a - WHERE a.component == this - && a.vulnerability == :vuln - && a.suppressed == true).isEmpty() - """; - final var params = new HashMap(); - params.put("vuln", vulnerability); - - if (isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) - && !hasAccessManagementPermission(principal)) { - queryStr += """ - && this.project.accessTeams.contains(team) - && :teamIds.contains(team.id) - VARIABLES alpine.model.Team team - """; - params.put("teamIds", getPrincipalTeamIds(super.principal)); - } - - final Query query = pm.newQuery(Query.JDOQL, queryStr); - final long affectedProjectCount; - try { - query.setNamedParameters(params); - affectedProjectCount = query.executeResultUnique(Long.class); - } finally { - query.closeAll(); - } - - return Math.toIntExact(affectedProjectCount); - } - public synchronized VulnerabilityAlias synchronizeVulnerabilityAlias(final VulnerabilityAlias alias) { return callInTransaction(() -> { // Query existing aliases that match AT LEAST ONE identifier of the given alias. diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityDao.java index a8199f7290..c77fad4973 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityDao.java @@ -31,6 +31,7 @@ import org.jdbi.v3.sqlobject.config.RegisterFieldMapper; import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.customizer.Define; import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; @@ -281,7 +282,9 @@ record AffectedProjectListRow( @RegisterRowMapper(VulnerabilityRowMapper.class) List getVulnerabilitiesByComponent(@Bind Long componentId, @Bind boolean includeSuppressed); - @SqlQuery(""" + @SqlQuery(/* language=InjectedFreeMarker */ """ + <#-- @ftlvariable name="includeSuppressed" type="boolean" --> + <#-- @ftlvariable name="apiProjectAclCondition" type="String" --> SELECT "VULNERABILITY"."ID" AS "id" , COUNT("PROJECT"."ID") AS "totalProjectCount" , COUNT(*) FILTER (WHERE "PROJECT"."INACTIVE_SINCE" IS NULL) AS "activeProjectCount" @@ -292,11 +295,22 @@ record AffectedProjectListRow( ON "COMPONENTS_VULNERABILITIES"."COMPONENT_ID" = "COMPONENT"."ID" INNER JOIN "PROJECT" ON "COMPONENT"."PROJECT_ID" = "PROJECT"."ID" + <#if !includeSuppressed> + LEFT JOIN "ANALYSIS" + ON "ANALYSIS"."VULNERABILITY_ID" = "VULNERABILITY"."ID" + AND "ANALYSIS"."COMPONENT_ID" = "COMPONENT"."ID" + WHERE "VULNERABILITY"."ID" = ANY(:vulnerabilityIds) + <#if !includeSuppressed> + AND ("ANALYSIS"."SUPPRESSED" IS NULL OR NOT "ANALYSIS"."SUPPRESSED") + + AND ${apiProjectAclCondition!"TRUE"} GROUP BY "VULNERABILITY"."ID" """) @RegisterConstructorMapper(AffectedProjectCountRow.class) - List getAffectedProjectCount(@Bind List vulnerabilityIds); + List getAffectedProjectCount( + @Bind List vulnerabilityIds, + @Define boolean includeSuppressed); record AffectedProjectCountRow( long id, diff --git a/src/main/java/org/dependencytrack/resources/v1/AbstractApiResource.java b/src/main/java/org/dependencytrack/resources/v1/AbstractApiResource.java new file mode 100644 index 0000000000..aeeeb12f73 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/AbstractApiResource.java @@ -0,0 +1,78 @@ +/* + * 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.resources.v1; + +import alpine.common.logging.Logger; +import alpine.server.resources.AlpineResource; +import org.dependencytrack.common.MdcScope; +import org.dependencytrack.exception.ProjectAccessDeniedException; +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.QueryManager; +import org.owasp.security.logging.SecurityMarkers; + +import java.util.Map; + +import static java.util.Objects.requireNonNullElse; +import static org.dependencytrack.common.MdcKeys.MDC_PROJECT_NAME; +import static org.dependencytrack.common.MdcKeys.MDC_PROJECT_UUID; +import static org.dependencytrack.common.MdcKeys.MDC_PROJECT_VERSION; + +/** + * @since 5.6.0 + */ +abstract class AbstractApiResource extends AlpineResource { + + private final Logger logger = Logger.getLogger(this.getClass()); + + /** + * @see #requireAccess(QueryManager, Project, String) + */ + void requireAccess(final QueryManager qm, final Project project) { + requireAccess(qm, project, null); + } + + /** + * Asserts that the authenticated {@link java.security.Principal} has access to a given {@link Project}. + * + * @param qm The {@link QueryManager} to use. + * @param project The {@link Project} to verify access permission for. + * @param message The message to use if a {@link ProjectAccessDeniedException} is thrown. + * @throws ProjectAccessDeniedException When the authenticated {@link java.security.Principal} + * does not have access to the given {@link Project}. + */ + void requireAccess(final QueryManager qm, final Project project, final String message) { + // TODO: Could make sense to cache the result for at least a few seconds. + // Frontend and API clients tend to make multiple successive requests targeting + // the same project. If we can avoid this overhead for even a few of those + // requests, that would already help under high traffic conditions. + + if (!qm.hasAccess(super.getPrincipal(), project)) { + try (var ignored = new MdcScope(Map.ofEntries( + Map.entry(MDC_PROJECT_UUID, project.getUuid().toString()), + Map.entry(MDC_PROJECT_NAME, project.getName()), + Map.entry(MDC_PROJECT_VERSION, String.valueOf(project.getVersion()))))) { + logSecurityEvent(logger, SecurityMarkers.SECURITY_FAILURE, "Unauthorized project access attempt"); + } + + throw new ProjectAccessDeniedException(requireNonNullElse( + message, "Access to the requested project is forbidden")); + } + } + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/AbstractConfigPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/AbstractConfigPropertyResource.java index 8e31105c7a..2549f91f40 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AbstractConfigPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AbstractConfigPropertyResource.java @@ -23,18 +23,17 @@ import alpine.common.util.UuidUtil; import alpine.model.IConfigProperty; import alpine.security.crypto.DataEncryption; -import alpine.server.resources.AlpineResource; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonReader; -import jakarta.json.JsonString; -import jakarta.ws.rs.core.Response; import org.dependencytrack.model.BomValidationMode; import org.dependencytrack.model.ConfigPropertyAccessMode; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.persistence.QueryManager; import org.owasp.security.logging.SecurityMarkers; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonReader; +import jakarta.json.JsonString; +import jakarta.ws.rs.core.Response; import java.io.StringReader; import java.math.BigDecimal; import java.net.MalformedURLException; @@ -42,7 +41,7 @@ import java.util.Arrays; import java.util.stream.Collectors; -abstract class AbstractConfigPropertyResource extends AlpineResource { +abstract class AbstractConfigPropertyResource extends AbstractApiResource { private final Logger LOGGER = Logger.getLogger(this.getClass()); // Use the classes that extend this, not this class itself static final String ENCRYPTED_PLACEHOLDER = "HiddenDecryptedPropertyPlaceholder"; diff --git a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java index f2c588a0fc..ccc0521af4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java @@ -24,7 +24,6 @@ import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -34,15 +33,6 @@ 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.GET; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Analysis; @@ -52,11 +42,21 @@ import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.problems.ProblemDetails; import org.dependencytrack.resources.v1.vo.AnalysisRequest; import org.dependencytrack.util.AnalysisCommentFormatter.AnalysisCommentField; import org.dependencytrack.util.AnalysisCommentUtil; import org.dependencytrack.util.NotificationUtil; +import jakarta.validation.Validator; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; @@ -74,7 +74,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class AnalysisResource extends AlpineResource { +public class AnalysisResource extends AbstractApiResource { @GET @Produces(MediaType.APPLICATION_JSON) @@ -89,6 +89,10 @@ public class AnalysisResource extends AlpineResource { content = @Content(schema = @Schema(implementation = Analysis.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project, component, or vulnerability could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) @@ -104,9 +108,8 @@ public Response retrieveAnalysis(@Parameter(description = "The UUID of the proje new ValidationTask(RegexSequence.Pattern.UUID, vulnerabilityUuid, "Vulnerability is not a valid UUID") ); try (QueryManager qm = new QueryManager()) { - final Project project; if (StringUtils.trimToNull(projectUuid) != null) { - project = qm.getObjectByUuid(Project.class, projectUuid); + final Project project = qm.getObjectByUuid(Project.class, projectUuid); if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -115,6 +118,7 @@ public Response retrieveAnalysis(@Parameter(description = "The UUID of the proje if (component == null) { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } + requireAccess(qm, component.getProject()); final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, vulnerabilityUuid); if (vulnerability == null) { return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build(); @@ -141,6 +145,10 @@ public Response retrieveAnalysis(@Parameter(description = "The UUID of the proje content = @Content(schema = @Schema(implementation = Analysis.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project, component, or vulnerability could not be found") }) @PermissionRequired({Permissions.Constants.VULNERABILITY_ANALYSIS, Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE}) @@ -165,6 +173,7 @@ public Response updateAnalysis(AnalysisRequest request) { if (component == null) { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } + requireAccess(qm, component.getProject()); final Vulnerability vulnerability = qm.getObjectByUuid(Vulnerability.class, request.getVulnerability()); if (vulnerability == null) { return Response.status(Response.Status.NOT_FOUND).entity("The vulnerability could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java b/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java index 8642804789..13a956abe7 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java @@ -28,7 +28,6 @@ import alpine.server.auth.AuthenticationNotRequired; import alpine.server.auth.JwtAuthenticationService; import alpine.server.filters.AuthenticationFilter; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -38,21 +37,22 @@ 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.ws.rs.GET; -import jakarta.ws.rs.HttpMethod; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Response; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.misc.Badger; +import org.dependencytrack.resources.v1.problems.ProblemDetails; import org.glassfish.jersey.server.ContainerRequest; import org.owasp.security.logging.SecurityMarkers; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Response; import javax.naming.AuthenticationException; import java.security.Principal; @@ -71,7 +71,7 @@ @SecurityRequirement(name = "BearerAuth"), @SecurityRequirement(name = "ApiKeyQueryAuth") }) -public class BadgeResource extends AlpineResource { +public class BadgeResource extends AbstractApiResource { private static final String SVG_MEDIA_TYPE = "image/svg+xml"; @@ -174,7 +174,10 @@ private boolean passesAuthorization(final QueryManager qm) { content = @Content(schema = @Schema(type = "string")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired @@ -191,8 +194,8 @@ public Response getProjectVulnerabilitiesBadge( } final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (!shouldBypassAuth && !qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + if (!shouldBypassAuth) { + requireAccess(qm, project); } final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); final Badger badger = new Badger(); @@ -217,7 +220,10 @@ public Response getProjectVulnerabilitiesBadge( content = @Content(schema = @Schema(type = "string")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired @@ -236,8 +242,8 @@ public Response getProjectVulnerabilitiesBadge( } final Project project = qm.getProject(name, version); if (project != null) { - if (!shouldBypassAuth && !qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + if (!shouldBypassAuth) { + requireAccess(qm, project); } final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); final Badger badger = new Badger(); @@ -262,7 +268,10 @@ public Response getProjectVulnerabilitiesBadge( content = @Content(schema = @Schema(type = "string")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired @@ -279,8 +288,8 @@ public Response getProjectPolicyViolationsBadge( } final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (!shouldBypassAuth && !qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + if (!shouldBypassAuth) { + requireAccess(qm, project); } final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); final Badger badger = new Badger(); @@ -305,7 +314,10 @@ public Response getProjectPolicyViolationsBadge( content = @Content(schema = @Schema(type = "string")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired @@ -324,8 +336,8 @@ public Response getProjectPolicyViolationsBadge( } final Project project = qm.getProject(name, version); if (project != null) { - if (!shouldBypassAuth && !qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + if (!shouldBypassAuth) { + requireAccess(qm, project); } final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); final Badger badger = new Badger(); diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 939a7c1e67..96cf368022 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -24,7 +24,6 @@ import alpine.notification.Notification; import alpine.notification.NotificationLevel; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -34,23 +33,6 @@ 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.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonReader; -import jakarta.json.JsonString; -import jakarta.validation.Validator; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; -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.QueryParam; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.StringUtils; @@ -80,6 +62,23 @@ import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonReader; +import jakarta.json.JsonString; +import jakarta.validation.Validator; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; +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.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -110,7 +109,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class BomResource extends AlpineResource { +public class BomResource extends AbstractApiResource { private static final Logger LOGGER = Logger.getLogger(BomResource.class); @@ -128,7 +127,10 @@ public class BomResource extends AlpineResource { content = @Content(schema = @Schema(type = "string")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -146,9 +148,7 @@ public Response exportProjectAsCycloneDx( if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); final CycloneDXExporter exporter; if (StringUtils.trimToNull(variant) == null || variant.equalsIgnoreCase("inventory")) { @@ -202,7 +202,10 @@ public Response exportProjectAsCycloneDx( content = @Content(schema = @Schema(type = "string")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -216,9 +219,7 @@ public Response exportComponentAsCycloneDx( if (component == null) { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } - if (!qm.hasAccess(super.getPrincipal(), component.getProject())) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); final CycloneDXExporter exporter = new CycloneDXExporter(CycloneDXExporter.Variant.INVENTORY, qm); try { @@ -281,7 +282,10 @@ public Response exportComponentAsCycloneDx( ), @ApiResponse(responseCode = "400", description = "The uploaded BOM is invalid"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.BOM_UPLOAD) @@ -322,19 +326,15 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) } if (parent == null) { // if parent project is specified but not found - return Response.status(Response.Status.NOT_FOUND).entity("The parent component could not be found.").build(); - } else if (!qm.hasAccess(super.getPrincipal(), parent)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified parent project is forbidden").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build(); } + requireAccess(qm, parent, "Access to the specified parent project is forbidden"); } final String trimmedProjectName = StringUtils.trimToNull(request.getProjectName()); if (request.isLatestProjectVersion()) { final Project oldLatest = qm.getLatestProjectVersion(trimmedProjectName); - if(oldLatest != null && !qm.hasAccess(super.getPrincipal(), oldLatest)) { - return Response.status(Response.Status.FORBIDDEN) - .entity("Cannot create latest version for project with this name. Access to current latest " + - "version is forbidden!") - .build(); + if(oldLatest != null) { + requireAccess(qm, oldLatest, "Access to the previous latest project version is forbidden"); } } project = qm.createProject(trimmedProjectName, null, @@ -389,7 +389,10 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) ), @ApiResponse(responseCode = "400", description = "The uploaded BOM is invalid"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.BOM_UPLOAD) @@ -429,18 +432,14 @@ public Response uploadBom( } if (parent == null) { // if parent project is specified but not found - return Response.status(Response.Status.NOT_FOUND).entity("The parent component could not be found.").build(); - } else if (!qm.hasAccess(super.getPrincipal(), parent)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified parent project is forbidden").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build(); } + requireAccess(qm, parent, "Access to the specified parent project is forbidden"); } if (isLatest) { final Project oldLatest = qm.getLatestProjectVersion(trimmedProjectName); - if(oldLatest != null && !qm.hasAccess(super.getPrincipal(), oldLatest)) { - return Response.status(Response.Status.FORBIDDEN) - .entity("Cannot create latest version for project with this name. Access to current latest " + - "version is forbidden!") - .build(); + if(oldLatest != null) { + requireAccess(qm, oldLatest, "Access to the previous latest project version is forbidden"); } } final List tags = (projectTags != null && !projectTags.isBlank()) @@ -463,9 +462,7 @@ public Response uploadBom( */ private Response process(QueryManager qm, Project project, String encodedBomData) { if (project != null) { - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); final File bomFile; try (final var encodedInputStream = new ByteArrayInputStream(encodedBomData.getBytes(StandardCharsets.UTF_8)); @@ -496,9 +493,7 @@ private Response process(QueryManager qm, Project project, List properties = qm.getComponentProperties(component); - // Detaches the objects and closes the persistence manager so that if/when encrypted string - // values are replaced by the placeholder, they are not erroneously persisted to the database. - qm.getPersistenceManager().detachCopyAll(properties); - qm.close(); - for (final ComponentProperty property : properties) { - // Replace the value of encrypted strings with the pre-defined placeholder - if (ComponentProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { - property.setPropertyValue(ENCRYPTED_PLACEHOLDER); - } + requireAccess(qm, component.getProject()); + final List properties = qm.getComponentProperties(component); + // Detaches the objects and closes the persistence manager so that if/when encrypted string + // values are replaced by the placeholder, they are not erroneously persisted to the database. + qm.getPersistenceManager().detachCopyAll(properties); + qm.close(); + for (final ComponentProperty property : properties) { + // Replace the value of encrypted strings with the pre-defined placeholder + if (ComponentProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { + property.setPropertyValue(ENCRYPTED_PLACEHOLDER); } - return Response.ok(properties).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } + return Response.ok(properties).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } @@ -119,7 +120,10 @@ public Response getProperties( content = @Content(schema = @Schema(implementation = ComponentProperty.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component could not be found"), @ApiResponse(responseCode = "409", description = "A property with the specified component/group/name combination already exists") }) @@ -138,27 +142,24 @@ public Response createProperty( try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - final List existingProperties = qm.getComponentProperties(component, - StringUtils.trimToNull(json.getGroupName()), StringUtils.trimToNull(json.getPropertyName())); - final var jsonPropertyIdentity = new ComponentProperty.Identity(json); - final boolean isDuplicate = existingProperties.stream() - .map(ComponentProperty.Identity::new) - .anyMatch(jsonPropertyIdentity::equals); - if (existingProperties.isEmpty() || !isDuplicate) { - final ComponentProperty property = qm.createComponentProperty(component, - StringUtils.trimToNull(json.getGroupName()), - StringUtils.trimToNull(json.getPropertyName()), - null, // Set value to null - this will be taken care of by updatePropertyValue below - json.getPropertyType(), - StringUtils.trimToNull(json.getDescription())); - updatePropertyValue(qm, json, property); - return Response.status(Response.Status.CREATED).entity(property).build(); - } else { - return Response.status(Response.Status.CONFLICT).entity("A property with the specified component/group/name/value combination already exists.").build(); - } + requireAccess(qm, component.getProject()); + final List existingProperties = qm.getComponentProperties(component, + StringUtils.trimToNull(json.getGroupName()), StringUtils.trimToNull(json.getPropertyName())); + final var jsonPropertyIdentity = new ComponentProperty.Identity(json); + final boolean isDuplicate = existingProperties.stream() + .map(ComponentProperty.Identity::new) + .anyMatch(jsonPropertyIdentity::equals); + if (existingProperties.isEmpty() || !isDuplicate) { + final ComponentProperty property = qm.createComponentProperty(component, + StringUtils.trimToNull(json.getGroupName()), + StringUtils.trimToNull(json.getPropertyName()), + null, // Set value to null - this will be taken care of by updatePropertyValue below + json.getPropertyType(), + StringUtils.trimToNull(json.getDescription())); + updatePropertyValue(qm, json, property); + return Response.status(Response.Status.CREATED).entity(property).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); + return Response.status(Response.Status.CONFLICT).entity("A property with the specified component/group/name/value combination already exists.").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); @@ -177,7 +178,10 @@ public Response createProperty( @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Property removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component or component property could not be found"), }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE}) @@ -189,15 +193,12 @@ public Response deleteProperty( try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, componentUuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - final long propertiesDeleted = qm.deleteComponentPropertyByUuid(component, UUID.fromString(propertyUuid)); - if (propertiesDeleted > 0) { - return Response.status(Response.Status.NO_CONTENT).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).entity("The component property could not be found.").build(); - } + requireAccess(qm, component.getProject()); + final long propertiesDeleted = qm.deleteComponentPropertyByUuid(component, UUID.fromString(propertyUuid)); + if (propertiesDeleted > 0) { + return Response.status(Response.Status.NO_CONTENT).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The component property could not be found.").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 510e5074bb..2be0bfd5aa 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -22,7 +22,6 @@ import alpine.event.framework.Event; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import io.swagger.v3.oas.annotations.Operation; @@ -36,18 +35,6 @@ 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.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.ComponentVulnerabilityAnalysisEvent; @@ -71,10 +58,23 @@ import org.dependencytrack.persistence.jdbi.ComponentDao; import org.dependencytrack.proto.repometaanalysis.v1.FetchMeta; import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.dependencytrack.resources.v1.problems.ProblemDetails; import org.dependencytrack.util.InternalComponentIdentifier; import org.dependencytrack.util.PurlUtil; import org.jdbi.v3.core.Handle; +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.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -97,7 +97,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class ComponentResource extends AlpineResource { +public class ComponentResource extends AbstractApiResource { private static final Logger LOGGER = Logger.getLogger(ComponentResource.class); private final KafkaEventDispatcher kafkaEventDispatcher = new KafkaEventDispatcher(); @@ -118,7 +118,10 @@ public class ComponentResource extends AlpineResource { content = @Content(array = @ArraySchema(schema = @Schema(implementation = Component.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -132,12 +135,9 @@ public Response getAllComponents( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final PaginatedResult result = qm.getComponents(project, true, onlyOutdated, onlyDirect); - return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + final PaginatedResult result = qm.getComponents(project, true, onlyOutdated, onlyDirect); + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -158,7 +158,10 @@ public Response getAllComponents( content = @Content(schema = @Schema(implementation = Component.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -171,25 +174,21 @@ public Response getComponentByUuid( try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { - final Project project = component.getProject(); - if (qm.hasAccess(super.getPrincipal(), project)) { - final Component detachedComponent = qm.detach(Component.class, component.getId()); // TODO: Force project to be loaded. It should be anyway, but JDO seems to be having issues here. - if ((includeRepositoryMetaData || includeIntegrityMetaData) && detachedComponent.getPurl() != null) { - final RepositoryType type = RepositoryType.resolve(detachedComponent.getPurl()); - if (RepositoryType.UNSUPPORTED != type) { - if (includeRepositoryMetaData) { - final RepositoryMetaComponent repoMetaComponent = qm.getRepositoryMetaComponent(type, detachedComponent.getPurl().getNamespace(), detachedComponent.getPurl().getName()); - detachedComponent.setRepositoryMeta(repoMetaComponent); - } - if (includeIntegrityMetaData) { - detachedComponent.setComponentMetaInformation(qm.getMetaInformation(component.getUuid())); - } + requireAccess(qm, component.getProject()); + final Component detachedComponent = qm.detach(Component.class, component.getId()); // TODO: Force project to be loaded. It should be anyway, but JDO seems to be having issues here. + if ((includeRepositoryMetaData || includeIntegrityMetaData) && detachedComponent.getPurl() != null) { + final RepositoryType type = RepositoryType.resolve(detachedComponent.getPurl()); + if (RepositoryType.UNSUPPORTED != type) { + if (includeRepositoryMetaData) { + final RepositoryMetaComponent repoMetaComponent = qm.getRepositoryMetaComponent(type, detachedComponent.getPurl().getNamespace(), detachedComponent.getPurl().getName()); + detachedComponent.setRepositoryMeta(repoMetaComponent); + } + if (includeIntegrityMetaData) { + detachedComponent.setComponentMetaInformation(qm.getMetaInformation(component.getUuid())); } } - return Response.ok(detachedComponent).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); } + return Response.ok(detachedComponent).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } @@ -255,6 +254,10 @@ public Response getIntegrityMetaComponent( content = @Content(schema = @Schema(implementation = IntegrityAnalysis.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The integrity analysis information for the specified component cannot be found"), }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -262,7 +265,13 @@ public Response getIntegrityStatus( @Parameter(description = "UUID of the component for which integrity status information is needed", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { - final IntegrityAnalysis result = qm.getIntegrityAnalysisByComponentUuid(UUID.fromString(uuid)); + final Component component = qm.getObjectByUuid(Component.class, uuid); + if (component == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); + } + requireAccess(qm, component.getProject()); + + final IntegrityAnalysis result = qm.getIntegrityAnalysisByComponentUuid(component.getUuid()); if (result == null) { return Response.status(Response.Status.NOT_FOUND).entity("The integrity status for the specified component cannot be found.").build(); } else { @@ -287,7 +296,11 @@ public Response getIntegrityStatus( headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of components", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Component.class))) ), - @ApiResponse(responseCode = "401", description = "Unauthorized") + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)) }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getComponentByIdentity(@Parameter(description = "The group of the component") @@ -311,9 +324,7 @@ public Response getComponentByIdentity(@Parameter(description = "The group of th if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); } PackageURL packageURL = null; if (purl != null) { @@ -378,7 +389,10 @@ public Response getComponentByHash( content = @Content(schema = @Schema(implementation = Component.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired({ Permissions.Constants.PORTFOLIO_MANAGEMENT, @@ -421,9 +435,7 @@ public Response createComponent(@Parameter(description = "The UUID of the projec if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); final License resolvedLicense = qm.getLicense(jsonComponent.getLicense()); Component component = new Component(); component.setProject(project); @@ -469,18 +481,22 @@ public Response createComponent(@Parameter(description = "The UUID of the projec component.setNotes(StringUtils.trimToNull(jsonComponent.getNotes())); component = qm.createComponent(component, true); - ComponentProjection componentProjection = - new ComponentProjection(component.getUuid(), component.getPurlCoordinates().toString(), - component.isInternal(), component.getPurl()); - try { - Handler repoMetaHandler = HandlerFactory.createHandler(componentProjection, qm, kafkaEventDispatcher, FetchMeta.FETCH_META_INTEGRITY_DATA_AND_LATEST_VERSION); - IntegrityMetaComponent integrityMetaComponent = repoMetaHandler.handle(); - if (integrityMetaComponent != null && (integrityMetaComponent.getStatus() == PROCESSED || integrityMetaComponent.getStatus() == NOT_AVAILABLE)) { - calculateIntegrityResult(integrityMetaComponent, component, qm); + + if (component.getPurl() != null) { + ComponentProjection componentProjection = + new ComponentProjection(component.getUuid(), component.getPurlCoordinates().toString(), + component.isInternal(), component.getPurl()); + try { + Handler repoMetaHandler = HandlerFactory.createHandler(componentProjection, qm, kafkaEventDispatcher, FetchMeta.FETCH_META_INTEGRITY_DATA_AND_LATEST_VERSION); + IntegrityMetaComponent integrityMetaComponent = repoMetaHandler.handle(); + if (integrityMetaComponent != null && (integrityMetaComponent.getStatus() == PROCESSED || integrityMetaComponent.getStatus() == NOT_AVAILABLE)) { + calculateIntegrityResult(integrityMetaComponent, component, qm); + } + } catch (MalformedPackageURLException ex) { + LOGGER.warn("Unable to process package url %s".formatted(componentProjection.purl())); } - } catch (MalformedPackageURLException ex) { - LOGGER.warn("Unable to process package url %s".formatted(componentProjection.purl())); } + final var vulnAnalysisEvent = new ComponentVulnerabilityAnalysisEvent(UUID.randomUUID(), component, VulnerabilityAnalysisLevel.MANUAL_ANALYSIS, true); qm.createVulnerabilityScan(VulnerabilityScan.TargetType.COMPONENT, component.getUuid(), vulnAnalysisEvent.token(), 1); kafkaEventDispatcher.dispatchEvent(vulnAnalysisEvent); @@ -502,7 +518,10 @@ public Response createComponent(@Parameter(description = "The UUID of the projec content = @Content(schema = @Schema(implementation = Component.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the component could not be found"), }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE}) @@ -532,9 +551,7 @@ public Response updateComponent(Component jsonComponent) { try (QueryManager qm = new QueryManager()) { Component component = qm.getObjectByUuid(Component.class, jsonComponent.getUuid()); if (component != null) { - if (!qm.hasAccess(super.getPrincipal(), component.getProject())) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); // Name cannot be empty or null - prevent it final String name = StringUtils.trimToNull(jsonComponent.getName()); if (name != null) { @@ -588,19 +605,23 @@ public Response updateComponent(Component jsonComponent) { component.setNotes(StringUtils.trimToNull(jsonComponent.getNotes())); component = qm.updateComponent(component, true); - ComponentProjection componentProjection = - new ComponentProjection(component.getUuid(), component.getPurlCoordinates().toString(), - component.isInternal(), component.getPurl()); - try { - Handler repoMetaHandler = HandlerFactory.createHandler(componentProjection, qm, kafkaEventDispatcher, FetchMeta.FETCH_META_INTEGRITY_DATA_AND_LATEST_VERSION); - IntegrityMetaComponent integrityMetaComponent = repoMetaHandler.handle(); - if (integrityMetaComponent != null && (integrityMetaComponent.getStatus() == PROCESSED || integrityMetaComponent.getStatus() == NOT_AVAILABLE)) { - calculateIntegrityResult(integrityMetaComponent, component, qm); + if (component.getPurl() != null) { + ComponentProjection componentProjection = + new ComponentProjection(component.getUuid(), component.getPurlCoordinates().toString(), + component.isInternal(), component.getPurl()); + try { + + Handler repoMetaHandler = HandlerFactory.createHandler(componentProjection, qm, kafkaEventDispatcher, FetchMeta.FETCH_META_INTEGRITY_DATA_AND_LATEST_VERSION); + IntegrityMetaComponent integrityMetaComponent = repoMetaHandler.handle(); + if (integrityMetaComponent != null && (integrityMetaComponent.getStatus() == PROCESSED || integrityMetaComponent.getStatus() == NOT_AVAILABLE)) { + calculateIntegrityResult(integrityMetaComponent, component, qm); + } + } catch (MalformedPackageURLException ex) { + LOGGER.warn("Unable to determine package url type for this purl %s".formatted(component.getPurl().getType()), ex); } - } catch (MalformedPackageURLException ex) { - LOGGER.warn("Unable to determine package url type for this purl %s".formatted(component.getPurl().getType()), ex); } + final var vulnAnalysisEvent = new ComponentVulnerabilityAnalysisEvent(UUID.randomUUID(), component, VulnerabilityAnalysisLevel.MANUAL_ANALYSIS, false); qm.createVulnerabilityScan(VulnerabilityScan.TargetType.COMPONENT, component.getUuid(), vulnAnalysisEvent.token(), 1); kafkaEventDispatcher.dispatchEvent(vulnAnalysisEvent); @@ -622,7 +643,10 @@ public Response updateComponent(Component jsonComponent) { @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Component removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the component could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE}) @@ -632,9 +656,7 @@ public Response deleteComponent( try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid, Component.FetchGroup.ALL.name()); if (component != null) { - if (!qm.hasAccess(super.getPrincipal(), component.getProject())) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); try (final Handle jdbiHandle = openJdbiHandle()) { final var componentDao = jdbiHandle.attach(ComponentDao.class); componentDao.deleteComponent(component.getUuid()); @@ -677,7 +699,10 @@ public Response identifyInternalComponents() { content = @Content(schema = @Schema(type = "object")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "- The UUID of the project could not be found\n- The UUID of the component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -691,10 +716,7 @@ public Response getDependencyGraphForComponent( if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } - - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden.").build(); - } + requireAccess(qm, project); final String[] componentUuidsSplit = componentUuids.split("\\|"); final List components = new ArrayList<>(); diff --git a/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java b/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java index e62ac02c6b..cb9a084928 100644 --- a/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/DependencyGraphResource.java @@ -19,7 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import io.swagger.v3.oas.annotations.Operation; @@ -32,6 +31,17 @@ 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 org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.RepositoryMetaComponent; +import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.RepositoryQueryManager; +import org.dependencytrack.resources.v1.problems.ProblemDetails; +import org.dependencytrack.resources.v1.vo.DependencyGraphResponse; + import jakarta.json.Json; import jakarta.json.JsonArray; import jakarta.json.JsonException; @@ -43,16 +53,6 @@ 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.model.Component; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.RepositoryMetaComponent; -import org.dependencytrack.model.RepositoryType; -import org.dependencytrack.model.validation.ValidUuid; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.persistence.RepositoryQueryManager; -import org.dependencytrack.resources.v1.vo.DependencyGraphResponse; - import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; @@ -70,7 +70,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class DependencyGraphResource extends AlpineResource { +public class DependencyGraphResource extends AbstractApiResource { @GET @Path("/project/{uuid}/directDependencies") @@ -87,7 +87,10 @@ public class DependencyGraphResource extends AlpineResource { content = @Content(array = @ArraySchema(schema = @Schema(implementation = DependencyGraphResponse.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to a specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "Any component can be found"), }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -99,9 +102,7 @@ public Response getComponentsAndServicesByProjectUuid(@Parameter(description = " return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); final String directDependenciesJSON = project.getDirectDependencies(); @@ -129,7 +130,10 @@ public Response getComponentsAndServicesByProjectUuid(@Parameter(description = " content = @Content(array = @ArraySchema(schema = @Schema(implementation = DependencyGraphResponse.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to a specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "Any component can be found"), }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -140,9 +144,7 @@ public Response getComponentsAndServicesByComponentUuid(@Parameter(description = return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } - if (!qm.hasAccess(super.getPrincipal(), component.getProject())) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); final String directDependenciesJSON = component.getDirectDependencies(); diff --git a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 64edd2f183..d12110fcca 100644 --- a/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -23,7 +23,6 @@ import alpine.model.About; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.template.PebbleTemplate; import io.swagger.v3.oas.annotations.Operation; @@ -37,15 +36,6 @@ 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.ws.rs.GET; -import jakarta.ws.rs.HeaderParam; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.text.WordUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.PortfolioRepositoryMetaAnalysisEvent; @@ -58,8 +48,18 @@ import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.problems.ProblemDetails; import org.dependencytrack.resources.v1.vo.BomUploadResponse; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; @@ -82,7 +82,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class FindingResource extends AlpineResource { +public class FindingResource extends AbstractApiResource { private static final Logger LOGGER = Logger.getLogger(FindingResource.class); public static final String MEDIA_TYPE_SARIF_JSON = "application/sarif+json"; @@ -105,7 +105,10 @@ public class FindingResource extends AlpineResource { } ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) @@ -119,26 +122,23 @@ public Response getFindingsByProject(@Parameter(description = "The UUID of the p try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final List findings = qm.getFindings(project, suppressed); - if (acceptHeader != null && acceptHeader.contains(MEDIA_TYPE_SARIF_JSON)) { - try { - return Response.ok(generateSARIF(findings), MEDIA_TYPE_SARIF_JSON) - .header("content-disposition", "attachment; filename=\"findings-" + uuid + ".sarif\"") - .build(); - } catch (IOException ioException) { - LOGGER.error(ioException.getMessage(), ioException); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("An error occurred while generating SARIF file").build(); - } - } - if (source != null) { - final List filteredList = findings.stream().filter(finding -> source.name().equals(finding.getVulnerability().get("source"))).collect(Collectors.toList()); - return Response.ok(filteredList).header(TOTAL_COUNT_HEADER, filteredList.size()).build(); - } else { - return Response.ok(findings).header(TOTAL_COUNT_HEADER, findings.size()).build(); + requireAccess(qm, project); + final List findings = qm.getFindings(project, suppressed); + if (acceptHeader != null && acceptHeader.contains(MEDIA_TYPE_SARIF_JSON)) { + try { + return Response.ok(generateSARIF(findings), MEDIA_TYPE_SARIF_JSON) + .header("content-disposition", "attachment; filename=\"findings-" + uuid + ".sarif\"") + .build(); + } catch (IOException ioException) { + LOGGER.error(ioException.getMessage(), ioException); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("An error occurred while generating SARIF file").build(); } + } + if (source != null) { + final List filteredList = findings.stream().filter(finding -> source.name().equals(finding.getVulnerability().get("source"))).collect(Collectors.toList()); + return Response.ok(filteredList).header(TOTAL_COUNT_HEADER, filteredList.size()).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.ok(findings).header(TOTAL_COUNT_HEADER, findings.size()).build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -160,7 +160,10 @@ public Response getFindingsByProject(@Parameter(description = "The UUID of the p content = @Content(schema = @Schema(type = "string")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) @@ -169,15 +172,12 @@ public Response exportFindingsByProject(@Parameter(description = "The UUID of th try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final List findings = qm.getFindings(project); - final FindingPackagingFormat fpf = new FindingPackagingFormat(UUID.fromString(uuid), findings); - final Response.ResponseBuilder rb = Response.ok(fpf.getDocument().toString(), "application/json"); - rb.header("Content-Disposition", "inline; filename=findings-" + uuid + ".fpf"); - return rb.build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + final List findings = qm.getFindings(project); + final FindingPackagingFormat fpf = new FindingPackagingFormat(UUID.fromString(uuid), findings); + final Response.ResponseBuilder rb = Response.ok(fpf.getDocument().toString(), "application/json"); + rb.header("Content-Disposition", "inline; filename=findings-" + uuid + ".fpf"); + return rb.build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -221,7 +221,10 @@ public Response analyzePortfolio() { content = @Content(schema = @Schema(implementation = BomUploadResponse.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_VULNERABILITY) @@ -231,19 +234,16 @@ public Response analyzeProject( try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - LOGGER.info("Analysis of project " + project.getUuid() + " requested by " + super.getPrincipal().getName()); + requireAccess(qm, project); + LOGGER.info("Analysis of project " + project.getUuid() + " requested by " + super.getPrincipal().getName()); - final ProjectVulnerabilityAnalysisEvent vae = new ProjectVulnerabilityAnalysisEvent(project.getUuid()); - qm.createReanalyzeSteps(vae.getChainIdentifier()); - Event.dispatch(vae); - final ProjectRepositoryMetaAnalysisEvent projectRepositoryMetaAnalysisEvent = new ProjectRepositoryMetaAnalysisEvent(project.getUuid()); - Event.dispatch(projectRepositoryMetaAnalysisEvent); + final ProjectVulnerabilityAnalysisEvent vae = new ProjectVulnerabilityAnalysisEvent(project.getUuid()); + qm.createReanalyzeSteps(vae.getChainIdentifier()); + Event.dispatch(vae); + final ProjectRepositoryMetaAnalysisEvent projectRepositoryMetaAnalysisEvent = new ProjectRepositoryMetaAnalysisEvent(project.getUuid()); + Event.dispatch(projectRepositoryMetaAnalysisEvent); - return Response.ok(Collections.singletonMap("token", vae.getChainIdentifier())).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + return Response.ok(Collections.singletonMap("token", vae.getChainIdentifier())).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java index f0af0b8cb4..20e748d297 100644 --- a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java @@ -20,7 +20,6 @@ import alpine.event.framework.Event; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -31,12 +30,6 @@ 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.ws.rs.GET; -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.apache.commons.lang3.time.DateUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.ComponentMetricsUpdateEvent; @@ -50,8 +43,15 @@ import org.dependencytrack.model.VulnerabilityMetrics; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.problems.ProblemDetails; import org.dependencytrack.util.DateUtil; +import jakarta.ws.rs.GET; +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.Date; import java.util.List; @@ -67,7 +67,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class MetricsResource extends AlpineResource { +public class MetricsResource extends AbstractApiResource { @GET @Path("/vulnerability") @@ -204,7 +204,10 @@ public Response RefreshPortfolioMetrics() { content = @Content(schema = @Schema(implementation = ProjectMetrics.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -214,12 +217,9 @@ public Response getProjectCurrentMetrics( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); - return Response.ok(metrics).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); + return Response.ok(metrics).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -242,7 +242,10 @@ public Response getProjectCurrentMetrics( content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProjectMetrics.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -270,7 +273,10 @@ public Response getProjectMetricsSince( content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProjectMetrics.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -294,7 +300,10 @@ public Response getProjectMetricsXDays( @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Refresh requested successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_READ}) @@ -304,12 +313,9 @@ public Response RefreshProjectMetrics( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - Event.dispatch(new ProjectMetricsUpdateEvent(project.getUuid())); - return Response.ok().build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + Event.dispatch(new ProjectMetricsUpdateEvent(project.getUuid())); + return Response.ok().build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -330,7 +336,10 @@ public Response RefreshProjectMetrics( content = @Content(schema = @Schema(implementation = DependencyMetrics.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested component is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -340,12 +349,9 @@ public Response getComponentCurrentMetrics( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - final DependencyMetrics metrics = qm.getMostRecentDependencyMetrics(component); - return Response.ok(metrics).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); + final DependencyMetrics metrics = qm.getMostRecentDependencyMetrics(component); + return Response.ok(metrics).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } @@ -368,7 +374,10 @@ public Response getComponentCurrentMetrics( content = @Content(array = @ArraySchema(schema = @Schema(implementation = DependencyMetrics.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested component is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -399,7 +408,10 @@ public Response getComponentMetricsSince( content = @Content(array = @ArraySchema(schema = @Schema(implementation = DependencyMetrics.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested component is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -423,7 +435,10 @@ public Response getComponentMetricsXDays( @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Refresh requested successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested component is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_READ}) @@ -433,17 +448,12 @@ public Response RefreshComponentMetrics( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - Event.dispatch(new ComponentMetricsUpdateEvent(component.getUuid())); - return Response.ok().build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); + Event.dispatch(new ComponentMetricsUpdateEvent(component.getUuid())); + return Response.ok().build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } - } catch (Exception e) { - throw new RuntimeException(e); } } @@ -458,12 +468,9 @@ private Response getProjectMetrics(String uuid, Date since) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final List metrics = qm.getProjectMetricsSince(project, since); - return Response.ok(metrics).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + final List metrics = qm.getProjectMetricsSince(project, since); + return Response.ok(metrics).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -481,12 +488,9 @@ private Response getComponentMetrics(String uuid, Date since) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - final List metrics = qm.getDependencyMetricsSince(component, since); - return Response.ok(metrics).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); + final List metrics = qm.getDependencyMetricsSince(component, since); + return Response.ok(metrics).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java index bde39188dc..5b3321de7d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationRuleResource.java @@ -21,7 +21,6 @@ import alpine.model.Team; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.headers.Header; @@ -33,6 +32,17 @@ 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 org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.NotificationPublisher; +import org.dependencytrack.model.NotificationRule; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.dependencytrack.resources.v1.problems.ProblemDetails; + import jakarta.validation.Validator; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -44,16 +54,6 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.model.NotificationPublisher; -import org.dependencytrack.model.NotificationRule; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.validation.ValidUuid; -import org.dependencytrack.notification.NotificationScope; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.resources.v1.openapi.PaginatedApi; - import java.util.List; import static org.dependencytrack.notification.publisher.PublisherClass.SendMailPublisher; @@ -70,7 +70,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class NotificationRuleResource extends AlpineResource { +public class NotificationRuleResource extends AbstractApiResource { @GET @Produces(MediaType.APPLICATION_JSON) @@ -209,6 +209,10 @@ public Response deleteNotificationRule(NotificationRule jsonRule) { ), @ApiResponse(responseCode = "304", description = "The rule already has the specified project assigned"), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The notification rule or project could not be found") }) @PermissionRequired({Permissions.Constants.SYSTEM_CONFIGURATION, Permissions.Constants.SYSTEM_CONFIGURATION_UPDATE}) @@ -229,6 +233,7 @@ public Response addProjectToRule( if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } + requireAccess(qm, project); final List projects = rule.getProjects(); if (projects != null && !projects.contains(project)) { rule.getProjects().add(project); @@ -254,6 +259,10 @@ public Response addProjectToRule( ), @ApiResponse(responseCode = "304", description = "The rule does not have the specified project assigned"), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The notification rule or project could not be found") }) @PermissionRequired({Permissions.Constants.SYSTEM_CONFIGURATION, Permissions.Constants.SYSTEM_CONFIGURATION_DELETE}) @@ -274,6 +283,7 @@ public Response removeProjectFromRule( if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } + requireAccess(qm, project); final List projects = rule.getProjects(); if (projects != null && projects.contains(project)) { rule.getProjects().remove(project); diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java index 1725665b04..e69dc631fa 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyResource.java @@ -20,7 +20,6 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.headers.Header; @@ -31,6 +30,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.dependencytrack.resources.v1.problems.ProblemDetails; + import jakarta.validation.Validator; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -42,14 +50,6 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.model.Policy; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.validation.ValidUuid; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.resources.v1.openapi.PaginatedApi; - import java.util.List; /** @@ -64,7 +64,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class PolicyResource extends AlpineResource { +public class PolicyResource extends AbstractApiResource { @GET @Produces(MediaType.APPLICATION_JSON) @@ -246,6 +246,10 @@ public Response deletePolicy( ), @ApiResponse(responseCode = "304", description = "The policy already has the specified project assigned"), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The policy or project could not be found") }) @PermissionRequired({Permissions.Constants.POLICY_MANAGEMENT, Permissions.Constants.POLICY_MANAGEMENT_UPDATE}) @@ -263,6 +267,7 @@ public Response addProjectToPolicy( if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } + requireAccess(qm, project); final List projects = policy.getProjects(); if (projects != null && !projects.contains(project)) { policy.getProjects().add(project); @@ -289,6 +294,10 @@ public Response addProjectToPolicy( ), @ApiResponse(responseCode = "304", description = "The policy does not have the specified project assigned"), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The policy or project could not be found") }) @PermissionRequired({Permissions.Constants.POLICY_MANAGEMENT, Permissions.Constants.POLICY_MANAGEMENT_DELETE}) @@ -306,6 +315,7 @@ public Response removeProjectFromPolicy( if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } + requireAccess(qm, project); final List projects = policy.getProjects(); if (projects != null && projects.contains(project)) { policy.getProjects().remove(project); diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java index 20c812964f..f9394b614b 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyViolationResource.java @@ -20,7 +20,6 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.headers.Header; @@ -32,13 +31,6 @@ 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.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Component; import org.dependencytrack.model.PolicyViolation; @@ -46,7 +38,15 @@ import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.dependencytrack.resources.v1.problems.ProblemDetails; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import javax.jdo.FetchPlan; import javax.jdo.PersistenceManager; import java.util.Collection; @@ -65,7 +65,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class PolicyViolationResource extends AlpineResource { +public class PolicyViolationResource extends AbstractApiResource { @GET @Produces(MediaType.APPLICATION_JSON) @@ -137,7 +137,10 @@ public Response getViolations(@Parameter(description = "Optionally includes supp content = @Content(array = @ArraySchema(schema = @Schema(implementation = PolicyViolation.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_POLICY_VIOLATION) @@ -148,14 +151,11 @@ public Response getViolationsByProject(@Parameter(description = "The UUID of the try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final PaginatedResult result = qm.getPolicyViolations(project, suppressed); - return Response.ok(detachViolations(qm, result.getList(PolicyViolation.class))) - .header(TOTAL_COUNT_HEADER, result.getTotal()) - .build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + final PaginatedResult result = qm.getPolicyViolations(project, suppressed); + return Response.ok(detachViolations(qm, result.getList(PolicyViolation.class))) + .header(TOTAL_COUNT_HEADER, result.getTotal()) + .build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -178,7 +178,10 @@ public Response getViolationsByProject(@Parameter(description = "The UUID of the content = @Content(array = @ArraySchema(schema = @Schema(implementation = PolicyViolation.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The component could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_POLICY_VIOLATION) @@ -189,14 +192,11 @@ public Response getViolationsByComponent(@Parameter(description = "The UUID of t try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - final PaginatedResult result = qm.getPolicyViolations(component, suppressed); - return Response.ok(detachViolations(qm, result.getList(PolicyViolation.class))) - .header(TOTAL_COUNT_HEADER, result.getTotal()) - .build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); + final PaginatedResult result = qm.getPolicyViolations(component, suppressed); + return Response.ok(detachViolations(qm, result.getList(PolicyViolation.class))) + .header(TOTAL_COUNT_HEADER, result.getTotal()) + .build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java index 8cec573319..f380f574f2 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectPropertyResource.java @@ -29,6 +29,14 @@ 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 org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.problems.ProblemDetails; + import jakarta.validation.Validator; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -40,13 +48,6 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.ProjectProperty; -import org.dependencytrack.model.validation.ValidUuid; -import org.dependencytrack.persistence.QueryManager; - import java.util.List; /** @@ -76,7 +77,10 @@ public class ProjectPropertyResource extends AbstractConfigPropertyResource { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProjectProperty.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_READ}) @@ -86,22 +90,19 @@ public Response getProperties( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final List properties = qm.getProjectProperties(project); - // Detaches the objects and closes the persistence manager so that if/when encrypted string - // values are replaced by the placeholder, they are not erroneously persisted to the database. - qm.getPersistenceManager().detachCopyAll(properties); - qm.close(); - for (final ProjectProperty property : properties) { - // Replace the value of encrypted strings with the pre-defined placeholder - if (ProjectProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { - property.setPropertyValue(ENCRYPTED_PLACEHOLDER); - } + requireAccess(qm, project); + final List properties = qm.getProjectProperties(project); + // Detaches the objects and closes the persistence manager so that if/when encrypted string + // values are replaced by the placeholder, they are not erroneously persisted to the database. + qm.getPersistenceManager().detachCopyAll(properties); + qm.close(); + for (final ProjectProperty property : properties) { + // Replace the value of encrypted strings with the pre-defined placeholder + if (ProjectProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { + property.setPropertyValue(ENCRYPTED_PLACEHOLDER); } - return Response.ok(properties).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } + return Response.ok(properties).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -122,7 +123,10 @@ public Response getProperties( content = @Content(schema = @Schema(implementation = ProjectProperty.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found"), @ApiResponse(responseCode = "409", description = "A property with the specified project/group/name combination already exists") }) @@ -135,32 +139,30 @@ public Response createProperty( failOnValidationError( validator.validateProperty(json, "groupName"), validator.validateProperty(json, "propertyName"), - validator.validateProperty(json, "propertyValue") + validator.validateProperty(json, "propertyValue"), + validator.validateProperty(json, "propertyType") ); try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final ProjectProperty existing = qm.getProjectProperty(project, - StringUtils.trimToNull(json.getGroupName()), StringUtils.trimToNull(json.getPropertyName())); - if (existing == null) { - final ProjectProperty property = qm.createProjectProperty(project, - StringUtils.trimToNull(json.getGroupName()), - StringUtils.trimToNull(json.getPropertyName()), - null, // Set value to null - this will be taken care of by updatePropertyValue below - json.getPropertyType(), - StringUtils.trimToNull(json.getDescription())); - updatePropertyValue(qm, json, property); - qm.getPersistenceManager().detachCopy(project); - if (ProjectProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { - property.setPropertyValue(ENCRYPTED_PLACEHOLDER); - } - return Response.status(Response.Status.CREATED).entity(property).build(); - } else { - return Response.status(Response.Status.CONFLICT).entity("A property with the specified project/group/name combination already exists.").build(); + requireAccess(qm, project); + final ProjectProperty existing = qm.getProjectProperty(project, + StringUtils.trimToNull(json.getGroupName()), StringUtils.trimToNull(json.getPropertyName())); + if (existing == null) { + final ProjectProperty property = qm.createProjectProperty(project, + StringUtils.trimToNull(json.getGroupName()), + StringUtils.trimToNull(json.getPropertyName()), + null, // Set value to null - this will be taken care of by updatePropertyValue below + json.getPropertyType(), + StringUtils.trimToNull(json.getDescription())); + updatePropertyValue(qm, json, property); + qm.getPersistenceManager().detachCopy(project); + if (ProjectProperty.PropertyType.ENCRYPTEDSTRING == property.getPropertyType()) { + property.setPropertyValue(ENCRYPTED_PLACEHOLDER); } + return Response.status(Response.Status.CREATED).entity(property).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.CONFLICT).entity("A property with the specified project/group/name combination already exists.").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -182,7 +184,10 @@ public Response createProperty( content = @Content(schema = @Schema(implementation = ProjectProperty.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found"), }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE}) @@ -199,15 +204,12 @@ public Response updateProperty( try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final ProjectProperty property = qm.getProjectProperty(project, json.getGroupName(), json.getPropertyName()); - if (property != null) { - return updatePropertyValue(qm, json, property); - } else { - return Response.status(Response.Status.NOT_FOUND).entity("A property with the specified project/group/name combination could not be found.").build(); - } + requireAccess(qm, project); + final ProjectProperty property = qm.getProjectProperty(project, json.getGroupName(), json.getPropertyName()); + if (property != null) { + return updatePropertyValue(qm, json, property); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.NOT_FOUND).entity("A property with the specified project/group/name combination could not be found.").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -225,7 +227,10 @@ public Response updateProperty( @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Project property removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project or project property could not be found"), }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE}) @@ -241,16 +246,13 @@ public Response deleteProperty( try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final ProjectProperty property = qm.getProjectProperty(project, json.getGroupName(), json.getPropertyName()); - if (property != null) { - qm.delete(property); - return Response.status(Response.Status.NO_CONTENT).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).entity("The project property could not be found.").build(); - } + requireAccess(qm, project); + final ProjectProperty property = qm.getProjectProperty(project, json.getGroupName(), json.getPropertyName()); + if (property != null) { + qm.delete(property); + return Response.status(Response.Status.NO_CONTENT).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The project property could not be found.").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 07ddbfd59e..f41bbdb37b 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -25,7 +25,6 @@ import alpine.model.UserPrincipal; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.jsonwebtoken.lang.Collections; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -37,21 +36,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; -import jakarta.validation.Validator; -import jakarta.ws.rs.ClientErrorException; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.PATCH; -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.QueryParam; -import jakarta.ws.rs.ServerErrorException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.CloneProjectEvent; @@ -66,11 +50,27 @@ import org.dependencytrack.persistence.jdbi.ProjectDao; import org.dependencytrack.persistence.jdbi.ProjectDao.ConciseProjectListRow; import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.dependencytrack.resources.v1.problems.ProblemDetails; import org.dependencytrack.resources.v1.vo.BomUploadResponse; import org.dependencytrack.resources.v1.vo.CloneProjectRequest; import org.dependencytrack.resources.v1.vo.ConciseProject; import org.jdbi.v3.core.Handle; +import jakarta.validation.Validator; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +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.QueryParam; +import jakarta.ws.rs.ServerErrorException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import javax.jdo.FetchGroup; import java.security.Principal; import java.util.Collection; @@ -78,6 +78,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; import java.util.UUID; import java.util.function.BiConsumer; @@ -102,7 +103,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class ProjectResource extends AlpineResource { +public class ProjectResource extends AbstractApiResource { private static final Logger LOGGER = Logger.getLogger(ProjectResource.class); @@ -243,10 +244,12 @@ public Response getProjectChildrenConcise( content = @Content(schema = @Schema(implementation = Project.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) - @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProject( @Parameter(description = "The UUID of the project to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @@ -254,11 +257,8 @@ public Response getProject( try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - return Response.ok(project).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + return Response.ok(project).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -279,7 +279,10 @@ public Response getProject( content = @Content(schema = @Schema(implementation = Project.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -289,11 +292,8 @@ public Response getLatestProjectByName( try (QueryManager qm = new QueryManager()) { final Project project = qm.getLatestProjectVersion(name); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - return Response.ok(project).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + return Response.ok(project).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -315,7 +315,10 @@ public Response getLatestProjectByName( content = @Content(schema = @Schema(implementation = Project.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -327,11 +330,8 @@ public Response getProject( try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(name, version); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - return Response.ok(project).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + return Response.ok(project).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -428,7 +428,10 @@ public Response getProjectsByClassifier( ), @ApiResponse(responseCode = "400", description = "Bad Request"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "The project version cannot be created as latest version because access to current latest version is forbidden."), + @ApiResponse( + responseCode = "403", + description = "Access to the provided parent project, or previous latest project version, is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "409", description = """
  • An inactive Parent cannot be selected as parent, or
  • @@ -457,16 +460,17 @@ public Response createProject(final Project jsonProject) { try (final var qm = new QueryManager()) { if(jsonProject.isLatest()) { final Project oldLatest = qm.getLatestProjectVersion(jsonProject.getName()); - if(oldLatest != null && !qm.hasAccess(super.getPrincipal(), oldLatest)) { - return Response.status(Response.Status.FORBIDDEN) - .entity("Cannot create latest version for project with this name. Access to current latest " + - "version is forbidden!") - .build(); + if(oldLatest != null) { + requireAccess(qm, oldLatest); } } final Project createdProject = qm.callInTransaction(() -> { if (jsonProject.getParent() != null && jsonProject.getParent().getUuid() != null) { Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); + if (parent == null) { + throw new NoSuchElementException("Parent project could not be found"); + } + requireAccess(qm, parent, "Access to the requested parent project is forbidden"); jsonProject.setParent(parent); } @@ -574,8 +578,10 @@ public Response createProject(final Project jsonProject) { content = @Content(schema = @Schema(implementation = Project.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "The project version cannot be set as latest version " + - "because access to current latest version is forbidden."), + @ApiResponse( + responseCode = "403", + description = "Access to the project, the provided parent, or the previous latest project version, is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found"), @ApiResponse(responseCode = "409", description = """
      @@ -612,11 +618,15 @@ public Response updateProject(Project jsonProject) { .entity("The UUID of the project could not be found.") .build()); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - throw new ClientErrorException(Response - .status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build()); + requireAccess(qm, project); + + if (jsonProject.getParent() != null && jsonProject.getParent().getUuid() != null) { + Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); + if (parent == null) { + throw new NoSuchElementException("Parent project could not be found"); + } + requireAccess(qm, parent, "Access to the requested parent project is forbidden"); + jsonProject.setParent(parent); } final String name = StringUtils.trimToNull(jsonProject.getName()); @@ -627,11 +637,8 @@ public Response updateProject(Project jsonProject) { // if project is newly set to latest, ensure user has access to current latest version to modify it if (jsonProject.isLatest() && !project.isLatest()) { final Project oldLatest = qm.getLatestProjectVersion(name); - if(oldLatest != null && !qm.hasAccess(super.getPrincipal(), oldLatest)) { - throw new ClientErrorException(Response - .status(Response.Status.FORBIDDEN) - .entity("Cannot set this project version to latest. Access to current latest version is forbidden.") - .build()); + if(oldLatest != null) { + requireAccess(qm, oldLatest); } } @@ -677,6 +684,10 @@ public Response updateProject(Project jsonProject) { description = "The updated project", content = @Content(schema = @Schema(implementation = Project.class)) ), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project, the provided parent, or the previous latest project version, is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found"), @ApiResponse(responseCode = "409", description = """ @@ -715,21 +726,13 @@ public Response patchProject( .entity("The UUID of the project could not be found.") .build()); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - throw new ClientErrorException(Response - .status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build()); - } + requireAccess(qm, project); // if project is newly set to latest, ensure user has access to current latest version to modify it if (jsonProject.isLatest() && !project.isLatest()) { final var oldName = jsonProject.getName() != null ? jsonProject.getName() : project.getName(); final Project oldLatest = qm.getLatestProjectVersion(oldName); - if(oldLatest != null && !qm.hasAccess(super.getPrincipal(), oldLatest)) { - throw new ClientErrorException(Response - .status(Response.Status.FORBIDDEN) - .entity("Cannot set this project version to latest. Access to current latest version is forbidden.") - .build()); + if(oldLatest != null) { + requireAccess(qm, oldLatest); } } @@ -757,12 +760,7 @@ public Response patchProject( .entity("The UUID of the parent project could not be found.") .build()); } - if (!qm.hasAccess(getPrincipal(), parent)) { - throw new ClientErrorException(Response - .status(Response.Status.FORBIDDEN) - .entity("Access to the specified parent project is forbidden") - .build()); - } + requireAccess(qm, parent, "Access to the requested parent project is forbidden"); modified |= project.getParent() == null || !parent.getUuid().equals(project.getParent().getUuid()); project.setParent(parent); } @@ -852,7 +850,10 @@ private boolean setIfDifferent(final Project source, final Project target, f @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Project removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found"), @ApiResponse(responseCode = "500", description = "Unable to delete components of the project") }) @@ -869,12 +870,7 @@ public Response deleteProject( .entity("The UUID of the project could not be found.") .build()); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - throw new ClientErrorException(Response - .status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build()); - } + requireAccess(qm, project); LOGGER.info("Project " + project + " deletion request by " + super.getPrincipal().getName()); @@ -906,8 +902,10 @@ public Response deleteProject( content = @Content(schema = @Schema(implementation = BomUploadResponse.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "The project clone cannot be set to latest version " + - "because access to current latest version is forbidden."), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project, or the previous latest project version, is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE}) @@ -926,12 +924,7 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { .entity("The UUID of the project could not be found.") .build()); } - if (!qm.hasAccess(super.getPrincipal(), sourceProject)) { - throw new ClientErrorException(Response - .status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build()); - } + requireAccess(qm, sourceProject); if (qm.doesProjectExist(sourceProject.getName(), StringUtils.trimToNull(jsonRequest.getVersion()))) { throw new ClientErrorException(Response .status(Response.Status.CONFLICT) @@ -941,11 +934,8 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { // if project is newly set to latest, ensure user has access to current latest version to modify it if (jsonRequest.makeCloneLatest() && !sourceProject.isLatest()) { final Project oldLatest = qm.getLatestProjectVersion(sourceProject.getName()); - if(oldLatest != null && !qm.hasAccess(super.getPrincipal(), oldLatest)) { - throw new ClientErrorException(Response - .status(Response.Status.CONFLICT) - .entity("Cannot set cloned project version to latest. Access to current latest version is forbidden.") - .build()); + if(oldLatest != null) { + requireAccess(qm, oldLatest); } } @@ -996,7 +986,10 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -1007,12 +1000,9 @@ public Response getChildrenProjects(@Parameter(description = "The UUID of the pr try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { + requireAccess(qm, project); final PaginatedResult result = qm.getChildrenProjects(project.getUuid(), true, excludeInactive); - if (qm.hasAccess(super.getPrincipal(), project)) { - return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } @@ -1035,7 +1025,10 @@ public Response getChildrenProjects(@Parameter(description = "The UUID of the pr content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -1049,13 +1042,10 @@ public Response getChildrenProjectsByClassifier( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { + requireAccess(qm, project); final Classifier classifier = Classifier.valueOf(classifierString); final PaginatedResult result = qm.getChildrenProjects(classifier, project.getUuid(), true, excludeInactive); - if (qm.hasAccess(super.getPrincipal(), project)) { - return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } @@ -1078,7 +1068,10 @@ public Response getChildrenProjectsByClassifier( content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -1092,13 +1085,10 @@ public Response getChildrenProjectsByTag( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { + requireAccess(qm, project); final Tag tag = qm.getTagByName(tagString); final PaginatedResult result = qm.getChildrenProjects(tag, project.getUuid(), true, excludeInactive); - if (qm.hasAccess(super.getPrincipal(), project)) { - return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } @@ -1121,7 +1111,10 @@ public Response getChildrenProjectsByTag( content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -1135,12 +1128,9 @@ public Response getProjectsWithoutDescendantsOf( try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final PaginatedResult result = (name != null) ? qm.getProjectsWithoutDescendantsOf(name, excludeInactive, project) : qm.getProjectsWithoutDescendantsOf(excludeInactive, project); - return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + final PaginatedResult result = (name != null) ? qm.getProjectsWithoutDescendantsOf(name, excludeInactive, project) : qm.getProjectsWithoutDescendantsOf(excludeInactive, project); + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java index efe0788ddd..a984f53b86 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ServiceResource.java @@ -20,7 +20,6 @@ import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.headers.Header; @@ -32,6 +31,17 @@ 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 org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ServiceComponent; +import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.ServiceComponentDao; +import org.dependencytrack.resources.v1.openapi.PaginatedApi; +import org.dependencytrack.resources.v1.problems.ProblemDetails; +import org.jdbi.v3.core.Handle; + import jakarta.validation.Validator; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -43,15 +53,6 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.ServiceComponent; -import org.dependencytrack.model.validation.ValidUuid; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.persistence.jdbi.ServiceComponentDao; -import org.dependencytrack.resources.v1.openapi.PaginatedApi; -import org.jdbi.v3.core.Handle; import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; @@ -68,7 +69,7 @@ @SecurityRequirement(name = "BearerAuth") }) public -class ServiceResource extends AlpineResource { +class ServiceResource extends AbstractApiResource { @GET @Path("/project/{uuid}") @@ -86,7 +87,10 @@ class ServiceResource extends AlpineResource { content = @Content(array = @ArraySchema(schema = @Schema(implementation = ServiceComponent.class))) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -95,12 +99,9 @@ public Response getAllServices(@Parameter(description = "The UUID of the project try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { - if (qm.hasAccess(super.getPrincipal(), project)) { - final PaginatedResult result = qm.getServiceComponents(project, true); - return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); + final PaginatedResult result = qm.getServiceComponents(project, true); + return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -121,7 +122,10 @@ public Response getAllServices(@Parameter(description = "The UUID of the project content = @Content(schema = @Schema(implementation = ServiceComponent.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified service is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The service could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) @@ -131,13 +135,9 @@ public Response getServiceByUuid( try (QueryManager qm = new QueryManager()) { final ServiceComponent service = qm.getObjectByUuid(ServiceComponent.class, uuid); if (service != null) { - final Project project = service.getProject(); - if (qm.hasAccess(super.getPrincipal(), project)) { - final ServiceComponent detachedService = qm.detach(ServiceComponent.class, service.getId()); // TODO: Force project to be loaded. It should be anyway, but JDO seems to be having issues here. - return Response.ok(detachedService).build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified service is forbidden").build(); - } + requireAccess(qm, service.getProject()); + final ServiceComponent detachedService = qm.detach(ServiceComponent.class, service.getId()); // TODO: Force project to be loaded. It should be anyway, but JDO seems to be having issues here. + return Response.ok(detachedService).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The service could not be found.").build(); } @@ -159,7 +159,10 @@ public Response getServiceByUuid( content = @Content(schema = @Schema(implementation = ServiceComponent.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE}) @@ -182,9 +185,7 @@ public Response createService(@Parameter(description = "The UUID of the project" if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); ServiceComponent service = new ServiceComponent(); service.setProject(project); service.setProvider(jsonService.getProvider()); @@ -216,7 +217,10 @@ public Response createService(@Parameter(description = "The UUID of the project" content = @Content(schema = @Schema(implementation = ServiceComponent.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified service is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the service could not be found"), }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE}) @@ -231,9 +235,7 @@ public Response updateService(ServiceComponent jsonService) { try (QueryManager qm = new QueryManager()) { ServiceComponent service = qm.getObjectByUuid(ServiceComponent.class, jsonService.getUuid()); if (service != null) { - if (!qm.hasAccess(super.getPrincipal(), service.getProject())) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified service is forbidden").build(); - } + requireAccess(qm, service.getProject()); // Name cannot be empty or null - prevent it final String name = StringUtils.trimToNull(jsonService.getName()); if (name != null) { @@ -267,7 +269,10 @@ public Response updateService(ServiceComponent jsonService) { @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Service removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified service is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The UUID of the service could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE}) @@ -277,9 +282,7 @@ public Response deleteService( try (QueryManager qm = new QueryManager()) { final ServiceComponent service = qm.getObjectByUuid(ServiceComponent.class, uuid, ServiceComponent.FetchGroup.ALL.name()); if (service != null) { - if (!qm.hasAccess(super.getPrincipal(), service.getProject())) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified service is forbidden").build(); - } + requireAccess(qm, service.getProject()); try (final Handle jdbiHandle = openJdbiHandle()) { final var serviceComponentDao = jdbiHandle.attach(ServiceComponentDao.class); serviceComponentDao.deleteServiceComponent(service.getUuid()); diff --git a/src/main/java/org/dependencytrack/resources/v1/VexResource.java b/src/main/java/org/dependencytrack/resources/v1/VexResource.java index 1ce36f01b2..7765bb2c9e 100644 --- a/src/main/java/org/dependencytrack/resources/v1/VexResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/VexResource.java @@ -21,7 +21,6 @@ import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.server.auth.PermissionRequired; -import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -31,17 +30,6 @@ 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.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.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BOMInputStream; import org.apache.commons.lang3.StringUtils; @@ -61,6 +49,17 @@ import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; +import jakarta.validation.Validator; +import jakarta.ws.rs.Consumes; +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.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.io.InputStream; import java.util.Base64; @@ -79,7 +78,7 @@ @SecurityRequirement(name = "ApiKeyAuth"), @SecurityRequirement(name = "BearerAuth") }) -public class VexResource extends AlpineResource { +public class VexResource extends AbstractApiResource { private static final Logger LOGGER = Logger.getLogger(VexResource.class); @@ -97,7 +96,10 @@ public class VexResource extends AlpineResource { content = @Content(schema = @Schema(type = "string")) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired({Permissions.Constants.VULNERABILITY_ANALYSIS, Permissions.Constants.VULNERABILITY_ANALYSIS_READ}) @@ -111,9 +113,7 @@ public Response exportProjectAsCycloneDx( if (project == null) { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); final CycloneDXExporter exporter = new CycloneDXExporter(CycloneDXExporter.Variant.VEX, qm); @@ -167,8 +167,12 @@ public Response exportProjectAsCycloneDx( schema = @Schema(implementation = InvalidBomProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON ) - ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired({Permissions.Constants.VULNERABILITY_ANALYSIS, Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE}) @@ -228,7 +232,10 @@ public Response uploadVex(VexSubmitRequest request) { ) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired({Permissions.Constants.VULNERABILITY_ANALYSIS, Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE}) @@ -256,9 +263,7 @@ public Response uploadVex(@FormDataParam("project") String projectUuid, */ private Response process(QueryManager qm, Project project, String encodedVexData) { if (project != null) { - if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); - } + requireAccess(qm, project); final byte[] decoded = Base64.getDecoder().decode(encodedVexData); BomResource.validate(decoded, project); final VexUploadEvent vexUploadEvent = new VexUploadEvent(project.getUuid(), decoded); @@ -276,9 +281,7 @@ private Response process(QueryManager qm, Project project, List vulnerabilities = qm.getVulnerabilities(project, suppressed); + return Response.ok(vulnerabilities).header(TOTAL_COUNT_HEADER, vulnerabilities.size()).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } @@ -582,7 +583,10 @@ public void recalculateScoresAndSeverityFromVectors(Vulnerability vuln) throws M @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Assignment successful"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The vulnerability or component could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE}) @@ -599,12 +603,9 @@ public Response assignVulnerability(@Parameter(description = "The vulnerability } final Component component = qm.getObjectByUuid(Component.class, componentUuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - qm.addVulnerability(vulnerability, component, AnalyzerIdentity.NONE); - return Response.ok().build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); + qm.addVulnerability(vulnerability, component, AnalyzerIdentity.NONE); + return Response.ok().build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } @@ -622,7 +623,10 @@ public Response assignVulnerability(@Parameter(description = "The vulnerability @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Assignment successful"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The vulnerability or component could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE}) @@ -637,12 +641,9 @@ public Response assignVulnerability(@Parameter(description = "The UUID of the vu } final Component component = qm.getObjectByUuid(Component.class, componentUuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - qm.addVulnerability(vulnerability, component, AnalyzerIdentity.NONE); - return Response.ok().build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); + qm.addVulnerability(vulnerability, component, AnalyzerIdentity.NONE); + return Response.ok().build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } @@ -660,7 +661,10 @@ public Response assignVulnerability(@Parameter(description = "The UUID of the vu @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Assignment removal successful"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The vulnerability or component could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE}) @@ -677,12 +681,9 @@ public Response unassignVulnerability(@Parameter(description = "The vulnerabilit } final Component component = qm.getObjectByUuid(Component.class, componentUuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - qm.removeVulnerability(vulnerability, component); - return Response.ok().build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); + qm.removeVulnerability(vulnerability, component); + return Response.ok().build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } @@ -700,7 +701,10 @@ public Response unassignVulnerability(@Parameter(description = "The vulnerabilit @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Assignment removal successful"), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "403", description = "Access to the specified component is forbidden"), + @ApiResponse( + responseCode = "403", + description = "Access to the requested project is forbidden", + content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)), @ApiResponse(responseCode = "404", description = "The vulnerability or component could not be found") }) @PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE}) @@ -715,12 +719,9 @@ public Response unassignVulnerability(@Parameter(description = "The UUID of the } Component component = qm.getObjectByUuid(Component.class, componentUuid); if (component != null) { - if (qm.hasAccess(super.getPrincipal(), component.getProject())) { - qm.removeVulnerability(vulnerability, component); - return Response.ok().build(); - } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified component is forbidden").build(); - } + requireAccess(qm, component.getProject()); + qm.removeVulnerability(vulnerability, component); + return Response.ok().build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The component could not be found.").build(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/exception/ProjectAccessDeniedExceptionMapper.java b/src/main/java/org/dependencytrack/resources/v1/exception/ProjectAccessDeniedExceptionMapper.java new file mode 100644 index 0000000000..c60af8c35f --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/exception/ProjectAccessDeniedExceptionMapper.java @@ -0,0 +1,43 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.exception; + +import org.dependencytrack.exception.ProjectAccessDeniedException; +import org.dependencytrack.resources.v1.problems.ProblemDetails; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * @since 5.6.0 + */ +@Provider +public class ProjectAccessDeniedExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(final ProjectAccessDeniedException exception) { + final var problemDetails = new ProblemDetails(); + problemDetails.setStatus(403); + problemDetails.setTitle("Project access denied"); + problemDetails.setDetail(exception.getMessage()); + return problemDetails.toResponse(); + } + +} diff --git a/src/test/java/org/dependencytrack/JerseyTestRule.java b/src/test/java/org/dependencytrack/JerseyTestRule.java index 6884712ec5..6378b2df63 100644 --- a/src/test/java/org/dependencytrack/JerseyTestRule.java +++ b/src/test/java/org/dependencytrack/JerseyTestRule.java @@ -18,6 +18,7 @@ */ package org.dependencytrack; +import org.dependencytrack.resources.v1.exception.ClientErrorExceptionMapper; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider; import org.glassfish.jersey.server.ResourceConfig; @@ -56,7 +57,9 @@ protected void configureClient(final ClientConfig config) { @Override protected DeploymentContext configureDeployment() { - return ServletDeploymentContext.forServlet(new ServletContainer(resourceConfig)).build(); + return ServletDeploymentContext.forServlet(new ServletContainer( + // Ensure exception mappers are registered. + resourceConfig.packages(ClientErrorExceptionMapper.class.getPackageName()))).build(); } }; diff --git a/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java index fee6c8bc6a..dc33125c2a 100644 --- a/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java @@ -23,12 +23,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import alpine.server.filters.AuthorizationFilter; -import jakarta.json.Json; -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.jcip.annotations.NotThreadSafe; import org.apache.http.HttpStatus; import org.dependencytrack.JerseyTestRule; @@ -55,10 +49,18 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.Json; +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.time.Duration; import java.util.List; import java.util.UUID; +import java.util.function.Supplier; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.dependencytrack.assertion.Assertions.assertConditionWithTimeout; import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle; @@ -289,6 +291,56 @@ public void retrieveAnalysisUnauthorizedTest() { assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); } + @Test + public void retrieveAnalysisWithAclTest() { + enablePortfolioAccessControl(); + + initializeWithPermissions(Permissions.VIEW_VULNERABILITY); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + qm.persist(vuln); + + qm.makeAnalysis( + component, vuln, AnalysisState.NOT_AFFECTED, AnalysisJustification.CODE_NOT_REACHABLE, + AnalysisResponse.WILL_NOT_FIX, "Analysis details here", true); + + final Supplier responseSupplier = () -> jersey + .target(V1_ANALYSIS) + .queryParam("component", component.getUuid()) + .queryParam("vulnerability", vuln.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_FORBIDDEN); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + } + @Test public void updateAnalysisCreateNewTest() throws Exception { initializeWithPermissions(Permissions.VULNERABILITY_ANALYSIS); @@ -836,6 +888,57 @@ public void updateAnalysisUnauthorizedTest() { assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); } + @Test + public void updateAnalysisWithAclTest() { + enablePortfolioAccessControl(); + + initializeWithPermissions(Permissions.VULNERABILITY_ANALYSIS); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + qm.persist(vuln); + + final Supplier responseSupplier = () -> jersey + .target(V1_ANALYSIS) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "project": "%s", + "component": "%s", + "vulnerability": "%s", + "comment": "bar" + } + """.formatted(project.getUuid(), component.getUuid(), vuln.getUuid()))); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_FORBIDDEN); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + } + @Test public void updateAnalysisWithAssociatedVulnerabilityPolicyTest() { initializeWithPermissions(Permissions.VULNERABILITY_ANALYSIS); diff --git a/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java index b95291fbe0..85c27bd9de 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java @@ -20,7 +20,6 @@ import alpine.model.IConfigProperty; import alpine.server.filters.ApiFilter; -import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; @@ -31,6 +30,7 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.ws.rs.core.Response; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 2469e5ca99..063df8d3c0 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -23,11 +23,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import junitparams.JUnitParamsRunner; import junitparams.Parameters; import net.javacrumbs.jsonunit.core.Option; @@ -57,7 +52,6 @@ import org.dependencytrack.model.WorkflowStep; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.parser.cyclonedx.CycloneDxValidator; -import org.dependencytrack.resources.v1.exception.JsonMappingExceptionMapper; import org.dependencytrack.resources.v1.vo.BomSubmitRequest; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.HttpUrlConnectorProvider; @@ -70,6 +64,11 @@ import org.junit.Test; import org.junit.runner.RunWith; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -86,6 +85,7 @@ import java.util.Collections; import java.util.List; import java.util.UUID; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -112,8 +112,7 @@ public class BomResourceTest extends ResourceTest { new ResourceConfig(BomResource.class) .register(ApiFilter.class) .register(AuthenticationFilter.class) - .register(MultiPartFeature.class) - .register(JsonMappingExceptionMapper.class)); + .register(MultiPartFeature.class)); @Before @Override @@ -149,6 +148,37 @@ public void exportProjectAsCycloneDxInvalidTest() { Assert.assertEquals("The project could not be found.", body); } + @Test + public void exportProjectAsCycloneDxAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_BOM + "/cyclonedx/project/" + project.getUuid()) + .queryParam("variant", "inventory") + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_FORBIDDEN); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + } + @Test public void exportProjectAsCycloneDxInventoryTest() { var vulnerability = new Vulnerability(); @@ -850,6 +880,42 @@ public void exportComponentAsCycloneDxInvalid() { Assert.assertEquals("The component could not be found.", body); } + @Test + public void exportComponentAsCycloneDxAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_BOM + "/cyclonedx/component/" + component.getUuid()) + .queryParam("variant", "inventory") + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_FORBIDDEN); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + } + @Test public void uploadBomTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD); @@ -1104,7 +1170,7 @@ public void uploadBomInvalidParentTest() throws Exception { .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); String body = getPlainTextBody(response); - Assert.assertEquals("The parent component could not be found.", body); + Assert.assertEquals("The parent project could not be found.", body); request = new BomSubmitRequest(null, "Acme Example", "2.0", null, true, null, "Non-existent parent", null, false, bomString); response = jersey.target(V1_BOM).request() @@ -1112,7 +1178,7 @@ public void uploadBomInvalidParentTest() throws Exception { .put(Entity.entity(request, MediaType.APPLICATION_JSON)); Assert.assertEquals(404, response.getStatus(), 0); body = getPlainTextBody(response); - Assert.assertEquals("The parent component could not be found.", body); + Assert.assertEquals("The parent project could not be found.", body); } @SuppressWarnings("unused") diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java index 5d436d5a99..483194bc7f 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentPropertyResourceTest.java @@ -34,6 +34,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.util.UUID; +import java.util.function.Supplier; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @@ -118,6 +119,40 @@ public void getPropertiesInvalidTest() { assertThat(getPlainTextBody(response)).isEqualTo("The component could not be found."); } + @Test + public void getPropertiesAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void createPropertyTest() { final var project = new Project(); @@ -292,6 +327,48 @@ public void createPropertyComponentNotFoundTest() { assertThat(getPlainTextBody(response)).isEqualTo("The component could not be found."); } + @Test + public void createPropertyAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target("%s/%s/property".formatted(V1_COMPONENT, component.getUuid())).request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "baz", + "propertyType": "STRING", + "description": "qux" + } + """)); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(201); + } + @Test public void deletePropertyTest() { final var project = new Project(); @@ -318,4 +395,47 @@ public void deletePropertyTest() { assertThat(response.getStatus()).isEqualTo(204); assertThat(getPlainTextBody(response)).isEmpty(); } + + @Test + public void deletePropertyAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var property = new ComponentProperty(); + property.setComponent(component); + property.setGroupName("foo"); + property.setPropertyName("bar"); + property.setPropertyValue("baz"); + property.setPropertyType(PropertyType.STRING); + qm.persist(property); + + final Supplier responseSupplier = () -> jersey + .target("%s/%s/property/%s".formatted(V1_COMPONENT, component.getUuid(), property.getUuid())).request() + .header(X_API_KEY, apiKey) + .delete(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(204); + } + } \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java index c9de441021..6b5259fe19 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java @@ -21,13 +21,6 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import com.github.packageurl.MalformedPackageURLException; -import com.github.packageurl.PackageURL; -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.apache.http.HttpStatus; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; @@ -48,12 +41,18 @@ 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 javax.jdo.JDOObjectNotFoundException; import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.UUID; +import java.util.function.Supplier; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @@ -78,59 +77,6 @@ public void getComponentsDefaultRequestTest() { Assert.assertEquals(405, response.getStatus()); // No longer prohibited in DT 4.0+ } - /** - * Generate a project with different dependencies - * - * @return A project with 1000 dpendencies:
        - *
      • 200 outdated dependencies, 75 direct and 125 transitive
      • - *
      • 800 recent dependencies, 25 direct, 775 transitive
      • - * @throws MalformedPackageURLException - */ - private Project prepareProject() throws MalformedPackageURLException { - final Project project = qm.createProject("Acme Application", null, null, null, null, null, null, false); - final List directDepencencies = new ArrayList<>(); - // Generate 1000 dependencies - for (int i = 0; i < 1000; i++) { - Component component = new Component(); - component.setProject(project); - component.setGroup("component-group"); - component.setName("component-name-" + i); - component.setVersion(String.valueOf(i) + ".0"); - component.setPurl(new PackageURL(RepositoryType.MAVEN.toString(), "component-group", "component-name-" + i, String.valueOf(i) + ".0", null, null)); - component = qm.createComponent(component, false); - // direct depencencies - if (i < 100) { - // 100 direct depencencies, 900 transitive depencencies - directDepencencies.add("{\"uuid\":\"" + component.getUuid() + "\"}"); - } - // Recent & Outdated - if ((i >= 25) && (i < 225)) { - // 100 outdated components, 75 of these are direct dependencies, 25 transitive - final var metaComponent = new RepositoryMetaComponent(); - metaComponent.setRepositoryType(RepositoryType.MAVEN); - metaComponent.setNamespace("component-group"); - metaComponent.setName("component-name-" + i); - metaComponent.setLatestVersion(String.valueOf(i + 1) + ".0"); - metaComponent.setLastCheck(new Date()); - qm.persist(metaComponent); - } else if (i < 500) { - // 300 recent components, 25 of these are direct dependencies - final var metaComponent = new RepositoryMetaComponent(); - metaComponent.setRepositoryType(RepositoryType.MAVEN); - metaComponent.setNamespace("component-group"); - metaComponent.setName("component-name-" + i); - metaComponent.setLatestVersion(String.valueOf(i) + ".0"); - metaComponent.setLastCheck(new Date()); - qm.persist(metaComponent); - } else { - // 500 components with no RepositoryMetaComponent containing version - // metadata, all transitive dependencies - } - } - project.setDirectDependencies("[" + String.join(",", directDepencencies.toArray(new String[0])) + "]"); - return project; - } - @Test public void getComponentByUuidTest() { Project project = qm.createProject("Acme Application", null, null, null, null, null, null, false); @@ -157,6 +103,41 @@ public void getComponentByInvalidUuidTest() { Assert.assertEquals("The component could not be found.", body); } + @Test + public void getComponentByUuidAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_COMPONENT + "/" + component.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void getComponentByUuidWithRepositoryMetaDataTest() { Project project = qm.createProject("Acme Application", null, null, null, null, null, null, false); @@ -303,6 +284,52 @@ public void integrityCheckStatusFailTest() { Assert.assertEquals(published.toString(), Date.from(Instant.ofEpochSecond(json.getJsonNumber("updatedAt").longValue() / 1000)).toString()); } + @Test + public void getIntegrityMetaComponentAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var integrityAnalysis = new IntegrityAnalysis(); + integrityAnalysis.setComponent(component); + integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_FAILED); + integrityAnalysis.setUpdatedAt(new Date()); + integrityAnalysis.setId(component.getId()); + integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_FAILED); + integrityAnalysis.setSha1HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha256HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha512HashMatchStatus(HASH_MATCH_FAILED); + qm.persist(integrityAnalysis); + + final Supplier responseSupplier = () -> jersey + .target(V1_COMPONENT + "/" + component.getUuid() + "/integritycheckstatus") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void integrityMetaDataFoundTest() { Project project = qm.createProject("Acme Application", null, null, null, null, null, null, false); @@ -405,6 +432,42 @@ public void getComponentByIdentityWithCoordinatesTest() { assertThat(jsonComponent.getString("uuid")).isEqualTo(componentB.getUuid().toString()); } + @Test + public void getComponentByIdentityAclTest() { + enablePortfolioAccessControl(); + + final var accessibleProject = new Project(); + accessibleProject.setName("acme-app-accessible"); + accessibleProject.addAccessTeam(super.team); + qm.persist(accessibleProject); + + final var accessibleComponent = new Component(); + accessibleComponent.setProject(accessibleProject); + accessibleComponent.setName("acme-lib"); + qm.persist(accessibleComponent); + + final var inaccessibleProject = new Project(); + inaccessibleProject.setName("acme-app-inaccessible"); + qm.persist(inaccessibleProject); + + final var inaccessibleComponent = new Component(); + inaccessibleComponent.setProject(inaccessibleProject); + inaccessibleComponent.setName("acme-lib"); + qm.persist(inaccessibleComponent); + + Response response = jersey.target(V1_COMPONENT + "/identity") + .queryParam("name", "acme-lib") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1"); + + final String responseJson = getPlainTextBody(response); + assertThatJson(responseJson).isArray().hasSize(1); + assertThatJson(responseJson).inPath("$[0].uuid").isEqualTo(accessibleComponent.getUuid().toString()); + } + @Test public void getDependencyGraphForComponentTestWithRepositoryMetaData() { Project project = qm.createProject("Acme Application", null, null, null, null, null, null, false); @@ -693,6 +756,39 @@ public void createComponentUpperCaseHashTest() { Assert.assertEquals(component.getMd5(), json.getString("md5")); } + @Test + public void createComponentAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_COMPONENT + "/project/" + project.getUuid()).request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "name": "acme-lib" + } + """)); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(201); + } + @Test public void updateComponentTest() { Project project = qm.createProject("Acme Application", null, null, null, null, null, null, false); @@ -796,6 +892,44 @@ public void updateComponentInvalidLicenseExpressionTest() { """); } + @Test + public void updateComponentAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey.target(V1_COMPONENT).request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "uuid": "%s", + "name": "acme-lib-foobar" + } + """.formatted(component.getUuid()))); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void deleteComponentTest() { Project project = qm.createProject("Acme Application", null, null, null, null, null, null, false); @@ -834,6 +968,40 @@ public void deleteComponentInvalidUuidTest() { Assert.assertEquals(404, response.getStatus(), 0); } + @Test + public void deleteComponentAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_COMPONENT + "/" + component.getUuid()).request() + .header(X_API_KEY, apiKey) + .delete(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(204); + } + @Test public void internalComponentIdentificationTest() { Response response = jersey.target(V1_COMPONENT + "/internal/identify") @@ -960,4 +1128,38 @@ public void getDependencyGraphForComponentIsNotComponentOfProject() { Assert.assertEquals(0, jsonWithoutComponent.size()); } + @Test + public void getDependencyGraphForComponentAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_COMPONENT + "/project/" + project.getUuid() + "/dependencyGraph/" + component.getUuid()).request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + } diff --git a/src/test/java/org/dependencytrack/resources/v1/DependencyGraphResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/DependencyGraphResourceTest.java index 8fed0f0b59..56119645ce 100644 --- a/src/test/java/org/dependencytrack/resources/v1/DependencyGraphResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/DependencyGraphResourceTest.java @@ -60,6 +60,7 @@ import java.util.Date; import java.util.List; import java.util.UUID; +import java.util.function.Supplier; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @@ -209,6 +210,40 @@ public void getComponentsAndServicesByComponentUuidWithRepositoryMetaTests() { assertThat(json.size()).isEqualTo(nbIteration * 2); } + @Test + public void getComponentsAndServicesByComponentUuidAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_DEPENDENCY_GRAPH + "/component/" + component.getUuid() + "/directDependencies").request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void getComponentsAndServicesByProjectUuidTests() { final int nbIteration = 100; @@ -396,4 +431,38 @@ public void getComponentsAndServicesByProjectUuidWithComponentsWithoutPurlTest() """); } + @Test + public void getComponentsAndServicesByProjectUuidAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_DEPENDENCY_GRAPH + "/project/" + project.getUuid() + "/directDependencies").request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + } \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java index c4979c744b..5ac834dd36 100644 --- a/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/FindingResourceTest.java @@ -51,6 +51,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Supplier; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @@ -169,6 +170,35 @@ public void getFindingsByProjectInvalidTest() { assertEquals("The project could not be found.", body); } + @Test + public void getFindingsByProjectAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_FINDING + "/project/" + project.getUuid()).request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void exportFindingsByProjectTest() { Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); @@ -256,6 +286,35 @@ public void exportFindingsByProjectInvalidTest() { assertEquals("The project could not be found.", body); } + @Test + public void exportFindingsByProjectAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_FINDING + "/project/" + project.getUuid() + "/export").request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void getFindingsByProjectWithComponentLatestVersionTest() { Project p1 = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); diff --git a/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java new file mode 100644 index 0000000000..e6614ece00 --- /dev/null +++ b/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java @@ -0,0 +1,316 @@ +/* + * 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.resources.v1; + +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; +import alpine.server.filters.AuthorizationFilter; +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.ResourceTest; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.ClassRule; +import org.junit.Test; + +import jakarta.ws.rs.core.Response; +import java.util.function.Supplier; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public class MetricsResourceTest extends ResourceTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(MetricsResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class) + .register(AuthorizationFilter.class)); + + @Test + public void getProjectCurrentMetricsAclTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_METRICS + "/project/" + project.getUuid() + "/current") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void getProjectMetricsSinceAclTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_METRICS + "/project/" + project.getUuid() + "/since/20250101") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void getProjectMetricsXDaysAclTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_METRICS + "/project/" + project.getUuid() + "/days/666") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void refreshProjectMetricsAclTest() { + initializeWithPermissions(Permissions.PORTFOLIO_MANAGEMENT); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_METRICS + "/project/" + project.getUuid() + "/refresh") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void getComponentCurrentMetricsAclTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_METRICS + "/component/" + component.getUuid() + "/current") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void getComponentMetricsSinceAclTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_METRICS + "/component/" + component.getUuid() + "/since/20250101") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void getComponentMetricsXDaysAclTest() { + initializeWithPermissions(Permissions.VIEW_PORTFOLIO); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_METRICS + "/component/" + component.getUuid() + "/days/666") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void refreshComponentMetricsAclTest() { + initializeWithPermissions(Permissions.PORTFOLIO_MANAGEMENT); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_METRICS + "/component/" + component.getUuid() + "/refresh") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + +} \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java index f05985eb1b..6e293bee81 100644 --- a/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java @@ -23,11 +23,6 @@ import alpine.notification.NotificationLevel; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -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.NotificationPublisher; @@ -44,10 +39,16 @@ 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.List; import java.util.UUID; +import java.util.function.Supplier; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -261,6 +262,40 @@ public void addProjectToRuleDuplicateProjectTest() { Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); } + @Test + public void addProjectToRuleAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final NotificationPublisher publisher = qm.getNotificationPublisher( + DefaultNotificationPublishers.SLACK.getPublisherName()); + final NotificationRule rule = qm.createNotificationRule( + "rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + + final Supplier responseSupplier = () -> jersey + .target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void removeProjectFromRuleTest() { Project project = qm.createProject("Acme Example", null, null, null, null, null, null, false); @@ -329,6 +364,41 @@ public void removeProjectFromRuleDuplicateProjectTest() { Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); } + @Test + public void removeProjectFromRuleAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final NotificationPublisher publisher = qm.getNotificationPublisher( + DefaultNotificationPublishers.SLACK.getPublisherName()); + final NotificationRule rule = qm.createNotificationRule( + "rule", NotificationScope.PORTFOLIO, NotificationLevel.INFORMATIONAL, publisher); + rule.setProjects(List.of(project)); + + final Supplier responseSupplier = () -> jersey + .target(V1_NOTIFICATION_RULE + "/" + rule.getUuid().toString() + "/project/" + project.getUuid().toString()).request() + .header(X_API_KEY, apiKey) + .delete(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void addTeamToRuleTest(){ Team team = qm.createTeam("Team Example", false); diff --git a/src/test/java/org/dependencytrack/resources/v1/PolicyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PolicyResourceTest.java index e41601c718..c6b2cca304 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PolicyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PolicyResourceTest.java @@ -21,11 +21,6 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -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.Component; @@ -37,9 +32,17 @@ 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.Date; +import java.util.List; +import java.util.function.Supplier; import static java.util.Collections.singletonList; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; public class PolicyResourceTest extends ResourceTest { @@ -253,6 +256,42 @@ public void addProjectToPolicyProjectAlreadyAddedTest() { assertThat(response.getStatus()).isEqualTo(304); } + @Test + public void addProjectToPolicyAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var policy = new Policy(); + policy.setName("policy"); + policy.setOperator(Policy.Operator.ANY); + policy.setViolationState(Policy.ViolationState.INFO); + qm.persist(policy); + + final Supplier responseSupplier = () -> jersey + .target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .post(null); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void removeProjectFromPolicyTest() { final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.INFO); @@ -281,4 +320,42 @@ public void removeProjectFromPolicyProjectAlreadyRemovedTest() { assertThat(response.getStatus()).isEqualTo(304); } + + @Test + public void removeProjectFromPolicyAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var policy = new Policy(); + policy.setName("policy"); + policy.setOperator(Policy.Operator.ANY); + policy.setViolationState(Policy.ViolationState.INFO); + policy.setProjects(List.of(project)); + qm.persist(policy); + + final Supplier responseSupplier = () -> jersey + .target(V1_POLICY + "/" + policy.getUuid() + "/project/" + project.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .delete(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + } diff --git a/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java index ced49bc930..d394e65c01 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java @@ -22,9 +22,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import alpine.server.filters.AuthorizationFilter; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; @@ -40,10 +37,15 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.Date; import java.util.UUID; +import java.util.function.Supplier; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; public class PolicyViolationResourceTest extends ResourceTest { @@ -244,6 +246,37 @@ public void getViolationsByProjectNotFoundTest() { assertThat(getPlainTextBody(response)).contains("project could not be found"); } + @Test + public void getViolationsByProjectAclTest() { + initializeWithPermissions(Permissions.VIEW_POLICY_VIOLATION); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_POLICY_VIOLATION + "/project/" + project.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void getViolationsByComponentTest() { initializeWithPermissions(Permissions.VIEW_POLICY_VIOLATION); @@ -311,6 +344,41 @@ public void getViolationsByComponentNotFoundTest() { assertThat(getPlainTextBody(response)).contains("component could not be found"); } + @Test + public void getViolationsByComponentAclTest() { + initializeWithPermissions(Permissions.VIEW_POLICY_VIOLATION); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey.target(V1_POLICY_VIOLATION + "/component/" + component.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void getViolationsWithAclEnabledTest() { initializeWithPermissions(Permissions.VIEW_POLICY_VIOLATION); @@ -394,13 +462,13 @@ public void getViolationsWithAclEnabledTest() { final Response responseB = jersey.target(V1_POLICY_VIOLATION) .request() - .header(X_API_KEY, team.getApiKeys().get(0).getKey()) + .header(X_API_KEY, team.getApiKeys().getFirst().getKey()) .get(); assertThat(responseB.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); - assertThat(responseB.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("2"); + assertThat(responseB.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("3"); final JsonArray jsonArray = parseJsonArray(responseB); - assertThat(jsonArray).hasSize(2); + assertThat(jsonArray).hasSize(3); final JsonObject jsonObjectA = jsonArray.getJsonObject(0); assertThat(jsonObjectA.getString("uuid")).isEqualTo(violationD.getUuid().toString()); @@ -412,13 +480,22 @@ public void getViolationsWithAclEnabledTest() { assertThat(jsonObjectA.getJsonObject("project").getString("uuid")).isEqualTo(projectA.getUuid().toString()); final JsonObject jsonObjectB = jsonArray.getJsonObject(1); - assertThat(jsonObjectB.getString("uuid")).isEqualTo(violationA.getUuid().toString()); + assertThat(jsonObjectB.getString("uuid")).isEqualTo(violationB.getUuid().toString()); assertThat(jsonObjectB.getString("type")).isEqualTo(PolicyViolation.Type.OPERATIONAL.name()); assertThat(jsonObjectB.getJsonObject("policyCondition")).isNotNull(); assertThat(jsonObjectB.getJsonObject("policyCondition").getJsonObject("policy")).isNotNull(); assertThat(jsonObjectB.getJsonObject("policyCondition").getJsonObject("policy").getString("name")).isEqualTo("Blacklisted Version"); assertThat(jsonObjectB.getJsonObject("policyCondition").getJsonObject("policy").getString("violationState")).isEqualTo("FAIL"); - assertThat(jsonObjectB.getJsonObject("project").getString("uuid")).isEqualTo(projectA.getUuid().toString()); + assertThat(jsonObjectB.getJsonObject("project").getString("uuid")).isEqualTo(projectA_child.getUuid().toString()); + + final JsonObject jsonObjectC = jsonArray.getJsonObject(2); + assertThat(jsonObjectC.getString("uuid")).isEqualTo(violationA.getUuid().toString()); + assertThat(jsonObjectC.getString("type")).isEqualTo(PolicyViolation.Type.OPERATIONAL.name()); + assertThat(jsonObjectC.getJsonObject("policyCondition")).isNotNull(); + assertThat(jsonObjectC.getJsonObject("policyCondition").getJsonObject("policy")).isNotNull(); + assertThat(jsonObjectC.getJsonObject("policyCondition").getJsonObject("policy").getString("name")).isEqualTo("Blacklisted Version"); + assertThat(jsonObjectC.getJsonObject("policyCondition").getJsonObject("policy").getString("violationState")).isEqualTo("FAIL"); + assertThat(jsonObjectC.getJsonObject("project").getString("uuid")).isEqualTo(projectA.getUuid().toString()); } @Test diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectPropertyResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectPropertyResourceTest.java index a093fdbbe9..03682aa54e 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectPropertyResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectPropertyResourceTest.java @@ -18,14 +18,9 @@ */ package org.dependencytrack.resources.v1; -import alpine.model.IConfigProperty; +import alpine.model.IConfigProperty.PropertyType; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -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.Project; @@ -36,7 +31,16 @@ 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.UUID; +import java.util.function.Supplier; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; public class ProjectPropertyResourceTest extends ResourceTest { @@ -49,8 +53,8 @@ public class ProjectPropertyResourceTest extends ResourceTest { @Test public void getPropertiesTest() { Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); - qm.createProjectProperty(project, "mygroup", "prop1", "value1", IConfigProperty.PropertyType.STRING, "Test Property 1"); - qm.createProjectProperty(project, "mygroup", "prop2", "value2", IConfigProperty.PropertyType.ENCRYPTEDSTRING, "Test Property 2"); + qm.createProjectProperty(project, "mygroup", "prop1", "value1", PropertyType.STRING, "Test Property 1"); + qm.createProjectProperty(project, "mygroup", "prop2", "value2", PropertyType.ENCRYPTEDSTRING, "Test Property 2"); Response response = jersey.target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() .header(X_API_KEY, apiKey) .get(Response.class); @@ -82,6 +86,36 @@ public void getPropertiesInvalidTest() { Assert.assertEquals("The project could not be found.", body); } + @Test + public void getPropertiesAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_PROJECT + "/" + project.getUuid() + "/property") + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void createPropertyTest() { Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); @@ -90,7 +124,7 @@ public void createPropertyTest() { property.setGroupName("mygroup"); property.setPropertyName("prop1"); property.setPropertyValue("value1"); - property.setPropertyType(IConfigProperty.PropertyType.STRING); + property.setPropertyType(PropertyType.STRING); property.setDescription("Test Property 1"); Response response = jersey.target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() .header(X_API_KEY, apiKey) @@ -113,7 +147,7 @@ public void createPropertyEncryptedTest() { property.setGroupName("mygroup"); property.setPropertyName("prop1"); property.setPropertyValue("value1"); - property.setPropertyType(IConfigProperty.PropertyType.ENCRYPTEDSTRING); + property.setPropertyType(PropertyType.ENCRYPTEDSTRING); property.setDescription("Test Property 1"); Response response = jersey.target(V1_PROJECT + "/" + project.getUuid().toString() + "/property").request() .header(X_API_KEY, apiKey) @@ -131,7 +165,7 @@ public void createPropertyEncryptedTest() { @Test public void createPropertyDuplicateTest() { Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); - qm.createProjectProperty(project, "mygroup", "prop1", "value1", IConfigProperty.PropertyType.STRING, null); + qm.createProjectProperty(project, "mygroup", "prop1", "value1", PropertyType.STRING, null); String uuid = project.getUuid().toString(); qm.close(); ProjectProperty property = new ProjectProperty(); @@ -139,7 +173,7 @@ public void createPropertyDuplicateTest() { property.setGroupName("mygroup"); property.setPropertyName("prop1"); property.setPropertyValue("value1"); - property.setPropertyType(IConfigProperty.PropertyType.STRING); + property.setPropertyType(PropertyType.STRING); property.setDescription("Test Property 1"); Response response = jersey.target(V1_PROJECT + "/" + uuid + "/property").request() .header(X_API_KEY, apiKey) @@ -158,7 +192,7 @@ public void createPropertyInvalidTest() { property.setGroupName("mygroup"); property.setPropertyName("prop1"); property.setPropertyValue("value1"); - property.setPropertyType(IConfigProperty.PropertyType.STRING); + property.setPropertyType(PropertyType.STRING); property.setDescription("Test Property 1"); Response response = jersey.target(V1_PROJECT + "/" + UUID.randomUUID() + "/property").request() .header(X_API_KEY, apiKey) @@ -169,11 +203,48 @@ public void createPropertyInvalidTest() { Assert.assertEquals("The project could not be found.", body); } + @Test + public void createPropertyAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_PROJECT + "/" + project.getUuid() + "/property") + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "baz", + "propertyType": "STRING" + } + """)); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(201); + } + @Test public void updatePropertyTest() { Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); String uuid = project.getUuid().toString(); - ProjectProperty property = qm.createProjectProperty(project, "mygroup", "prop1", "value1", IConfigProperty.PropertyType.STRING, null); + ProjectProperty property = qm.createProjectProperty(project, "mygroup", "prop1", "value1", PropertyType.STRING, null); qm.getPersistenceManager().detachCopy(property); qm.close(); property.setPropertyValue("updatedValue"); @@ -197,7 +268,7 @@ public void updatePropertyInvalidTest() { property.setGroupName("mygroup"); property.setPropertyName("prop1"); property.setPropertyValue("value1"); - property.setPropertyType(IConfigProperty.PropertyType.STRING); + property.setPropertyType(PropertyType.STRING); property.setDescription("Test Property 1"); Response response = jersey.target(V1_PROJECT + "/" + UUID.randomUUID().toString() + "/property").request() .header(X_API_KEY, apiKey) @@ -208,10 +279,55 @@ public void updatePropertyInvalidTest() { Assert.assertEquals("The project could not be found.", body); } + @Test + public void updatePropertyAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var property = new ProjectProperty(); + property.setProject(project); + property.setGroupName("foo"); + property.setPropertyName("bar"); + property.setPropertyValue("baz"); + property.setPropertyType(PropertyType.STRING); + qm.persist(property); + + final Supplier responseSupplier = () -> jersey + .target(V1_PROJECT + "/" + project.getUuid() + "/property") + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "groupName": "foo", + "propertyName": "bar", + "propertyValue": "qux", + "propertyType": "STRING" + } + """)); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void deletePropertyTest() { Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false); - ProjectProperty property = qm.createProjectProperty(project, "mygroup", "prop1", "value1", IConfigProperty.PropertyType.STRING, null); + ProjectProperty property = qm.createProjectProperty(project, "mygroup", "prop1", "value1", PropertyType.STRING, null); String uuid = project.getUuid().toString(); qm.getPersistenceManager().detachCopy(property); qm.close(); @@ -221,4 +337,49 @@ public void deletePropertyTest() { .method("DELETE", Entity.entity(property, MediaType.APPLICATION_JSON)); // HACK Assert.assertEquals(204, response.getStatus(), 0); } + + @Test + public void deletePropertyAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var property = new ProjectProperty(); + property.setProject(project); + property.setGroupName("foo"); + property.setPropertyName("bar"); + property.setPropertyValue("baz"); + property.setPropertyType(PropertyType.STRING); + qm.persist(property); + + final Supplier responseSupplier = () -> jersey + .target(V1_PROJECT + "/" + project.getUuid() + "/property") + .request() + .header(X_API_KEY, apiKey) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) + .method("DELETE", Entity.json(/* language=JSON */ """ + { + "groupName": "foo", + "propertyName": "bar" + } + """)); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(204); + } + } diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 9a29e71b1c..1db7f7c7e1 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -26,14 +26,6 @@ import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.ws.rs.HttpMethod; -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.auth.Permissions; @@ -74,6 +66,14 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -88,6 +88,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -274,7 +275,13 @@ public void getProjectLookupNotPermittedTest() { .header(X_API_KEY, apiKey) .get(); assertThat(response.getStatus()).isEqualTo(403); - assertThat(getPlainTextBody(response)).isEqualTo("Access to the specified project is forbidden"); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); } @Test @@ -1393,7 +1400,13 @@ public void getProjectByUuidNotPermittedTest() { .header(X_API_KEY, apiKey) .get(); assertThat(response.getStatus()).isEqualTo(403); - assertThat(getPlainTextBody(response)).isEqualTo("Access to the specified project is forbidden"); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); } @Test @@ -1463,6 +1476,36 @@ public void getProjectByUnknownTagTest() { Assert.assertEquals(0, json.size()); } + @Test + public void getProjectsByTagAclTest() { + enablePortfolioAccessControl(); + + final var accessibleProject = new Project(); + accessibleProject.setName("acme-app-accessible"); + accessibleProject.addAccessTeam(super.team); + qm.persist(accessibleProject); + + final var inaccessibleProject = new Project(); + inaccessibleProject.setName("acme-app-inaccessible"); + qm.persist(inaccessibleProject); + + final Tag tag = new Tag("foo"); + qm.persist(tag); + + qm.bind(accessibleProject, List.of(tag)); + qm.bind(inaccessibleProject, List.of(tag)); + + final Response response = jersey.target(V1_PROJECT + "/tag/foo") + .request() + .header(X_API_KEY, apiKey) + .get(); + assertThat(response.getStatus()).isEqualTo(200); + + final String responseJson = getPlainTextBody(response); + assertThatJson(responseJson).isArray().hasSize(1); + assertThatJson(responseJson).inPath("$[0].uuid").isEqualTo(accessibleProject.getUuid().toString()); + } + @Test public void createProjectTest() throws Exception { Project project = new Project(); @@ -1580,6 +1623,66 @@ public void createProjectEmptyTest() { Assert.assertEquals(400, response.getStatus(), 0); } + @Test + public void createProjectNonExistentParentTest() { + final Response response = jersey.target(V1_PROJECT) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "parent": { + "uuid": "5e506116-8d58-4403-8631-971ec31961f6" + }, + "name": "acme-app" + } + """)); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 404, + "title": "Resource does not exist", + "detail": "Parent project could not be found" + } + """); + } + + @Test + public void createProjectInaccessibleParentTest() { + enablePortfolioAccessControl(); + + final var parentProject = new Project(); + parentProject.setName("acme-app-parent"); + qm.persist(parentProject); + + final Supplier responseSupplier = () -> jersey + .target(V1_PROJECT) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "parent": { + "uuid": "%s" + }, + "name": "acme-app" + } + """.formatted(parentProject.getUuid()))); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested parent project is forbidden" + } + """); + + parentProject.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(201); + } + @Test public void updateProjectTest() { Project project = qm.createProject("ABC", null, "1.0", null, null, null, null, false); @@ -1623,14 +1726,20 @@ public void updateProjectNotPermittedTest() { final Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) - .post(Entity.json(""" + .post(Entity.json(/* language=JSON */ """ { "uuid": "%s", "name": "acme-app-foo" } """.formatted(project.getUuid()))); assertThat(response.getStatus()).isEqualTo(403); - assertThat(getPlainTextBody(response)).isEqualTo("Access to the specified project is forbidden"); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); } @Test @@ -1716,6 +1825,77 @@ public void updateProjectDuplicateTest() { Assert.assertEquals("A project with the specified name and version already exists.", body); } + @Test + public void updateProjectInaccessibleParentTest() { + enablePortfolioAccessControl(); + + final var parentProject = new Project(); + parentProject.setName("acme-app-parent"); + qm.persist(parentProject); + + final var project = new Project(); + project.setName("acme-app"); + project.addAccessTeam(super.team); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_PROJECT) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "parent": { + "uuid": "%s" + }, + "uuid": "%s", + "name": "acme-app" + } + """.formatted(parentProject.getUuid(), project.getUuid()))); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested parent project is forbidden" + } + """); + + parentProject.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void updateProjectNonExistentParentTest() { + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Response response = jersey.target(V1_PROJECT) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "parent": { + "uuid": "b99bd9cf-d8d1-48ae-972e-615e6cc59e52" + }, + "uuid": "%s", + "name": "acme-app" + } + """.formatted(project.getUuid()))); + assertThat(response.getStatus()).isEqualTo(404); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 404, + "title": "Resource does not exist", + "detail": "Parent project could not be found" + } + """); + } + @Test public void deleteProjectTest() { Project project = qm.createProject("ABC", null, "1.0", null, null, null, null, false); @@ -1736,6 +1916,36 @@ public void deleteProjectInvalidUuidTest() { Assert.assertEquals(404, response.getStatus(), 0); } + @Test + public void deleteProjectAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_PROJECT + "/" + project.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .delete(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(204); + } + @Test public void patchProjectNotModifiedTest() { final var tags = Stream.of("tag1", "tag2").map(qm::createTag).collect(Collectors.toUnmodifiableList()); @@ -1790,13 +2000,19 @@ public void patchProjectNotPermittedTest() { .request() .header(X_API_KEY, apiKey) .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) - .method("PATCH", Entity.json(""" + .method("PATCH", Entity.json(/* language=JSON */ """ { "name": "acme-app-foo" } """)); assertThat(response.getStatus()).isEqualTo(403); - assertThat(getPlainTextBody(response)).isEqualTo("Access to the specified project is forbidden"); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); } @Test @@ -1902,6 +2118,43 @@ public void patchProjectParentNotFoundTest() { assertThat(project.getParent().getUuid()).isEqualTo(parent.getUuid()); } + @Test + public void patchProjectParentInaccessibleTest() { + enablePortfolioAccessControl(); + + final var parentProject = new Project(); + parentProject.setName("acme-app-parent"); + qm.persist(parentProject); + + final var project = new Project(); + project.setName("acme-app"); + project.addAccessTeam(super.team); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target(V1_PROJECT + "/" + project.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) + .method(HttpMethod.PATCH, Entity.json(/* language=JSON */ """ + { + "parent": { + "uuid": "%s" + } + } + """.formatted(parentProject.getUuid()))); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested parent project is forbidden" + } + """); + } + @Test public void patchProjectSuccessfullyPatchedTest() { final var tags = Stream.of("tag1", "tag2").map(qm::createTag).collect(Collectors.toUnmodifiableList()); @@ -2356,18 +2609,24 @@ public void cloneProjectWithAclTest() { Response response = jersey.target("%s/clone".formatted(V1_PROJECT)).request() .header(X_API_KEY, apiKey) - .put(Entity.json(""" + .put(Entity.json(/* language=JSON */ """ { "project": "%s", "version": "1.1.0" } """.formatted(noAccessProject.getUuid()))); assertThat(response.getStatus()).isEqualTo(403); - assertThat(getPlainTextBody(response)).isEqualTo("Access to the specified project is forbidden"); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); response = jersey.target("%s/clone".formatted(V1_PROJECT)).request() .header(X_API_KEY, apiKey) - .put(Entity.json(""" + .put(Entity.json(/* language=JSON */ """ { "project": "%s", "version": "1.1.0" @@ -3022,6 +3281,7 @@ public void getLatestProjectWithAclEnabledNoAccessTest() { Assert.assertEquals(403, response.getStatus(), 0); } + @Test public void createProjectAsUserWithAclEnabledAndExistingTeamByUuidTest() { qm.createConfigProperty( ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), @@ -3058,7 +3318,8 @@ public void createProjectAsUserWithAclEnabledAndExistingTeamByUuidTest() { "children": [], "properties": [], "tags": [], - "active": true + "active": true, + "isLatest": false } """); diff --git a/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java index 6624eb06c3..38bfe52463 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TagResourceTest.java @@ -21,10 +21,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import alpine.server.filters.AuthorizationFilter; -import jakarta.json.JsonArray; -import jakarta.ws.rs.HttpMethod; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; @@ -34,15 +30,16 @@ import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.notification.NotificationScope; -import org.dependencytrack.resources.v1.exception.ConstraintViolationExceptionMapper; -import org.dependencytrack.resources.v1.exception.NoSuchElementExceptionMapper; -import org.dependencytrack.resources.v1.exception.TagOperationFailedExceptionMapper; 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.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -60,10 +57,7 @@ public class TagResourceTest extends ResourceTest { new ResourceConfig(TagResource.class) .register(ApiFilter.class) .register(AuthenticationFilter.class) - .register(AuthorizationFilter.class) - .register(ConstraintViolationExceptionMapper.class) - .register(NoSuchElementExceptionMapper.class) - .register(TagOperationFailedExceptionMapper.class)); + .register(AuthorizationFilter.class)); @Test public void getTagsTest() { diff --git a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index eb364e1097..b460ed2126 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -21,9 +21,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; -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; @@ -38,16 +35,19 @@ import org.dependencytrack.model.Severity; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.parser.cyclonedx.CycloneDxValidator; -import org.dependencytrack.resources.v1.exception.JsonMappingExceptionMapper; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @@ -64,8 +64,7 @@ public class VexResourceTest extends ResourceTest { new ResourceConfig(VexResource.class) .register(ApiFilter.class) .register(AuthenticationFilter.class) - .register(MultiPartFeature.class) - .register(JsonMappingExceptionMapper.class)); + .register(MultiPartFeature.class)); @Before @Override @@ -226,6 +225,36 @@ public void exportProjectAsCycloneDxTest() { """); } + @Test + public void exportProjectAsCycloneDxAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Supplier responseSupplier = () -> jersey + .target("%s/cyclonedx/project/%s".formatted(V1_VEX, project.getUuid())) + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void uploadVexInvalidJsonTest() { initializeWithPermissions(Permissions.BOM_UPLOAD); @@ -348,6 +377,49 @@ public void uploadVexTooLargeViaPutTest() { """); } + @Test + public void uploadVexAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final String encodedVex = Base64.getEncoder().encodeToString(/* language=JSON */ """ + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1 + } + """.getBytes()); + + final Supplier responseSupplier = () -> jersey.target(V1_VEX).request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "projectName": "acme-app", + "projectVersion": "1.0.0", + "vex": "%s" + } + """.formatted(encodedVex))); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void exportVexWithSameVulnAnalysisValidJsonTest() { var project = new Project(); diff --git a/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java index 49571c6f70..9537cff5d9 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ViolationAnalysisResourceTest.java @@ -53,7 +53,9 @@ import java.time.Duration; import java.util.Date; import java.util.UUID; +import java.util.function.Supplier; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.dependencytrack.assertion.Assertions.assertConditionWithTimeout; import static org.dependencytrack.proto.notification.v1.Group.GROUP_PROJECT_AUDIT_CHANGE; @@ -169,6 +171,44 @@ public void retrieveAnalysisViolationNotFoundTest() { assertThat(getPlainTextBody(response)).contains("policy violation could not be found"); } + @Test + public void retrieveAnalysisAclTest() { + initializeWithPermissions(Permissions.VIEW_POLICY_VIOLATION); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Component component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_VIOLATION_ANALYSIS) + .queryParam("component", component.getUuid()) + .queryParam("policyViolation", UUID.randomUUID()) + .request() + .header(X_API_KEY, apiKey) + .get(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(404); + } + @Test public void updateAnalysisCreateNewTest() throws Exception { initializeWithPermissions(Permissions.POLICY_VIOLATION_ANALYSIS); @@ -505,4 +545,48 @@ public void updateAnalysisViolationNotFoundTest() { assertThat(getPlainTextBody(response)).contains("policy violation could not be found"); } + @Test + public void updateAnalysisAclTest() { + initializeWithPermissions(Permissions.POLICY_VIOLATION_ANALYSIS); + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final Component component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Supplier responseSupplier = () -> jersey + .target(V1_VIOLATION_ANALYSIS) + .queryParam("component", component.getUuid()) + .queryParam("policyViolation", UUID.randomUUID()) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "component": "%s", + "policyViolation": "9b0e0cec-4bef-4d6d-b767-02f280f55e76", + "comment": "foo" + } + """.formatted(component.getUuid()))); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(404); + } + } \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java index 3f75284f6c..30f13637d4 100644 --- a/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java @@ -21,12 +21,6 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import jakarta.json.Json; -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; @@ -47,8 +41,15 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.Json; +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.List; import java.util.UUID; +import java.util.function.Supplier; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; @@ -980,6 +981,86 @@ public void assignVulnerabilityByUuidInvalidComponentTest() { Assert.assertEquals("The component could not be found.", body); } + @Test + public void assignVulnerabilityAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + qm.persist(vuln); + + final Supplier responseSupplier = () -> jersey + .target(V1_VULNERABILITY + "/source/INTERNAL/vuln/INT-001/component/" + component.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void assignVulnerabilityByUuidAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + qm.persist(vuln); + + final Supplier responseSupplier = () -> jersey + .target(V1_VULNERABILITY + "/" + vuln.getUuid() + "/component/" + component.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json("")); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test public void unassignVulnerabilityTest() { Vulnerability vuln = new Vulnerability(); @@ -1078,6 +1159,88 @@ public void unassignVulnerabilityByUuidInvalidComponentTest() { Assert.assertEquals("The component could not be found.", body); } + @Test + public void unassignVulnerabilityAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + vuln.setComponents(List.of(component)); + qm.persist(vuln); + + final Supplier responseSupplier = () -> jersey + .target(V1_VULNERABILITY + "/source/INTERNAL/vuln/INT-001/component/" + component.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .delete(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void unassignVulnerabilityByUuidAclTest() { + enablePortfolioAccessControl(); + + final var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-001"); + vuln.setSource(Vulnerability.Source.INTERNAL); + vuln.setComponents(List.of(component)); + qm.persist(vuln); + + final Supplier responseSupplier = () -> jersey + .target(V1_VULNERABILITY + "/" + vuln.getUuid() + "/component/" + component.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .delete(); + + Response response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(403); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 403, + "title": "Project access denied", + "detail": "Access to the requested project is forbidden" + } + """); + + project.addAccessTeam(super.team); + + response = responseSupplier.get(); + assertThat(response.getStatus()).isEqualTo(200); + } + private class SampleData { final Project p1; final Project p2; From 8d6d78944dea761221fbf61d3ca943bff63dacb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:31:37 +0000 Subject: [PATCH 026/181] Bump docker/setup-buildx-action from 3.9.0 to 3.10.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.9.0 to 3.10.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3.9.0...v3.10.0) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index dd55730fc5..4ecd8b2192 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -97,7 +97,7 @@ jobs: uses: docker/setup-qemu-action@v3.4.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.9.0 + uses: docker/setup-buildx-action@v3.10.0 id: buildx with: install: true From a02d686501dd2c2b6ee7ee5944045e6640ffb86e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:31:30 +0000 Subject: [PATCH 027/181] Bump docker/build-push-action from 6.13.0 to 6.15.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.13.0 to 6.15.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6.13.0...v6.15.0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 4ecd8b2192..5d9c7c8fb0 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -128,7 +128,7 @@ jobs: echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Build multi-arch Container Image - uses: docker/build-push-action@v6.13.0 + uses: docker/build-push-action@v6.15.0 with: tags: ${{ steps.tags.outputs.tags }} build-args: |- From 93a54400f0b379c92a0b29821bb8a5e67648fcd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:31:35 +0000 Subject: [PATCH 028/181] Bump actions/download-artifact from 4.1.8 to 4.1.9 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.8 to 4.1.9. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4.1.8...v4.1.9) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-publish.yaml | 2 +- .github/workflows/ci-test-pr-coverage.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 5d9c7c8fb0..6af96f8c4b 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -88,7 +88,7 @@ jobs: uses: actions/checkout@v4.2.2 - name: Download Artifacts - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v4.1.9 with: name: assembled-wars path: target diff --git a/.github/workflows/ci-publish.yaml b/.github/workflows/ci-publish.yaml index 86b24e2a1e..79801f04c7 100644 --- a/.github/workflows/ci-publish.yaml +++ b/.github/workflows/ci-publish.yaml @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v4.2.2 - name: Download Artifacts - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@v4.1.9 with: name: assembled-wars path: target diff --git a/.github/workflows/ci-test-pr-coverage.yml b/.github/workflows/ci-test-pr-coverage.yml index 6d89ef7880..e2660d27e3 100644 --- a/.github/workflows/ci-test-pr-coverage.yml +++ b/.github/workflows/ci-test-pr-coverage.yml @@ -34,7 +34,7 @@ jobs: && github.event.workflow_run.conclusion == 'success' steps: - name: Download PR test coverage report - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # tag=v4.1.8 + uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # tag=v4.1.9 with: name: pr-test-coverage-report github-token: ${{ secrets.GITHUB_TOKEN }} From 9370cb88b8dab94338fc57aa6df752a154a2a311 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:31:24 +0000 Subject: [PATCH 029/181] Bump docker/setup-qemu-action from 3.4.0 to 3.6.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.4.0 to 3.6.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v3.4.0...v3.6.0) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- .github/workflows/_meta-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 6af96f8c4b..129b0f52fb 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -94,7 +94,7 @@ jobs: path: target - name: Set up QEMU - uses: docker/setup-qemu-action@v3.4.0 + uses: docker/setup-qemu-action@v3.6.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.10.0 From 98948b9ccab49431cad29359b9350055a3327f75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:31:28 +0000 Subject: [PATCH 030/181] Bump actions/upload-artifact from 4.6.0 to 4.6.1 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.0 to 4.6.1. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.6.0...v4.6.1) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- .github/workflows/_meta-build.yaml | 2 +- .github/workflows/ci-test.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_meta-build.yaml b/.github/workflows/_meta-build.yaml index 129b0f52fb..a1a3a2b1c6 100644 --- a/.github/workflows/_meta-build.yaml +++ b/.github/workflows/_meta-build.yaml @@ -69,7 +69,7 @@ jobs: mvn -B --no-transfer-progress cyclonedx:makeBom -Dservices.bom.merge.skip=false org.codehaus.mojo:exec-maven-plugin:exec@merge-services-bom - name: Upload Artifacts - uses: actions/upload-artifact@v4.6.0 + uses: actions/upload-artifact@v4.6.1 with: name: assembled-wars path: |- diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index 9408b491c4..f6b50c3144 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -84,7 +84,7 @@ jobs: - name: Upload PR test coverage report if: ${{ github.event_name == 'pull_request' }} - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # tag=v4.4.0 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # tag=v4.4.0 with: name: pr-test-coverage-report path: |- From 6820bdafa6ac191fafefac06b60e24e3aa5dfaa0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:29:01 +0000 Subject: [PATCH 031/181] Bump debian from `5724d31` to `5484adc` in /src/main/docker Bumps debian from `5724d31` to `5484adc`. --- updated-dependencies: - dependency-name: debian dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index b6057471e6..46f6ab0c3e 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -16,7 +16,7 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. FROM eclipse-temurin:21.0.5_11-jre-jammy@sha256:5f8358c9d5615c18e95728e8b8528bda7ff40a7a5da2ac9a35b7a01f5d9b231a AS jre-build -FROM debian:stable-slim@sha256:5724d31208341cef9af6ae2be86be9cda6a87271f362a03481a522c9c19d401b +FROM debian:stable-slim@sha256:5484adc33b4c352c5a9f4c4ae295fc49aed1cb54a7a0712a1b29674fb6f4f10f # Arguments that can be passed at build time # Directory names must end with / to avoid errors when ADDing and COPYing From 39734cfc7eda9cdb0fb5def8c5c349ca644888b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:58:44 +0000 Subject: [PATCH 032/181] Bump org.wiremock:wiremock from 3.12.0 to 3.12.1 Bumps [org.wiremock:wiremock](https://github.com/wiremock/wiremock) from 3.12.0 to 3.12.1. - [Release notes](https://github.com/wiremock/wiremock/releases) - [Commits](https://github.com/wiremock/wiremock/compare/3.12.0...3.12.1) --- updated-dependencies: - dependency-name: org.wiremock:wiremock dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dbefafee4b..f55f532e96 100644 --- a/pom.xml +++ b/pom.xml @@ -109,7 +109,7 @@ 0.2.2 8.5.17 2.0.0 - 3.12.0 + 3.12.1 1.5.0 0.5.3.2 3.2.2 From f26f12fc99da2dd9f1ec41ab1e621209b7a280a3 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:29:35 -0600 Subject: [PATCH 033/181] refactor: implement adding/removing users to/from roles (#13) * refactor: implement adding/removing users to/from roles Signed-off-by: Jonathan Howard * refactor: add RoleDao methods for removing role from user Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard --- .../persistence/RoleQueryManager.java | 78 +++++++++++++++++-- .../persistence/jdbi/RoleDao.java | 38 ++++++++- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 9681746ad2..4fae8ce2f8 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,20 +18,30 @@ */ package org.dependencytrack.persistence; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.jdo.PersistenceManager; import javax.jdo.Query; +import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; +import org.dependencytrack.persistence.jdbi.RoleDao; +import org.jdbi.v3.core.Handle; import alpine.common.logging.Logger; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; + final class RoleQueryManager extends QueryManager implements IQueryManager { private static final Logger LOGGER = Logger.getLogger(RoleQueryManager.class); @@ -98,14 +108,72 @@ public Role updateRole(Role transientRole) { @Override public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { - // TODO: Implement addRoleToUser - return true; + Query query = pm.newQuery(MappedRole.class) + .filter("project.id == :projectId && role.id == :roleId") + .setNamedParameters(Map.of( + "roleId", role.getId(), + "projectId", project.getId())); + + try { + query.getFetchPlan().setGroup(MappedRole.FetchGroup.ALL.name()); + MappedRole result = query.executeUnique(); + + if (result == null) { + LOGGER.info("Creating role mapping for project: %s / role: %s" + .formatted(project.getName(), role.getName())); + + result = new MappedRole(); + result.setProject(project); + result.setRole(role); + } + + result.setLdapUsers(result.getLdapUsers() != null ? result.getLdapUsers() : new ArrayList<>()); + result.setManagedUsers(result.getManagedUsers() != null ? result.getManagedUsers() : new ArrayList<>()); + result.setOidcUsers(result.getOidcUsers() != null ? result.getOidcUsers() : new ArrayList<>()); + + final MappedRole mappedRole = result; + + boolean modified = switch (user) { + case LdapUser ldapUser when !mappedRole.getLdapUsers().contains(ldapUser) -> { + mappedRole.addLdapUsers(ldapUser); + yield true; + } + case ManagedUser managedUser when !mappedRole.getManagedUsers().contains(managedUser) -> { + mappedRole.addManagedUsers(managedUser); + yield true; + } + case OidcUser oidcUser when !mappedRole.getOidcUsers().contains(oidcUser) -> { + mappedRole.addOidcUsers(oidcUser); + yield true; + } + default -> false; + }; + + if (modified) + persist(mappedRole); + + return modified; + } finally { + query.closeAll(); + } } @Override - public boolean removeRoleFromUser(UserPrincipal principal, Role role, Project project) { - // TODO: Implement removeRoleFromUser - return true; + public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { + try (final Handle jdbiHandle = openJdbiHandle()) { + int count = switch (user) { + case LdapUser ldapUser -> jdbiHandle.attach(RoleDao.class) + .removeRoleFromLdapUser(ldapUser.getId(), project.getId(), role.getId()); + case ManagedUser managedUser -> jdbiHandle.attach(RoleDao.class) + .removeRoleFromManagedUser(managedUser.getId(), project.getId(), role.getId()); + case OidcUser oidcUser -> jdbiHandle.attach(RoleDao.class) + .removeRoleFromOidcUser(oidcUser.getId(), project.getId(), role.getId()); + default -> 0; + }; + + return count == 1; + } + } } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index c64a25de4e..0ce3d1ac77 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -26,11 +26,47 @@ */ public interface RoleDao { - @SqlUpdate(""" + @SqlUpdate(/* language=sql */ """ DELETE FROM "ROLE" WHERE "ID" = :roleId """) int deleteRole(@Bind final long roleId); + @SqlUpdate(/* language=sql */ """ + DELETE + FROM "LDAPUSERS_PROJECTS_ROLES" + WHERE "LDAPUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) + int removeRoleFromLdapUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + + @SqlUpdate(/* language=sql */ """ + DELETE + FROM "MANAGEDUSERS_PROJECTS_ROLES" + WHERE "MANAGEDUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) + int removeRoleFromManagedUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + + @SqlUpdate(/* language=sql */ """ + DELETE + FROM "OIDCUSERS_PROJECTS_ROLES" + WHERE "OIDCUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) + int removeRoleFromOidcUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + } From 3e1f95b88681718d7d6896c637f89bd6facc4957 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Tue, 4 Mar 2025 16:56:03 -0700 Subject: [PATCH 034/181] refactor: remove description field Signed-off-by: Allen Shearin --- .../java/org/dependencytrack/model/Role.java | 18 ------------------ .../persistence/DefaultObjectGenerator.java | 8 ++++---- .../persistence/QueryManager.java | 4 ++-- .../persistence/RoleQueryManager.java | 4 +--- .../resources/v1/RoleResource.java | 2 +- .../resources/migration/changelog-v5.6.0.xml | 4 ---- 6 files changed, 8 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Role.java b/src/main/java/org/dependencytrack/model/Role.java index 47f8516575..f278a9ebd7 100644 --- a/src/main/java/org/dependencytrack/model/Role.java +++ b/src/main/java/org/dependencytrack/model/Role.java @@ -59,7 +59,6 @@ @PersistenceCapable @FetchGroup(name = "ALL", members = { @Persistent(name = "name"), - @Persistent(name = "description"), @Persistent(name = "permissions"), @Persistent(name = "uuid"), }) @@ -90,14 +89,6 @@ public enum FetchGroup { message = "The name may only contain printable characters") private String name; - @Persistent - @Column(name = "DESCRIPTION", jdbcType = "VARCHAR") - @Size(max = 255) - @JsonDeserialize(using = TrimmedStringDeserializer.class) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, - message = "The description may only contain printable characters") - private String description; - @Persistent(table = "ROLES_PERMISSIONS", defaultFetchGroup = "true") @Unique(name = "ROLES_PERMISSIONS_IDX") @Join(column = "ROLE_ID") @@ -127,14 +118,6 @@ public void setName(String name) { this.name = name; } - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - public List getPermissions() { return permissions; } @@ -173,7 +156,6 @@ public String toString() { getClass().getSimpleName(), id, name, - description != null ? description : "", permissionStrings); } } diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index b8c02a22ab..7972f870ec 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -308,13 +308,13 @@ private void loadDefaultRoles() { LOGGER.info("Adding default roles to datastore"); LOGGER.debug("Creating role: Project Admin"); - final Role projectAdmin = qm.createRole("Project Admin", null, getProjectAdminPermissions(fullList)); + final Role projectAdmin = qm.createRole("Project Admin", getProjectAdminPermissions(fullList)); LOGGER.debug("Creating role: Project Auditor"); - final Role projectAuditor = qm.createRole("Project Auditor", null, getProjectAuditorPermissions(fullList)); + final Role projectAuditor = qm.createRole("Project Auditor", getProjectAuditorPermissions(fullList)); LOGGER.debug("Creating role: Project Editor"); - final Role projectEditor = qm.createRole("Project Editor", null, getProjectEditorPermissions(fullList)); + final Role projectEditor = qm.createRole("Project Editor", getProjectEditorPermissions(fullList)); LOGGER.debug("Creating role: Project Viewer"); - final Role projectViewer = qm.createRole("Project Viewer", null, getProjectViewerPermissions(fullList)); + final Role projectViewer = qm.createRole("Project Viewer", getProjectViewerPermissions(fullList)); qm.persist(projectAdmin); qm.persist(projectAuditor); diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 29ad2e03c9..e92a3317c5 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -923,8 +923,8 @@ public void deletePolicyCondition(PolicyCondition policyCondition) { getPolicyQueryManager().deletePolicyCondition(policyCondition); } - public Role createRole(final String name, final String description, final List permissions) { - return getRoleQueryManager().createRole(name, description, permissions); + public Role createRole(final String name, final List permissions) { + return getRoleQueryManager().createRole(name, permissions); } public List getRoles() { diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 4fae8ce2f8..918cc1df9d 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -55,10 +55,9 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { } @Override - public Role createRole(final String name, final String description, final List permissions) { + public Role createRole(final String name, final List permissions) { Role role = new Role(); role.setName(name); - role.setDescription(description); role.setPermissions(permissions); return persist(role); @@ -101,7 +100,6 @@ public Role updateRole(Role transientRole) { return null; role.setName(transientRole.getName()); - role.setDescription(transientRole.getDescription()); return persist(role); } diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index c1445ef64c..595bb08a2f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -141,7 +141,7 @@ public Response createRole(Role jsonRole) { failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); try (QueryManager qm = new QueryManager()) { - final Role role = qm.createRole(jsonRole.getName(), jsonRole.getDescription(), jsonRole.getPermissions()); + final Role role = qm.createRole(jsonRole.getName(), jsonRole.getPermissions()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role created: " + role.getName()); return Response.status(Response.Status.CREATED).entity(role).build(); diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index 58a35e1f17..765f563856 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -675,10 +675,6 @@ validateNullable="true" validateUnique="true" /> - - - - Date: Mon, 10 Mar 2025 14:14:48 -0600 Subject: [PATCH 035/181] remove tostring bug, update role serialVersionID Signed-off-by: Allen Shearin --- src/main/java/org/dependencytrack/model/Role.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Role.java b/src/main/java/org/dependencytrack/model/Role.java index f278a9ebd7..6adac75ba5 100644 --- a/src/main/java/org/dependencytrack/model/Role.java +++ b/src/main/java/org/dependencytrack/model/Role.java @@ -65,7 +65,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class Role implements Serializable { - private static final long serialVersionUID = -7592438796591673355L; + private static final long serialVersionUID = -427858073810766917L; /** * Defines JDO fetch groups for this class. @@ -152,7 +152,7 @@ public String toString() { .map(permission -> permission.getName()) .toList(); - return "%s{id=%d, name='%s', description='%s', permissions=%s}".formatted( + return "%s{id=%d, name='%s', permissions=%s}".formatted( getClass().getSimpleName(), id, name, From cafffc86827fd2c9a8baa25c7ea67689db78a7ea Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 12 Mar 2025 16:59:10 -0600 Subject: [PATCH 036/181] add getunassignedprojects to role dao.java, getunassignedprojects to ac resoucre.java Signed-off-by: Allen Shearin --- .../persistence/jdbi/RoleDao.java | 84 ++++++++++++++---- .../jdbi/mapping/ProjectRowMapper.java | 88 +++++++++++++++++++ .../resources/v1/AccessControlResource.java | 55 ++++++++++++ 3 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 0ce3d1ac77..92c5dc8031 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -18,7 +18,13 @@ */ package org.dependencytrack.persistence.jdbi; +import java.util.List; + +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.jdbi.mapping.ProjectRowMapper; +import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; /** @@ -34,27 +40,27 @@ public interface RoleDao { int deleteRole(@Bind final long roleId); @SqlUpdate(/* language=sql */ """ - DELETE - FROM "LDAPUSERS_PROJECTS_ROLES" - WHERE "LDAPUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) + DELETE + FROM "LDAPUSERS_PROJECTS_ROLES" + WHERE "LDAPUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) int removeRoleFromLdapUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); @SqlUpdate(/* language=sql */ """ - DELETE - FROM "MANAGEDUSERS_PROJECTS_ROLES" - WHERE "MANAGEDUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) + DELETE + FROM "MANAGEDUSERS_PROJECTS_ROLES" + WHERE "MANAGEDUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) int removeRoleFromManagedUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); @SqlUpdate(/* language=sql */ """ @@ -69,4 +75,46 @@ public interface RoleDao { """) int removeRoleFromOidcUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + @SqlQuery(/* language=sql */ """ + SELECT * + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "LDAPUSERS_PROJECTS_ROLES" + ON "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "LDAPUSER" + ON "LDAPUSER"."ID" = "LDAPUSERS_PROJECTS_ROLES"."LDAPUSER_ID" + WHERE "LDAPUSER"."USERNAME" != :username OR "LDAPUSER"."USERNAME" IS NULL + """) + @RegisterRowMapper(ProjectRowMapper.class) + List getLdapUserUnassignedProjects(@Bind final String username); + + @SqlQuery(/* language=sql */ """ + SELECT * + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "MANAGEDUSERS_PROJECTS_ROLES" + ON "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "MANAGEDUSER" + ON "MANAGEDUSER"."ID" = "MANAGEDUSERS_PROJECTS_ROLES"."MANAGEDUSER_ID" + WHERE "MANAGEDUSER"."USERNAME" != :username OR "MANAGEDUSER"."USERNAME" IS NULL + """) + @RegisterRowMapper(ProjectRowMapper.class) + List getManagedUserUnassignedProjects(@Bind final String username); + + @SqlQuery(/* language=sql */ """ + SELECT * + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "OIDCUSERS_PROJECTS_ROLES" + ON "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "OIDCUSER" + ON "OIDCUSER"."ID" = "OIDCUSERS_PROJECTS_ROLES"."OIDCUSER_ID" + WHERE "OIDCUSER"."USERNAME" != :username OR "OIDCUSER"."USERNAME" IS NULL + """) + @RegisterRowMapper(ProjectRowMapper.class) + List getOidcUserUnassignedProjects(@Bind final String username); + } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java new file mode 100644 index 0000000000..ba435afed2 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java @@ -0,0 +1,88 @@ +/* + * 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.jdbi.mapping; + +import org.cyclonedx.model.ExternalReference; +import org.dependencytrack.model.Classifier; +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.converter.OrganizationalContactsJsonConverter; +import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; + +import com.fasterxml.jackson.core.type.TypeReference; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.deserializeJson; +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; + +public class ProjectRowMapper implements RowMapper { + + private static final TypeReference> EXTERNAL_REFS_TYPE_REF = new TypeReference<>() { + }; + + @Override + public Project map(final ResultSet rs, final StatementContext ctx) throws SQLException { + final var project = new Project(); + + maybeSet(rs, "ID", ResultSet::getLong, project::setId); + maybeSet(rs, "CLASSIFIER", ResultSet::getString, Classifier::valueOf); + maybeSet(rs, "CPE", ResultSet::getString, project::setCpe); + maybeSet(rs, "DESCRIPTION", ResultSet::getString, project::setDescription); + maybeSet(rs, "DIRECT_DEPENDENCIES", ResultSet::getString, project::setDirectDependencies); + deserializeJson(rs, "EXTERNAL_REFERENCES", EXTERNAL_REFS_TYPE_REF); + maybeSet(rs, "GROUP", ResultSet::getString, project::setGroup); + maybeSet(rs, "LAST_BOM_IMPORTED", ResultSet::getDate, project::setLastBomImport); + maybeSet(rs, "LAST_BOM_IMPORTED_FORMAT", ResultSet::getString, project::setLastBomImportFormat); + maybeSet(rs, "LAST_RISKSCORE", ResultSet::getDouble, project::setLastInheritedRiskScore); + maybeSet(rs, "NAME", ResultSet::getString, project::setName); + maybeSet(rs, "PARENT_PROJECT_ID", ResultSet::getLong, value -> { + var parent = new Project(); + parent.setId(value); + project.setParent(parent); + }); + maybeSet(rs, "PUBLISHER", ResultSet::getString, project::setPublisher); + maybeSet(rs, "PURL", ResultSet::getString, project::setPurl); + maybeSet(rs, "SWIDTAGID", ResultSet::getString, project::setSwidTagId); + maybeSet(rs, "UUID", ResultSet::getString, value -> { + var uuid = UUID.fromString(value); + project.setUuid(uuid); + }); + maybeSet(rs, "VERSION", ResultSet::getString, project::setVersion); + maybeSet(rs, "SUPPLIER", ResultSet::getString, value -> { + var converter = new OrganizationalEntityJsonConverter(); + project.setSupplier(converter.convertToAttribute(value)); + }); + maybeSet(rs, "MANUFACTURER", ResultSet::getString, value -> { + var converter = new OrganizationalEntityJsonConverter(); + project.setManufacturer(converter.convertToAttribute(value)); + }); + maybeSet(rs, "AUTHORS", ResultSet::getString, values -> { + var converter = new OrganizationalContactsJsonConverter(); + project.setAuthors(converter.convertToAttribute(values)); + }); + + return project; + } + +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 822f03bf50..34d4e74105 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -19,7 +19,11 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Team; +import alpine.model.UserPrincipal; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -48,10 +52,15 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.RoleDao; import org.dependencytrack.resources.v1.openapi.PaginatedApi; import org.dependencytrack.resources.v1.vo.AclMappingRequest; +import org.jdbi.v3.core.Handle; + +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -106,6 +115,52 @@ public Response retrieveProjects(@Parameter(description = "The UUID of the team } } + @GET + @Path("/user/{username}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns the projects accessible by the specified user", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_READ

        ") + @PaginatedApi + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Projects accessible by the specified user", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", + schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "No unassigned projects for specified user."), + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) + public Response retrieveUserProjects( + @Parameter(description = "The username to retrieve projects for", + required = true) @PathParam("username") String username, + @Parameter(description = "Optionally excludes inactive projects from being returned", + required = false) @QueryParam("excludeInactive") boolean excludeInactive, + @Parameter(description = "Optionally excludes children projects from being returned", + required = false) @QueryParam("onlyRoot") boolean onlyRoot) { + + try (QueryManager qm = new QueryManager()) { + UserPrincipal principal = qm.getUserPrincipal(username); + + try (final Handle jdbiHandle = openJdbiHandle()) { + var dao = jdbiHandle.attach(RoleDao.class); + List projects = switch (principal) { + case LdapUser user -> dao.getLdapUserUnassignedProjects(user.getUsername()); + case ManagedUser user -> dao.getManagedUserUnassignedProjects(user.getUsername()); + case OidcUser user -> dao.getOidcUserUnassignedProjects(user.getUsername()); + default -> Collections.emptyList(); + }; + + if (projects.isEmpty()) + return Response.status(Response.Status.NOT_FOUND).entity("No unassigned projects for specified user.").build(); + + return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); + } + } + } + @PUT @Path("/mapping") @Produces(MediaType.APPLICATION_JSON) From 9c1d6ee4f6278c8c68ef2c05bc38a369fb2d29f8 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Fri, 14 Mar 2025 16:24:51 -0600 Subject: [PATCH 037/181] addRoleToUser working Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 46 +++++++ .../persistence/RoleQueryManager.java | 81 ++++-------- .../persistence/jdbi/RoleDao.java | 124 +++++++++++------- .../resources/v1/AccessControlResource.java | 6 +- .../resources/v1/RoleProjectRequest.java | 45 +++++++ .../resources/v1/UserResource.java | 24 ++-- 6 files changed, 204 insertions(+), 122 deletions(-) create mode 100644 src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index e92a3317c5..61b611100f 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -24,6 +24,9 @@ import alpine.model.ApiKey; import alpine.model.ConfigProperty; import alpine.model.IConfigProperty.PropertyType; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.Team; import alpine.model.UserPrincipal; @@ -65,6 +68,7 @@ import org.dependencytrack.model.IntegrityMetaComponent; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; +import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.Policy; @@ -127,6 +131,7 @@ import java.util.UUID; import java.util.concurrent.Callable; import java.util.function.Predicate; +import java.util.function.Supplier; import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED; import static org.dependencytrack.proto.vulnanalysis.v1.ScanStatus.SCAN_STATUS_FAILED; @@ -504,6 +509,38 @@ public QueryManager withL2CacheDisabled() { return this; } + /** + * Get the IDs of the {@link MappedRole}s a given {@link Principal} is a member of. + * + * @return A {@link Set} of {@link MappedRole} IDs + */ + protected Set getRoleIds(final Principal principal, final Project project) { + String usersField; + + final MappedRole mappedRole = new MappedRole(); + Supplier roleIds = mappedRole.getRole()::getId; + + switch (principal) { + case LdapUser ldapUser -> usersField = "ldapUsers"; + case ManagedUser managedUser -> usersField = "managedUsers"; + case OidcUser oidcUser -> usersField = "oidcUsers"; + default -> { + return Collections.emptySet(); + } + }; + + Query query = pm.newQuery(MappedRole.class) + .filter("project.id == :projectId && %s.contains(:principal)".formatted(usersField)) + .setNamedParameters(Map.ofEntries( + Map.entry("principal", principal), + Map.entry("projectId", project.getId()))); + + return Set.of(executeAndCloseList(query).stream() + .map(MappedRole::getRole) + .map(Role::getId) + .toArray(Long[]::new)); + } + /** * Get the IDs of the {@link Team}s a given {@link Principal} is a member of. * @@ -935,10 +972,19 @@ public Role getRole(String uuid) { return getRoleQueryManager().getRole(null); } + public List getUserRoles(UserPrincipal user) { + return getRoleQueryManager().getUserRoles(user); + } + public Role updateRole(Role transientRole) { return getRoleQueryManager().updateRole(transientRole); } + public List getUnassignedProjects(UserPrincipal user) { + // TODO Auto-generated method stub + return getRoleQueryManager().getUnassignedProjects(user); + } + public Vulnerability createVulnerability(Vulnerability vulnerability, boolean commitIndex) { return getVulnerabilityQueryManager().createVulnerability(vulnerability, commitIndex); } diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 918cc1df9d..ddaf9397f2 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.persistence; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -29,6 +28,7 @@ import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; +import org.dependencytrack.persistence.jdbi.JdbiFactory; import org.dependencytrack.persistence.jdbi.RoleDao; import org.jdbi.v3.core.Handle; @@ -72,6 +72,28 @@ public List getRoles() { return query.executeList(); } + @Override + public List getUserRoles(UserPrincipal user) { + String usersField; + + switch (user) { + case LdapUser ldapUser -> usersField = "ldapUsers"; + case ManagedUser managedUser -> usersField = "managedUsers"; + case OidcUser oidcUser -> usersField = "oidcUsers"; + default -> { + return null; + } + } + ; + + Query query = pm.newQuery(MappedRole.class) + .filter("%s.contains(:user)".formatted(usersField)) + .setNamedParameters(Map.ofEntries( + Map.entry("user", user))); + + return executeAndCloseList(query); + } + @Override public Role getRole(String uuid) { final Query query = pm.newQuery(Role.class, "uuid == :uuid"); @@ -83,11 +105,6 @@ public List getUnassignedProjects(final String username) { return getUnassignedProjects(getUserPrincipal(username)); } - public List getUnassignedProjects(final UserPrincipal principal) { - // TODO: Implement getUnassignedProjects - return Collections.emptyList(); - } - public List getUnassignedRolePermissions(final Role role) { // TODO: Implement getUnassignedRolePermissions return Collections.emptyList(); @@ -106,54 +123,12 @@ public Role updateRole(Role transientRole) { @Override public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { - Query query = pm.newQuery(MappedRole.class) - .filter("project.id == :projectId && role.id == :roleId") - .setNamedParameters(Map.of( - "roleId", role.getId(), - "projectId", project.getId())); - - try { - query.getFetchPlan().setGroup(MappedRole.FetchGroup.ALL.name()); - MappedRole result = query.executeUnique(); - - if (result == null) { - LOGGER.info("Creating role mapping for project: %s / role: %s" - .formatted(project.getName(), role.getName())); - - result = new MappedRole(); - result.setProject(project); - result.setRole(role); - } + return JdbiFactory.withJdbiHandle(handle -> { + RoleDao dao = handle.attach(RoleDao.class); + MappedRole mappedRole = dao.addProjectAccessRole(project.getId(), role.getId()); - result.setLdapUsers(result.getLdapUsers() != null ? result.getLdapUsers() : new ArrayList<>()); - result.setManagedUsers(result.getManagedUsers() != null ? result.getManagedUsers() : new ArrayList<>()); - result.setOidcUsers(result.getOidcUsers() != null ? result.getOidcUsers() : new ArrayList<>()); - - final MappedRole mappedRole = result; - - boolean modified = switch (user) { - case LdapUser ldapUser when !mappedRole.getLdapUsers().contains(ldapUser) -> { - mappedRole.addLdapUsers(ldapUser); - yield true; - } - case ManagedUser managedUser when !mappedRole.getManagedUsers().contains(managedUser) -> { - mappedRole.addManagedUsers(managedUser); - yield true; - } - case OidcUser oidcUser when !mappedRole.getOidcUsers().contains(oidcUser) -> { - mappedRole.addOidcUsers(oidcUser); - yield true; - } - default -> false; - }; - - if (modified) - persist(mappedRole); - - return modified; - } finally { - query.closeAll(); - } + return dao.addRoleToUser(user, mappedRole.getId()) == 1; + }); } @Override diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 92c5dc8031..ab8f20096d 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -20,26 +20,96 @@ import java.util.List; +import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; -import org.dependencytrack.persistence.jdbi.mapping.ProjectRowMapper; -import org.jdbi.v3.sqlobject.config.RegisterRowMapper; +import org.jdbi.v3.sqlobject.config.RegisterFieldMapper; import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.customizer.Define; +import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings; +import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import alpine.model.UserPrincipal; + /** * @since 5.6.0 */ public interface RoleDao { @SqlUpdate(/* language=sql */ """ + INSERT INTO "PROJECT_ACCESS_ROLES" ("PROJECT_ID", "ROLE_ID") + VALUES (:projectId, :roleId) + ON CONFLICT DO NOTHING + """) + @GetGeneratedKeys + @RegisterFieldMapper(MappedRole.class) + MappedRole addProjectAccessRole(@Bind final long projectId, @Bind final long roleId); + + @SqlUpdate(/* language=sql */ """ + <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> + INSERT INTO "${user.getClass().getSimpleName()?upper_case}S_PROJECTS_ROLES" + ("${user.getClass().getSimpleName()?upper_case}_ID", "PROJECT_ACCESS_ROLE_ID") + VALUES + (${user.getId()}, :projectAccessRoleId) + ON CONFLICT DO NOTHING + """) + @DefineNamedBindings + int addRoleToUser(@Define T user, @Bind final long projectAccessRoleId); + + @SqlUpdate(/* language=sql */""" DELETE FROM "ROLE" WHERE "ID" = :roleId """) int deleteRole(@Bind final long roleId); - @SqlUpdate(/* language=sql */ """ + @SqlQuery(/* language=sql */""" + SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "LDAPUSERS_PROJECTS_ROLES" + ON "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "LDAPUSER" + ON "LDAPUSER"."ID" = "LDAPUSERS_PROJECTS_ROLES"."LDAPUSER_ID" + WHERE "LDAPUSER"."USERNAME" != :username + OR "LDAPUSER"."USERNAME" IS NULL + """) + @RegisterFieldMapper(Project.class) + List getLdapUserUnassignedProjects(@Bind final String username); + + @SqlQuery(/* language=sql */""" + SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "MANAGEDUSERS_PROJECTS_ROLES" + ON "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "MANAGEDUSER" + ON "MANAGEDUSER"."ID" = "MANAGEDUSERS_PROJECTS_ROLES"."MANAGEDUSER_ID" + WHERE "MANAGEDUSER"."USERNAME" != :username + OR "MANAGEDUSER"."USERNAME" IS NULL + """) + @RegisterFieldMapper(Project.class) + List getManagedUserUnassignedProjects(@Bind final String username); + + @SqlQuery(/* language=sql */""" + SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "OIDCUSERS_PROJECTS_ROLES" + ON "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "OIDCUSER" + ON "OIDCUSER"."ID" = "OIDCUSERS_PROJECTS_ROLES"."OIDCUSER_ID" + WHERE "OIDCUSER"."USERNAME" != :username + OR "OIDCUSER"."USERNAME" IS NULL + """) + @RegisterFieldMapper(Project.class) + List getOidcUserUnassignedProjects(@Bind final String username); + + @SqlUpdate(/* language=sql */""" DELETE FROM "LDAPUSERS_PROJECTS_ROLES" WHERE "LDAPUSER_ID" = :userId @@ -51,7 +121,7 @@ public interface RoleDao { """) int removeRoleFromLdapUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - @SqlUpdate(/* language=sql */ """ + @SqlUpdate(/* language=sql */""" DELETE FROM "MANAGEDUSERS_PROJECTS_ROLES" WHERE "MANAGEDUSER_ID" = :userId @@ -63,7 +133,7 @@ public interface RoleDao { """) int removeRoleFromManagedUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - @SqlUpdate(/* language=sql */ """ + @SqlUpdate(/* language=sql */""" DELETE FROM "OIDCUSERS_PROJECTS_ROLES" WHERE "OIDCUSER_ID" = :userId @@ -75,46 +145,4 @@ public interface RoleDao { """) int removeRoleFromOidcUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - @SqlQuery(/* language=sql */ """ - SELECT * - FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "LDAPUSERS_PROJECTS_ROLES" - ON "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "LDAPUSER" - ON "LDAPUSER"."ID" = "LDAPUSERS_PROJECTS_ROLES"."LDAPUSER_ID" - WHERE "LDAPUSER"."USERNAME" != :username OR "LDAPUSER"."USERNAME" IS NULL - """) - @RegisterRowMapper(ProjectRowMapper.class) - List getLdapUserUnassignedProjects(@Bind final String username); - - @SqlQuery(/* language=sql */ """ - SELECT * - FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "MANAGEDUSERS_PROJECTS_ROLES" - ON "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "MANAGEDUSER" - ON "MANAGEDUSER"."ID" = "MANAGEDUSERS_PROJECTS_ROLES"."MANAGEDUSER_ID" - WHERE "MANAGEDUSER"."USERNAME" != :username OR "MANAGEDUSER"."USERNAME" IS NULL - """) - @RegisterRowMapper(ProjectRowMapper.class) - List getManagedUserUnassignedProjects(@Bind final String username); - - @SqlQuery(/* language=sql */ """ - SELECT * - FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "OIDCUSERS_PROJECTS_ROLES" - ON "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "OIDCUSER" - ON "OIDCUSER"."ID" = "OIDCUSERS_PROJECTS_ROLES"."OIDCUSER_ID" - WHERE "OIDCUSER"."USERNAME" != :username OR "OIDCUSER"."USERNAME" IS NULL - """) - @RegisterRowMapper(ProjectRowMapper.class) - List getOidcUserUnassignedProjects(@Bind final String username); - -} +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 34d4e74105..950653c79d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -135,11 +135,7 @@ public Response retrieveProjects(@Parameter(description = "The UUID of the team @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) public Response retrieveUserProjects( @Parameter(description = "The username to retrieve projects for", - required = true) @PathParam("username") String username, - @Parameter(description = "Optionally excludes inactive projects from being returned", - required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @Parameter(description = "Optionally excludes children projects from being returned", - required = false) @QueryParam("onlyRoot") boolean onlyRoot) { + required = true) @PathParam("username") String username) { try (QueryManager qm = new QueryManager()) { UserPrincipal principal = qm.getUserPrincipal(username); diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java b/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java new file mode 100644 index 0000000000..ccc744c5a7 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java @@ -0,0 +1,45 @@ +/* + * 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.resources.v1; + +import org.dependencytrack.model.validation.ValidUuid; + +public class RoleProjectRequest { + @ValidUuid + private String roleUUID; + + @ValidUuid + private String projectUUID; + + public String getRoleUUID() { + return roleUUID; + } + + public void setRoleUUID(String roleUUID) { + this.roleUUID = roleUUID; + } + + public String getProjectUUID() { + return projectUUID; + } + + public void setProjectUUID(String projectUUID) { + this.projectUUID = projectUUID; + } +} diff --git a/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 347bf46a53..352c627fa2 100644 --- a/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -801,21 +801,13 @@ private UserSubject buildUserSubject(final String username, final String email) @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) public Response addRoleToUser( @Parameter(description = "A valid username", required = true) - @PathParam("username") - String username, - - @Parameter(description = "The UUID of the role to associate username with", required = true) - IdentifiableObject identifiableObject, - - @Parameter(description = "The name of the project", required = true) - @QueryParam("projectName") - String projectName, - - @Parameter(description = "The version of the project") - @QueryParam("projectVersion") - String projectVersion) { + @PathParam("username") String username, + @Parameter(description = "Role and project information", required = true) + RoleProjectRequest roleProjectRequest) { try (QueryManager qm = new QueryManager()) { - final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); + LOGGER.info("received Project info: " + roleProjectRequest.getProjectUUID()); + LOGGER.info("received Role info: " + roleProjectRequest.getRoleUUID()); + final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.getRoleUUID()); if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); @@ -823,7 +815,7 @@ public Response addRoleToUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - Project project = qm.getProject(projectName, projectVersion); + Project project = qm.getProject(roleProjectRequest.getProjectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -834,7 +826,7 @@ public Response addRoleToUser( principal = qm.getObjectById(principal.getClass(), principal.getId()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s" - .formatted(principal.getName(), role.getName(), projectName)); + .formatted(principal.getName(), role.getName(), project.getName())); return Response.ok(principal).build(); } From 237d9a068f8d21c5ed314082f6c0b9f82fb31ab9 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:34:16 -0500 Subject: [PATCH 038/181] refactor: flatten user role join tables (#14) Signed-off-by: Jonathan Howard --- .../org/dependencytrack/model/MappedRole.java | 200 --------------- .../dependencytrack/model/ProjectRole.java | 234 ++++++++++++++++++ .../persistence/ProjectQueryManager.java | 28 +-- .../persistence/QueryManager.java | 80 +++--- .../persistence/RoleQueryManager.java | 79 +++--- .../persistence/jdbi/RoleDao.java | 146 ++++------- .../jdbi/mapping/ProjectRoleRowMapper.java | 65 +++++ .../resources/v1/AccessControlResource.java | 13 +- src/main/resources/META-INF/persistence.xml | 10 +- .../resources/migration/changelog-v5.6.0.xml | 115 ++++----- .../function_has-project-access.sql | 49 +++- 11 files changed, 533 insertions(+), 486 deletions(-) delete mode 100644 src/main/java/org/dependencytrack/model/MappedRole.java create mode 100644 src/main/java/org/dependencytrack/model/ProjectRole.java create mode 100644 src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java diff --git a/src/main/java/org/dependencytrack/model/MappedRole.java b/src/main/java/org/dependencytrack/model/MappedRole.java deleted file mode 100644 index 6480e4f9a2..0000000000 --- a/src/main/java/org/dependencytrack/model/MappedRole.java +++ /dev/null @@ -1,200 +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.model; - -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.jdo.annotations.Column; -import javax.jdo.annotations.Element; -import javax.jdo.annotations.Extension; -import javax.jdo.annotations.FetchGroup; -import javax.jdo.annotations.IdGeneratorStrategy; -import javax.jdo.annotations.Join; -import javax.jdo.annotations.Order; -import javax.jdo.annotations.PersistenceCapable; -import javax.jdo.annotations.Persistent; -import javax.jdo.annotations.PrimaryKey; -import javax.jdo.annotations.Unique; - -/** - * Model for associating a role on a given project with users. - * - * @author Allen Shearin - * @since 5.6.0 - */ -@PersistenceCapable(table = "PROJECT_ACCESS_ROLES") -@Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "LDAPUSERS_PROJECTS_ROLES", - members = { "ldapUsers", "project", "role" }, - deferred = "true") -@Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "MANAGEDUSERS_PROJECTS_ROLES", - members = { "managedUsers", "project", "role" }, - deferred = "true") -@Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "OIDCUSERS_PROJECTS_ROLES", - members = { "oidcUsers", "project", "role" }, - deferred = "true") -@FetchGroup(name = "ALL", members = { - @Persistent(name = "role"), - @Persistent(name = "project"), - @Persistent(name = "ldapUsers"), - @Persistent(name = "managedUsers"), - @Persistent(name = "oidcUsers") -}) -@JsonInclude(JsonInclude.Include.NON_NULL) -public class MappedRole implements Serializable { - - private static final long serialVersionUID = 1982348710987098723L; - - /** - * Defines JDO fetch groups for this class. - */ - public enum FetchGroup { - ALL - } - - @PrimaryKey - @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) - @JsonIgnore - private long id; - - @Persistent - @Column(name = "ROLE_ID", allowsNull = "false") - @JsonIgnore - private Role role; - - @Persistent - @Column(name = "PROJECT_ID", allowsNull = "false") - @JsonIgnore - private Project project; - - @Persistent(table = "LDAPUSERS_PROJECTS_ROLES", defaultFetchGroup = "true") - @Join(column = "PROJECT_ACCESS_ROLE_ID") - @Element(column = "LDAPUSER_ID") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List ldapUsers; - - @Persistent(table = "MANAGEDUSERS_PROJECTS_ROLES") - @Join(column = "PROJECT_ACCESS_ROLE_ID") - @Element(column = "MANAGEDUSER_ID") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List managedUsers; - - @Persistent(table = "OIDCUSERS_PROJECTS_ROLES") - @Join(column = "PROJECT_ACCESS_ROLE_ID") - @Element(column = "OIDCUSER_ID") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List oidcUsers; - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public Role getRole() { - return role; - } - - public void setRole(Role role) { - this.role = role; - } - - public Project getProject() { - return project; - } - - public void setProject(Project project) { - this.project = project; - } - - public List getLdapUsers() { - return ldapUsers; - } - - public void setLdapUsers(List ldapUsers) { - this.ldapUsers = ldapUsers; - } - - public void addLdapUsers(LdapUser... ldapUsers) { - if (this.ldapUsers == null) { - this.ldapUsers = new ArrayList<>(Arrays.asList(ldapUsers)); - - return; - } - - for (var user : ldapUsers) - if (!this.ldapUsers.contains(user)) - this.ldapUsers.add(user); - } - - public List getManagedUsers() { - return managedUsers; - } - - public void setManagedUsers(List managedUsers) { - this.managedUsers = managedUsers; - } - - public void addManagedUsers(ManagedUser... managedUsers) { - if (this.managedUsers == null) { - this.managedUsers = new ArrayList<>(Arrays.asList(managedUsers)); - - return; - } - - for (var user : managedUsers) - if (!this.managedUsers.contains(user)) - this.managedUsers.add(user); - } - - public List getOidcUsers() { - return oidcUsers; - } - - public void setOidcUsers(List oidcUsers) { - this.oidcUsers = oidcUsers; - } - - public void addOidcUsers(OidcUser... oidcUsers) { - if (this.oidcUsers == null) { - this.oidcUsers = new ArrayList<>(Arrays.asList(oidcUsers)); - - return; - } - - for (var user : oidcUsers) - if (!this.oidcUsers.contains(user)) - this.oidcUsers.add(user); - } - -} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/model/ProjectRole.java b/src/main/java/org/dependencytrack/model/ProjectRole.java new file mode 100644 index 0000000000..dbf4807603 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/ProjectRole.java @@ -0,0 +1,234 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.FetchGroup; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceAware; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.Unique; + +/** + * Base class for user-project-role mapping. + * + * @author Jonathan Howard + * @since 5.6.0 + */ +@PersistenceAware +@JsonInclude(JsonInclude.Include.NON_NULL) +public abstract class ProjectRole implements Serializable { + + @Persistent(defaultFetchGroup = "true") + @Column(name = "ROLE_ID", allowsNull = "false") + @JsonIgnore + private Role role; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "PROJECT_ID", allowsNull = "false") + @JsonIgnore + private Project project; + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public Project getProject() { + return project; + } + + public void setProject(Project project) { + this.project = project; + } + + /** + * Model for associating a role on a given project with LDAP users. + * + * @author Allen Shearin + * @since 5.6.0 + */ + @PersistenceCapable(table = "LDAPUSERS_PROJECTS_ROLES") + @Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "LDAPUSERS_PROJECTS_ROLES", + members = { "ldapUsers", "project", "role" }, + deferred = "true") + @FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "ldapUsers") + }) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class LdapUserProjectRole extends ProjectRole { + + private static final long serialVersionUID = 6018553054343647649L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @Persistent(defaultFetchGroup = "true") + @Column(name = "LDAPUSER_ID", allowsNull = "false") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List ldapUsers; + + public List getLdapUsers() { + return ldapUsers; + } + + public void setLdapUsers(List ldapUsers) { + this.ldapUsers = ldapUsers; + } + + public void addLdapUsers(LdapUser... ldapUsers) { + this.ldapUsers = Objects.requireNonNullElse(this.ldapUsers, new ArrayList()); + this.ldapUsers = Stream.concat(this.ldapUsers.stream(), Arrays.stream(ldapUsers)) + .distinct() + .sorted(Comparator.comparing(LdapUser::getUsername)) + .toList(); + } + + } + + /** + * Model for associating a role on a given project with managed users. + * + * @author Allen Shearin + * @since 5.6.0 + */ + @PersistenceCapable(table = "MANAGEDUSERS_PROJECTS_ROLES") + @Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "MANAGEDUSERS_PROJECTS_ROLES", + members = { "managedUsers", "project", "role" }, + deferred = "true") + @FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "managedUsers") + }) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class ManagedUserProjectRole extends ProjectRole { + + private static final long serialVersionUID = -380122087527236991L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @Persistent(defaultFetchGroup = "true") + @Column(name = "MANAGEDUSER_ID", allowsNull = "false") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List managedUsers; + + public List getManagedUsers() { + return managedUsers; + } + + public void setManagedUsers(List managedUsers) { + this.managedUsers = managedUsers; + } + + public void addManagedUsers(ManagedUser... managedUsers) { + this.managedUsers = Objects.requireNonNullElse(this.managedUsers, new ArrayList()); + this.managedUsers = Stream.concat(this.managedUsers.stream(), Arrays.stream(managedUsers)) + .distinct() + .sorted(Comparator.comparing(ManagedUser::getUsername)) + .toList(); + } + + } + + /** + * Model for associating a role on a given project with OIDC users. + * + * @author Allen Shearin + * @since 5.6.0 + */ + @PersistenceCapable(table = "OIDCUSERS_PROJECTS_ROLES") + @Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "OIDCUSERS_PROJECTS_ROLES", + members = { "oidcUsers", "project", "role" }, + deferred = "true") + @FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "oidcUsers") + }) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class OidcUserProjectRole extends ProjectRole { + + private static final long serialVersionUID = -5029209056240375886L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @Persistent(defaultFetchGroup = "true") + @Column(name = "OIDCUSER_ID", allowsNull = "false") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List oidcUsers; + + public List getOidcUsers() { + return oidcUsers; + } + + public void setOidcUsers(List oidcUsers) { + this.oidcUsers = oidcUsers; + } + + public void addOidcUsers(OidcUser... oidcUsers) { + this.oidcUsers = Objects.requireNonNullElse(this.oidcUsers, new ArrayList()); + this.oidcUsers = Stream.concat(this.oidcUsers.stream(), Arrays.stream(oidcUsers)) + .distinct() + .sorted(Comparator.comparing(OidcUser::getUsername)) + .toList(); + } + + } + +} diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 56be456988..c4653b2fc6 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -940,31 +940,23 @@ public Project updateLastBomImport(Project p, Date date, String bomFormat) { @Override public boolean hasAccess(final Principal principal, final Project project) { - if (principal == null) { - // This is a system request being made (e.g. MetricsUpdateTask, etc) where there isn't a principal + if (!isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) + || principal == null // System request (e.g. MetricsUpdateTask, etc) where there isn't a principal + || super.hasAccessManagementPermission(principal)) // TODO: After Alpine >= 3.2.0: request.getEffectivePermission().contains(Permissions.ACCESS_MANAGEMENT.name()) return true; - } - - if (!isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED)) { - return true; - } - - // TODO: After upgrading to Alpine >= 3.2.0, this should become: - // request.getEffectivePermission().contains(Permissions.ACCESS_MANAGEMENT.name()) - // https://github.com/stevespringett/Alpine/pull/764 - if (super.hasAccessManagementPermission(principal)) { - return true; - } + final Set roleIds = getRoleIds(principal, project); final Set teamIds = getTeamIds(principal); - if (teamIds.isEmpty()) { + + if (teamIds.isEmpty() && roleIds.isEmpty()) return false; - } - final Query query = pm.newQuery(Query.SQL, "SELECT HAS_PROJECT_ACCESS(:projectId, :teamIds)"); + final Query query = pm.newQuery(Query.SQL, "SELECT HAS_PROJECT_ACCESS(:projectId, :teamIds, :roleIds)"); query.setNamedParameters(Map.ofEntries( Map.entry("projectId", project.getId()), - Map.entry("teamIds", teamIds.toArray(new Long[0])))); + Map.entry("teamIds", teamIds.toArray(Long[]::new)), + Map.entry("roleIds", roleIds.toArray(Long[]::new)))); + return executeAndCloseResultUnique(query, Boolean.class); } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 61b611100f..e1f18ca5a1 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -23,10 +23,10 @@ import alpine.common.validation.RegexSequence; import alpine.model.ApiKey; import alpine.model.ConfigProperty; -import alpine.model.IConfigProperty.PropertyType; import alpine.model.LdapUser; import alpine.model.ManagedUser; import alpine.model.OidcUser; +import alpine.model.IConfigProperty.PropertyType; import alpine.model.Permission; import alpine.model.Team; import alpine.model.UserPrincipal; @@ -68,7 +68,6 @@ import org.dependencytrack.model.IntegrityMetaComponent; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; -import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.Policy; @@ -78,6 +77,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.model.ProjectRole; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; @@ -124,14 +124,12 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.function.Predicate; -import java.util.function.Supplier; import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED; import static org.dependencytrack.proto.vulnanalysis.v1.ScanStatus.SCAN_STATUS_FAILED; @@ -510,33 +508,40 @@ public QueryManager withL2CacheDisabled() { } /** - * Get the IDs of the {@link MappedRole}s a given {@link Principal} is a member of. + * Get the IDs of the {@link ProjectRole}s a given {@link Principal} is a member of. * - * @return A {@link Set} of {@link MappedRole} IDs + * @return A {@link Set} of {@link ProjectRole} IDs */ protected Set getRoleIds(final Principal principal, final Project project) { String usersField; - - final MappedRole mappedRole = new MappedRole(); - Supplier roleIds = mappedRole.getRole()::getId; + Class cls; switch (principal) { - case LdapUser ldapUser -> usersField = "ldapUsers"; - case ManagedUser managedUser -> usersField = "managedUsers"; - case OidcUser oidcUser -> usersField = "oidcUsers"; + case LdapUser ldapUser -> { + usersField = "ldapUsers"; + cls = ProjectRole.LdapUserProjectRole.class; + } + case ManagedUser managedUser -> { + usersField = "managedUsers"; + cls = ProjectRole.ManagedUserProjectRole.class; + } + case OidcUser oidcUser -> { + usersField = "oidcUsers"; + cls = ProjectRole.OidcUserProjectRole.class; + } default -> { return Collections.emptySet(); } }; - Query query = pm.newQuery(MappedRole.class) + Query query = pm.newQuery(cls) .filter("project.id == :projectId && %s.contains(:principal)".formatted(usersField)) .setNamedParameters(Map.ofEntries( Map.entry("principal", principal), Map.entry("projectId", project.getId()))); return Set.of(executeAndCloseList(query).stream() - .map(MappedRole::getRole) + .map(ProjectRole::getRole) .map(Role::getId) .toArray(Long[]::new)); } @@ -547,19 +552,11 @@ protected Set getRoleIds(final Principal principal, final Project project) * @return A {@link Set} of {@link Team} IDs */ protected Set getTeamIds(final Principal principal) { - final var principalTeamIds = new HashSet(); - if (principal instanceof final UserPrincipal userPrincipal - && userPrincipal.getTeams() != null) { - for (final Team userInTeam : userPrincipal.getTeams()) { - principalTeamIds.add(userInTeam.getId()); - } - } else if (principal instanceof final ApiKey apiKey - && apiKey.getTeams() != null) { - for (final Team userInTeam : apiKey.getTeams()) { - principalTeamIds.add(userInTeam.getId()); - } - } - return principalTeamIds; + return switch (principal) { + case UserPrincipal userPrincipal -> Set.of(userPrincipal.getTeams().toArray(Long[]::new)); + case ApiKey apiKey -> Set.of(apiKey.getTeams().toArray(Long[]::new)); + default -> Collections.emptySet(); + }; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -972,19 +969,10 @@ public Role getRole(String uuid) { return getRoleQueryManager().getRole(null); } - public List getUserRoles(UserPrincipal user) { - return getRoleQueryManager().getUserRoles(user); - } - public Role updateRole(Role transientRole) { return getRoleQueryManager().updateRole(transientRole); } - public List getUnassignedProjects(UserPrincipal user) { - // TODO Auto-generated method stub - return getRoleQueryManager().getUnassignedProjects(user); - } - public Vulnerability createVulnerability(Vulnerability vulnerability, boolean commitIndex) { return getVulnerabilityQueryManager().createVulnerability(vulnerability, commitIndex); } @@ -1394,8 +1382,24 @@ public boolean addRoleToUser(UserPrincipal principal, Role role, Project project return getRoleQueryManager().addRoleToUser(principal, role, project); } - public boolean removeRoleFromUser(UserPrincipal principal, Role role, Project project){ - return getRoleQueryManager().removeRoleFromUser(principal, role, project); + public List getUnassignedProjects(final String username) { + return getRoleQueryManager().getUnassignedProjects(username); + } + + public List getUnassignedProjects(final UserPrincipal user) { + return getRoleQueryManager().getUnassignedProjects(user); + } + + public List getUnassignedRolePermissions(final Role role) { + return getRoleQueryManager().getUnassignedRolePermissions(role); + } + + public List getUserRoles(UserPrincipal user) { + return getRoleQueryManager().getUserRoles(user); + } + + public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { + return getRoleQueryManager().removeRoleFromUser(user, role, project); } public NotificationRule createNotificationRule(String name, NotificationScope scope, NotificationLevel level, NotificationPublisher publisher) { diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index ddaf9397f2..9d09558591 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,30 +18,24 @@ */ package org.dependencytrack.persistence; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.jdo.PersistenceManager; import javax.jdo.Query; -import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; +import org.dependencytrack.model.ProjectRole; import org.dependencytrack.persistence.jdbi.JdbiFactory; import org.dependencytrack.persistence.jdbi.RoleDao; -import org.jdbi.v3.core.Handle; import alpine.common.logging.Logger; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; -import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; - final class RoleQueryManager extends QueryManager implements IQueryManager { private static final Logger LOGGER = Logger.getLogger(RoleQueryManager.class); @@ -72,28 +66,6 @@ public List getRoles() { return query.executeList(); } - @Override - public List getUserRoles(UserPrincipal user) { - String usersField; - - switch (user) { - case LdapUser ldapUser -> usersField = "ldapUsers"; - case ManagedUser managedUser -> usersField = "managedUsers"; - case OidcUser oidcUser -> usersField = "oidcUsers"; - default -> { - return null; - } - } - ; - - Query query = pm.newQuery(MappedRole.class) - .filter("%s.contains(:user)".formatted(usersField)) - .setNamedParameters(Map.ofEntries( - Map.entry("user", user))); - - return executeAndCloseList(query); - } - @Override public Role getRole(String uuid) { final Query query = pm.newQuery(Role.class, "uuid == :uuid"); @@ -101,13 +73,33 @@ public Role getRole(String uuid) { return query.executeUnique(); } + @Override + public List getUserRoles(UserPrincipal user) { + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user)); + } + public List getUnassignedProjects(final String username) { return getUnassignedProjects(getUserPrincipal(username)); } + public List getUnassignedProjects(final UserPrincipal user) { + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserUnassignedProjects(user)); + } + public List getUnassignedRolePermissions(final Role role) { - // TODO: Implement getUnassignedRolePermissions - return Collections.emptyList(); + List permissions = new ArrayList<>(); + + var permissionNames = role.getPermissions().stream() + .map(Permission::getName) + .toList(); + + Query query = pm.newQuery(Permission.class) + .filter("!:permissionNames.contains(name)") + .setNamedParameters(Map.of("permissionNames", permissionNames)); + + permissions.addAll(executeAndCloseList(query)); + + return permissions; } @Override @@ -123,29 +115,14 @@ public Role updateRole(Role transientRole) { @Override public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { - return JdbiFactory.withJdbiHandle(handle -> { - RoleDao dao = handle.attach(RoleDao.class); - MappedRole mappedRole = dao.addProjectAccessRole(project.getId(), role.getId()); - - return dao.addRoleToUser(user, mappedRole.getId()) == 1; - }); + return JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addRoleToUser(user, project.getId(), role.getId())) == 1; } @Override public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { - try (final Handle jdbiHandle = openJdbiHandle()) { - int count = switch (user) { - case LdapUser ldapUser -> jdbiHandle.attach(RoleDao.class) - .removeRoleFromLdapUser(ldapUser.getId(), project.getId(), role.getId()); - case ManagedUser managedUser -> jdbiHandle.attach(RoleDao.class) - .removeRoleFromManagedUser(managedUser.getId(), project.getId(), role.getId()); - case OidcUser oidcUser -> jdbiHandle.attach(RoleDao.class) - .removeRoleFromOidcUser(oidcUser.getId(), project.getId(), role.getId()); - default -> 0; - }; - - return count == 1; - } + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser(user, + project, role.getId())) > 0; } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index ab8f20096d..3a7bd058ef 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -20,13 +20,15 @@ import java.util.List; -import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.persistence.jdbi.mapping.ProjectRoleRowMapper; + import org.jdbi.v3.sqlobject.config.RegisterFieldMapper; +import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.customizer.Define; import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings; -import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; @@ -38,111 +40,73 @@ public interface RoleDao { @SqlUpdate(/* language=sql */ """ - INSERT INTO "PROJECT_ACCESS_ROLES" ("PROJECT_ID", "ROLE_ID") - VALUES (:projectId, :roleId) - ON CONFLICT DO NOTHING + DELETE + FROM "ROLE" + WHERE "ID" = :roleId """) - @GetGeneratedKeys - @RegisterFieldMapper(MappedRole.class) - MappedRole addProjectAccessRole(@Bind final long projectId, @Bind final long roleId); + int deleteRole(@Bind final long roleId); @SqlUpdate(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - INSERT INTO "${user.getClass().getSimpleName()?upper_case}S_PROJECTS_ROLES" - ("${user.getClass().getSimpleName()?upper_case}_ID", "PROJECT_ACCESS_ROLE_ID") + <#assign prefix = user.getClass().getSimpleName()?upper_case> + INSERT INTO "${prefix}S_PROJECTS_ROLES" + ("${prefix}_ID", "PROJECT_ID", "ROLE_ID") VALUES - (${user.getId()}, :projectAccessRoleId) + (${user.getId()}, :projectId, :roleId) ON CONFLICT DO NOTHING """) @DefineNamedBindings - int addRoleToUser(@Define T user, @Bind final long projectAccessRoleId); + int addRoleToUser(@Define T user, @Bind long projectId, @Bind long roleId); - @SqlUpdate(/* language=sql */""" + @SqlUpdate(/* language=sql */ """ + <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> + <#-- @ftlvariable name="project" type="org.dependencytrack.model.Project" --> + <#assign prefix = user.getClass().getSimpleName()?upper_case> DELETE - FROM "ROLE" - WHERE "ID" = :roleId + FROM "${prefix}S_PROJECTS_ROLES" + WHERE "${prefix}_ID" = ${user.getId()} + AND "ROLE_ID" = :roleId + AND "PROJECT_ID" IN ( + SELECT "ID" + FROM "PROJECT" + WHERE "NAME" = '${project.getName()}' + ) """) - int deleteRole(@Bind final long roleId); + @DefineNamedBindings + int removeRoleFromUser(@Define T user, @Define Project project, @Bind long roleId); - @SqlQuery(/* language=sql */""" - SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + @SqlQuery(/* language=sql */ """ + <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> + <#assign prefix = user.getClass().getSimpleName()?upper_case> + SELECT "PROJECT"."ID" AS "PROJECT_ID", + "ROLE"."ID" AS "ROLE_ID" FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "LDAPUSERS_PROJECTS_ROLES" - ON "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "LDAPUSER" - ON "LDAPUSER"."ID" = "LDAPUSERS_PROJECTS_ROLES"."LDAPUSER_ID" - WHERE "LDAPUSER"."USERNAME" != :username - OR "LDAPUSER"."USERNAME" IS NULL - """) - @RegisterFieldMapper(Project.class) - List getLdapUserUnassignedProjects(@Bind final String username); - - @SqlQuery(/* language=sql */""" - SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" - FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "MANAGEDUSERS_PROJECTS_ROLES" - ON "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "MANAGEDUSER" - ON "MANAGEDUSER"."ID" = "MANAGEDUSERS_PROJECTS_ROLES"."MANAGEDUSER_ID" - WHERE "MANAGEDUSER"."USERNAME" != :username - OR "MANAGEDUSER"."USERNAME" IS NULL + INNER JOIN "${prefix}S_PROJECTS_ROLES" + ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + INNER JOIN "${prefix}" + ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" + INNER JOIN "ROLE" + ON "ROLE"."ID" = "${prefix}S_PROJECTS_ROLES"."ROLE_ID" + WHERE "${prefix}"."USERNAME" != '${user.getUsername()}' + OR "${prefix}"."USERNAME" IS NULL """) - @RegisterFieldMapper(Project.class) - List getManagedUserUnassignedProjects(@Bind final String username); + @RegisterRowMapper(ProjectRoleRowMapper.class) + @DefineNamedBindings + List getUserRoles(@Define T user); - @SqlQuery(/* language=sql */""" - SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + @SqlQuery(/* language=sql */ """ + <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> + <#assign prefix = user.getClass().getSimpleName()?upper_case> + SELECT "PROJECT"."ID", "PROJECT"."NAME" FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "OIDCUSERS_PROJECTS_ROLES" - ON "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "OIDCUSER" - ON "OIDCUSER"."ID" = "OIDCUSERS_PROJECTS_ROLES"."OIDCUSER_ID" - WHERE "OIDCUSER"."USERNAME" != :username - OR "OIDCUSER"."USERNAME" IS NULL + LEFT JOIN "${prefix}S_PROJECTS_ROLES" + ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "${prefix}" + ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" + WHERE "${prefix}"."USERNAME" != '${user.getUsername()}' + OR "${prefix}"."USERNAME" IS NULL """) @RegisterFieldMapper(Project.class) - List getOidcUserUnassignedProjects(@Bind final String username); - - @SqlUpdate(/* language=sql */""" - DELETE - FROM "LDAPUSERS_PROJECTS_ROLES" - WHERE "LDAPUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) - int removeRoleFromLdapUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - - @SqlUpdate(/* language=sql */""" - DELETE - FROM "MANAGEDUSERS_PROJECTS_ROLES" - WHERE "MANAGEDUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) - int removeRoleFromManagedUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - - @SqlUpdate(/* language=sql */""" - DELETE - FROM "OIDCUSERS_PROJECTS_ROLES" - WHERE "OIDCUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) - int removeRoleFromOidcUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + List getUserUnassignedProjects(@Define T user); -} \ No newline at end of file +} diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java new file mode 100644 index 0000000000..d71d0bfdbe --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java @@ -0,0 +1,65 @@ +/* + * 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.jdbi.mapping; + +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; +import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; +import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; +import org.dependencytrack.model.Role; + +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.hasColumn; +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; + +public class ProjectRoleRowMapper implements RowMapper { + + public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) throws SQLException { + ProjectRole projectRole; + + switch (resultSet) { + case ResultSet rs when hasColumn(rs, "LDAPUSER_ID") -> projectRole = new LdapUserProjectRole(); + case ResultSet rs when hasColumn(rs, "MANAGEDUSER_ID") -> projectRole = new ManagedUserProjectRole(); + case ResultSet rs when hasColumn(rs, "OIDCUSER_ID") -> projectRole = new OidcUserProjectRole(); + default -> { + return null; + } + } + + maybeSet(resultSet, "PROJECT_ID", ResultSet::getLong, value -> { + var project = new Project(); + project.setId(value); + projectRole.setProject(project); + }); + + maybeSet(resultSet, "ROLE_ID", ResultSet::getLong, value -> { + var role = new Role(); + role.setId(value); + projectRole.setRole(role); + }); + + return projectRole; + } + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 950653c79d..b1437b7f84 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -19,9 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.persistence.PaginatedResult; @@ -60,7 +57,6 @@ import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -142,14 +138,9 @@ public Response retrieveUserProjects( try (final Handle jdbiHandle = openJdbiHandle()) { var dao = jdbiHandle.attach(RoleDao.class); - List projects = switch (principal) { - case LdapUser user -> dao.getLdapUserUnassignedProjects(user.getUsername()); - case ManagedUser user -> dao.getManagedUserUnassignedProjects(user.getUsername()); - case OidcUser user -> dao.getOidcUserUnassignedProjects(user.getUsername()); - default -> Collections.emptyList(); - }; + List projects = dao.getUserUnassignedProjects(principal); - if (projects.isEmpty()) + if (projects == null || projects.isEmpty()) return Response.status(Response.Status.NOT_FOUND).entity("No unassigned projects for specified user.").build(); return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); diff --git a/src/main/resources/META-INF/persistence.xml b/src/main/resources/META-INF/persistence.xml index cf0d7ac966..b7dd12703b 100644 --- a/src/main/resources/META-INF/persistence.xml +++ b/src/main/resources/META-INF/persistence.xml @@ -27,13 +27,13 @@ org.dependencytrack.model.Bom org.dependencytrack.model.Component org.dependencytrack.model.ComponentProperty - org.dependencytrack.model.IntegrityMetaComponent org.dependencytrack.model.DependencyMetrics org.dependencytrack.model.Epss org.dependencytrack.model.FindingAttribution + org.dependencytrack.model.IntegrityAnalysis + org.dependencytrack.model.IntegrityMetaComponent org.dependencytrack.model.License org.dependencytrack.model.LicenseGroup - org.dependencytrack.model.MappedRole org.dependencytrack.model.NotificationPublisher org.dependencytrack.model.NotificationRule org.dependencytrack.model.Policy @@ -44,6 +44,9 @@ org.dependencytrack.model.ProjectMetadata org.dependencytrack.model.ProjectMetrics org.dependencytrack.model.ProjectProperty + org.dependencytrack.model.ProjectRole$LdapUserProjectRole + org.dependencytrack.model.ProjectRole$ManagedUserProjectRole + org.dependencytrack.model.ProjectRole$OidcUserProjectRole org.dependencytrack.model.Repository org.dependencytrack.model.RepositoryMetaComponent org.dependencytrack.model.Role @@ -56,11 +59,10 @@ org.dependencytrack.model.Vulnerability org.dependencytrack.model.VulnerabilityAlias org.dependencytrack.model.VulnerabilityMetrics + org.dependencytrack.model.VulnerabilityPolicyBundle org.dependencytrack.model.VulnerabilityScan org.dependencytrack.model.VulnerableSoftware org.dependencytrack.model.WorkflowState - org.dependencytrack.model.IntegrityAnalysis - org.dependencytrack.model.VulnerabilityPolicyBundle true NONE diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index 765f563856..adbea1e092 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -646,21 +646,8 @@
        - - - - - - - - - - - - - - + @@ -710,15 +697,20 @@ - - - + + + @@ -726,48 +718,24 @@ - - - - - - - - - - - - - - - - - - + + + + - + @@ -780,12 +748,21 @@ validateUnique="true" validateForeignKey="true" /> - + + + + + @@ -794,8 +771,12 @@ + + + + - + @@ -808,12 +789,21 @@ validateUnique="true" validateForeignKey="true" /> - + + + + + @@ -822,9 +812,12 @@ - - + + + + + diff --git a/src/main/resources/migration/procedures/function_has-project-access.sql b/src/main/resources/migration/procedures/function_has-project-access.sql index 25dd6b4864..3fd727255b 100644 --- a/src/main/resources/migration/procedures/function_has-project-access.sql +++ b/src/main/resources/migration/procedures/function_has-project-access.sql @@ -1,6 +1,7 @@ create or replace function has_project_access( - project_id bigint -, team_ids bigint[] + project_id bigint, + team_ids bigint[], + role_ids bigint[] ) returns bool language "sql" parallel safe @@ -8,22 +9,46 @@ create or replace function has_project_access( as $$ with recursive project_hierarchy(id, parent_id) as( - select "ID" as id - , "PARENT_PROJECT_ID" as parent_id + select "ID" as id, + "PARENT_PROJECT_ID" as parent_id from "PROJECT" where "ID" = project_id union all - select "PROJECT"."ID" as id - , "PROJECT"."PARENT_PROJECT_ID" as parent_id + select "PROJECT"."ID" as id, + "PROJECT"."PARENT_PROJECT_ID" as parent_id from "PROJECT" inner join project_hierarchy on project_hierarchy.parent_id = "PROJECT"."ID" ) -select exists( - select 1 - from project_hierarchy - inner join "PROJECT_ACCESS_TEAMS" - on "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = project_hierarchy.id - where "PROJECT_ACCESS_TEAMS"."TEAM_ID" = any(team_ids) +select coalesce( + ( + select true + from project_hierarchy + inner join "PROJECT_ACCESS_TEAMS" + on "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = project_hierarchy.id + where "PROJECT_ACCESS_TEAMS"."TEAM_ID" = any(team_ids) + ), + ( + select true + from project_hierarchy + inner join "LDAPUSERS_PROJECTS_ROLES" + on "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ID" = project_hierarchy.id + where "LDAPUSERS_PROJECTS_ROLES"."ROLE_ID" = any(role_ids) + ), + ( + select true + from project_hierarchy + inner join "MANAGEDUSERS_PROJECTS_ROLES" + on "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ID" = project_hierarchy.id + where "MANAGEDUSERS_PROJECTS_ROLES"."ROLE_ID" = any(role_ids) + ), + ( + select true + from project_hierarchy + inner join "OIDCUSERS_PROJECTS_ROLES" + on "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ID" = project_hierarchy.id + where "OIDCUSERS_PROJECTS_ROLES"."ROLE_ID" = any(role_ids) + ), + false ) $$; \ No newline at end of file From c8f65d118ce69162e91a0f1aa35dd6c493452d86 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 17 Mar 2025 14:13:24 -0600 Subject: [PATCH 039/181] update getunassignedprojects dao query Signed-off-by: Allen Shearin --- src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 3a7bd058ef..8dd9dffd15 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -97,7 +97,7 @@ public interface RoleDao { @SqlQuery(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> <#assign prefix = user.getClass().getSimpleName()?upper_case> - SELECT "PROJECT"."ID", "PROJECT"."NAME" + SELECT "PROJECT"."ID", "PROJECT"."NAME", "PROJECT"."UUID" FROM "PROJECT" LEFT JOIN "${prefix}S_PROJECTS_ROLES" ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" From b157864e8422f366458d210d67fcc6e9fc19c8de Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 19 Mar 2025 12:39:39 -0600 Subject: [PATCH 040/181] working getUserRoles and associated mapper/methods/queries, draft removeRoleFromUser, move RoleProjectRequest Signed-off-by: Allen Shearin --- .../dependencytrack/model/ProjectRole.java | 3 -- .../persistence/QueryManager.java | 4 +- .../persistence/jdbi/RoleDao.java | 14 +++++-- .../jdbi/mapping/ProjectRoleRowMapper.java | 25 ++++++------ .../resources/v1/RoleResource.java | 38 +++++++++++++++++++ .../resources/v1/UserResource.java | 31 +++++---------- .../v1/{ => vo}/RoleProjectRequest.java | 2 +- 7 files changed, 74 insertions(+), 43 deletions(-) rename src/main/java/org/dependencytrack/resources/v1/{ => vo}/RoleProjectRequest.java (96%) diff --git a/src/main/java/org/dependencytrack/model/ProjectRole.java b/src/main/java/org/dependencytrack/model/ProjectRole.java index dbf4807603..e0ac7d206d 100644 --- a/src/main/java/org/dependencytrack/model/ProjectRole.java +++ b/src/main/java/org/dependencytrack/model/ProjectRole.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import alpine.model.LdapUser; @@ -54,12 +53,10 @@ public abstract class ProjectRole implements Serializable { @Persistent(defaultFetchGroup = "true") @Column(name = "ROLE_ID", allowsNull = "false") - @JsonIgnore private Role role; @Persistent(defaultFetchGroup = "true") @Column(name = "PROJECT_ID", allowsNull = "false") - @JsonIgnore private Project project; public Role getRole() { diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 821feb4293..2837facd02 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -554,8 +554,8 @@ protected Set getRoleIds(final Principal principal, final Project project) */ protected Set getTeamIds(final Principal principal) { return switch (principal) { - case UserPrincipal userPrincipal -> Set.of(userPrincipal.getTeams().toArray(Long[]::new)); - case ApiKey apiKey -> Set.of(apiKey.getTeams().toArray(Long[]::new)); + case UserPrincipal user -> Set.copyOf(user.getTeams().stream().map(Team::getId).toList()); + case ApiKey apiKey -> Set.copyOf(apiKey.getTeams().stream().map(Team::getId).toList()); default -> Collections.emptySet(); }; } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 8dd9dffd15..3495a4b278 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -74,12 +74,19 @@ public interface RoleDao { """) @DefineNamedBindings int removeRoleFromUser(@Define T user, @Define Project project, @Bind long roleId); + // (@Bind long userId, @Bind String projectName, @Bind long roleId) @SqlQuery(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> <#assign prefix = user.getClass().getSimpleName()?upper_case> - SELECT "PROJECT"."ID" AS "PROJECT_ID", - "ROLE"."ID" AS "ROLE_ID" + SELECT + "PROJECT"."ID" AS "PROJECT_ID", + "PROJECT"."NAME" AS "PROJECT_NAME", + "PROJECT"."UUID" AS "PROJECT_UUID", + "ROLE"."ID" AS "ROLE_ID", + "ROLE"."NAME" AS "ROLE_NAME", + "ROLE"."UUID" AS "ROLE_UUID", + "${prefix}"."ID" AS "${prefix}_ID" FROM "PROJECT" INNER JOIN "${prefix}S_PROJECTS_ROLES" ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" @@ -87,8 +94,7 @@ public interface RoleDao { ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" INNER JOIN "ROLE" ON "ROLE"."ID" = "${prefix}S_PROJECTS_ROLES"."ROLE_ID" - WHERE "${prefix}"."USERNAME" != '${user.getUsername()}' - OR "${prefix}"."USERNAME" IS NULL + WHERE "${prefix}"."USERNAME" = '${user.getUsername()}' """) @RegisterRowMapper(ProjectRoleRowMapper.class) @DefineNamedBindings diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java index d71d0bfdbe..6c7b115152 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java @@ -27,14 +27,15 @@ import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.statement.StatementContext; + import java.sql.ResultSet; import java.sql.SQLException; +import java.util.UUID; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.hasColumn; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; public class ProjectRoleRowMapper implements RowMapper { - public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) throws SQLException { ProjectRole projectRole; @@ -47,19 +48,21 @@ public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) th } } - maybeSet(resultSet, "PROJECT_ID", ResultSet::getLong, value -> { - var project = new Project(); - project.setId(value); - projectRole.setProject(project); + projectRole.setProject(new Project()); + projectRole.setRole(new Role()); + + maybeSet(resultSet, "PROJECT_ID", ResultSet::getLong, projectRole.getProject()::setId); + maybeSet(resultSet, "PROJECT_NAME", ResultSet::getString, projectRole.getProject()::setName); + maybeSet(resultSet, "PROJECT_UUID", ResultSet::getString, value -> { + projectRole.getProject().setUuid(UUID.fromString(value)); }); - maybeSet(resultSet, "ROLE_ID", ResultSet::getLong, value -> { - var role = new Role(); - role.setId(value); - projectRole.setRole(role); + maybeSet(resultSet, "ROLE_ID", ResultSet::getLong, projectRole.getRole()::setId); + maybeSet(resultSet, "ROLE_NAME", ResultSet::getString, projectRole.getRole()::setName); + maybeSet(resultSet, "ROLE_UUID", ResultSet::getString, value -> { + projectRole.getRole().setUuid(UUID.fromString(value)); }); return projectRole; } - -} +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 595bb08a2f..cc3e3385a6 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -48,13 +48,18 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.model.Role; +import org.dependencytrack.model.ProjectRole; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.persistence.jdbi.RoleDao; import org.jdbi.v3.core.Handle; import org.owasp.security.logging.SecurityMarkers; +import alpine.model.UserPrincipal; + import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; +import java.util.List; + /** * JAX-RS resources for processing roles. * @@ -205,4 +210,37 @@ public Response deleteRole(Role jsonRole) { } } + @GET + @Path("/{username}/roles") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of roles assigned to the specified user", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_READ

        ") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of roles assigned to the user", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProjectRole.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user could not be found") + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) + public Response getUserRoles( + @Parameter(description = "A valid username", required = true) @PathParam("username") String username) { + try (QueryManager qm = new QueryManager()) { + UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) { + LOGGER.warn("User not found: " + username); + return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); + } + + List roles = qm.getUserRoles(principal); + if (roles == null || roles.isEmpty()) { + LOGGER.info("No roles found for user: " + username); + return Response.ok(List.of()).build(); + } + + return Response.ok(roles).build(); + } + } } diff --git a/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 352c627fa2..18f8958e23 100644 --- a/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -33,6 +33,7 @@ import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.proto.notification.v1.UserSubject; +import org.dependencytrack.resources.v1.vo.RoleProjectRequest; import org.owasp.security.logging.SecurityMarkers; import alpine.Config; @@ -74,7 +75,6 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -844,31 +844,18 @@ public Response addRoleToUser( @ApiResponse( responseCode = "200", description = "Updated user with a specific role removed", - content = @Content(schema = @Schema(implementation = UserPrincipal.class)) - ), + content = @Content(schema = @Schema(implementation = UserPrincipal.class))), @ApiResponse(responseCode = "204", description = "The role has been successfully removed from the user"), @ApiResponse(responseCode = "304", description = "The user is not a member of the specified role"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The user or role could not be found") -}) - @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE }) public Response removeRoleFromUser( - @Parameter(description = "A valid username", required = true) - @PathParam("username") - String username, - - @Parameter(description = "The UUID of the role to associate username with", required = true) - IdentifiableObject identifiableObject, - - @Parameter(description = "The name of the project", required = true) - @QueryParam("projectName") - String projectName, - - @Parameter(description = "The version of the project") - @QueryParam("projectVersion") - String projectVersion) { + @Parameter(description = "A valid username", required = true) @PathParam("username") String username, + @Parameter(description = "Role and project information", required = true) RoleProjectRequest roleProjectRequest) { try (QueryManager qm = new QueryManager()) { - final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); + final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.getRoleUUID()); if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); @@ -876,7 +863,7 @@ public Response removeRoleFromUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - Project project = qm.getProject(projectName, projectVersion); + Project project = qm.getProject(roleProjectRequest.getProjectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -887,7 +874,7 @@ public Response removeRoleFromUser( principal = qm.getObjectById(principal.getClass(), principal.getId()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed role membership for: %s / role: %s / project: %s" - .formatted(principal.getName(), role.getName(), projectName)); + .formatted(principal.getName(), role.getName(), project.getName())); return Response.noContent().build(); } diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java b/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java similarity index 96% rename from src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java rename to src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java index ccc744c5a7..664410074e 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java +++ b/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) OWASP Foundation. All Rights Reserved. */ -package org.dependencytrack.resources.v1; +package org.dependencytrack.resources.v1.vo; import org.dependencytrack.model.validation.ValidUuid; From fabbe9e5a07578fe4e06c5236af8f31fe6cdb96f Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 19 Mar 2025 13:59:27 -0600 Subject: [PATCH 041/181] alter getuserUnassignedProjects to not return 404 if no projects are unassigned, instead return empty list Signed-off-by: Allen Shearin --- .../org/dependencytrack/resources/v1/AccessControlResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index b1437b7f84..dcaf1763c5 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -141,7 +141,7 @@ public Response retrieveUserProjects( List projects = dao.getUserUnassignedProjects(principal); if (projects == null || projects.isEmpty()) - return Response.status(Response.Status.NOT_FOUND).entity("No unassigned projects for specified user.").build(); + return Response.ok(List.of()).entity("No unassigned projects for specified user.").build(); return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); } From 001d856ec1d29d5d7a6c498e623a672031c683e3 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:31:28 -0500 Subject: [PATCH 042/181] refactor: create view of project effective permissions for user (#15) * refactor: create view of project effective permissions for user Signed-off-by: Jonathan Howard * fix: add public qualifier to new methods Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard --- .../persistence/QueryManager.java | 18 ++-- .../persistence/RoleQueryManager.java | 89 ++++++++++++++++--- .../resources/migration/changelog-v5.6.0.xml | 25 ++++++ 3 files changed, 115 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 2837facd02..1cefd32ffe 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -553,11 +553,13 @@ protected Set getRoleIds(final Principal principal, final Project project) * @return A {@link Set} of {@link Team} IDs */ protected Set getTeamIds(final Principal principal) { - return switch (principal) { - case UserPrincipal user -> Set.copyOf(user.getTeams().stream().map(Team::getId).toList()); - case ApiKey apiKey -> Set.copyOf(apiKey.getTeams().stream().map(Team::getId).toList()); - default -> Collections.emptySet(); + List teams = switch (principal) { + case UserPrincipal user -> user.getTeams(); + case ApiKey apiKey -> apiKey.getTeams(); + default -> Collections.emptyList(); }; + + return Set.copyOf(teams.stream().map(Team::getId).toList()); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1279,11 +1281,15 @@ public List getUnassignedRolePermissions(final Role role) { return getRoleQueryManager().getUnassignedRolePermissions(role); } - public List getUserRoles(UserPrincipal user) { + public List getUserRoles(final UserPrincipal user) { return getRoleQueryManager().getUserRoles(user); } - public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { + public List getUserProjectPermissions(final String username, final String projectName) { + return getRoleQueryManager().getUserProjectPermissions(username, projectName); + } + + public boolean removeRoleFromUser(final UserPrincipal user, final Role role, final Project project) { return getRoleQueryManager().removeRoleFromUser(user, role, project); } diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 9d09558591..6f75325319 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.jdo.PersistenceManager; import javax.jdo.Query; @@ -32,12 +33,29 @@ import org.dependencytrack.persistence.jdbi.RoleDao; import alpine.common.logging.Logger; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { + /** + * Represents a row returned by the USER_PROJECT_EFFECTIVE_PERMISSIONS view. + * + * @since 5.6.0 + */ + public record UserProjectEffectivePermissionsRow( + Long ldapUserId, + Long managedUserId, + Long oidcUserId, + Long projectId, + Long permissionId, + String permissionName) { + } + private static final Logger LOGGER = Logger.getLogger(RoleQueryManager.class); RoleQueryManager(final PersistenceManager pm) { @@ -50,7 +68,7 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { @Override public Role createRole(final String name, final List permissions) { - Role role = new Role(); + final Role role = new Role(); role.setName(name); role.setPermissions(permissions); @@ -67,15 +85,15 @@ public List getRoles() { } @Override - public Role getRole(String uuid) { + public Role getRole(final String uuid) { final Query query = pm.newQuery(Role.class, "uuid == :uuid"); return query.executeUnique(); } @Override - public List getUserRoles(UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user)); + public List getUserRoles(final UserPrincipal user) { + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user.getUsername())); } public List getUnassignedProjects(final String username) { @@ -87,13 +105,13 @@ public List getUnassignedProjects(final UserPrincipal user) { } public List getUnassignedRolePermissions(final Role role) { - List permissions = new ArrayList<>(); + final List permissions = new ArrayList<>(); - var permissionNames = role.getPermissions().stream() + final var permissionNames = role.getPermissions().stream() .map(Permission::getName) .toList(); - Query query = pm.newQuery(Permission.class) + final Query query = pm.newQuery(Permission.class) .filter("!:permissionNames.contains(name)") .setNamedParameters(Map.of("permissionNames", permissionNames)); @@ -103,7 +121,7 @@ public List getUnassignedRolePermissions(final Role role) { } @Override - public Role updateRole(Role transientRole) { + public Role updateRole(final Role transientRole) { final Role role = getObjectByUuid(Role.class, transientRole.getUuid()); if (role == null) return null; @@ -114,16 +132,65 @@ public Role updateRole(Role transientRole) { } @Override - public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { + public List getUserProjectPermissions(final String username, final String projectName) { + final UserPrincipal user = getUserPrincipal(username); + final String columnName; + + switch (user) { + case LdapUser ldapUser -> columnName = "LDAPUSER_ID"; + case ManagedUser managedUser -> columnName = "MANAGEDUSER_ID"; + case OidcUser oidcUser -> columnName = "OIDCUSER_ID"; + default -> { + return null; + } + }; + + final Query projectsQuery = pm.newQuery(Project.class) + .filter("name == :projectName") + .setNamedParameters(Map.of("projectName", projectName)); + + final String projectIds = executeAndCloseList(projectsQuery).stream() + .map(Project::getId) + .map(String::valueOf) + .collect(Collectors.joining(", ", "'{", "}'")); + + // language=SQL + final var queryString = """ + SELECT + upep."LDAPUSER_ID", + upep."MANAGEDUSER_ID", + upep."OIDCUSER_ID", + upep."PROJECT_ID", + upep."PERMISSION_ID", + upep."PERMISSION_NAME" + FROM "USER_PROJECT_EFFECTIVE_PERMISSIONS" upep + WHERE upep."%s" = :userId + AND upep."PROJECT_ID" = ANY(%s) + """.formatted(columnName, projectIds); + + final Query query = pm.newQuery(Query.SQL, queryString); + query.setNamedParameters(Map.of( + "userId", user.getId(), + "projectIds", projectIds)); + + return executeAndCloseResultList(query, UserProjectEffectivePermissionsRow.class) + .stream() + .map(UserProjectEffectivePermissionsRow::permissionName) + .map(this::getPermission) + .distinct() + .toList(); + } + + @Override + public boolean addRoleToUser(final UserPrincipal user, final Role role, final Project project) { return JdbiFactory.withJdbiHandle( handle -> handle.attach(RoleDao.class).addRoleToUser(user, project.getId(), role.getId())) == 1; } @Override - public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { + public boolean removeRoleFromUser(final UserPrincipal user, final Role role, final Project project) { return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser(user, project, role.getId())) > 0; - } } diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index 1ce7003cc0..bbd8edf36a 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -857,5 +857,30 @@ + + + SELECT + lpr."LDAPUSER_ID" AS "LDAPUSER_ID", + mpr."MANAGEDUSER_ID" AS "MANAGEDUSER_ID", + opr."OIDCUSER_ID" AS "OIDCUSER_ID", + pr."ID" AS "PROJECT_ID", + p."ID" AS "PERMISSION_ID", + p."NAME" AS "PERMISSION_NAME" + FROM "PERMISSION" p + INNER JOIN "ROLES_PERMISSIONS" rp + ON rp."PERMISSION_ID" = p."ID" + FULL OUTER JOIN "LDAPUSERS_PROJECTS_ROLES" lpr + ON lpr."ROLE_ID" = rp."ROLE_ID" + FULL OUTER JOIN "MANAGEDUSERS_PROJECTS_ROLES" mpr + ON lpr."PROJECT_ID" = mpr."PROJECT_ID" + AND lpr."ROLE_ID" = mpr."ROLE_ID" + FULL OUTER JOIN "OIDCUSERS_PROJECTS_ROLES" opr + ON mpr."PROJECT_ID" = opr."PROJECT_ID" + AND mpr."ROLE_ID" = opr."ROLE_ID" + INNER JOIN "PROJECT" pr + ON lpr."PROJECT_ID" = pr."ID" + OR mpr."PROJECT_ID" = pr."ID" + OR opr."PROJECT_ID" = pr."ID" + From b2cae30c65dfd5d15bcc8e74420fe7b07eea633c Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 19 Mar 2025 18:10:36 -0600 Subject: [PATCH 043/181] code cleanup Signed-off-by: Allen Shearin --- .../java/org/dependencytrack/persistence/RoleQueryManager.java | 2 +- .../java/org/dependencytrack/resources/v1/UserResource.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 6f75325319..8fc72e4da0 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -93,7 +93,7 @@ public Role getRole(final String uuid) { @Override public List getUserRoles(final UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user.getUsername())); + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user)); } public List getUnassignedProjects(final String username) { diff --git a/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 18f8958e23..4408efeba8 100644 --- a/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -823,7 +823,6 @@ public Response addRoleToUser( if (!modified) return Response.notModified().entity("The user is already a member of the specified role.").build(); - principal = qm.getObjectById(principal.getClass(), principal.getId()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s" .formatted(principal.getName(), role.getName(), project.getName())); From b43a7d3951d3ae72c5b844c3759ba8fa10057485 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 20 Mar 2025 17:26:00 -0600 Subject: [PATCH 044/181] correct merge issue Signed-off-by: Allen Shearin --- .../resources/v1/AccessControlResource.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 326c11dbb5..abdaeb431a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -54,18 +54,11 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.model.Project; -import org.dependencytrack.model.validation.ValidUuid; -import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.persistence.jdbi.RoleDao; -import org.dependencytrack.resources.v1.openapi.PaginatedApi; -import org.dependencytrack.resources.v1.vo.AclMappingRequest; import org.jdbi.v3.core.Handle; import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; -import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; From 14329aa5cb0cce694a52023a1584db22bd7832ba Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Mon, 24 Mar 2025 15:12:44 -0500 Subject: [PATCH 045/181] Role DAO fixes (#16) * fix: replace RoleDao method binds with interpolation Signed-off-by: Jonathan Howard * fix: QueryManager.getRole not passing uuid Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard --- .../dependencytrack/model/ProjectRole.java | 15 +- .../java/org/dependencytrack/model/Role.java | 32 ++- .../persistence/DefaultObjectGenerator.java | 221 +++++++----------- .../persistence/QueryManager.java | 2 +- .../persistence/RoleQueryManager.java | 37 +-- .../persistence/jdbi/RoleDao.java | 80 ++++--- .../jdbi/mapping/ProjectRoleRowMapper.java | 6 +- .../resources/v1/AccessControlResource.java | 2 +- .../resources/v1/RoleResource.java | 2 +- .../resources/migration/changelog-v5.6.0.xml | 4 +- 10 files changed, 180 insertions(+), 221 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/ProjectRole.java b/src/main/java/org/dependencytrack/model/ProjectRole.java index e0ac7d206d..f5f25e8e61 100644 --- a/src/main/java/org/dependencytrack/model/ProjectRole.java +++ b/src/main/java/org/dependencytrack/model/ProjectRole.java @@ -82,10 +82,7 @@ public void setProject(Project project) { * @since 5.6.0 */ @PersistenceCapable(table = "LDAPUSERS_PROJECTS_ROLES") - @Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "LDAPUSERS_PROJECTS_ROLES", - members = { "ldapUsers", "project", "role" }, - deferred = "true") + @Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "ldapUsers", "project", "role" }) @FetchGroup(name = "ALL", members = { @Persistent(name = "role"), @Persistent(name = "project"), @@ -133,10 +130,7 @@ public void addLdapUsers(LdapUser... ldapUsers) { * @since 5.6.0 */ @PersistenceCapable(table = "MANAGEDUSERS_PROJECTS_ROLES") - @Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "MANAGEDUSERS_PROJECTS_ROLES", - members = { "managedUsers", "project", "role" }, - deferred = "true") + @Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "managedUsers", "project", "role" }) @FetchGroup(name = "ALL", members = { @Persistent(name = "role"), @Persistent(name = "project"), @@ -184,10 +178,7 @@ public void addManagedUsers(ManagedUser... managedUsers) { * @since 5.6.0 */ @PersistenceCapable(table = "OIDCUSERS_PROJECTS_ROLES") - @Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "OIDCUSERS_PROJECTS_ROLES", - members = { "oidcUsers", "project", "role" }, - deferred = "true") + @Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "oidcUsers", "project", "role" }) @FetchGroup(name = "ALL", members = { @Persistent(name = "role"), @Persistent(name = "project"), diff --git a/src/main/java/org/dependencytrack/model/Role.java b/src/main/java/org/dependencytrack/model/Role.java index 6adac75ba5..15ea00f4ef 100644 --- a/src/main/java/org/dependencytrack/model/Role.java +++ b/src/main/java/org/dependencytrack/model/Role.java @@ -32,9 +32,10 @@ import jakarta.validation.constraints.Size; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; + +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; import java.util.UUID; import javax.jdo.annotations.Column; @@ -85,8 +86,7 @@ public enum FetchGroup { @NotBlank @Size(min = 1, max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, - message = "The name may only contain printable characters") + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The name may only contain printable characters") private String name; @Persistent(table = "ROLES_PERMISSIONS", defaultFetchGroup = "true") @@ -94,7 +94,7 @@ public enum FetchGroup { @Join(column = "ROLE_ID") @Element(column = "PERMISSION_ID") @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) - private List permissions; + private Set permissions = new LinkedHashSet<>(); @Persistent(customValueStrategy = "uuid") @Unique(name = "ROLE_UUID_IDX") @@ -118,24 +118,18 @@ public void setName(String name) { this.name = name; } - public List getPermissions() { + public Set getPermissions() { return permissions; } - public void setPermissions(List permissions) { + public void setPermissions(Set permissions) { this.permissions = permissions; } - public void addPermissions(Permission... permissions) { - if (this.permissions == null) { - this.permissions = new ArrayList<>(Arrays.asList(permissions)); - - return; - } + public boolean addPermissions(Permission... permissions) { + this.permissions = Objects.requireNonNullElse(this.permissions, new LinkedHashSet<>()); - for (var permission : permissions) - if (!this.permissions.contains(permission)) - this.permissions.add(permission); + return this.permissions.addAll(Set.of(permissions)); } public UUID getUuid() { @@ -152,10 +146,12 @@ public String toString() { .map(permission -> permission.getName()) .toList(); - return "%s{id=%d, name='%s', permissions=%s}".formatted( + return "%s{id=%d, uuid='%s', name='%s', permissions=%s}".formatted( getClass().getSimpleName(), id, + uuid, name, permissionStrings); } + } diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 6d3e1fbec1..3b934d4c80 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -32,7 +32,6 @@ import org.dependencytrack.common.ConfigKey; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; -import org.dependencytrack.model.Role; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter; @@ -43,7 +42,7 @@ import java.time.Duration; import java.time.Instant; - +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,7 +62,56 @@ public class DefaultObjectGenerator implements ServletContextListener { private static final Logger LOGGER = Logger.getLogger(DefaultObjectGenerator.class); - private final Map permissionsMap = new HashMap<>(); + private static final Map PERMISSIONS_MAP = new HashMap<>(); + + private static final Map> DEFAULT_TEAM_PERMISSIONS = Map.of( + "Administrators", Stream.of(Permissions.values()) + .map(Permissions::name) + .toList(), + "Portfolio Managers", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.PORTFOLIO_MANAGEMENT, + Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE), + "Automation", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.BOM_UPLOAD), + "Badge Viewers", List.of( + Permissions.Constants.VIEW_BADGES)); + + private static final Map> DEFAULT_ROLE_PERMISSIONS = Map.of( + "Project Admin", List.of( + Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE, + Permissions.Constants.VULNERABILITY_ANALYSIS, + Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ, + Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE, + Permissions.Constants.POLICY_MANAGEMENT, + Permissions.Constants.POLICY_MANAGEMENT_CREATE, + Permissions.Constants.POLICY_MANAGEMENT_READ, + Permissions.Constants.POLICY_MANAGEMENT_UPDATE, + Permissions.Constants.POLICY_MANAGEMENT_DELETE), + "Project Auditor", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VIEW_POLICY_VIOLATION, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ), + "Project Editor", List.of( + Permissions.Constants.BOM_UPLOAD, + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ, + Permissions.Constants.PROJECT_CREATION_UPLOAD), + "Project Viewer", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VIEW_BADGES)); /** * {@inheritDoc} @@ -125,6 +173,7 @@ private void executeLocked() { loadDefaultLicenses(qm); loadDefaultLicenseGroups(qm); loadDefaultRepositories(qm); + loadDefaultRoles(qm); loadDefaultConfigProperties(qm); loadDefaultNotificationPublishers(qm); recordDefaultObjectsVersion(qm); @@ -215,23 +264,16 @@ public void loadDefaultPermissions() { private void loadDefaultPermissions(final QueryManager qm) { LOGGER.info("Synchronizing permissions to datastore"); - for (final Permissions permission : Permissions.values()) { - if (qm.getPermission(permission.name()) == null) { - LOGGER.debug("Creating permission: " + permission.name()); - permissionsMap.put(permission.name(), - qm.createPermission(permission.name(), permission.getDescription())); - } - } - } - - private void createTeam(final QueryManager qm, final String name, final List permissions, final boolean createApiKey) { - LOGGER.debug("Creating team: " + name); - var team = qm.createTeam(name, createApiKey); - - LOGGER.debug("Assigning default permissions for team: " + name); - team.setPermissions(permissions); + List existing = Objects.requireNonNullElse(qm.getPermissions(), Collections.emptyList()) + .stream() + .map(Permission::getName) + .toList(); - qm.persist(team); + for (final Permissions value : Permissions.values()) + if (!existing.contains(value.name())) { + LOGGER.debug("Creating permission: " + value.name()); + PERMISSIONS_MAP.put(value.name(), qm.createPermission(value.name(), value.getDescription())); + } } @SuppressWarnings("unused") @@ -249,14 +291,20 @@ private void loadDefaultPersonas(final QueryManager qm) { 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); - createTeam(qm, "Administrators", List.copyOf(permissionsMap.values()), false); - createTeam(qm, "Portfolio Managers", getPortfolioManagersPermissions(), false); - createTeam(qm, "Automation", getAutomationPermissions(), true); - createTeam(qm, "Badge Viewers", getBadgesPermissions(), true); + for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { + 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")); @@ -266,126 +314,29 @@ private void loadDefaultPersonas(final QueryManager qm) { qm.persist(admin); } - private List getPortfolioManagersPermissions() { - return getPermissionsByName(Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.PORTFOLIO_MANAGEMENT, - Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, - Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE); - } - - private List getAutomationPermissions() { - return getPermissionsByName(Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.BOM_UPLOAD); + /** + * 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(PERMISSIONS_MAP::get).filter(Objects::nonNull).toList(); } - private List getBadgesPermissions() { - return getPermissionsByName(Permissions.Constants.VIEW_BADGES); - } - /** * Loads the default Roles */ - private void loadDefaultRoles() { - try (QueryManager qm = new QueryManager()) { - if (!qm.getRoles().isEmpty()) { - return; - } - LOGGER.debug("Assigning default permissions to roles"); - final List fullList = qm.getPermissions(); - - LOGGER.info("Adding default roles to datastore"); - LOGGER.debug("Creating role: Project Admin"); - final Role projectAdmin = qm.createRole("Project Admin", getProjectAdminPermissions(fullList)); - LOGGER.debug("Creating role: Project Auditor"); - final Role projectAuditor = qm.createRole("Project Auditor", getProjectAuditorPermissions(fullList)); - LOGGER.debug("Creating role: Project Editor"); - final Role projectEditor = qm.createRole("Project Editor", getProjectEditorPermissions(fullList)); - LOGGER.debug("Creating role: Project Viewer"); - final Role projectViewer = qm.createRole("Project Viewer", getProjectViewerPermissions(fullList)); - - qm.persist(projectAdmin); - qm.persist(projectAuditor); - qm.persist(projectEditor); - qm.persist(projectViewer); - } - } - - private List getProjectAdminPermissions(final List fullList) { - final List permissions = new ArrayList<>(); - for (final Permission permission : fullList) { - if (permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT.name()) || - permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_CREATE.name()) || - permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_READ.name()) || - permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_UPDATE.name()) || - permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_DELETE.name()) || - permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS.name()) || - permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_CREATE.name()) || - permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_READ.name()) || - permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_UPDATE.name()) || - permission.getName().equals(Permissions.POLICY_MANAGEMENT.name()) || - permission.getName().equals(Permissions.POLICY_MANAGEMENT_CREATE.name()) || - permission.getName().equals(Permissions.POLICY_MANAGEMENT_READ.name()) || - permission.getName().equals(Permissions.POLICY_MANAGEMENT_UPDATE.name()) || - permission.getName().equals(Permissions.POLICY_MANAGEMENT_DELETE.name())) { - permissions.add(permission); - } - } - return permissions; - } + private void loadDefaultRoles(final QueryManager qm) { + if (!qm.getRoles().isEmpty()) + return; - private List getProjectAuditorPermissions(final List fullList) { - final List permissions = new ArrayList<>(); - for (final Permission permission : fullList) { - if (permission.getName().equals(Permissions.VIEW_PORTFOLIO.name()) || - permission.getName().equals(Permissions.VIEW_VULNERABILITY.name()) || - permission.getName().equals(Permissions.VIEW_POLICY_VIOLATION.name()) || - permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_READ.name())) { - permissions.add(permission); - } - } - return permissions; - } + LOGGER.info("Adding default roles to datastore"); - private List getProjectEditorPermissions(final List fullList) { - final List permissions = new ArrayList<>(); - for (final Permission permission : fullList) { - if (permission.getName().equals(Permissions.BOM_UPLOAD.name()) || - permission.getName().equals(Permissions.VIEW_PORTFOLIO.name()) || - permission.getName().equals(Permissions.PORTFOLIO_MANAGEMENT_READ.name()) || - permission.getName().equals(Permissions.VIEW_VULNERABILITY.name()) || - permission.getName().equals(Permissions.VULNERABILITY_ANALYSIS_READ.name()) || - permission.getName().equals(Permissions.PROJECT_CREATION_UPLOAD.name())) { - permissions.add(permission); - } + for (var name : new String[] { "Project Admin", "Project Auditor", "Project Editor", "Project Viewer" }) { + LOGGER.debug("Creating role: " + name); + qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name))); } - return permissions; - } - - private List getProjectViewerPermissions(final List fullList) { - final List permissions = new ArrayList<>(); - for (final Permission permission : fullList) { - if (permission.getName().equals(Permissions.VIEW_PORTFOLIO.name()) || - permission.getName().equals(Permissions.VIEW_VULNERABILITY.name()) || - permission.getName().equals(Permissions.VIEW_BADGES.name())) { - permissions.add(permission); - } - } - return permissions; - } - - /** - * Perform a lookup of {@link Permission}s for specified name(s). - * - * @param names permission names - * @return list of {@link Permission}s - */ - private List getPermissionsByName(String... names) { - return Stream.of(names) - .map(permissionsMap::get) - .filter(Objects::nonNull) - .toList(); } public void loadDefaultRepositories() { diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 3d58609119..d819a98011 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -912,7 +912,7 @@ public List getRoles() { } public Role getRole(String uuid) { - return getRoleQueryManager().getRole(null); + return getRoleQueryManager().getRole(uuid); } public Role updateRole(Role transientRole) { diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 8fc72e4da0..4fda1b840a 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import javax.jdo.PersistenceManager; @@ -68,11 +69,13 @@ public record UserProjectEffectivePermissionsRow( @Override public Role createRole(final String name, final List permissions) { - final Role role = new Role(); - role.setName(name); - role.setPermissions(permissions); + return callInTransaction(() -> { + final Role role = new Role(); + role.setName(name); + role.setPermissions(Set.copyOf(permissions)); - return persist(role); + return persist(role); + }); } @Override @@ -86,14 +89,13 @@ public List getRoles() { @Override public Role getRole(final String uuid) { - final Query query = pm.newQuery(Role.class, "uuid == :uuid"); - - return query.executeUnique(); + return getObjectByUuid(Role.class, uuid, Role.FetchGroup.ALL.name()); } @Override public List getUserRoles(final UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user)); + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class) + .getUserRoles(user.getClass(), user.getUsername())); } public List getUnassignedProjects(final String username) { @@ -101,7 +103,9 @@ public List getUnassignedProjects(final String username) { } public List getUnassignedProjects(final UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserUnassignedProjects(user)); + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserUnassignedProjects( + user.getClass(), + user.getUsername())); } public List getUnassignedRolePermissions(final Role role) { @@ -143,7 +147,7 @@ public List getUserProjectPermissions(final String username, final S default -> { return null; } - }; + } final Query projectsQuery = pm.newQuery(Project.class) .filter("name == :projectName") @@ -184,13 +188,20 @@ public List getUserProjectPermissions(final String username, final S @Override public boolean addRoleToUser(final UserPrincipal user, final Role role, final Project project) { return JdbiFactory.withJdbiHandle( - handle -> handle.attach(RoleDao.class).addRoleToUser(user, project.getId(), role.getId())) == 1; + handle -> handle.attach(RoleDao.class).addRoleToUser( + user.getClass(), + user.getId(), + project.getId(), + role.getId())) == 1; } @Override public boolean removeRoleFromUser(final UserPrincipal user, final Role role, final Project project) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser(user, - project, role.getId())) > 0; + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser( + user.getClass(), + user.getId(), + project.getName(), + role.getId())) > 0; } } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 3495a4b278..7990b5ebea 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -48,71 +48,79 @@ public interface RoleDao { @SqlUpdate(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = user.getClass().getSimpleName()?upper_case> + <#assign prefix = userClass.getSimpleName()?upper_case> INSERT INTO "${prefix}S_PROJECTS_ROLES" ("${prefix}_ID", "PROJECT_ID", "ROLE_ID") VALUES - (${user.getId()}, :projectId, :roleId) + (:userId, :projectId, :roleId) ON CONFLICT DO NOTHING """) @DefineNamedBindings - int addRoleToUser(@Define T user, @Bind long projectId, @Bind long roleId); + int addRoleToUser( + @Define Class userClass, + @Bind long userId, + @Bind long projectId, + @Bind long roleId); @SqlUpdate(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#-- @ftlvariable name="project" type="org.dependencytrack.model.Project" --> - <#assign prefix = user.getClass().getSimpleName()?upper_case> + <#assign prefix = userClass.getSimpleName()?upper_case> DELETE FROM "${prefix}S_PROJECTS_ROLES" - WHERE "${prefix}_ID" = ${user.getId()} + WHERE "${prefix}_ID" = :userId AND "ROLE_ID" = :roleId AND "PROJECT_ID" IN ( SELECT "ID" FROM "PROJECT" - WHERE "NAME" = '${project.getName()}' + WHERE "NAME" = :projectName ) """) @DefineNamedBindings - int removeRoleFromUser(@Define T user, @Define Project project, @Bind long roleId); - // (@Bind long userId, @Bind String projectName, @Bind long roleId) + int removeRoleFromUser( + @Define Class userClass, + @Bind long userId, + @Bind String projectName, + @Bind long roleId); @SqlQuery(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = user.getClass().getSimpleName()?upper_case> + <#assign prefix = userClass.getSimpleName()?upper_case> SELECT - "PROJECT"."ID" AS "PROJECT_ID", - "PROJECT"."NAME" AS "PROJECT_NAME", - "PROJECT"."UUID" AS "PROJECT_UUID", - "ROLE"."ID" AS "ROLE_ID", - "ROLE"."NAME" AS "ROLE_NAME", - "ROLE"."UUID" AS "ROLE_UUID", - "${prefix}"."ID" AS "${prefix}_ID" - FROM "PROJECT" - INNER JOIN "${prefix}S_PROJECTS_ROLES" - ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - INNER JOIN "${prefix}" - ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" - INNER JOIN "ROLE" - ON "ROLE"."ID" = "${prefix}S_PROJECTS_ROLES"."ROLE_ID" - WHERE "${prefix}"."USERNAME" = '${user.getUsername()}' + p."ID" AS "PROJECT_ID", + p."NAME" AS "PROJECT_NAME", + p."UUID" AS "PROJECT_UUID", + r."ID" AS "ROLE_ID", + r."NAME" AS "ROLE_NAME", + r."UUID" AS "ROLE_UUID", + u."ID" AS "${prefix}_ID" + FROM "PROJECT" p + INNER JOIN "${prefix}S_PROJECTS_ROLES" pr + ON pr."PROJECT_ID" = p."ID" + INNER JOIN "${prefix}" u + ON u."ID" = pr."${prefix}_ID" + INNER JOIN "ROLE" r + ON r."ID" = pr."ROLE_ID" + WHERE u."USERNAME" = :username """) @RegisterRowMapper(ProjectRoleRowMapper.class) @DefineNamedBindings - List getUserRoles(@Define T user); + List getUserRoles(@Define Class userClass, @Bind String username); @SqlQuery(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = user.getClass().getSimpleName()?upper_case> - SELECT "PROJECT"."ID", "PROJECT"."NAME", "PROJECT"."UUID" - FROM "PROJECT" - LEFT JOIN "${prefix}S_PROJECTS_ROLES" - ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "${prefix}" - ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" - WHERE "${prefix}"."USERNAME" != '${user.getUsername()}' - OR "${prefix}"."USERNAME" IS NULL + <#assign prefix = userClass.getSimpleName()?upper_case> + SELECT p."ID", p."NAME", p."UUID" + FROM "PROJECT" p + LEFT JOIN "${prefix}S_PROJECTS_ROLES" pr + ON pr."PROJECT_ID" = p."ID" + LEFT JOIN "${prefix}" u + ON u."ID" = pr."${prefix}_ID" + WHERE u."USERNAME" != :username + OR u."USERNAME" IS NULL """) @RegisterFieldMapper(Project.class) - List getUserUnassignedProjects(@Define T user); + List getUserUnassignedProjects( + @Define Class userClass, + @Bind String username); } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java index 6c7b115152..013c5efade 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java @@ -36,8 +36,9 @@ import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; public class ProjectRoleRowMapper implements RowMapper { + public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) throws SQLException { - ProjectRole projectRole; + final ProjectRole projectRole; switch (resultSet) { case ResultSet rs when hasColumn(rs, "LDAPUSER_ID") -> projectRole = new LdapUserProjectRole(); @@ -65,4 +66,5 @@ public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) th return projectRole; } -} \ No newline at end of file + +} diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index abdaeb431a..a244b1d699 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -141,7 +141,7 @@ public Response retrieveUserProjects( try (final Handle jdbiHandle = openJdbiHandle()) { var dao = jdbiHandle.attach(RoleDao.class); - List projects = dao.getUserUnassignedProjects(principal); + List projects = dao.getUserUnassignedProjects(principal.getClass(), principal.getUsername()); if (projects == null || projects.isEmpty()) return Response.ok(List.of()).entity("No unassigned projects for specified user.").build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index cc3e3385a6..157fada21d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -146,7 +146,7 @@ public Response createRole(Role jsonRole) { failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); try (QueryManager qm = new QueryManager()) { - final Role role = qm.createRole(jsonRole.getName(), jsonRole.getPermissions()); + final Role role = qm.createRole(jsonRole.getName(), jsonRole.getPermissions().stream().toList()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role created: " + role.getName()); return Response.status(Response.Status.CREATED).entity(role).build(); diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index def41d17eb..70dff0770a 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -703,7 +703,7 @@ constraintName="PROJECT_ACCESS_TEAMS_PK"/> - + @@ -716,7 +716,7 @@ - + From 535e1019eac374b6f10b22dedd356e2ee88914a0 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 24 Mar 2025 15:24:01 -0600 Subject: [PATCH 046/181] merge corrections Signed-off-by: Allen Shearin --- .../dependencytrack/persistence/DefaultObjectGenerator.java | 2 +- .../org/dependencytrack/resources/v1/PermissionResource.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 3b934d4c80..68f4c154c9 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -298,7 +298,7 @@ private void loadDefaultPersonas(final QueryManager qm) { for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { LOGGER.debug("Creating team: " + name); - var team = qm.createTeam(name); + var team = qm.createTeam(name, false); LOGGER.debug("Assigning default permissions for team: " + name); team.setPermissions(getPermissionsByName(DEFAULT_TEAM_PERMISSIONS.get(name))); diff --git a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 3a714c09d3..ae206e2002 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -49,6 +49,7 @@ import org.owasp.security.logging.SecurityMarkers; import java.util.List; +import java.util.Set; /** * JAX-RS resources for processing permissions. @@ -254,7 +255,7 @@ public Response removePermissionFromRole( if (permission == null) { return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); } - final List permissions = role.getPermissions(); + final Set permissions = role.getPermissions(); if (permissions != null && permissions.contains(permission)) { permissions.remove(permission); role.setPermissions(permissions); @@ -298,7 +299,7 @@ public Response addPermissionToRole( if (permission == null) { return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); } - final List permissions = role.getPermissions(); + final Set permissions = role.getPermissions(); if (permissions != null && !permissions.contains(permission)) { permissions.add(permission); role.setPermissions(permissions); From c7b7f751ad9917b844adbd0d640ef7103f4712b7 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Tue, 25 Mar 2025 09:50:14 -0600 Subject: [PATCH 047/181] merge corrections Signed-off-by: Allen Shearin --- pom.xml | 2 +- .../persistence/DefaultObjectGenerator.java | 45 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 82f70c1d5a..d805c158e6 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ us.springett alpine-parent - 3.1.2 + 3.2.0-SNAPSHOT 4.0.0 diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 68f4c154c9..a8c6dfd636 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -21,6 +21,7 @@ import alpine.Config; import alpine.common.logging.Logger; import alpine.model.ConfigProperty; +import alpine.model.LdapUser; import alpine.model.ManagedUser; import alpine.model.Permission; import alpine.server.auth.PasswordService; @@ -32,6 +33,7 @@ import org.dependencytrack.common.ConfigKey; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; +import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter; @@ -294,11 +296,11 @@ private void loadDefaultPersonas(final QueryManager qm) { LOGGER.debug("Creating user: admin"); ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost", - new String(PasswordService.createHash("admin".toCharArray())), true, true, false); + new String(PasswordService.createHash("admin".toCharArray())), false, true, false); for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { LOGGER.debug("Creating team: " + name); - var team = qm.createTeam(name, false); + var team = qm.createTeam(name); LOGGER.debug("Assigning default permissions for team: " + name); team.setPermissions(getPermissionsByName(DEFAULT_TEAM_PERMISSIONS.get(name))); @@ -337,6 +339,45 @@ private void loadDefaultRoles(final QueryManager qm) { LOGGER.debug("Creating role: " + name); qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name))); } + + ManagedUser user = qm.createManagedUser("testuser", "Test User", "testuser@localhost", + new String(PasswordService.createHash("admin".toCharArray())), false, true, false); + + LdapUser ldapuser = qm.createLdapUser("Sadie"); + + var project = new Project(); + var projectAdmin = (qm.getRoles()).get(0); + project.setName("test-project"); + project.setDescription("Project for testing role schemas"); + project.setVersion("v0.1.0"); + + qm.persist(project); + + LOGGER.info("Adding role to user result #1: " + qm.addRoleToUser(user, projectAdmin, project)); + + project = new Project(); + project.setName("test-project"); + project.setDescription("Project for testing role schemas"); + project.setVersion("v0.1.1"); + + qm.persist(project); + + LOGGER.info("Adding role to user result #2: " + qm.addRoleToUser(user, projectAdmin, project)); + LOGGER.info("Has project access? " + qm.hasAccess(user, project)); + + LOGGER.info("testuser roles: " + qm.getUserRoles(user)); + LOGGER.info("admin roles: " + qm.getUserRoles(qm.getUserPrincipal("admin"))); + + LOGGER.info("Adding role to ldap user: " + qm.addRoleToUser(ldapuser, projectAdmin, project)); + + LOGGER.info("ldap user roles: " + qm.getUserRoles(ldapuser)); + LOGGER.info("admin roles: " + qm.getUserRoles(qm.getUserPrincipal("admin"))); + + LOGGER.info("testuser unassigned projects: " + qm.getUnassignedProjects(user)); + LOGGER.info("admin unassigned projects: " + qm.getUnassignedProjects("admin")); + + var unassigned = qm.getUnassignedRolePermissions(projectAdmin); + LOGGER.info("unassigned: " + unassigned.stream().map(Permission::getName).sorted().toList()); } public void loadDefaultRepositories() { From 238a7ccd3f0c5a7e97e4ec0783030964d850c270 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 27 Mar 2025 08:59:05 -0600 Subject: [PATCH 048/181] revert test changes Signed-off-by: Allen Shearin --- .../persistence/DefaultObjectGenerator.java | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index a8c6dfd636..b9264582ce 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -296,7 +296,7 @@ private void loadDefaultPersonas(final QueryManager qm) { LOGGER.debug("Creating user: admin"); ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost", - new String(PasswordService.createHash("admin".toCharArray())), false, true, false); + new String(PasswordService.createHash("admin".toCharArray())), true, true, false); for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { LOGGER.debug("Creating team: " + name); @@ -339,45 +339,6 @@ private void loadDefaultRoles(final QueryManager qm) { LOGGER.debug("Creating role: " + name); qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name))); } - - ManagedUser user = qm.createManagedUser("testuser", "Test User", "testuser@localhost", - new String(PasswordService.createHash("admin".toCharArray())), false, true, false); - - LdapUser ldapuser = qm.createLdapUser("Sadie"); - - var project = new Project(); - var projectAdmin = (qm.getRoles()).get(0); - project.setName("test-project"); - project.setDescription("Project for testing role schemas"); - project.setVersion("v0.1.0"); - - qm.persist(project); - - LOGGER.info("Adding role to user result #1: " + qm.addRoleToUser(user, projectAdmin, project)); - - project = new Project(); - project.setName("test-project"); - project.setDescription("Project for testing role schemas"); - project.setVersion("v0.1.1"); - - qm.persist(project); - - LOGGER.info("Adding role to user result #2: " + qm.addRoleToUser(user, projectAdmin, project)); - LOGGER.info("Has project access? " + qm.hasAccess(user, project)); - - LOGGER.info("testuser roles: " + qm.getUserRoles(user)); - LOGGER.info("admin roles: " + qm.getUserRoles(qm.getUserPrincipal("admin"))); - - LOGGER.info("Adding role to ldap user: " + qm.addRoleToUser(ldapuser, projectAdmin, project)); - - LOGGER.info("ldap user roles: " + qm.getUserRoles(ldapuser)); - LOGGER.info("admin roles: " + qm.getUserRoles(qm.getUserPrincipal("admin"))); - - LOGGER.info("testuser unassigned projects: " + qm.getUnassignedProjects(user)); - LOGGER.info("admin unassigned projects: " + qm.getUnassignedProjects("admin")); - - var unassigned = qm.getUnassignedRolePermissions(projectAdmin); - LOGGER.info("unassigned: " + unassigned.stream().map(Permission::getName).sorted().toList()); } public void loadDefaultRepositories() { From 44dd2a22df446fcb7760c730b57483ad87e63fbd Mon Sep 17 00:00:00 2001 From: lmphil <126618132+lmphil@users.noreply.github.com> Date: Sat, 29 Mar 2025 18:35:06 -0400 Subject: [PATCH 049/181] feat: get user project permissions (wip) (#11) * roleQueryManagerTest Signed-off-by: Philippe * update role query manager test Signed-off-by: Philippe --------- Signed-off-by: Philippe --- .../persistence/RoleQueryManagerTest.java | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java diff --git a/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java b/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java new file mode 100644 index 0000000000..c59227b733 --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java @@ -0,0 +1,171 @@ +/* + * 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.model.Project; +import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; +import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; +import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; +import org.dependencytrack.model.Role; +import org.jdbi.v3.core.Jdbi; + +import alpine.Config; +import alpine.model.ManagedUser; +import alpine.model.Permission; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.common.ConfigKey; +import org.dependencytrack.event.kafka.KafkaProducerInitializer; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +public class RoleQueryManagerTest extends PersistenceCapableTest { + + private PostgreSQLContainer postgresContainer; + private Jdbi jdbi; + + @Before + public void setUp() { + System.setProperty("javax.jdo.PersistenceManagerFactoryClass", + "org.datanucleus.api.jdo.JDOPersistenceManagerFactory"); + + postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:11-alpine")); + postgresContainer.start(); + + jdbi = Jdbi.create( + postgresContainer.getJdbcUrl(), + postgresContainer.getUsername(), + postgresContainer.getPassword()); + } + + @After + public void tearDown() { + if (postgresContainer != null) { + postgresContainer.stop(); + } + } + + // @BeforeClass + // public static void beforeClass() { + // Config.enableUnitTests(); + // } + + // @AfterClass + // public static void afterClass() { + // KafkaProducerInitializer.tearDown(); + // } + + // @Rule + // public WireMockRule wireMockRule = new WireMockRule(); + + @Test + public void testGetUserProjectPermissions() throws ParseException { + 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); + + final var testProject = new Project(); + testProject.setId(1); + testProject.setName("test-project"); + testProject.setVersion("1.0.0"); + qm.persist(testProject); + + final var readPermission = new Permission(); + readPermission.setId(1); + readPermission.setName("read"); + readPermission.setDescription("permission to read"); + qm.persist(readPermission); + + final var writePermission = new Permission(); + writePermission.setId(2); + writePermission.setName("write"); + writePermission.setDescription("permission to write"); + qm.persist(writePermission); + + List expectedPermissionsList = Arrays.asList( + readPermission, + writePermission); + + Set expectedPermissions = new HashSet<>(expectedPermissionsList); + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername("test-user"); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + qm.persist(testUser); + + final var maintainerRole = new Role(); + maintainerRole.setId(1); + maintainerRole.setName("maintainer"); + maintainerRole.setPermissions(expectedPermissions); + qm.persist(maintainerRole); + + // final var ldapUserProjectRole = new LdapUserProjectRole(); + // ldapUserProjectRole.setProject(testProject); + + final var managedUserProjectRole = new ManagedUserProjectRole(); + // managedUserProjectRole.setId(1); + managedUserProjectRole.setProject(testProject); + managedUserProjectRole.setManagedUsers(Arrays.asList(testUser)); + managedUserProjectRole.setRole(maintainerRole); + // qm.persist(managedUserProjectRole); + + // final var oidcUserProjectRole = new OidcUserProjectRole(); + + List actualPermissions = qm.getUserProjectPermissions("test-user", "test-project"); + + Assert.assertEquals(actualPermissions, expectedPermissionsList); + } + +} From 24c66a0fc00dea61f51e4b06110ae4d6c0cc41a4 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Sat, 29 Mar 2025 16:35:27 -0600 Subject: [PATCH 050/181] Revert "feat: get user project permissions (wip) (#11)" (#17) This reverts commit 44dd2a22df446fcb7760c730b57483ad87e63fbd. --- .../persistence/RoleQueryManagerTest.java | 171 ------------------ 1 file changed, 171 deletions(-) delete mode 100644 src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java diff --git a/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java b/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java deleted file mode 100644 index c59227b733..0000000000 --- a/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java +++ /dev/null @@ -1,171 +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.model.Project; -import org.dependencytrack.model.ProjectRole; -import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; -import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; -import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; -import org.dependencytrack.model.Role; -import org.jdbi.v3.core.Jdbi; - -import alpine.Config; -import alpine.model.ManagedUser; -import alpine.model.Permission; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.common.ConfigKey; -import org.dependencytrack.event.kafka.KafkaProducerInitializer; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.utility.DockerImageName; - -import com.github.tomakehurst.wiremock.junit.WireMockRule; - -public class RoleQueryManagerTest extends PersistenceCapableTest { - - private PostgreSQLContainer postgresContainer; - private Jdbi jdbi; - - @Before - public void setUp() { - System.setProperty("javax.jdo.PersistenceManagerFactoryClass", - "org.datanucleus.api.jdo.JDOPersistenceManagerFactory"); - - postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:11-alpine")); - postgresContainer.start(); - - jdbi = Jdbi.create( - postgresContainer.getJdbcUrl(), - postgresContainer.getUsername(), - postgresContainer.getPassword()); - } - - @After - public void tearDown() { - if (postgresContainer != null) { - postgresContainer.stop(); - } - } - - // @BeforeClass - // public static void beforeClass() { - // Config.enableUnitTests(); - // } - - // @AfterClass - // public static void afterClass() { - // KafkaProducerInitializer.tearDown(); - // } - - // @Rule - // public WireMockRule wireMockRule = new WireMockRule(); - - @Test - public void testGetUserProjectPermissions() throws ParseException { - 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); - - final var testProject = new Project(); - testProject.setId(1); - testProject.setName("test-project"); - testProject.setVersion("1.0.0"); - qm.persist(testProject); - - final var readPermission = new Permission(); - readPermission.setId(1); - readPermission.setName("read"); - readPermission.setDescription("permission to read"); - qm.persist(readPermission); - - final var writePermission = new Permission(); - writePermission.setId(2); - writePermission.setName("write"); - writePermission.setDescription("permission to write"); - qm.persist(writePermission); - - List expectedPermissionsList = Arrays.asList( - readPermission, - writePermission); - - Set expectedPermissions = new HashSet<>(expectedPermissionsList); - - final var testUser = new ManagedUser(); - testUser.setFullname("test user created for testing"); - testUser.setId(1); - testUser.setUsername("test-user"); - DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); - testUser.setLastPasswordChange(dateFormatter.parse("20250324")); - testUser.setPassword("password"); - qm.persist(testUser); - - final var maintainerRole = new Role(); - maintainerRole.setId(1); - maintainerRole.setName("maintainer"); - maintainerRole.setPermissions(expectedPermissions); - qm.persist(maintainerRole); - - // final var ldapUserProjectRole = new LdapUserProjectRole(); - // ldapUserProjectRole.setProject(testProject); - - final var managedUserProjectRole = new ManagedUserProjectRole(); - // managedUserProjectRole.setId(1); - managedUserProjectRole.setProject(testProject); - managedUserProjectRole.setManagedUsers(Arrays.asList(testUser)); - managedUserProjectRole.setRole(maintainerRole); - // qm.persist(managedUserProjectRole); - - // final var oidcUserProjectRole = new OidcUserProjectRole(); - - List actualPermissions = qm.getUserProjectPermissions("test-user", "test-project"); - - Assert.assertEquals(actualPermissions, expectedPermissionsList); - } - -} From be0c3ee2cc5ebb9a5a1bb1b55aaa7c20df5196c5 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 31 Mar 2025 15:41:10 -0600 Subject: [PATCH 051/181] convert roleprojectrequest to record, remove logging, dev change Signed-off-by: Allen Shearin --- pom.xml | 2 +- .../resources/v1/UserResource.java | 10 ++++---- .../resources/v1/vo/RoleProjectRequest.java | 23 +------------------ 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index d805c158e6..82f70c1d5a 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ us.springett alpine-parent - 3.2.0-SNAPSHOT + 3.1.2 4.0.0 diff --git a/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 4408efeba8..05b427a385 100644 --- a/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -805,9 +805,7 @@ public Response addRoleToUser( @Parameter(description = "Role and project information", required = true) RoleProjectRequest roleProjectRequest) { try (QueryManager qm = new QueryManager()) { - LOGGER.info("received Project info: " + roleProjectRequest.getProjectUUID()); - LOGGER.info("received Role info: " + roleProjectRequest.getRoleUUID()); - final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.getRoleUUID()); + final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.roleUUID()); if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); @@ -815,7 +813,7 @@ public Response addRoleToUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - Project project = qm.getProject(roleProjectRequest.getProjectUUID()); + Project project = qm.getProject(roleProjectRequest.projectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -854,7 +852,7 @@ public Response removeRoleFromUser( @Parameter(description = "A valid username", required = true) @PathParam("username") String username, @Parameter(description = "Role and project information", required = true) RoleProjectRequest roleProjectRequest) { try (QueryManager qm = new QueryManager()) { - final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.getRoleUUID()); + final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.roleUUID()); if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); @@ -862,7 +860,7 @@ public Response removeRoleFromUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - Project project = qm.getProject(roleProjectRequest.getProjectUUID()); + Project project = qm.getProject(roleProjectRequest.projectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java b/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java index 664410074e..28c64e517d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java +++ b/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java @@ -20,26 +20,5 @@ import org.dependencytrack.model.validation.ValidUuid; -public class RoleProjectRequest { - @ValidUuid - private String roleUUID; - - @ValidUuid - private String projectUUID; - - public String getRoleUUID() { - return roleUUID; - } - - public void setRoleUUID(String roleUUID) { - this.roleUUID = roleUUID; - } - - public String getProjectUUID() { - return projectUUID; - } - - public void setProjectUUID(String projectUUID) { - this.projectUUID = projectUUID; - } +public record RoleProjectRequest(@ValidUuid String roleUUID, @ValidUuid String projectUUID) { } From 6b9bc9387212cc76176a5759a86df350f4d05f72 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Tue, 1 Apr 2025 16:34:06 -0600 Subject: [PATCH 052/181] temporary changelog fixes Signed-off-by: Allen Shearin --- .../resources/migration/changelog-main.xml | 1 + .../migration/changelog-v5.6.0-roles.xml | 217 ++++++++++++++++++ .../resources/migration/changelog-v5.6.0.xml | 209 ----------------- 3 files changed, 218 insertions(+), 209 deletions(-) create mode 100644 src/main/resources/migration/changelog-v5.6.0-roles.xml diff --git a/src/main/resources/migration/changelog-main.xml b/src/main/resources/migration/changelog-main.xml index e55e42262c..be507a195d 100644 --- a/src/main/resources/migration/changelog-main.xml +++ b/src/main/resources/migration/changelog-main.xml @@ -13,5 +13,6 @@ + \ No newline at end of file diff --git a/src/main/resources/migration/changelog-v5.6.0-roles.xml b/src/main/resources/migration/changelog-v5.6.0-roles.xml new file mode 100644 index 0000000000..2498c87b08 --- /dev/null +++ b/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT + lpr."LDAPUSER_ID" AS "LDAPUSER_ID", + mpr."MANAGEDUSER_ID" AS "MANAGEDUSER_ID", + opr."OIDCUSER_ID" AS "OIDCUSER_ID", + pr."ID" AS "PROJECT_ID", + p."ID" AS "PERMISSION_ID", + p."NAME" AS "PERMISSION_NAME" + FROM "PERMISSION" p + INNER JOIN "ROLES_PERMISSIONS" rp + ON rp."PERMISSION_ID" = p."ID" + FULL OUTER JOIN "LDAPUSERS_PROJECTS_ROLES" lpr + ON lpr."ROLE_ID" = rp."ROLE_ID" + FULL OUTER JOIN "MANAGEDUSERS_PROJECTS_ROLES" mpr + ON lpr."PROJECT_ID" = mpr."PROJECT_ID" + AND lpr."ROLE_ID" = mpr."ROLE_ID" + FULL OUTER JOIN "OIDCUSERS_PROJECTS_ROLES" opr + ON mpr."PROJECT_ID" = opr."PROJECT_ID" + AND mpr."ROLE_ID" = opr."ROLE_ID" + INNER JOIN "PROJECT" pr + ON lpr."PROJECT_ID" = pr."ID" + OR mpr."PROJECT_ID" = pr."ID" + OR opr."PROJECT_ID" = pr."ID" + + + diff --git a/src/main/resources/migration/changelog-v5.6.0.xml b/src/main/resources/migration/changelog-v5.6.0.xml index 6c5f8cacfe..e00416c560 100644 --- a/src/main/resources/migration/changelog-v5.6.0.xml +++ b/src/main/resources/migration/changelog-v5.6.0.xml @@ -824,215 +824,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SELECT - lpr."LDAPUSER_ID" AS "LDAPUSER_ID", - mpr."MANAGEDUSER_ID" AS "MANAGEDUSER_ID", - opr."OIDCUSER_ID" AS "OIDCUSER_ID", - pr."ID" AS "PROJECT_ID", - p."ID" AS "PERMISSION_ID", - p."NAME" AS "PERMISSION_NAME" - FROM "PERMISSION" p - INNER JOIN "ROLES_PERMISSIONS" rp - ON rp."PERMISSION_ID" = p."ID" - FULL OUTER JOIN "LDAPUSERS_PROJECTS_ROLES" lpr - ON lpr."ROLE_ID" = rp."ROLE_ID" - FULL OUTER JOIN "MANAGEDUSERS_PROJECTS_ROLES" mpr - ON lpr."PROJECT_ID" = mpr."PROJECT_ID" - AND lpr."ROLE_ID" = mpr."ROLE_ID" - FULL OUTER JOIN "OIDCUSERS_PROJECTS_ROLES" opr - ON mpr."PROJECT_ID" = opr."PROJECT_ID" - AND mpr."ROLE_ID" = opr."ROLE_ID" - INNER JOIN "PROJECT" pr - ON lpr."PROJECT_ID" = pr."ID" - OR mpr."PROJECT_ID" = pr."ID" - OR opr."PROJECT_ID" = pr."ID" - - - From b13b4e836dd5c2375f7944987756e539665d64ae Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:28:50 -0500 Subject: [PATCH 053/181] refactor: add triggers to update effective permissions table with role changes (#18) Signed-off-by: Jonathan Howard --- .../migration/changelog-v5.6.0-roles.xml | 321 ++++++++++++++++-- 1 file changed, 287 insertions(+), 34 deletions(-) diff --git a/src/main/resources/migration/changelog-v5.6.0-roles.xml b/src/main/resources/migration/changelog-v5.6.0-roles.xml index 2498c87b08..fa21c426c8 100644 --- a/src/main/resources/migration/changelog-v5.6.0-roles.xml +++ b/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -95,15 +95,18 @@ - + - + - + @@ -136,15 +139,18 @@ - + - + - + @@ -154,7 +160,7 @@ uniqueConstraintName="OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX" foreignKeyName="OIDCUSERS_PROJECTS_ROLES_OIDCUSER_FK" referencedTableName="OIDCUSER" referencedColumnNames="ID" - deferrable="true" initiallyDeferred="true" deleteCascade="false" + deferrable="true" initiallyDeferred="true" deleteCascade="false" validateUnique="true" validateForeignKey="true" /> @@ -177,41 +183,288 @@ - + - + - + - - SELECT - lpr."LDAPUSER_ID" AS "LDAPUSER_ID", - mpr."MANAGEDUSER_ID" AS "MANAGEDUSER_ID", - opr."OIDCUSER_ID" AS "OIDCUSER_ID", - pr."ID" AS "PROJECT_ID", - p."ID" AS "PERMISSION_ID", - p."NAME" AS "PERMISSION_NAME" - FROM "PERMISSION" p - INNER JOIN "ROLES_PERMISSIONS" rp - ON rp."PERMISSION_ID" = p."ID" - FULL OUTER JOIN "LDAPUSERS_PROJECTS_ROLES" lpr - ON lpr."ROLE_ID" = rp."ROLE_ID" - FULL OUTER JOIN "MANAGEDUSERS_PROJECTS_ROLES" mpr - ON lpr."PROJECT_ID" = mpr."PROJECT_ID" - AND lpr."ROLE_ID" = mpr."ROLE_ID" - FULL OUTER JOIN "OIDCUSERS_PROJECTS_ROLES" opr - ON mpr."PROJECT_ID" = opr."PROJECT_ID" - AND mpr."ROLE_ID" = opr."ROLE_ID" - INNER JOIN "PROJECT" pr - ON lpr."PROJECT_ID" = pr."ID" - OR mpr."PROJECT_ID" = pr."ID" - OR opr."PROJECT_ID" = pr."ID" - + + -- Helper function to recalculate all user permissions for a project. + -- Called by trigger functions to update the values in the USER_PROJECT_EFFECTIVE_PERMISSIONS table. + CREATE OR REPLACE FUNCTION recalc_user_project_role_effective_permissions(project_ids BIGINT[]) + RETURNS void AS $$ + DECLARE + tbl_prefix TEXT; + BEGIN + -- Remove any existing effective permissions for this project + DELETE FROM "USER_PROJECT_EFFECTIVE_PERMISSIONS" + WHERE "PROJECT_ID" = ANY(project_ids); + + -- Rebuild effective permissions for all user types + FOREACH tbl_prefix IN ARRAY ARRAY['LDAP', 'MANAGED', 'OIDC'] + LOOP + EXECUTE format($query$ + INSERT INTO "USER_PROJECT_EFFECTIVE_PERMISSIONS" + (%I, "PROJECT_ID", "PERMISSION_ID", "PERMISSION_NAME") + SELECT DISTINCT upr.%I, upr."PROJECT_ID", rp."PERMISSION_ID", p."NAME" + FROM %I upr + INNER JOIN "ROLES_PERMISSIONS" rp + ON rp."ROLE_ID" = upr."ROLE_ID" + INNER JOIN "PERMISSION" p + ON p."ID" = rp."PERMISSION_ID" + WHERE upr."PROJECT_ID" = ANY($1); + $query$, + tbl_prefix || 'USER_ID', + tbl_prefix || 'USER_ID', + tbl_prefix || 'USERS_PROJECTS_ROLES') + USING project_ids; + END LOOP; + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_delete() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG(DISTINCT "ROLE_ID") + INTO role_ids + FROM old_table; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT lpr."PROJECT_ID" + FROM "LDAPUSERS_PROJECTS_ROLES" lpr + INNER JOIN old_table + ON old_table."ROLE_ID" = lpr."ROLE_ID" + UNION + SELECT mpr."PROJECT_ID" + FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr + INNER JOIN old_table + ON old_table."ROLE_ID" = mpr."ROLE_ID" + UNION + SELECT opr."PROJECT_ID" + FROM "OIDCUSERS_PROJECTS_ROLES" opr + INNER JOIN old_table + ON old_table."ROLE_ID" = opr."ROLE_ID" + ) sub; + ELSE + EXECUTE format($query$ + SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") + FROM %I AS t + WHERE t."ROLE_ID" = ANY($1) + $query$, TG_TABLE_NAME) + USING role_ids + INTO project_ids; + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_insert() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG(DISTINCT "ROLE_ID") + INTO role_ids + FROM new_table; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT lpr."PROJECT_ID" + FROM "LDAPUSERS_PROJECTS_ROLES" lpr + INNER JOIN new_table + ON new_table."ROLE_ID" = lpr."ROLE_ID" + UNION + SELECT mpr."PROJECT_ID" + FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr + INNER JOIN new_table + ON new_table."ROLE_ID" = mpr."ROLE_ID" + UNION + SELECT opr."PROJECT_ID" + FROM "OIDCUSERS_PROJECTS_ROLES" opr + INNER JOIN new_table + ON new_table."ROLE_ID" = opr."ROLE_ID" + ) sub; + ELSE + EXECUTE format($query$ + SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") + FROM %I AS t + WHERE t."ROLE_ID" = ANY($1) + $query$, TG_TABLE_NAME) + USING role_ids + INTO project_ids; + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_update() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG("ROLE_ID") + INTO role_ids + FROM ( + SELECT "ROLE_ID" FROM old_table + UNION + SELECT "ROLE_ID" FROM new_table + ) roles; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT lpr."PROJECT_ID" + FROM "LDAPUSERS_PROJECTS_ROLES" lpr + INNER JOIN new_table + ON new_table."ROLE_ID" = lpr."ROLE_ID" + FULL OUTER JOIN old_table + ON new_table."ROLE_ID" = old_table."ROLE_ID" + UNION + SELECT mpr."PROJECT_ID" + FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr + INNER JOIN new_table + ON new_table."ROLE_ID" = mpr."ROLE_ID" + FULL OUTER JOIN old_table + ON new_table."ROLE_ID" = old_table."ROLE_ID" + UNION + SELECT opr."PROJECT_ID" + FROM "OIDCUSERS_PROJECTS_ROLES" opr + INNER JOIN new_table + ON new_table."ROLE_ID" = opr."ROLE_ID" + FULL OUTER JOIN old_table + ON new_table."ROLE_ID" = old_table."ROLE_ID" + ) sub; + ELSE + EXECUTE format($query$ + SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") + FROM %I AS t + WHERE t."ROLE_ID" = ANY($1) + $query$, TG_TABLE_NAME) + USING role_ids + INTO project_ids; + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + -- INSERT trigger for LDAPUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_insert + AFTER INSERT ON "LDAPUSERS_PROJECTS_ROLES" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for LDAPUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_delete + AFTER DELETE ON "LDAPUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for LDAPUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_update + AFTER UPDATE ON "LDAPUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_update(); + + -- INSERT trigger for MANAGEDUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_insert + AFTER INSERT ON "MANAGEDUSERS_PROJECTS_ROLES" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for MANAGEDUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_delete + AFTER DELETE ON "MANAGEDUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for MANAGEDUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_update + AFTER UPDATE ON "MANAGEDUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_update(); + + -- INSERT trigger for OIDCUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_insert + AFTER INSERT ON "OIDCUSERS_PROJECTS_ROLES" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for OIDCUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_delete + AFTER DELETE ON "OIDCUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for OIDCUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_update + AFTER UPDATE ON "OIDCUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_update(); + + -- INSERT trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_insert + AFTER INSERT ON "ROLES_PERMISSIONS" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_delete + AFTER DELETE ON "ROLES_PERMISSIONS" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_update + AFTER UPDATE ON "ROLES_PERMISSIONS" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION effective_permissions_mx_on_update(); + From f20717fae44fb81b7ab936bc716b53d3c08cb5c4 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 2 Apr 2025 09:21:24 -0600 Subject: [PATCH 054/181] revert test change Signed-off-by: Allen Shearin --- .../org/dependencytrack/persistence/DefaultObjectGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 65dd9c9483..3b934d4c80 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -294,7 +294,7 @@ private void loadDefaultPersonas(final QueryManager qm) { LOGGER.debug("Creating user: admin"); ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost", - new String(PasswordService.createHash("admin".toCharArray())), false, true, false); + new String(PasswordService.createHash("admin".toCharArray())), true, true, false); for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { LOGGER.debug("Creating team: " + name); From 2dcfefa2447f9fc73508d4cdf463e2d0aa64dada Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 3 Apr 2025 10:13:45 -0600 Subject: [PATCH 055/181] refactor: add no content response to retrieveUserProjects Signed-off-by: Allen Shearin --- .../resources/v1/AccessControlResource.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index a244b1d699..b95002fc38 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -128,24 +128,29 @@ public Response retrieveProjects(@Parameter(description = "The UUID of the team headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse(responseCode = "204", description = "No unassigned projects for specified user."), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "404", description = "No unassigned projects for specified user."), + @ApiResponse(responseCode = "404", description = "User not found"), }) @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) public Response retrieveUserProjects( - @Parameter(description = "The username to retrieve projects for", - required = true) @PathParam("username") String username) { + @Parameter(description = "The username to retrieve projects for", required = true) @PathParam("username") String username) { try (QueryManager qm = new QueryManager()) { UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + try (final Handle jdbiHandle = openJdbiHandle()) { var dao = jdbiHandle.attach(RoleDao.class); List projects = dao.getUserUnassignedProjects(principal.getClass(), principal.getUsername()); - if (projects == null || projects.isEmpty()) - return Response.ok(List.of()).entity("No unassigned projects for specified user.").build(); - + if (projects == null || projects.isEmpty()) { + return Response.noContent().build(); + } + return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); } } From 5edc1e926f4693f040443b09e1996d65fb92646e Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 056/181] feat: initial GitLab integration (WIP) Signed-off-by: Jonathan Howard --- .../event/GitLabSyncEvent.java | 30 +++++ .../integrations/PermissionsSyncer.java | 30 +++++ .../integrations/gitlab/GitLabClient.java | 105 +++++++++++++++++ .../integrations/gitlab/GitLabSyncer.java | 109 ++++++++++++++++++ .../model/ConfigPropertyConstants.java | 5 + .../dependencytrack/tasks/GitLabSyncTask.java | 67 +++++++++++ 6 files changed, 346 insertions(+) create mode 100644 apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java create mode 100644 apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java create mode 100644 apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java create mode 100644 apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java create mode 100644 apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java diff --git a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java new file mode 100644 index 0000000000..251999f7d7 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java @@ -0,0 +1,30 @@ +/* + * 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.event; + +import alpine.event.framework.Event; + +/** + * Defines an event used to start a sync task of current user's GitLab groups. + * + * @author Jonathan Howard + */ +public class GitLabSyncEvent implements Event { + +} diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java new file mode 100644 index 0000000000..ae935da247 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java @@ -0,0 +1,30 @@ +/* + * 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.integrations; + +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.QueryManager; + +public interface PermissionsSyncer extends IntegrationPoint { + boolean isEnabled(); + + void setQueryManager(QueryManager qm); + + void synchronize(Project project); +} diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java new file mode 100644 index 0000000000..2f029e37cd --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -0,0 +1,105 @@ +/* + * 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.integrations.gitlab; + +import alpine.common.logging.Logger; + +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.dependencytrack.common.HttpClientPool; +import org.json.JSONArray; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; + +public class GitLabClient { + + private static final Logger LOGGER = Logger.getLogger(GitLabClient.class); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + private final GitLabSyncer syncer; + private final URL baseURL; + + public GitLabClient(final GitLabSyncer syncer, final URL baseURL) { + this.syncer = syncer; + this.baseURL = baseURL; + } + + public String buildUrl(final String appId, final String state, String redirectUri) { + try { + URIBuilder builder = new URIBuilder(redirectUri).setPath("/static/oidc-callback.html"); + redirectUri = builder.build().toString(); + + builder = new URIBuilder(this.baseURL.toString()) + .setPath("/oauth/authorize") + .addParameter("client_id", appId) + .addParameter("redirect_uri", redirectUri) + .addParameter("response_type", "code") + .addParameter("state", state) + .addParameter("scope", String.join("+", "openid", "profile", "email", "read_api")); + + if (builder.getScheme() == null || builder.getScheme().trim().isEmpty()) + builder.setScheme("https"); + + return builder.build().toString(); + } catch (URISyntaxException ex) { + syncer.handleException(LOGGER, ex); + } + + return null; + } + + public void getGitLabGroupClaims(final String token, final String appId, final String state, String redirectUri) { + LOGGER.debug("Synchronizing Dependency-Track permissions with GitLab instance"); + + String url = buildUrl(appId, state, redirectUri); + + HttpGet request = new HttpGet(url); + request.addHeader("accept", "application/json"); + request.addHeader("Authorization", "Bearer " + token); + + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + StatusLine status = response.getStatusLine(); + + if (status.getStatusCode() == HttpStatus.SC_OK) { + LOGGER.debug("Successfully synchronized GitLab permissions"); + } else { + syncer.handleUnexpectedHttpResponse(LOGGER, url, status.getStatusCode(), status.getReasonPhrase()); + } + } catch (IOException ex) { + syncer.handleException(LOGGER, ex); + } + } + + // JSONArray to ArrayList simple converter + public ArrayList jsonToList(final JSONArray jsonArray) { + ArrayList list = new ArrayList<>(); + + for (Object o : jsonArray != null ? jsonArray : Collections.emptyList()) + list.add(o.toString()); + + return list; + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java new file mode 100644 index 0000000000..acf165876b --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -0,0 +1,109 @@ +/* + * 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.integrations.gitlab; + +import alpine.common.logging.Logger; +import alpine.model.ConfigProperty; +import alpine.model.Team; +import alpine.model.UserPrincipal; +import alpine.server.auth.AuthorizationTokenCookie; +import alpine.server.auth.JsonWebToken; + +import org.dependencytrack.integrations.AbstractIntegrationPoint; +import org.dependencytrack.integrations.PermissionsSyncer; +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.UserResource; + +import java.net.URL; +import java.security.Principal; + +import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_APP_ID; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_TOKEN; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_URL; + +public class GitLabSyncer extends AbstractIntegrationPoint implements PermissionsSyncer { + + private static final Logger LOGGER = Logger.getLogger(GitLabSyncer.class); + private static final String INTEGRATIONS_GROUP = GITLAB_ENABLED.getGroupName(); + private static final String GENERAL_GROUP = GENERAL_BASE_URL.getGroupName(); + private static final String ROLE_CLAIM_PREFIX = "https://gitlab.org/claims/groups/"; + private static final String ROLE_DEVELOPER = "developer"; + private static final String ROLE_MAINTAINER = "maintainer"; + private static final String ROLE_OWNER = "owner"; + + @Override + public String name() { + return "GitLab"; + } + + @Override + public String description() { + return "Synchronizes user permissions from connected GitLab instance"; + } + + @Override + public boolean isEnabled() { + final ConfigProperty enabled = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_ENABLED.getPropertyName()); + return enabled != null && Boolean.valueOf(enabled.getPropertyValue()); + } + + @Override + public void synchronize(final Project project) { + final ConfigProperty gitLabAppId = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_APP_ID.getPropertyName()); + final ConfigProperty gitLabToken = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_TOKEN.getPropertyName()); + final ConfigProperty gitLabUrl = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_URL.getPropertyName()); + final ConfigProperty baseUrl = qm.getConfigProperty(GENERAL_GROUP, GENERAL_BASE_URL.getPropertyName()); + + UserPrincipal principal = (new UserResource()).getSelf().readEntity(UserPrincipal.class); + String token = new JsonWebToken().createToken((Principal) principal); + AuthorizationTokenCookie cookie = new AuthorizationTokenCookie(token); + String state = cookie.toString(); + + try (final QueryManager qm = new QueryManager()) { + final GitLabClient client = new GitLabClient(this, new URL(gitLabUrl.getPropertyValue())); + + // Send request to GitLab API to get OIDC user's groups + client.getGitLabGroupClaims(gitLabToken.getPropertyValue(), + gitLabAppId.getPropertyValue(), + baseUrl.getPropertyValue(), + state); + + // TODO: Get effective access for groups + + // Create team and add to project + for (String s : new String[] { ROLE_DEVELOPER, ROLE_MAINTAINER, ROLE_OWNER }) { + final String teamName = String.join("-", project.getName(), s); + + Team team = qm.getTeam(teamName); + team = team != null ? team : qm.createTeam(teamName, false); + + // TODO: set permissions for team + // team.setPermissions(null); + + project.addAccessTeam(team); + } + } catch (Exception e) { + LOGGER.error("An error occurred attempting to synchronize GitLab groups", e); + handleException(LOGGER, e); + } + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index cd2b1b20c4..474262fb0a 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -86,6 +86,11 @@ public enum ConfigPropertyConstants { FORTIFY_SSC_SYNC_CADENCE("integrations", "fortify.ssc.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), FORTIFY_SSC_URL("integrations", "fortify.ssc.url", null, PropertyType.URL, "Base URL to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), FORTIFY_SSC_TOKEN("integrations", "fortify.ssc.token", null, PropertyType.ENCRYPTEDSTRING, "The token to use to authenticate to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_ENABLED("integrations", "gitlab.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitLab integration", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_GROUPS("integrations", "gitlab.groups", "[]", PropertyType.STRING, "JSON array of GitLab group names for which to create teams/roles", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_APP_ID("integrations", "gitlab.app.id", null, PropertyType.STRING, "ID for the configured GitLab application", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_TOKEN("integrations", "gitlab.token", null, PropertyType.ENCRYPTEDSTRING, "The token (with API read permission) to use to authenticate to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_ENABLED("integrations", "defectdojo.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo integration", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_REIMPORT_ENABLED("integrations", "defectdojo.reimport.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo reimport-scan API endpoint", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_SYNC_CADENCE("integrations", "defectdojo.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to DefectDojo", ConfigPropertyAccessMode.READ_WRITE), diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java new file mode 100644 index 0000000000..59c6d6c35a --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -0,0 +1,67 @@ +/* + * 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.tasks; + +import alpine.common.logging.Logger; +import alpine.event.framework.Event; +import alpine.event.framework.LoggableSubscriber; +import alpine.model.ConfigProperty; + +import org.dependencytrack.event.GitLabSyncEvent; +import org.dependencytrack.event.kafka.KafkaEventDispatcher; +import org.dependencytrack.persistence.QueryManager; + +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_TOKEN; + +public class GitLabSyncTask implements LoggableSubscriber { + + private static final Logger LOGGER = Logger.getLogger(GitLabSyncTask.class); + private final boolean isEnabled; + private String accessToken; + + public GitLabSyncTask() { + final String groupName = GITLAB_ENABLED.getGroupName(); + + try (final QueryManager qm = new QueryManager()) { + final ConfigProperty enabled = qm.getConfigProperty(groupName, GITLAB_ENABLED.getPropertyName()); + final ConfigProperty accessToken = qm.getConfigProperty(groupName, GITLAB_TOKEN.getPropertyName()); + + this.isEnabled = enabled != null && Boolean.valueOf(enabled.getPropertyValue()); + this.accessToken = accessToken != null ? accessToken.getPropertyValue() : ""; + } + } + + /** + * {@inheritDoc} + */ + public void inform(final Event e) { + if (!(e instanceof GitLabSyncEvent && this.isEnabled)) { + return; + } + + if (this.accessToken == null) { + LOGGER.warn("GitLab syncing is enabled, but no personal access token is configured. Skipping."); + return; + } + + LOGGER.info("Starting GitLab sync task"); + new KafkaEventDispatcher().dispatchEvent(new GitLabSyncEvent()).join(); + } +} From a8918c355d5918ef901696677a9636394583b33e Mon Sep 17 00:00:00 2001 From: Patrick Kwiatkowski Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 057/181] fix: added gitlab auth class Signed-off-by: Patrick Kwiatkowski --- .../auth/GitlabAuthenticationService.java | 327 ++++++++++++++++++ .../resources/v1/UserResource.java | 4 +- pom.xml | 12 + 3 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/auth/GitlabAuthenticationService.java diff --git a/apiserver/src/main/java/org/dependencytrack/auth/GitlabAuthenticationService.java b/apiserver/src/main/java/org/dependencytrack/auth/GitlabAuthenticationService.java new file mode 100644 index 0000000000..e5751f6bb3 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/auth/GitlabAuthenticationService.java @@ -0,0 +1,327 @@ +/* + * 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.auth; + + import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +//import org.dependencytrack.persistence.QueryManager; +//import org.gitlab4j.api.GitLabApi; +//import org.gitlab4j.api.GitLabApiException; +//import org.gitlab4j.api.Pager; +//import org.gitlab4j.api.models.Project; +//import org.gitlab4j.models.Constants.TokenType; + +import com.nimbusds.openid.connect.sdk.claims.UserInfo; + +import alpine.Config; +import alpine.common.logging.Logger; +import alpine.model.OidcGroup; +import alpine.model.OidcUser; +import alpine.model.Team; +import alpine.persistence.AlpineQueryManager; +import alpine.server.auth.AlpineAuthenticationException; +import alpine.server.auth.AuthenticationService; +import alpine.server.auth.OidcConfiguration; +import alpine.server.auth.OidcConfigurationResolver; +import alpine.server.auth.OidcIdTokenAuthenticator; +import alpine.server.auth.OidcProfile; +import alpine.server.auth.OidcProfileCreator; +import alpine.server.auth.OidcUserInfoAuthenticator; +import alpine.server.util.OidcUtil; +import jakarta.annotation.Nonnull; + + /** + * @since 1.8.0 + */ + public class GitlabAuthenticationService implements AuthenticationService { + + private static final Logger LOGGER = Logger.getLogger(GitlabAuthenticationService.class); + + private final Config config; + private final OidcConfiguration oidcConfiguration; + private final OidcIdTokenAuthenticator idTokenAuthenticator; + private final OidcUserInfoAuthenticator userInfoAuthenticator; + private final String idToken; + private final String accessToken; + + /** + * @param accessToken The access token acquired by authenticating with an IdP + * @deprecated Use {@link #GitalbAuthenticationService(String, String)} instead + */ + @Deprecated + public GitlabAuthenticationService(final String accessToken) { + this(Config.getInstance(), OidcConfigurationResolver.getInstance().resolve(), null, accessToken); + } + + /** + * @param idToken The ID token acquired by authenticating with an IdP + * @param accessToken The access token acquired by authenticating with an IdP + * @since 1.10.0 + */ + public GitlabAuthenticationService(final String idToken, final String accessToken) { + this(Config.getInstance(), OidcConfigurationResolver.getInstance().resolve(), idToken, accessToken); + } + + /** + * Constructor for unit tests + */ + GitlabAuthenticationService(final Config config, final OidcConfiguration oidcConfiguration, final String idToken, final String accessToken) { + this(config, oidcConfiguration, new OidcIdTokenAuthenticator(oidcConfiguration, config.getProperty(Config.AlpineKey.OIDC_CLIENT_ID)), new OidcUserInfoAuthenticator(oidcConfiguration), idToken, accessToken); + } + + /** + * Constructor for unit tests + * + * @since 1.10.0 + */ + GitlabAuthenticationService(final Config config, + final OidcConfiguration oidcConfiguration, + final OidcIdTokenAuthenticator idTokenAuthenticator, + final OidcUserInfoAuthenticator userInfoAuthenticator, + final String idToken, + final String accessToken) { + this.config = config; + this.oidcConfiguration = oidcConfiguration; + this.idTokenAuthenticator = idTokenAuthenticator; + this.userInfoAuthenticator = userInfoAuthenticator; + this.idToken = idToken; + this.accessToken = accessToken; + } + + @Override + public boolean isSpecified() { + return OidcUtil.isOidcAvailable(config, oidcConfiguration) + && (accessToken != null || idToken != null); + } + + /** + * Authenticate a {@link Principal} using the provided credentials. + *

        + * If an ID token is provided, Alpine will validate it and source configured claims from it. + *

        + * If an access token is provided, Alpine will call the IdP's {@code /userinfo} endpoint with it + * to verify its validity, and source configured claims from the response. + *

        + * If both access token and ID token are provided, the ID token takes precedence. + * When all configured claims are found in the ID token, {@code /userinfo} won't be requested. + * When not all claims were found in the ID token, {@code /userinfo} will be requested supplementary. + * + * @return An authenticated {@link Principal} + * @throws AlpineAuthenticationException When authentication failed + */ + @Nonnull + @Override + public Principal authenticate() throws AlpineAuthenticationException { + final String usernameClaimName = config.getProperty(Config.AlpineKey.OIDC_USERNAME_CLAIM); + if (usernameClaimName == null) { + LOGGER.error("No username claim has been configured"); + throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); + } + + final boolean teamSyncEnabled = config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION); + final String teamsClaimName = config.getProperty(Config.AlpineKey.OIDC_TEAMS_CLAIM); + if (teamSyncEnabled && teamsClaimName == null) { + LOGGER.error("Team synchronization is enabled, but no teams claim has been configured"); + throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); + } + + final OidcProfileCreator profileCreator = claims -> { + final var profile = new OidcProfile(); + profile.setSubject(claims.getStringClaim(UserInfo.SUB_CLAIM_NAME)); + profile.setUsername(claims.getStringClaim(usernameClaimName)); + List devGroups = claims.getStringListClaim("https://gitlab.org/claims/groups/developer"); + List maintainerGroups = claims.getStringListClaim("https://gitlab.org/claims/groups/maintainer"); + List ownerGroups = claims.getStringListClaim("https://gitlab.org/claims/groups/owner"); + List groups = new ArrayList(); + if(devGroups != null) { + for(String group : devGroups) { + groups.add(group + "_" + "developer"); + } + } + if(maintainerGroups != null) { + for(String group : maintainerGroups) { + groups.add(group + "_" + "maintainer"); + } + } + if(ownerGroups != null) { + for(String group : ownerGroups) { + groups.add(group + "_" + "owner"); + } + } + if(!groups.isEmpty()) + profile.setGroups(groups); + profile.setEmail(claims.getStringClaim(UserInfo.EMAIL_CLAIM_NAME)); + return profile; + }; + + OidcProfile idTokenProfile = null; + if (idToken != null) { + idTokenProfile = idTokenAuthenticator.authenticate(idToken, profileCreator); + LOGGER.debug("ID token profile: " + idTokenProfile); + + if (isProfileComplete(idTokenProfile, teamSyncEnabled)) { + LOGGER.debug("ID token profile is complete, proceeding to authenticate"); + return authenticateInternal(idTokenProfile); + } + } + + OidcProfile userInfoProfile = null; + if (accessToken != null) { + userInfoProfile = userInfoAuthenticator.authenticate(accessToken, profileCreator); + LOGGER.debug("UserInfo profile: " + userInfoProfile); + + if (isProfileComplete(userInfoProfile, teamSyncEnabled)) { + LOGGER.debug("UserInfo profile is complete, proceeding to authenticate"); + return authenticateInternal(userInfoProfile); + } + } + + OidcProfile mergedProfile = null; + if (idTokenProfile != null && userInfoProfile != null) { + mergedProfile = mergeProfiles(idTokenProfile, userInfoProfile); + LOGGER.debug("Merged profile: " + mergedProfile); + + if (isProfileComplete(mergedProfile, teamSyncEnabled)) { + LOGGER.debug("Merged profile is complete, proceeding to authenticate"); + return authenticateInternal(mergedProfile); + } + } + + LOGGER.error("Unable to assemble complete profile (ID token: " + idTokenProfile + + ", UserInfo: " + userInfoProfile + ", Merged: " + mergedProfile + ")"); + throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); + } + + private OidcUser authenticateInternal(final OidcProfile profile) throws AlpineAuthenticationException { + try (final var qm = new AlpineQueryManager()) { + OidcUser user = qm.getOidcUser(profile.getUsername()); + if (user != null) { + LOGGER.debug("Attempting to authenticate user: " + user.getUsername()); + if (user.getSubjectIdentifier() == null) { + LOGGER.debug("Assigning subject identifier " + profile.getSubject() + " to user " + user.getUsername()); + user.setSubjectIdentifier(profile.getSubject()); + user.setEmail(profile.getEmail()); + return qm.updateOidcUser(user); + } else if (!user.getSubjectIdentifier().equals(profile.getSubject())) { + LOGGER.error("Refusing to authenticate user " + user.getUsername() + ": subject identifier has changed (" + + user.getSubjectIdentifier() + " to " + profile.getSubject() + ")"); + throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.INVALID_CREDENTIALS); + } + if (!Objects.equals(user.getEmail(), profile.getEmail())) { + LOGGER.debug("Updating email of user " + user.getUsername() + ": " + user.getEmail() + " -> " + profile.getEmail()); + user.setEmail(profile.getEmail()); + user = qm.updateOidcUser(user); + } + + // sync gitlab groups + syncGitlabGroups(qm, user, profile.getGroups(), accessToken); + + if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { + return qm.synchronizeTeamMembership(user, profile.getGroups()); + } + return user; + } else if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_USER_PROVISIONING)) { + LOGGER.debug("The user (" + profile.getUsername() + ") authenticated successfully but the account has not been provisioned"); + return autoProvision(qm, profile); + } else { + LOGGER.debug("The user (" + profile.getUsername() + ") is unmapped and user provisioning is not enabled"); + throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.UNMAPPED_ACCOUNT); + } + } + } + + private boolean isProfileComplete(final OidcProfile profile, final boolean teamSyncEnabled) { + return profile.getSubject() != null + && profile.getUsername() != null + && (!teamSyncEnabled || (profile.getGroups() != null)); + } + + private OidcProfile mergeProfiles(final OidcProfile left, final OidcProfile right) { + final var profile = new OidcProfile(); + profile.setSubject(selectProfileClaim(left.getSubject(), right.getSubject())); + profile.setUsername(selectProfileClaim(left.getUsername(), right.getUsername())); + profile.setGroups(selectProfileClaim(left.getGroups(), right.getGroups())); + profile.setEmail(selectProfileClaim(left.getEmail(), right.getEmail())); + return profile; + } + + private T selectProfileClaim(final T left, final T right) { + return (left != null) ? left : right; + } + + private OidcUser autoProvision(final AlpineQueryManager qm, final OidcProfile profile) { + var user = new OidcUser(); + user.setUsername(profile.getUsername()); + user.setSubjectIdentifier(profile.getSubject()); + user.setEmail(profile.getEmail()); + user = qm.persist(user); + + if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { + LOGGER.debug("Synchronizing teams for user " + user.getUsername()); + return qm.synchronizeTeamMembership(user, profile.getGroups()); + } else { + // Only apply default teams during auto-provisioning, not on later updates: + return qm.addUserToTeams(user, config.getPropertyAsList(Config.AlpineKey.OIDC_TEAMS_DEFAULT)); + } + } + + private void syncGitlabGroups(final AlpineQueryManager qm, final OidcUser user, final List groupNames, final String accessToken) { + for (final String groupName : groupNames) { + final OidcGroup group = qm.getOidcGroup(groupName); + if (group == null) { + LOGGER.debug("Unknown OpenID Connect group " + groupName); + OidcGroup createdGroup = qm.createOidcGroup(groupName); + Team createdTeam = qm.createTeam(groupName, false); + // createdTeam.setPermissions(null); + // Add team permissions + // create projects and add to team + qm.createMappedOidcGroup(createdTeam, createdGroup); + } + } + + +// GitLabApi gitLabApi = new GitLabApi("https://gitlab.us.lmco.com", TokenType.OAUTH2_ACCESS, accessToken); +// +// try (final var depTrackQueryManager = new QueryManager()) { +// +// // Get the list of projects your account has access to +// try { +// // Create a GitLabApi instance to communicate with your GitLab server +// Pager projects = gitLabApi.getProjectApi().getProjects(50); +// +// for(Project project : projects.all()) { +// depTrackQueryManager.getProject(project, null); +// } +// +// +// } catch (GitLabApiException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// } + } + + + + } + \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 249cb68d00..5336924cd2 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -33,7 +33,6 @@ import alpine.server.auth.AuthenticationNotRequired; import alpine.server.auth.Authenticator; import alpine.server.auth.JsonWebToken; -import alpine.server.auth.OidcAuthenticationService; import alpine.server.auth.PasswordService; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -60,6 +59,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.GitlabAuthenticationService; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.model.IdentifiableObject; @@ -154,7 +154,7 @@ public Response validateCredentials(@FormParam("username") String username, @For public Response validateOidcAccessToken(@Parameter(description = "An OAuth2 access token", required = true) @FormParam("idToken") final String idToken, @FormParam("accessToken") final String accessToken) { - final OidcAuthenticationService authService = new OidcAuthenticationService(idToken, accessToken); + final GitlabAuthenticationService authService = new GitlabAuthenticationService(idToken, accessToken); if (!authService.isSpecified()) { super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "An OpenID Connect login attempt was made, but OIDC is disabled or not properly configured"); diff --git a/pom.xml b/pom.xml index bc4743bedb..d1d6abf3c5 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,13 @@ + + + us.springett + alpine-parent + 3.1.2 + + 4.0.0 org.dependencytrack dependency-track-parent @@ -94,6 +101,11 @@ true + + + jitpack.io + https://jitpack.io + From 86f09136f8ffefe15a6ac6370543b12e3fac3b47 Mon Sep 17 00:00:00 2001 From: Patrick Kwiatkowski Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 058/181] fixed pom merge issue Signed-off-by: Jonathan Howard --- ....java => GitLabAuthenticationService.java} | 96 +++++++++---------- .../resources/v1/UserResource.java | 4 +- 2 files changed, 45 insertions(+), 55 deletions(-) rename apiserver/src/main/java/org/dependencytrack/auth/{GitlabAuthenticationService.java => GitLabAuthenticationService.java} (94%) diff --git a/apiserver/src/main/java/org/dependencytrack/auth/GitlabAuthenticationService.java b/apiserver/src/main/java/org/dependencytrack/auth/GitLabAuthenticationService.java similarity index 94% rename from apiserver/src/main/java/org/dependencytrack/auth/GitlabAuthenticationService.java rename to apiserver/src/main/java/org/dependencytrack/auth/GitLabAuthenticationService.java index e5751f6bb3..67ec547f33 100644 --- a/apiserver/src/main/java/org/dependencytrack/auth/GitlabAuthenticationService.java +++ b/apiserver/src/main/java/org/dependencytrack/auth/GitLabAuthenticationService.java @@ -17,20 +17,13 @@ * Copyright (c) OWASP Foundation. All Rights Reserved. */ - package org.dependencytrack.auth; +package org.dependencytrack.auth; - import java.security.Principal; +import java.security.Principal; import java.util.ArrayList; import java.util.List; import java.util.Objects; -//import org.dependencytrack.persistence.QueryManager; -//import org.gitlab4j.api.GitLabApi; -//import org.gitlab4j.api.GitLabApiException; -//import org.gitlab4j.api.Pager; -//import org.gitlab4j.api.models.Project; -//import org.gitlab4j.models.Constants.TokenType; - import com.nimbusds.openid.connect.sdk.claims.UserInfo; import alpine.Config; @@ -49,52 +42,52 @@ import alpine.server.auth.OidcUserInfoAuthenticator; import alpine.server.util.OidcUtil; import jakarta.annotation.Nonnull; - + /** * @since 1.8.0 */ - public class GitlabAuthenticationService implements AuthenticationService { - - private static final Logger LOGGER = Logger.getLogger(GitlabAuthenticationService.class); - + public class GitLabAuthenticationService implements AuthenticationService { + + private static final Logger LOGGER = Logger.getLogger(GitLabAuthenticationService.class); + private final Config config; private final OidcConfiguration oidcConfiguration; private final OidcIdTokenAuthenticator idTokenAuthenticator; private final OidcUserInfoAuthenticator userInfoAuthenticator; private final String idToken; private final String accessToken; - + /** * @param accessToken The access token acquired by authenticating with an IdP * @deprecated Use {@link #GitalbAuthenticationService(String, String)} instead */ @Deprecated - public GitlabAuthenticationService(final String accessToken) { + public GitLabAuthenticationService(final String accessToken) { this(Config.getInstance(), OidcConfigurationResolver.getInstance().resolve(), null, accessToken); } - + /** * @param idToken The ID token acquired by authenticating with an IdP * @param accessToken The access token acquired by authenticating with an IdP * @since 1.10.0 */ - public GitlabAuthenticationService(final String idToken, final String accessToken) { + public GitLabAuthenticationService(final String idToken, final String accessToken) { this(Config.getInstance(), OidcConfigurationResolver.getInstance().resolve(), idToken, accessToken); } - + /** * Constructor for unit tests */ - GitlabAuthenticationService(final Config config, final OidcConfiguration oidcConfiguration, final String idToken, final String accessToken) { + GitLabAuthenticationService(final Config config, final OidcConfiguration oidcConfiguration, final String idToken, final String accessToken) { this(config, oidcConfiguration, new OidcIdTokenAuthenticator(oidcConfiguration, config.getProperty(Config.AlpineKey.OIDC_CLIENT_ID)), new OidcUserInfoAuthenticator(oidcConfiguration), idToken, accessToken); } - + /** * Constructor for unit tests * * @since 1.10.0 */ - GitlabAuthenticationService(final Config config, + GitLabAuthenticationService(final Config config, final OidcConfiguration oidcConfiguration, final OidcIdTokenAuthenticator idTokenAuthenticator, final OidcUserInfoAuthenticator userInfoAuthenticator, @@ -107,13 +100,13 @@ public GitlabAuthenticationService(final String idToken, final String accessToke this.idToken = idToken; this.accessToken = accessToken; } - + @Override public boolean isSpecified() { return OidcUtil.isOidcAvailable(config, oidcConfiguration) && (accessToken != null || idToken != null); } - + /** * Authenticate a {@link Principal} using the provided credentials. *

        @@ -137,14 +130,14 @@ public Principal authenticate() throws AlpineAuthenticationException { LOGGER.error("No username claim has been configured"); throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); } - + final boolean teamSyncEnabled = config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION); final String teamsClaimName = config.getProperty(Config.AlpineKey.OIDC_TEAMS_CLAIM); if (teamSyncEnabled && teamsClaimName == null) { LOGGER.error("Team synchronization is enabled, but no teams claim has been configured"); throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); } - + final OidcProfileCreator profileCreator = claims -> { final var profile = new OidcProfile(); profile.setSubject(claims.getStringClaim(UserInfo.SUB_CLAIM_NAME)); @@ -173,45 +166,45 @@ public Principal authenticate() throws AlpineAuthenticationException { profile.setEmail(claims.getStringClaim(UserInfo.EMAIL_CLAIM_NAME)); return profile; }; - + OidcProfile idTokenProfile = null; if (idToken != null) { idTokenProfile = idTokenAuthenticator.authenticate(idToken, profileCreator); LOGGER.debug("ID token profile: " + idTokenProfile); - + if (isProfileComplete(idTokenProfile, teamSyncEnabled)) { LOGGER.debug("ID token profile is complete, proceeding to authenticate"); return authenticateInternal(idTokenProfile); } } - + OidcProfile userInfoProfile = null; if (accessToken != null) { userInfoProfile = userInfoAuthenticator.authenticate(accessToken, profileCreator); LOGGER.debug("UserInfo profile: " + userInfoProfile); - + if (isProfileComplete(userInfoProfile, teamSyncEnabled)) { LOGGER.debug("UserInfo profile is complete, proceeding to authenticate"); return authenticateInternal(userInfoProfile); } } - + OidcProfile mergedProfile = null; if (idTokenProfile != null && userInfoProfile != null) { mergedProfile = mergeProfiles(idTokenProfile, userInfoProfile); LOGGER.debug("Merged profile: " + mergedProfile); - + if (isProfileComplete(mergedProfile, teamSyncEnabled)) { LOGGER.debug("Merged profile is complete, proceeding to authenticate"); return authenticateInternal(mergedProfile); } } - + LOGGER.error("Unable to assemble complete profile (ID token: " + idTokenProfile + ", UserInfo: " + userInfoProfile + ", Merged: " + mergedProfile + ")"); throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); } - + private OidcUser authenticateInternal(final OidcProfile profile) throws AlpineAuthenticationException { try (final var qm = new AlpineQueryManager()) { OidcUser user = qm.getOidcUser(profile.getUsername()); @@ -232,10 +225,10 @@ private OidcUser authenticateInternal(final OidcProfile profile) throws AlpineAu user.setEmail(profile.getEmail()); user = qm.updateOidcUser(user); } - + // sync gitlab groups syncGitlabGroups(qm, user, profile.getGroups(), accessToken); - + if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { return qm.synchronizeTeamMembership(user, profile.getGroups()); } @@ -249,13 +242,13 @@ private OidcUser authenticateInternal(final OidcProfile profile) throws AlpineAu } } } - + private boolean isProfileComplete(final OidcProfile profile, final boolean teamSyncEnabled) { return profile.getSubject() != null && profile.getUsername() != null && (!teamSyncEnabled || (profile.getGroups() != null)); } - + private OidcProfile mergeProfiles(final OidcProfile left, final OidcProfile right) { final var profile = new OidcProfile(); profile.setSubject(selectProfileClaim(left.getSubject(), right.getSubject())); @@ -264,18 +257,18 @@ private OidcProfile mergeProfiles(final OidcProfile left, final OidcProfile righ profile.setEmail(selectProfileClaim(left.getEmail(), right.getEmail())); return profile; } - + private T selectProfileClaim(final T left, final T right) { return (left != null) ? left : right; } - + private OidcUser autoProvision(final AlpineQueryManager qm, final OidcProfile profile) { var user = new OidcUser(); user.setUsername(profile.getUsername()); user.setSubjectIdentifier(profile.getSubject()); user.setEmail(profile.getEmail()); user = qm.persist(user); - + if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { LOGGER.debug("Synchronizing teams for user " + user.getUsername()); return qm.synchronizeTeamMembership(user, profile.getGroups()); @@ -284,7 +277,7 @@ private OidcUser autoProvision(final AlpineQueryManager qm, final OidcProfile pr return qm.addUserToTeams(user, config.getPropertyAsList(Config.AlpineKey.OIDC_TEAMS_DEFAULT)); } } - + private void syncGitlabGroups(final AlpineQueryManager qm, final OidcUser user, final List groupNames, final String accessToken) { for (final String groupName : groupNames) { final OidcGroup group = qm.getOidcGroup(groupName); @@ -293,35 +286,32 @@ private void syncGitlabGroups(final AlpineQueryManager qm, final OidcUser user, OidcGroup createdGroup = qm.createOidcGroup(groupName); Team createdTeam = qm.createTeam(groupName, false); // createdTeam.setPermissions(null); - // Add team permissions + // Add team permissions // create projects and add to team qm.createMappedOidcGroup(createdTeam, createdGroup); } } - + // GitLabApi gitLabApi = new GitLabApi("https://gitlab.us.lmco.com", TokenType.OAUTH2_ACCESS, accessToken); -// +// // try (final var depTrackQueryManager = new QueryManager()) { -// +// // // Get the list of projects your account has access to // try { // // Create a GitLabApi instance to communicate with your GitLab server // Pager projects = gitLabApi.getProjectApi().getProjects(50); -// +// // for(Project project : projects.all()) { // depTrackQueryManager.getProject(project, null); // } -// -// +// +// // } catch (GitLabApiException e) { // // TODO Auto-generated catch block // e.printStackTrace(); // } // } } - - - + } - \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 5336924cd2..39625deda9 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -59,7 +59,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.GitlabAuthenticationService; +import org.dependencytrack.auth.GitLabAuthenticationService; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.model.IdentifiableObject; @@ -154,7 +154,7 @@ public Response validateCredentials(@FormParam("username") String username, @For public Response validateOidcAccessToken(@Parameter(description = "An OAuth2 access token", required = true) @FormParam("idToken") final String idToken, @FormParam("accessToken") final String accessToken) { - final GitlabAuthenticationService authService = new GitlabAuthenticationService(idToken, accessToken); + final GitLabAuthenticationService authService = new GitLabAuthenticationService(idToken, accessToken); if (!authService.isSpecified()) { super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "An OpenID Connect login attempt was made, but OIDC is disabled or not properly configured"); From 77ef659db9b49fd23639cd2a0adf0fc1857df2ec Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 059/181] refactor: implement customizer, register event Signed-off-by: Jonathan Howard --- .../auth/GitLabAuthenticationService.java | 317 ------------------ .../event/EventSubsystemInitializer.java | 3 + .../event/GitLabSyncEvent.java | 27 +- .../GitLabAuthenticationCustomizer.java | 41 +++ .../integrations/gitlab/GitLabClient.java | 63 +--- .../integrations/gitlab/GitLabSyncer.java | 62 +--- .../model/ConfigPropertyConstants.java | 1 - .../resources/v1/UserResource.java | 13 +- .../dependencytrack/tasks/GitLabSyncTask.java | 27 +- .../dependencytrack/tasks/TaskScheduler.java | 2 + ...e.server.auth.OidcAuthenticationCustomizer | 1 + 11 files changed, 113 insertions(+), 444 deletions(-) delete mode 100644 apiserver/src/main/java/org/dependencytrack/auth/GitLabAuthenticationService.java create mode 100644 apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java create mode 100644 apiserver/src/main/resources/META-INF/services/alpine.server.auth.OidcAuthenticationCustomizer diff --git a/apiserver/src/main/java/org/dependencytrack/auth/GitLabAuthenticationService.java b/apiserver/src/main/java/org/dependencytrack/auth/GitLabAuthenticationService.java deleted file mode 100644 index 67ec547f33..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/auth/GitLabAuthenticationService.java +++ /dev/null @@ -1,317 +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.auth; - -import java.security.Principal; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import com.nimbusds.openid.connect.sdk.claims.UserInfo; - -import alpine.Config; -import alpine.common.logging.Logger; -import alpine.model.OidcGroup; -import alpine.model.OidcUser; -import alpine.model.Team; -import alpine.persistence.AlpineQueryManager; -import alpine.server.auth.AlpineAuthenticationException; -import alpine.server.auth.AuthenticationService; -import alpine.server.auth.OidcConfiguration; -import alpine.server.auth.OidcConfigurationResolver; -import alpine.server.auth.OidcIdTokenAuthenticator; -import alpine.server.auth.OidcProfile; -import alpine.server.auth.OidcProfileCreator; -import alpine.server.auth.OidcUserInfoAuthenticator; -import alpine.server.util.OidcUtil; -import jakarta.annotation.Nonnull; - - /** - * @since 1.8.0 - */ - public class GitLabAuthenticationService implements AuthenticationService { - - private static final Logger LOGGER = Logger.getLogger(GitLabAuthenticationService.class); - - private final Config config; - private final OidcConfiguration oidcConfiguration; - private final OidcIdTokenAuthenticator idTokenAuthenticator; - private final OidcUserInfoAuthenticator userInfoAuthenticator; - private final String idToken; - private final String accessToken; - - /** - * @param accessToken The access token acquired by authenticating with an IdP - * @deprecated Use {@link #GitalbAuthenticationService(String, String)} instead - */ - @Deprecated - public GitLabAuthenticationService(final String accessToken) { - this(Config.getInstance(), OidcConfigurationResolver.getInstance().resolve(), null, accessToken); - } - - /** - * @param idToken The ID token acquired by authenticating with an IdP - * @param accessToken The access token acquired by authenticating with an IdP - * @since 1.10.0 - */ - public GitLabAuthenticationService(final String idToken, final String accessToken) { - this(Config.getInstance(), OidcConfigurationResolver.getInstance().resolve(), idToken, accessToken); - } - - /** - * Constructor for unit tests - */ - GitLabAuthenticationService(final Config config, final OidcConfiguration oidcConfiguration, final String idToken, final String accessToken) { - this(config, oidcConfiguration, new OidcIdTokenAuthenticator(oidcConfiguration, config.getProperty(Config.AlpineKey.OIDC_CLIENT_ID)), new OidcUserInfoAuthenticator(oidcConfiguration), idToken, accessToken); - } - - /** - * Constructor for unit tests - * - * @since 1.10.0 - */ - GitLabAuthenticationService(final Config config, - final OidcConfiguration oidcConfiguration, - final OidcIdTokenAuthenticator idTokenAuthenticator, - final OidcUserInfoAuthenticator userInfoAuthenticator, - final String idToken, - final String accessToken) { - this.config = config; - this.oidcConfiguration = oidcConfiguration; - this.idTokenAuthenticator = idTokenAuthenticator; - this.userInfoAuthenticator = userInfoAuthenticator; - this.idToken = idToken; - this.accessToken = accessToken; - } - - @Override - public boolean isSpecified() { - return OidcUtil.isOidcAvailable(config, oidcConfiguration) - && (accessToken != null || idToken != null); - } - - /** - * Authenticate a {@link Principal} using the provided credentials. - *

        - * If an ID token is provided, Alpine will validate it and source configured claims from it. - *

        - * If an access token is provided, Alpine will call the IdP's {@code /userinfo} endpoint with it - * to verify its validity, and source configured claims from the response. - *

        - * If both access token and ID token are provided, the ID token takes precedence. - * When all configured claims are found in the ID token, {@code /userinfo} won't be requested. - * When not all claims were found in the ID token, {@code /userinfo} will be requested supplementary. - * - * @return An authenticated {@link Principal} - * @throws AlpineAuthenticationException When authentication failed - */ - @Nonnull - @Override - public Principal authenticate() throws AlpineAuthenticationException { - final String usernameClaimName = config.getProperty(Config.AlpineKey.OIDC_USERNAME_CLAIM); - if (usernameClaimName == null) { - LOGGER.error("No username claim has been configured"); - throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); - } - - final boolean teamSyncEnabled = config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION); - final String teamsClaimName = config.getProperty(Config.AlpineKey.OIDC_TEAMS_CLAIM); - if (teamSyncEnabled && teamsClaimName == null) { - LOGGER.error("Team synchronization is enabled, but no teams claim has been configured"); - throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); - } - - final OidcProfileCreator profileCreator = claims -> { - final var profile = new OidcProfile(); - profile.setSubject(claims.getStringClaim(UserInfo.SUB_CLAIM_NAME)); - profile.setUsername(claims.getStringClaim(usernameClaimName)); - List devGroups = claims.getStringListClaim("https://gitlab.org/claims/groups/developer"); - List maintainerGroups = claims.getStringListClaim("https://gitlab.org/claims/groups/maintainer"); - List ownerGroups = claims.getStringListClaim("https://gitlab.org/claims/groups/owner"); - List groups = new ArrayList(); - if(devGroups != null) { - for(String group : devGroups) { - groups.add(group + "_" + "developer"); - } - } - if(maintainerGroups != null) { - for(String group : maintainerGroups) { - groups.add(group + "_" + "maintainer"); - } - } - if(ownerGroups != null) { - for(String group : ownerGroups) { - groups.add(group + "_" + "owner"); - } - } - if(!groups.isEmpty()) - profile.setGroups(groups); - profile.setEmail(claims.getStringClaim(UserInfo.EMAIL_CLAIM_NAME)); - return profile; - }; - - OidcProfile idTokenProfile = null; - if (idToken != null) { - idTokenProfile = idTokenAuthenticator.authenticate(idToken, profileCreator); - LOGGER.debug("ID token profile: " + idTokenProfile); - - if (isProfileComplete(idTokenProfile, teamSyncEnabled)) { - LOGGER.debug("ID token profile is complete, proceeding to authenticate"); - return authenticateInternal(idTokenProfile); - } - } - - OidcProfile userInfoProfile = null; - if (accessToken != null) { - userInfoProfile = userInfoAuthenticator.authenticate(accessToken, profileCreator); - LOGGER.debug("UserInfo profile: " + userInfoProfile); - - if (isProfileComplete(userInfoProfile, teamSyncEnabled)) { - LOGGER.debug("UserInfo profile is complete, proceeding to authenticate"); - return authenticateInternal(userInfoProfile); - } - } - - OidcProfile mergedProfile = null; - if (idTokenProfile != null && userInfoProfile != null) { - mergedProfile = mergeProfiles(idTokenProfile, userInfoProfile); - LOGGER.debug("Merged profile: " + mergedProfile); - - if (isProfileComplete(mergedProfile, teamSyncEnabled)) { - LOGGER.debug("Merged profile is complete, proceeding to authenticate"); - return authenticateInternal(mergedProfile); - } - } - - LOGGER.error("Unable to assemble complete profile (ID token: " + idTokenProfile + - ", UserInfo: " + userInfoProfile + ", Merged: " + mergedProfile + ")"); - throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.OTHER); - } - - private OidcUser authenticateInternal(final OidcProfile profile) throws AlpineAuthenticationException { - try (final var qm = new AlpineQueryManager()) { - OidcUser user = qm.getOidcUser(profile.getUsername()); - if (user != null) { - LOGGER.debug("Attempting to authenticate user: " + user.getUsername()); - if (user.getSubjectIdentifier() == null) { - LOGGER.debug("Assigning subject identifier " + profile.getSubject() + " to user " + user.getUsername()); - user.setSubjectIdentifier(profile.getSubject()); - user.setEmail(profile.getEmail()); - return qm.updateOidcUser(user); - } else if (!user.getSubjectIdentifier().equals(profile.getSubject())) { - LOGGER.error("Refusing to authenticate user " + user.getUsername() + ": subject identifier has changed (" + - user.getSubjectIdentifier() + " to " + profile.getSubject() + ")"); - throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.INVALID_CREDENTIALS); - } - if (!Objects.equals(user.getEmail(), profile.getEmail())) { - LOGGER.debug("Updating email of user " + user.getUsername() + ": " + user.getEmail() + " -> " + profile.getEmail()); - user.setEmail(profile.getEmail()); - user = qm.updateOidcUser(user); - } - - // sync gitlab groups - syncGitlabGroups(qm, user, profile.getGroups(), accessToken); - - if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { - return qm.synchronizeTeamMembership(user, profile.getGroups()); - } - return user; - } else if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_USER_PROVISIONING)) { - LOGGER.debug("The user (" + profile.getUsername() + ") authenticated successfully but the account has not been provisioned"); - return autoProvision(qm, profile); - } else { - LOGGER.debug("The user (" + profile.getUsername() + ") is unmapped and user provisioning is not enabled"); - throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.UNMAPPED_ACCOUNT); - } - } - } - - private boolean isProfileComplete(final OidcProfile profile, final boolean teamSyncEnabled) { - return profile.getSubject() != null - && profile.getUsername() != null - && (!teamSyncEnabled || (profile.getGroups() != null)); - } - - private OidcProfile mergeProfiles(final OidcProfile left, final OidcProfile right) { - final var profile = new OidcProfile(); - profile.setSubject(selectProfileClaim(left.getSubject(), right.getSubject())); - profile.setUsername(selectProfileClaim(left.getUsername(), right.getUsername())); - profile.setGroups(selectProfileClaim(left.getGroups(), right.getGroups())); - profile.setEmail(selectProfileClaim(left.getEmail(), right.getEmail())); - return profile; - } - - private T selectProfileClaim(final T left, final T right) { - return (left != null) ? left : right; - } - - private OidcUser autoProvision(final AlpineQueryManager qm, final OidcProfile profile) { - var user = new OidcUser(); - user.setUsername(profile.getUsername()); - user.setSubjectIdentifier(profile.getSubject()); - user.setEmail(profile.getEmail()); - user = qm.persist(user); - - if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { - LOGGER.debug("Synchronizing teams for user " + user.getUsername()); - return qm.synchronizeTeamMembership(user, profile.getGroups()); - } else { - // Only apply default teams during auto-provisioning, not on later updates: - return qm.addUserToTeams(user, config.getPropertyAsList(Config.AlpineKey.OIDC_TEAMS_DEFAULT)); - } - } - - private void syncGitlabGroups(final AlpineQueryManager qm, final OidcUser user, final List groupNames, final String accessToken) { - for (final String groupName : groupNames) { - final OidcGroup group = qm.getOidcGroup(groupName); - if (group == null) { - LOGGER.debug("Unknown OpenID Connect group " + groupName); - OidcGroup createdGroup = qm.createOidcGroup(groupName); - Team createdTeam = qm.createTeam(groupName, false); - // createdTeam.setPermissions(null); - // Add team permissions - // create projects and add to team - qm.createMappedOidcGroup(createdTeam, createdGroup); - } - } - - -// GitLabApi gitLabApi = new GitLabApi("https://gitlab.us.lmco.com", TokenType.OAUTH2_ACCESS, accessToken); -// -// try (final var depTrackQueryManager = new QueryManager()) { -// -// // Get the list of projects your account has access to -// try { -// // Create a GitLabApi instance to communicate with your GitLab server -// Pager projects = gitLabApi.getProjectApi().getProjects(50); -// -// for(Project project : projects.all()) { -// depTrackQueryManager.getProject(project, null); -// } -// -// -// } catch (GitLabApiException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); -// } -// } - } - - } diff --git a/apiserver/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/apiserver/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index 4fae1bf1ee..d7d1528c9b 100644 --- a/apiserver/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/apiserver/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -40,6 +40,7 @@ import org.dependencytrack.tasks.EpssMirrorTask; import org.dependencytrack.tasks.FortifySscUploadTask; import org.dependencytrack.tasks.GitHubAdvisoryMirrorTask; +import org.dependencytrack.tasks.GitLabSyncTask; import org.dependencytrack.tasks.IntegrityAnalysisTask; import org.dependencytrack.tasks.IntegrityMetaInitializerTask; import org.dependencytrack.tasks.InternalComponentIdentificationTask; @@ -96,6 +97,7 @@ public void contextInitialized(final ServletContextEvent event) { EVENT_SERVICE.subscribe(VexUploadEvent.class, VexUploadProcessingTask.class); EVENT_SERVICE.subscribe(LdapSyncEvent.class, LdapSyncTaskWrapper.class); EVENT_SERVICE.subscribe(GitHubAdvisoryMirrorEvent.class, GitHubAdvisoryMirrorTask.class); + EVENT_SERVICE.subscribe(GitLabSyncEvent.class, GitLabSyncTask.class); EVENT_SERVICE.subscribe(OsvMirrorEvent.class, OsvMirrorTask.class); EVENT_SERVICE.subscribe(ProjectVulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class); EVENT_SERVICE.subscribe(PortfolioVulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class); @@ -143,6 +145,7 @@ public void contextDestroyed(final ServletContextEvent event) { EVENT_SERVICE.unsubscribe(VexUploadProcessingTask.class); EVENT_SERVICE.unsubscribe(LdapSyncTaskWrapper.class); EVENT_SERVICE.unsubscribe(GitHubAdvisoryMirrorTask.class); + EVENT_SERVICE.unsubscribe(GitLabSyncTask.class); EVENT_SERVICE.unsubscribe(OsvMirrorTask.class); EVENT_SERVICE.unsubscribe(VulnerabilityAnalysisTask.class); EVENT_SERVICE.unsubscribe(RepositoryMetaAnalysisTask.class); diff --git a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java index 251999f7d7..79a34d8524 100644 --- a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java +++ b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java @@ -18,13 +18,36 @@ */ package org.dependencytrack.event; -import alpine.event.framework.Event; +import alpine.event.framework.UnblockedEvent; /** * Defines an event used to start a sync task of current user's GitLab groups. * * @author Jonathan Howard */ -public class GitLabSyncEvent implements Event { +public class GitLabSyncEvent implements UnblockedEvent { + + private String accessToken; + + public GitLabSyncEvent() { + + } + + public GitLabSyncEvent(final String accessToken) { + this.accessToken = accessToken; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(final String accessToken) { + this.accessToken = accessToken; + } + + @Override + public String toString() { + return "%s{accessToken=%s}".formatted(getClass().getName(), accessToken); + } } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java new file mode 100644 index 0000000000..b719aaafbd --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -0,0 +1,41 @@ +/* + * 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.integrations.gitlab; + +import org.dependencytrack.event.GitLabSyncEvent; +import org.dependencytrack.event.kafka.KafkaEventDispatcher; + +import alpine.server.auth.DefaultOidcAuthenticationCustomizer; +import alpine.server.auth.OidcAuthenticationCustomizer; +import alpine.server.auth.OidcProfile; + +public class GitLabAuthenticationCustomizer + extends DefaultOidcAuthenticationCustomizer + implements OidcAuthenticationCustomizer { + + public GitLabAuthenticationCustomizer() { + + } + + @Override + public void onAuthenticationSuccess(OidcProfile profile, String idToken, String accessToken) { + new KafkaEventDispatcher().dispatchEvent(new GitLabSyncEvent(accessToken)).join(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index 2f029e37cd..29e7b84a1d 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -18,23 +18,15 @@ */ package org.dependencytrack.integrations.gitlab; -import alpine.common.logging.Logger; - -import org.apache.http.HttpStatus; -import org.apache.http.StatusLine; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URIBuilder; -import org.dependencytrack.common.HttpClientPool; -import org.json.JSONArray; - -import java.io.IOException; -import java.net.URISyntaxException; import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; +import org.json.JSONArray; + +import alpine.common.logging.Logger; + public class GitLabClient { private static final Logger LOGGER = Logger.getLogger(GitLabClient.class); @@ -47,52 +39,6 @@ public GitLabClient(final GitLabSyncer syncer, final URL baseURL) { this.baseURL = baseURL; } - public String buildUrl(final String appId, final String state, String redirectUri) { - try { - URIBuilder builder = new URIBuilder(redirectUri).setPath("/static/oidc-callback.html"); - redirectUri = builder.build().toString(); - - builder = new URIBuilder(this.baseURL.toString()) - .setPath("/oauth/authorize") - .addParameter("client_id", appId) - .addParameter("redirect_uri", redirectUri) - .addParameter("response_type", "code") - .addParameter("state", state) - .addParameter("scope", String.join("+", "openid", "profile", "email", "read_api")); - - if (builder.getScheme() == null || builder.getScheme().trim().isEmpty()) - builder.setScheme("https"); - - return builder.build().toString(); - } catch (URISyntaxException ex) { - syncer.handleException(LOGGER, ex); - } - - return null; - } - - public void getGitLabGroupClaims(final String token, final String appId, final String state, String redirectUri) { - LOGGER.debug("Synchronizing Dependency-Track permissions with GitLab instance"); - - String url = buildUrl(appId, state, redirectUri); - - HttpGet request = new HttpGet(url); - request.addHeader("accept", "application/json"); - request.addHeader("Authorization", "Bearer " + token); - - try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { - StatusLine status = response.getStatusLine(); - - if (status.getStatusCode() == HttpStatus.SC_OK) { - LOGGER.debug("Successfully synchronized GitLab permissions"); - } else { - syncer.handleUnexpectedHttpResponse(LOGGER, url, status.getStatusCode(), status.getReasonPhrase()); - } - } catch (IOException ex) { - syncer.handleException(LOGGER, ex); - } - } - // JSONArray to ArrayList simple converter public ArrayList jsonToList(final JSONArray jsonArray) { ArrayList list = new ArrayList<>(); @@ -102,4 +48,5 @@ public ArrayList jsonToList(final JSONArray jsonArray) { return list; } + } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index acf165876b..ec84cf9af4 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -20,25 +20,13 @@ import alpine.common.logging.Logger; import alpine.model.ConfigProperty; -import alpine.model.Team; -import alpine.model.UserPrincipal; -import alpine.server.auth.AuthorizationTokenCookie; -import alpine.server.auth.JsonWebToken; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PermissionsSyncer; import org.dependencytrack.model.Project; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.resources.v1.UserResource; - -import java.net.URL; -import java.security.Principal; import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL; -import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_APP_ID; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; -import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_TOKEN; -import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_URL; public class GitLabSyncer extends AbstractIntegrationPoint implements PermissionsSyncer { @@ -50,6 +38,16 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private static final String ROLE_MAINTAINER = "maintainer"; private static final String ROLE_OWNER = "owner"; + private final String accessToken; + + public GitLabSyncer(final String accessToken) { + this.accessToken = accessToken; + } + + public String getAccessToken() { + return accessToken; + } + @Override public String name() { return "GitLab"; @@ -63,47 +61,13 @@ public String description() { @Override public boolean isEnabled() { final ConfigProperty enabled = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_ENABLED.getPropertyName()); - return enabled != null && Boolean.valueOf(enabled.getPropertyValue()); + + return enabled != null && Boolean.parseBoolean(enabled.getPropertyValue()); } @Override public void synchronize(final Project project) { - final ConfigProperty gitLabAppId = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_APP_ID.getPropertyName()); - final ConfigProperty gitLabToken = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_TOKEN.getPropertyName()); - final ConfigProperty gitLabUrl = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_URL.getPropertyName()); - final ConfigProperty baseUrl = qm.getConfigProperty(GENERAL_GROUP, GENERAL_BASE_URL.getPropertyName()); - - UserPrincipal principal = (new UserResource()).getSelf().readEntity(UserPrincipal.class); - String token = new JsonWebToken().createToken((Principal) principal); - AuthorizationTokenCookie cookie = new AuthorizationTokenCookie(token); - String state = cookie.toString(); - try (final QueryManager qm = new QueryManager()) { - final GitLabClient client = new GitLabClient(this, new URL(gitLabUrl.getPropertyValue())); - - // Send request to GitLab API to get OIDC user's groups - client.getGitLabGroupClaims(gitLabToken.getPropertyValue(), - gitLabAppId.getPropertyValue(), - baseUrl.getPropertyValue(), - state); - - // TODO: Get effective access for groups - - // Create team and add to project - for (String s : new String[] { ROLE_DEVELOPER, ROLE_MAINTAINER, ROLE_OWNER }) { - final String teamName = String.join("-", project.getName(), s); - - Team team = qm.getTeam(teamName); - team = team != null ? team : qm.createTeam(teamName, false); - - // TODO: set permissions for team - // team.setPermissions(null); - - project.addAccessTeam(team); - } - } catch (Exception e) { - LOGGER.error("An error occurred attempting to synchronize GitLab groups", e); - handleException(LOGGER, e); - } } + } diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 474262fb0a..23d271cf4c 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -90,7 +90,6 @@ public enum ConfigPropertyConstants { GITLAB_GROUPS("integrations", "gitlab.groups", "[]", PropertyType.STRING, "JSON array of GitLab group names for which to create teams/roles", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), GITLAB_APP_ID("integrations", "gitlab.app.id", null, PropertyType.STRING, "ID for the configured GitLab application", ConfigPropertyAccessMode.READ_WRITE), - GITLAB_TOKEN("integrations", "gitlab.token", null, PropertyType.ENCRYPTEDSTRING, "The token (with API read permission) to use to authenticate to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_ENABLED("integrations", "defectdojo.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo integration", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_REIMPORT_ENABLED("integrations", "defectdojo.reimport.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo reimport-scan API endpoint", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_SYNC_CADENCE("integrations", "defectdojo.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to DefectDojo", ConfigPropertyAccessMode.READ_WRITE), diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 39625deda9..dedbc0e367 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -33,6 +33,7 @@ import alpine.server.auth.AuthenticationNotRequired; import alpine.server.auth.Authenticator; import alpine.server.auth.JsonWebToken; +import alpine.server.auth.OidcAuthenticationService; import alpine.server.auth.PasswordService; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -59,7 +60,6 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.GitLabAuthenticationService; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.model.IdentifiableObject; @@ -154,7 +154,7 @@ public Response validateCredentials(@FormParam("username") String username, @For public Response validateOidcAccessToken(@Parameter(description = "An OAuth2 access token", required = true) @FormParam("idToken") final String idToken, @FormParam("accessToken") final String accessToken) { - final GitLabAuthenticationService authService = new GitLabAuthenticationService(idToken, accessToken); + final OidcAuthenticationService authService = new OidcAuthenticationService(idToken, accessToken); if (!authService.isSpecified()) { super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "An OpenID Connect login attempt was made, but OIDC is disabled or not properly configured"); @@ -164,10 +164,11 @@ public Response validateOidcAccessToken(@Parameter(description = "An OAuth2 acce try (final QueryManager qm = new QueryManager()) { final Principal principal = authService.authenticate(); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_SUCCESS, "Successful OpenID Connect login / username: " + principal.getName()); + final List permissions = qm.getEffectivePermissions((UserPrincipal) principal); - final KeyManager km = KeyManager.getInstance(); - final JsonWebToken jwt = new JsonWebToken(km.getSecretKey()); + final JsonWebToken jwt = new JsonWebToken(); final String token = jwt.createToken(principal, permissions); + return Response.ok(token).build(); } catch (AlpineAuthenticationException e) { super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_FAILURE, "Unauthorized OpenID Connect login attempt"); @@ -207,8 +208,8 @@ public Response forceChangePassword(@FormParam("username") String username, @For throw new AlpineAuthenticationException(e.getCauseType()); } } - if (principal instanceof ManagedUser) { - final ManagedUser user = qm.getManagedUser(((ManagedUser) principal).getUsername()); + if (principal instanceof ManagedUser managedUser) { + final ManagedUser user = qm.getManagedUser(managedUser.getUsername()); if (StringUtils.isNotBlank(newPassword) && StringUtils.isNotBlank(confirmPassword) && newPassword.equals(confirmPassword)) { if (PasswordService.matches(newPassword.toCharArray(), user)) { super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_FAILURE, "Existing password is the same as new password. Password not changed. / username: " + username); diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index 59c6d6c35a..183b92e60d 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -24,44 +24,49 @@ import alpine.model.ConfigProperty; import org.dependencytrack.event.GitLabSyncEvent; -import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.persistence.QueryManager; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; -import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_TOKEN; public class GitLabSyncTask implements LoggableSubscriber { private static final Logger LOGGER = Logger.getLogger(GitLabSyncTask.class); private final boolean isEnabled; - private String accessToken; + private final String accessToken; - public GitLabSyncTask() { + public GitLabSyncTask(final String accessToken) { final String groupName = GITLAB_ENABLED.getGroupName(); try (final QueryManager qm = new QueryManager()) { final ConfigProperty enabled = qm.getConfigProperty(groupName, GITLAB_ENABLED.getPropertyName()); - final ConfigProperty accessToken = qm.getConfigProperty(groupName, GITLAB_TOKEN.getPropertyName()); - this.isEnabled = enabled != null && Boolean.valueOf(enabled.getPropertyValue()); - this.accessToken = accessToken != null ? accessToken.getPropertyValue() : ""; + this.isEnabled = enabled != null && Boolean.parseBoolean(enabled.getPropertyValue()); + this.accessToken = accessToken; } } /** * {@inheritDoc} */ - public void inform(final Event e) { - if (!(e instanceof GitLabSyncEvent && this.isEnabled)) { + @Override + public void inform(final Event event) { + if (!(event instanceof GitLabSyncEvent && this.isEnabled)) { return; } if (this.accessToken == null) { - LOGGER.warn("GitLab syncing is enabled, but no personal access token is configured. Skipping."); + LOGGER.warn("GitLab syncing is enabled, but no access token was provided. Skipping."); return; } LOGGER.info("Starting GitLab sync task"); - new KafkaEventDispatcher().dispatchEvent(new GitLabSyncEvent()).join(); + + // TODO: Get user GitLab project memberships (use alpine.security.crypto.DataEncryption for request) + // TODO: Create Dependency-Track hierarchical project structure for user's GitLab projects + // TODO: Create Dependency-Track teams such as -maintainer + // TODO: Assign Dependency-Track permissions (TBD) to teams based on GitLab role + // TODO: Map user OIDC groups to Dependency-Track teams + // TODO: Configure portfolio access control } + } diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/TaskScheduler.java b/apiserver/src/main/java/org/dependencytrack/tasks/TaskScheduler.java index 708872c4a4..1f71d25b41 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/TaskScheduler.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/TaskScheduler.java @@ -26,6 +26,7 @@ import org.dependencytrack.event.EpssMirrorEvent; import org.dependencytrack.event.FortifySscUploadEventAbstract; import org.dependencytrack.event.GitHubAdvisoryMirrorEvent; +import org.dependencytrack.event.GitLabSyncEvent; import org.dependencytrack.event.IntegrityMetaInitializerEvent; import org.dependencytrack.event.InternalComponentIdentificationEvent; import org.dependencytrack.event.KennaSecurityUploadEventAbstract; @@ -85,6 +86,7 @@ private TaskScheduler() { Map.entry(new NistMirrorEvent(), getCronScheduleForTask(NistMirrorTask.class)), Map.entry(new OsvMirrorEvent(null), getCronScheduleForTask(OsvMirrorTask.class)), Map.entry(new GitHubAdvisoryMirrorEvent(), getCronScheduleForTask(GitHubAdvisoryMirrorTask.class)), + Map.entry(new GitLabSyncEvent(), getCronScheduleForTask(GitLabSyncTask.class)), Map.entry(new EpssMirrorEvent(), getCronScheduleForTask(EpssMirrorTask.class)), Map.entry(new PortfolioMetricsUpdateEvent(), getCronScheduleForTask(PortfolioMetricsUpdateTask.class)), Map.entry(new VulnerabilityMetricsUpdateEvent(), getCronScheduleForTask(VulnerabilityMetricsUpdateTask.class)), diff --git a/apiserver/src/main/resources/META-INF/services/alpine.server.auth.OidcAuthenticationCustomizer b/apiserver/src/main/resources/META-INF/services/alpine.server.auth.OidcAuthenticationCustomizer new file mode 100644 index 0000000000..c1b46c168e --- /dev/null +++ b/apiserver/src/main/resources/META-INF/services/alpine.server.auth.OidcAuthenticationCustomizer @@ -0,0 +1 @@ +org.dependencytrack.integrations.gitlab.GitLabAuthenticationCustomizer \ No newline at end of file From 5506eb3ec5f0ecc6151fcb9f086b27177b10f49e Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 060/181] refactor: remove token class member from sync task Signed-off-by: Jonathan Howard --- .../org/dependencytrack/tasks/GitLabSyncTask.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index 183b92e60d..bc9bd5fccc 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -32,16 +32,14 @@ public class GitLabSyncTask implements LoggableSubscriber { private static final Logger LOGGER = Logger.getLogger(GitLabSyncTask.class); private final boolean isEnabled; - private final String accessToken; - public GitLabSyncTask(final String accessToken) { + public GitLabSyncTask() { final String groupName = GITLAB_ENABLED.getGroupName(); try (final QueryManager qm = new QueryManager()) { final ConfigProperty enabled = qm.getConfigProperty(groupName, GITLAB_ENABLED.getPropertyName()); this.isEnabled = enabled != null && Boolean.parseBoolean(enabled.getPropertyValue()); - this.accessToken = accessToken; } } @@ -54,13 +52,18 @@ public void inform(final Event event) { return; } - if (this.accessToken == null) { + GitLabSyncEvent gitLabSyncEvent = (GitLabSyncEvent) event; + String accessToken = gitLabSyncEvent.getAccessToken(); + + if (accessToken == null || accessToken.isEmpty()) { LOGGER.warn("GitLab syncing is enabled, but no access token was provided. Skipping."); return; } LOGGER.info("Starting GitLab sync task"); + GitLabSyncer syncer = new GitLabSyncer(accessToken); + // TODO: Get user GitLab project memberships (use alpine.security.crypto.DataEncryption for request) // TODO: Create Dependency-Track hierarchical project structure for user's GitLab projects // TODO: Create Dependency-Track teams such as -maintainer From db1457d811a97e7eef0ea70f8eef9e86548949e0 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 061/181] refactor: make user available to syncer Signed-off-by: Jonathan Howard --- .../event/GitLabSyncEvent.java | 15 ++- .../integrations/PermissionsSyncer.java | 7 +- .../GitLabAuthenticationCustomizer.java | 7 +- .../integrations/gitlab/GitLabSyncer.java | 125 +++++++++++++++++- .../integrations/gitlab/query.graphql | 15 +++ .../dependencytrack/tasks/GitLabSyncTask.java | 26 +++- 6 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/integrations/gitlab/query.graphql diff --git a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java index 79a34d8524..a87c84df32 100644 --- a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java +++ b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java @@ -19,6 +19,7 @@ package org.dependencytrack.event; import alpine.event.framework.UnblockedEvent; +import alpine.model.OidcUser; /** * Defines an event used to start a sync task of current user's GitLab groups. @@ -28,13 +29,15 @@ public class GitLabSyncEvent implements UnblockedEvent { private String accessToken; + private OidcUser user; public GitLabSyncEvent() { } - public GitLabSyncEvent(final String accessToken) { + public GitLabSyncEvent(final String accessToken, final OidcUser user) { this.accessToken = accessToken; + this.user = user; } public String getAccessToken() { @@ -45,9 +48,17 @@ public void setAccessToken(final String accessToken) { this.accessToken = accessToken; } + public OidcUser getUser() { + return user; + } + + public void setUser(OidcUser user) { + this.user = user; + } + @Override public String toString() { - return "%s{accessToken=%s}".formatted(getClass().getName(), accessToken); + return "%s{accessToken=%s, user=%s}".formatted(getClass().getName(), accessToken, user); } } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java index ae935da247..742168ea89 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java @@ -18,13 +18,16 @@ */ package org.dependencytrack.integrations; -import org.dependencytrack.model.Project; +import alpine.model.OidcUser; + import org.dependencytrack.persistence.QueryManager; public interface PermissionsSyncer extends IntegrationPoint { + boolean isEnabled(); void setQueryManager(QueryManager qm); - void synchronize(Project project); + void synchronize(OidcUser user); + } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index b719aaafbd..5fcd1188d5 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -21,6 +21,7 @@ import org.dependencytrack.event.GitLabSyncEvent; import org.dependencytrack.event.kafka.KafkaEventDispatcher; +import alpine.model.OidcUser; import alpine.server.auth.DefaultOidcAuthenticationCustomizer; import alpine.server.auth.OidcAuthenticationCustomizer; import alpine.server.auth.OidcProfile; @@ -34,8 +35,10 @@ public GitLabAuthenticationCustomizer() { } @Override - public void onAuthenticationSuccess(OidcProfile profile, String idToken, String accessToken) { - new KafkaEventDispatcher().dispatchEvent(new GitLabSyncEvent(accessToken)).join(); + public OidcUser onAuthenticationSuccess(OidcUser user, OidcProfile profile, String idToken, String accessToken) { + new KafkaEventDispatcher().dispatchEvent(new GitLabSyncEvent(accessToken, user)).join(); + + return user; } } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index ec84cf9af4..59796f9db8 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -20,10 +20,19 @@ import alpine.common.logging.Logger; import alpine.model.ConfigProperty; +import alpine.model.OidcUser; +import alpine.model.Permission; +import alpine.model.Team; +import org.dependencytrack.auth.Permissions; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PermissionsSyncer; import org.dependencytrack.model.Project; +import org.dependencytrack.resources.v1.UserResource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; @@ -34,9 +43,11 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private static final String INTEGRATIONS_GROUP = GITLAB_ENABLED.getGroupName(); private static final String GENERAL_GROUP = GENERAL_BASE_URL.getGroupName(); private static final String ROLE_CLAIM_PREFIX = "https://gitlab.org/claims/groups/"; - private static final String ROLE_DEVELOPER = "developer"; - private static final String ROLE_MAINTAINER = "maintainer"; - private static final String ROLE_OWNER = "owner"; + private static final String ROLE_DEVELOPER = "DEVELOPER"; + private static final String ROLE_GUEST = "GUEST"; + private static final String ROLE_MAINTAINER = "MAINTAINER"; + private static final String ROLE_OWNER = "OWNER"; + private static final String ROLE_REPORTER = "REPORTER"; private final String accessToken; @@ -66,8 +77,114 @@ public boolean isEnabled() { } @Override - public void synchronize(final Project project) { + public void synchronize(final OidcUser user) { + // TODO: GitLab API call to get user projects + List projects = new ArrayList<>(); + + createProjectStructure(projects); + } + + /** + * Create hierarchical project structure for user's GitLab projects. + * + * For example, if a GitLab project path is org/group/subgroup/project, + * then the following Dependency-Track projects will be created: + * + * - org + * - org/group + * - org/group/subgroup + * - org/group/subgroup/project + * + * @param projects the list of GitLab project names available to the user + */ + private void createProjectStructure(List projects) { + OidcUser user = (new UserResource()).getSelf().readEntity(OidcUser.class); + + for (String project : projects) { + Project parent = null; + List toCreate = getProjectNames(project); + + for (String group : toCreate) { + LOGGER.debug("Creating project " + group); + + Project existingProject = qm.getProject(group, null); + if (qm.getProject(group, null) != null) { + parent = existingProject; + continue; + } + + parent = qm.createProject(group, null, null, null, parent, null, null, false); + } + + // Set access teams for last project created (the full path of the GitLab project) + List teams = createProjectTeams(parent); + + List userTeams = user.getTeams(); + for (Team team : userTeams) { + if (!teams.contains(team)) { + userTeams.add(team); + } + } + + user.setTeams(userTeams); + } + } + + /** + * Create teams for a Dependency-Track project representing a project within GitLab. + * + * @param project Dependency-Track project representing a GitLab project + * @return the Dependency-Track teams for the project + */ + private List createProjectTeams(Project project) { + List teams = new ArrayList<>(); + + for (String role : new String[]{ROLE_DEVELOPER, ROLE_GUEST, ROLE_MAINTAINER, ROLE_OWNER, ROLE_REPORTER}) { + final String teamName = "%s_%s".formatted(project.getName(), role); + + Team team = qm.getTeam(teamName); + team = team != null ? team : qm.createTeam(teamName); + + List permissions = team.getPermissions(); + Permission viewPermission = qm.getPermission(Permissions.Constants.VIEW_PORTFOLIO); + + if (!permissions.contains(viewPermission)) { + permissions.add(viewPermission); + } + + team.setPermissions(permissions); + + project.addAccessTeam(team); + teams.add(team); + } + + return teams; + } + + /** + * Generate list of hierarchical projects to be created to represent user's + * GitLab projects. + * + * For example, if a GitLab project path is org/group/subgroup/project, then + * the following project names will be returned: + * + * - org + * - org/group + * - org/group/subgroup + * - org/group/subgroup/project + * + * @param project the GitLab project name + * @return the project names to be created + */ + private List getProjectNames(String project) { + List projects = new ArrayList<>(); + List parts = Arrays.asList(project.split("/")); + + for (int i = 0; i < parts.size(); i++) { + projects.add(String.join("/", parts.subList(0, i + 1))); + } + return projects; } } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/query.graphql b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/query.graphql new file mode 100644 index 0000000000..e7f853c907 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/query.graphql @@ -0,0 +1,15 @@ +query { + projects(membership: true, search: "", first: 10) { + nodes { + name + fullPath + maxAccessLevel { + stringValue + } + } + pageInfo { + endCursor + hasNextPage + } + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index bc9bd5fccc..8eb2e30cac 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -22,8 +22,10 @@ import alpine.event.framework.Event; import alpine.event.framework.LoggableSubscriber; import alpine.model.ConfigProperty; +import alpine.model.OidcUser; import org.dependencytrack.event.GitLabSyncEvent; +import org.dependencytrack.integrations.gitlab.GitLabSyncer; import org.dependencytrack.persistence.QueryManager; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; @@ -53,23 +55,33 @@ public void inform(final Event event) { } GitLabSyncEvent gitLabSyncEvent = (GitLabSyncEvent) event; - String accessToken = gitLabSyncEvent.getAccessToken(); + String accessToken = gitLabSyncEvent.getAccessToken(); if (accessToken == null || accessToken.isEmpty()) { LOGGER.warn("GitLab syncing is enabled, but no access token was provided. Skipping."); return; } + OidcUser user = gitLabSyncEvent.getUser(); + if (user == null) { + LOGGER.warn("GitLab syncing is enabled, but no authenticated user was provided. Skipping."); + return; + } + LOGGER.info("Starting GitLab sync task"); GitLabSyncer syncer = new GitLabSyncer(accessToken); + syncer.synchronize(user); - // TODO: Get user GitLab project memberships (use alpine.security.crypto.DataEncryption for request) - // TODO: Create Dependency-Track hierarchical project structure for user's GitLab projects - // TODO: Create Dependency-Track teams such as -maintainer - // TODO: Assign Dependency-Track permissions (TBD) to teams based on GitLab role - // TODO: Map user OIDC groups to Dependency-Track teams - // TODO: Configure portfolio access control + // TODO: + // - [X] Assign authenticated OIDC user the VIEW_PORTFOLIO permission + // - [ ] Get user GitLab project memberships (use alpine.security.crypto.DataEncryption for request) + // - [X] Create Dependency-Track hierarchical project structure for user's GitLab projects + // - [X] Create Dependency-Track teams such as -maintainer + // - [ ] Come up with a set of Dependency-Track project permissions per GitLab role + // - [ ] Assign Dependency-Track permissions (TBD) to teams based on GitLab role + // - [ ] Map user OIDC groups to Dependency-Track teams + // - [ ] Configure portfolio access control (map team to project access) } } From cf6a3069f852140e7a43ee641007cfa3a0fbb21f Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 062/181] fix: remove scheduled task registration Signed-off-by: Jonathan Howard --- .../src/main/java/org/dependencytrack/tasks/TaskScheduler.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/TaskScheduler.java b/apiserver/src/main/java/org/dependencytrack/tasks/TaskScheduler.java index 1f71d25b41..708872c4a4 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/TaskScheduler.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/TaskScheduler.java @@ -26,7 +26,6 @@ import org.dependencytrack.event.EpssMirrorEvent; import org.dependencytrack.event.FortifySscUploadEventAbstract; import org.dependencytrack.event.GitHubAdvisoryMirrorEvent; -import org.dependencytrack.event.GitLabSyncEvent; import org.dependencytrack.event.IntegrityMetaInitializerEvent; import org.dependencytrack.event.InternalComponentIdentificationEvent; import org.dependencytrack.event.KennaSecurityUploadEventAbstract; @@ -86,7 +85,6 @@ private TaskScheduler() { Map.entry(new NistMirrorEvent(), getCronScheduleForTask(NistMirrorTask.class)), Map.entry(new OsvMirrorEvent(null), getCronScheduleForTask(OsvMirrorTask.class)), Map.entry(new GitHubAdvisoryMirrorEvent(), getCronScheduleForTask(GitHubAdvisoryMirrorTask.class)), - Map.entry(new GitLabSyncEvent(), getCronScheduleForTask(GitLabSyncTask.class)), Map.entry(new EpssMirrorEvent(), getCronScheduleForTask(EpssMirrorTask.class)), Map.entry(new PortfolioMetricsUpdateEvent(), getCronScheduleForTask(PortfolioMetricsUpdateTask.class)), Map.entry(new VulnerabilityMetricsUpdateEvent(), getCronScheduleForTask(VulnerabilityMetricsUpdateTask.class)), From b2347e8f8f1a7f3b43ca67301a2cfad09c5ac649 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 063/181] style: javadoc list Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitLabSyncer.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 59796f9db8..db98f9bf73 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -87,13 +87,14 @@ public void synchronize(final OidcUser user) { /** * Create hierarchical project structure for user's GitLab projects. * - * For example, if a GitLab project path is org/group/subgroup/project, - * then the following Dependency-Track projects will be created: - * - * - org - * - org/group - * - org/group/subgroup - * - org/group/subgroup/project + * For example, if a GitLab project path is org/group/subgroup/project, then + * the following Dependency-Track projects will be created: + *

          + *
        • org + *
        • org/group + *
        • org/group/subgroup + *
        • org/group/subgroup/project + *
        * * @param projects the list of GitLab project names available to the user */ @@ -131,7 +132,8 @@ private void createProjectStructure(List projects) { } /** - * Create teams for a Dependency-Track project representing a project within GitLab. + * Create teams for a Dependency-Track project representing a project within + * GitLab. * * @param project Dependency-Track project representing a GitLab project * @return the Dependency-Track teams for the project @@ -167,11 +169,12 @@ private List createProjectTeams(Project project) { * * For example, if a GitLab project path is org/group/subgroup/project, then * the following project names will be returned: - * - * - org - * - org/group - * - org/group/subgroup - * - org/group/subgroup/project + *
          + *
        • org + *
        • org/group + *
        • org/group/subgroup + *
        • org/group/subgroup/project + *
        * * @param project the GitLab project name * @return the project names to be created From 6e0c6d9abef0d155b9ef744c040d11f876692c6d Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 064/181] refactor: use Event.dispatch, remove pom repository Signed-off-by: Jonathan Howard --- .../GitLabAuthenticationCustomizer.java | 8 +-- pom.xml | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index 5fcd1188d5..43f1b4d9e0 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -18,14 +18,14 @@ */ package org.dependencytrack.integrations.gitlab; -import org.dependencytrack.event.GitLabSyncEvent; -import org.dependencytrack.event.kafka.KafkaEventDispatcher; - +import alpine.event.framework.Event; import alpine.model.OidcUser; import alpine.server.auth.DefaultOidcAuthenticationCustomizer; import alpine.server.auth.OidcAuthenticationCustomizer; import alpine.server.auth.OidcProfile; +import org.dependencytrack.event.GitLabSyncEvent; + public class GitLabAuthenticationCustomizer extends DefaultOidcAuthenticationCustomizer implements OidcAuthenticationCustomizer { @@ -36,7 +36,7 @@ public GitLabAuthenticationCustomizer() { @Override public OidcUser onAuthenticationSuccess(OidcUser user, OidcProfile profile, String idToken, String accessToken) { - new KafkaEventDispatcher().dispatchEvent(new GitLabSyncEvent(accessToken, user)).join(); + Event.dispatch(new GitLabSyncEvent(accessToken, user)); return user; } diff --git a/pom.xml b/pom.xml index d1d6abf3c5..6074479f01 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,71 @@ UTF-8 UTF-8 . + + ${project.parent.version} + 4.2.2 + 0.5.1 + 10.20.1 + 1.21.0 + 1.27.1 + 2.1.0 + 1.4.3 + 1.0.1 + 9.1.0 + 0.3.0 + 6.0.0-rc.8 + 4.0.5 + + 2.17.3 + 2.17.3 + 3.47.0 + 4.1.0 + 4.13.2 + 3.9.0 + 4.30.0 + 0.2.2 + 8.5.17 + 2.0.0 + 3.12.0 + 1.5.0 + 0.5.3.2 + 3.2.2 + 4.29.3 + 1.20.5 + 2.2.0 + + 2.2.25 + 2.1.23 + 1.19.0 + 0.7.0 + 7.1.0 + 1.1.1 + 2.0.16 + 4.5.14 + 6.3.0 + 1.4.0 + 42.7.4 + + application + json + false + + 3.6.0 + 3.11.4 + 12.0.16 + + src/main/webapp/** + + com.google.protobuf:protoc:${lib.protobuf-java.version} + + cyclonedx + true From 2cedd168ded778aa5966cc00a32cd66fe3a87a7b Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 065/181] chore: fix pom alpine-parent version and formatting Signed-off-by: Jonathan Howard --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6074479f01..4b5e40df80 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ us.springett alpine-parent - 3.1.2 + 3.2.0-SNAPSHOT 4.0.0 From 3ee2ef71c64a52b5bed8c55fe69946593c64bf1c Mon Sep 17 00:00:00 2001 From: Patrick Kwiatkowski Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 066/181] added user as syncer class var Signed-off-by: Patrick Kwiatkowski --- .../integrations/PermissionsSyncer.java | 4 +-- .../integrations/gitlab/GitLabSyncer.java | 30 +++++++++---------- .../dependencytrack/tasks/GitLabSyncTask.java | 3 +- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java index 742168ea89..1c00502d4c 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/PermissionsSyncer.java @@ -18,8 +18,6 @@ */ package org.dependencytrack.integrations; -import alpine.model.OidcUser; - import org.dependencytrack.persistence.QueryManager; public interface PermissionsSyncer extends IntegrationPoint { @@ -28,6 +26,6 @@ public interface PermissionsSyncer extends IntegrationPoint { void setQueryManager(QueryManager qm); - void synchronize(OidcUser user); + void synchronize(); } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index db98f9bf73..b1aa10daf2 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -18,24 +18,23 @@ */ package org.dependencytrack.integrations.gitlab; -import alpine.common.logging.Logger; -import alpine.model.ConfigProperty; -import alpine.model.OidcUser; -import alpine.model.Permission; -import alpine.model.Team; +import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.dependencytrack.auth.Permissions; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PermissionsSyncer; import org.dependencytrack.model.Project; -import org.dependencytrack.resources.v1.UserResource; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL; -import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import alpine.common.logging.Logger; +import alpine.model.ConfigProperty; +import alpine.model.OidcUser; +import alpine.model.Permission; +import alpine.model.Team; public class GitLabSyncer extends AbstractIntegrationPoint implements PermissionsSyncer { @@ -50,9 +49,11 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private static final String ROLE_REPORTER = "REPORTER"; private final String accessToken; + private final OidcUser user; - public GitLabSyncer(final String accessToken) { + public GitLabSyncer(final String accessToken, final OidcUser user) { this.accessToken = accessToken; + this.user = user; } public String getAccessToken() { @@ -77,7 +78,7 @@ public boolean isEnabled() { } @Override - public void synchronize(final OidcUser user) { + public void synchronize() { // TODO: GitLab API call to get user projects List projects = new ArrayList<>(); @@ -99,7 +100,6 @@ public void synchronize(final OidcUser user) { * @param projects the list of GitLab project names available to the user */ private void createProjectStructure(List projects) { - OidcUser user = (new UserResource()).getSelf().readEntity(OidcUser.class); for (String project : projects) { Project parent = null; diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index 8eb2e30cac..1537fd35af 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -70,8 +70,7 @@ public void inform(final Event event) { LOGGER.info("Starting GitLab sync task"); - GitLabSyncer syncer = new GitLabSyncer(accessToken); - syncer.synchronize(user); + new GitLabSyncer(accessToken, user).synchronize(); // TODO: // - [X] Assign authenticated OIDC user the VIEW_PORTFOLIO permission From d817bde6b9a8a670743b89ba7e15227ae63b4ee8 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 067/181] refactor: add GitLabRole enum Signed-off-by: Jonathan Howard --- .../event/GitLabSyncEvent.java | 2 +- .../integrations/gitlab/GitLabRole.java | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java diff --git a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java index a87c84df32..ef2655e4db 100644 --- a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java +++ b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java @@ -58,7 +58,7 @@ public void setUser(OidcUser user) { @Override public String toString() { - return "%s{accessToken=%s, user=%s}".formatted(getClass().getName(), accessToken, user); + return "%s{accessToken=%s, user=%s}".formatted(getClass().getSimpleName(), accessToken, user); } } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java new file mode 100644 index 0000000000..c63f53d629 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java @@ -0,0 +1,31 @@ +/* + * 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.integrations.gitlab; + +/** + * Definitions of access levels/roles as defined by GitLab. + */ +public enum GitLabRole { + GUEST, // Applies to private and internal projects only + PLANNER, + REPORTER, + DEVELOPER, + MAINTAINER, + OWNER +} From ea1246a2211f8f3ca25b701bfcbc6884d4a5e260 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 068/181] fix: remove redundant superinterface Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitLabAuthenticationCustomizer.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index 43f1b4d9e0..ccb29e0766 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -21,14 +21,11 @@ import alpine.event.framework.Event; import alpine.model.OidcUser; import alpine.server.auth.DefaultOidcAuthenticationCustomizer; -import alpine.server.auth.OidcAuthenticationCustomizer; import alpine.server.auth.OidcProfile; import org.dependencytrack.event.GitLabSyncEvent; -public class GitLabAuthenticationCustomizer - extends DefaultOidcAuthenticationCustomizer - implements OidcAuthenticationCustomizer { +public class GitLabAuthenticationCustomizer extends DefaultOidcAuthenticationCustomizer { public GitLabAuthenticationCustomizer() { From a4457fc242a5a73b40ef88c0a9ff206015bbaf23 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 069/181] fix: ensure query manager instantiated Signed-off-by: Jonathan Howard --- .../main/java/org/dependencytrack/tasks/GitLabSyncTask.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index 1537fd35af..e84f9c1aee 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -70,7 +70,11 @@ public void inform(final Event event) { LOGGER.info("Starting GitLab sync task"); - new GitLabSyncer(accessToken, user).synchronize(); + try (QueryManager qm = new QueryManager()) { + GitLabSyncer syncer = new GitLabSyncer(accessToken, user); + syncer.setQueryManager(qm); + syncer.synchronize(); + } // TODO: // - [X] Assign authenticated OIDC user the VIEW_PORTFOLIO permission From dc39b30e8748433b34eb1044a7095f983284f6e9 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:57:59 -0600 Subject: [PATCH 070/181] fix: implement base Event interface Signed-off-by: Jonathan Howard --- .../main/java/org/dependencytrack/event/GitLabSyncEvent.java | 4 ++-- .../org/dependencytrack/integrations/gitlab/GitLabSyncer.java | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java index ef2655e4db..32e649a37c 100644 --- a/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java +++ b/apiserver/src/main/java/org/dependencytrack/event/GitLabSyncEvent.java @@ -18,7 +18,7 @@ */ package org.dependencytrack.event; -import alpine.event.framework.UnblockedEvent; +import alpine.event.framework.Event; import alpine.model.OidcUser; /** @@ -26,7 +26,7 @@ * * @author Jonathan Howard */ -public class GitLabSyncEvent implements UnblockedEvent { +public class GitLabSyncEvent implements Event { private String accessToken; private OidcUser user; diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index b1aa10daf2..f302e71f9b 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -100,7 +100,6 @@ public void synchronize() { * @param projects the list of GitLab project names available to the user */ private void createProjectStructure(List projects) { - for (String project : projects) { Project parent = null; List toCreate = getProjectNames(project); @@ -109,7 +108,7 @@ private void createProjectStructure(List projects) { LOGGER.debug("Creating project " + group); Project existingProject = qm.getProject(group, null); - if (qm.getProject(group, null) != null) { + if (existingProject != null) { parent = existingProject; continue; } From 6e8be399405ade3fab0bd17572a3492f3c463b9e Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 071/181] feat: add GitLab Project Class and GitLab graphql query functionality Signed-off-by: Allen Shearin --- .../integrations/gitlab/GitLabClient.java | 143 +++++++++++++++++- .../integrations/gitlab/GitLabSyncer.java | 30 ++-- .../integrations/gitlab/GitlabProject.java | 86 +++++++++++ .../graphql/gitlab-projects.graphql} | 6 +- 4 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java rename apiserver/src/main/{java/org/dependencytrack/integrations/gitlab/query.graphql => resources/graphql/gitlab-projects.graphql} (64%) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index 29e7b84a1d..fcee7fede8 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -16,27 +16,160 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) OWASP Foundation. All Rights Reserved. */ + package org.dependencytrack.integrations.gitlab; -import java.net.URL; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.List; +import java.util.Map; -import org.json.JSONArray; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.common.HttpClientPool; import alpine.common.logging.Logger; +import net.minidev.json.JSONArray; +import net.minidev.json.JSONObject; +import net.minidev.json.JSONValue; + +import static org.apache.commons.io.IOUtils.resourceToString; + public class GitLabClient { private static final Logger LOGGER = Logger.getLogger(GitLabClient.class); private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + private static final String GRAPHQL_ENDPOINT = "/api/graphql"; + + private final String accessToken; private final GitLabSyncer syncer; - private final URL baseURL; + private final URI baseURL; - public GitLabClient(final GitLabSyncer syncer, final URL baseURL) { - this.syncer = syncer; + private final Map> rolePermissions = Map.of( + GitLabRole.GUEST, List.of( + Permissions.VIEW_PORTFOLIO, + Permissions.VIEW_VULNERABILITY, + Permissions.VIEW_BADGES), + GitLabRole.PLANNER, List.of( + Permissions.VIEW_PORTFOLIO, + Permissions.VIEW_VULNERABILITY, + Permissions.VIEW_POLICY_VIOLATION, + Permissions.VIEW_BADGES), + GitLabRole.REPORTER, List.of( + Permissions.VIEW_PORTFOLIO, + Permissions.VIEW_VULNERABILITY, + Permissions.VIEW_POLICY_VIOLATION, + Permissions.VIEW_BADGES), + GitLabRole.DEVELOPER, List.of( + Permissions.BOM_UPLOAD, + Permissions.VIEW_PORTFOLIO, + Permissions.PORTFOLIO_MANAGEMENT_READ, + Permissions.VIEW_VULNERABILITY, + Permissions.VULNERABILITY_ANALYSIS_READ, + Permissions.PROJECT_CREATION_UPLOAD), + GitLabRole.MAINTAINER, List.of( + Permissions.BOM_UPLOAD, + Permissions.PORTFOLIO_MANAGEMENT, + Permissions.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.PORTFOLIO_MANAGEMENT_READ, + Permissions.PORTFOLIO_MANAGEMENT_UPDATE, + Permissions.PORTFOLIO_MANAGEMENT_DELETE, + Permissions.VULNERABILITY_ANALYSIS, + Permissions.VULNERABILITY_ANALYSIS_CREATE, + Permissions.VULNERABILITY_ANALYSIS_READ, + Permissions.VULNERABILITY_ANALYSIS_UPDATE, + Permissions.POLICY_MANAGEMENT, + Permissions.POLICY_MANAGEMENT_CREATE, + Permissions.POLICY_MANAGEMENT_READ, + Permissions.POLICY_MANAGEMENT_UPDATE, + Permissions.POLICY_MANAGEMENT_DELETE), + GitLabRole.OWNER, List.of( + Permissions.ACCESS_MANAGEMENT, + Permissions.ACCESS_MANAGEMENT_CREATE, + Permissions.ACCESS_MANAGEMENT_READ, + Permissions.ACCESS_MANAGEMENT_UPDATE, + Permissions.ACCESS_MANAGEMENT_DELETE, + Permissions.SYSTEM_CONFIGURATION, + Permissions.SYSTEM_CONFIGURATION_CREATE, + Permissions.SYSTEM_CONFIGURATION_READ, + Permissions.SYSTEM_CONFIGURATION_UPDATE, + Permissions.SYSTEM_CONFIGURATION_DELETE, + Permissions.TAG_MANAGEMENT, + Permissions.TAG_MANAGEMENT_DELETE)); + + public GitLabClient(final GitLabSyncer syncer, final URI baseURL, final String accessToken) { + this.accessToken = accessToken; this.baseURL = baseURL; + this.syncer = syncer; + } + + public List getGitLabProjects() { + List projects = new ArrayList<>(); + + JSONObject variables = new JSONObject(); + JSONObject queryObject = new JSONObject(); + + try { + queryObject.put("query", resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8)); + + URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT); + + HttpPost request = new HttpPost(builder.build()); + request.setHeader("Authorization", "Bearer " + accessToken); + request.setHeader("Content-Type", "application/json"); + + while (true) { + queryObject.put("variables", variables); + + StringEntity entity = new StringEntity(queryObject.toString(), StandardCharsets.UTF_8); + request.setEntity(entity); + + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + HttpEntity responseEntity = response.getEntity(); + + if (responseEntity == null) + break; + + String responseBody = EntityUtils.toString(responseEntity); + JSONObject responseData = JSONValue.parse(responseBody, JSONObject.class); + JSONObject dataObject = (JSONObject) responseData.get("data"); + JSONObject projectsObject = (JSONObject) dataObject.get("projects"); + JSONArray nodes = (JSONArray) projectsObject.get("nodes"); + + for (Object nodeObject : nodes) { + JSONObject node = (JSONObject) nodeObject; + projects.add(GitLabProject.parse(node.toJSONString())); + } + + JSONObject pageInfo = (JSONObject) projectsObject.get("pageInfo"); + + if (!(boolean) pageInfo.get("hasNextPage")) + break; + + variables.put("cursor", pageInfo.getAsString("endCursor")); + } + } + } catch (IOException | URISyntaxException ex) { + LOGGER.error("An error occurred while querying GitLab GraphQL API", ex); + syncer.handleException(LOGGER, ex); + } + + return projects; + } + + public List getRolePermissions(final GitLabRole role) { + return rolePermissions.get(role); } // JSONArray to ArrayList simple converter diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index f302e71f9b..3aa4b61a06 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -21,9 +21,11 @@ import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import alpine.Config; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.net.URI; import org.dependencytrack.auth.Permissions; import org.dependencytrack.integrations.AbstractIntegrationPoint; @@ -42,11 +44,7 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private static final String INTEGRATIONS_GROUP = GITLAB_ENABLED.getGroupName(); private static final String GENERAL_GROUP = GENERAL_BASE_URL.getGroupName(); private static final String ROLE_CLAIM_PREFIX = "https://gitlab.org/claims/groups/"; - private static final String ROLE_DEVELOPER = "DEVELOPER"; - private static final String ROLE_GUEST = "GUEST"; - private static final String ROLE_MAINTAINER = "MAINTAINER"; - private static final String ROLE_OWNER = "OWNER"; - private static final String ROLE_REPORTER = "REPORTER"; + private static final URI baseURL = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); private final String accessToken; private final OidcUser user; @@ -79,8 +77,9 @@ public boolean isEnabled() { @Override public void synchronize() { - // TODO: GitLab API call to get user projects - List projects = new ArrayList<>(); + GitLabClient gitLabClient = new GitLabClient(this, baseURL, this.accessToken); + + List projects = gitLabClient.getGitLabProjects(); createProjectStructure(projects); } @@ -99,10 +98,11 @@ public void synchronize() { * * @param projects the list of GitLab project names available to the user */ - private void createProjectStructure(List projects) { - for (String project : projects) { + public void createProjectStructure(List projects) { + + for (GitLabProject project : projects) { Project parent = null; - List toCreate = getProjectNames(project); + List toCreate = getProjectNames(project.getFullPath()); for (String group : toCreate) { LOGGER.debug("Creating project " + group); @@ -137,11 +137,11 @@ private void createProjectStructure(List projects) { * @param project Dependency-Track project representing a GitLab project * @return the Dependency-Track teams for the project */ - private List createProjectTeams(Project project) { + public List createProjectTeams(Project project) { List teams = new ArrayList<>(); - for (String role : new String[]{ROLE_DEVELOPER, ROLE_GUEST, ROLE_MAINTAINER, ROLE_OWNER, ROLE_REPORTER}) { - final String teamName = "%s_%s".formatted(project.getName(), role); + for (GitLabRole role : GitLabRole.values()) { + final String teamName = "%s_%s".formatted(project.getName(), role.name()); Team team = qm.getTeam(teamName); team = team != null ? team : qm.createTeam(teamName); @@ -178,7 +178,7 @@ private List createProjectTeams(Project project) { * @param project the GitLab project name * @return the project names to be created */ - private List getProjectNames(String project) { + public List getProjectNames(String project) { List projects = new ArrayList<>(); List parts = Arrays.asList(project.split("/")); @@ -189,4 +189,4 @@ private List getProjectNames(String project) { return projects; } -} +} \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java new file mode 100644 index 0000000000..4a6f099829 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java @@ -0,0 +1,86 @@ +/* + * 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.integrations.gitlab; + +import net.minidev.json.JSONObject; +import net.minidev.json.JSONValue; + +/** + * Representation of a GitLab project. + * + * @author Allen Shearin + */ +class GitLabProject { + + class MaxAccessLevel { + + private final GitLabRole stringValue; + + MaxAccessLevel(final GitLabRole role) { + this.stringValue = role; + } + + GitLabRole getStringValue() { + return stringValue; + } + } + + private final String name; + private final String fullPath; + private final MaxAccessLevel maxAccessLevel; + + GitLabProject(final String name, final String fullPath, final GitLabRole maxAccessLevel) { + this.name = name; + this.fullPath = fullPath; + this.maxAccessLevel = new MaxAccessLevel(maxAccessLevel); + } + + public String getName() { + return name; + } + + public String getFullPath() { + return fullPath; + } + + public MaxAccessLevel getMaxAccessLevel() { + return maxAccessLevel; + } + + public static GitLabProject parse(final String data) { + JSONObject obj = JSONValue.parse(data, JSONObject.class); + String name = obj.getAsString("name"); + String fullPath = obj.getAsString("fullPath"); + + JSONObject maxAccessLevel = (JSONObject) obj.get("maxAccessLevel"); + String stringValue = maxAccessLevel.getAsString("stringValue"); + + return new GitLabProject(name, fullPath, GitLabRole.valueOf(stringValue)); + } + + @Override + public String toString() { + return "%s{name=%s, fullPath=%s, maxAccessLevel=%s}".formatted( + getClass().getSimpleName(), + name, + fullPath, + maxAccessLevel.getStringValue().toString()); + } + +} \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/query.graphql b/apiserver/src/main/resources/graphql/gitlab-projects.graphql similarity index 64% rename from apiserver/src/main/java/org/dependencytrack/integrations/gitlab/query.graphql rename to apiserver/src/main/resources/graphql/gitlab-projects.graphql index e7f853c907..54c88a4ae6 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/query.graphql +++ b/apiserver/src/main/resources/graphql/gitlab-projects.graphql @@ -1,5 +1,5 @@ -query { - projects(membership: true, search: "", first: 10) { +query ($cursor: String) { + projects(membership: true, first: 10, after: $cursor) { nodes { name fullPath @@ -12,4 +12,4 @@ query { hasNextPage } } -} +} \ No newline at end of file From 6bf24deefbb43a9591e55a3bd62600fad51cfb05 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 072/181] chore: formatting and visibility fixes Signed-off-by: Allen Shearin --- .../integrations/gitlab/GitLabClient.java | 125 +++++++++++++++++- .../integrations/gitlab/GitLabSyncer.java | 9 +- 2 files changed, 128 insertions(+), 6 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index fcee7fede8..f8eb189a49 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -182,4 +182,127 @@ public ArrayList jsonToList(final JSONArray jsonArray) { return list; } -} + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 3aa4b61a06..d4a853eadd 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -78,7 +78,7 @@ public boolean isEnabled() { @Override public void synchronize() { GitLabClient gitLabClient = new GitLabClient(this, baseURL, this.accessToken); - + List projects = gitLabClient.getGitLabProjects(); createProjectStructure(projects); @@ -98,8 +98,7 @@ public void synchronize() { * * @param projects the list of GitLab project names available to the user */ - public void createProjectStructure(List projects) { - + private void createProjectStructure(List projects) { for (GitLabProject project : projects) { Project parent = null; List toCreate = getProjectNames(project.getFullPath()); @@ -137,7 +136,7 @@ public void createProjectStructure(List projects) { * @param project Dependency-Track project representing a GitLab project * @return the Dependency-Track teams for the project */ - public List createProjectTeams(Project project) { + private List createProjectTeams(Project project) { List teams = new ArrayList<>(); for (GitLabRole role : GitLabRole.values()) { @@ -178,7 +177,7 @@ public List createProjectTeams(Project project) { * @param project the GitLab project name * @return the project names to be created */ - public List getProjectNames(String project) { + private List getProjectNames(String project) { List projects = new ArrayList<>(); List parts = Arrays.asList(project.split("/")); From 504c8f446564b63e386b78e0c92b3bd88e3e2457 Mon Sep 17 00:00:00 2001 From: Ephraim Mensah Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 073/181] add map permission to gitlab default roles Signed-off-by: Ephraim Mensah --- .../org/dependencytrack/integrations/gitlab/GitLabClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index f8eb189a49..1d4a7e55e0 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; From 2533d4997d24c517dedd34fc0adc5a5f8b76e701 Mon Sep 17 00:00:00 2001 From: Ephraim Mensah Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 074/181] add GitlabClientTest file Signed-off-by: Ephraim Mensah --- .../integrations/gitlab/GitLabClientTest.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java new file mode 100644 index 0000000000..66654bc635 --- /dev/null +++ b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -0,0 +1,84 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.integrations.gitlab; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Map; + +import org.dependencytrack.auth.Permissions; +import org.json.JSONArray; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Test; + +public class GitLabClientTest { + + @SuppressWarnings("deprecation") + @Test + public void testJsonToList() { + GitLabClient client = null; + try { + client = new GitLabClient(new GitLabSyncer(null), new URL("https://gitlab.com")); + } catch (MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + JSONArray jsonArray = new JSONArray(); + jsonArray.put("value1"); + jsonArray.put("value2"); + jsonArray.put("value3"); + List list = client.jsonToList(jsonArray); + assertEquals(3, list.size()); + assertEquals("value1", list.get(0)); + assertEquals("value2", list.get(1)); + assertEquals("value3", list.get(2)); + } + + @Test + public void testJsonToListNull() throws Exception { + @SuppressWarnings("deprecation") + GitLabClient client = new GitLabClient(new GitLabSyncer(null), new URL("https://gitlab.com")); + List list = client.jsonToList(null); + assertEquals(0, list.size()); + } + + @SuppressWarnings("deprecation") + @Test + public void testJsonToListEmpty() throws MalformedURLException { + GitLabClient client = new GitLabClient(new GitLabSyncer(null), new URL("https://gitlab.com")); + JSONArray jsonArray = new JSONArray(); + List list = client.jsonToList(jsonArray); + assertEquals(0, list.size()); + } + + @SuppressWarnings("deprecation") + @Test + public void testMapPermissionsToRoles() throws MalformedURLException { + GitLabClient client = new GitLabClient(new GitLabSyncer(null), new URL("https://gitlab.com")); + Map> permissionMap = client.mapPermissionsToRoles(); + assertEquals(5, permissionMap.size()); + assertNotNull(permissionMap.get("Guest")); + assertNotNull(permissionMap.get("Reporter")); + assertNotNull(permissionMap.get("Developer")); + assertNotNull(permissionMap.get("Maintainer")); + assertNotNull(permissionMap.get("Owner")); + } +} \ No newline at end of file From ee66deb92af4dfb2d686c2d5f6bfac151b78d769 Mon Sep 17 00:00:00 2001 From: Ephraim Mensah Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 075/181] modify permission to static member of the class Signed-off-by: Ephraim Mensah --- .../integrations/gitlab/GitLabClient.java | 2 +- .../integrations/gitlab/GitLabClientTest.java | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index 1d4a7e55e0..b3d4aa3c86 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -25,10 +25,10 @@ import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index 66654bc635..f8980a7080 100644 --- a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -36,7 +36,7 @@ public class GitLabClientTest { public void testJsonToList() { GitLabClient client = null; try { - client = new GitLabClient(new GitLabSyncer(null), new URL("https://gitlab.com")); + client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -55,7 +55,7 @@ public void testJsonToList() { @Test public void testJsonToListNull() throws Exception { @SuppressWarnings("deprecation") - GitLabClient client = new GitLabClient(new GitLabSyncer(null), new URL("https://gitlab.com")); + GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); List list = client.jsonToList(null); assertEquals(0, list.size()); } @@ -63,7 +63,7 @@ public void testJsonToListNull() throws Exception { @SuppressWarnings("deprecation") @Test public void testJsonToListEmpty() throws MalformedURLException { - GitLabClient client = new GitLabClient(new GitLabSyncer(null), new URL("https://gitlab.com")); + GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); JSONArray jsonArray = new JSONArray(); List list = client.jsonToList(jsonArray); assertEquals(0, list.size()); @@ -72,13 +72,13 @@ public void testJsonToListEmpty() throws MalformedURLException { @SuppressWarnings("deprecation") @Test public void testMapPermissionsToRoles() throws MalformedURLException { - GitLabClient client = new GitLabClient(new GitLabSyncer(null), new URL("https://gitlab.com")); + GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); Map> permissionMap = client.mapPermissionsToRoles(); assertEquals(5, permissionMap.size()); - assertNotNull(permissionMap.get("Guest")); - assertNotNull(permissionMap.get("Reporter")); - assertNotNull(permissionMap.get("Developer")); - assertNotNull(permissionMap.get("Maintainer")); - assertNotNull(permissionMap.get("Owner")); + assertNotNull(permissionMap.get("GUEST")); + assertNotNull(permissionMap.get("REPORTER")); + assertNotNull(permissionMap.get("DEVELOPER")); + assertNotNull(permissionMap.get("MAINTAINER")); + assertNotNull(permissionMap.get("OWNER")); } } \ No newline at end of file From 5084abc7b3a46ba5ed60dfe72618b15b69a1152e Mon Sep 17 00:00:00 2001 From: Ephraim Mensah Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 076/181] supress warning on mapPermissionToRoles set to uncheck Signed-off-by: Ephraim Mensah --- .../dependencytrack/integrations/gitlab/GitLabClientTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index f8980a7080..de91b00813 100644 --- a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -73,6 +73,7 @@ public void testJsonToListEmpty() throws MalformedURLException { @Test public void testMapPermissionsToRoles() throws MalformedURLException { GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); + @SuppressWarnings("unchecked") Map> permissionMap = client.mapPermissionsToRoles(); assertEquals(5, permissionMap.size()); assertNotNull(permissionMap.get("GUEST")); From 6067d606a87538a60311cb344156362c68e47853 Mon Sep 17 00:00:00 2001 From: Ephraim Mensah Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 077/181] clean up remove unused import Signed-off-by: Ephraim Mensah --- .../integrations/gitlab/GitLabClient.java | 1 - .../integrations/gitlab/GitLabClientTest.java | 120 +++++++++--------- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index b3d4aa3c86..f8eb189a49 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -28,7 +28,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index de91b00813..d47a2e77ff 100644 --- a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -16,70 +16,70 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) OWASP Foundation. All Rights Reserved. */ -package org.dependencytrack.integrations.gitlab; +// package org.dependencytrack.integrations.gitlab; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; -import java.util.Map; +// import java.net.MalformedURLException; +// import java.net.URL; +// import java.util.List; +// import java.util.Map; -import org.dependencytrack.auth.Permissions; -import org.json.JSONArray; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import org.junit.Test; +// import org.dependencytrack.auth.Permissions; +// import org.json.JSONArray; +// import static org.junit.Assert.assertEquals; +// import static org.junit.Assert.assertNotNull; +// import org.junit.Test; -public class GitLabClientTest { +// public class GitLabClientTest { - @SuppressWarnings("deprecation") - @Test - public void testJsonToList() { - GitLabClient client = null; - try { - client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); - } catch (MalformedURLException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - JSONArray jsonArray = new JSONArray(); - jsonArray.put("value1"); - jsonArray.put("value2"); - jsonArray.put("value3"); - List list = client.jsonToList(jsonArray); - assertEquals(3, list.size()); - assertEquals("value1", list.get(0)); - assertEquals("value2", list.get(1)); - assertEquals("value3", list.get(2)); - } +// @SuppressWarnings("deprecation") +// @Test +// public void testJsonToList() { +// GitLabClient client = null; +// try { +// client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); +// } catch (MalformedURLException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// JSONArray jsonArray = new JSONArray(); +// jsonArray.put("value1"); +// jsonArray.put("value2"); +// jsonArray.put("value3"); +// List list = client.jsonToList(jsonArray); +// assertEquals(3, list.size()); +// assertEquals("value1", list.get(0)); +// assertEquals("value2", list.get(1)); +// assertEquals("value3", list.get(2)); +// } - @Test - public void testJsonToListNull() throws Exception { - @SuppressWarnings("deprecation") - GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); - List list = client.jsonToList(null); - assertEquals(0, list.size()); - } +// @Test +// public void testJsonToListNull() throws Exception { +// @SuppressWarnings("deprecation") +// GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); +// List list = client.jsonToList(null); +// assertEquals(0, list.size()); +// } - @SuppressWarnings("deprecation") - @Test - public void testJsonToListEmpty() throws MalformedURLException { - GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); - JSONArray jsonArray = new JSONArray(); - List list = client.jsonToList(jsonArray); - assertEquals(0, list.size()); - } +// @SuppressWarnings("deprecation") +// @Test +// public void testJsonToListEmpty() throws MalformedURLException { +// GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); +// JSONArray jsonArray = new JSONArray(); +// List list = client.jsonToList(jsonArray); +// assertEquals(0, list.size()); +// } - @SuppressWarnings("deprecation") - @Test - public void testMapPermissionsToRoles() throws MalformedURLException { - GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); - @SuppressWarnings("unchecked") - Map> permissionMap = client.mapPermissionsToRoles(); - assertEquals(5, permissionMap.size()); - assertNotNull(permissionMap.get("GUEST")); - assertNotNull(permissionMap.get("REPORTER")); - assertNotNull(permissionMap.get("DEVELOPER")); - assertNotNull(permissionMap.get("MAINTAINER")); - assertNotNull(permissionMap.get("OWNER")); - } -} \ No newline at end of file +// @SuppressWarnings("deprecation") +// @Test +// public void testMapPermissionsToRoles() throws MalformedURLException { +// GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); +// @SuppressWarnings("unchecked") +// Map> permissionMap = client.mapPermissionsToRoles(); +// assertEquals(5, permissionMap.size()); +// assertNotNull(permissionMap.get("GUEST")); +// assertNotNull(permissionMap.get("REPORTER")); +// assertNotNull(permissionMap.get("DEVELOPER")); +// assertNotNull(permissionMap.get("MAINTAINER")); +// assertNotNull(permissionMap.get("OWNER")); +// } +// } \ No newline at end of file From 7d76063c98175f8e44bd6f7acde0919ac38634e7 Mon Sep 17 00:00:00 2001 From: Ephraim Mensah Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 078/181] add the planner role with permission Signed-off-by: Ephraim Mensah --- .../integrations/gitlab/GitLabClientTest.java | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java deleted file mode 100644 index d47a2e77ff..0000000000 --- a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ /dev/null @@ -1,85 +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.integrations.gitlab; - -// import java.net.MalformedURLException; -// import java.net.URL; -// import java.util.List; -// import java.util.Map; - -// import org.dependencytrack.auth.Permissions; -// import org.json.JSONArray; -// import static org.junit.Assert.assertEquals; -// import static org.junit.Assert.assertNotNull; -// import org.junit.Test; - -// public class GitLabClientTest { - -// @SuppressWarnings("deprecation") -// @Test -// public void testJsonToList() { -// GitLabClient client = null; -// try { -// client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); -// } catch (MalformedURLException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); -// } -// JSONArray jsonArray = new JSONArray(); -// jsonArray.put("value1"); -// jsonArray.put("value2"); -// jsonArray.put("value3"); -// List list = client.jsonToList(jsonArray); -// assertEquals(3, list.size()); -// assertEquals("value1", list.get(0)); -// assertEquals("value2", list.get(1)); -// assertEquals("value3", list.get(2)); -// } - -// @Test -// public void testJsonToListNull() throws Exception { -// @SuppressWarnings("deprecation") -// GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); -// List list = client.jsonToList(null); -// assertEquals(0, list.size()); -// } - -// @SuppressWarnings("deprecation") -// @Test -// public void testJsonToListEmpty() throws MalformedURLException { -// GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); -// JSONArray jsonArray = new JSONArray(); -// List list = client.jsonToList(jsonArray); -// assertEquals(0, list.size()); -// } - -// @SuppressWarnings("deprecation") -// @Test -// public void testMapPermissionsToRoles() throws MalformedURLException { -// GitLabClient client = new GitLabClient(new GitLabSyncer(null, null), new URL("https://gitlab.com")); -// @SuppressWarnings("unchecked") -// Map> permissionMap = client.mapPermissionsToRoles(); -// assertEquals(5, permissionMap.size()); -// assertNotNull(permissionMap.get("GUEST")); -// assertNotNull(permissionMap.get("REPORTER")); -// assertNotNull(permissionMap.get("DEVELOPER")); -// assertNotNull(permissionMap.get("MAINTAINER")); -// assertNotNull(permissionMap.get("OWNER")); -// } -// } \ No newline at end of file From ae936afbc6d14bcba6f409f8dd75bc7ef0722cd2 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 079/181] chore: refine gitlabsyncer class variables Signed-off-by: Allen Shearin --- .../dependencytrack/integrations/gitlab/GitLabSyncer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index d4a853eadd..4508de10f2 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -44,10 +44,10 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private static final String INTEGRATIONS_GROUP = GITLAB_ENABLED.getGroupName(); private static final String GENERAL_GROUP = GENERAL_BASE_URL.getGroupName(); private static final String ROLE_CLAIM_PREFIX = "https://gitlab.org/claims/groups/"; - private static final URI baseURL = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); private final String accessToken; private final OidcUser user; + private GitLabClient gitLabClient; public GitLabSyncer(final String accessToken, final OidcUser user) { this.accessToken = accessToken; @@ -77,7 +77,8 @@ public boolean isEnabled() { @Override public void synchronize() { - GitLabClient gitLabClient = new GitLabClient(this, baseURL, this.accessToken); + final URI gitLabUrl = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); + gitLabClient = new GitLabClient(this, gitLabUrl, accessToken); List projects = gitLabClient.getGitLabProjects(); From 91b0682d05e6b4a5076cbf40098abc4e344b44c5 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 080/181] refactor: flatten created project structure Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitLabClient.java | 125 +----------------- .../integrations/gitlab/GitLabSyncer.java | 108 +++++---------- 2 files changed, 37 insertions(+), 196 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index f8eb189a49..fcee7fede8 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -182,127 +182,4 @@ public ArrayList jsonToList(final JSONArray jsonArray) { return list; } - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +} diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 4508de10f2..1d16af36b4 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -21,17 +21,17 @@ import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; -import alpine.Config; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.net.URI; import org.dependencytrack.auth.Permissions; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PermissionsSyncer; import org.dependencytrack.model.Project; +import alpine.Config; import alpine.common.logging.Logger; import alpine.model.ConfigProperty; import alpine.model.OidcUser; @@ -80,54 +80,42 @@ public void synchronize() { final URI gitLabUrl = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); gitLabClient = new GitLabClient(this, gitLabUrl, accessToken); - List projects = gitLabClient.getGitLabProjects(); + List gitLabProjects = gitLabClient.getGitLabProjects(); + List projects = createProjects(gitLabProjects); - createProjectStructure(projects); - } + for (Project project : projects) { + List teams = createProjectTeams(project); - /** - * Create hierarchical project structure for user's GitLab projects. - * - * For example, if a GitLab project path is org/group/subgroup/project, then - * the following Dependency-Track projects will be created: - *
          - *
        • org - *
        • org/group - *
        • org/group/subgroup - *
        • org/group/subgroup/project - *
        - * - * @param projects the list of GitLab project names available to the user - */ - private void createProjectStructure(List projects) { - for (GitLabProject project : projects) { - Project parent = null; - List toCreate = getProjectNames(project.getFullPath()); - - for (String group : toCreate) { - LOGGER.debug("Creating project " + group); - - Project existingProject = qm.getProject(group, null); - if (existingProject != null) { - parent = existingProject; - continue; + for (Team team : teams) { + List teamUsers = team.getOidcUsers(); + if (!teamUsers.contains(user)) { + teamUsers.add(user); + team.setOidcUsers(teamUsers); } - parent = qm.createProject(group, null, null, null, parent, null, null, false); + qm.updateTeam(team); } + } + } - // Set access teams for last project created (the full path of the GitLab project) - List teams = createProjectTeams(parent); + private List createProjects(List gitLabProjects) { + List projects = new ArrayList<>(); - List userTeams = user.getTeams(); - for (Team team : userTeams) { - if (!teams.contains(team)) { - userTeams.add(team); - } + for (GitLabProject gitLabProject : gitLabProjects) { + Project project = qm.getProject(gitLabProject.getFullPath(), null); + + if (project == null) { + LOGGER.debug("Creating project " + gitLabProject.getFullPath()); + + project = new Project(); + project.setName(gitLabProject.getFullPath()); + project = qm.persist(project); } - user.setTeams(userTeams); + projects.add(project); } + + return projects; } /** @@ -146,47 +134,23 @@ private List createProjectTeams(Project project) { Team team = qm.getTeam(teamName); team = team != null ? team : qm.createTeam(teamName); - List permissions = team.getPermissions(); - Permission viewPermission = qm.getPermission(Permissions.Constants.VIEW_PORTFOLIO); + List rolePermissions = gitLabClient.getRolePermissions(role); + List permissions = new ArrayList<>(team.getPermissions()); - if (!permissions.contains(viewPermission)) { - permissions.add(viewPermission); + for (Permissions rolePermission : rolePermissions) { + Permission permission = qm.getPermission(rolePermission.name()); + if (permission != null && !permissions.contains(permission)) + permissions.add(permission); } team.setPermissions(permissions); - project.addAccessTeam(team); + qm.updateProject(project, false); + teams.add(team); } return teams; } - /** - * Generate list of hierarchical projects to be created to represent user's - * GitLab projects. - * - * For example, if a GitLab project path is org/group/subgroup/project, then - * the following project names will be returned: - *
          - *
        • org - *
        • org/group - *
        • org/group/subgroup - *
        • org/group/subgroup/project - *
        - * - * @param project the GitLab project name - * @return the project names to be created - */ - private List getProjectNames(String project) { - List projects = new ArrayList<>(); - List parts = Arrays.asList(project.split("/")); - - for (int i = 0; i < parts.size(); i++) { - projects.add(String.join("/", parts.subList(0, i + 1))); - } - - return projects; - } - } \ No newline at end of file From 425621f8f905dbb458a124fda37b280c3f8e8113 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 12 Feb 2025 08:58:00 -0600 Subject: [PATCH 081/181] fix: add user to GitLab role teams Signed-off-by: Jonathan Howard --- .../GitLabAuthenticationCustomizer.java | 19 ++++++++++ .../integrations/gitlab/GitLabSyncer.java | 36 +++++++++++-------- .../integrations/gitlab/GitlabProject.java | 2 +- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index ccb29e0766..96001a78fd 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -23,7 +23,11 @@ import alpine.server.auth.DefaultOidcAuthenticationCustomizer; import alpine.server.auth.OidcProfile; +import java.util.ArrayList; +import java.util.List; + import org.dependencytrack.event.GitLabSyncEvent; +import org.dependencytrack.persistence.QueryManager; public class GitLabAuthenticationCustomizer extends DefaultOidcAuthenticationCustomizer { @@ -31,8 +35,23 @@ public GitLabAuthenticationCustomizer() { } + @Override + public boolean isProfileComplete(OidcProfile profile, boolean teamSyncEnabled) { + return super.isProfileComplete(profile, true); + } + @Override public OidcUser onAuthenticationSuccess(OidcUser user, OidcProfile profile, String idToken, String accessToken) { + try (QueryManager qm = new QueryManager()) { + List groups = profile.getGroups(); + groups = groups != null ? groups : new ArrayList(); + + for (String groupName : groups) { + if (qm.getOidcGroup(groupName) == null) + qm.createOidcGroup(groupName); + } + } + Event.dispatch(new GitLabSyncEvent(accessToken, user)); return user; diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 1d16af36b4..007c75cad7 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -23,7 +23,7 @@ import java.net.URI; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Date; import java.util.List; import org.dependencytrack.auth.Permissions; @@ -82,20 +82,24 @@ public void synchronize() { List gitLabProjects = gitLabClient.getGitLabProjects(); List projects = createProjects(gitLabProjects); + List teams = new ArrayList<>(); - for (Project project : projects) { - List teams = createProjectTeams(project); + for (Project project : projects) + teams.addAll(createProjectTeams(project)); - for (Team team : teams) { - List teamUsers = team.getOidcUsers(); - if (!teamUsers.contains(user)) { - teamUsers.add(user); - team.setOidcUsers(teamUsers); - } + List teamNames = new ArrayList<>(); + for (GitLabProject gitLabProject : gitLabProjects) + teamNames.add("%s_%s".formatted( + gitLabProject.getFullPath(), + gitLabProject.getMaxAccessLevel().getStringValue().toString())); - qm.updateTeam(team); - } - } + qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames); + + for (Team team : teams) + qm.updateTeam(team); + + for (Project project : projects) + qm.updateProject(project, false); } private List createProjects(List gitLabProjects) { @@ -112,7 +116,11 @@ private List createProjects(List gitLabProjects) { project = qm.persist(project); } - projects.add(project); + project.setActive(project.getLastBomImport() != null); + if (!project.isActive() && project.getInactiveSince() == null) + project.setInactiveSince(new Date()); + + projects.add(qm.updateProject(project, false)); } return projects; @@ -153,4 +161,4 @@ private List createProjectTeams(Project project) { return teams; } -} \ No newline at end of file +} diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java index 4a6f099829..467e399de3 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java @@ -83,4 +83,4 @@ public String toString() { maxAccessLevel.getStringValue().toString()); } -} \ No newline at end of file +} From 01c8e68a00ff7c1775e556e410db4b69ca1868ea Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 12 Feb 2025 18:05:26 -0700 Subject: [PATCH 082/181] tests: add gitlabsyncer tests Signed-off-by: Allen Shearin --- .../integrations/gitlab/GitLabClient.java | 63 ++++--- .../integrations/gitlab/GitLabSyncer.java | 56 ++++--- .../dependencytrack/tasks/GitLabSyncTask.java | 8 +- .../integrations/gitlab/GitLabSyncerTest.java | 158 ++++++++++++++++++ 4 files changed, 222 insertions(+), 63 deletions(-) create mode 100644 src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index fcee7fede8..94957133ad 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -53,7 +53,6 @@ public class GitLabClient { private static final String GRAPHQL_ENDPOINT = "/api/graphql"; private final String accessToken; - private final GitLabSyncer syncer; private final URI baseURL; private final Map> rolePermissions = Map.of( @@ -108,61 +107,55 @@ public class GitLabClient { Permissions.TAG_MANAGEMENT, Permissions.TAG_MANAGEMENT_DELETE)); - public GitLabClient(final GitLabSyncer syncer, final URI baseURL, final String accessToken) { + public GitLabClient(final URI baseURL, final String accessToken) { this.accessToken = accessToken; this.baseURL = baseURL; - this.syncer = syncer; } - public List getGitLabProjects() { + public List getGitLabProjects() throws IOException, URISyntaxException { List projects = new ArrayList<>(); JSONObject variables = new JSONObject(); JSONObject queryObject = new JSONObject(); - try { - queryObject.put("query", resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8)); + queryObject.put("query", resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8)); - URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT); + URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT); - HttpPost request = new HttpPost(builder.build()); - request.setHeader("Authorization", "Bearer " + accessToken); - request.setHeader("Content-Type", "application/json"); + HttpPost request = new HttpPost(builder.build()); + request.setHeader("Authorization", "Bearer " + accessToken); + request.setHeader("Content-Type", "application/json"); - while (true) { - queryObject.put("variables", variables); + while (true) { + queryObject.put("variables", variables); - StringEntity entity = new StringEntity(queryObject.toString(), StandardCharsets.UTF_8); - request.setEntity(entity); + StringEntity entity = new StringEntity(queryObject.toString(), StandardCharsets.UTF_8); + request.setEntity(entity); - try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { - HttpEntity responseEntity = response.getEntity(); + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + HttpEntity responseEntity = response.getEntity(); - if (responseEntity == null) - break; + if (responseEntity == null) + break; - String responseBody = EntityUtils.toString(responseEntity); - JSONObject responseData = JSONValue.parse(responseBody, JSONObject.class); - JSONObject dataObject = (JSONObject) responseData.get("data"); - JSONObject projectsObject = (JSONObject) dataObject.get("projects"); - JSONArray nodes = (JSONArray) projectsObject.get("nodes"); + String responseBody = EntityUtils.toString(responseEntity); + JSONObject responseData = JSONValue.parse(responseBody, JSONObject.class); + JSONObject dataObject = (JSONObject) responseData.get("data"); + JSONObject projectsObject = (JSONObject) dataObject.get("projects"); + JSONArray nodes = (JSONArray) projectsObject.get("nodes"); - for (Object nodeObject : nodes) { - JSONObject node = (JSONObject) nodeObject; - projects.add(GitLabProject.parse(node.toJSONString())); - } + for (Object nodeObject : nodes) { + JSONObject node = (JSONObject) nodeObject; + projects.add(GitLabProject.parse(node.toJSONString())); + } - JSONObject pageInfo = (JSONObject) projectsObject.get("pageInfo"); + JSONObject pageInfo = (JSONObject) projectsObject.get("pageInfo"); - if (!(boolean) pageInfo.get("hasNextPage")) - break; + if (!(boolean) pageInfo.get("hasNextPage")) + break; - variables.put("cursor", pageInfo.getAsString("endCursor")); - } + variables.put("cursor", pageInfo.getAsString("endCursor")); } - } catch (IOException | URISyntaxException ex) { - LOGGER.error("An error occurred while querying GitLab GraphQL API", ex); - syncer.handleException(LOGGER, ex); } return projects; diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 007c75cad7..a7beeeae25 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -21,7 +21,8 @@ import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BASE_URL; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; -import java.net.URI; +import java.net.URISyntaxException; +import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -31,7 +32,6 @@ import org.dependencytrack.integrations.PermissionsSyncer; import org.dependencytrack.model.Project; -import alpine.Config; import alpine.common.logging.Logger; import alpine.model.ConfigProperty; import alpine.model.OidcUser; @@ -49,9 +49,10 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private final OidcUser user; private GitLabClient gitLabClient; - public GitLabSyncer(final String accessToken, final OidcUser user) { + public GitLabSyncer(final String accessToken, final OidcUser user, final GitLabClient gitlabClient) { this.accessToken = accessToken; this.user = user; + this.gitLabClient = gitlabClient; } public String getAccessToken() { @@ -77,29 +78,31 @@ public boolean isEnabled() { @Override public void synchronize() { - final URI gitLabUrl = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); - gitLabClient = new GitLabClient(this, gitLabUrl, accessToken); - - List gitLabProjects = gitLabClient.getGitLabProjects(); - List projects = createProjects(gitLabProjects); - List teams = new ArrayList<>(); - - for (Project project : projects) - teams.addAll(createProjectTeams(project)); - - List teamNames = new ArrayList<>(); - for (GitLabProject gitLabProject : gitLabProjects) - teamNames.add("%s_%s".formatted( - gitLabProject.getFullPath(), - gitLabProject.getMaxAccessLevel().getStringValue().toString())); - - qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames); - - for (Team team : teams) - qm.updateTeam(team); - - for (Project project : projects) - qm.updateProject(project, false); + try { + List gitLabProjects = gitLabClient.getGitLabProjects(); + List projects = createProjects(gitLabProjects); + List teams = new ArrayList<>(); + + for (Project project : projects) + teams.addAll(createProjectTeams(project)); + + List teamNames = new ArrayList<>(); + for (GitLabProject gitLabProject : gitLabProjects) + teamNames.add("%s_%s".formatted( + gitLabProject.getFullPath(), + gitLabProject.getMaxAccessLevel().getStringValue().toString())); + + qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames); + + for (Team team : teams) + qm.updateTeam(team); + + for (Project project : projects) + qm.updateProject(project, false); + } catch (IOException | URISyntaxException ex) { + LOGGER.error("An error occurred while querying GitLab GraphQL API", ex); + this.handleException(LOGGER, ex); + } } private List createProjects(List gitLabProjects) { @@ -119,7 +122,6 @@ private List createProjects(List gitLabProjects) { project.setActive(project.getLastBomImport() != null); if (!project.isActive() && project.getInactiveSince() == null) project.setInactiveSince(new Date()); - projects.add(qm.updateProject(project, false)); } diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index e84f9c1aee..0ca2388d85 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.tasks; +import alpine.Config; import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.event.framework.LoggableSubscriber; @@ -25,11 +26,14 @@ import alpine.model.OidcUser; import org.dependencytrack.event.GitLabSyncEvent; +import org.dependencytrack.integrations.gitlab.GitLabClient; import org.dependencytrack.integrations.gitlab.GitLabSyncer; import org.dependencytrack.persistence.QueryManager; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import java.net.URI; + public class GitLabSyncTask implements LoggableSubscriber { private static final Logger LOGGER = Logger.getLogger(GitLabSyncTask.class); @@ -71,7 +75,9 @@ public void inform(final Event event) { LOGGER.info("Starting GitLab sync task"); try (QueryManager qm = new QueryManager()) { - GitLabSyncer syncer = new GitLabSyncer(accessToken, user); + final URI gitLabUrl = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); + GitLabClient gitLabClient = new GitLabClient(gitLabUrl, accessToken); + GitLabSyncer syncer = new GitLabSyncer(accessToken, user, gitLabClient); syncer.setQueryManager(qm); syncer.synchronize(); } diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java new file mode 100644 index 0000000000..07f4b3c69f --- /dev/null +++ b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java @@ -0,0 +1,158 @@ +/* + * 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.integrations.gitlab; + +import org.junit.Assert; +import alpine.model.IConfigProperty; +import alpine.model.OidcUser; +import alpine.model.Permission; +import alpine.model.Team; +import java.net.URISyntaxException; +import java.io.IOException; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Project; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; + +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; + + +/** + * This test suite validates the integration with the GitLab API. + */ +@RunWith(MockitoJUnitRunner.class) +public class GitLabSyncerTest extends PersistenceCapableTest { + + @Mock + private OidcUser user; + + @Mock + private GitLabClient gitLabClient; + + @InjectMocks + private GitLabSyncer gitLabSyncer; + + /** + * Validates that the integration metadata is correctly defined. + */ + @Test + public void testIntegrationMetadata() { + GitLabSyncer extension = new GitLabSyncer("test_token", user, gitLabClient); + Assert.assertEquals("GitLab", extension.name()); + Assert.assertEquals("Synchronizes user permissions from connected GitLab instance", extension.description()); + } + + /** + * Validates that the integration is enabled when the GITLAB_ENABLED property is + * set to true. + */ + @Test + public void testIsEnabled() { + qm.createConfigProperty( + GITLAB_ENABLED.getGroupName(), + GITLAB_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + GitLabSyncer extension = new GitLabSyncer("test_token", user, gitLabClient); + extension.setQueryManager(qm); + Assert.assertTrue(extension.isEnabled()); + } + + /** + * Validates that the integration is disabled when the GITLAB_ENABLED property + * is set to false. + */ + @Test + public void testIsDisabled() { + qm.createConfigProperty( + GITLAB_ENABLED.getGroupName(), + GITLAB_ENABLED.getPropertyName(), + "false", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + GitLabSyncer extension = new GitLabSyncer("test_token", user, gitLabClient); + extension.setQueryManager(qm); + Assert.assertFalse(extension.isEnabled()); + } + + /** + * Validates that the synchronize method is correctly executed when the + * integration is enabled. + */ + @Test + public void testSynchronizeSuccess() { + qm.createConfigProperty( + GITLAB_ENABLED.getGroupName(), + GITLAB_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null + ); + qm.createOidcUser( + "test_user" + ); + OidcUser testUser = new OidcUser(); + testUser.setUsername("test_user"); + GitLabClient mockClient = mock(GitLabClient.class); + GitLabSyncer extension = new GitLabSyncer("test_token", testUser, mockClient); + extension.setQueryManager(qm); + try{ + when(mockClient.getGitLabProjects()).thenReturn(Arrays.asList(new GitLabProject("project1", "this/test/project1", GitLabRole.MAINTAINER), + new GitLabProject("project2", "that/test/project2", GitLabRole.REPORTER))); + extension.synchronize(); + }catch (IOException | URISyntaxException ex) { + Assert.fail("Exception " + ex); + } + Project testProject1 = qm.getProject("this/test/project1", null); + Assert.assertFalse(testProject1.isActive()); + + Project testProject2 = qm.getProject("that/test/project2", null); + Assert.assertFalse(testProject2.isActive()); + + List testTeams = new ArrayList(); + Team team1 = qm.getTeam("this/test/project1_MAINTAINER"); + testTeams.add(team1); + List t1perm = team1.getPermissions(); + Assert.assertEquals(t1perm.size(), mockClient.getRolePermissions(GitLabRole.MAINTAINER).size()); + + Team team2 = qm.getTeam("that/test/project2_REPORTER"); + testTeams.add(team2); + List t2perm = team2.getPermissions(); + Assert.assertEquals(t2perm.size(), mockClient.getRolePermissions(GitLabRole.REPORTER).size()); + + for (Team team : testTeams) { + List testTeamUsers = team.getOidcUsers(); + Assert.assertEquals(testTeamUsers.size(), 1); + Assert.assertEquals(testTeamUsers.get(0).getUsername(), "test_user"); + } + } +} \ No newline at end of file From f546b726c4c5b8ebb3e03f599698b8d5cd8f2b74 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Thu, 13 Feb 2025 10:29:46 -0600 Subject: [PATCH 083/181] style: use modern Java conventions Signed-off-by: Jonathan Howard --- .../GitLabAuthenticationCustomizer.java | 7 +-- .../integrations/gitlab/GitLabSyncer.java | 60 ++++++++----------- .../dependencytrack/tasks/GitLabSyncTask.java | 2 + 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index 96001a78fd..1d86dc706d 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -46,10 +46,9 @@ public OidcUser onAuthenticationSuccess(OidcUser user, OidcProfile profile, Stri List groups = profile.getGroups(); groups = groups != null ? groups : new ArrayList(); - for (String groupName : groups) { - if (qm.getOidcGroup(groupName) == null) - qm.createOidcGroup(groupName); - } + groups.stream() + .filter(groupName -> groupName == null) + .map(groupName -> qm.createOidcGroup(groupName)); } Event.dispatch(new GitLabSyncEvent(accessToken, user)); diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index a7beeeae25..0bcadad701 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -27,7 +27,6 @@ import java.util.Date; import java.util.List; -import org.dependencytrack.auth.Permissions; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PermissionsSyncer; import org.dependencytrack.model.Project; @@ -35,7 +34,6 @@ import alpine.common.logging.Logger; import alpine.model.ConfigProperty; import alpine.model.OidcUser; -import alpine.model.Permission; import alpine.model.Team; public class GitLabSyncer extends AbstractIntegrationPoint implements PermissionsSyncer { @@ -78,37 +76,30 @@ public boolean isEnabled() { @Override public void synchronize() { - try { - List gitLabProjects = gitLabClient.getGitLabProjects(); - List projects = createProjects(gitLabProjects); - List teams = new ArrayList<>(); - - for (Project project : projects) - teams.addAll(createProjectTeams(project)); - - List teamNames = new ArrayList<>(); - for (GitLabProject gitLabProject : gitLabProjects) - teamNames.add("%s_%s".formatted( + final URI gitLabUrl = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); + gitLabClient = new GitLabClient(this, gitLabUrl, accessToken); + + List gitLabProjects = gitLabClient.getGitLabProjects(); + List projects = createProjects(gitLabProjects); + List teams = projects.stream() + .flatMap(project -> createProjectTeams(project).stream()) + .toList(); + List teamNames = gitLabProjects.stream() + .map(gitLabProject -> "%s_%s".formatted( gitLabProject.getFullPath(), - gitLabProject.getMaxAccessLevel().getStringValue().toString())); - - qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames); - - for (Team team : teams) - qm.updateTeam(team); - - for (Project project : projects) - qm.updateProject(project, false); - } catch (IOException | URISyntaxException ex) { - LOGGER.error("An error occurred while querying GitLab GraphQL API", ex); - this.handleException(LOGGER, ex); - } + gitLabProject.getMaxAccessLevel().getStringValue().toString())) + .toList(); + + qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames); + + teams = teams.stream().map(team -> qm.updateTeam(team)).toList(); + projects = projects.stream().map(project -> qm.updateProject(project, false)).toList(); } private List createProjects(List gitLabProjects) { List projects = new ArrayList<>(); - for (GitLabProject gitLabProject : gitLabProjects) { + for (var gitLabProject : gitLabProjects) { Project project = qm.getProject(gitLabProject.getFullPath(), null); if (project == null) { @@ -138,20 +129,17 @@ private List createProjects(List gitLabProjects) { private List createProjectTeams(Project project) { List teams = new ArrayList<>(); - for (GitLabRole role : GitLabRole.values()) { + for (var role : GitLabRole.values()) { final String teamName = "%s_%s".formatted(project.getName(), role.name()); Team team = qm.getTeam(teamName); team = team != null ? team : qm.createTeam(teamName); - List rolePermissions = gitLabClient.getRolePermissions(role); - List permissions = new ArrayList<>(team.getPermissions()); - - for (Permissions rolePermission : rolePermissions) { - Permission permission = qm.getPermission(rolePermission.name()); - if (permission != null && !permissions.contains(permission)) - permissions.add(permission); - } + var permissions = gitLabClient.getRolePermissions(role).stream() + .map(rolePermission -> qm.getPermission(rolePermission.name())) + .filter(permission -> permission != null) + .distinct() + .toList(); team.setPermissions(permissions); project.addAccessTeam(team); diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index 0ca2388d85..c78cadeecb 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -82,6 +82,8 @@ public void inform(final Event event) { syncer.synchronize(); } + LOGGER.info("GitLab sync complete"); + // TODO: // - [X] Assign authenticated OIDC user the VIEW_PORTFOLIO permission // - [ ] Get user GitLab project memberships (use alpine.security.crypto.DataEncryption for request) From 3465b52ae188d3d7d22e7588c070a4b29124bc4f Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Thu, 13 Feb 2025 10:30:16 -0600 Subject: [PATCH 084/181] perf: exclude archived GitLab projects Signed-off-by: Jonathan Howard --- apiserver/src/main/resources/graphql/gitlab-projects.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/src/main/resources/graphql/gitlab-projects.graphql b/apiserver/src/main/resources/graphql/gitlab-projects.graphql index 54c88a4ae6..31bc1d3a43 100644 --- a/apiserver/src/main/resources/graphql/gitlab-projects.graphql +++ b/apiserver/src/main/resources/graphql/gitlab-projects.graphql @@ -1,5 +1,5 @@ query ($cursor: String) { - projects(membership: true, first: 10, after: $cursor) { + projects(archived: EXCLUDE, membership: true, first: 10, after: $cursor) { nodes { name fullPath From 57f7f2f0e7d5addd6ea06d7ba683f11a9bde0be7 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Thu, 13 Feb 2025 11:17:19 -0600 Subject: [PATCH 085/181] refactor: configurable GraphQL query inputs Signed-off-by: Jonathan Howard --- .../main/resources/graphql/gitlab-projects.graphql | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apiserver/src/main/resources/graphql/gitlab-projects.graphql b/apiserver/src/main/resources/graphql/gitlab-projects.graphql index 31bc1d3a43..9eb08ab49f 100644 --- a/apiserver/src/main/resources/graphql/gitlab-projects.graphql +++ b/apiserver/src/main/resources/graphql/gitlab-projects.graphql @@ -1,5 +1,15 @@ -query ($cursor: String) { - projects(archived: EXCLUDE, membership: true, first: 10, after: $cursor) { +query ( + $cursor: String + $archived: ProjectArchived = EXCLUDE + $topics: [String!] = [] +) { + projects( + archived: $archived + topics: $topics + membership: true + first: 100 + after: $cursor + ) { nodes { name fullPath From 4f48c6df9ee9a4a102f03d0d88b34ad6764cb1f6 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 13 Feb 2025 10:35:24 -0700 Subject: [PATCH 086/181] chore: move baseURL initialization to gitlab client constructor Signed-off-by: Allen Shearin --- .../dependencytrack/integrations/gitlab/GitLabClient.java | 5 +++-- .../main/java/org/dependencytrack/tasks/GitLabSyncTask.java | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index 94957133ad..003d8baaf3 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -38,6 +38,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.common.HttpClientPool; +import alpine.Config; import alpine.common.logging.Logger; import net.minidev.json.JSONArray; @@ -107,9 +108,9 @@ public class GitLabClient { Permissions.TAG_MANAGEMENT, Permissions.TAG_MANAGEMENT_DELETE)); - public GitLabClient(final URI baseURL, final String accessToken) { + public GitLabClient(final String accessToken) { this.accessToken = accessToken; - this.baseURL = baseURL; + this.baseURL = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER));; } public List getGitLabProjects() throws IOException, URISyntaxException { diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index c78cadeecb..c5dd6192c7 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.tasks; -import alpine.Config; import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.event.framework.LoggableSubscriber; @@ -32,8 +31,6 @@ import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; -import java.net.URI; - public class GitLabSyncTask implements LoggableSubscriber { private static final Logger LOGGER = Logger.getLogger(GitLabSyncTask.class); @@ -75,8 +72,7 @@ public void inform(final Event event) { LOGGER.info("Starting GitLab sync task"); try (QueryManager qm = new QueryManager()) { - final URI gitLabUrl = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); - GitLabClient gitLabClient = new GitLabClient(gitLabUrl, accessToken); + GitLabClient gitLabClient = new GitLabClient(accessToken); GitLabSyncer syncer = new GitLabSyncer(accessToken, user, gitLabClient); syncer.setQueryManager(qm); syncer.synchronize(); From 1c23235b1d57531adc2726819de82528ee1554cf Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 13 Feb 2025 11:08:55 -0700 Subject: [PATCH 087/181] chore: remove duplicate accesstoken member Signed-off-by: Allen Shearin --- .../integrations/gitlab/GitLabSyncer.java | 46 +++++++++---------- .../dependencytrack/tasks/GitLabSyncTask.java | 2 +- .../integrations/gitlab/GitLabSyncerTest.java | 8 ++-- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 0bcadad701..c6dd2fe24d 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -43,20 +43,14 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private static final String GENERAL_GROUP = GENERAL_BASE_URL.getGroupName(); private static final String ROLE_CLAIM_PREFIX = "https://gitlab.org/claims/groups/"; - private final String accessToken; private final OidcUser user; private GitLabClient gitLabClient; - public GitLabSyncer(final String accessToken, final OidcUser user, final GitLabClient gitlabClient) { - this.accessToken = accessToken; + public GitLabSyncer(final OidcUser user, final GitLabClient gitlabClient) { this.user = user; this.gitLabClient = gitlabClient; } - public String getAccessToken() { - return accessToken; - } - @Override public String name() { return "GitLab"; @@ -76,24 +70,26 @@ public boolean isEnabled() { @Override public void synchronize() { - final URI gitLabUrl = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER)); - gitLabClient = new GitLabClient(this, gitLabUrl, accessToken); - - List gitLabProjects = gitLabClient.getGitLabProjects(); - List projects = createProjects(gitLabProjects); - List teams = projects.stream() - .flatMap(project -> createProjectTeams(project).stream()) - .toList(); - List teamNames = gitLabProjects.stream() - .map(gitLabProject -> "%s_%s".formatted( - gitLabProject.getFullPath(), - gitLabProject.getMaxAccessLevel().getStringValue().toString())) - .toList(); - - qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames); - - teams = teams.stream().map(team -> qm.updateTeam(team)).toList(); - projects = projects.stream().map(project -> qm.updateProject(project, false)).toList(); + try { + List gitLabProjects = gitLabClient.getGitLabProjects(); + List projects = createProjects(gitLabProjects); + List teams = projects.stream() + .flatMap(project -> createProjectTeams(project).stream()) + .toList(); + List teamNames = gitLabProjects.stream() + .map(gitLabProject -> "%s_%s".formatted( + gitLabProject.getFullPath(), + gitLabProject.getMaxAccessLevel().getStringValue().toString())) + .toList(); + + qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames); + + teams = teams.stream().map(team -> qm.updateTeam(team)).toList(); + projects = projects.stream().map(project -> qm.updateProject(project, false)).toList(); + } catch (IOException | URISyntaxException ex) { + LOGGER.error("An error occurred while querying GitLab GraphQL API", ex); + handleException(LOGGER, ex); + } } private List createProjects(List gitLabProjects) { diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index c5dd6192c7..dd765268aa 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -73,7 +73,7 @@ public void inform(final Event event) { try (QueryManager qm = new QueryManager()) { GitLabClient gitLabClient = new GitLabClient(accessToken); - GitLabSyncer syncer = new GitLabSyncer(accessToken, user, gitLabClient); + GitLabSyncer syncer = new GitLabSyncer(user, gitLabClient); syncer.setQueryManager(qm); syncer.synchronize(); } diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java index 07f4b3c69f..a8f4d275f9 100644 --- a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java +++ b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java @@ -63,7 +63,7 @@ public class GitLabSyncerTest extends PersistenceCapableTest { */ @Test public void testIntegrationMetadata() { - GitLabSyncer extension = new GitLabSyncer("test_token", user, gitLabClient); + GitLabSyncer extension = new GitLabSyncer(user, gitLabClient); Assert.assertEquals("GitLab", extension.name()); Assert.assertEquals("Synchronizes user permissions from connected GitLab instance", extension.description()); } @@ -81,7 +81,7 @@ public void testIsEnabled() { IConfigProperty.PropertyType.BOOLEAN, null ); - GitLabSyncer extension = new GitLabSyncer("test_token", user, gitLabClient); + GitLabSyncer extension = new GitLabSyncer(user, gitLabClient); extension.setQueryManager(qm); Assert.assertTrue(extension.isEnabled()); } @@ -99,7 +99,7 @@ public void testIsDisabled() { IConfigProperty.PropertyType.BOOLEAN, null ); - GitLabSyncer extension = new GitLabSyncer("test_token", user, gitLabClient); + GitLabSyncer extension = new GitLabSyncer(user, gitLabClient); extension.setQueryManager(qm); Assert.assertFalse(extension.isEnabled()); } @@ -123,7 +123,7 @@ public void testSynchronizeSuccess() { OidcUser testUser = new OidcUser(); testUser.setUsername("test_user"); GitLabClient mockClient = mock(GitLabClient.class); - GitLabSyncer extension = new GitLabSyncer("test_token", testUser, mockClient); + GitLabSyncer extension = new GitLabSyncer(testUser, mockClient); extension.setQueryManager(qm); try{ when(mockClient.getGitLabProjects()).thenReturn(Arrays.asList(new GitLabProject("project1", "this/test/project1", GitLabRole.MAINTAINER), From c36fd939ac9cfb20493e0320f4511bc51b8f3725 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 17 Feb 2025 14:57:39 -0600 Subject: [PATCH 088/181] refactor: locks to manage concurrent project access Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitLabSyncer.java | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index c6dd2fe24d..51cb87999f 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -26,6 +26,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.concurrent.locks.Lock; + +import com.google.common.util.concurrent.Striped; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PermissionsSyncer; @@ -44,9 +47,12 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private static final String ROLE_CLAIM_PREFIX = "https://gitlab.org/claims/groups/"; private final OidcUser user; + private final Striped locks; + private GitLabClient gitLabClient; public GitLabSyncer(final OidcUser user, final GitLabClient gitlabClient) { + this.locks = Striped.lock(128); this.user = user; this.gitLabClient = gitlabClient; } @@ -96,20 +102,28 @@ private List createProjects(List gitLabProjects) { List projects = new ArrayList<>(); for (var gitLabProject : gitLabProjects) { - Project project = qm.getProject(gitLabProject.getFullPath(), null); + final Lock lock = locks.get(gitLabProject.getFullPath()); + lock.lock(); - if (project == null) { - LOGGER.debug("Creating project " + gitLabProject.getFullPath()); + try { + Project project = qm.getProject(gitLabProject.getFullPath(), null); - project = new Project(); - project.setName(gitLabProject.getFullPath()); - project = qm.persist(project); - } + if (project == null) { + LOGGER.debug("Creating project " + gitLabProject.getFullPath()); - project.setActive(project.getLastBomImport() != null); - if (!project.isActive() && project.getInactiveSince() == null) - project.setInactiveSince(new Date()); - projects.add(qm.updateProject(project, false)); + project = new Project(); + project.setName(gitLabProject.getFullPath()); + project = qm.persist(project); + } + + project.setActive(project.getLastBomImport() != null); + if (!project.isActive() && project.getInactiveSince() == null) + project.setInactiveSince(new Date()); + + projects.add(qm.updateProject(project, false)); + } finally { + lock.unlock(); + } } return projects; @@ -127,21 +141,27 @@ private List createProjectTeams(Project project) { for (var role : GitLabRole.values()) { final String teamName = "%s_%s".formatted(project.getName(), role.name()); - - Team team = qm.getTeam(teamName); - team = team != null ? team : qm.createTeam(teamName); - - var permissions = gitLabClient.getRolePermissions(role).stream() - .map(rolePermission -> qm.getPermission(rolePermission.name())) - .filter(permission -> permission != null) - .distinct() - .toList(); - - team.setPermissions(permissions); - project.addAccessTeam(team); - qm.updateProject(project, false); - - teams.add(team); + final Lock lock = locks.get(project.getName()); + lock.lock(); + + try { + Team team = qm.getTeam(teamName); + team = team != null ? team : qm.createTeam(teamName); + + var permissions = gitLabClient.getRolePermissions(role).stream() + .map(rolePermission -> qm.getPermission(rolePermission.name())) + .filter(permission -> permission != null) + .distinct() + .toList(); + + team.setPermissions(permissions); + project.addAccessTeam(team); + qm.updateProject(project, false); + + teams.add(team); + } finally { + lock.unlock(); + } } return teams; From 7fb9a8359977318dcdf17cddbab93a8486fc61d8 Mon Sep 17 00:00:00 2001 From: Philippe Date: Mon, 24 Feb 2025 13:58:07 -0400 Subject: [PATCH 089/181] add getGitLabProjects unit test Signed-off-by: Philippe --- .../integrations/gitlab/GitLabClient.java | 8 +- .../integrations/gitlab/GitLabClientTest.java | 88 +++++++++++++++++++ ...api-getgitlabprojects-response-page-1.json | 33 +++++++ ...api-getgitlabprojects-response-page-2.json | 19 ++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java create mode 100644 apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-1.json create mode 100644 apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-2.json diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index 003d8baaf3..f9952e1301 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -55,6 +55,7 @@ public class GitLabClient { private final String accessToken; private final URI baseURL; + private final Config config; private final Map> rolePermissions = Map.of( GitLabRole.GUEST, List.of( @@ -109,8 +110,13 @@ public class GitLabClient { Permissions.TAG_MANAGEMENT_DELETE)); public GitLabClient(final String accessToken) { + this(accessToken, Config.getInstance()); + } + + public GitLabClient(final String accessToken, final Config config) { + this.config = config; this.accessToken = accessToken; - this.baseURL = URI.create(Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER));; + this.baseURL = URI.create(config.getProperty(Config.AlpineKey.OIDC_ISSUER)); } public List getGitLabProjects() throws IOException, URISyntaxException { diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java new file mode 100644 index 0000000000..e28e3a6701 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -0,0 +1,88 @@ +package org.dependencytrack.integrations.gitlab; + +import alpine.Config; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.stubbing.Scenario; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.http.HttpHeaders; +import org.dependencytrack.event.kafka.KafkaProducerInitializer; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +import static org.testcontainers.shaded.org.apache.commons.io.IOUtils.resourceToString; + +public class GitLabClientTest { + + @BeforeClass + public static void beforeClass() { + Config.enableUnitTests(); + } + + @AfterClass + public static void after() { + KafkaProducerInitializer.tearDown(); + } + + @Rule + public WireMockRule wireMockRule = new WireMockRule(); + + @Test + public void testGetGitLabProjects() throws URISyntaxException, IOException { + String accessToken = "TEST_ACCESS_TOKEN"; + + String page1Result = resourceToString("/unit/gitlab-api-getgitlabprojects-response-page-1.json", + StandardCharsets.UTF_8); + String page2Result = resourceToString("/unit/gitlab-api-getgitlabprojects-response-page-2.json", + StandardCharsets.UTF_8); + + WireMock.stubFor(WireMock.post("/api/graphql") + .inScenario("test-get-gitlab-projects") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withBody(page1Result)) + .willSetStateTo("second-page")); + + WireMock.stubFor(WireMock.post("/api/graphql") + .inScenario("test-get-gitlab-projects") + .whenScenarioStateIs("second-page") + .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withBody(page2Result)) + .willSetStateTo("Finished")); + + final var configMock = mock(Config.class); + + when(configMock.getProperty(eq(Config.AlpineKey.OIDC_ISSUER))).thenReturn(wireMockRule.baseUrl()); + + GitLabClient gitLabClient = new GitLabClient(accessToken, configMock); + + List gitLabProjects = gitLabClient.getGitLabProjects(); + + List actualProjectPaths = new ArrayList(); + for (GitLabProject project : gitLabProjects) { + actualProjectPaths.add(project.getFullPath()); + } + + List expectedProjectPaths = Arrays.asList( + "test-group/test-subgroup/test-project-1", + "test-group/test-subgroup/test-project-2", + "test-group/test-subgroup-2/test-project-3", + "test-group/test-subgroup-2/test-project-4"); + + Assert.assertEquals(actualProjectPaths, expectedProjectPaths); + } +} \ No newline at end of file diff --git a/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-1.json b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-1.json new file mode 100644 index 0000000000..8d4008e744 --- /dev/null +++ b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-1.json @@ -0,0 +1,33 @@ +{ + "data": { + "projects": { + "nodes": [ + { + "name": "test-project-1", + "fullPath": "test-group/test-subgroup/test-project-1", + "maxAccessLevel": { + "stringValue": "DEVELOPER" + } + }, + { + "name": "test-project-2", + "fullPath": "test-group/test-subgroup/test-project-2", + "maxAccessLevel": { + "stringValue": "REPORTER" + } + }, + { + "name": "test-project-3", + "fullPath": "test-group/test-subgroup-2/test-project-3", + "maxAccessLevel": { + "stringValue": "REPORTER" + } + } + ], + "pageInfo": { + "endCursor": "eyJpZCI6IjE1ODU5NyJ9", + "hasNextPage": true + } + } + } +} diff --git a/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-2.json b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-2.json new file mode 100644 index 0000000000..6ef7d5a45a --- /dev/null +++ b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-2.json @@ -0,0 +1,19 @@ +{ + "data": { + "projects": { + "nodes": [ + { + "name": "test-project-4", + "fullPath": "test-group/test-subgroup-2/test-project-4", + "maxAccessLevel": { + "stringValue": "DEVELOPER" + } + }, + ], + "pageInfo": { + "endCursor": "zzzzzzzzzzzzzzzzzzzz", + "hasNextPage": false + } + } + } +} From c0bd749a61b94ec486a7a0fa1c729d4cd3893dea Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 24 Feb 2025 12:07:08 -0600 Subject: [PATCH 090/181] refactor: remove GitLabProject name field Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitlabProject.java | 14 +++----------- .../main/resources/graphql/gitlab-projects.graphql | 1 - 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java index 467e399de3..71458a9886 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java @@ -41,20 +41,14 @@ GitLabRole getStringValue() { } } - private final String name; private final String fullPath; private final MaxAccessLevel maxAccessLevel; - GitLabProject(final String name, final String fullPath, final GitLabRole maxAccessLevel) { - this.name = name; + GitLabProject(final String fullPath, final GitLabRole maxAccessLevel) { this.fullPath = fullPath; this.maxAccessLevel = new MaxAccessLevel(maxAccessLevel); } - public String getName() { - return name; - } - public String getFullPath() { return fullPath; } @@ -65,20 +59,18 @@ public MaxAccessLevel getMaxAccessLevel() { public static GitLabProject parse(final String data) { JSONObject obj = JSONValue.parse(data, JSONObject.class); - String name = obj.getAsString("name"); String fullPath = obj.getAsString("fullPath"); JSONObject maxAccessLevel = (JSONObject) obj.get("maxAccessLevel"); String stringValue = maxAccessLevel.getAsString("stringValue"); - return new GitLabProject(name, fullPath, GitLabRole.valueOf(stringValue)); + return new GitLabProject(fullPath, GitLabRole.valueOf(stringValue)); } @Override public String toString() { - return "%s{name=%s, fullPath=%s, maxAccessLevel=%s}".formatted( + return "%s{fullPath=%s, maxAccessLevel=%s}".formatted( getClass().getSimpleName(), - name, fullPath, maxAccessLevel.getStringValue().toString()); } diff --git a/apiserver/src/main/resources/graphql/gitlab-projects.graphql b/apiserver/src/main/resources/graphql/gitlab-projects.graphql index 9eb08ab49f..384ecadf1a 100644 --- a/apiserver/src/main/resources/graphql/gitlab-projects.graphql +++ b/apiserver/src/main/resources/graphql/gitlab-projects.graphql @@ -11,7 +11,6 @@ query ( after: $cursor ) { nodes { - name fullPath maxAccessLevel { stringValue From 0ba1ff40ef575db976e0a9d9e7b627c9a0eabcfb Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 24 Feb 2025 15:24:35 -0600 Subject: [PATCH 091/181] refactor: add GitLab integration constants Signed-off-by: Jonathan Howard --- .../model/ConfigPropertyConstants.java | 2 + .../integrations/gitlab/GitLabClientTest.java | 108 ++++++++++-------- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 23d271cf4c..5d5f0b8afe 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -89,6 +89,8 @@ public enum ConfigPropertyConstants { GITLAB_ENABLED("integrations", "gitlab.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitLab integration", ConfigPropertyAccessMode.READ_WRITE), GITLAB_GROUPS("integrations", "gitlab.groups", "[]", PropertyType.STRING, "JSON array of GitLab group names for which to create teams/roles", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_INCLUDE_ARCHIVED("integrations", "gitlab.include.archived", "false", PropertyType.BOOLEAN, "Flag to enable/disable syncing of archived GitLab projects", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_TOPICS("integrations", "gitlab.topics", "[]", PropertyType.STRING, "JSON array of topics to include when syncing GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_APP_ID("integrations", "gitlab.app.id", null, PropertyType.STRING, "ID for the configured GitLab application", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_ENABLED("integrations", "defectdojo.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo integration", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_REIMPORT_ENABLED("integrations", "defectdojo.reimport.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo reimport-scan API endpoint", ConfigPropertyAccessMode.READ_WRITE), diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index e28e3a6701..fc76f584d8 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -1,3 +1,21 @@ +/* + * 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.integrations.gitlab; import alpine.Config; @@ -28,61 +46,61 @@ public class GitLabClientTest { - @BeforeClass - public static void beforeClass() { - Config.enableUnitTests(); - } + @BeforeClass + public static void beforeClass() { + Config.enableUnitTests(); + } - @AfterClass - public static void after() { - KafkaProducerInitializer.tearDown(); - } + @AfterClass + public static void after() { + KafkaProducerInitializer.tearDown(); + } - @Rule - public WireMockRule wireMockRule = new WireMockRule(); + @Rule + public WireMockRule wireMockRule = new WireMockRule(); - @Test - public void testGetGitLabProjects() throws URISyntaxException, IOException { - String accessToken = "TEST_ACCESS_TOKEN"; + @Test + public void testGetGitLabProjects() throws URISyntaxException, IOException { + String accessToken = "TEST_ACCESS_TOKEN"; - String page1Result = resourceToString("/unit/gitlab-api-getgitlabprojects-response-page-1.json", - StandardCharsets.UTF_8); - String page2Result = resourceToString("/unit/gitlab-api-getgitlabprojects-response-page-2.json", - StandardCharsets.UTF_8); + String page1Result = resourceToString("/unit/gitlab-api-getgitlabprojects-response-page-1.json", + StandardCharsets.UTF_8); + String page2Result = resourceToString("/unit/gitlab-api-getgitlabprojects-response-page-2.json", + StandardCharsets.UTF_8); - WireMock.stubFor(WireMock.post("/api/graphql") - .inScenario("test-get-gitlab-projects") - .whenScenarioStateIs(Scenario.STARTED) - .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") - .withBody(page1Result)) - .willSetStateTo("second-page")); + WireMock.stubFor(WireMock.post("/api/graphql") + .inScenario("test-get-gitlab-projects") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withBody(page1Result)) + .willSetStateTo("second-page")); - WireMock.stubFor(WireMock.post("/api/graphql") - .inScenario("test-get-gitlab-projects") - .whenScenarioStateIs("second-page") - .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") - .withBody(page2Result)) - .willSetStateTo("Finished")); + WireMock.stubFor(WireMock.post("/api/graphql") + .inScenario("test-get-gitlab-projects") + .whenScenarioStateIs("second-page") + .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withBody(page2Result)) + .willSetStateTo("Finished")); - final var configMock = mock(Config.class); + final var configMock = mock(Config.class); - when(configMock.getProperty(eq(Config.AlpineKey.OIDC_ISSUER))).thenReturn(wireMockRule.baseUrl()); + when(configMock.getProperty(eq(Config.AlpineKey.OIDC_ISSUER))).thenReturn(wireMockRule.baseUrl()); - GitLabClient gitLabClient = new GitLabClient(accessToken, configMock); + GitLabClient gitLabClient = new GitLabClient(accessToken, configMock); - List gitLabProjects = gitLabClient.getGitLabProjects(); + List gitLabProjects = gitLabClient.getGitLabProjects(); - List actualProjectPaths = new ArrayList(); - for (GitLabProject project : gitLabProjects) { - actualProjectPaths.add(project.getFullPath()); - } + List actualProjectPaths = new ArrayList<>(); + for (var project : gitLabProjects) + actualProjectPaths.add(project.getFullPath()); - List expectedProjectPaths = Arrays.asList( - "test-group/test-subgroup/test-project-1", - "test-group/test-subgroup/test-project-2", - "test-group/test-subgroup-2/test-project-3", - "test-group/test-subgroup-2/test-project-4"); + List expectedProjectPaths = Arrays.asList( + "test-group/test-subgroup/test-project-1", + "test-group/test-subgroup/test-project-2", + "test-group/test-subgroup-2/test-project-3", + "test-group/test-subgroup-2/test-project-4"); - Assert.assertEquals(actualProjectPaths, expectedProjectPaths); - } -} \ No newline at end of file + Assert.assertEquals(actualProjectPaths, expectedProjectPaths); + } + +} From 92e64ec1d1f9a736a24975d2edd2a1696ed3de81 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Thu, 20 Feb 2025 10:49:30 -0600 Subject: [PATCH 092/181] fix: changeset unique constraints Signed-off-by: Jonathan Howard Signed-off-by: Allen Shearin --- apiserver/src/main/resources/META-INF/persistence.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apiserver/src/main/resources/META-INF/persistence.xml b/apiserver/src/main/resources/META-INF/persistence.xml index 83435e63d5..6d71d8b85e 100644 --- a/apiserver/src/main/resources/META-INF/persistence.xml +++ b/apiserver/src/main/resources/META-INF/persistence.xml @@ -34,6 +34,7 @@ org.dependencytrack.model.FindingAttribution org.dependencytrack.model.License org.dependencytrack.model.LicenseGroup + org.dependencytrack.model.MappedRole org.dependencytrack.model.NotificationPublisher org.dependencytrack.model.NotificationRule org.dependencytrack.model.Policy @@ -46,6 +47,7 @@ org.dependencytrack.model.ProjectProperty org.dependencytrack.model.Repository org.dependencytrack.model.RepositoryMetaComponent + org.dependencytrack.model.Role org.dependencytrack.model.ServiceComponent org.dependencytrack.model.Tag org.dependencytrack.model.Vex From 5f8ce347b9d471276345557863f06a2a25102efd Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 17 Feb 2025 19:19:35 +0100 Subject: [PATCH 093/181] Consider project hierarchies for portfolio ACL checks Signed-off-by: nscuro Signed-off-by: Allen Shearin --- .../method/ProjectIsAccessibleByMethod.java | 109 ++++++++ .../ProjectIsAccessibleByMethodTest.java | 239 ++++++++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 src/main/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethod.java create mode 100644 src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java diff --git a/src/main/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethod.java b/src/main/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethod.java new file mode 100644 index 0000000000..d8af7f3342 --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethod.java @@ -0,0 +1,109 @@ +/* + * 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.datanucleus.method; + +import org.datanucleus.store.query.expression.Expression; +import org.datanucleus.store.rdbms.mapping.java.JavaTypeMapping; +import org.datanucleus.store.rdbms.sql.SQLStatement; +import org.datanucleus.store.rdbms.sql.expression.ArrayLiteral; +import org.datanucleus.store.rdbms.sql.expression.BooleanExpression; +import org.datanucleus.store.rdbms.sql.expression.BooleanLiteral; +import org.datanucleus.store.rdbms.sql.expression.ObjectExpression; +import org.datanucleus.store.rdbms.sql.expression.SQLExpression; +import org.datanucleus.store.rdbms.sql.expression.StringExpression; +import org.datanucleus.store.rdbms.sql.expression.StringLiteral; +import org.datanucleus.store.rdbms.sql.method.SQLMethod; +import org.dependencytrack.model.Project; + +import java.util.List; +import java.util.StringJoiner; + +/** + * @since 5.6.0 + */ +public class ProjectIsAccessibleByMethod implements SQLMethod { + + @Override + public SQLExpression getExpression( + final SQLStatement stmt, + final SQLExpression expr, + final List args) { + if (!(expr instanceof final ObjectExpression objectExpr)) { + // DataNucleus should prevent this from ever happening since + // the method is explicitly registered for java.lang.Object. + throw new IllegalStateException( + "Expected expression to be of type %s, but got: %s".formatted( + ObjectExpression.class.getName(), expr.getClass().getName())); + } + + final String objectTypeName = objectExpr.getJavaTypeMapping().getType(); + if (!Project.class.getName().equals(objectTypeName)) { + throw new IllegalStateException( + "isAccessibleBy is only allowed for objects of type %s, but was called on %s".formatted( + Project.class.getName(), objectTypeName)); + } + + if (args == null) { + throw new IllegalArgumentException(); + } else if (args.size() != 1) { + throw new IllegalArgumentException("Expected exactly one argument, but got " + args.size()); + } + + // TODO: When a list, set, etc. is passed as argument, it will be of type CollectionLiteral. + // Array literals are easier to verify the type of, hence we're focusing on that for now. + + if (!(args.getFirst() instanceof final ArrayLiteral arrayLiteralArg)) { + throw new IllegalArgumentException( + "Expected argument to be of type %s, but got %s".formatted( + ArrayLiteral.class.getName(), args.getFirst().getClass().getName())); + } + if (!(arrayLiteralArg.getValue() instanceof final Long[] teamIds)) { + throw new IllegalArgumentException( + "Expected array argument to be of type %s, but got %s".formatted( + Long[].class.getName(), arrayLiteralArg.getValue().getClass().getName())); + } + + final JavaTypeMapping booleanTypeMapping = stmt.getSQLExpressionFactory().getMappingForType(Boolean.class); + final JavaTypeMapping stringTypeMapping = stmt.getSQLExpressionFactory().getMappingForType(String.class); + + // Transform the array literal to have the correct type for Postgres. + // Will result in the following expression: cast('{1,2,3}' as bigint[]) + final StringJoiner joiner = new StringJoiner(",", "{", "}"); + for (final Long teamId : teamIds) { + joiner.add(String.valueOf(teamId)); + } + final var teamIdsLiteral = new StringLiteral( + stmt, stringTypeMapping, joiner.toString(), null); + final var teamIdsExpr = new StringExpression( + stmt, stringTypeMapping, "cast", List.of(teamIdsLiteral), List.of("bigint[]")); + + // NB: objectExpr will compile to a reference of the object table's ID column, e.g.: + // * "A0"."ID" + // * "B0"."PROJECT_ID" + final var hasProjectAccessFunctionExpr = new StringExpression( + stmt, stringTypeMapping, "has_project_access", List.of(objectExpr, teamIdsExpr)); + + // Wrap the function call in a boolean expression. Final result(s) will be: + // * has_project_access("A0"."ID", cast('{1,2,3}' as bigint[])) = TRUE + // * has_project_access("B0"."PROJECT_ID", cast('{1,2,3}' as bigint[])) = TRUE + final var booleanTrueLiteral = new BooleanLiteral(stmt, booleanTypeMapping, Boolean.TRUE, null); + return new BooleanExpression(hasProjectAccessFunctionExpr, Expression.OP_EQ, booleanTrueLiteral); + } + +} diff --git a/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java b/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java new file mode 100644 index 0000000000..3b6b17ccca --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java @@ -0,0 +1,239 @@ +/* + * 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.datanucleus.method; + +import alpine.model.Team; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Project; +import org.junit.Test; + +import javax.jdo.Query; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class ProjectIsAccessibleByMethodTest extends PersistenceCapableTest { + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToTrueWhenProjectIsAccessible() { + final var teamA = new Team(); + teamA.setName("team-a"); + qm.persist(teamA); + + final var teamB = new Team(); + teamB.setName("team-b"); + qm.persist(teamB); + + final var project = new Project(); + project.setName("acme-app"); + project.setAccessTeams(List.of(teamA, teamB)); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{teamA.getId(), teamB.getId()})); + + final List projects = query.executeList(); + assertThat(projects).hasSize(1); + } + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToTrueWhenProjectParentIsAccessible() { + final var team = new Team(); + team.setName("team"); + qm.persist(team); + + final var parentProject = new Project(); + parentProject.setName("acme-app-parent"); + parentProject.setAccessTeams(List.of(team)); + qm.persist(parentProject); + + final var project = new Project(); + project.setParent(parentProject); + project.setName("acme-app"); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("name == 'acme-app' && this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); + + final List projects = query.executeList(); + assertThat(projects).hasSize(1); + } + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToTrueWhenProjectGrandParentIsAccessible() { + final var team = new Team(); + team.setName("team"); + qm.persist(team); + + final var grandParentProject = new Project(); + grandParentProject.setName("acme-app-grand-parent"); + grandParentProject.setAccessTeams(List.of(team)); + qm.persist(grandParentProject); + + final var parentProject = new Project(); + parentProject.setParent(grandParentProject); + parentProject.setName("acme-app-parent"); + qm.persist(parentProject); + + final var project = new Project(); + project.setParent(parentProject); + project.setName("acme-app"); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("name == 'acme-app' && this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); + + final List projects = query.executeList(); + assertThat(projects).hasSize(1); + } + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToFalseWhenProjectIsNotAccessible() { + final var teamA = new Team(); + teamA.setName("team-a"); + qm.persist(teamA); + + final var teamB = new Team(); + teamB.setName("team-b"); + qm.persist(teamB); + + final var project = new Project(); + project.setName("acme-app"); + project.setAccessTeams(List.of(teamA)); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{teamB.getId()})); + + final List projects = query.executeList(); + assertThat(projects).isEmpty(); + } + + @Test + @SuppressWarnings("resource") + public void shouldEvaluateToFalseWhenOnlyChildProjectIsAccessible() { + final var team = new Team(); + team.setName("team"); + qm.persist(team); + + final var parentProject = new Project(); + parentProject.setName("acme-app-parent"); + qm.persist(parentProject); + + final var project = new Project(); + project.setParent(parentProject); + project.setName("acme-app"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("name == 'acme-app-parent' && this.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); + + final List projects = query.executeList(); + assertThat(projects).hasSize(0); + } + + @Test + @SuppressWarnings("resource") + public void shouldBeAllowedOnProjectMembersOfNonProjectObjects() { + final var team = new Team(); + team.setName("team"); + qm.persist(team); + + final var project = new Project(); + project.setName("acme-app"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + qm.persist(component); + + final Query query = qm.getPersistenceManager().newQuery(Component.class); + query.setFilter("project.isAccessibleBy(:teamIds)"); + query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); + + final List components = query.executeList(); + assertThat(components).hasSize(1); + } + + @Test + @SuppressWarnings("resource") + public void shouldThrowWhenCalledOnNonProjectObject() { + final Query query = qm.getPersistenceManager().newQuery(Component.class); + query.setFilter("this.isAccessibleBy(:teamIds)"); + query.setParameters(Arrays.asList(1L, 2L, 3L)); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(query::execute) + .withMessage(""" + isAccessibleBy is only allowed for objects of type org.dependencytrack.model.Project, \ + but was called on org.dependencytrack.model.Component"""); + } + + @Test + @SuppressWarnings("resource") + public void shouldThrowWhenNoArgs() { + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy()"); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(query::execute) + .withMessage("Expected exactly one argument, but got 0"); + } + + @Test + @SuppressWarnings("resource") + public void shouldThrowWhenArgIsOfUnexpectedType() { + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy(:teamIdsString)"); + query.setParameters("1, 2, 3"); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(query::execute) + .withMessage(""" + Expected argument to be of type org.datanucleus.store.rdbms.sql.expression.ArrayLiteral, \ + but got org.datanucleus.store.rdbms.sql.expression.ParameterLiteral"""); + } + + @Test + @SuppressWarnings("resource") + public void shouldThrowWhenArgIsOfUnexpectedArrayType() { + final Query query = qm.getPersistenceManager().newQuery(Project.class); + query.setFilter("this.isAccessibleBy(:teamIdStrings)"); + query.setNamedParameters(Map.of("teamIdsStrings", new String[]{"1", "2", "3"})); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(query::execute) + .withMessage(""" + Expected argument to be of type org.datanucleus.store.rdbms.sql.expression.ArrayLiteral, \ + but got org.datanucleus.store.rdbms.sql.expression.ParameterLiteral"""); + } + +} \ No newline at end of file From 9b5972e4538600d021f6547dfa474eb52199ec08 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 24 Feb 2025 10:52:58 -0700 Subject: [PATCH 094/181] feat: add role and mappedrole classes (#2) Signed-off-by: Allen Shearin --- .../org/dependencytrack/model/MappedRole.java | 200 ++++++++++++++++++ .../java/org/dependencytrack/model/Role.java | 161 ++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 apiserver/src/main/java/org/dependencytrack/model/MappedRole.java create mode 100644 apiserver/src/main/java/org/dependencytrack/model/Role.java diff --git a/apiserver/src/main/java/org/dependencytrack/model/MappedRole.java b/apiserver/src/main/java/org/dependencytrack/model/MappedRole.java new file mode 100644 index 0000000000..6480e4f9a2 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/model/MappedRole.java @@ -0,0 +1,200 @@ +/* + * 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 alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.FetchGroup; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; +import javax.jdo.annotations.Unique; + +/** + * Model for associating a role on a given project with users. + * + * @author Allen Shearin + * @since 5.6.0 + */ +@PersistenceCapable(table = "PROJECT_ACCESS_ROLES") +@Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "LDAPUSERS_PROJECTS_ROLES", + members = { "ldapUsers", "project", "role" }, + deferred = "true") +@Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "MANAGEDUSERS_PROJECTS_ROLES", + members = { "managedUsers", "project", "role" }, + deferred = "true") +@Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "OIDCUSERS_PROJECTS_ROLES", + members = { "oidcUsers", "project", "role" }, + deferred = "true") +@FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "ldapUsers"), + @Persistent(name = "managedUsers"), + @Persistent(name = "oidcUsers") +}) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MappedRole implements Serializable { + + private static final long serialVersionUID = 1982348710987098723L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Column(name = "ROLE_ID", allowsNull = "false") + @JsonIgnore + private Role role; + + @Persistent + @Column(name = "PROJECT_ID", allowsNull = "false") + @JsonIgnore + private Project project; + + @Persistent(table = "LDAPUSERS_PROJECTS_ROLES", defaultFetchGroup = "true") + @Join(column = "PROJECT_ACCESS_ROLE_ID") + @Element(column = "LDAPUSER_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List ldapUsers; + + @Persistent(table = "MANAGEDUSERS_PROJECTS_ROLES") + @Join(column = "PROJECT_ACCESS_ROLE_ID") + @Element(column = "MANAGEDUSER_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List managedUsers; + + @Persistent(table = "OIDCUSERS_PROJECTS_ROLES") + @Join(column = "PROJECT_ACCESS_ROLE_ID") + @Element(column = "OIDCUSER_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List oidcUsers; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public Project getProject() { + return project; + } + + public void setProject(Project project) { + this.project = project; + } + + public List getLdapUsers() { + return ldapUsers; + } + + public void setLdapUsers(List ldapUsers) { + this.ldapUsers = ldapUsers; + } + + public void addLdapUsers(LdapUser... ldapUsers) { + if (this.ldapUsers == null) { + this.ldapUsers = new ArrayList<>(Arrays.asList(ldapUsers)); + + return; + } + + for (var user : ldapUsers) + if (!this.ldapUsers.contains(user)) + this.ldapUsers.add(user); + } + + public List getManagedUsers() { + return managedUsers; + } + + public void setManagedUsers(List managedUsers) { + this.managedUsers = managedUsers; + } + + public void addManagedUsers(ManagedUser... managedUsers) { + if (this.managedUsers == null) { + this.managedUsers = new ArrayList<>(Arrays.asList(managedUsers)); + + return; + } + + for (var user : managedUsers) + if (!this.managedUsers.contains(user)) + this.managedUsers.add(user); + } + + public List getOidcUsers() { + return oidcUsers; + } + + public void setOidcUsers(List oidcUsers) { + this.oidcUsers = oidcUsers; + } + + public void addOidcUsers(OidcUser... oidcUsers) { + if (this.oidcUsers == null) { + this.oidcUsers = new ArrayList<>(Arrays.asList(oidcUsers)); + + return; + } + + for (var user : oidcUsers) + if (!this.oidcUsers.contains(user)) + this.oidcUsers.add(user); + } + +} \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/model/Role.java b/apiserver/src/main/java/org/dependencytrack/model/Role.java new file mode 100644 index 0000000000..1b2ed3b042 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/model/Role.java @@ -0,0 +1,161 @@ +/* + * 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 alpine.common.validation.RegexSequence; +import alpine.model.Permission; +import alpine.server.json.TrimmedStringDeserializer; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Element; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.FetchGroup; +import javax.jdo.annotations.IdGeneratorStrategy; +import javax.jdo.annotations.Join; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.PrimaryKey; +import javax.jdo.annotations.Unique; + +/** + * Model for tracking roles. Roles define static sets of permissions + * that can be applied to a user with the scope of a project. + * + * @author Allen Shearin + * @since 5.6.0 + */ +@PersistenceCapable +@FetchGroup(name = "ALL", members = { + @Persistent(name = "name"), + @Persistent(name = "description"), + @Persistent(name = "permissions"), +}) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Role implements Serializable { + + private static final long serialVersionUID = -7592438796591673355L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @PrimaryKey + @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) + @JsonIgnore + private long id; + + @Persistent + @Unique(name = "ROLE_NAME_IDX", deferred = "true") + @Column(name = "NAME", jdbcType = "VARCHAR", allowsNull = "false") + @NotBlank + @Size(min = 1, max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, + message = "The name may only contain printable characters") + private String name; + + @Persistent + @Column(name = "DESCRIPTION", jdbcType = "VARCHAR") + @Size(max = 255) + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, + message = "The description may only contain printable characters") + private String description; + + @Persistent(table = "ROLES_PERMISSIONS", defaultFetchGroup = "true") + @Unique(name = "ROLES_PERMISSIONS_IDX") + @Join(column = "ROLE_ID") + @Element(column = "PERMISSION_ID") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) + private List permissions; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } + + public void addPermissions(Permission... permissions) { + if (this.permissions == null) { + this.permissions = new ArrayList<>(Arrays.asList(permissions)); + + return; + } + + for (var permission : permissions) + if (!this.permissions.contains(permission)) + this.permissions.add(permission); + } + + @Override + public String toString() { + var permissionStrings = permissions.stream() + .map(permission -> permission.getName()) + .toList(); + + return "%s{id=%d, name='%s', description='%s', permissions=%s}".formatted( + getClass().getSimpleName(), + id, + name, + description != null ? description : "", + permissionStrings); + } +} \ No newline at end of file From f2572cab6f951f1cd27cd918ad08dbfbcc4a1e1b Mon Sep 17 00:00:00 2001 From: jmayer-lm Date: Tue, 25 Feb 2025 15:02:10 -0500 Subject: [PATCH 095/181] Define new Role permissisons & Initial creation of RoleResource.java (#5) * Defining new role permissions Signed-off-by: Johnny Mayer * Defining new role permissions Signed-off-by: Johnny Mayer * Initial creation of RolesResource Signed-off-by: Johnny Mayer * address comments Signed-off-by: Johnny Mayer * Adding logger statement, removing getRoles() stub. Signed-off-by: Johnny Mayer * update @since, update permissions, added log statement Signed-off-by: Johnny Mayer --------- Signed-off-by: Johnny Mayer Signed-off-by: Allen Shearin --- .../org/dependencytrack/auth/Permissions.java | 10 + .../resources/v1/RoleResource.java | 181 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java diff --git a/apiserver/src/main/java/org/dependencytrack/auth/Permissions.java b/apiserver/src/main/java/org/dependencytrack/auth/Permissions.java index 982eefe1a7..a8e124a6b8 100644 --- a/apiserver/src/main/java/org/dependencytrack/auth/Permissions.java +++ b/apiserver/src/main/java/org/dependencytrack/auth/Permissions.java @@ -61,6 +61,11 @@ public enum Permissions { 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"), + ROLE_MANAGEMENT("Allows the creation, modification, and deletion of roles"), + ROLE_MANAGEMENT_CREATE("Allows the creation of roles"), + ROLE_MANAGEMENT_READ("Allows reading of roles"), + ROLE_MANAGEMENT_UPDATE("Allows update of roles"), + ROLE_MANAGEMENT_DELETE("Allows the deletion of roles"), TAG_MANAGEMENT("Allows the modification and deletion of tags"), TAG_MANAGEMENT_DELETE("Allows the deletion of a tag"), VIEW_BADGES("Provides the ability to view badges"); @@ -111,6 +116,11 @@ public static class Constants { public static final String POLICY_MANAGEMENT_READ = "POLICY_MANAGEMENT_READ"; public static final String POLICY_MANAGEMENT_UPDATE = "POLICY_MANAGEMENT_UPDATE"; public static final String POLICY_MANAGEMENT_DELETE = "POLICY_MANAGEMENT_DELETE"; + public static final String ROLE_MANAGEMENT = "ROLE_MANAGEMENT"; + public static final String ROLE_MANAGEMENT_CREATE = "ROLE_MANAGEMENT_CREATE"; + public static final String ROLE_MANAGEMENT_READ = "ROLE_MANAGEMENT_READ"; + public static final String ROLE_MANAGEMENT_UPDATE = "ROLE_MANAGEMENT_UPDATE"; + public static final String ROLE_MANAGEMENT_DELETE = "ROLE_MANAGEMENT_DELETE"; public static final String TAG_MANAGEMENT = "TAG_MANAGEMENT"; public static final String TAG_MANAGEMENT_DELETE = "TAG_MANAGEMENT_DELETE"; public static final String VIEW_BADGES = "VIEW_BADGES"; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java new file mode 100644 index 0000000000..175531de0a --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -0,0 +1,181 @@ +/* + * 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.resources.v1; + +import alpine.common.logging.Logger; +import alpine.model.Permission; +import alpine.model.Role; +import alpine.model.UserPrincipal; +import alpine.server.auth.PermissionRequired; +import alpine.server.resources.AlpineResource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +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.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.model.validation.ValidUuid; +import org.dependencytrack.persistence.QueryManager; +import org.owasp.security.logging.SecurityMarkers; + +import java.util.List; + +/** + * JAX-RS resources for processing roles. + * + * @author Johnny Mayer + * @since 5.6.0 + */ +@Path("/v1/role") +@Tag(name = "role") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth") +}) +public class RoleResource extends AlpineResource { + + private static final Logger LOGGER = Logger.getLogger(RoleResource.class); + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of all roles", + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

        " + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of all roles", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of roles", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Role.class))) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ}) + public Response getRoles() { + return Response.ok(roles).header(TOTAL_COUNT_HEADER, totalCount).build(); + } + + @GET + @Path("/{uuid}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a specific role", + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

        " + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A specific role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ}) + public Response getRole( + @Parameter(description = "The UUID of the role to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Returned role: " + role.getName()); + return Response.ok(role).build(); + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Creates a new role", + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

        " + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "The created role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE}) + public Response createRole(Role jsonRole) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + role.getName()); + return Response.ok(role).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Updates a role's fields", + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_UPDATE

        " + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "The updated role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE}) + public Response updateRole(Role jsonRole) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Updated role: " + role.getName()); + return Response.ok(role).build(); + } + + @DELETE + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Deletes a role", + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_DELETE

        " + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Role removed successfully"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE}) + public Response deleteRole(Role jsonRole) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Delete role: " + role.getName()); + return Response.ok(role).build(); + } + + + +} From 731b1bf90bc883b66f50a073c7ea0ec0cd6042f5 Mon Sep 17 00:00:00 2001 From: lmphil <126618132+lmphil@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:20:36 -0400 Subject: [PATCH 096/181] add roleQueryManager method stubs (#6) Signed-off-by: Philippe Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 18 +++++++++ .../persistence/RoleQueryManager.java | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/main/java/org/dependencytrack/persistence/RoleQueryManager.java diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 0ef5e03649..4131aed6f1 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -25,6 +25,7 @@ import alpine.model.ConfigProperty; import alpine.model.Permission; import alpine.model.IConfigProperty.PropertyType; +import alpine.model.Permission; import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.notification.NotificationLevel; @@ -73,6 +74,7 @@ import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; +import org.dependencytrack.model.Role; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vex; @@ -150,6 +152,7 @@ public class QueryManager extends AlpineQueryManager { private PolicyQueryManager policyQueryManager; private ProjectQueryManager projectQueryManager; private RepositoryQueryManager repositoryQueryManager; + private RoleQueryManager roleQueryManager; private ServiceComponentQueryManager serviceComponentQueryManager; private VexQueryManager vexQueryManager; private VulnerabilityQueryManager vulnerabilityQueryManager; @@ -428,6 +431,13 @@ private RepositoryQueryManager getRepositoryQueryManager() { return repositoryQueryManager; } + private RoleQueryManager getRoleQueryManager(){ + if (roleQueryManager == null) { + roleQueryManager = (request ==null) ? new RoleQueryManager(getPersistenceManager()) : new RoleQueryManager(getPersistenceManager(), request); + } + return roleQueryManager; + } + /** * Lazy instantiation of NotificationQueryManager. * @@ -1139,6 +1149,14 @@ public synchronized RepositoryMetaComponent synchronizeRepositoryMetaComponent(f return getRepositoryQueryManager().synchronizeRepositoryMetaComponent(transientRepositoryMetaComponent); } + public boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ + return getRoleQueryManager().addRoleToUser(principal, role, roleName, projectName); + } + + public boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName){ + return getRoleQueryManager().removeRoleFromUser(principal, role, roleName, projectName); + } + public NotificationRule createNotificationRule(String name, NotificationScope scope, NotificationLevel level, NotificationPublisher publisher) { return getNotificationQueryManager().createNotificationRule(name, scope, level, publisher); } diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java new file mode 100644 index 0000000000..dc7924137a --- /dev/null +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -0,0 +1,40 @@ +package org.dependencytrack.persistence; + +import java.nio.file.attribute.UserPrincipal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jdo.PersistenceManager; +import javax.jdo.Query; + +import org.dependencytrack.model.Role; + +import alpine.common.logging.Logger; +import alpine.model.Permission; +import alpine.resources.AlpineRequest; + +final class RoleQueryManager extends QueryManager implements IQueryManager { + + private static final Logger LOGGER = Logger.getLogger(ProjectQueryManager.class); + + RoleQueryManager(final PersistenceManager pm) { + super(pm); + } + + RoleQueryManager(final PersistenceManager pm, final AlpineRequest request) { + super(pm, request); + } + + boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ + //WARNING: This method is a stub. + //TODO: Implement addRoleToUser + return true; + } + + boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName){ + //WARNING: This method is a stub. + //TODO: Implement removeRoleFromUser + return true; + } +} From 2aa0682fc06606d7dfecfb6f8e28f94475ce2434 Mon Sep 17 00:00:00 2001 From: jmayer-lm Date: Thu, 27 Feb 2025 12:16:22 -0500 Subject: [PATCH 097/181] Add /v1/user Endpoints & CRUD Method Stubs (#7) * Add POST and DELETE role endpoints to UserResource Signed-off-by: Johnny Mayer * apply code style suggestions Signed-off-by: Johnny Mayer * Add CRUD method stubs to RoleQueryManager and QueryManager Signed-off-by: Johnny Mayer * Apply suggestions from code review Co-authored-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Signed-off-by: Allen Shearin --------- Signed-off-by: Johnny Mayer Signed-off-by: Allen Shearin Co-authored-by: Allen Shearin Co-authored-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 20 +++ .../resources/v1/UserResource.java | 129 ++++++++++++++++-- .../persistence/RoleQueryManager.java | 30 +++- 3 files changed, 161 insertions(+), 18 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 4131aed6f1..1da148be1a 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -858,6 +858,26 @@ public void deletePolicyCondition(PolicyCondition policyCondition) { getPolicyQueryManager().deletePolicyCondition(policyCondition); } + public Role createRole(Role role) { + return getRoleQueryManager().createRole(role); + } + + public List getRoles() { + return getRoleQueryManager().getRoles(); + } + + public Role getRole(String uuid) { + return getRoleQueryManager().getRole(null); + } + + public Role updateRole(Role role) { + return getRoleQueryManager().updateRole(role); + } + + public boolean deleteRole(String uuid, boolean value) { + return getRoleQueryManager().deleteRole(uuid, value); + } + public Vulnerability createVulnerability(Vulnerability vulnerability, boolean commitIndex) { return getVulnerabilityQueryManager().createVulnerability(vulnerability, commitIndex); } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index dedbc0e367..e7a36ff8ea 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -18,6 +18,22 @@ */ package org.dependencytrack.resources.v1; +import java.security.Principal; +import java.util.List; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.event.kafka.KafkaEventDispatcher; +import org.dependencytrack.model.IdentifiableObject; +import org.dependencytrack.model.Role; +import org.dependencytrack.notification.NotificationConstants; +import org.dependencytrack.notification.NotificationGroup; +import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.proto.notification.v1.UserSubject; +import org.owasp.security.logging.SecurityMarkers; + import alpine.Config; import alpine.common.logging.Logger; import alpine.model.LdapUser; @@ -57,22 +73,9 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.event.kafka.KafkaEventDispatcher; -import org.dependencytrack.model.IdentifiableObject; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.proto.notification.v1.UserSubject; -import org.owasp.security.logging.SecurityMarkers; - -import java.security.Principal; -import java.util.List; -import java.util.Optional; /** * JAX-RS resources for processing users. @@ -776,4 +779,102 @@ private UserSubject buildUserSubject(final String username, final String email) Optional.ofNullable(email).ifPresent(userBuilder::setEmail); return userBuilder.build(); } + + @POST + @Path("/{username}/role") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Adds role to specific user.", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

        " + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Updated user with a specific role", + content = @Content(schema = @Schema(implementation = UserPrincipal.class)) + ), + @ApiResponse(responseCode = "304", description = "The user has already been assigned to this role."), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user or role could not be found") + }) + @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) + public Response addRoleToUser( + @Parameter(description = "A valid username", required = true) + @PathParam("username") String username, + @Parameter(description = "The UUID of the role to associate username with", required = true) + IdentifiableObject identifiableObject, + @Parameter(description = "The name of the role", required = true) + @QueryParam("roleName") String roleName, + @Parameter(description = "The name of the project", required = true) + @QueryParam("projectName") String projectName) { + try (QueryManager qm = new QueryManager()) { + final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) + return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); + + final boolean modified = qm.addRoleToUser(principal, role, roleName, projectName); + principal = qm.getObjectById(principal.getClass(), principal.getId()); + if (modified) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s".formatted(principal.getName(), role.getName(), projectName)); + return Response.ok(principal).build(); + } + + return Response.status(Response.Status.NOT_MODIFIED).entity("The user is already a member of the specified role.").build(); + } + } + + @DELETE + @Path("/{username}/role") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Removes role from specific user.", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

        " +) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Updated user with a specific role removed", + content = @Content(schema = @Schema(implementation = UserPrincipal.class)) + ), + @ApiResponse(responseCode = "204", description = "The role has been successfully removed from the user"), + @ApiResponse(responseCode = "304", description = "The user is not a member of the specified role"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user or role could not be found") +}) + @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) + public Response removeRoleFromUser( + @Parameter(description = "A valid username", required = true) + @PathParam("username") String username, + @Parameter(description = "The UUID of the role to remove from the username", required = true) + IdentifiableObject identifiableObject, + @Parameter(description = "The name of the role", required = true) + @QueryParam("roleName") String roleName, + @Parameter(description = "The name of the project", required = true) + @QueryParam("projectName") String projectName) { + try (QueryManager qm = new QueryManager()) { + final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) + return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); + + final boolean modified = qm.removeRoleFromUser(principal, role, roleName, projectName); + + principal = qm.getObjectById(principal.getClass(), principal.getId()); + if (modified) { + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed role membership for: %s / role: %s / project: %s".formatted(principal.getName(), role.getName(), projectName)); + return Response.noContent().build(); + } + + return Response.status(Response.Status.NOT_MODIFIED).entity("The user is not a member of the specified role.").build(); + } + } } diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index dc7924137a..94dae4f731 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -1,17 +1,14 @@ package org.dependencytrack.persistence; import java.nio.file.attribute.UserPrincipal; -import java.util.HashMap; +import java.util.Collections; import java.util.List; -import java.util.Map; import javax.jdo.PersistenceManager; -import javax.jdo.Query; import org.dependencytrack.model.Role; import alpine.common.logging.Logger; -import alpine.model.Permission; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { @@ -26,6 +23,31 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { super(pm, request); } + public Role createRole(Role role) { + // TODO:Implement role creation logic + return role; + } + + public List getRoles() { + // TODO:Implement role retrieval logic + return Collections.emptyList(); + } + + public Role getRole(String uuid) { + // TODO:Implement role retrieval logic + return null; + } + + public Role updateRole(Role role) { + // TODO:Implement role update logic + return role; + } + + public boolean deleteRole(String uuid, boolean value) { + // TODO:Implement role deletion logic + return false; + } + boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ //WARNING: This method is a stub. //TODO: Implement addRoleToUser From 9777566b113665a5ac5c1773089f34fef39460bf Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 27 Feb 2025 12:27:06 -0700 Subject: [PATCH 098/181] cleaup createRoles and loadDefaultRoles Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 6 +-- .../resources/v1/RoleResource.java | 48 ++++++++++--------- .../persistence/RoleQueryManager.java | 41 ++++++++++++---- 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 1da148be1a..a562d0a52f 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -858,11 +858,11 @@ public void deletePolicyCondition(PolicyCondition policyCondition) { getPolicyQueryManager().deletePolicyCondition(policyCondition); } - public Role createRole(Role role) { - return getRoleQueryManager().createRole(role); + public Role createRole(final String name, final String description, final List permissions) { + return getRoleQueryManager().createRole(name, description, permissions); } - public List getRoles() { + public List getRoles() { return getRoleQueryManager().getRoles(); } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 175531de0a..0d88ba23f4 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -19,9 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; -import alpine.model.Permission; -import alpine.model.Role; -import alpine.model.UserPrincipal; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -50,10 +47,11 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.model.Role; import org.dependencytrack.persistence.QueryManager; import org.owasp.security.logging.SecurityMarkers; -import java.util.List; +import java.util.Collections; /** * JAX-RS resources for processing roles. @@ -86,10 +84,10 @@ public class RoleResource extends AlpineResource { ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRoles() { - return Response.ok(roles).header(TOTAL_COUNT_HEADER, totalCount).build(); - } + return Response.ok(Collections.emptyList()).header(TOTAL_COUNT_HEADER, 0).build(); + } @GET @Path("/{uuid}") @@ -107,12 +105,18 @@ public Response getRoles() { @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRole( - @Parameter(description = "The UUID of the role to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Returned role: " + role.getName()); - return Response.ok(role).build(); + @Parameter(description = "The UUID of the role to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { + try (QueryManager qm = new QueryManager()) { + Role role = qm.getObjectByUuid(Role.class, uuid); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); + + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Returned role: " + role.getName()); + return Response.ok(role).build(); + } } @PUT @@ -130,10 +134,10 @@ public Response getRole( ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE }) public Response createRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + role.getName()); - return Response.ok(role).build(); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + jsonRole.getName()); + return Response.ok(jsonRole).build(); } @POST @@ -152,10 +156,10 @@ public Response createRole(Role jsonRole) { @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE }) public Response updateRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Updated role: " + role.getName()); - return Response.ok(role).build(); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Updated role: " + jsonRole.getName()); + return Response.ok(jsonRole).build(); } @DELETE @@ -170,12 +174,10 @@ public Response updateRole(Role jsonRole) { @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE}) + @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE }) public Response deleteRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Delete role: " + role.getName()); - return Response.ok(role).build(); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Delete role: " + jsonRole.getName()); + return Response.ok(jsonRole).build(); } - - } diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 94dae4f731..fe03019757 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -1,3 +1,21 @@ +/* + * 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 java.nio.file.attribute.UserPrincipal; @@ -9,6 +27,7 @@ import org.dependencytrack.model.Role; import alpine.common.logging.Logger; +import alpine.model.Permission; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { @@ -23,9 +42,13 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { super(pm, request); } - public Role createRole(Role role) { - // TODO:Implement role creation logic - return role; + public Role createRole(final String name, final String description, final List permissions) { + Role role = new Role(); + role.setName(name); + role.setDescription(description); + role.setPermissions(permissions); + + return persist(role); } public List getRoles() { @@ -48,15 +71,15 @@ public boolean deleteRole(String uuid, boolean value) { return false; } - boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ - //WARNING: This method is a stub. - //TODO: Implement addRoleToUser + boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName) { + // WARNING: This method is a stub. + // TODO: Implement addRoleToUser return true; } - boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName){ - //WARNING: This method is a stub. - //TODO: Implement removeRoleFromUser + boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName) { + // WARNING: This method is a stub. + // TODO: Implement removeRoleFromUser return true; } } From 2f8aebd6d994ce837c9e94aa3b5b7790852d4d39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:12:42 +0000 Subject: [PATCH 099/181] Bump org.slf4j:log4j-over-slf4j from 2.0.16 to 2.0.17 Bumps org.slf4j:log4j-over-slf4j from 2.0.16 to 2.0.17. --- updated-dependencies: - dependency-name: org.slf4j:log4j-over-slf4j dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4b5e40df80..2ea1623ef5 100644 --- a/pom.xml +++ b/pom.xml @@ -135,7 +135,7 @@ 0.7.0 7.1.0 1.1.1 - 2.0.16 + 2.0.17 4.5.14 6.3.0 1.4.0 From a81a825ac550786625f229d94dc5daf73320d488 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 27 Feb 2025 13:56:16 -0700 Subject: [PATCH 100/181] Flesh out stubbed api endpoints, call createDefaultRoles Signed-off-by: Allen Shearin Signed-off-by: Allen Shearin --- .../resources/v1/PermissionResource.java | 89 +++++++++++++++++++ .../resources/v1/RoleResource.java | 4 +- .../persistence/RoleQueryManager.java | 22 +++-- 3 files changed, 108 insertions(+), 7 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 19d305a1d1..3a714c09d3 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -43,6 +43,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Role; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.owasp.security.logging.SecurityMarkers; @@ -221,6 +222,94 @@ public Response addPermissionToTeam( } } + @DELETE + @Path("/{permission}/role/{uuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_DELETE

        " + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "The updated role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "304", description = "The role already has the specified permission assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_DELETE}) + public Response removePermissionFromRole( + @Parameter(description = "A valid role uuid", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "A valid permission", required = true) + @PathParam("permission") String permissionName) { + try (QueryManager qm = new QueryManager()) { + Role role = qm.getObjectByUuid(Role.class, uuid); + if (role == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + } + final Permission permission = qm.getPermission(permissionName); + if (permission == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); + } + final List permissions = role.getPermissions(); + if (permissions != null && permissions.contains(permission)) { + permissions.remove(permission); + role.setPermissions(permissions); + role = qm.persist(role); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed permission for role: " + role.getName() + " / permission: " + permission.getName()); + return Response.ok(role).build(); + } + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + } + + @POST + @Path("/{permission}/role/{uuid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

        " + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "The updated role", + content = @Content(schema = @Schema(implementation = Role.class)) + ), + @ApiResponse(responseCode = "304", description = "The role already has the specified permission assigned"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) + public Response addPermissionToRole( + @Parameter(description = "A valid role uuid", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "A valid permission", required = true) + @PathParam("permission") String permissionName) { + try (QueryManager qm = new QueryManager()) { + Role role = qm.getObjectByUuid(Role.class, uuid); + if (role == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + } + final Permission permission = qm.getPermission(permissionName); + if (permission == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); + } + final List permissions = role.getPermissions(); + if (permissions != null && !permissions.contains(permission)) { + permissions.add(permission); + role.setPermissions(permissions); + role = qm.persist(role); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added permission for role: " + role.getName() + " / permission: " + permission.getName()); + return Response.ok(role).build(); + } + return Response.status(Response.Status.NOT_MODIFIED).build(); + } + } + @DELETE @Path("/{permission}/team/{uuid}") @Consumes(MediaType.APPLICATION_JSON) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 0d88ba23f4..2630f99293 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -137,7 +137,9 @@ public Response getRole( @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE }) public Response createRole(Role jsonRole) { super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + jsonRole.getName()); - return Response.ok(jsonRole).build(); + try (QueryManager qm = new QueryManager()) { + return Response.ok(qm.persist(jsonRole)).build(); + } } @POST diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index fe03019757..5628ee4fb3 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,16 +18,16 @@ */ package org.dependencytrack.persistence; -import java.nio.file.attribute.UserPrincipal; -import java.util.Collections; import java.util.List; import javax.jdo.PersistenceManager; +import javax.jdo.Query; import org.dependencytrack.model.Role; import alpine.common.logging.Logger; import alpine.model.Permission; +import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { @@ -42,6 +42,7 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { super(pm, request); } + @Override public Role createRole(final String name, final String description, final List permissions) { Role role = new Role(); role.setName(name); @@ -51,33 +52,42 @@ public Role createRole(final String name, final String description, final List

        getRoles() { - // TODO:Implement role retrieval logic - return Collections.emptyList(); + final Query query = pm.newQuery(Role.class); + if (orderBy == null) + query.setOrdering("name asc"); + + return query.executeList(); } + @Override public Role getRole(String uuid) { // TODO:Implement role retrieval logic return null; } + @Override public Role updateRole(Role role) { // TODO:Implement role update logic return role; } + @Override public boolean deleteRole(String uuid, boolean value) { // TODO:Implement role deletion logic return false; } - boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName) { + @Override + public boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName) { // WARNING: This method is a stub. // TODO: Implement addRoleToUser return true; } - boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName) { + @Override + public boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName) { // WARNING: This method is a stub. // TODO: Implement removeRoleFromUser return true; From c336548a7851919b7aff50d36e79f9b20a066baf Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 27 Feb 2025 16:31:26 -0700 Subject: [PATCH 101/181] update getRoles Signed-off-by: Allen Shearin --- .../org/dependencytrack/resources/v1/RoleResource.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 2630f99293..3072d65d0d 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -51,8 +51,6 @@ import org.dependencytrack.persistence.QueryManager; import org.owasp.security.logging.SecurityMarkers; -import java.util.Collections; - /** * JAX-RS resources for processing roles. * @@ -86,8 +84,11 @@ public class RoleResource extends AlpineResource { }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRoles() { - return Response.ok(Collections.emptyList()).header(TOTAL_COUNT_HEADER, 0).build(); + try (QueryManager qm = new QueryManager()) { + return Response.ok(qm.getRoles()).header(TOTAL_COUNT_HEADER, 0).build(); + } } + @GET @Path("/{uuid}") From 5d0bbc6593fdf4bdf3596eff68483268f19b0965 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:33:54 -0600 Subject: [PATCH 102/181] fix: add role UUID field (#9) * fix: add role UUID field Signed-off-by: Jonathan Howard * fix: add uuid field to fetch group Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard Signed-off-by: Allen Shearin --- .../java/org/dependencytrack/model/Role.java | 20 ++++++++++++++++++- .../resources/migration/changelog-v5.6.0.xml | 6 ++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/model/Role.java b/apiserver/src/main/java/org/dependencytrack/model/Role.java index 1b2ed3b042..47f8516575 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/Role.java +++ b/apiserver/src/main/java/org/dependencytrack/model/Role.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @@ -34,6 +35,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; + import javax.jdo.annotations.Column; import javax.jdo.annotations.Element; import javax.jdo.annotations.Extension; @@ -58,6 +61,7 @@ @Persistent(name = "name"), @Persistent(name = "description"), @Persistent(name = "permissions"), + @Persistent(name = "uuid"), }) @JsonInclude(JsonInclude.Include.NON_NULL) public class Role implements Serializable { @@ -101,6 +105,12 @@ public enum FetchGroup { @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) private List permissions; + @Persistent(customValueStrategy = "uuid") + @Unique(name = "ROLE_UUID_IDX") + @Column(name = "UUID", sqlType = "UUID", allowsNull = "false") + @NotNull + private UUID uuid; + public long getId() { return id; } @@ -145,6 +155,14 @@ public void addPermissions(Permission... permissions) { this.permissions.add(permission); } + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + @Override public String toString() { var permissionStrings = permissions.stream() @@ -158,4 +176,4 @@ public String toString() { description != null ? description : "", permissionStrings); } -} \ No newline at end of file +} diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0.xml index 4622e133b9..8bf3d50943 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0.xml @@ -717,6 +717,12 @@ + + + + Date: Mon, 3 Mar 2025 10:52:25 -0600 Subject: [PATCH 103/181] refactor: implement role endpoint methods (#10) * refactor: implement role endpoint methods Signed-off-by: Jonathan Howard * style: restore original method order Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 16 ++-- .../persistence/RoleQueryManager.java | 44 ++++++---- .../persistence/jdbi/RoleDao.java | 36 ++++++++ .../resources/v1/RoleResource.java | 84 ++++++++++++------- .../resources/v1/UserResource.java | 71 +++++++++++----- 5 files changed, 173 insertions(+), 78 deletions(-) rename {src => apiserver/src}/main/java/org/dependencytrack/persistence/RoleQueryManager.java (64%) create mode 100644 apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index a562d0a52f..b904b73468 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -870,12 +870,8 @@ public Role getRole(String uuid) { return getRoleQueryManager().getRole(null); } - public Role updateRole(Role role) { - return getRoleQueryManager().updateRole(role); - } - - public boolean deleteRole(String uuid, boolean value) { - return getRoleQueryManager().deleteRole(uuid, value); + public Role updateRole(Role transientRole) { + return getRoleQueryManager().updateRole(transientRole); } public Vulnerability createVulnerability(Vulnerability vulnerability, boolean commitIndex) { @@ -1169,12 +1165,12 @@ public synchronized RepositoryMetaComponent synchronizeRepositoryMetaComponent(f return getRepositoryQueryManager().synchronizeRepositoryMetaComponent(transientRepositoryMetaComponent); } - public boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName){ - return getRoleQueryManager().addRoleToUser(principal, role, roleName, projectName); + public boolean addRoleToUser(UserPrincipal principal, Role role, Project project){ + return getRoleQueryManager().addRoleToUser(principal, role, project); } - public boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName){ - return getRoleQueryManager().removeRoleFromUser(principal, role, roleName, projectName); + public boolean removeRoleFromUser(UserPrincipal principal, Role role, Project project){ + return getRoleQueryManager().removeRoleFromUser(principal, role, project); } public NotificationRule createNotificationRule(String name, NotificationScope scope, NotificationLevel level, NotificationPublisher publisher) { diff --git a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java similarity index 64% rename from src/main/java/org/dependencytrack/persistence/RoleQueryManager.java rename to apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 5628ee4fb3..9681746ad2 100644 --- a/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,11 +18,13 @@ */ package org.dependencytrack.persistence; +import java.util.Collections; import java.util.List; import javax.jdo.PersistenceManager; import javax.jdo.Query; +import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; import alpine.common.logging.Logger; @@ -32,7 +34,7 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { - private static final Logger LOGGER = Logger.getLogger(ProjectQueryManager.class); + private static final Logger LOGGER = Logger.getLogger(RoleQueryManager.class); RoleQueryManager(final PersistenceManager pm) { super(pm); @@ -63,33 +65,47 @@ public List getRoles() { @Override public Role getRole(String uuid) { - // TODO:Implement role retrieval logic - return null; + final Query query = pm.newQuery(Role.class, "uuid == :uuid"); + + return query.executeUnique(); } - @Override - public Role updateRole(Role role) { - // TODO:Implement role update logic - return role; + public List getUnassignedProjects(final String username) { + return getUnassignedProjects(getUserPrincipal(username)); + } + + public List getUnassignedProjects(final UserPrincipal principal) { + // TODO: Implement getUnassignedProjects + return Collections.emptyList(); + } + + public List getUnassignedRolePermissions(final Role role) { + // TODO: Implement getUnassignedRolePermissions + return Collections.emptyList(); } @Override - public boolean deleteRole(String uuid, boolean value) { - // TODO:Implement role deletion logic - return false; + public Role updateRole(Role transientRole) { + final Role role = getObjectByUuid(Role.class, transientRole.getUuid()); + if (role == null) + return null; + + role.setName(transientRole.getName()); + role.setDescription(transientRole.getDescription()); + + return persist(role); } @Override - public boolean addRoleToUser(UserPrincipal principal, Role role, String roleName, String projectName) { - // WARNING: This method is a stub. + public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { // TODO: Implement addRoleToUser return true; } @Override - public boolean removeRoleFromUser(UserPrincipal principal, Role role, String roleName, String projectName) { - // WARNING: This method is a stub. + public boolean removeRoleFromUser(UserPrincipal principal, Role role, Project project) { // TODO: Implement removeRoleFromUser return true; } + } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java new file mode 100644 index 0000000000..c64a25de4e --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -0,0 +1,36 @@ +/* + * 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.jdbi; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +/** + * @since 5.6.0 + */ +public interface RoleDao { + + @SqlUpdate(""" + DELETE + FROM "ROLE" + WHERE "ID" = :roleId + """) + int deleteRole(@Bind final long roleId); + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 3072d65d0d..c1445ef64c 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -49,8 +49,12 @@ import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.model.Role; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.RoleDao; +import org.jdbi.v3.core.Handle; import org.owasp.security.logging.SecurityMarkers; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; + /** * JAX-RS resources for processing roles. * @@ -71,51 +75,50 @@ public class RoleResource extends AlpineResource { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Returns a list of all roles", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

        " - ) + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

        ") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "A list of all roles", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of roles", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Role.class))) - ), + headers = @Header( + name = TOTAL_COUNT_HEADER, + description = "The total number of roles", + schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Role.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRoles() { try (QueryManager qm = new QueryManager()) { - return Response.ok(qm.getRoles()).header(TOTAL_COUNT_HEADER, 0).build(); + return Response.ok(qm.getRoles()).header(TOTAL_COUNT_HEADER, qm.getCount(Role.class)).build(); } } - @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Returns a specific role", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

        " - ) + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

        ") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "A specific role", - content = @Content(schema = @Schema(implementation = Role.class)) - ), + content = @Content(schema = @Schema(implementation = Role.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) public Response getRole( - @Parameter(description = "The UUID of the role to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { + @Parameter( + description = "The UUID of the role to retrieve", + schema = @Schema(type = "string", format = "uuid"), + required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { Role role = qm.getObjectByUuid(Role.class, uuid); if (role == null) - return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, - "Returned role: " + role.getName()); return Response.ok(role).build(); } } @@ -125,21 +128,23 @@ public Response getRole( @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Creates a new role", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

        " - ) + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

        ") @ApiResponses(value = { @ApiResponse( responseCode = "201", description = "The created role", - content = @Content(schema = @Schema(implementation = Role.class)) - ), + content = @Content(schema = @Schema(implementation = Role.class))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE }) public Response createRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Created role: " + jsonRole.getName()); + failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); + try (QueryManager qm = new QueryManager()) { - return Response.ok(qm.persist(jsonRole)).build(); + final Role role = qm.createRole(jsonRole.getName(), jsonRole.getDescription(), jsonRole.getPermissions()); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role created: " + role.getName()); + + return Response.status(Response.Status.CREATED).entity(role).build(); } } @@ -148,21 +153,28 @@ public Response createRole(Role jsonRole) { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Updates a role's fields", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_UPDATE

        " - ) + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_UPDATE

        ") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "The updated role", - content = @Content(schema = @Schema(implementation = Role.class)) - ), + content = @Content(schema = @Schema(implementation = Role.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE }) public Response updateRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Updated role: " + jsonRole.getName()); - return Response.ok(jsonRole).build(); + failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); + + try (QueryManager qm = new QueryManager()) { + Role role = qm.updateRole(jsonRole); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role updated: " + role.getName()); + + return Response.ok(role).build(); + } } @DELETE @@ -170,8 +182,7 @@ public Response updateRole(Role jsonRole) { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Deletes a role", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_DELETE

        " - ) + description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_DELETE

        ") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Role removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -179,8 +190,19 @@ public Response updateRole(Role jsonRole) { }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE }) public Response deleteRole(Role jsonRole) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Delete role: " + jsonRole.getName()); - return Response.ok(jsonRole).build(); + try (QueryManager qm = new QueryManager()) { + final Role role = qm.getObjectByUuid(Role.class, jsonRole.getUuid(), Role.FetchGroup.ALL.name()); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + try (final Handle jdbiHandle = openJdbiHandle()) { + jdbiHandle.attach(RoleDao.class).deleteRole(role.getId()); + } + + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role deleted: " + role.getName()); + + return Response.noContent().build(); + } } } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index e7a36ff8ea..572367b8d7 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -26,6 +26,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.model.IdentifiableObject; +import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; @@ -801,13 +802,19 @@ private UserSubject buildUserSubject(final String username, final String email) @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) public Response addRoleToUser( @Parameter(description = "A valid username", required = true) - @PathParam("username") String username, + @PathParam("username") + String username, + @Parameter(description = "The UUID of the role to associate username with", required = true) - IdentifiableObject identifiableObject, - @Parameter(description = "The name of the role", required = true) - @QueryParam("roleName") String roleName, + IdentifiableObject identifiableObject, + @Parameter(description = "The name of the project", required = true) - @QueryParam("projectName") String projectName) { + @QueryParam("projectName") + String projectName, + + @Parameter(description = "The version of the project") + @QueryParam("projectVersion") + String projectVersion) { try (QueryManager qm = new QueryManager()) { final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); if (role == null) @@ -817,14 +824,20 @@ public Response addRoleToUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - final boolean modified = qm.addRoleToUser(principal, role, roleName, projectName); + Project project = qm.getProject(projectName, projectVersion); + if (project == null) + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + + final boolean modified = qm.addRoleToUser(principal, role, project); + if (!modified) + return Response.notModified().entity("The user is already a member of the specified role.").build(); + principal = qm.getObjectById(principal.getClass(), principal.getId()); - if (modified) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s".formatted(principal.getName(), role.getName(), projectName)); - return Response.ok(principal).build(); - } + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Added role membership for: %s / role: %s / project: %s" + .formatted(principal.getName(), role.getName(), projectName)); - return Response.status(Response.Status.NOT_MODIFIED).entity("The user is already a member of the specified role.").build(); + return Response.ok(principal).build(); } } @@ -850,13 +863,19 @@ public Response addRoleToUser( @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) public Response removeRoleFromUser( @Parameter(description = "A valid username", required = true) - @PathParam("username") String username, - @Parameter(description = "The UUID of the role to remove from the username", required = true) - IdentifiableObject identifiableObject, - @Parameter(description = "The name of the role", required = true) - @QueryParam("roleName") String roleName, + @PathParam("username") + String username, + + @Parameter(description = "The UUID of the role to associate username with", required = true) + IdentifiableObject identifiableObject, + @Parameter(description = "The name of the project", required = true) - @QueryParam("projectName") String projectName) { + @QueryParam("projectName") + String projectName, + + @Parameter(description = "The version of the project") + @QueryParam("projectVersion") + String projectVersion) { try (QueryManager qm = new QueryManager()) { final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); if (role == null) @@ -866,15 +885,21 @@ public Response removeRoleFromUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - final boolean modified = qm.removeRoleFromUser(principal, role, roleName, projectName); + Project project = qm.getProject(projectName, projectVersion); + if (project == null) + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + + final boolean modified = qm.removeRoleFromUser(principal, role, project); + if (!modified) + return Response.notModified().entity("The user is not a member of the specified role.").build(); principal = qm.getObjectById(principal.getClass(), principal.getId()); - if (modified) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed role membership for: %s / role: %s / project: %s".formatted(principal.getName(), role.getName(), projectName)); - return Response.noContent().build(); - } + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Removed role membership for: %s / role: %s / project: %s" + .formatted(principal.getName(), role.getName(), projectName)); - return Response.status(Response.Status.NOT_MODIFIED).entity("The user is not a member of the specified role.").build(); + return Response.noContent().build(); } } + } From 463e4961d8667fcc7f2ca7f117d16252f4618f3d Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 20 Feb 2025 16:32:14 +0100 Subject: [PATCH 104/181] Improve coverage of portfolio ACL checks in REST API endpoints Signed-off-by: nscuro Signed-off-by: Allen Shearin --- .../resources/v1/AnalysisResource.java | 18 +++++++++--------- .../resources/v1/FindingResource.java | 9 +++++++++ .../resources/v1/AnalysisResourceTest.java | 12 ++++++------ .../resources/v1/BomResourceTest.java | 10 +++++----- .../resources/v1/ProjectResourceTest.java | 16 ++++++++-------- .../resources/v1/VexResourceTest.java | 6 +++--- .../v1/VulnerabilityResourceTest.java | 12 ++++++------ 7 files changed, 46 insertions(+), 37 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java index 389417f095..43f93fc6e3 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java @@ -35,15 +35,6 @@ 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.GET; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaEventDispatcher; @@ -63,6 +54,15 @@ import org.dependencytrack.resources.v1.vo.AnalysisRequest; import org.dependencytrack.util.AnalysisCommentFormatter.AnalysisCommentField; +import jakarta.validation.Validator; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.UUID; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 8a06e674a4..37148f4688 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -67,6 +67,15 @@ import org.dependencytrack.resources.v1.vo.BomUploadResponse; import org.dependencytrack.util.PurlUtil; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java index 4c2c32368a..efedb47377 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java @@ -23,12 +23,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import alpine.server.filters.AuthorizationFilter; -import jakarta.json.Json; -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.jcip.annotations.NotThreadSafe; import org.apache.http.HttpStatus; import org.dependencytrack.JerseyTestRule; @@ -55,6 +49,12 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.Json; +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.time.Duration; import java.util.List; import java.util.UUID; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 185ce53f14..d11140c469 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -23,11 +23,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import junitparams.JUnitParamsRunner; import junitparams.Parameters; import net.javacrumbs.jsonunit.core.Option; @@ -74,6 +69,11 @@ import org.junit.contrib.java.lang.system.EnvironmentVariables; import org.junit.runner.RunWith; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 4523a7e44c..4b79e23488 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -26,14 +26,6 @@ import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.ws.rs.HttpMethod; -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.auth.Permissions; @@ -76,6 +68,14 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index 94f42839ea..4eb0f84409 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -21,9 +21,6 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; -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; @@ -45,6 +42,9 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.Base64; import java.util.Collections; import java.util.List; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java index 0f63abe496..17a2aefdf1 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java @@ -21,12 +21,6 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import jakarta.json.Json; -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; @@ -48,6 +42,12 @@ import org.junit.ClassRule; import org.junit.Test; +import jakarta.json.Json; +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.List; import java.util.Set; import java.util.UUID; From b69b14e278a0caf19b2a4d37f0551b74da762041 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:58:44 +0000 Subject: [PATCH 105/181] Bump org.wiremock:wiremock from 3.12.0 to 3.12.1 Bumps [org.wiremock:wiremock](https://github.com/wiremock/wiremock) from 3.12.0 to 3.12.1. - [Release notes](https://github.com/wiremock/wiremock/releases) - [Commits](https://github.com/wiremock/wiremock/compare/3.12.0...3.12.1) --- updated-dependencies: - dependency-name: org.wiremock:wiremock dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: Allen Shearin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2ea1623ef5..320d852ed3 100644 --- a/pom.xml +++ b/pom.xml @@ -118,7 +118,7 @@ 0.2.2 8.5.17 2.0.0 - 3.12.0 + 3.12.1 1.5.0 0.5.3.2 3.2.2 From 4b8e859e43057047bdb10d222a0e6118320cd85d Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:29:35 -0600 Subject: [PATCH 106/181] refactor: implement adding/removing users to/from roles (#13) * refactor: implement adding/removing users to/from roles Signed-off-by: Jonathan Howard * refactor: add RoleDao methods for removing role from user Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard --- .../persistence/RoleQueryManager.java | 78 +++++++++++++++++-- .../persistence/jdbi/RoleDao.java | 38 ++++++++- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 9681746ad2..4fae8ce2f8 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,20 +18,30 @@ */ package org.dependencytrack.persistence; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.jdo.PersistenceManager; import javax.jdo.Query; +import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; +import org.dependencytrack.persistence.jdbi.RoleDao; +import org.jdbi.v3.core.Handle; import alpine.common.logging.Logger; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; + final class RoleQueryManager extends QueryManager implements IQueryManager { private static final Logger LOGGER = Logger.getLogger(RoleQueryManager.class); @@ -98,14 +108,72 @@ public Role updateRole(Role transientRole) { @Override public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { - // TODO: Implement addRoleToUser - return true; + Query query = pm.newQuery(MappedRole.class) + .filter("project.id == :projectId && role.id == :roleId") + .setNamedParameters(Map.of( + "roleId", role.getId(), + "projectId", project.getId())); + + try { + query.getFetchPlan().setGroup(MappedRole.FetchGroup.ALL.name()); + MappedRole result = query.executeUnique(); + + if (result == null) { + LOGGER.info("Creating role mapping for project: %s / role: %s" + .formatted(project.getName(), role.getName())); + + result = new MappedRole(); + result.setProject(project); + result.setRole(role); + } + + result.setLdapUsers(result.getLdapUsers() != null ? result.getLdapUsers() : new ArrayList<>()); + result.setManagedUsers(result.getManagedUsers() != null ? result.getManagedUsers() : new ArrayList<>()); + result.setOidcUsers(result.getOidcUsers() != null ? result.getOidcUsers() : new ArrayList<>()); + + final MappedRole mappedRole = result; + + boolean modified = switch (user) { + case LdapUser ldapUser when !mappedRole.getLdapUsers().contains(ldapUser) -> { + mappedRole.addLdapUsers(ldapUser); + yield true; + } + case ManagedUser managedUser when !mappedRole.getManagedUsers().contains(managedUser) -> { + mappedRole.addManagedUsers(managedUser); + yield true; + } + case OidcUser oidcUser when !mappedRole.getOidcUsers().contains(oidcUser) -> { + mappedRole.addOidcUsers(oidcUser); + yield true; + } + default -> false; + }; + + if (modified) + persist(mappedRole); + + return modified; + } finally { + query.closeAll(); + } } @Override - public boolean removeRoleFromUser(UserPrincipal principal, Role role, Project project) { - // TODO: Implement removeRoleFromUser - return true; + public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { + try (final Handle jdbiHandle = openJdbiHandle()) { + int count = switch (user) { + case LdapUser ldapUser -> jdbiHandle.attach(RoleDao.class) + .removeRoleFromLdapUser(ldapUser.getId(), project.getId(), role.getId()); + case ManagedUser managedUser -> jdbiHandle.attach(RoleDao.class) + .removeRoleFromManagedUser(managedUser.getId(), project.getId(), role.getId()); + case OidcUser oidcUser -> jdbiHandle.attach(RoleDao.class) + .removeRoleFromOidcUser(oidcUser.getId(), project.getId(), role.getId()); + default -> 0; + }; + + return count == 1; + } + } } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index c64a25de4e..0ce3d1ac77 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -26,11 +26,47 @@ */ public interface RoleDao { - @SqlUpdate(""" + @SqlUpdate(/* language=sql */ """ DELETE FROM "ROLE" WHERE "ID" = :roleId """) int deleteRole(@Bind final long roleId); + @SqlUpdate(/* language=sql */ """ + DELETE + FROM "LDAPUSERS_PROJECTS_ROLES" + WHERE "LDAPUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) + int removeRoleFromLdapUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + + @SqlUpdate(/* language=sql */ """ + DELETE + FROM "MANAGEDUSERS_PROJECTS_ROLES" + WHERE "MANAGEDUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) + int removeRoleFromManagedUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + + @SqlUpdate(/* language=sql */ """ + DELETE + FROM "OIDCUSERS_PROJECTS_ROLES" + WHERE "OIDCUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) + int removeRoleFromOidcUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + } From 8ea9f2c0abf54fb9853d53dd92294461a942c91c Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Tue, 4 Mar 2025 16:56:03 -0700 Subject: [PATCH 107/181] refactor: remove description field Signed-off-by: Allen Shearin --- .../java/org/dependencytrack/model/Role.java | 18 ------------------ .../persistence/QueryManager.java | 4 ++-- .../persistence/RoleQueryManager.java | 4 +--- .../resources/v1/RoleResource.java | 2 +- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/model/Role.java b/apiserver/src/main/java/org/dependencytrack/model/Role.java index 47f8516575..f278a9ebd7 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/Role.java +++ b/apiserver/src/main/java/org/dependencytrack/model/Role.java @@ -59,7 +59,6 @@ @PersistenceCapable @FetchGroup(name = "ALL", members = { @Persistent(name = "name"), - @Persistent(name = "description"), @Persistent(name = "permissions"), @Persistent(name = "uuid"), }) @@ -90,14 +89,6 @@ public enum FetchGroup { message = "The name may only contain printable characters") private String name; - @Persistent - @Column(name = "DESCRIPTION", jdbcType = "VARCHAR") - @Size(max = 255) - @JsonDeserialize(using = TrimmedStringDeserializer.class) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, - message = "The description may only contain printable characters") - private String description; - @Persistent(table = "ROLES_PERMISSIONS", defaultFetchGroup = "true") @Unique(name = "ROLES_PERMISSIONS_IDX") @Join(column = "ROLE_ID") @@ -127,14 +118,6 @@ public void setName(String name) { this.name = name; } - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - public List getPermissions() { return permissions; } @@ -173,7 +156,6 @@ public String toString() { getClass().getSimpleName(), id, name, - description != null ? description : "", permissionStrings); } } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index b904b73468..4c05df905f 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -858,8 +858,8 @@ public void deletePolicyCondition(PolicyCondition policyCondition) { getPolicyQueryManager().deletePolicyCondition(policyCondition); } - public Role createRole(final String name, final String description, final List permissions) { - return getRoleQueryManager().createRole(name, description, permissions); + public Role createRole(final String name, final List permissions) { + return getRoleQueryManager().createRole(name, permissions); } public List getRoles() { diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 4fae8ce2f8..918cc1df9d 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -55,10 +55,9 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { } @Override - public Role createRole(final String name, final String description, final List permissions) { + public Role createRole(final String name, final List permissions) { Role role = new Role(); role.setName(name); - role.setDescription(description); role.setPermissions(permissions); return persist(role); @@ -101,7 +100,6 @@ public Role updateRole(Role transientRole) { return null; role.setName(transientRole.getName()); - role.setDescription(transientRole.getDescription()); return persist(role); } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index c1445ef64c..595bb08a2f 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -141,7 +141,7 @@ public Response createRole(Role jsonRole) { failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); try (QueryManager qm = new QueryManager()) { - final Role role = qm.createRole(jsonRole.getName(), jsonRole.getDescription(), jsonRole.getPermissions()); + final Role role = qm.createRole(jsonRole.getName(), jsonRole.getPermissions()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role created: " + role.getName()); return Response.status(Response.Status.CREATED).entity(role).build(); From 578ce5d8dd32a11deb8ab1394b22a9264311088d Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 10 Mar 2025 14:14:48 -0600 Subject: [PATCH 108/181] remove tostring bug, update role serialVersionID Signed-off-by: Allen Shearin --- apiserver/src/main/java/org/dependencytrack/model/Role.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/model/Role.java b/apiserver/src/main/java/org/dependencytrack/model/Role.java index f278a9ebd7..6adac75ba5 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/Role.java +++ b/apiserver/src/main/java/org/dependencytrack/model/Role.java @@ -65,7 +65,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class Role implements Serializable { - private static final long serialVersionUID = -7592438796591673355L; + private static final long serialVersionUID = -427858073810766917L; /** * Defines JDO fetch groups for this class. @@ -152,7 +152,7 @@ public String toString() { .map(permission -> permission.getName()) .toList(); - return "%s{id=%d, name='%s', description='%s', permissions=%s}".formatted( + return "%s{id=%d, name='%s', permissions=%s}".formatted( getClass().getSimpleName(), id, name, From e54b52cbdbe178da3570ed5e5182b83e80275a2c Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 12 Mar 2025 16:59:10 -0600 Subject: [PATCH 109/181] add getunassignedprojects to role dao.java, getunassignedprojects to ac resoucre.java Signed-off-by: Allen Shearin --- .../persistence/jdbi/RoleDao.java | 84 ++++++++++++++---- .../jdbi/mapping/ProjectRowMapper.java | 88 +++++++++++++++++++ .../resources/v1/AccessControlResource.java | 50 +++++++++++ 3 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 0ce3d1ac77..92c5dc8031 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -18,7 +18,13 @@ */ package org.dependencytrack.persistence.jdbi; +import java.util.List; + +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.jdbi.mapping.ProjectRowMapper; +import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; /** @@ -34,27 +40,27 @@ public interface RoleDao { int deleteRole(@Bind final long roleId); @SqlUpdate(/* language=sql */ """ - DELETE - FROM "LDAPUSERS_PROJECTS_ROLES" - WHERE "LDAPUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) + DELETE + FROM "LDAPUSERS_PROJECTS_ROLES" + WHERE "LDAPUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) int removeRoleFromLdapUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); @SqlUpdate(/* language=sql */ """ - DELETE - FROM "MANAGEDUSERS_PROJECTS_ROLES" - WHERE "MANAGEDUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) + DELETE + FROM "MANAGEDUSERS_PROJECTS_ROLES" + WHERE "MANAGEDUSER_ID" = :userId + AND "PROJECT_ACCESS_ROLE_ID" IN ( + SELECT "ID" + FROM "PROJECT_ACCESS_ROLES" + WHERE "ROLE_ID" = :roleId + AND "PROJECT_ID" = :projectId) + """) int removeRoleFromManagedUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); @SqlUpdate(/* language=sql */ """ @@ -69,4 +75,46 @@ public interface RoleDao { """) int removeRoleFromOidcUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + @SqlQuery(/* language=sql */ """ + SELECT * + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "LDAPUSERS_PROJECTS_ROLES" + ON "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "LDAPUSER" + ON "LDAPUSER"."ID" = "LDAPUSERS_PROJECTS_ROLES"."LDAPUSER_ID" + WHERE "LDAPUSER"."USERNAME" != :username OR "LDAPUSER"."USERNAME" IS NULL + """) + @RegisterRowMapper(ProjectRowMapper.class) + List getLdapUserUnassignedProjects(@Bind final String username); + + @SqlQuery(/* language=sql */ """ + SELECT * + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "MANAGEDUSERS_PROJECTS_ROLES" + ON "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "MANAGEDUSER" + ON "MANAGEDUSER"."ID" = "MANAGEDUSERS_PROJECTS_ROLES"."MANAGEDUSER_ID" + WHERE "MANAGEDUSER"."USERNAME" != :username OR "MANAGEDUSER"."USERNAME" IS NULL + """) + @RegisterRowMapper(ProjectRowMapper.class) + List getManagedUserUnassignedProjects(@Bind final String username); + + @SqlQuery(/* language=sql */ """ + SELECT * + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "OIDCUSERS_PROJECTS_ROLES" + ON "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "OIDCUSER" + ON "OIDCUSER"."ID" = "OIDCUSERS_PROJECTS_ROLES"."OIDCUSER_ID" + WHERE "OIDCUSER"."USERNAME" != :username OR "OIDCUSER"."USERNAME" IS NULL + """) + @RegisterRowMapper(ProjectRowMapper.class) + List getOidcUserUnassignedProjects(@Bind final String username); + } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java new file mode 100644 index 0000000000..ba435afed2 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java @@ -0,0 +1,88 @@ +/* + * 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.jdbi.mapping; + +import org.cyclonedx.model.ExternalReference; +import org.dependencytrack.model.Classifier; +import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.converter.OrganizationalContactsJsonConverter; +import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; + +import com.fasterxml.jackson.core.type.TypeReference; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.deserializeJson; +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; + +public class ProjectRowMapper implements RowMapper { + + private static final TypeReference> EXTERNAL_REFS_TYPE_REF = new TypeReference<>() { + }; + + @Override + public Project map(final ResultSet rs, final StatementContext ctx) throws SQLException { + final var project = new Project(); + + maybeSet(rs, "ID", ResultSet::getLong, project::setId); + maybeSet(rs, "CLASSIFIER", ResultSet::getString, Classifier::valueOf); + maybeSet(rs, "CPE", ResultSet::getString, project::setCpe); + maybeSet(rs, "DESCRIPTION", ResultSet::getString, project::setDescription); + maybeSet(rs, "DIRECT_DEPENDENCIES", ResultSet::getString, project::setDirectDependencies); + deserializeJson(rs, "EXTERNAL_REFERENCES", EXTERNAL_REFS_TYPE_REF); + maybeSet(rs, "GROUP", ResultSet::getString, project::setGroup); + maybeSet(rs, "LAST_BOM_IMPORTED", ResultSet::getDate, project::setLastBomImport); + maybeSet(rs, "LAST_BOM_IMPORTED_FORMAT", ResultSet::getString, project::setLastBomImportFormat); + maybeSet(rs, "LAST_RISKSCORE", ResultSet::getDouble, project::setLastInheritedRiskScore); + maybeSet(rs, "NAME", ResultSet::getString, project::setName); + maybeSet(rs, "PARENT_PROJECT_ID", ResultSet::getLong, value -> { + var parent = new Project(); + parent.setId(value); + project.setParent(parent); + }); + maybeSet(rs, "PUBLISHER", ResultSet::getString, project::setPublisher); + maybeSet(rs, "PURL", ResultSet::getString, project::setPurl); + maybeSet(rs, "SWIDTAGID", ResultSet::getString, project::setSwidTagId); + maybeSet(rs, "UUID", ResultSet::getString, value -> { + var uuid = UUID.fromString(value); + project.setUuid(uuid); + }); + maybeSet(rs, "VERSION", ResultSet::getString, project::setVersion); + maybeSet(rs, "SUPPLIER", ResultSet::getString, value -> { + var converter = new OrganizationalEntityJsonConverter(); + project.setSupplier(converter.convertToAttribute(value)); + }); + maybeSet(rs, "MANUFACTURER", ResultSet::getString, value -> { + var converter = new OrganizationalEntityJsonConverter(); + project.setManufacturer(converter.convertToAttribute(value)); + }); + maybeSet(rs, "AUTHORS", ResultSet::getString, values -> { + var converter = new OrganizationalContactsJsonConverter(); + project.setAuthors(converter.convertToAttribute(values)); + }); + + return project; + } + +} \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 3cac61d67e..e4fa187b8e 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -19,7 +19,11 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Team; +import alpine.model.UserPrincipal; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -107,6 +111,52 @@ public Response retrieveProjects(@Parameter(description = "The UUID of the team } } + @GET + @Path("/user/{username}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns the projects accessible by the specified user", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_READ

        ") + @PaginatedApi + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Projects accessible by the specified user", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", + schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "No unassigned projects for specified user."), + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) + public Response retrieveUserProjects( + @Parameter(description = "The username to retrieve projects for", + required = true) @PathParam("username") String username, + @Parameter(description = "Optionally excludes inactive projects from being returned", + required = false) @QueryParam("excludeInactive") boolean excludeInactive, + @Parameter(description = "Optionally excludes children projects from being returned", + required = false) @QueryParam("onlyRoot") boolean onlyRoot) { + + try (QueryManager qm = new QueryManager()) { + UserPrincipal principal = qm.getUserPrincipal(username); + + try (final Handle jdbiHandle = openJdbiHandle()) { + var dao = jdbiHandle.attach(RoleDao.class); + List projects = switch (principal) { + case LdapUser user -> dao.getLdapUserUnassignedProjects(user.getUsername()); + case ManagedUser user -> dao.getManagedUserUnassignedProjects(user.getUsername()); + case OidcUser user -> dao.getOidcUserUnassignedProjects(user.getUsername()); + default -> Collections.emptyList(); + }; + + if (projects.isEmpty()) + return Response.status(Response.Status.NOT_FOUND).entity("No unassigned projects for specified user.").build(); + + return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); + } + } + } + @PUT @Path("/mapping") @Produces(MediaType.APPLICATION_JSON) From e8ee4b32826ba435dd78318e41e80bf5b73cc1b7 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Fri, 14 Mar 2025 16:24:51 -0600 Subject: [PATCH 110/181] addRoleToUser working Signed-off-by: Allen Shearin --- .../persistence/QueryManager.java | 46 +++++++ .../persistence/RoleQueryManager.java | 81 ++++-------- .../persistence/jdbi/RoleDao.java | 124 +++++++++++------- .../resources/v1/AccessControlResource.java | 6 +- .../resources/v1/RoleProjectRequest.java | 45 +++++++ .../resources/v1/UserResource.java | 24 ++-- 6 files changed, 204 insertions(+), 122 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 4c05df905f..b3d0c8690e 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -25,6 +25,9 @@ import alpine.model.ConfigProperty; import alpine.model.Permission; import alpine.model.IConfigProperty.PropertyType; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.Team; import alpine.model.UserPrincipal; @@ -62,6 +65,7 @@ import org.dependencytrack.model.IntegrityMetaComponent; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; +import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.Policy; @@ -126,6 +130,7 @@ import java.util.UUID; import java.util.concurrent.Callable; import java.util.function.Predicate; +import java.util.function.Supplier; import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED; import static org.dependencytrack.proto.vulnanalysis.v1.ScanStatus.SCAN_STATUS_FAILED; @@ -491,6 +496,38 @@ public QueryManager withL2CacheDisabled() { return this; } + /** + * Get the IDs of the {@link MappedRole}s a given {@link Principal} is a member of. + * + * @return A {@link Set} of {@link MappedRole} IDs + */ + protected Set getRoleIds(final Principal principal, final Project project) { + String usersField; + + final MappedRole mappedRole = new MappedRole(); + Supplier roleIds = mappedRole.getRole()::getId; + + switch (principal) { + case LdapUser ldapUser -> usersField = "ldapUsers"; + case ManagedUser managedUser -> usersField = "managedUsers"; + case OidcUser oidcUser -> usersField = "oidcUsers"; + default -> { + return Collections.emptySet(); + } + }; + + Query query = pm.newQuery(MappedRole.class) + .filter("project.id == :projectId && %s.contains(:principal)".formatted(usersField)) + .setNamedParameters(Map.ofEntries( + Map.entry("principal", principal), + Map.entry("projectId", project.getId()))); + + return Set.of(executeAndCloseList(query).stream() + .map(MappedRole::getRole) + .map(Role::getId) + .toArray(Long[]::new)); + } + /** * Get the IDs of the {@link Team}s a given {@link Principal} is a member of. * @@ -870,10 +907,19 @@ public Role getRole(String uuid) { return getRoleQueryManager().getRole(null); } + public List getUserRoles(UserPrincipal user) { + return getRoleQueryManager().getUserRoles(user); + } + public Role updateRole(Role transientRole) { return getRoleQueryManager().updateRole(transientRole); } + public List getUnassignedProjects(UserPrincipal user) { + // TODO Auto-generated method stub + return getRoleQueryManager().getUnassignedProjects(user); + } + public Vulnerability createVulnerability(Vulnerability vulnerability, boolean commitIndex) { return getVulnerabilityQueryManager().createVulnerability(vulnerability, commitIndex); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 918cc1df9d..ddaf9397f2 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.persistence; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -29,6 +28,7 @@ import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; +import org.dependencytrack.persistence.jdbi.JdbiFactory; import org.dependencytrack.persistence.jdbi.RoleDao; import org.jdbi.v3.core.Handle; @@ -72,6 +72,28 @@ public List getRoles() { return query.executeList(); } + @Override + public List getUserRoles(UserPrincipal user) { + String usersField; + + switch (user) { + case LdapUser ldapUser -> usersField = "ldapUsers"; + case ManagedUser managedUser -> usersField = "managedUsers"; + case OidcUser oidcUser -> usersField = "oidcUsers"; + default -> { + return null; + } + } + ; + + Query query = pm.newQuery(MappedRole.class) + .filter("%s.contains(:user)".formatted(usersField)) + .setNamedParameters(Map.ofEntries( + Map.entry("user", user))); + + return executeAndCloseList(query); + } + @Override public Role getRole(String uuid) { final Query query = pm.newQuery(Role.class, "uuid == :uuid"); @@ -83,11 +105,6 @@ public List getUnassignedProjects(final String username) { return getUnassignedProjects(getUserPrincipal(username)); } - public List getUnassignedProjects(final UserPrincipal principal) { - // TODO: Implement getUnassignedProjects - return Collections.emptyList(); - } - public List getUnassignedRolePermissions(final Role role) { // TODO: Implement getUnassignedRolePermissions return Collections.emptyList(); @@ -106,54 +123,12 @@ public Role updateRole(Role transientRole) { @Override public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { - Query query = pm.newQuery(MappedRole.class) - .filter("project.id == :projectId && role.id == :roleId") - .setNamedParameters(Map.of( - "roleId", role.getId(), - "projectId", project.getId())); - - try { - query.getFetchPlan().setGroup(MappedRole.FetchGroup.ALL.name()); - MappedRole result = query.executeUnique(); - - if (result == null) { - LOGGER.info("Creating role mapping for project: %s / role: %s" - .formatted(project.getName(), role.getName())); - - result = new MappedRole(); - result.setProject(project); - result.setRole(role); - } + return JdbiFactory.withJdbiHandle(handle -> { + RoleDao dao = handle.attach(RoleDao.class); + MappedRole mappedRole = dao.addProjectAccessRole(project.getId(), role.getId()); - result.setLdapUsers(result.getLdapUsers() != null ? result.getLdapUsers() : new ArrayList<>()); - result.setManagedUsers(result.getManagedUsers() != null ? result.getManagedUsers() : new ArrayList<>()); - result.setOidcUsers(result.getOidcUsers() != null ? result.getOidcUsers() : new ArrayList<>()); - - final MappedRole mappedRole = result; - - boolean modified = switch (user) { - case LdapUser ldapUser when !mappedRole.getLdapUsers().contains(ldapUser) -> { - mappedRole.addLdapUsers(ldapUser); - yield true; - } - case ManagedUser managedUser when !mappedRole.getManagedUsers().contains(managedUser) -> { - mappedRole.addManagedUsers(managedUser); - yield true; - } - case OidcUser oidcUser when !mappedRole.getOidcUsers().contains(oidcUser) -> { - mappedRole.addOidcUsers(oidcUser); - yield true; - } - default -> false; - }; - - if (modified) - persist(mappedRole); - - return modified; - } finally { - query.closeAll(); - } + return dao.addRoleToUser(user, mappedRole.getId()) == 1; + }); } @Override diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 92c5dc8031..ab8f20096d 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -20,26 +20,96 @@ import java.util.List; +import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; -import org.dependencytrack.persistence.jdbi.mapping.ProjectRowMapper; -import org.jdbi.v3.sqlobject.config.RegisterRowMapper; +import org.jdbi.v3.sqlobject.config.RegisterFieldMapper; import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.customizer.Define; +import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings; +import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import alpine.model.UserPrincipal; + /** * @since 5.6.0 */ public interface RoleDao { @SqlUpdate(/* language=sql */ """ + INSERT INTO "PROJECT_ACCESS_ROLES" ("PROJECT_ID", "ROLE_ID") + VALUES (:projectId, :roleId) + ON CONFLICT DO NOTHING + """) + @GetGeneratedKeys + @RegisterFieldMapper(MappedRole.class) + MappedRole addProjectAccessRole(@Bind final long projectId, @Bind final long roleId); + + @SqlUpdate(/* language=sql */ """ + <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> + INSERT INTO "${user.getClass().getSimpleName()?upper_case}S_PROJECTS_ROLES" + ("${user.getClass().getSimpleName()?upper_case}_ID", "PROJECT_ACCESS_ROLE_ID") + VALUES + (${user.getId()}, :projectAccessRoleId) + ON CONFLICT DO NOTHING + """) + @DefineNamedBindings + int addRoleToUser(@Define T user, @Bind final long projectAccessRoleId); + + @SqlUpdate(/* language=sql */""" DELETE FROM "ROLE" WHERE "ID" = :roleId """) int deleteRole(@Bind final long roleId); - @SqlUpdate(/* language=sql */ """ + @SqlQuery(/* language=sql */""" + SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "LDAPUSERS_PROJECTS_ROLES" + ON "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "LDAPUSER" + ON "LDAPUSER"."ID" = "LDAPUSERS_PROJECTS_ROLES"."LDAPUSER_ID" + WHERE "LDAPUSER"."USERNAME" != :username + OR "LDAPUSER"."USERNAME" IS NULL + """) + @RegisterFieldMapper(Project.class) + List getLdapUserUnassignedProjects(@Bind final String username); + + @SqlQuery(/* language=sql */""" + SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "MANAGEDUSERS_PROJECTS_ROLES" + ON "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "MANAGEDUSER" + ON "MANAGEDUSER"."ID" = "MANAGEDUSERS_PROJECTS_ROLES"."MANAGEDUSER_ID" + WHERE "MANAGEDUSER"."USERNAME" != :username + OR "MANAGEDUSER"."USERNAME" IS NULL + """) + @RegisterFieldMapper(Project.class) + List getManagedUserUnassignedProjects(@Bind final String username); + + @SqlQuery(/* language=sql */""" + SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + FROM "PROJECT" + LEFT JOIN "PROJECT_ACCESS_ROLES" + ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "OIDCUSERS_PROJECTS_ROLES" + ON "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" + LEFT JOIN "OIDCUSER" + ON "OIDCUSER"."ID" = "OIDCUSERS_PROJECTS_ROLES"."OIDCUSER_ID" + WHERE "OIDCUSER"."USERNAME" != :username + OR "OIDCUSER"."USERNAME" IS NULL + """) + @RegisterFieldMapper(Project.class) + List getOidcUserUnassignedProjects(@Bind final String username); + + @SqlUpdate(/* language=sql */""" DELETE FROM "LDAPUSERS_PROJECTS_ROLES" WHERE "LDAPUSER_ID" = :userId @@ -51,7 +121,7 @@ public interface RoleDao { """) int removeRoleFromLdapUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - @SqlUpdate(/* language=sql */ """ + @SqlUpdate(/* language=sql */""" DELETE FROM "MANAGEDUSERS_PROJECTS_ROLES" WHERE "MANAGEDUSER_ID" = :userId @@ -63,7 +133,7 @@ public interface RoleDao { """) int removeRoleFromManagedUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - @SqlUpdate(/* language=sql */ """ + @SqlUpdate(/* language=sql */""" DELETE FROM "OIDCUSERS_PROJECTS_ROLES" WHERE "OIDCUSER_ID" = :userId @@ -75,46 +145,4 @@ public interface RoleDao { """) int removeRoleFromOidcUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - @SqlQuery(/* language=sql */ """ - SELECT * - FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "LDAPUSERS_PROJECTS_ROLES" - ON "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "LDAPUSER" - ON "LDAPUSER"."ID" = "LDAPUSERS_PROJECTS_ROLES"."LDAPUSER_ID" - WHERE "LDAPUSER"."USERNAME" != :username OR "LDAPUSER"."USERNAME" IS NULL - """) - @RegisterRowMapper(ProjectRowMapper.class) - List getLdapUserUnassignedProjects(@Bind final String username); - - @SqlQuery(/* language=sql */ """ - SELECT * - FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "MANAGEDUSERS_PROJECTS_ROLES" - ON "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "MANAGEDUSER" - ON "MANAGEDUSER"."ID" = "MANAGEDUSERS_PROJECTS_ROLES"."MANAGEDUSER_ID" - WHERE "MANAGEDUSER"."USERNAME" != :username OR "MANAGEDUSER"."USERNAME" IS NULL - """) - @RegisterRowMapper(ProjectRowMapper.class) - List getManagedUserUnassignedProjects(@Bind final String username); - - @SqlQuery(/* language=sql */ """ - SELECT * - FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "OIDCUSERS_PROJECTS_ROLES" - ON "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "OIDCUSER" - ON "OIDCUSER"."ID" = "OIDCUSERS_PROJECTS_ROLES"."OIDCUSER_ID" - WHERE "OIDCUSER"."USERNAME" != :username OR "OIDCUSER"."USERNAME" IS NULL - """) - @RegisterRowMapper(ProjectRowMapper.class) - List getOidcUserUnassignedProjects(@Bind final String username); - -} +} \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index e4fa187b8e..f1d758521c 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -131,11 +131,7 @@ public Response retrieveProjects(@Parameter(description = "The UUID of the team @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) public Response retrieveUserProjects( @Parameter(description = "The username to retrieve projects for", - required = true) @PathParam("username") String username, - @Parameter(description = "Optionally excludes inactive projects from being returned", - required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @Parameter(description = "Optionally excludes children projects from being returned", - required = false) @QueryParam("onlyRoot") boolean onlyRoot) { + required = true) @PathParam("username") String username) { try (QueryManager qm = new QueryManager()) { UserPrincipal principal = qm.getUserPrincipal(username); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java new file mode 100644 index 0000000000..ccc744c5a7 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java @@ -0,0 +1,45 @@ +/* + * 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.resources.v1; + +import org.dependencytrack.model.validation.ValidUuid; + +public class RoleProjectRequest { + @ValidUuid + private String roleUUID; + + @ValidUuid + private String projectUUID; + + public String getRoleUUID() { + return roleUUID; + } + + public void setRoleUUID(String roleUUID) { + this.roleUUID = roleUUID; + } + + public String getProjectUUID() { + return projectUUID; + } + + public void setProjectUUID(String projectUUID) { + this.projectUUID = projectUUID; + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 572367b8d7..4e3a399496 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -802,21 +802,13 @@ private UserSubject buildUserSubject(final String username, final String email) @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) public Response addRoleToUser( @Parameter(description = "A valid username", required = true) - @PathParam("username") - String username, - - @Parameter(description = "The UUID of the role to associate username with", required = true) - IdentifiableObject identifiableObject, - - @Parameter(description = "The name of the project", required = true) - @QueryParam("projectName") - String projectName, - - @Parameter(description = "The version of the project") - @QueryParam("projectVersion") - String projectVersion) { + @PathParam("username") String username, + @Parameter(description = "Role and project information", required = true) + RoleProjectRequest roleProjectRequest) { try (QueryManager qm = new QueryManager()) { - final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); + LOGGER.info("received Project info: " + roleProjectRequest.getProjectUUID()); + LOGGER.info("received Role info: " + roleProjectRequest.getRoleUUID()); + final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.getRoleUUID()); if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); @@ -824,7 +816,7 @@ public Response addRoleToUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - Project project = qm.getProject(projectName, projectVersion); + Project project = qm.getProject(roleProjectRequest.getProjectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -835,7 +827,7 @@ public Response addRoleToUser( principal = qm.getObjectById(principal.getClass(), principal.getId()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s" - .formatted(principal.getName(), role.getName(), projectName)); + .formatted(principal.getName(), role.getName(), project.getName())); return Response.ok(principal).build(); } From b584ef5b170e26cca3801759e3bcf2a60d4f0319 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:34:16 -0500 Subject: [PATCH 111/181] refactor: flatten user role join tables (#14) Signed-off-by: Jonathan Howard --- .../org/dependencytrack/model/MappedRole.java | 200 --------------- .../dependencytrack/model/ProjectRole.java | 234 ++++++++++++++++++ .../persistence/ProjectQueryManager.java | 28 +-- .../persistence/QueryManager.java | 81 +++--- .../persistence/RoleQueryManager.java | 79 +++--- .../persistence/jdbi/RoleDao.java | 146 ++++------- .../jdbi/mapping/ProjectRoleRowMapper.java | 65 +++++ .../resources/v1/AccessControlResource.java | 14 +- .../main/resources/META-INF/persistence.xml | 10 +- 9 files changed, 443 insertions(+), 414 deletions(-) delete mode 100644 apiserver/src/main/java/org/dependencytrack/model/MappedRole.java create mode 100644 apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java create mode 100644 apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java diff --git a/apiserver/src/main/java/org/dependencytrack/model/MappedRole.java b/apiserver/src/main/java/org/dependencytrack/model/MappedRole.java deleted file mode 100644 index 6480e4f9a2..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/model/MappedRole.java +++ /dev/null @@ -1,200 +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.model; - -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.jdo.annotations.Column; -import javax.jdo.annotations.Element; -import javax.jdo.annotations.Extension; -import javax.jdo.annotations.FetchGroup; -import javax.jdo.annotations.IdGeneratorStrategy; -import javax.jdo.annotations.Join; -import javax.jdo.annotations.Order; -import javax.jdo.annotations.PersistenceCapable; -import javax.jdo.annotations.Persistent; -import javax.jdo.annotations.PrimaryKey; -import javax.jdo.annotations.Unique; - -/** - * Model for associating a role on a given project with users. - * - * @author Allen Shearin - * @since 5.6.0 - */ -@PersistenceCapable(table = "PROJECT_ACCESS_ROLES") -@Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "LDAPUSERS_PROJECTS_ROLES", - members = { "ldapUsers", "project", "role" }, - deferred = "true") -@Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "MANAGEDUSERS_PROJECTS_ROLES", - members = { "managedUsers", "project", "role" }, - deferred = "true") -@Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "OIDCUSERS_PROJECTS_ROLES", - members = { "oidcUsers", "project", "role" }, - deferred = "true") -@FetchGroup(name = "ALL", members = { - @Persistent(name = "role"), - @Persistent(name = "project"), - @Persistent(name = "ldapUsers"), - @Persistent(name = "managedUsers"), - @Persistent(name = "oidcUsers") -}) -@JsonInclude(JsonInclude.Include.NON_NULL) -public class MappedRole implements Serializable { - - private static final long serialVersionUID = 1982348710987098723L; - - /** - * Defines JDO fetch groups for this class. - */ - public enum FetchGroup { - ALL - } - - @PrimaryKey - @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE) - @JsonIgnore - private long id; - - @Persistent - @Column(name = "ROLE_ID", allowsNull = "false") - @JsonIgnore - private Role role; - - @Persistent - @Column(name = "PROJECT_ID", allowsNull = "false") - @JsonIgnore - private Project project; - - @Persistent(table = "LDAPUSERS_PROJECTS_ROLES", defaultFetchGroup = "true") - @Join(column = "PROJECT_ACCESS_ROLE_ID") - @Element(column = "LDAPUSER_ID") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List ldapUsers; - - @Persistent(table = "MANAGEDUSERS_PROJECTS_ROLES") - @Join(column = "PROJECT_ACCESS_ROLE_ID") - @Element(column = "MANAGEDUSER_ID") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List managedUsers; - - @Persistent(table = "OIDCUSERS_PROJECTS_ROLES") - @Join(column = "PROJECT_ACCESS_ROLE_ID") - @Element(column = "OIDCUSER_ID") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List oidcUsers; - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public Role getRole() { - return role; - } - - public void setRole(Role role) { - this.role = role; - } - - public Project getProject() { - return project; - } - - public void setProject(Project project) { - this.project = project; - } - - public List getLdapUsers() { - return ldapUsers; - } - - public void setLdapUsers(List ldapUsers) { - this.ldapUsers = ldapUsers; - } - - public void addLdapUsers(LdapUser... ldapUsers) { - if (this.ldapUsers == null) { - this.ldapUsers = new ArrayList<>(Arrays.asList(ldapUsers)); - - return; - } - - for (var user : ldapUsers) - if (!this.ldapUsers.contains(user)) - this.ldapUsers.add(user); - } - - public List getManagedUsers() { - return managedUsers; - } - - public void setManagedUsers(List managedUsers) { - this.managedUsers = managedUsers; - } - - public void addManagedUsers(ManagedUser... managedUsers) { - if (this.managedUsers == null) { - this.managedUsers = new ArrayList<>(Arrays.asList(managedUsers)); - - return; - } - - for (var user : managedUsers) - if (!this.managedUsers.contains(user)) - this.managedUsers.add(user); - } - - public List getOidcUsers() { - return oidcUsers; - } - - public void setOidcUsers(List oidcUsers) { - this.oidcUsers = oidcUsers; - } - - public void addOidcUsers(OidcUser... oidcUsers) { - if (this.oidcUsers == null) { - this.oidcUsers = new ArrayList<>(Arrays.asList(oidcUsers)); - - return; - } - - for (var user : oidcUsers) - if (!this.oidcUsers.contains(user)) - this.oidcUsers.add(user); - } - -} \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java b/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java new file mode 100644 index 0000000000..dbf4807603 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java @@ -0,0 +1,234 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import javax.jdo.annotations.Column; +import javax.jdo.annotations.Extension; +import javax.jdo.annotations.FetchGroup; +import javax.jdo.annotations.Order; +import javax.jdo.annotations.PersistenceAware; +import javax.jdo.annotations.PersistenceCapable; +import javax.jdo.annotations.Persistent; +import javax.jdo.annotations.Unique; + +/** + * Base class for user-project-role mapping. + * + * @author Jonathan Howard + * @since 5.6.0 + */ +@PersistenceAware +@JsonInclude(JsonInclude.Include.NON_NULL) +public abstract class ProjectRole implements Serializable { + + @Persistent(defaultFetchGroup = "true") + @Column(name = "ROLE_ID", allowsNull = "false") + @JsonIgnore + private Role role; + + @Persistent(defaultFetchGroup = "true") + @Column(name = "PROJECT_ID", allowsNull = "false") + @JsonIgnore + private Project project; + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public Project getProject() { + return project; + } + + public void setProject(Project project) { + this.project = project; + } + + /** + * Model for associating a role on a given project with LDAP users. + * + * @author Allen Shearin + * @since 5.6.0 + */ + @PersistenceCapable(table = "LDAPUSERS_PROJECTS_ROLES") + @Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "LDAPUSERS_PROJECTS_ROLES", + members = { "ldapUsers", "project", "role" }, + deferred = "true") + @FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "ldapUsers") + }) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class LdapUserProjectRole extends ProjectRole { + + private static final long serialVersionUID = 6018553054343647649L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @Persistent(defaultFetchGroup = "true") + @Column(name = "LDAPUSER_ID", allowsNull = "false") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List ldapUsers; + + public List getLdapUsers() { + return ldapUsers; + } + + public void setLdapUsers(List ldapUsers) { + this.ldapUsers = ldapUsers; + } + + public void addLdapUsers(LdapUser... ldapUsers) { + this.ldapUsers = Objects.requireNonNullElse(this.ldapUsers, new ArrayList()); + this.ldapUsers = Stream.concat(this.ldapUsers.stream(), Arrays.stream(ldapUsers)) + .distinct() + .sorted(Comparator.comparing(LdapUser::getUsername)) + .toList(); + } + + } + + /** + * Model for associating a role on a given project with managed users. + * + * @author Allen Shearin + * @since 5.6.0 + */ + @PersistenceCapable(table = "MANAGEDUSERS_PROJECTS_ROLES") + @Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "MANAGEDUSERS_PROJECTS_ROLES", + members = { "managedUsers", "project", "role" }, + deferred = "true") + @FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "managedUsers") + }) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class ManagedUserProjectRole extends ProjectRole { + + private static final long serialVersionUID = -380122087527236991L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @Persistent(defaultFetchGroup = "true") + @Column(name = "MANAGEDUSER_ID", allowsNull = "false") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List managedUsers; + + public List getManagedUsers() { + return managedUsers; + } + + public void setManagedUsers(List managedUsers) { + this.managedUsers = managedUsers; + } + + public void addManagedUsers(ManagedUser... managedUsers) { + this.managedUsers = Objects.requireNonNullElse(this.managedUsers, new ArrayList()); + this.managedUsers = Stream.concat(this.managedUsers.stream(), Arrays.stream(managedUsers)) + .distinct() + .sorted(Comparator.comparing(ManagedUser::getUsername)) + .toList(); + } + + } + + /** + * Model for associating a role on a given project with OIDC users. + * + * @author Allen Shearin + * @since 5.6.0 + */ + @PersistenceCapable(table = "OIDCUSERS_PROJECTS_ROLES") + @Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", + table = "OIDCUSERS_PROJECTS_ROLES", + members = { "oidcUsers", "project", "role" }, + deferred = "true") + @FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "oidcUsers") + }) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class OidcUserProjectRole extends ProjectRole { + + private static final long serialVersionUID = -5029209056240375886L; + + /** + * Defines JDO fetch groups for this class. + */ + public enum FetchGroup { + ALL + } + + @Persistent(defaultFetchGroup = "true") + @Column(name = "OIDCUSER_ID", allowsNull = "false") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List oidcUsers; + + public List getOidcUsers() { + return oidcUsers; + } + + public void setOidcUsers(List oidcUsers) { + this.oidcUsers = oidcUsers; + } + + public void addOidcUsers(OidcUser... oidcUsers) { + this.oidcUsers = Objects.requireNonNullElse(this.oidcUsers, new ArrayList()); + this.oidcUsers = Stream.concat(this.oidcUsers.stream(), Arrays.stream(oidcUsers)) + .distinct() + .sorted(Comparator.comparing(OidcUser::getUsername)) + .toList(); + } + + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 845a32cd7b..58b38ef473 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -1011,31 +1011,23 @@ public void bind(final Project project, final List tags) { @Override public boolean hasAccess(final Principal principal, final Project project) { - if (principal == null) { - // This is a system request being made (e.g. MetricsUpdateTask, etc) where there isn't a principal + if (!isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) + || principal == null // System request (e.g. MetricsUpdateTask, etc) where there isn't a principal + || super.hasAccessManagementPermission(principal)) // TODO: After Alpine >= 3.2.0: request.getEffectivePermission().contains(Permissions.ACCESS_MANAGEMENT.name()) return true; - } - - if (!isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED)) { - return true; - } - - // TODO: After upgrading to Alpine >= 3.2.0, this should become: - // request.getEffectivePermission().contains(Permissions.ACCESS_MANAGEMENT.name()) - // https://github.com/stevespringett/Alpine/pull/764 - if (super.hasAccessManagementPermission(principal)) { - return true; - } + final Set roleIds = getRoleIds(principal, project); final Set teamIds = getTeamIds(principal); - if (teamIds.isEmpty()) { + + if (teamIds.isEmpty() && roleIds.isEmpty()) return false; - } - final Query query = pm.newQuery(Query.SQL, "SELECT HAS_PROJECT_ACCESS(:projectId, :teamIds)"); + final Query query = pm.newQuery(Query.SQL, "SELECT HAS_PROJECT_ACCESS(:projectId, :teamIds, :roleIds)"); query.setNamedParameters(Map.ofEntries( Map.entry("projectId", project.getId()), - Map.entry("teamIds", teamIds.toArray(new Long[0])))); + Map.entry("teamIds", teamIds.toArray(Long[]::new)), + Map.entry("roleIds", roleIds.toArray(Long[]::new)))); + return executeAndCloseResultUnique(query, Boolean.class); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index b3d0c8690e..b777aea8c8 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -23,11 +23,10 @@ import alpine.common.validation.RegexSequence; import alpine.model.ApiKey; import alpine.model.ConfigProperty; -import alpine.model.Permission; -import alpine.model.IConfigProperty.PropertyType; import alpine.model.LdapUser; import alpine.model.ManagedUser; import alpine.model.OidcUser; +import alpine.model.IConfigProperty.PropertyType; import alpine.model.Permission; import alpine.model.Team; import alpine.model.UserPrincipal; @@ -65,7 +64,6 @@ import org.dependencytrack.model.IntegrityMetaComponent; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; -import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.NotificationPublisher; import org.dependencytrack.model.NotificationRule; import org.dependencytrack.model.Policy; @@ -75,6 +73,7 @@ import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.ProjectProperty; +import org.dependencytrack.model.ProjectRole; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; @@ -123,14 +122,12 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.function.Predicate; -import java.util.function.Supplier; import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED; import static org.dependencytrack.proto.vulnanalysis.v1.ScanStatus.SCAN_STATUS_FAILED; @@ -497,33 +494,40 @@ public QueryManager withL2CacheDisabled() { } /** - * Get the IDs of the {@link MappedRole}s a given {@link Principal} is a member of. + * Get the IDs of the {@link ProjectRole}s a given {@link Principal} is a member of. * - * @return A {@link Set} of {@link MappedRole} IDs + * @return A {@link Set} of {@link ProjectRole} IDs */ protected Set getRoleIds(final Principal principal, final Project project) { String usersField; - - final MappedRole mappedRole = new MappedRole(); - Supplier roleIds = mappedRole.getRole()::getId; + Class cls; switch (principal) { - case LdapUser ldapUser -> usersField = "ldapUsers"; - case ManagedUser managedUser -> usersField = "managedUsers"; - case OidcUser oidcUser -> usersField = "oidcUsers"; + case LdapUser ldapUser -> { + usersField = "ldapUsers"; + cls = ProjectRole.LdapUserProjectRole.class; + } + case ManagedUser managedUser -> { + usersField = "managedUsers"; + cls = ProjectRole.ManagedUserProjectRole.class; + } + case OidcUser oidcUser -> { + usersField = "oidcUsers"; + cls = ProjectRole.OidcUserProjectRole.class; + } default -> { return Collections.emptySet(); } }; - Query query = pm.newQuery(MappedRole.class) + Query query = pm.newQuery(cls) .filter("project.id == :projectId && %s.contains(:principal)".formatted(usersField)) .setNamedParameters(Map.ofEntries( Map.entry("principal", principal), Map.entry("projectId", project.getId()))); return Set.of(executeAndCloseList(query).stream() - .map(MappedRole::getRole) + .map(ProjectRole::getRole) .map(Role::getId) .toArray(Long[]::new)); } @@ -534,19 +538,11 @@ protected Set getRoleIds(final Principal principal, final Project project) * @return A {@link Set} of {@link Team} IDs */ protected Set getTeamIds(final Principal principal) { - final var principalTeamIds = new HashSet(); - if (principal instanceof final UserPrincipal userPrincipal - && userPrincipal.getTeams() != null) { - for (final Team userInTeam : userPrincipal.getTeams()) { - principalTeamIds.add(userInTeam.getId()); - } - } else if (principal instanceof final ApiKey apiKey - && apiKey.getTeams() != null) { - for (final Team userInTeam : apiKey.getTeams()) { - principalTeamIds.add(userInTeam.getId()); - } - } - return principalTeamIds; + return switch (principal) { + case UserPrincipal userPrincipal -> Set.of(userPrincipal.getTeams().toArray(Long[]::new)); + case ApiKey apiKey -> Set.of(apiKey.getTeams().toArray(Long[]::new)); + default -> Collections.emptySet(); + }; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -907,19 +903,10 @@ public Role getRole(String uuid) { return getRoleQueryManager().getRole(null); } - public List getUserRoles(UserPrincipal user) { - return getRoleQueryManager().getUserRoles(user); - } - public Role updateRole(Role transientRole) { return getRoleQueryManager().updateRole(transientRole); } - public List getUnassignedProjects(UserPrincipal user) { - // TODO Auto-generated method stub - return getRoleQueryManager().getUnassignedProjects(user); - } - public Vulnerability createVulnerability(Vulnerability vulnerability, boolean commitIndex) { return getVulnerabilityQueryManager().createVulnerability(vulnerability, commitIndex); } @@ -1215,8 +1202,24 @@ public boolean addRoleToUser(UserPrincipal principal, Role role, Project project return getRoleQueryManager().addRoleToUser(principal, role, project); } - public boolean removeRoleFromUser(UserPrincipal principal, Role role, Project project){ - return getRoleQueryManager().removeRoleFromUser(principal, role, project); + public List getUnassignedProjects(final String username) { + return getRoleQueryManager().getUnassignedProjects(username); + } + + public List getUnassignedProjects(final UserPrincipal user) { + return getRoleQueryManager().getUnassignedProjects(user); + } + + public List getUnassignedRolePermissions(final Role role) { + return getRoleQueryManager().getUnassignedRolePermissions(role); + } + + public List getUserRoles(UserPrincipal user) { + return getRoleQueryManager().getUserRoles(user); + } + + public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { + return getRoleQueryManager().removeRoleFromUser(user, role, project); } public NotificationRule createNotificationRule(String name, NotificationScope scope, NotificationLevel level, NotificationPublisher publisher) { diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index ddaf9397f2..9d09558591 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -18,30 +18,24 @@ */ package org.dependencytrack.persistence; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.jdo.PersistenceManager; import javax.jdo.Query; -import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; +import org.dependencytrack.model.ProjectRole; import org.dependencytrack.persistence.jdbi.JdbiFactory; import org.dependencytrack.persistence.jdbi.RoleDao; -import org.jdbi.v3.core.Handle; import alpine.common.logging.Logger; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; -import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; - final class RoleQueryManager extends QueryManager implements IQueryManager { private static final Logger LOGGER = Logger.getLogger(RoleQueryManager.class); @@ -72,28 +66,6 @@ public List getRoles() { return query.executeList(); } - @Override - public List getUserRoles(UserPrincipal user) { - String usersField; - - switch (user) { - case LdapUser ldapUser -> usersField = "ldapUsers"; - case ManagedUser managedUser -> usersField = "managedUsers"; - case OidcUser oidcUser -> usersField = "oidcUsers"; - default -> { - return null; - } - } - ; - - Query query = pm.newQuery(MappedRole.class) - .filter("%s.contains(:user)".formatted(usersField)) - .setNamedParameters(Map.ofEntries( - Map.entry("user", user))); - - return executeAndCloseList(query); - } - @Override public Role getRole(String uuid) { final Query query = pm.newQuery(Role.class, "uuid == :uuid"); @@ -101,13 +73,33 @@ public Role getRole(String uuid) { return query.executeUnique(); } + @Override + public List getUserRoles(UserPrincipal user) { + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user)); + } + public List getUnassignedProjects(final String username) { return getUnassignedProjects(getUserPrincipal(username)); } + public List getUnassignedProjects(final UserPrincipal user) { + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserUnassignedProjects(user)); + } + public List getUnassignedRolePermissions(final Role role) { - // TODO: Implement getUnassignedRolePermissions - return Collections.emptyList(); + List permissions = new ArrayList<>(); + + var permissionNames = role.getPermissions().stream() + .map(Permission::getName) + .toList(); + + Query query = pm.newQuery(Permission.class) + .filter("!:permissionNames.contains(name)") + .setNamedParameters(Map.of("permissionNames", permissionNames)); + + permissions.addAll(executeAndCloseList(query)); + + return permissions; } @Override @@ -123,29 +115,14 @@ public Role updateRole(Role transientRole) { @Override public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { - return JdbiFactory.withJdbiHandle(handle -> { - RoleDao dao = handle.attach(RoleDao.class); - MappedRole mappedRole = dao.addProjectAccessRole(project.getId(), role.getId()); - - return dao.addRoleToUser(user, mappedRole.getId()) == 1; - }); + return JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addRoleToUser(user, project.getId(), role.getId())) == 1; } @Override public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { - try (final Handle jdbiHandle = openJdbiHandle()) { - int count = switch (user) { - case LdapUser ldapUser -> jdbiHandle.attach(RoleDao.class) - .removeRoleFromLdapUser(ldapUser.getId(), project.getId(), role.getId()); - case ManagedUser managedUser -> jdbiHandle.attach(RoleDao.class) - .removeRoleFromManagedUser(managedUser.getId(), project.getId(), role.getId()); - case OidcUser oidcUser -> jdbiHandle.attach(RoleDao.class) - .removeRoleFromOidcUser(oidcUser.getId(), project.getId(), role.getId()); - default -> 0; - }; - - return count == 1; - } + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser(user, + project, role.getId())) > 0; } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index ab8f20096d..3a7bd058ef 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -20,13 +20,15 @@ import java.util.List; -import org.dependencytrack.model.MappedRole; import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.persistence.jdbi.mapping.ProjectRoleRowMapper; + import org.jdbi.v3.sqlobject.config.RegisterFieldMapper; +import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.customizer.Define; import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings; -import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; @@ -38,111 +40,73 @@ public interface RoleDao { @SqlUpdate(/* language=sql */ """ - INSERT INTO "PROJECT_ACCESS_ROLES" ("PROJECT_ID", "ROLE_ID") - VALUES (:projectId, :roleId) - ON CONFLICT DO NOTHING + DELETE + FROM "ROLE" + WHERE "ID" = :roleId """) - @GetGeneratedKeys - @RegisterFieldMapper(MappedRole.class) - MappedRole addProjectAccessRole(@Bind final long projectId, @Bind final long roleId); + int deleteRole(@Bind final long roleId); @SqlUpdate(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - INSERT INTO "${user.getClass().getSimpleName()?upper_case}S_PROJECTS_ROLES" - ("${user.getClass().getSimpleName()?upper_case}_ID", "PROJECT_ACCESS_ROLE_ID") + <#assign prefix = user.getClass().getSimpleName()?upper_case> + INSERT INTO "${prefix}S_PROJECTS_ROLES" + ("${prefix}_ID", "PROJECT_ID", "ROLE_ID") VALUES - (${user.getId()}, :projectAccessRoleId) + (${user.getId()}, :projectId, :roleId) ON CONFLICT DO NOTHING """) @DefineNamedBindings - int addRoleToUser(@Define T user, @Bind final long projectAccessRoleId); + int addRoleToUser(@Define T user, @Bind long projectId, @Bind long roleId); - @SqlUpdate(/* language=sql */""" + @SqlUpdate(/* language=sql */ """ + <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> + <#-- @ftlvariable name="project" type="org.dependencytrack.model.Project" --> + <#assign prefix = user.getClass().getSimpleName()?upper_case> DELETE - FROM "ROLE" - WHERE "ID" = :roleId + FROM "${prefix}S_PROJECTS_ROLES" + WHERE "${prefix}_ID" = ${user.getId()} + AND "ROLE_ID" = :roleId + AND "PROJECT_ID" IN ( + SELECT "ID" + FROM "PROJECT" + WHERE "NAME" = '${project.getName()}' + ) """) - int deleteRole(@Bind final long roleId); + @DefineNamedBindings + int removeRoleFromUser(@Define T user, @Define Project project, @Bind long roleId); - @SqlQuery(/* language=sql */""" - SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + @SqlQuery(/* language=sql */ """ + <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> + <#assign prefix = user.getClass().getSimpleName()?upper_case> + SELECT "PROJECT"."ID" AS "PROJECT_ID", + "ROLE"."ID" AS "ROLE_ID" FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "LDAPUSERS_PROJECTS_ROLES" - ON "LDAPUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "LDAPUSER" - ON "LDAPUSER"."ID" = "LDAPUSERS_PROJECTS_ROLES"."LDAPUSER_ID" - WHERE "LDAPUSER"."USERNAME" != :username - OR "LDAPUSER"."USERNAME" IS NULL - """) - @RegisterFieldMapper(Project.class) - List getLdapUserUnassignedProjects(@Bind final String username); - - @SqlQuery(/* language=sql */""" - SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" - FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "MANAGEDUSERS_PROJECTS_ROLES" - ON "MANAGEDUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "MANAGEDUSER" - ON "MANAGEDUSER"."ID" = "MANAGEDUSERS_PROJECTS_ROLES"."MANAGEDUSER_ID" - WHERE "MANAGEDUSER"."USERNAME" != :username - OR "MANAGEDUSER"."USERNAME" IS NULL + INNER JOIN "${prefix}S_PROJECTS_ROLES" + ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + INNER JOIN "${prefix}" + ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" + INNER JOIN "ROLE" + ON "ROLE"."ID" = "${prefix}S_PROJECTS_ROLES"."ROLE_ID" + WHERE "${prefix}"."USERNAME" != '${user.getUsername()}' + OR "${prefix}"."USERNAME" IS NULL """) - @RegisterFieldMapper(Project.class) - List getManagedUserUnassignedProjects(@Bind final String username); + @RegisterRowMapper(ProjectRoleRowMapper.class) + @DefineNamedBindings + List getUserRoles(@Define T user); - @SqlQuery(/* language=sql */""" - SELECT DISTINCT "PROJECT"."UUID", "PROJECT"."NAME" + @SqlQuery(/* language=sql */ """ + <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> + <#assign prefix = user.getClass().getSimpleName()?upper_case> + SELECT "PROJECT"."ID", "PROJECT"."NAME" FROM "PROJECT" - LEFT JOIN "PROJECT_ACCESS_ROLES" - ON "PROJECT_ACCESS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "OIDCUSERS_PROJECTS_ROLES" - ON "OIDCUSERS_PROJECTS_ROLES"."PROJECT_ACCESS_ROLE_ID" = "PROJECT_ACCESS_ROLES"."ID" - LEFT JOIN "OIDCUSER" - ON "OIDCUSER"."ID" = "OIDCUSERS_PROJECTS_ROLES"."OIDCUSER_ID" - WHERE "OIDCUSER"."USERNAME" != :username - OR "OIDCUSER"."USERNAME" IS NULL + LEFT JOIN "${prefix}S_PROJECTS_ROLES" + ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" + LEFT JOIN "${prefix}" + ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" + WHERE "${prefix}"."USERNAME" != '${user.getUsername()}' + OR "${prefix}"."USERNAME" IS NULL """) @RegisterFieldMapper(Project.class) - List getOidcUserUnassignedProjects(@Bind final String username); - - @SqlUpdate(/* language=sql */""" - DELETE - FROM "LDAPUSERS_PROJECTS_ROLES" - WHERE "LDAPUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) - int removeRoleFromLdapUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - - @SqlUpdate(/* language=sql */""" - DELETE - FROM "MANAGEDUSERS_PROJECTS_ROLES" - WHERE "MANAGEDUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) - int removeRoleFromManagedUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); - - @SqlUpdate(/* language=sql */""" - DELETE - FROM "OIDCUSERS_PROJECTS_ROLES" - WHERE "OIDCUSER_ID" = :userId - AND "PROJECT_ACCESS_ROLE_ID" IN ( - SELECT "ID" - FROM "PROJECT_ACCESS_ROLES" - WHERE "ROLE_ID" = :roleId - AND "PROJECT_ID" = :projectId) - """) - int removeRoleFromOidcUser(@Bind final long userId, @Bind final long projectId, @Bind final long roleId); + List getUserUnassignedProjects(@Define T user); -} \ No newline at end of file +} diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java new file mode 100644 index 0000000000..d71d0bfdbe --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java @@ -0,0 +1,65 @@ +/* + * 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.jdbi.mapping; + +import org.dependencytrack.model.Project; +import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; +import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; +import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; +import org.dependencytrack.model.Role; + +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.hasColumn; +import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; + +public class ProjectRoleRowMapper implements RowMapper { + + public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) throws SQLException { + ProjectRole projectRole; + + switch (resultSet) { + case ResultSet rs when hasColumn(rs, "LDAPUSER_ID") -> projectRole = new LdapUserProjectRole(); + case ResultSet rs when hasColumn(rs, "MANAGEDUSER_ID") -> projectRole = new ManagedUserProjectRole(); + case ResultSet rs when hasColumn(rs, "OIDCUSER_ID") -> projectRole = new OidcUserProjectRole(); + default -> { + return null; + } + } + + maybeSet(resultSet, "PROJECT_ID", ResultSet::getLong, value -> { + var project = new Project(); + project.setId(value); + projectRole.setProject(project); + }); + + maybeSet(resultSet, "ROLE_ID", ResultSet::getLong, value -> { + var role = new Role(); + role.setId(value); + projectRole.setRole(role); + }); + + return projectRole; + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index f1d758521c..7952de5882 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -19,9 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.persistence.PaginatedResult; @@ -138,14 +135,9 @@ public Response retrieveUserProjects( try (final Handle jdbiHandle = openJdbiHandle()) { var dao = jdbiHandle.attach(RoleDao.class); - List projects = switch (principal) { - case LdapUser user -> dao.getLdapUserUnassignedProjects(user.getUsername()); - case ManagedUser user -> dao.getManagedUserUnassignedProjects(user.getUsername()); - case OidcUser user -> dao.getOidcUserUnassignedProjects(user.getUsername()); - default -> Collections.emptyList(); - }; - - if (projects.isEmpty()) + List projects = dao.getUserUnassignedProjects(principal); + + if (projects == null || projects.isEmpty()) return Response.status(Response.Status.NOT_FOUND).entity("No unassigned projects for specified user.").build(); return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); diff --git a/apiserver/src/main/resources/META-INF/persistence.xml b/apiserver/src/main/resources/META-INF/persistence.xml index 6d71d8b85e..511460ad2d 100644 --- a/apiserver/src/main/resources/META-INF/persistence.xml +++ b/apiserver/src/main/resources/META-INF/persistence.xml @@ -28,13 +28,13 @@ org.dependencytrack.model.Component org.dependencytrack.model.ComponentOccurrence org.dependencytrack.model.ComponentProperty - org.dependencytrack.model.IntegrityMetaComponent org.dependencytrack.model.DependencyMetrics org.dependencytrack.model.Epss org.dependencytrack.model.FindingAttribution + org.dependencytrack.model.IntegrityAnalysis + org.dependencytrack.model.IntegrityMetaComponent org.dependencytrack.model.License org.dependencytrack.model.LicenseGroup - org.dependencytrack.model.MappedRole org.dependencytrack.model.NotificationPublisher org.dependencytrack.model.NotificationRule org.dependencytrack.model.Policy @@ -45,6 +45,9 @@ org.dependencytrack.model.ProjectMetadata org.dependencytrack.model.ProjectMetrics org.dependencytrack.model.ProjectProperty + org.dependencytrack.model.ProjectRole$LdapUserProjectRole + org.dependencytrack.model.ProjectRole$ManagedUserProjectRole + org.dependencytrack.model.ProjectRole$OidcUserProjectRole org.dependencytrack.model.Repository org.dependencytrack.model.RepositoryMetaComponent org.dependencytrack.model.Role @@ -57,11 +60,10 @@ org.dependencytrack.model.Vulnerability org.dependencytrack.model.VulnerabilityAlias org.dependencytrack.model.VulnerabilityMetrics + org.dependencytrack.model.VulnerabilityPolicyBundle org.dependencytrack.model.VulnerabilityScan org.dependencytrack.model.VulnerableSoftware org.dependencytrack.model.WorkflowState - org.dependencytrack.model.IntegrityAnalysis - org.dependencytrack.model.VulnerabilityPolicyBundle true NONE From 8bf2100bd07218516a0087bf2fd539ff5d8f4912 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 17 Mar 2025 14:13:24 -0600 Subject: [PATCH 112/181] update getunassignedprojects dao query Signed-off-by: Allen Shearin --- .../main/java/org/dependencytrack/persistence/jdbi/RoleDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 3a7bd058ef..8dd9dffd15 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -97,7 +97,7 @@ public interface RoleDao { @SqlQuery(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> <#assign prefix = user.getClass().getSimpleName()?upper_case> - SELECT "PROJECT"."ID", "PROJECT"."NAME" + SELECT "PROJECT"."ID", "PROJECT"."NAME", "PROJECT"."UUID" FROM "PROJECT" LEFT JOIN "${prefix}S_PROJECTS_ROLES" ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" From 6a5eabba06f939eda2cb6360de9bfe5e0aa46899 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 19 Mar 2025 12:39:39 -0600 Subject: [PATCH 113/181] working getUserRoles and associated mapper/methods/queries, draft removeRoleFromUser, move RoleProjectRequest Signed-off-by: Allen Shearin --- .../dependencytrack/model/ProjectRole.java | 3 -- .../persistence/QueryManager.java | 4 +- .../persistence/jdbi/RoleDao.java | 14 +++++-- .../jdbi/mapping/ProjectRoleRowMapper.java | 25 ++++++------ .../resources/v1/RoleResource.java | 38 +++++++++++++++++++ .../resources/v1/UserResource.java | 31 +++++---------- .../v1/{ => vo}/RoleProjectRequest.java | 2 +- 7 files changed, 74 insertions(+), 43 deletions(-) rename apiserver/src/main/java/org/dependencytrack/resources/v1/{ => vo}/RoleProjectRequest.java (96%) diff --git a/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java b/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java index dbf4807603..e0ac7d206d 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java @@ -18,7 +18,6 @@ */ package org.dependencytrack.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import alpine.model.LdapUser; @@ -54,12 +53,10 @@ public abstract class ProjectRole implements Serializable { @Persistent(defaultFetchGroup = "true") @Column(name = "ROLE_ID", allowsNull = "false") - @JsonIgnore private Role role; @Persistent(defaultFetchGroup = "true") @Column(name = "PROJECT_ID", allowsNull = "false") - @JsonIgnore private Project project; public Role getRole() { diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index b777aea8c8..83f5db70a5 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -539,8 +539,8 @@ protected Set getRoleIds(final Principal principal, final Project project) */ protected Set getTeamIds(final Principal principal) { return switch (principal) { - case UserPrincipal userPrincipal -> Set.of(userPrincipal.getTeams().toArray(Long[]::new)); - case ApiKey apiKey -> Set.of(apiKey.getTeams().toArray(Long[]::new)); + case UserPrincipal user -> Set.copyOf(user.getTeams().stream().map(Team::getId).toList()); + case ApiKey apiKey -> Set.copyOf(apiKey.getTeams().stream().map(Team::getId).toList()); default -> Collections.emptySet(); }; } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 8dd9dffd15..3495a4b278 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -74,12 +74,19 @@ public interface RoleDao { """) @DefineNamedBindings int removeRoleFromUser(@Define T user, @Define Project project, @Bind long roleId); + // (@Bind long userId, @Bind String projectName, @Bind long roleId) @SqlQuery(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> <#assign prefix = user.getClass().getSimpleName()?upper_case> - SELECT "PROJECT"."ID" AS "PROJECT_ID", - "ROLE"."ID" AS "ROLE_ID" + SELECT + "PROJECT"."ID" AS "PROJECT_ID", + "PROJECT"."NAME" AS "PROJECT_NAME", + "PROJECT"."UUID" AS "PROJECT_UUID", + "ROLE"."ID" AS "ROLE_ID", + "ROLE"."NAME" AS "ROLE_NAME", + "ROLE"."UUID" AS "ROLE_UUID", + "${prefix}"."ID" AS "${prefix}_ID" FROM "PROJECT" INNER JOIN "${prefix}S_PROJECTS_ROLES" ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" @@ -87,8 +94,7 @@ public interface RoleDao { ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" INNER JOIN "ROLE" ON "ROLE"."ID" = "${prefix}S_PROJECTS_ROLES"."ROLE_ID" - WHERE "${prefix}"."USERNAME" != '${user.getUsername()}' - OR "${prefix}"."USERNAME" IS NULL + WHERE "${prefix}"."USERNAME" = '${user.getUsername()}' """) @RegisterRowMapper(ProjectRoleRowMapper.class) @DefineNamedBindings diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java index d71d0bfdbe..6c7b115152 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java @@ -27,14 +27,15 @@ import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.statement.StatementContext; + import java.sql.ResultSet; import java.sql.SQLException; +import java.util.UUID; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.hasColumn; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; public class ProjectRoleRowMapper implements RowMapper { - public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) throws SQLException { ProjectRole projectRole; @@ -47,19 +48,21 @@ public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) th } } - maybeSet(resultSet, "PROJECT_ID", ResultSet::getLong, value -> { - var project = new Project(); - project.setId(value); - projectRole.setProject(project); + projectRole.setProject(new Project()); + projectRole.setRole(new Role()); + + maybeSet(resultSet, "PROJECT_ID", ResultSet::getLong, projectRole.getProject()::setId); + maybeSet(resultSet, "PROJECT_NAME", ResultSet::getString, projectRole.getProject()::setName); + maybeSet(resultSet, "PROJECT_UUID", ResultSet::getString, value -> { + projectRole.getProject().setUuid(UUID.fromString(value)); }); - maybeSet(resultSet, "ROLE_ID", ResultSet::getLong, value -> { - var role = new Role(); - role.setId(value); - projectRole.setRole(role); + maybeSet(resultSet, "ROLE_ID", ResultSet::getLong, projectRole.getRole()::setId); + maybeSet(resultSet, "ROLE_NAME", ResultSet::getString, projectRole.getRole()::setName); + maybeSet(resultSet, "ROLE_UUID", ResultSet::getString, value -> { + projectRole.getRole().setUuid(UUID.fromString(value)); }); return projectRole; } - -} +} \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 595bb08a2f..cc3e3385a6 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -48,13 +48,18 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.model.Role; +import org.dependencytrack.model.ProjectRole; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.persistence.jdbi.RoleDao; import org.jdbi.v3.core.Handle; import org.owasp.security.logging.SecurityMarkers; +import alpine.model.UserPrincipal; + import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; +import java.util.List; + /** * JAX-RS resources for processing roles. * @@ -205,4 +210,37 @@ public Response deleteRole(Role jsonRole) { } } + @GET + @Path("/{username}/roles") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of roles assigned to the specified user", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_READ

        ") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of roles assigned to the user", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProjectRole.class)))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user could not be found") + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) + public Response getUserRoles( + @Parameter(description = "A valid username", required = true) @PathParam("username") String username) { + try (QueryManager qm = new QueryManager()) { + UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) { + LOGGER.warn("User not found: " + username); + return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); + } + + List roles = qm.getUserRoles(principal); + if (roles == null || roles.isEmpty()) { + LOGGER.info("No roles found for user: " + username); + return Response.ok(List.of()).build(); + } + + return Response.ok(roles).build(); + } + } } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 4e3a399496..a2214f038f 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -33,6 +33,7 @@ import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.proto.notification.v1.UserSubject; +import org.dependencytrack.resources.v1.vo.RoleProjectRequest; import org.owasp.security.logging.SecurityMarkers; import alpine.Config; @@ -74,7 +75,6 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -845,31 +845,18 @@ public Response addRoleToUser( @ApiResponse( responseCode = "200", description = "Updated user with a specific role removed", - content = @Content(schema = @Schema(implementation = UserPrincipal.class)) - ), + content = @Content(schema = @Schema(implementation = UserPrincipal.class))), @ApiResponse(responseCode = "204", description = "The role has been successfully removed from the user"), @ApiResponse(responseCode = "304", description = "The user is not a member of the specified role"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The user or role could not be found") -}) - @PermissionRequired({Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE}) + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE }) public Response removeRoleFromUser( - @Parameter(description = "A valid username", required = true) - @PathParam("username") - String username, - - @Parameter(description = "The UUID of the role to associate username with", required = true) - IdentifiableObject identifiableObject, - - @Parameter(description = "The name of the project", required = true) - @QueryParam("projectName") - String projectName, - - @Parameter(description = "The version of the project") - @QueryParam("projectVersion") - String projectVersion) { + @Parameter(description = "A valid username", required = true) @PathParam("username") String username, + @Parameter(description = "Role and project information", required = true) RoleProjectRequest roleProjectRequest) { try (QueryManager qm = new QueryManager()) { - final Role role = qm.getObjectByUuid(Role.class, identifiableObject.getUuid()); + final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.getRoleUUID()); if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); @@ -877,7 +864,7 @@ public Response removeRoleFromUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - Project project = qm.getProject(projectName, projectVersion); + Project project = qm.getProject(roleProjectRequest.getProjectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -888,7 +875,7 @@ public Response removeRoleFromUser( principal = qm.getObjectById(principal.getClass(), principal.getId()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed role membership for: %s / role: %s / project: %s" - .formatted(principal.getName(), role.getName(), projectName)); + .formatted(principal.getName(), role.getName(), project.getName())); return Response.noContent().build(); } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java similarity index 96% rename from apiserver/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java rename to apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java index ccc744c5a7..664410074e 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleProjectRequest.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java @@ -16,7 +16,7 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) OWASP Foundation. All Rights Reserved. */ -package org.dependencytrack.resources.v1; +package org.dependencytrack.resources.v1.vo; import org.dependencytrack.model.validation.ValidUuid; From bd4f63a2355c85f8151827d8d03dea85fb0154e5 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 19 Mar 2025 13:59:27 -0600 Subject: [PATCH 114/181] alter getuserUnassignedProjects to not return 404 if no projects are unassigned, instead return empty list Signed-off-by: Allen Shearin --- .../org/dependencytrack/resources/v1/AccessControlResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 7952de5882..3cd27012dd 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -138,7 +138,7 @@ public Response retrieveUserProjects( List projects = dao.getUserUnassignedProjects(principal); if (projects == null || projects.isEmpty()) - return Response.status(Response.Status.NOT_FOUND).entity("No unassigned projects for specified user.").build(); + return Response.ok(List.of()).entity("No unassigned projects for specified user.").build(); return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); } From ef55e06874040256b1166d438d35269967504264 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:31:28 -0500 Subject: [PATCH 115/181] refactor: create view of project effective permissions for user (#15) * refactor: create view of project effective permissions for user Signed-off-by: Jonathan Howard * fix: add public qualifier to new methods Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard --- .../persistence/QueryManager.java | 18 ++-- .../persistence/RoleQueryManager.java | 89 ++++++++++++++++--- .../resources/migration/changelog-v5.6.0.xml | 25 ++++++ 3 files changed, 115 insertions(+), 17 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 83f5db70a5..9e474cad33 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -538,11 +538,13 @@ protected Set getRoleIds(final Principal principal, final Project project) * @return A {@link Set} of {@link Team} IDs */ protected Set getTeamIds(final Principal principal) { - return switch (principal) { - case UserPrincipal user -> Set.copyOf(user.getTeams().stream().map(Team::getId).toList()); - case ApiKey apiKey -> Set.copyOf(apiKey.getTeams().stream().map(Team::getId).toList()); - default -> Collections.emptySet(); + List teams = switch (principal) { + case UserPrincipal user -> user.getTeams(); + case ApiKey apiKey -> apiKey.getTeams(); + default -> Collections.emptyList(); }; + + return Set.copyOf(teams.stream().map(Team::getId).toList()); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1214,11 +1216,15 @@ public List getUnassignedRolePermissions(final Role role) { return getRoleQueryManager().getUnassignedRolePermissions(role); } - public List getUserRoles(UserPrincipal user) { + public List getUserRoles(final UserPrincipal user) { return getRoleQueryManager().getUserRoles(user); } - public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { + public List getUserProjectPermissions(final String username, final String projectName) { + return getRoleQueryManager().getUserProjectPermissions(username, projectName); + } + + public boolean removeRoleFromUser(final UserPrincipal user, final Role role, final Project project) { return getRoleQueryManager().removeRoleFromUser(user, role, project); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 9d09558591..6f75325319 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.jdo.PersistenceManager; import javax.jdo.Query; @@ -32,12 +33,29 @@ import org.dependencytrack.persistence.jdbi.RoleDao; import alpine.common.logging.Logger; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.UserPrincipal; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { + /** + * Represents a row returned by the USER_PROJECT_EFFECTIVE_PERMISSIONS view. + * + * @since 5.6.0 + */ + public record UserProjectEffectivePermissionsRow( + Long ldapUserId, + Long managedUserId, + Long oidcUserId, + Long projectId, + Long permissionId, + String permissionName) { + } + private static final Logger LOGGER = Logger.getLogger(RoleQueryManager.class); RoleQueryManager(final PersistenceManager pm) { @@ -50,7 +68,7 @@ final class RoleQueryManager extends QueryManager implements IQueryManager { @Override public Role createRole(final String name, final List permissions) { - Role role = new Role(); + final Role role = new Role(); role.setName(name); role.setPermissions(permissions); @@ -67,15 +85,15 @@ public List getRoles() { } @Override - public Role getRole(String uuid) { + public Role getRole(final String uuid) { final Query query = pm.newQuery(Role.class, "uuid == :uuid"); return query.executeUnique(); } @Override - public List getUserRoles(UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user)); + public List getUserRoles(final UserPrincipal user) { + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user.getUsername())); } public List getUnassignedProjects(final String username) { @@ -87,13 +105,13 @@ public List getUnassignedProjects(final UserPrincipal user) { } public List getUnassignedRolePermissions(final Role role) { - List permissions = new ArrayList<>(); + final List permissions = new ArrayList<>(); - var permissionNames = role.getPermissions().stream() + final var permissionNames = role.getPermissions().stream() .map(Permission::getName) .toList(); - Query query = pm.newQuery(Permission.class) + final Query query = pm.newQuery(Permission.class) .filter("!:permissionNames.contains(name)") .setNamedParameters(Map.of("permissionNames", permissionNames)); @@ -103,7 +121,7 @@ public List getUnassignedRolePermissions(final Role role) { } @Override - public Role updateRole(Role transientRole) { + public Role updateRole(final Role transientRole) { final Role role = getObjectByUuid(Role.class, transientRole.getUuid()); if (role == null) return null; @@ -114,16 +132,65 @@ public Role updateRole(Role transientRole) { } @Override - public boolean addRoleToUser(UserPrincipal user, Role role, Project project) { + public List getUserProjectPermissions(final String username, final String projectName) { + final UserPrincipal user = getUserPrincipal(username); + final String columnName; + + switch (user) { + case LdapUser ldapUser -> columnName = "LDAPUSER_ID"; + case ManagedUser managedUser -> columnName = "MANAGEDUSER_ID"; + case OidcUser oidcUser -> columnName = "OIDCUSER_ID"; + default -> { + return null; + } + }; + + final Query projectsQuery = pm.newQuery(Project.class) + .filter("name == :projectName") + .setNamedParameters(Map.of("projectName", projectName)); + + final String projectIds = executeAndCloseList(projectsQuery).stream() + .map(Project::getId) + .map(String::valueOf) + .collect(Collectors.joining(", ", "'{", "}'")); + + // language=SQL + final var queryString = """ + SELECT + upep."LDAPUSER_ID", + upep."MANAGEDUSER_ID", + upep."OIDCUSER_ID", + upep."PROJECT_ID", + upep."PERMISSION_ID", + upep."PERMISSION_NAME" + FROM "USER_PROJECT_EFFECTIVE_PERMISSIONS" upep + WHERE upep."%s" = :userId + AND upep."PROJECT_ID" = ANY(%s) + """.formatted(columnName, projectIds); + + final Query query = pm.newQuery(Query.SQL, queryString); + query.setNamedParameters(Map.of( + "userId", user.getId(), + "projectIds", projectIds)); + + return executeAndCloseResultList(query, UserProjectEffectivePermissionsRow.class) + .stream() + .map(UserProjectEffectivePermissionsRow::permissionName) + .map(this::getPermission) + .distinct() + .toList(); + } + + @Override + public boolean addRoleToUser(final UserPrincipal user, final Role role, final Project project) { return JdbiFactory.withJdbiHandle( handle -> handle.attach(RoleDao.class).addRoleToUser(user, project.getId(), role.getId())) == 1; } @Override - public boolean removeRoleFromUser(UserPrincipal user, Role role, Project project) { + public boolean removeRoleFromUser(final UserPrincipal user, final Role role, final Project project) { return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser(user, project, role.getId())) > 0; - } } diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0.xml index 8bf3d50943..d479c03206 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0.xml @@ -1403,5 +1403,30 @@ + + + SELECT + lpr."LDAPUSER_ID" AS "LDAPUSER_ID", + mpr."MANAGEDUSER_ID" AS "MANAGEDUSER_ID", + opr."OIDCUSER_ID" AS "OIDCUSER_ID", + pr."ID" AS "PROJECT_ID", + p."ID" AS "PERMISSION_ID", + p."NAME" AS "PERMISSION_NAME" + FROM "PERMISSION" p + INNER JOIN "ROLES_PERMISSIONS" rp + ON rp."PERMISSION_ID" = p."ID" + FULL OUTER JOIN "LDAPUSERS_PROJECTS_ROLES" lpr + ON lpr."ROLE_ID" = rp."ROLE_ID" + FULL OUTER JOIN "MANAGEDUSERS_PROJECTS_ROLES" mpr + ON lpr."PROJECT_ID" = mpr."PROJECT_ID" + AND lpr."ROLE_ID" = mpr."ROLE_ID" + FULL OUTER JOIN "OIDCUSERS_PROJECTS_ROLES" opr + ON mpr."PROJECT_ID" = opr."PROJECT_ID" + AND mpr."ROLE_ID" = opr."ROLE_ID" + INNER JOIN "PROJECT" pr + ON lpr."PROJECT_ID" = pr."ID" + OR mpr."PROJECT_ID" = pr."ID" + OR opr."PROJECT_ID" = pr."ID" +
        From 5647be0231620d2d9ba5b9d6d6442168423e301a Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 19 Mar 2025 18:10:36 -0600 Subject: [PATCH 116/181] code cleanup Signed-off-by: Allen Shearin --- .../java/org/dependencytrack/persistence/RoleQueryManager.java | 2 +- .../java/org/dependencytrack/resources/v1/UserResource.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 6f75325319..8fc72e4da0 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -93,7 +93,7 @@ public Role getRole(final String uuid) { @Override public List getUserRoles(final UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user.getUsername())); + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user)); } public List getUnassignedProjects(final String username) { diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index a2214f038f..03fe5dbd36 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -824,7 +824,6 @@ public Response addRoleToUser( if (!modified) return Response.notModified().entity("The user is already a member of the specified role.").build(); - principal = qm.getObjectById(principal.getClass(), principal.getId()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s" .formatted(principal.getName(), role.getName(), project.getName())); From b5e2149044642180e495e41a31d8c2f637b33bdd Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 20 Mar 2025 17:26:00 -0600 Subject: [PATCH 117/181] correct merge issue Signed-off-by: Allen Shearin --- .../dependencytrack/resources/v1/AccessControlResource.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index 3cd27012dd..abdaeb431a 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -54,6 +54,12 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.dependencytrack.persistence.jdbi.RoleDao; +import org.jdbi.v3.core.Handle; + +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; + +import java.util.List; import java.util.NoSuchElementException; /** From 2d1aed362be00d3849721e99c7e635d51e960347 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Mon, 24 Mar 2025 15:12:44 -0500 Subject: [PATCH 118/181] Role DAO fixes (#16) * fix: replace RoleDao method binds with interpolation Signed-off-by: Jonathan Howard * fix: QueryManager.getRole not passing uuid Signed-off-by: Jonathan Howard --------- Signed-off-by: Jonathan Howard --- .../dependencytrack/model/ProjectRole.java | 15 +- .../java/org/dependencytrack/model/Role.java | 32 ++--- .../persistence/DefaultObjectGenerator.java | 133 ++++++++++++------ .../persistence/QueryManager.java | 2 +- .../persistence/RoleQueryManager.java | 37 +++-- .../persistence/jdbi/RoleDao.java | 80 ++++++----- .../jdbi/mapping/ProjectRoleRowMapper.java | 6 +- .../resources/v1/AccessControlResource.java | 2 +- .../resources/v1/RoleResource.java | 2 +- 9 files changed, 180 insertions(+), 129 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java b/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java index e0ac7d206d..f5f25e8e61 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java @@ -82,10 +82,7 @@ public void setProject(Project project) { * @since 5.6.0 */ @PersistenceCapable(table = "LDAPUSERS_PROJECTS_ROLES") - @Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "LDAPUSERS_PROJECTS_ROLES", - members = { "ldapUsers", "project", "role" }, - deferred = "true") + @Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "ldapUsers", "project", "role" }) @FetchGroup(name = "ALL", members = { @Persistent(name = "role"), @Persistent(name = "project"), @@ -133,10 +130,7 @@ public void addLdapUsers(LdapUser... ldapUsers) { * @since 5.6.0 */ @PersistenceCapable(table = "MANAGEDUSERS_PROJECTS_ROLES") - @Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "MANAGEDUSERS_PROJECTS_ROLES", - members = { "managedUsers", "project", "role" }, - deferred = "true") + @Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "managedUsers", "project", "role" }) @FetchGroup(name = "ALL", members = { @Persistent(name = "role"), @Persistent(name = "project"), @@ -184,10 +178,7 @@ public void addManagedUsers(ManagedUser... managedUsers) { * @since 5.6.0 */ @PersistenceCapable(table = "OIDCUSERS_PROJECTS_ROLES") - @Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", - table = "OIDCUSERS_PROJECTS_ROLES", - members = { "oidcUsers", "project", "role" }, - deferred = "true") + @Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "oidcUsers", "project", "role" }) @FetchGroup(name = "ALL", members = { @Persistent(name = "role"), @Persistent(name = "project"), diff --git a/apiserver/src/main/java/org/dependencytrack/model/Role.java b/apiserver/src/main/java/org/dependencytrack/model/Role.java index 6adac75ba5..15ea00f4ef 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/Role.java +++ b/apiserver/src/main/java/org/dependencytrack/model/Role.java @@ -32,9 +32,10 @@ import jakarta.validation.constraints.Size; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; + +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; import java.util.UUID; import javax.jdo.annotations.Column; @@ -85,8 +86,7 @@ public enum FetchGroup { @NotBlank @Size(min = 1, max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) - @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, - message = "The name may only contain printable characters") + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The name may only contain printable characters") private String name; @Persistent(table = "ROLES_PERMISSIONS", defaultFetchGroup = "true") @@ -94,7 +94,7 @@ public enum FetchGroup { @Join(column = "ROLE_ID") @Element(column = "PERMISSION_ID") @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) - private List permissions; + private Set permissions = new LinkedHashSet<>(); @Persistent(customValueStrategy = "uuid") @Unique(name = "ROLE_UUID_IDX") @@ -118,24 +118,18 @@ public void setName(String name) { this.name = name; } - public List getPermissions() { + public Set getPermissions() { return permissions; } - public void setPermissions(List permissions) { + public void setPermissions(Set permissions) { this.permissions = permissions; } - public void addPermissions(Permission... permissions) { - if (this.permissions == null) { - this.permissions = new ArrayList<>(Arrays.asList(permissions)); - - return; - } + public boolean addPermissions(Permission... permissions) { + this.permissions = Objects.requireNonNullElse(this.permissions, new LinkedHashSet<>()); - for (var permission : permissions) - if (!this.permissions.contains(permission)) - this.permissions.add(permission); + return this.permissions.addAll(Set.of(permissions)); } public UUID getUuid() { @@ -152,10 +146,12 @@ public String toString() { .map(permission -> permission.getName()) .toList(); - return "%s{id=%d, name='%s', permissions=%s}".formatted( + return "%s{id=%d, uuid='%s', name='%s', permissions=%s}".formatted( getClass().getSimpleName(), id, + uuid, name, permissionStrings); } + } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index a8e603ab81..3b934d4c80 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -42,7 +42,7 @@ import java.time.Duration; import java.time.Instant; - +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -62,7 +62,56 @@ public class DefaultObjectGenerator implements ServletContextListener { private static final Logger LOGGER = Logger.getLogger(DefaultObjectGenerator.class); - private final Map permissionsMap = new HashMap<>(); + private static final Map PERMISSIONS_MAP = new HashMap<>(); + + private static final Map> DEFAULT_TEAM_PERMISSIONS = Map.of( + "Administrators", Stream.of(Permissions.values()) + .map(Permissions::name) + .toList(), + "Portfolio Managers", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.PORTFOLIO_MANAGEMENT, + Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE), + "Automation", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.BOM_UPLOAD), + "Badge Viewers", List.of( + Permissions.Constants.VIEW_BADGES)); + + private static final Map> DEFAULT_ROLE_PERMISSIONS = Map.of( + "Project Admin", List.of( + Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE, + Permissions.Constants.VULNERABILITY_ANALYSIS, + Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ, + Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE, + Permissions.Constants.POLICY_MANAGEMENT, + Permissions.Constants.POLICY_MANAGEMENT_CREATE, + Permissions.Constants.POLICY_MANAGEMENT_READ, + Permissions.Constants.POLICY_MANAGEMENT_UPDATE, + Permissions.Constants.POLICY_MANAGEMENT_DELETE), + "Project Auditor", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VIEW_POLICY_VIOLATION, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ), + "Project Editor", List.of( + Permissions.Constants.BOM_UPLOAD, + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ, + Permissions.Constants.PROJECT_CREATION_UPLOAD), + "Project Viewer", List.of( + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VIEW_BADGES)); /** * {@inheritDoc} @@ -124,6 +173,7 @@ private void executeLocked() { loadDefaultLicenses(qm); loadDefaultLicenseGroups(qm); loadDefaultRepositories(qm); + loadDefaultRoles(qm); loadDefaultConfigProperties(qm); loadDefaultNotificationPublishers(qm); recordDefaultObjectsVersion(qm); @@ -214,23 +264,16 @@ public void loadDefaultPermissions() { private void loadDefaultPermissions(final QueryManager qm) { LOGGER.info("Synchronizing permissions to datastore"); - for (final Permissions permission : Permissions.values()) { - if (qm.getPermission(permission.name()) == null) { - LOGGER.debug("Creating permission: " + permission.name()); - permissionsMap.put(permission.name(), - qm.createPermission(permission.name(), permission.getDescription())); - } - } - } - - private void createTeam(final QueryManager qm, final String name, final List permissions, final boolean createApiKey) { - LOGGER.debug("Creating team: " + name); - var team = qm.createTeam(name, createApiKey); - - LOGGER.debug("Assigning default permissions for team: " + name); - team.setPermissions(permissions); + List existing = Objects.requireNonNullElse(qm.getPermissions(), Collections.emptyList()) + .stream() + .map(Permission::getName) + .toList(); - qm.persist(team); + for (final Permissions value : Permissions.values()) + if (!existing.contains(value.name())) { + LOGGER.debug("Creating permission: " + value.name()); + PERMISSIONS_MAP.put(value.name(), qm.createPermission(value.name(), value.getDescription())); + } } @SuppressWarnings("unused") @@ -248,14 +291,20 @@ private void loadDefaultPersonas(final QueryManager qm) { 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); - createTeam(qm, "Administrators", List.copyOf(permissionsMap.values()), false); - createTeam(qm, "Portfolio Managers", getPortfolioManagersPermissions(), false); - createTeam(qm, "Automation", getAutomationPermissions(), true); - createTeam(qm, "Badge Viewers", getBadgesPermissions(), true); + for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { + 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")); @@ -265,35 +314,29 @@ private void loadDefaultPersonas(final QueryManager qm) { qm.persist(admin); } - private List getPortfolioManagersPermissions() { - return getPermissionsByName(Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.PORTFOLIO_MANAGEMENT, - Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, - Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE); - } - - private List getAutomationPermissions() { - return getPermissionsByName(Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.BOM_UPLOAD); - } - - private List getBadgesPermissions() { - return getPermissionsByName(Permissions.Constants.VIEW_BADGES); - } - /** * Perform a lookup of {@link Permission}s for specified name(s). * * @param names permission names * @return list of {@link Permission}s */ - private List getPermissionsByName(String... names) { - return Stream.of(names) - .map(permissionsMap::get) - .filter(Objects::nonNull) - .toList(); + private List getPermissionsByName(List names) { + return names.stream().map(PERMISSIONS_MAP::get).filter(Objects::nonNull).toList(); + } + + /** + * 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 : new String[] { "Project Admin", "Project Auditor", "Project Editor", "Project Viewer" }) { + LOGGER.debug("Creating role: " + name); + qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name))); + } } public void loadDefaultRepositories() { diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 9e474cad33..d69335b2bc 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -902,7 +902,7 @@ public List getRoles() { } public Role getRole(String uuid) { - return getRoleQueryManager().getRole(null); + return getRoleQueryManager().getRole(uuid); } public Role updateRole(Role transientRole) { diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 8fc72e4da0..4fda1b840a 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import javax.jdo.PersistenceManager; @@ -68,11 +69,13 @@ public record UserProjectEffectivePermissionsRow( @Override public Role createRole(final String name, final List permissions) { - final Role role = new Role(); - role.setName(name); - role.setPermissions(permissions); + return callInTransaction(() -> { + final Role role = new Role(); + role.setName(name); + role.setPermissions(Set.copyOf(permissions)); - return persist(role); + return persist(role); + }); } @Override @@ -86,14 +89,13 @@ public List getRoles() { @Override public Role getRole(final String uuid) { - final Query query = pm.newQuery(Role.class, "uuid == :uuid"); - - return query.executeUnique(); + return getObjectByUuid(Role.class, uuid, Role.FetchGroup.ALL.name()); } @Override public List getUserRoles(final UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserRoles(user)); + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class) + .getUserRoles(user.getClass(), user.getUsername())); } public List getUnassignedProjects(final String username) { @@ -101,7 +103,9 @@ public List getUnassignedProjects(final String username) { } public List getUnassignedProjects(final UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserUnassignedProjects(user)); + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserUnassignedProjects( + user.getClass(), + user.getUsername())); } public List getUnassignedRolePermissions(final Role role) { @@ -143,7 +147,7 @@ public List getUserProjectPermissions(final String username, final S default -> { return null; } - }; + } final Query projectsQuery = pm.newQuery(Project.class) .filter("name == :projectName") @@ -184,13 +188,20 @@ public List getUserProjectPermissions(final String username, final S @Override public boolean addRoleToUser(final UserPrincipal user, final Role role, final Project project) { return JdbiFactory.withJdbiHandle( - handle -> handle.attach(RoleDao.class).addRoleToUser(user, project.getId(), role.getId())) == 1; + handle -> handle.attach(RoleDao.class).addRoleToUser( + user.getClass(), + user.getId(), + project.getId(), + role.getId())) == 1; } @Override public boolean removeRoleFromUser(final UserPrincipal user, final Role role, final Project project) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser(user, - project, role.getId())) > 0; + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser( + user.getClass(), + user.getId(), + project.getName(), + role.getId())) > 0; } } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 3495a4b278..7990b5ebea 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -48,71 +48,79 @@ public interface RoleDao { @SqlUpdate(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = user.getClass().getSimpleName()?upper_case> + <#assign prefix = userClass.getSimpleName()?upper_case> INSERT INTO "${prefix}S_PROJECTS_ROLES" ("${prefix}_ID", "PROJECT_ID", "ROLE_ID") VALUES - (${user.getId()}, :projectId, :roleId) + (:userId, :projectId, :roleId) ON CONFLICT DO NOTHING """) @DefineNamedBindings - int addRoleToUser(@Define T user, @Bind long projectId, @Bind long roleId); + int addRoleToUser( + @Define Class userClass, + @Bind long userId, + @Bind long projectId, + @Bind long roleId); @SqlUpdate(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#-- @ftlvariable name="project" type="org.dependencytrack.model.Project" --> - <#assign prefix = user.getClass().getSimpleName()?upper_case> + <#assign prefix = userClass.getSimpleName()?upper_case> DELETE FROM "${prefix}S_PROJECTS_ROLES" - WHERE "${prefix}_ID" = ${user.getId()} + WHERE "${prefix}_ID" = :userId AND "ROLE_ID" = :roleId AND "PROJECT_ID" IN ( SELECT "ID" FROM "PROJECT" - WHERE "NAME" = '${project.getName()}' + WHERE "NAME" = :projectName ) """) @DefineNamedBindings - int removeRoleFromUser(@Define T user, @Define Project project, @Bind long roleId); - // (@Bind long userId, @Bind String projectName, @Bind long roleId) + int removeRoleFromUser( + @Define Class userClass, + @Bind long userId, + @Bind String projectName, + @Bind long roleId); @SqlQuery(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = user.getClass().getSimpleName()?upper_case> + <#assign prefix = userClass.getSimpleName()?upper_case> SELECT - "PROJECT"."ID" AS "PROJECT_ID", - "PROJECT"."NAME" AS "PROJECT_NAME", - "PROJECT"."UUID" AS "PROJECT_UUID", - "ROLE"."ID" AS "ROLE_ID", - "ROLE"."NAME" AS "ROLE_NAME", - "ROLE"."UUID" AS "ROLE_UUID", - "${prefix}"."ID" AS "${prefix}_ID" - FROM "PROJECT" - INNER JOIN "${prefix}S_PROJECTS_ROLES" - ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - INNER JOIN "${prefix}" - ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" - INNER JOIN "ROLE" - ON "ROLE"."ID" = "${prefix}S_PROJECTS_ROLES"."ROLE_ID" - WHERE "${prefix}"."USERNAME" = '${user.getUsername()}' + p."ID" AS "PROJECT_ID", + p."NAME" AS "PROJECT_NAME", + p."UUID" AS "PROJECT_UUID", + r."ID" AS "ROLE_ID", + r."NAME" AS "ROLE_NAME", + r."UUID" AS "ROLE_UUID", + u."ID" AS "${prefix}_ID" + FROM "PROJECT" p + INNER JOIN "${prefix}S_PROJECTS_ROLES" pr + ON pr."PROJECT_ID" = p."ID" + INNER JOIN "${prefix}" u + ON u."ID" = pr."${prefix}_ID" + INNER JOIN "ROLE" r + ON r."ID" = pr."ROLE_ID" + WHERE u."USERNAME" = :username """) @RegisterRowMapper(ProjectRoleRowMapper.class) @DefineNamedBindings - List getUserRoles(@Define T user); + List getUserRoles(@Define Class userClass, @Bind String username); @SqlQuery(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = user.getClass().getSimpleName()?upper_case> - SELECT "PROJECT"."ID", "PROJECT"."NAME", "PROJECT"."UUID" - FROM "PROJECT" - LEFT JOIN "${prefix}S_PROJECTS_ROLES" - ON "${prefix}S_PROJECTS_ROLES"."PROJECT_ID" = "PROJECT"."ID" - LEFT JOIN "${prefix}" - ON "${prefix}"."ID" = "${prefix}S_PROJECTS_ROLES"."${prefix}_ID" - WHERE "${prefix}"."USERNAME" != '${user.getUsername()}' - OR "${prefix}"."USERNAME" IS NULL + <#assign prefix = userClass.getSimpleName()?upper_case> + SELECT p."ID", p."NAME", p."UUID" + FROM "PROJECT" p + LEFT JOIN "${prefix}S_PROJECTS_ROLES" pr + ON pr."PROJECT_ID" = p."ID" + LEFT JOIN "${prefix}" u + ON u."ID" = pr."${prefix}_ID" + WHERE u."USERNAME" != :username + OR u."USERNAME" IS NULL """) @RegisterFieldMapper(Project.class) - List getUserUnassignedProjects(@Define T user); + List getUserUnassignedProjects( + @Define Class userClass, + @Bind String username); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java index 6c7b115152..013c5efade 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java @@ -36,8 +36,9 @@ import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; public class ProjectRoleRowMapper implements RowMapper { + public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) throws SQLException { - ProjectRole projectRole; + final ProjectRole projectRole; switch (resultSet) { case ResultSet rs when hasColumn(rs, "LDAPUSER_ID") -> projectRole = new LdapUserProjectRole(); @@ -65,4 +66,5 @@ public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) th return projectRole; } -} \ No newline at end of file + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index abdaeb431a..a244b1d699 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -141,7 +141,7 @@ public Response retrieveUserProjects( try (final Handle jdbiHandle = openJdbiHandle()) { var dao = jdbiHandle.attach(RoleDao.class); - List projects = dao.getUserUnassignedProjects(principal); + List projects = dao.getUserUnassignedProjects(principal.getClass(), principal.getUsername()); if (projects == null || projects.isEmpty()) return Response.ok(List.of()).entity("No unassigned projects for specified user.").build(); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index cc3e3385a6..157fada21d 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -146,7 +146,7 @@ public Response createRole(Role jsonRole) { failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); try (QueryManager qm = new QueryManager()) { - final Role role = qm.createRole(jsonRole.getName(), jsonRole.getPermissions()); + final Role role = qm.createRole(jsonRole.getName(), jsonRole.getPermissions().stream().toList()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role created: " + role.getName()); return Response.status(Response.Status.CREATED).entity(role).build(); From 8215b4c51de599d1811020cad586d039554d6bb4 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 24 Mar 2025 15:24:01 -0600 Subject: [PATCH 119/181] merge corrections Signed-off-by: Allen Shearin --- .../dependencytrack/persistence/DefaultObjectGenerator.java | 2 +- .../org/dependencytrack/resources/v1/PermissionResource.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 3b934d4c80..68f4c154c9 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -298,7 +298,7 @@ private void loadDefaultPersonas(final QueryManager qm) { for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { LOGGER.debug("Creating team: " + name); - var team = qm.createTeam(name); + var team = qm.createTeam(name, false); LOGGER.debug("Assigning default permissions for team: " + name); team.setPermissions(getPermissionsByName(DEFAULT_TEAM_PERMISSIONS.get(name))); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 3a714c09d3..ae206e2002 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -49,6 +49,7 @@ import org.owasp.security.logging.SecurityMarkers; import java.util.List; +import java.util.Set; /** * JAX-RS resources for processing permissions. @@ -254,7 +255,7 @@ public Response removePermissionFromRole( if (permission == null) { return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); } - final List permissions = role.getPermissions(); + final Set permissions = role.getPermissions(); if (permissions != null && permissions.contains(permission)) { permissions.remove(permission); role.setPermissions(permissions); @@ -298,7 +299,7 @@ public Response addPermissionToRole( if (permission == null) { return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); } - final List permissions = role.getPermissions(); + final Set permissions = role.getPermissions(); if (permissions != null && !permissions.contains(permission)) { permissions.add(permission); role.setPermissions(permissions); From 3ba18912bf453f900445d3b484d2d91f04d67e2a Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Tue, 25 Mar 2025 09:50:14 -0600 Subject: [PATCH 120/181] merge corrections Signed-off-by: Allen Shearin --- .../persistence/DefaultObjectGenerator.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 68f4c154c9..a8c6dfd636 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -21,6 +21,7 @@ import alpine.Config; import alpine.common.logging.Logger; import alpine.model.ConfigProperty; +import alpine.model.LdapUser; import alpine.model.ManagedUser; import alpine.model.Permission; import alpine.server.auth.PasswordService; @@ -32,6 +33,7 @@ import org.dependencytrack.common.ConfigKey; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; +import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter; @@ -294,11 +296,11 @@ private void loadDefaultPersonas(final QueryManager qm) { LOGGER.debug("Creating user: admin"); ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost", - new String(PasswordService.createHash("admin".toCharArray())), true, true, false); + new String(PasswordService.createHash("admin".toCharArray())), false, true, false); for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { LOGGER.debug("Creating team: " + name); - var team = qm.createTeam(name, false); + var team = qm.createTeam(name); LOGGER.debug("Assigning default permissions for team: " + name); team.setPermissions(getPermissionsByName(DEFAULT_TEAM_PERMISSIONS.get(name))); @@ -337,6 +339,45 @@ private void loadDefaultRoles(final QueryManager qm) { LOGGER.debug("Creating role: " + name); qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name))); } + + ManagedUser user = qm.createManagedUser("testuser", "Test User", "testuser@localhost", + new String(PasswordService.createHash("admin".toCharArray())), false, true, false); + + LdapUser ldapuser = qm.createLdapUser("Sadie"); + + var project = new Project(); + var projectAdmin = (qm.getRoles()).get(0); + project.setName("test-project"); + project.setDescription("Project for testing role schemas"); + project.setVersion("v0.1.0"); + + qm.persist(project); + + LOGGER.info("Adding role to user result #1: " + qm.addRoleToUser(user, projectAdmin, project)); + + project = new Project(); + project.setName("test-project"); + project.setDescription("Project for testing role schemas"); + project.setVersion("v0.1.1"); + + qm.persist(project); + + LOGGER.info("Adding role to user result #2: " + qm.addRoleToUser(user, projectAdmin, project)); + LOGGER.info("Has project access? " + qm.hasAccess(user, project)); + + LOGGER.info("testuser roles: " + qm.getUserRoles(user)); + LOGGER.info("admin roles: " + qm.getUserRoles(qm.getUserPrincipal("admin"))); + + LOGGER.info("Adding role to ldap user: " + qm.addRoleToUser(ldapuser, projectAdmin, project)); + + LOGGER.info("ldap user roles: " + qm.getUserRoles(ldapuser)); + LOGGER.info("admin roles: " + qm.getUserRoles(qm.getUserPrincipal("admin"))); + + LOGGER.info("testuser unassigned projects: " + qm.getUnassignedProjects(user)); + LOGGER.info("admin unassigned projects: " + qm.getUnassignedProjects("admin")); + + var unassigned = qm.getUnassignedRolePermissions(projectAdmin); + LOGGER.info("unassigned: " + unassigned.stream().map(Permission::getName).sorted().toList()); } public void loadDefaultRepositories() { From ffd4ffc22c56f52d2d3f0213b86bb80c71843747 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 27 Mar 2025 08:59:05 -0600 Subject: [PATCH 121/181] revert test changes Signed-off-by: Allen Shearin --- .../persistence/DefaultObjectGenerator.java | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index a8c6dfd636..b9264582ce 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -296,7 +296,7 @@ private void loadDefaultPersonas(final QueryManager qm) { LOGGER.debug("Creating user: admin"); ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost", - new String(PasswordService.createHash("admin".toCharArray())), false, true, false); + new String(PasswordService.createHash("admin".toCharArray())), true, true, false); for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { LOGGER.debug("Creating team: " + name); @@ -339,45 +339,6 @@ private void loadDefaultRoles(final QueryManager qm) { LOGGER.debug("Creating role: " + name); qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name))); } - - ManagedUser user = qm.createManagedUser("testuser", "Test User", "testuser@localhost", - new String(PasswordService.createHash("admin".toCharArray())), false, true, false); - - LdapUser ldapuser = qm.createLdapUser("Sadie"); - - var project = new Project(); - var projectAdmin = (qm.getRoles()).get(0); - project.setName("test-project"); - project.setDescription("Project for testing role schemas"); - project.setVersion("v0.1.0"); - - qm.persist(project); - - LOGGER.info("Adding role to user result #1: " + qm.addRoleToUser(user, projectAdmin, project)); - - project = new Project(); - project.setName("test-project"); - project.setDescription("Project for testing role schemas"); - project.setVersion("v0.1.1"); - - qm.persist(project); - - LOGGER.info("Adding role to user result #2: " + qm.addRoleToUser(user, projectAdmin, project)); - LOGGER.info("Has project access? " + qm.hasAccess(user, project)); - - LOGGER.info("testuser roles: " + qm.getUserRoles(user)); - LOGGER.info("admin roles: " + qm.getUserRoles(qm.getUserPrincipal("admin"))); - - LOGGER.info("Adding role to ldap user: " + qm.addRoleToUser(ldapuser, projectAdmin, project)); - - LOGGER.info("ldap user roles: " + qm.getUserRoles(ldapuser)); - LOGGER.info("admin roles: " + qm.getUserRoles(qm.getUserPrincipal("admin"))); - - LOGGER.info("testuser unassigned projects: " + qm.getUnassignedProjects(user)); - LOGGER.info("admin unassigned projects: " + qm.getUnassignedProjects("admin")); - - var unassigned = qm.getUnassignedRolePermissions(projectAdmin); - LOGGER.info("unassigned: " + unassigned.stream().map(Permission::getName).sorted().toList()); } public void loadDefaultRepositories() { From db673de65d39ad3f1a99149ad02bc377d1a73102 Mon Sep 17 00:00:00 2001 From: lmphil <126618132+lmphil@users.noreply.github.com> Date: Sat, 29 Mar 2025 18:35:06 -0400 Subject: [PATCH 122/181] feat: get user project permissions (wip) (#11) * roleQueryManagerTest Signed-off-by: Philippe * update role query manager test Signed-off-by: Philippe --------- Signed-off-by: Philippe --- .../persistence/RoleQueryManagerTest.java | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java diff --git a/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java b/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java new file mode 100644 index 0000000000..c59227b733 --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java @@ -0,0 +1,171 @@ +/* + * 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.model.Project; +import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; +import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; +import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; +import org.dependencytrack.model.Role; +import org.jdbi.v3.core.Jdbi; + +import alpine.Config; +import alpine.model.ManagedUser; +import alpine.model.Permission; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.common.ConfigKey; +import org.dependencytrack.event.kafka.KafkaProducerInitializer; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +public class RoleQueryManagerTest extends PersistenceCapableTest { + + private PostgreSQLContainer postgresContainer; + private Jdbi jdbi; + + @Before + public void setUp() { + System.setProperty("javax.jdo.PersistenceManagerFactoryClass", + "org.datanucleus.api.jdo.JDOPersistenceManagerFactory"); + + postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:11-alpine")); + postgresContainer.start(); + + jdbi = Jdbi.create( + postgresContainer.getJdbcUrl(), + postgresContainer.getUsername(), + postgresContainer.getPassword()); + } + + @After + public void tearDown() { + if (postgresContainer != null) { + postgresContainer.stop(); + } + } + + // @BeforeClass + // public static void beforeClass() { + // Config.enableUnitTests(); + // } + + // @AfterClass + // public static void afterClass() { + // KafkaProducerInitializer.tearDown(); + // } + + // @Rule + // public WireMockRule wireMockRule = new WireMockRule(); + + @Test + public void testGetUserProjectPermissions() throws ParseException { + 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); + + final var testProject = new Project(); + testProject.setId(1); + testProject.setName("test-project"); + testProject.setVersion("1.0.0"); + qm.persist(testProject); + + final var readPermission = new Permission(); + readPermission.setId(1); + readPermission.setName("read"); + readPermission.setDescription("permission to read"); + qm.persist(readPermission); + + final var writePermission = new Permission(); + writePermission.setId(2); + writePermission.setName("write"); + writePermission.setDescription("permission to write"); + qm.persist(writePermission); + + List expectedPermissionsList = Arrays.asList( + readPermission, + writePermission); + + Set expectedPermissions = new HashSet<>(expectedPermissionsList); + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername("test-user"); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + qm.persist(testUser); + + final var maintainerRole = new Role(); + maintainerRole.setId(1); + maintainerRole.setName("maintainer"); + maintainerRole.setPermissions(expectedPermissions); + qm.persist(maintainerRole); + + // final var ldapUserProjectRole = new LdapUserProjectRole(); + // ldapUserProjectRole.setProject(testProject); + + final var managedUserProjectRole = new ManagedUserProjectRole(); + // managedUserProjectRole.setId(1); + managedUserProjectRole.setProject(testProject); + managedUserProjectRole.setManagedUsers(Arrays.asList(testUser)); + managedUserProjectRole.setRole(maintainerRole); + // qm.persist(managedUserProjectRole); + + // final var oidcUserProjectRole = new OidcUserProjectRole(); + + List actualPermissions = qm.getUserProjectPermissions("test-user", "test-project"); + + Assert.assertEquals(actualPermissions, expectedPermissionsList); + } + +} From 7b11522f16699e2e118795e8089b91f3968d6b46 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Sat, 29 Mar 2025 16:35:27 -0600 Subject: [PATCH 123/181] Revert "feat: get user project permissions (wip) (#11)" (#17) This reverts commit 44dd2a22df446fcb7760c730b57483ad87e63fbd. --- .../persistence/RoleQueryManagerTest.java | 171 ------------------ 1 file changed, 171 deletions(-) delete mode 100644 src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java diff --git a/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java b/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java deleted file mode 100644 index c59227b733..0000000000 --- a/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java +++ /dev/null @@ -1,171 +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.model.Project; -import org.dependencytrack.model.ProjectRole; -import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; -import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; -import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; -import org.dependencytrack.model.Role; -import org.jdbi.v3.core.Jdbi; - -import alpine.Config; -import alpine.model.ManagedUser; -import alpine.model.Permission; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.common.ConfigKey; -import org.dependencytrack.event.kafka.KafkaProducerInitializer; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.utility.DockerImageName; - -import com.github.tomakehurst.wiremock.junit.WireMockRule; - -public class RoleQueryManagerTest extends PersistenceCapableTest { - - private PostgreSQLContainer postgresContainer; - private Jdbi jdbi; - - @Before - public void setUp() { - System.setProperty("javax.jdo.PersistenceManagerFactoryClass", - "org.datanucleus.api.jdo.JDOPersistenceManagerFactory"); - - postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:11-alpine")); - postgresContainer.start(); - - jdbi = Jdbi.create( - postgresContainer.getJdbcUrl(), - postgresContainer.getUsername(), - postgresContainer.getPassword()); - } - - @After - public void tearDown() { - if (postgresContainer != null) { - postgresContainer.stop(); - } - } - - // @BeforeClass - // public static void beforeClass() { - // Config.enableUnitTests(); - // } - - // @AfterClass - // public static void afterClass() { - // KafkaProducerInitializer.tearDown(); - // } - - // @Rule - // public WireMockRule wireMockRule = new WireMockRule(); - - @Test - public void testGetUserProjectPermissions() throws ParseException { - 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); - - final var testProject = new Project(); - testProject.setId(1); - testProject.setName("test-project"); - testProject.setVersion("1.0.0"); - qm.persist(testProject); - - final var readPermission = new Permission(); - readPermission.setId(1); - readPermission.setName("read"); - readPermission.setDescription("permission to read"); - qm.persist(readPermission); - - final var writePermission = new Permission(); - writePermission.setId(2); - writePermission.setName("write"); - writePermission.setDescription("permission to write"); - qm.persist(writePermission); - - List expectedPermissionsList = Arrays.asList( - readPermission, - writePermission); - - Set expectedPermissions = new HashSet<>(expectedPermissionsList); - - final var testUser = new ManagedUser(); - testUser.setFullname("test user created for testing"); - testUser.setId(1); - testUser.setUsername("test-user"); - DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); - testUser.setLastPasswordChange(dateFormatter.parse("20250324")); - testUser.setPassword("password"); - qm.persist(testUser); - - final var maintainerRole = new Role(); - maintainerRole.setId(1); - maintainerRole.setName("maintainer"); - maintainerRole.setPermissions(expectedPermissions); - qm.persist(maintainerRole); - - // final var ldapUserProjectRole = new LdapUserProjectRole(); - // ldapUserProjectRole.setProject(testProject); - - final var managedUserProjectRole = new ManagedUserProjectRole(); - // managedUserProjectRole.setId(1); - managedUserProjectRole.setProject(testProject); - managedUserProjectRole.setManagedUsers(Arrays.asList(testUser)); - managedUserProjectRole.setRole(maintainerRole); - // qm.persist(managedUserProjectRole); - - // final var oidcUserProjectRole = new OidcUserProjectRole(); - - List actualPermissions = qm.getUserProjectPermissions("test-user", "test-project"); - - Assert.assertEquals(actualPermissions, expectedPermissionsList); - } - -} From 8e64369e492555cd19f1f06875d844c81151faa7 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 31 Mar 2025 15:41:10 -0600 Subject: [PATCH 124/181] convert roleprojectrequest to record, remove logging, dev change Signed-off-by: Allen Shearin --- .../resources/v1/UserResource.java | 10 ++++---- .../resources/v1/vo/RoleProjectRequest.java | 23 +------------------ pom.xml | 2 +- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 03fe5dbd36..aba56fb1da 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -806,9 +806,7 @@ public Response addRoleToUser( @Parameter(description = "Role and project information", required = true) RoleProjectRequest roleProjectRequest) { try (QueryManager qm = new QueryManager()) { - LOGGER.info("received Project info: " + roleProjectRequest.getProjectUUID()); - LOGGER.info("received Role info: " + roleProjectRequest.getRoleUUID()); - final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.getRoleUUID()); + final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.roleUUID()); if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); @@ -816,7 +814,7 @@ public Response addRoleToUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - Project project = qm.getProject(roleProjectRequest.getProjectUUID()); + Project project = qm.getProject(roleProjectRequest.projectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -855,7 +853,7 @@ public Response removeRoleFromUser( @Parameter(description = "A valid username", required = true) @PathParam("username") String username, @Parameter(description = "Role and project information", required = true) RoleProjectRequest roleProjectRequest) { try (QueryManager qm = new QueryManager()) { - final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.getRoleUUID()); + final Role role = qm.getObjectByUuid(Role.class, roleProjectRequest.roleUUID()); if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); @@ -863,7 +861,7 @@ public Response removeRoleFromUser( if (principal == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - Project project = qm.getProject(roleProjectRequest.getProjectUUID()); + Project project = qm.getProject(roleProjectRequest.projectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java index 664410074e..28c64e517d 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RoleProjectRequest.java @@ -20,26 +20,5 @@ import org.dependencytrack.model.validation.ValidUuid; -public class RoleProjectRequest { - @ValidUuid - private String roleUUID; - - @ValidUuid - private String projectUUID; - - public String getRoleUUID() { - return roleUUID; - } - - public void setRoleUUID(String roleUUID) { - this.roleUUID = roleUUID; - } - - public String getProjectUUID() { - return projectUUID; - } - - public void setProjectUUID(String projectUUID) { - this.projectUUID = projectUUID; - } +public record RoleProjectRequest(@ValidUuid String roleUUID, @ValidUuid String projectUUID) { } diff --git a/pom.xml b/pom.xml index 320d852ed3..7eb073b65e 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ us.springett alpine-parent - 3.2.0-SNAPSHOT + 3.1.2 4.0.0 From a519fdf5074a403779e1cd766ef3cf697f22cba5 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Tue, 1 Apr 2025 16:34:06 -0600 Subject: [PATCH 125/181] temporary changelog fixes Signed-off-by: Allen Shearin --- .../resources/migration/changelog-main.xml | 1 + .../migration/changelog-v5.6.0-roles.xml | 217 ++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml diff --git a/apiserver/src/main/resources/migration/changelog-main.xml b/apiserver/src/main/resources/migration/changelog-main.xml index e55e42262c..be507a195d 100644 --- a/apiserver/src/main/resources/migration/changelog-main.xml +++ b/apiserver/src/main/resources/migration/changelog-main.xml @@ -13,5 +13,6 @@ + \ No newline at end of file diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml new file mode 100644 index 0000000000..2498c87b08 --- /dev/null +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT + lpr."LDAPUSER_ID" AS "LDAPUSER_ID", + mpr."MANAGEDUSER_ID" AS "MANAGEDUSER_ID", + opr."OIDCUSER_ID" AS "OIDCUSER_ID", + pr."ID" AS "PROJECT_ID", + p."ID" AS "PERMISSION_ID", + p."NAME" AS "PERMISSION_NAME" + FROM "PERMISSION" p + INNER JOIN "ROLES_PERMISSIONS" rp + ON rp."PERMISSION_ID" = p."ID" + FULL OUTER JOIN "LDAPUSERS_PROJECTS_ROLES" lpr + ON lpr."ROLE_ID" = rp."ROLE_ID" + FULL OUTER JOIN "MANAGEDUSERS_PROJECTS_ROLES" mpr + ON lpr."PROJECT_ID" = mpr."PROJECT_ID" + AND lpr."ROLE_ID" = mpr."ROLE_ID" + FULL OUTER JOIN "OIDCUSERS_PROJECTS_ROLES" opr + ON mpr."PROJECT_ID" = opr."PROJECT_ID" + AND mpr."ROLE_ID" = opr."ROLE_ID" + INNER JOIN "PROJECT" pr + ON lpr."PROJECT_ID" = pr."ID" + OR mpr."PROJECT_ID" = pr."ID" + OR opr."PROJECT_ID" = pr."ID" + + + From 7f7b4ce74d9df6a17d6b4da9c1c73145405cd9f1 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:28:50 -0500 Subject: [PATCH 126/181] refactor: add triggers to update effective permissions table with role changes (#18) Signed-off-by: Jonathan Howard --- .../migration/changelog-v5.6.0-roles.xml | 321 ++++++++++++++++-- 1 file changed, 287 insertions(+), 34 deletions(-) diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml index 2498c87b08..fa21c426c8 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -95,15 +95,18 @@ - + - + - + @@ -136,15 +139,18 @@ - + - + - + @@ -154,7 +160,7 @@ uniqueConstraintName="OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX" foreignKeyName="OIDCUSERS_PROJECTS_ROLES_OIDCUSER_FK" referencedTableName="OIDCUSER" referencedColumnNames="ID" - deferrable="true" initiallyDeferred="true" deleteCascade="false" + deferrable="true" initiallyDeferred="true" deleteCascade="false" validateUnique="true" validateForeignKey="true" /> @@ -177,41 +183,288 @@ - + - + - + - - SELECT - lpr."LDAPUSER_ID" AS "LDAPUSER_ID", - mpr."MANAGEDUSER_ID" AS "MANAGEDUSER_ID", - opr."OIDCUSER_ID" AS "OIDCUSER_ID", - pr."ID" AS "PROJECT_ID", - p."ID" AS "PERMISSION_ID", - p."NAME" AS "PERMISSION_NAME" - FROM "PERMISSION" p - INNER JOIN "ROLES_PERMISSIONS" rp - ON rp."PERMISSION_ID" = p."ID" - FULL OUTER JOIN "LDAPUSERS_PROJECTS_ROLES" lpr - ON lpr."ROLE_ID" = rp."ROLE_ID" - FULL OUTER JOIN "MANAGEDUSERS_PROJECTS_ROLES" mpr - ON lpr."PROJECT_ID" = mpr."PROJECT_ID" - AND lpr."ROLE_ID" = mpr."ROLE_ID" - FULL OUTER JOIN "OIDCUSERS_PROJECTS_ROLES" opr - ON mpr."PROJECT_ID" = opr."PROJECT_ID" - AND mpr."ROLE_ID" = opr."ROLE_ID" - INNER JOIN "PROJECT" pr - ON lpr."PROJECT_ID" = pr."ID" - OR mpr."PROJECT_ID" = pr."ID" - OR opr."PROJECT_ID" = pr."ID" - + + -- Helper function to recalculate all user permissions for a project. + -- Called by trigger functions to update the values in the USER_PROJECT_EFFECTIVE_PERMISSIONS table. + CREATE OR REPLACE FUNCTION recalc_user_project_role_effective_permissions(project_ids BIGINT[]) + RETURNS void AS $$ + DECLARE + tbl_prefix TEXT; + BEGIN + -- Remove any existing effective permissions for this project + DELETE FROM "USER_PROJECT_EFFECTIVE_PERMISSIONS" + WHERE "PROJECT_ID" = ANY(project_ids); + + -- Rebuild effective permissions for all user types + FOREACH tbl_prefix IN ARRAY ARRAY['LDAP', 'MANAGED', 'OIDC'] + LOOP + EXECUTE format($query$ + INSERT INTO "USER_PROJECT_EFFECTIVE_PERMISSIONS" + (%I, "PROJECT_ID", "PERMISSION_ID", "PERMISSION_NAME") + SELECT DISTINCT upr.%I, upr."PROJECT_ID", rp."PERMISSION_ID", p."NAME" + FROM %I upr + INNER JOIN "ROLES_PERMISSIONS" rp + ON rp."ROLE_ID" = upr."ROLE_ID" + INNER JOIN "PERMISSION" p + ON p."ID" = rp."PERMISSION_ID" + WHERE upr."PROJECT_ID" = ANY($1); + $query$, + tbl_prefix || 'USER_ID', + tbl_prefix || 'USER_ID', + tbl_prefix || 'USERS_PROJECTS_ROLES') + USING project_ids; + END LOOP; + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_delete() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG(DISTINCT "ROLE_ID") + INTO role_ids + FROM old_table; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT lpr."PROJECT_ID" + FROM "LDAPUSERS_PROJECTS_ROLES" lpr + INNER JOIN old_table + ON old_table."ROLE_ID" = lpr."ROLE_ID" + UNION + SELECT mpr."PROJECT_ID" + FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr + INNER JOIN old_table + ON old_table."ROLE_ID" = mpr."ROLE_ID" + UNION + SELECT opr."PROJECT_ID" + FROM "OIDCUSERS_PROJECTS_ROLES" opr + INNER JOIN old_table + ON old_table."ROLE_ID" = opr."ROLE_ID" + ) sub; + ELSE + EXECUTE format($query$ + SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") + FROM %I AS t + WHERE t."ROLE_ID" = ANY($1) + $query$, TG_TABLE_NAME) + USING role_ids + INTO project_ids; + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_insert() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG(DISTINCT "ROLE_ID") + INTO role_ids + FROM new_table; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT lpr."PROJECT_ID" + FROM "LDAPUSERS_PROJECTS_ROLES" lpr + INNER JOIN new_table + ON new_table."ROLE_ID" = lpr."ROLE_ID" + UNION + SELECT mpr."PROJECT_ID" + FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr + INNER JOIN new_table + ON new_table."ROLE_ID" = mpr."ROLE_ID" + UNION + SELECT opr."PROJECT_ID" + FROM "OIDCUSERS_PROJECTS_ROLES" opr + INNER JOIN new_table + ON new_table."ROLE_ID" = opr."ROLE_ID" + ) sub; + ELSE + EXECUTE format($query$ + SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") + FROM %I AS t + WHERE t."ROLE_ID" = ANY($1) + $query$, TG_TABLE_NAME) + USING role_ids + INTO project_ids; + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_update() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG("ROLE_ID") + INTO role_ids + FROM ( + SELECT "ROLE_ID" FROM old_table + UNION + SELECT "ROLE_ID" FROM new_table + ) roles; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT lpr."PROJECT_ID" + FROM "LDAPUSERS_PROJECTS_ROLES" lpr + INNER JOIN new_table + ON new_table."ROLE_ID" = lpr."ROLE_ID" + FULL OUTER JOIN old_table + ON new_table."ROLE_ID" = old_table."ROLE_ID" + UNION + SELECT mpr."PROJECT_ID" + FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr + INNER JOIN new_table + ON new_table."ROLE_ID" = mpr."ROLE_ID" + FULL OUTER JOIN old_table + ON new_table."ROLE_ID" = old_table."ROLE_ID" + UNION + SELECT opr."PROJECT_ID" + FROM "OIDCUSERS_PROJECTS_ROLES" opr + INNER JOIN new_table + ON new_table."ROLE_ID" = opr."ROLE_ID" + FULL OUTER JOIN old_table + ON new_table."ROLE_ID" = old_table."ROLE_ID" + ) sub; + ELSE + EXECUTE format($query$ + SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") + FROM %I AS t + WHERE t."ROLE_ID" = ANY($1) + $query$, TG_TABLE_NAME) + USING role_ids + INTO project_ids; + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + -- INSERT trigger for LDAPUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_insert + AFTER INSERT ON "LDAPUSERS_PROJECTS_ROLES" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for LDAPUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_delete + AFTER DELETE ON "LDAPUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for LDAPUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_update + AFTER UPDATE ON "LDAPUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_update(); + + -- INSERT trigger for MANAGEDUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_insert + AFTER INSERT ON "MANAGEDUSERS_PROJECTS_ROLES" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for MANAGEDUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_delete + AFTER DELETE ON "MANAGEDUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for MANAGEDUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_update + AFTER UPDATE ON "MANAGEDUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_update(); + + -- INSERT trigger for OIDCUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_insert + AFTER INSERT ON "OIDCUSERS_PROJECTS_ROLES" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for OIDCUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_delete + AFTER DELETE ON "OIDCUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for OIDCUSERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_update + AFTER UPDATE ON "OIDCUSERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_update(); + + -- INSERT trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_insert + AFTER INSERT ON "ROLES_PERMISSIONS" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_delete + AFTER DELETE ON "ROLES_PERMISSIONS" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_update + AFTER UPDATE ON "ROLES_PERMISSIONS" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION effective_permissions_mx_on_update(); +
        From 56b0ff4aed72c57ba824219bb0bbfbd31b772a9d Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 3 Apr 2025 10:13:45 -0600 Subject: [PATCH 127/181] refactor: add no content response to retrieveUserProjects Signed-off-by: Allen Shearin --- .../resources/v1/AccessControlResource.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index a244b1d699..b95002fc38 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -128,24 +128,29 @@ public Response retrieveProjects(@Parameter(description = "The UUID of the team headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse(responseCode = "204", description = "No unassigned projects for specified user."), @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "404", description = "No unassigned projects for specified user."), + @ApiResponse(responseCode = "404", description = "User not found"), }) @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) public Response retrieveUserProjects( - @Parameter(description = "The username to retrieve projects for", - required = true) @PathParam("username") String username) { + @Parameter(description = "The username to retrieve projects for", required = true) @PathParam("username") String username) { try (QueryManager qm = new QueryManager()) { UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + try (final Handle jdbiHandle = openJdbiHandle()) { var dao = jdbiHandle.attach(RoleDao.class); List projects = dao.getUserUnassignedProjects(principal.getClass(), principal.getUsername()); - if (projects == null || projects.isEmpty()) - return Response.ok(List.of()).entity("No unassigned projects for specified user.").build(); - + if (projects == null || projects.isEmpty()) { + return Response.noContent().build(); + } + return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); } } From e2117c3f57751848ce130385fd3b6849cbd1b6e0 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 14 Apr 2025 16:49:58 -0500 Subject: [PATCH 128/181] refactor: synchronize project roles instead of teams Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitLabClient.java | 15 +++- .../integrations/gitlab/GitLabRole.java | 90 +++++++++++++++++-- .../integrations/gitlab/GitLabSyncer.java | 60 ++----------- .../integrations/gitlab/GitlabProject.java | 14 +-- .../java/org/dependencytrack/model/Role.java | 14 +-- .../persistence/QueryManager.java | 4 + .../persistence/RoleQueryManager.java | 12 +++ .../dependencytrack/tasks/GitLabSyncTask.java | 13 ++- .../resources/graphql/gitlab-projects.graphql | 44 ++++++--- .../integrations/gitlab/GitLabClientTest.java | 2 +- .../integrations/gitlab/GitLabSyncerTest.java | 37 ++++---- 11 files changed, 194 insertions(+), 111 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index f9952e1301..a2ca80182a 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -56,6 +56,7 @@ public class GitLabClient { private final String accessToken; private final URI baseURL; private final Config config; + private final List topics; private final Map> rolePermissions = Map.of( GitLabRole.GUEST, List.of( @@ -110,13 +111,18 @@ public class GitLabClient { Permissions.TAG_MANAGEMENT_DELETE)); public GitLabClient(final String accessToken) { - this(accessToken, Config.getInstance()); + this(accessToken, Config.getInstance(), null); } - public GitLabClient(final String accessToken, final Config config) { + public GitLabClient(final String accessToken, final List topics) { + this(accessToken, Config.getInstance(), topics); + } + + public GitLabClient(final String accessToken, final Config config, final List topics) { this.config = config; this.accessToken = accessToken; this.baseURL = URI.create(config.getProperty(Config.AlpineKey.OIDC_ISSUER)); + this.topics = topics; } public List getGitLabProjects() throws IOException, URISyntaxException { @@ -125,6 +131,11 @@ public List getGitLabProjects() throws IOException, URISyntaxExce JSONObject variables = new JSONObject(); JSONObject queryObject = new JSONObject(); + if (topics != null && !topics.isEmpty()) { + variables.put("includeTopics", true); + variables.put("topics", topics); + } + queryObject.put("query", resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8)); URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT); diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java index c63f53d629..f632869946 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java @@ -18,14 +18,92 @@ */ package org.dependencytrack.integrations.gitlab; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Stream; + +import org.dependencytrack.auth.Permissions; + /** * Definitions of access levels/roles as defined by GitLab. */ public enum GitLabRole { - GUEST, // Applies to private and internal projects only - PLANNER, - REPORTER, - DEVELOPER, - MAINTAINER, - OWNER + + GUEST(10, "GitLab Project Guest", Set.of( // Applies to private and internal projects only + Permissions.Constants.VIEW_PORTFOLIO, + Permissions.Constants.VIEW_VULNERABILITY, + Permissions.Constants.VIEW_BADGES)), + PLANNER(15, "GitLab Project Planner", Set.of( + Permissions.Constants.VIEW_POLICY_VIOLATION)), + REPORTER(20, "GitLab Project Reporter", Set.of( + Permissions.Constants.VIEW_POLICY_VIOLATION)), + DEVELOPER(30, "GitLab Project Developer", Set.of( + Permissions.Constants.BOM_UPLOAD, + Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ, + Permissions.Constants.PROJECT_CREATION_UPLOAD)), + MAINTAINER(40, "GitLab Project Maintainer", Set.of( + Permissions.Constants.PORTFOLIO_MANAGEMENT, + Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, + Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE, + Permissions.Constants.VULNERABILITY_ANALYSIS, + Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE, + Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE, + Permissions.Constants.POLICY_MANAGEMENT, + Permissions.Constants.POLICY_MANAGEMENT_CREATE, + Permissions.Constants.POLICY_MANAGEMENT_READ, + Permissions.Constants.POLICY_MANAGEMENT_UPDATE, + Permissions.Constants.POLICY_MANAGEMENT_DELETE)), + OWNER(50, "GitLab Project Owner", Set.of( + Permissions.Constants.ACCESS_MANAGEMENT, + Permissions.Constants.ACCESS_MANAGEMENT_CREATE, + Permissions.Constants.ACCESS_MANAGEMENT_READ, + Permissions.Constants.ACCESS_MANAGEMENT_UPDATE, + Permissions.Constants.ACCESS_MANAGEMENT_DELETE, + Permissions.Constants.SYSTEM_CONFIGURATION, + Permissions.Constants.SYSTEM_CONFIGURATION_CREATE, + Permissions.Constants.SYSTEM_CONFIGURATION_READ, + Permissions.Constants.SYSTEM_CONFIGURATION_UPDATE, + Permissions.Constants.SYSTEM_CONFIGURATION_DELETE, + Permissions.Constants.TAG_MANAGEMENT, + Permissions.Constants.TAG_MANAGEMENT_DELETE)); + + private final int accessLevel; + private final String description; + private final Set permissions; + + GitLabRole(final int accessLevel, final String description, final Set permissions) { + this.accessLevel = accessLevel; + this.description = description; + this.permissions = permissions; + } + + public int getAccessLevel() { + return accessLevel; + } + + public String getDescription() { + return description; + } + + /** + * Get a set of permissions consisting of this role's permissions + * combined with permissions from the roles with lesser access levels. + * + * @return A sorted set of permissions for this role. + */ + public Set getPermissions() { + var permissions = Set.copyOf(this.permissions); + + permissions.addAll(Stream.of(GitLabRole.values()) + .filter(value -> value.getAccessLevel() <= getAccessLevel()) + .map(GitLabRole::getPermissions) + .flatMap(Collection::stream) + .toList()); + + return new LinkedHashSet<>(permissions.stream().distinct().sorted().toList()); + } + } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 51cb87999f..839b4324f9 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -24,20 +24,24 @@ import java.net.URISyntaxException; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.concurrent.locks.Lock; +import java.util.function.Function; +import java.util.stream.Collectors; import com.google.common.util.concurrent.Striped; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PermissionsSyncer; import org.dependencytrack.model.Project; +import org.dependencytrack.model.Role; import alpine.common.logging.Logger; import alpine.model.ConfigProperty; import alpine.model.OidcUser; -import alpine.model.Team; public class GitLabSyncer extends AbstractIntegrationPoint implements PermissionsSyncer { @@ -79,18 +83,6 @@ public void synchronize() { try { List gitLabProjects = gitLabClient.getGitLabProjects(); List projects = createProjects(gitLabProjects); - List teams = projects.stream() - .flatMap(project -> createProjectTeams(project).stream()) - .toList(); - List teamNames = gitLabProjects.stream() - .map(gitLabProject -> "%s_%s".formatted( - gitLabProject.getFullPath(), - gitLabProject.getMaxAccessLevel().getStringValue().toString())) - .toList(); - - qm.addUserToTeams(qm.getOidcUser(user.getUsername()), teamNames); - - teams = teams.stream().map(team -> qm.updateTeam(team)).toList(); projects = projects.stream().map(project -> qm.updateProject(project, false)).toList(); } catch (IOException | URISyntaxException ex) { LOGGER.error("An error occurred while querying GitLab GraphQL API", ex); @@ -101,6 +93,9 @@ public void synchronize() { private List createProjects(List gitLabProjects) { List projects = new ArrayList<>(); + Map roleMap = Arrays.stream(GitLabRole.values()) + .collect(Collectors.toMap(Function.identity(), role -> qm.getRoleByName(role.getDescription()))); + for (var gitLabProject : gitLabProjects) { final Lock lock = locks.get(gitLabProject.getFullPath()); lock.lock(); @@ -120,6 +115,7 @@ private List createProjects(List gitLabProjects) { if (!project.isActive() && project.getInactiveSince() == null) project.setInactiveSince(new Date()); + qm.addRoleToUser(user, roleMap.get(gitLabProject.getMaxAccessLevel().stringValue()), project); projects.add(qm.updateProject(project, false)); } finally { lock.unlock(); @@ -129,42 +125,4 @@ private List createProjects(List gitLabProjects) { return projects; } - /** - * Create teams for a Dependency-Track project representing a project within - * GitLab. - * - * @param project Dependency-Track project representing a GitLab project - * @return the Dependency-Track teams for the project - */ - private List createProjectTeams(Project project) { - List teams = new ArrayList<>(); - - for (var role : GitLabRole.values()) { - final String teamName = "%s_%s".formatted(project.getName(), role.name()); - final Lock lock = locks.get(project.getName()); - lock.lock(); - - try { - Team team = qm.getTeam(teamName); - team = team != null ? team : qm.createTeam(teamName); - - var permissions = gitLabClient.getRolePermissions(role).stream() - .map(rolePermission -> qm.getPermission(rolePermission.name())) - .filter(permission -> permission != null) - .distinct() - .toList(); - - team.setPermissions(permissions); - project.addAccessTeam(team); - qm.updateProject(project, false); - - teams.add(team); - } finally { - lock.unlock(); - } - } - - return teams; - } - } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java index 71458a9886..b7f8933912 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitlabProject.java @@ -28,17 +28,7 @@ */ class GitLabProject { - class MaxAccessLevel { - - private final GitLabRole stringValue; - - MaxAccessLevel(final GitLabRole role) { - this.stringValue = role; - } - - GitLabRole getStringValue() { - return stringValue; - } + record MaxAccessLevel(GitLabRole stringValue) { } private final String fullPath; @@ -72,7 +62,7 @@ public String toString() { return "%s{fullPath=%s, maxAccessLevel=%s}".formatted( getClass().getSimpleName(), fullPath, - maxAccessLevel.getStringValue().toString()); + maxAccessLevel.stringValue().toString()); } } diff --git a/apiserver/src/main/java/org/dependencytrack/model/Role.java b/apiserver/src/main/java/org/dependencytrack/model/Role.java index 15ea00f4ef..9c8c4fe93e 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/Role.java +++ b/apiserver/src/main/java/org/dependencytrack/model/Role.java @@ -102,6 +102,14 @@ public enum FetchGroup { @NotNull private UUID uuid; + public Role() { + } + + public Role(String name, Set permissions) { + this.name = name; + this.permissions = permissions; + } + public long getId() { return id; } @@ -142,16 +150,12 @@ public void setUuid(UUID uuid) { @Override public String toString() { - var permissionStrings = permissions.stream() - .map(permission -> permission.getName()) - .toList(); - return "%s{id=%d, uuid='%s', name='%s', permissions=%s}".formatted( getClass().getSimpleName(), id, uuid, name, - permissionStrings); + permissions.stream().map(Permission::getName).toList()); } } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index d69335b2bc..555b6c3484 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -905,6 +905,10 @@ public Role getRole(String uuid) { return getRoleQueryManager().getRole(uuid); } + public Role getRoleByName(String name) { + return getRoleQueryManager().getRoleByName(name); + } + public Role updateRole(Role transientRole) { return getRoleQueryManager().updateRole(transientRole); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 4fda1b840a..3c3479cac4 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -27,6 +27,7 @@ import javax.jdo.PersistenceManager; import javax.jdo.Query; +import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.Project; import org.dependencytrack.model.Role; import org.dependencytrack.model.ProjectRole; @@ -92,6 +93,17 @@ public Role getRole(final String uuid) { return getObjectByUuid(Role.class, uuid, Role.FetchGroup.ALL.name()); } + @Override + public Role getRoleByName(final String name) { + final String role = StringUtils.lowerCase(StringUtils.trimToNull(name)); + final Query query = pm.newQuery(Role.class) + .filter("name == :name") + .setNamedParameters(Map.of("name", role)) + .range(0, 1); + + return executeAndCloseUnique(query); + } + @Override public List getUserRoles(final UserPrincipal user) { return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class) diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index dd765268aa..bce16f6306 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -23,13 +23,20 @@ import alpine.event.framework.LoggableSubscriber; import alpine.model.ConfigProperty; import alpine.model.OidcUser; +import alpine.model.Permission; +import net.minidev.json.JSONArray; +import net.minidev.json.JSONValue; +import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.GitLabSyncEvent; import org.dependencytrack.integrations.gitlab.GitLabClient; import org.dependencytrack.integrations.gitlab.GitLabSyncer; import org.dependencytrack.persistence.QueryManager; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_TOPICS; + +import java.util.List; public class GitLabSyncTask implements LoggableSubscriber { @@ -72,7 +79,11 @@ public void inform(final Event event) { LOGGER.info("Starting GitLab sync task"); try (QueryManager qm = new QueryManager()) { - GitLabClient gitLabClient = new GitLabClient(accessToken); + String topicsProperty = qm.getConfigProperty( + GITLAB_TOPICS.getGroupName(), GITLAB_TOPICS.getPropertyName()).getPropertyValue(); + List topics = List.of(JSONValue.parse(topicsProperty, JSONArray.class).toArray(String[]::new)); + + GitLabClient gitLabClient = new GitLabClient(accessToken, topics); GitLabSyncer syncer = new GitLabSyncer(user, gitLabClient); syncer.setQueryManager(qm); syncer.synchronize(); diff --git a/apiserver/src/main/resources/graphql/gitlab-projects.graphql b/apiserver/src/main/resources/graphql/gitlab-projects.graphql index 384ecadf1a..e308d92ae1 100644 --- a/apiserver/src/main/resources/graphql/gitlab-projects.graphql +++ b/apiserver/src/main/resources/graphql/gitlab-projects.graphql @@ -1,24 +1,44 @@ +fragment projectFields on ProjectConnection { + nodes { + group { + fullPath + maxAccessLevel { + humanAccess + } + } + fullPath + maxAccessLevel { + humanAccess + } + } + pageInfo { + endCursor + hasNextPage + } +} + query ( $cursor: String $archived: ProjectArchived = EXCLUDE $topics: [String!] = [] + $includeTopics: Boolean! = false ) { - projects( + withTopic: projects( archived: $archived topics: $topics membership: true first: 100 after: $cursor - ) { - nodes { - fullPath - maxAccessLevel { - stringValue - } - } - pageInfo { - endCursor - hasNextPage - } + ) @include(if: $includeTopics) { + ...projectFields + } + + projects( + archived: $archived + membership: true + first: 100 + after: $cursor + ) @skip(if: $includeTopics) { + ...projectFields } } \ No newline at end of file diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index fc76f584d8..886b821557 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -86,7 +86,7 @@ public void testGetGitLabProjects() throws URISyntaxException, IOException { when(configMock.getProperty(eq(Config.AlpineKey.OIDC_ISSUER))).thenReturn(wireMockRule.baseUrl()); - GitLabClient gitLabClient = new GitLabClient(accessToken, configMock); + GitLabClient gitLabClient = new GitLabClient(accessToken, configMock, null); List gitLabProjects = gitLabClient.getGitLabProjects(); diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java index a8f4d275f9..a0fae1842a 100644 --- a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java +++ b/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java @@ -36,13 +36,11 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.mock; -import java.util.Arrays; import java.util.ArrayList; import java.util.List; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; - /** * This test suite validates the integration with the GitLab API. */ @@ -79,8 +77,7 @@ public void testIsEnabled() { GITLAB_ENABLED.getPropertyName(), "true", IConfigProperty.PropertyType.BOOLEAN, - null - ); + null); GitLabSyncer extension = new GitLabSyncer(user, gitLabClient); extension.setQueryManager(qm); Assert.assertTrue(extension.isEnabled()); @@ -97,8 +94,7 @@ public void testIsDisabled() { GITLAB_ENABLED.getPropertyName(), "false", IConfigProperty.PropertyType.BOOLEAN, - null - ); + null); GitLabSyncer extension = new GitLabSyncer(user, gitLabClient); extension.setQueryManager(qm); Assert.assertFalse(extension.isEnabled()); @@ -115,23 +111,22 @@ public void testSynchronizeSuccess() { GITLAB_ENABLED.getPropertyName(), "true", IConfigProperty.PropertyType.BOOLEAN, - null - ); - qm.createOidcUser( - "test_user" - ); - OidcUser testUser = new OidcUser(); - testUser.setUsername("test_user"); + null); + GitLabClient mockClient = mock(GitLabClient.class); - GitLabSyncer extension = new GitLabSyncer(testUser, mockClient); + GitLabSyncer extension = new GitLabSyncer(qm.createOidcUser("test_user"), mockClient); extension.setQueryManager(qm); - try{ - when(mockClient.getGitLabProjects()).thenReturn(Arrays.asList(new GitLabProject("project1", "this/test/project1", GitLabRole.MAINTAINER), - new GitLabProject("project2", "that/test/project2", GitLabRole.REPORTER))); - extension.synchronize(); - }catch (IOException | URISyntaxException ex) { + + try { + when(mockClient.getGitLabProjects()) + .thenReturn(List.of( + new GitLabProject("this/test/project1", GitLabRole.MAINTAINER), + new GitLabProject("that/test/project2", GitLabRole.REPORTER))); + extension.synchronize(); + } catch (IOException | URISyntaxException ex) { Assert.fail("Exception " + ex); } + Project testProject1 = qm.getProject("this/test/project1", null); Assert.assertFalse(testProject1.isActive()); @@ -148,11 +143,11 @@ public void testSynchronizeSuccess() { testTeams.add(team2); List t2perm = team2.getPermissions(); Assert.assertEquals(t2perm.size(), mockClient.getRolePermissions(GitLabRole.REPORTER).size()); - + for (Team team : testTeams) { List testTeamUsers = team.getOidcUsers(); Assert.assertEquals(testTeamUsers.size(), 1); Assert.assertEquals(testTeamUsers.get(0).getUsername(), "test_user"); } } -} \ No newline at end of file +} From 6a72a4da7353cc86e23034633e5ed5c040e42844 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Wed, 16 Apr 2025 14:13:00 -0500 Subject: [PATCH 129/181] chore: rebase cleanup Signed-off-by: Jonathan Howard --- .../resources/v1/AnalysisResource.java | 18 +- .../resources/v1/FindingResource.java | 9 - .../resources/migration/changelog-v5.6.0.xml | 31 --- .../integrations/gitlab/GitLabSyncerTest.java | 0 .../resources/v1/AnalysisResourceTest.java | 12 +- .../resources/v1/BomResourceTest.java | 10 +- .../resources/v1/ProjectResourceTest.java | 16 +- .../resources/v1/VexResourceTest.java | 6 +- .../v1/VulnerabilityResourceTest.java | 12 +- pom.xml | 77 ------ .../method/ProjectIsAccessibleByMethod.java | 109 -------- .../ProjectIsAccessibleByMethodTest.java | 239 ------------------ 12 files changed, 37 insertions(+), 502 deletions(-) rename {src => apiserver/src}/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java (100%) delete mode 100644 src/main/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethod.java delete mode 100644 src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java index 43f93fc6e3..389417f095 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java @@ -35,6 +35,15 @@ 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.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaEventDispatcher; @@ -54,15 +63,6 @@ import org.dependencytrack.resources.v1.vo.AnalysisRequest; import org.dependencytrack.util.AnalysisCommentFormatter.AnalysisCommentField; -import jakarta.validation.Validator; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.UUID; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java index 37148f4688..8a06e674a4 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/FindingResource.java @@ -67,15 +67,6 @@ import org.dependencytrack.resources.v1.vo.BomUploadResponse; import org.dependencytrack.util.PurlUtil; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.HeaderParam; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0.xml index d479c03206..4622e133b9 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0.xml @@ -717,12 +717,6 @@ - - - - - - - SELECT - lpr."LDAPUSER_ID" AS "LDAPUSER_ID", - mpr."MANAGEDUSER_ID" AS "MANAGEDUSER_ID", - opr."OIDCUSER_ID" AS "OIDCUSER_ID", - pr."ID" AS "PROJECT_ID", - p."ID" AS "PERMISSION_ID", - p."NAME" AS "PERMISSION_NAME" - FROM "PERMISSION" p - INNER JOIN "ROLES_PERMISSIONS" rp - ON rp."PERMISSION_ID" = p."ID" - FULL OUTER JOIN "LDAPUSERS_PROJECTS_ROLES" lpr - ON lpr."ROLE_ID" = rp."ROLE_ID" - FULL OUTER JOIN "MANAGEDUSERS_PROJECTS_ROLES" mpr - ON lpr."PROJECT_ID" = mpr."PROJECT_ID" - AND lpr."ROLE_ID" = mpr."ROLE_ID" - FULL OUTER JOIN "OIDCUSERS_PROJECTS_ROLES" opr - ON mpr."PROJECT_ID" = opr."PROJECT_ID" - AND mpr."ROLE_ID" = opr."ROLE_ID" - INNER JOIN "PROJECT" pr - ON lpr."PROJECT_ID" = pr."ID" - OR mpr."PROJECT_ID" = pr."ID" - OR opr."PROJECT_ID" = pr."ID" - diff --git a/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java similarity index 100% rename from src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java rename to apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java index efedb47377..4c2c32368a 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java @@ -23,6 +23,12 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import alpine.server.filters.AuthorizationFilter; +import jakarta.json.Json; +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.jcip.annotations.NotThreadSafe; import org.apache.http.HttpStatus; import org.dependencytrack.JerseyTestRule; @@ -49,12 +55,6 @@ import org.junit.ClassRule; import org.junit.Test; -import jakarta.json.Json; -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.time.Duration; import java.util.List; import java.util.UUID; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index d11140c469..185ce53f14 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -23,6 +23,11 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import junitparams.JUnitParamsRunner; import junitparams.Parameters; import net.javacrumbs.jsonunit.core.Option; @@ -69,11 +74,6 @@ import org.junit.contrib.java.lang.system.EnvironmentVariables; import org.junit.runner.RunWith; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 4b79e23488..4523a7e44c 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -26,6 +26,14 @@ import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.HttpMethod; +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.auth.Permissions; @@ -68,14 +76,6 @@ import org.junit.ClassRule; import org.junit.Test; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; -import jakarta.ws.rs.HttpMethod; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java index 4eb0f84409..94f42839ea 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/VexResourceTest.java @@ -21,6 +21,9 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import com.fasterxml.jackson.core.StreamReadConstraints; +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; @@ -42,9 +45,6 @@ import org.junit.ClassRule; import org.junit.Test; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.util.Base64; import java.util.Collections; import java.util.List; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java index 17a2aefdf1..0f63abe496 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/VulnerabilityResourceTest.java @@ -21,6 +21,12 @@ import alpine.common.util.UuidUtil; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import jakarta.json.Json; +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; @@ -42,12 +48,6 @@ import org.junit.ClassRule; import org.junit.Test; -import jakarta.json.Json; -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.List; import java.util.Set; import java.util.UUID; diff --git a/pom.xml b/pom.xml index 7eb073b65e..bc4743bedb 100644 --- a/pom.xml +++ b/pom.xml @@ -20,13 +20,6 @@ - - - us.springett - alpine-parent - 3.1.2 - - 4.0.0 org.dependencytrack dependency-track-parent @@ -90,71 +83,6 @@ UTF-8 UTF-8 . - - ${project.parent.version} - 4.2.2 - 0.5.1 - 10.20.1 - 1.21.0 - 1.27.1 - 2.1.0 - 1.4.3 - 1.0.1 - 9.1.0 - 0.3.0 - 6.0.0-rc.8 - 4.0.5 - - 2.17.3 - 2.17.3 - 3.47.0 - 4.1.0 - 4.13.2 - 3.9.0 - 4.30.0 - 0.2.2 - 8.5.17 - 2.0.0 - 3.12.1 - 1.5.0 - 0.5.3.2 - 3.2.2 - 4.29.3 - 1.20.5 - 2.2.0 - - 2.2.25 - 2.1.23 - 1.19.0 - 0.7.0 - 7.1.0 - 1.1.1 - 2.0.17 - 4.5.14 - 6.3.0 - 1.4.0 - 42.7.4 - - application - json - false - - 3.6.0 - 3.11.4 - 12.0.16 - - src/main/webapp/** - - com.google.protobuf:protoc:${lib.protobuf-java.version} - - cyclonedx - true @@ -166,11 +94,6 @@ true - - - jitpack.io - https://jitpack.io - diff --git a/src/main/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethod.java b/src/main/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethod.java deleted file mode 100644 index d8af7f3342..0000000000 --- a/src/main/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethod.java +++ /dev/null @@ -1,109 +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.datanucleus.method; - -import org.datanucleus.store.query.expression.Expression; -import org.datanucleus.store.rdbms.mapping.java.JavaTypeMapping; -import org.datanucleus.store.rdbms.sql.SQLStatement; -import org.datanucleus.store.rdbms.sql.expression.ArrayLiteral; -import org.datanucleus.store.rdbms.sql.expression.BooleanExpression; -import org.datanucleus.store.rdbms.sql.expression.BooleanLiteral; -import org.datanucleus.store.rdbms.sql.expression.ObjectExpression; -import org.datanucleus.store.rdbms.sql.expression.SQLExpression; -import org.datanucleus.store.rdbms.sql.expression.StringExpression; -import org.datanucleus.store.rdbms.sql.expression.StringLiteral; -import org.datanucleus.store.rdbms.sql.method.SQLMethod; -import org.dependencytrack.model.Project; - -import java.util.List; -import java.util.StringJoiner; - -/** - * @since 5.6.0 - */ -public class ProjectIsAccessibleByMethod implements SQLMethod { - - @Override - public SQLExpression getExpression( - final SQLStatement stmt, - final SQLExpression expr, - final List args) { - if (!(expr instanceof final ObjectExpression objectExpr)) { - // DataNucleus should prevent this from ever happening since - // the method is explicitly registered for java.lang.Object. - throw new IllegalStateException( - "Expected expression to be of type %s, but got: %s".formatted( - ObjectExpression.class.getName(), expr.getClass().getName())); - } - - final String objectTypeName = objectExpr.getJavaTypeMapping().getType(); - if (!Project.class.getName().equals(objectTypeName)) { - throw new IllegalStateException( - "isAccessibleBy is only allowed for objects of type %s, but was called on %s".formatted( - Project.class.getName(), objectTypeName)); - } - - if (args == null) { - throw new IllegalArgumentException(); - } else if (args.size() != 1) { - throw new IllegalArgumentException("Expected exactly one argument, but got " + args.size()); - } - - // TODO: When a list, set, etc. is passed as argument, it will be of type CollectionLiteral. - // Array literals are easier to verify the type of, hence we're focusing on that for now. - - if (!(args.getFirst() instanceof final ArrayLiteral arrayLiteralArg)) { - throw new IllegalArgumentException( - "Expected argument to be of type %s, but got %s".formatted( - ArrayLiteral.class.getName(), args.getFirst().getClass().getName())); - } - if (!(arrayLiteralArg.getValue() instanceof final Long[] teamIds)) { - throw new IllegalArgumentException( - "Expected array argument to be of type %s, but got %s".formatted( - Long[].class.getName(), arrayLiteralArg.getValue().getClass().getName())); - } - - final JavaTypeMapping booleanTypeMapping = stmt.getSQLExpressionFactory().getMappingForType(Boolean.class); - final JavaTypeMapping stringTypeMapping = stmt.getSQLExpressionFactory().getMappingForType(String.class); - - // Transform the array literal to have the correct type for Postgres. - // Will result in the following expression: cast('{1,2,3}' as bigint[]) - final StringJoiner joiner = new StringJoiner(",", "{", "}"); - for (final Long teamId : teamIds) { - joiner.add(String.valueOf(teamId)); - } - final var teamIdsLiteral = new StringLiteral( - stmt, stringTypeMapping, joiner.toString(), null); - final var teamIdsExpr = new StringExpression( - stmt, stringTypeMapping, "cast", List.of(teamIdsLiteral), List.of("bigint[]")); - - // NB: objectExpr will compile to a reference of the object table's ID column, e.g.: - // * "A0"."ID" - // * "B0"."PROJECT_ID" - final var hasProjectAccessFunctionExpr = new StringExpression( - stmt, stringTypeMapping, "has_project_access", List.of(objectExpr, teamIdsExpr)); - - // Wrap the function call in a boolean expression. Final result(s) will be: - // * has_project_access("A0"."ID", cast('{1,2,3}' as bigint[])) = TRUE - // * has_project_access("B0"."PROJECT_ID", cast('{1,2,3}' as bigint[])) = TRUE - final var booleanTrueLiteral = new BooleanLiteral(stmt, booleanTypeMapping, Boolean.TRUE, null); - return new BooleanExpression(hasProjectAccessFunctionExpr, Expression.OP_EQ, booleanTrueLiteral); - } - -} diff --git a/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java b/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java deleted file mode 100644 index 3b6b17ccca..0000000000 --- a/src/test/java/org/dependencytrack/persistence/datanucleus/method/ProjectIsAccessibleByMethodTest.java +++ /dev/null @@ -1,239 +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.datanucleus.method; - -import alpine.model.Team; -import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.model.Component; -import org.dependencytrack.model.Project; -import org.junit.Test; - -import javax.jdo.Query; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -public class ProjectIsAccessibleByMethodTest extends PersistenceCapableTest { - - @Test - @SuppressWarnings("resource") - public void shouldEvaluateToTrueWhenProjectIsAccessible() { - final var teamA = new Team(); - teamA.setName("team-a"); - qm.persist(teamA); - - final var teamB = new Team(); - teamB.setName("team-b"); - qm.persist(teamB); - - final var project = new Project(); - project.setName("acme-app"); - project.setAccessTeams(List.of(teamA, teamB)); - qm.persist(project); - - final Query query = qm.getPersistenceManager().newQuery(Project.class); - query.setFilter("this.isAccessibleBy(:teamIds)"); - query.setNamedParameters(Map.of("teamIds", new Long[]{teamA.getId(), teamB.getId()})); - - final List projects = query.executeList(); - assertThat(projects).hasSize(1); - } - - @Test - @SuppressWarnings("resource") - public void shouldEvaluateToTrueWhenProjectParentIsAccessible() { - final var team = new Team(); - team.setName("team"); - qm.persist(team); - - final var parentProject = new Project(); - parentProject.setName("acme-app-parent"); - parentProject.setAccessTeams(List.of(team)); - qm.persist(parentProject); - - final var project = new Project(); - project.setParent(parentProject); - project.setName("acme-app"); - qm.persist(project); - - final Query query = qm.getPersistenceManager().newQuery(Project.class); - query.setFilter("name == 'acme-app' && this.isAccessibleBy(:teamIds)"); - query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); - - final List projects = query.executeList(); - assertThat(projects).hasSize(1); - } - - @Test - @SuppressWarnings("resource") - public void shouldEvaluateToTrueWhenProjectGrandParentIsAccessible() { - final var team = new Team(); - team.setName("team"); - qm.persist(team); - - final var grandParentProject = new Project(); - grandParentProject.setName("acme-app-grand-parent"); - grandParentProject.setAccessTeams(List.of(team)); - qm.persist(grandParentProject); - - final var parentProject = new Project(); - parentProject.setParent(grandParentProject); - parentProject.setName("acme-app-parent"); - qm.persist(parentProject); - - final var project = new Project(); - project.setParent(parentProject); - project.setName("acme-app"); - qm.persist(project); - - final Query query = qm.getPersistenceManager().newQuery(Project.class); - query.setFilter("name == 'acme-app' && this.isAccessibleBy(:teamIds)"); - query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); - - final List projects = query.executeList(); - assertThat(projects).hasSize(1); - } - - @Test - @SuppressWarnings("resource") - public void shouldEvaluateToFalseWhenProjectIsNotAccessible() { - final var teamA = new Team(); - teamA.setName("team-a"); - qm.persist(teamA); - - final var teamB = new Team(); - teamB.setName("team-b"); - qm.persist(teamB); - - final var project = new Project(); - project.setName("acme-app"); - project.setAccessTeams(List.of(teamA)); - qm.persist(project); - - final Query query = qm.getPersistenceManager().newQuery(Project.class); - query.setFilter("this.isAccessibleBy(:teamIds)"); - query.setNamedParameters(Map.of("teamIds", new Long[]{teamB.getId()})); - - final List projects = query.executeList(); - assertThat(projects).isEmpty(); - } - - @Test - @SuppressWarnings("resource") - public void shouldEvaluateToFalseWhenOnlyChildProjectIsAccessible() { - final var team = new Team(); - team.setName("team"); - qm.persist(team); - - final var parentProject = new Project(); - parentProject.setName("acme-app-parent"); - qm.persist(parentProject); - - final var project = new Project(); - project.setParent(parentProject); - project.setName("acme-app"); - project.setAccessTeams(List.of(team)); - qm.persist(project); - - final Query query = qm.getPersistenceManager().newQuery(Project.class); - query.setFilter("name == 'acme-app-parent' && this.isAccessibleBy(:teamIds)"); - query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); - - final List projects = query.executeList(); - assertThat(projects).hasSize(0); - } - - @Test - @SuppressWarnings("resource") - public void shouldBeAllowedOnProjectMembersOfNonProjectObjects() { - final var team = new Team(); - team.setName("team"); - qm.persist(team); - - final var project = new Project(); - project.setName("acme-app"); - project.setAccessTeams(List.of(team)); - qm.persist(project); - - final var component = new Component(); - component.setProject(project); - component.setName("acme-lib"); - qm.persist(component); - - final Query query = qm.getPersistenceManager().newQuery(Component.class); - query.setFilter("project.isAccessibleBy(:teamIds)"); - query.setNamedParameters(Map.of("teamIds", new Long[]{team.getId()})); - - final List components = query.executeList(); - assertThat(components).hasSize(1); - } - - @Test - @SuppressWarnings("resource") - public void shouldThrowWhenCalledOnNonProjectObject() { - final Query query = qm.getPersistenceManager().newQuery(Component.class); - query.setFilter("this.isAccessibleBy(:teamIds)"); - query.setParameters(Arrays.asList(1L, 2L, 3L)); - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(query::execute) - .withMessage(""" - isAccessibleBy is only allowed for objects of type org.dependencytrack.model.Project, \ - but was called on org.dependencytrack.model.Component"""); - } - - @Test - @SuppressWarnings("resource") - public void shouldThrowWhenNoArgs() { - final Query query = qm.getPersistenceManager().newQuery(Project.class); - query.setFilter("this.isAccessibleBy()"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(query::execute) - .withMessage("Expected exactly one argument, but got 0"); - } - - @Test - @SuppressWarnings("resource") - public void shouldThrowWhenArgIsOfUnexpectedType() { - final Query query = qm.getPersistenceManager().newQuery(Project.class); - query.setFilter("this.isAccessibleBy(:teamIdsString)"); - query.setParameters("1, 2, 3"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(query::execute) - .withMessage(""" - Expected argument to be of type org.datanucleus.store.rdbms.sql.expression.ArrayLiteral, \ - but got org.datanucleus.store.rdbms.sql.expression.ParameterLiteral"""); - } - - @Test - @SuppressWarnings("resource") - public void shouldThrowWhenArgIsOfUnexpectedArrayType() { - final Query query = qm.getPersistenceManager().newQuery(Project.class); - query.setFilter("this.isAccessibleBy(:teamIdStrings)"); - query.setNamedParameters(Map.of("teamIdsStrings", new String[]{"1", "2", "3"})); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(query::execute) - .withMessage(""" - Expected argument to be of type org.datanucleus.store.rdbms.sql.expression.ArrayLiteral, \ - but got org.datanucleus.store.rdbms.sql.expression.ParameterLiteral"""); - } - -} \ No newline at end of file From 4b2e4476123bea33dcf28a402f3ca39fc7a89fad Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 17 Apr 2025 10:56:58 -0600 Subject: [PATCH 130/181] merge cleanup Signed-off-by: Allen Shearin --- apiserver/src/main/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/src/main/docker/Dockerfile b/apiserver/src/main/docker/Dockerfile index f7960091b0..78cae51c5c 100644 --- a/apiserver/src/main/docker/Dockerfile +++ b/apiserver/src/main/docker/Dockerfile @@ -66,7 +66,7 @@ RUN mkdir -p ${APP_DIR} ${DATA_DIR} \ COPY --from=jre-build /opt/java/openjdk $JAVA_HOME # Copy the compiled WAR to the application directory created above -COPY ./target/${WAR_FILENAME} ./src/main/docker/logback-json.xml ${APP_DIR} +COPY ./target/dependency-track-apiserver.jar ./src/main/docker/logback-json.xml ${APP_DIR} # Specify the user to run as (in numeric format for compatibility with Kubernetes/OpenShift's SCC) USER ${UID} From b255fe8a74a9ce3140ca417cc8d733933979f682 Mon Sep 17 00:00:00 2001 From: Emmanuel Meremikwu Date: Mon, 21 Apr 2025 09:40:53 -0500 Subject: [PATCH 131/181] refactor: user/permission resource for bulk put request Signed-off-by: Emmanuel Meremikwu --- .../model/validation/ValidUuidList.java | 43 +++++++++++ .../validation/ValidUuidListValidator.java | 46 +++++++++++ .../resources/v1/PermissionResource.java | 62 +++++++++++++++ .../resources/v1/UserResource.java | 74 +++++++++++++++++- .../v1/vo/PermissionsSetRequest.java | 27 +++++++ .../resources/v1/vo/TeamsSetRequest.java | 45 +++++++++++ .../resources/v1/PermissionResourceTest.java | 56 ++++++++++++++ .../v1/UserResourceAuthenticatedTest.java | 77 +++++++++++++++++++ 8 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidList.java create mode 100644 apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidListValidator.java create mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java create mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/vo/TeamsSetRequest.java diff --git a/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidList.java b/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidList.java new file mode 100644 index 0000000000..6b8d2daef3 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidList.java @@ -0,0 +1,43 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ + +package org.dependencytrack.model.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Documented +@Constraint(validatedBy = ValidUuidListValidator.class) +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) + +public @interface ValidUuidList { + String message() default "Invalid UUID"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidListValidator.java b/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidListValidator.java new file mode 100644 index 0000000000..3c5683eb18 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidListValidator.java @@ -0,0 +1,46 @@ +/* + * 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.validation; + +import java.util.Set; +import java.util.UUID; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class ValidUuidListValidator implements ConstraintValidator> { + + @Override + public boolean isValid(final Set uuidList, final ConstraintValidatorContext validatorContext) { + if (uuidList == null || uuidList.isEmpty()) { + // null or empty sets are considered valid + return true; + } + + for (String uuidString : uuidList) { + try { + UUID.fromString(uuidString); + } catch (IllegalArgumentException e) { + return false; + } + } + return true; + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 19d305a1d1..22ee0d4776 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -37,6 +37,7 @@ 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; @@ -45,9 +46,13 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.vo.PermissionsSetRequest; import org.owasp.security.logging.SecurityMarkers; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * JAX-RS resources for processing permissions. @@ -264,4 +269,61 @@ public Response removePermissionFromTeam( return Response.status(Response.Status.NOT_MODIFIED).build(); } } + + @PUT + @Path("/user/{username}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_DELETE

        ") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "The updated user", content = @Content(schema = @Schema(implementation = Team.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user could not be found") + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_DELETE }) + public Response setUserPermissions( + @Parameter(description = "A valid user uuid", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("username") String username, + @Parameter(description = "A valid permission", required = true) PermissionsSetRequest request) { + try (QueryManager qm = new QueryManager()) { + UserPrincipal user = qm.getUserPrincipal(username); + if (user == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); + } + + final List requestedPermissions = request.permissions() + .stream() + .map(qm::getPermission) + .toList(); + + // check that all requested teams exist + List notFound = new ArrayList<>(); + for (int i = 0; i < requestedPermissions.size(); i++) { + if (requestedPermissions.get(i) == null) { + notFound.add(request.permissions().stream().toList().get(i)); + } + } + + if(notFound.size() > 0){ + Map response = new HashMap<>(); + response.put("error", "One or more permissions could not be found"); + response.put("permissions", notFound); + return Response.status(Response.Status.BAD_REQUEST).entity(response).build(); + } + + final List currentPermissions = user.getPermissions(); + + if (currentPermissions.equals(requestedPermissions)) { + return Response.notModified().entity("User already has selected permission(s).").build(); + } + + user.setPermissions(requestedPermissions); + user = qm.persist(user); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Set permissions for user: %s / permissions: %s" + .formatted(user.getName(), request.permissions())); + + return Response.ok(user).build(); + } + } + } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 249cb68d00..ad043ef75a 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -48,6 +48,7 @@ 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.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.FormParam; @@ -68,11 +69,19 @@ import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.proto.notification.v1.UserSubject; +import org.dependencytrack.resources.v1.vo.TeamsSetRequest; import org.owasp.security.logging.SecurityMarkers; import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; /** * JAX-RS resources for processing users. @@ -153,7 +162,7 @@ public Response validateCredentials(@FormParam("username") String username, @For @AuthenticationNotRequired public Response validateOidcAccessToken(@Parameter(description = "An OAuth2 access token", required = true) @FormParam("idToken") final String idToken, - @FormParam("accessToken") final String accessToken) { + @FormParam("accessToken") final String accessToken) { final OidcAuthenticationService authService = new OidcAuthenticationService(idToken, accessToken); if (!authService.isSpecified()) { @@ -194,7 +203,7 @@ public Response validateOidcAccessToken(@Parameter(description = "An OAuth2 acce }) @AuthenticationNotRequired public Response forceChangePassword(@FormParam("username") String username, @FormParam("password") String password, - @FormParam("newPassword") String newPassword, @FormParam("confirmPassword") String confirmPassword) { + @FormParam("newPassword") String newPassword, @FormParam("confirmPassword") String confirmPassword) { final Authenticator auth = new Authenticator(username, password); Principal principal; try (QueryManager qm = new QueryManager()) { @@ -775,4 +784,65 @@ private UserSubject buildUserSubject(final String username, final String email) Optional.ofNullable(email).ifPresent(userBuilder::setEmail); return userBuilder.build(); } + + @PUT + @Path("/{username}/membership") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Adds the username to the specified team.", description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

        ") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "The updated user", content = @Content(schema = @Schema(implementation = UserPrincipal.class))), + @ApiResponse(responseCode = "304", description = "The user is already a member of the specified team(s)"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The user or team(s) could not be found") + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE }) + public Response setUserTeams( + @Parameter(description = "A valid username", required = true) @PathParam("username") String username, + @Parameter(description = "The UUID(s) of the team(s) to associate username with", required = true) @Valid TeamsSetRequest request) { + try (QueryManager qm = new QueryManager()) { + UserPrincipal principal = qm.getUserPrincipal(username); + if (principal == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); + } + + // Compare given team uuids against current user teams + final Set currentUserTeams = principal.getTeams() == null ? Collections.emptySet() + : principal.getTeams().stream() + .map(Team::getUuid) + .map(UUID::toString) + .collect(Collectors.toSet()); + + + if (currentUserTeams.equals(request.getTeams())) { + return Response.notModified().entity("The user is already a member of the selected team(s)").build(); + } + + List requestedTeams = request.getTeams() + .stream() + .map(uuid -> qm.getObjectByUuid(Team.class, uuid)) + .toList(); + + // check that all requested teams exist + List notFound = new ArrayList<>(); + for (int i = 0; i < requestedTeams.size(); i++) { + if(requestedTeams.get(i) == null) + notFound.add(request.getTeams().stream().toList().get(i)); + } + + if (notFound.size() > 0) { + // String msg = String.format("One or more teams could not be found:\n%s", ); + Map response = new HashMap<>(); + response.put("error", "One or more teams could not be found"); + response.put("teams", notFound); + return Response.status(Response.Status.BAD_REQUEST).entity(response).build(); + } + + principal.setTeams(requestedTeams); + qm.persist(principal); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Added team membership for: " + principal.getName() + " / team: " + requestedTeams.toString()); + return Response.ok(principal).build(); + } + } } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java new file mode 100644 index 0000000000..f2d5cc2712 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java @@ -0,0 +1,27 @@ +/* + * 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.resources.v1.vo; + +import java.util.Set; + +import jakarta.validation.constraints.NotNull; + +public record PermissionsSetRequest(@NotNull Set permissions) { +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/TeamsSetRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/TeamsSetRequest.java new file mode 100644 index 0000000000..0ff61bf848 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/TeamsSetRequest.java @@ -0,0 +1,45 @@ +/* + * 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.resources.v1.vo; + +import java.util.Set; + +import org.dependencytrack.model.validation.ValidUuidList; + +import jakarta.validation.constraints.NotNull; + +public class TeamsSetRequest { + + @NotNull + @ValidUuidList + private Set teams; + + public TeamsSetRequest() { + // Default constructor for deserialization + } + + public TeamsSetRequest(Set teams) { + this.teams = teams; + } + + public Set getTeams() { + return teams; + } +} 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 636775f935..51a37d1a81 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java @@ -27,6 +27,7 @@ import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.resources.v1.vo.PermissionsSetRequest; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; import org.junit.Before; @@ -38,6 +39,8 @@ import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.Set; import java.util.UUID; public class PermissionResourceTest extends ResourceTest { @@ -288,4 +291,57 @@ public void removePermissionFromTeamNoChangesTest() { Assert.assertEquals(304, response.getStatus(), 0); Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); } + + @Test + public void setUserTeamTest() { + qm.createManagedUser("user2", TEST_USER_PASSWORD_HASH); + List testPermissions = List.of( + qm.getPermission("ACCESS_MANAGEMENT"), + qm.getPermission("ACCESS_MANAGEMENT_CREATE"), + qm.getPermission("ACCESS_MANAGEMENT_DELETE") + ); + + PermissionsSetRequest requestBody = new PermissionsSetRequest( + Set.of("ACCESS_MANAGEMENT","ACCESS_MANAGEMENT_CREATE","ACCESS_MANAGEMENT_DELETE") + ); + + Response response = jersey.target(V1_PERMISSION + "/user/user2") + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(requestBody, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus()); + + JsonObject jsonResponse = parseJsonObject(response); + Assert.assertNotNull(jsonResponse); + + + ManagedUser user = qm.getManagedUser("user2"); + Assert.assertNotNull(user); + List userPermissions = user.getPermissions(); + + Assert.assertEquals(userPermissions.size(), 3); + Assert.assertTrue(userPermissions.equals(testPermissions)); + } + + @Test + public void setUserTeamsInvalidPermissionsTest() { + qm.createManagedUser("user2", TEST_USER_PASSWORD_HASH); + PermissionsSetRequest badRequestBody = new PermissionsSetRequest( + Set.of("Invalid", "Permission", "List", "Four") + ); + + Response response = jersey.target(V1_PERMISSION + "/user/user2") + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(badRequestBody, MediaType.APPLICATION_JSON)); + Assert.assertEquals(400, response.getStatus()); + + JsonObject jsonResponse = parseJsonObject(response); + Assert.assertNotNull(jsonResponse); + + JsonArray permissionsArray = jsonResponse.getJsonArray("permissions"); + Assert.assertNotNull(permissionsArray); + + Assert.assertEquals(permissionsArray.size(), 4); + } } diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java index 59ba559a6d..b89d012624 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java @@ -22,6 +22,7 @@ import alpine.model.ManagedUser; import alpine.model.OidcUser; import alpine.model.Team; +import alpine.model.UserPrincipal; import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; @@ -30,6 +31,7 @@ import org.dependencytrack.event.kafka.KafkaTopics; import org.dependencytrack.model.IdentifiableObject; import org.dependencytrack.notification.NotificationConstants; +import org.dependencytrack.resources.v1.vo.TeamsSetRequest; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; @@ -43,6 +45,8 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.time.Duration; +import java.util.List; +import java.util.Set; import java.util.UUID; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -655,4 +659,77 @@ public void removeTeamFromUserTest() { // Hack: Workaround to https://github.com/eclipse-ee4j/jersey/issues/3798 Assert.assertEquals(200, response.getStatus(), 0); } + + @Test + public void setUserTeamsTest() { + Team team1 = qm.createTeam("Pirates", false); + Team team2 = qm.createTeam("Penguins", false); + Team team3 = qm.createTeam("Steelers", false); + + /* ManagedUser user = */ + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", + TEST_USER_PASSWORD_HASH, false, false, false); + + TeamsSetRequest requestBody = new TeamsSetRequest( + Set.of( + team1.getUuid().toString(), + team2.getUuid().toString(), + team3.getUuid().toString())); + + Response response = jersey.target(V1_USER + "/blackbeard/membership").request() + .header(X_API_KEY, apiKey) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) + .put(Entity.entity(requestBody, MediaType.APPLICATION_JSON)); + + Assert.assertEquals(200, response.getStatus(), 0); + + UserPrincipal user = qm.getUserPrincipal("blackbeard"); + Assert.assertNotNull(user); + + List userTeams = user.getTeams(); + Assert.assertEquals(userTeams.size(), 3); + Assert.assertTrue(userTeams.contains(team1)); + Assert.assertTrue(userTeams.contains(team2)); + Assert.assertTrue(userTeams.contains(team3)); + } + + @Test + public void setUserTeamsInvalidTeamsTest() { + /* ManagedUser user = */ + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", + TEST_USER_PASSWORD_HASH, false, false, false); + + Team validTeam = qm.createTeam("Pirates", false); + TeamsSetRequest requestBody = new TeamsSetRequest( + // 1 valid, 2 invalid + Set.of( + validTeam.getUuid().toString(), + "b3ed3497-8f50-41c5-b754-ab31853b07", + "9f4851-9bac-4ad9-b23f-209943cda7")); + + Response response = jersey.target(V1_USER + "/blackbeard/membership").request() + .header(X_API_KEY, apiKey) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) + .put(Entity.entity(requestBody, MediaType.APPLICATION_JSON)); + Assert.assertEquals(400, response.getStatus()); + + JsonObject jsonResponse = parseJsonObject(response); + Assert.assertNotNull(jsonResponse); + + JsonArray invalidTeamsArray = jsonResponse.getJsonArray("teams"); + Assert.assertEquals(2, invalidTeamsArray.size()); + } + @Test + public void setUserTeamsInvalidUuidTest() { + /* ManagedUser user = */ + qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", + TEST_USER_PASSWORD_HASH, false, false, false); + TeamsSetRequest requestBody = new TeamsSetRequest(Set.of("Not a Uuid")); + + Response response = jersey.target(V1_USER + "/blackbeard/membership").request() + .header(X_API_KEY, apiKey) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) + .put(Entity.entity(requestBody, MediaType.APPLICATION_JSON)); + Assert.assertEquals(400, response.getStatus()); + } } From c207fa00cf3e9429d8171bdf424ffd0be2adfca4 Mon Sep 17 00:00:00 2001 From: Emmanuel Meremikwu Date: Mon, 21 Apr 2025 12:37:06 -0500 Subject: [PATCH 132/181] refactor: Update User/Permission resource test Signed-off-by: Emmanuel Meremikwu --- .../resources/v1/PermissionResource.java | 2 +- .../resources/v1/UserResource.java | 3 +- .../resources/v1/PermissionResourceTest.java | 38 +++++++++++++------ .../v1/UserResourceAuthenticatedTest.java | 38 ++++++++++++++----- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 22ee0d4776..268e7ae76a 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -303,7 +303,7 @@ public Response setUserPermissions( } } - if(notFound.size() > 0){ + if (notFound.size() > 0) { Map response = new HashMap<>(); response.put("error", "One or more permissions could not be found"); response.put("permissions", notFound); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index ad043ef75a..4bcb888f99 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -813,7 +813,6 @@ public Response setUserTeams( .map(UUID::toString) .collect(Collectors.toSet()); - if (currentUserTeams.equals(request.getTeams())) { return Response.notModified().entity("The user is already a member of the selected team(s)").build(); } @@ -826,7 +825,7 @@ public Response setUserTeams( // check that all requested teams exist List notFound = new ArrayList<>(); for (int i = 0; i < requestedTeams.size(); i++) { - if(requestedTeams.get(i) == null) + if (requestedTeams.get(i) == null) notFound.add(request.getTeams().stream().toList().get(i)); } 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 51a37d1a81..bc9e32543f 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java @@ -293,17 +293,16 @@ public void removePermissionFromTeamNoChangesTest() { } @Test - public void setUserTeamTest() { + public void setUserPermissionsTest() { qm.createManagedUser("user2", TEST_USER_PASSWORD_HASH); + List testPermissions = List.of( - qm.getPermission("ACCESS_MANAGEMENT"), - qm.getPermission("ACCESS_MANAGEMENT_CREATE"), - qm.getPermission("ACCESS_MANAGEMENT_DELETE") - ); + qm.getPermission("ACCESS_MANAGEMENT"), + qm.getPermission("ACCESS_MANAGEMENT_CREATE"), + qm.getPermission("ACCESS_MANAGEMENT_DELETE")); PermissionsSetRequest requestBody = new PermissionsSetRequest( - Set.of("ACCESS_MANAGEMENT","ACCESS_MANAGEMENT_CREATE","ACCESS_MANAGEMENT_DELETE") - ); + Set.of("ACCESS_MANAGEMENT", "ACCESS_MANAGEMENT_CREATE", "ACCESS_MANAGEMENT_DELETE")); Response response = jersey.target(V1_PERMISSION + "/user/user2") .request() @@ -314,21 +313,38 @@ public void setUserTeamTest() { JsonObject jsonResponse = parseJsonObject(response); Assert.assertNotNull(jsonResponse); - ManagedUser user = qm.getManagedUser("user2"); Assert.assertNotNull(user); List userPermissions = user.getPermissions(); Assert.assertEquals(userPermissions.size(), 3); Assert.assertTrue(userPermissions.equals(testPermissions)); + + Permission test_1 = qm.createPermission("TEST_PERMISSION_1", "TEST PERMISSION"); + Permission test_2 = qm.createPermission("TEST_PERMISSION_2", "TEST PERMISSION"); + + requestBody = new PermissionsSetRequest(Set.of(test_1.getName(), test_2.getName())); + + response = jersey.target(V1_PERMISSION + "/user/user2") + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(requestBody, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus()); + + // refresh + user = qm.getManagedUser("user2"); + userPermissions = user.getPermissions(); + + Assert.assertTrue(userPermissions.contains(test_1)); + Assert.assertTrue(userPermissions.contains(test_2)); + } @Test - public void setUserTeamsInvalidPermissionsTest() { + public void setUserPermissionsInvalidPermissionsTest() { qm.createManagedUser("user2", TEST_USER_PASSWORD_HASH); PermissionsSetRequest badRequestBody = new PermissionsSetRequest( - Set.of("Invalid", "Permission", "List", "Four") - ); + Set.of("Invalid", "Permission", "List", "Four")); Response response = jersey.target(V1_PERMISSION + "/user/user2") .request() diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java index b89d012624..8486f6ae4c 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java @@ -666,22 +666,20 @@ public void setUserTeamsTest() { Team team2 = qm.createTeam("Penguins", false); Team team3 = qm.createTeam("Steelers", false); - /* ManagedUser user = */ qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); - TeamsSetRequest requestBody = new TeamsSetRequest( - Set.of( - team1.getUuid().toString(), - team2.getUuid().toString(), - team3.getUuid().toString())); + TeamsSetRequest requestBody = new TeamsSetRequest(Set.of( + team1.getUuid().toString(), + team2.getUuid().toString(), + team3.getUuid().toString())); Response response = jersey.target(V1_USER + "/blackbeard/membership").request() .header(X_API_KEY, apiKey) .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) .put(Entity.entity(requestBody, MediaType.APPLICATION_JSON)); - Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals(200, response.getStatus()); UserPrincipal user = qm.getUserPrincipal("blackbeard"); Assert.assertNotNull(user); @@ -691,11 +689,33 @@ public void setUserTeamsTest() { Assert.assertTrue(userTeams.contains(team1)); Assert.assertTrue(userTeams.contains(team2)); Assert.assertTrue(userTeams.contains(team3)); + + Team team4 = qm.createTeam("Strawhats", false); + Team team5 = qm.createTeam("Cowboys", false); + + requestBody = new TeamsSetRequest(Set.of( + team4.getUuid().toString(), + team5.getUuid().toString())); + + response = jersey.target(V1_USER + "/blackbeard/membership").request() + .header(X_API_KEY, apiKey) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) + .put(Entity.entity(requestBody, MediaType.APPLICATION_JSON)); + + user = qm.getUserPrincipal("blackbeard"); + userTeams = user.getTeams(); + + Assert.assertEquals(200, response.getStatus()); + + Assert.assertFalse(userTeams.contains(team1)); + Assert.assertFalse(userTeams.contains(team2)); + Assert.assertFalse(userTeams.contains(team3)); + Assert.assertTrue(userTeams.contains(team4)); + Assert.assertTrue(userTeams.contains(team5)); } @Test public void setUserTeamsInvalidTeamsTest() { - /* ManagedUser user = */ qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); @@ -719,9 +739,9 @@ public void setUserTeamsInvalidTeamsTest() { JsonArray invalidTeamsArray = jsonResponse.getJsonArray("teams"); Assert.assertEquals(2, invalidTeamsArray.size()); } + @Test public void setUserTeamsInvalidUuidTest() { - /* ManagedUser user = */ qm.createManagedUser("blackbeard", "Captain BlackBeard", "blackbeard@example.com", TEST_USER_PASSWORD_HASH, false, false, false); TeamsSetRequest requestBody = new TeamsSetRequest(Set.of("Not a Uuid")); From edc7c4ad79e42c4ee6665ba06b04260e126ff0b9 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 21 Apr 2025 13:48:17 -0600 Subject: [PATCH 133/181] merge fixes Signed-off-by: Allen Shearin --- .../dependencytrack/resources/v1/UserResource.java | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index f503a7cf60..fcd4dcb7cf 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -1,3 +1,4 @@ +/* * This file is part of Dependency-Track. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -77,25 +78,12 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.auth.Permissions; -import org.dependencytrack.event.kafka.KafkaEventDispatcher; -import org.dependencytrack.model.IdentifiableObject; -import org.dependencytrack.notification.NotificationConstants; -import org.dependencytrack.notification.NotificationGroup; -import org.dependencytrack.notification.NotificationScope; -import org.dependencytrack.persistence.QueryManager; -import org.dependencytrack.proto.notification.v1.UserSubject; import org.dependencytrack.resources.v1.vo.TeamsSetRequest; -import org.owasp.security.logging.SecurityMarkers; -import java.security.Principal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; From 08fcb04f44cd05099c01cf38cdb71bfdf32158e8 Mon Sep 17 00:00:00 2001 From: e388459 Date: Tue, 22 Apr 2025 12:04:49 -0400 Subject: [PATCH 134/181] FE: Expand Gitlab integration page to include sbom push options Signed-off-by: e388459 --- .../org/dependencytrack/model/ConfigPropertyConstants.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 5d5f0b8afe..8b840d245a 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -86,6 +86,8 @@ public enum ConfigPropertyConstants { FORTIFY_SSC_SYNC_CADENCE("integrations", "fortify.ssc.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), FORTIFY_SSC_URL("integrations", "fortify.ssc.url", null, PropertyType.URL, "Base URL to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), FORTIFY_SSC_TOKEN("integrations", "fortify.ssc.token", null, PropertyType.ENCRYPTEDSTRING, "The token to use to authenticate to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_AUDIENCE("integrations", "gitlab.audience", null, PropertyType.STRING, "The audience to use when authenticating to GitLab", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_AUTOCREATE_PROJECTS("integrations", "gitlab.autocreate.projects", "false", PropertyType.BOOLEAN, "Flag to enable/disable auto-creation of projects in GitLab", ConfigPropertyAccessMode.READ_WRITE), GITLAB_ENABLED("integrations", "gitlab.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitLab integration", ConfigPropertyAccessMode.READ_WRITE), GITLAB_GROUPS("integrations", "gitlab.groups", "[]", PropertyType.STRING, "JSON array of GitLab group names for which to create teams/roles", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), @@ -104,6 +106,7 @@ public enum ConfigPropertyConstants { ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio", ConfigPropertyAccessMode.READ_WRITE, true), NOTIFICATION_TEMPLATE_BASE_DIR("notification", "template.baseDir", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_BASE_DIRECTORY", System.getProperty("user.home")), PropertyType.STRING, "The base directory to use when searching for notification templates", ConfigPropertyAccessMode.READ_WRITE), NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED("notification", "template.default.override.enabled", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_ENABLED", "false"), PropertyType.BOOLEAN, "Flag to enable/disable override of default notification templates", ConfigPropertyAccessMode.READ_WRITE), + SBOM_PUSH("integrations", "sbom.push.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_LDAP_SYNC_CADENCE("task-scheduler", "ldap.sync.cadence", "6", PropertyType.INTEGER, "Sync cadence (in hours) for LDAP", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_GHSA_MIRROR_CADENCE("task-scheduler", "ghsa.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for Github Security Advisories", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_OSV_MIRROR_CADENCE("task-scheduler", "osv.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for OSV database", ConfigPropertyAccessMode.READ_WRITE), From 932ee5aecf2438e5791f52ed9fecd2b43b347a0a Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 23 Apr 2025 12:30:37 -0600 Subject: [PATCH 135/181] fix defaultobjectgenerator merge conflicts Signed-off-by: Allen Shearin --- .../persistence/DefaultObjectGenerator.java | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 12c9d6bce3..3b934d4c80 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -24,6 +24,10 @@ 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; @@ -34,9 +38,8 @@ import org.dependencytrack.util.NotificationUtil; import org.dependencytrack.util.WaitingLockConfiguration; -import jakarta.servlet.ServletContextEvent; -import jakarta.servlet.ServletContextListener; import java.io.IOException; + import java.time.Duration; import java.time.Instant; import java.util.Collections; @@ -261,21 +264,10 @@ public void loadDefaultPermissions() { private void loadDefaultPermissions(final QueryManager qm) { LOGGER.info("Synchronizing permissions to datastore"); - for (final Permissions permission : Permissions.values()) { - if (qm.getPermission(permission.name()) == null) { - LOGGER.debug("Creating permission: " + permission.name()); - permissionsMap.put(permission.name(), - qm.createPermission(permission.name(), permission.getDescription())); - } - } - } - - private void createTeam(final QueryManager qm, final String name, final List permissions) { - LOGGER.debug("Creating team: " + name); - var team = qm.createTeam(name); - - LOGGER.debug("Assigning default permissions for team: " + name); - team.setPermissions(permissions); + List existing = Objects.requireNonNullElse(qm.getPermissions(), Collections.emptyList()) + .stream() + .map(Permission::getName) + .toList(); for (final Permissions value : Permissions.values()) if (!existing.contains(value.name())) { @@ -304,10 +296,15 @@ private void loadDefaultPersonas(final QueryManager qm) { ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost", new String(PasswordService.createHash("admin".toCharArray())), true, true, false); - createTeam(qm, "Administrators", List.copyOf(permissionsMap.values())); - createTeam(qm, "Portfolio Managers", getPortfolioManagersPermissions()); - createTeam(qm, "Automation", getAutomationPermissions()); - createTeam(qm, "Badge Viewers", getBadgesPermissions()); + for (var name : new String[] { "Administrators", "Portfolio Managers", "Automation", "Badge Viewers" }) { + 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")); From 162f2f9d95d7658ee3aadf4a42d4dc2b021bf77c Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 23 Apr 2025 12:40:42 -0600 Subject: [PATCH 136/181] remove duplicate changeset Signed-off-by: Allen Shearin --- .../resources/migration/changelog-v5.6.0-roles.xml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml index fa21c426c8..4c89dcea3f 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -6,19 +6,6 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - - - - - - - - - - - - - From ac517dd19018ff38faccfccb1aceeed26f303511 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Fri, 25 Apr 2025 13:12:14 -0500 Subject: [PATCH 137/181] refactor: pull changes from Alpine PR Signed-off-by: Jonathan Howard --- .../src/main/java/alpine/Config.java | 5 +- .../DefaultOidcAuthenticationCustomizer.java | 88 ++++++++ .../java/alpine/server/auth/JsonWebToken.java | 202 ++++++++++++++---- .../auth/OidcAuthenticationCustomizer.java | 35 +++ .../auth/OidcAuthenticationService.java | 72 ++++--- .../java/alpine/server/auth/OidcProfile.java | 49 +++-- ...e.server.auth.OidcAuthenticationCustomizer | 1 + 7 files changed, 357 insertions(+), 95 deletions(-) create mode 100644 alpine/alpine-server/src/main/java/alpine/server/auth/DefaultOidcAuthenticationCustomizer.java create mode 100644 alpine/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationCustomizer.java create mode 100644 alpine/alpine-server/src/main/resources/META-INF/services/alpine.server.auth.OidcAuthenticationCustomizer diff --git a/alpine/alpine-common/src/main/java/alpine/Config.java b/alpine/alpine-common/src/main/java/alpine/Config.java index ca67eb99c5..5c75b6274d 100644 --- a/alpine/alpine-common/src/main/java/alpine/Config.java +++ b/alpine/alpine-common/src/main/java/alpine/Config.java @@ -102,6 +102,7 @@ public interface Key { } public enum AlpineKey implements Key { + // @formatter:off WORKER_THREADS ("alpine.worker.threads", 0), WORKER_THREAD_MULTIPLIER ("alpine.worker.thread.multiplier", 4), DATA_DIRECTORY ("alpine.data.directory", "~/.alpine"), @@ -168,6 +169,7 @@ public enum AlpineKey implements Key { OIDC_TEAM_SYNCHRONIZATION ("alpine.oidc.team.synchronization", false), OIDC_TEAMS_CLAIM ("alpine.oidc.teams.claim", "groups"), OIDC_TEAMS_DEFAULT ("alpine.oidc.teams.default", null), + OIDC_AUTH_CUSTOMIZER ("alpine.oidc.auth.customizer", "alpine.server.auth.DefaultOidcAuthenticationCustomizer"), HTTP_PROXY_ADDRESS ("alpine.http.proxy.address", null), HTTP_PROXY_PORT ("alpine.http.proxy.port", null), HTTP_PROXY_USERNAME ("alpine.http.proxy.username", null), @@ -187,10 +189,11 @@ public enum AlpineKey implements Key { WATCHDOG_LOGGING_INTERVAL ("alpine.watchdog.logging.interval", 0), API_KEY_PREFIX ("alpine.api.key.prefix", "alpine_"), AUTH_JWT_TTL_SECONDS ("alpine.auth.jwt.ttl.seconds", 7 * 24 * 60 * 60); - + // @formatter:on private String propertyName; private Object defaultValue; + AlpineKey(String item, Object defaultValue) { this.propertyName = item; this.defaultValue = defaultValue; diff --git a/alpine/alpine-server/src/main/java/alpine/server/auth/DefaultOidcAuthenticationCustomizer.java b/alpine/alpine-server/src/main/java/alpine/server/auth/DefaultOidcAuthenticationCustomizer.java new file mode 100644 index 0000000000..c8231c8857 --- /dev/null +++ b/alpine/alpine-server/src/main/java/alpine/server/auth/DefaultOidcAuthenticationCustomizer.java @@ -0,0 +1,88 @@ +/* + * This file is part of Alpine. + * + * 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) Steve Springett. All Rights Reserved. + */ + +package alpine.server.auth; + +import net.minidev.json.JSONObject; + +import alpine.Config; +import alpine.model.OidcUser; + +import com.nimbusds.openid.connect.sdk.claims.ClaimsSet; +import com.nimbusds.openid.connect.sdk.claims.UserInfo; + +public class DefaultOidcAuthenticationCustomizer implements OidcAuthenticationCustomizer { + + public DefaultOidcAuthenticationCustomizer() { + } + + @Override + public OidcProfile createProfile(ClaimsSet claimsSet) { + final String teamsClaimName = Config.getInstance().getProperty(Config.AlpineKey.OIDC_TEAMS_CLAIM); + final String usernameClaimName = Config.getInstance().getProperty(Config.AlpineKey.OIDC_USERNAME_CLAIM); + final var profile = new OidcProfile(); + + profile.setSubject(claimsSet.getStringClaim(UserInfo.SUB_CLAIM_NAME)); + profile.setUsername(claimsSet.getStringClaim(usernameClaimName)); + profile.setGroups(claimsSet.getStringListClaim(teamsClaimName)); + profile.setEmail(claimsSet.getStringClaim(UserInfo.EMAIL_CLAIM_NAME)); + + JSONObject claimsObj = claimsSet.toJSONObject(); + claimsObj.remove(UserInfo.EMAIL_CLAIM_NAME); + claimsObj.remove(UserInfo.SUB_CLAIM_NAME); + claimsObj.remove(teamsClaimName); + claimsObj.remove(usernameClaimName); + + profile.setCustomValues(claimsObj); + + return profile; + } + + @Override + public boolean isProfileComplete(final OidcProfile profile, final boolean teamSyncEnabled) { + return profile.getSubject() != null && profile.getUsername() != null + && (!teamSyncEnabled || (profile.getGroups() != null)); + } + + @Override + public OidcProfile mergeProfiles(final OidcProfile left, final OidcProfile right) { + final var profile = new OidcProfile(); + + profile.setSubject(selectProfileClaim(left.getSubject(), right.getSubject())); + profile.setUsername(selectProfileClaim(left.getUsername(), right.getUsername())); + profile.setGroups(selectProfileClaim(left.getGroups(), right.getGroups())); + profile.setEmail(selectProfileClaim(left.getEmail(), right.getEmail())); + + JSONObject customValues = left.getCustomValues(); + customValues.merge(right.getCustomValues()); + profile.setCustomValues(customValues); + + return profile; + } + + @Override + public OidcUser onAuthenticationSuccess(OidcUser user, OidcProfile profile, String idToken, String accessToken) { + return user; + } + + private T selectProfileClaim(final T left, final T right) { + return (left != null) ? left : right; + } + +} diff --git a/alpine/alpine-server/src/main/java/alpine/server/auth/JsonWebToken.java b/alpine/alpine-server/src/main/java/alpine/server/auth/JsonWebToken.java index 35d58ed15f..94e1c36f98 100644 --- a/alpine/alpine-server/src/main/java/alpine/server/auth/JsonWebToken.java +++ b/alpine/alpine-server/src/main/java/alpine/server/auth/JsonWebToken.java @@ -24,16 +24,17 @@ import alpine.model.OidcUser; import alpine.model.Permission; import alpine.security.crypto.KeyManager; + import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; + import org.owasp.security.logging.SecurityMarkers; import javax.crypto.SecretKey; @@ -42,6 +43,7 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -56,19 +58,19 @@ public class JsonWebToken { private static final Logger LOGGER = Logger.getLogger(JsonWebToken.class); private static final String IDENTITY_PROVIDER_CLAIM = "idp"; - private static String ISSUER = "Alpine"; + private static final String PERMISSIONS_CLAIM = "permissions"; + private static String ISSUER = Config.getInstance().getApplicationName(); static { - if (Config.getInstance().getApplicationName() != null) { - ISSUER = Config.getInstance().getApplicationName(); - } else { - Config.getInstance().getFrameworkName(); - } + ISSUER = ISSUER != null ? ISSUER : Config.getInstance().getFrameworkName(); } private final SecretKey key; private String subject; + private int ttl = Config.getInstance().getPropertyAsInt(Config.AlpineKey.AUTH_JWT_TTL_SECONDS); private Date expiration; private IdentityProvider identityProvider; + private List permissions; + private Map extraClaims; /** * Constructs a new JsonWekToken object using the specified SecretKey which can @@ -97,6 +99,90 @@ public JsonWebToken() { this(KeyManager.getInstance().getSecretKey()); } + /** + * Finalize and generate the encoded String representation of the JsonWebToken. + * + * @return a String representation of the generated token + * @since 3.2.0 + */ + public String build() { + final Date now = new Date(); + this.expiration = addSeconds(now, ttl); + + return Jwts.builder().subject(subject) + .issuedAt(now) + .issuer(ISSUER) + .expiration(expiration) + .claim(PERMISSIONS_CLAIM, this.permissions == null ? null + : permissions.stream() + .map(Permission::getName) + .collect(Collectors.joining(","))) + .claim(IDENTITY_PROVIDER_CLAIM, identityProvider) + .claims(extraClaims) + .signWith(key) + .compact(); + } + + /** + * Builder method to set the value of the {@code exp} claim. + * + * @param date the expiration date + * @return the updated JsonWebToken object + * @since 3.2.0 + */ + public JsonWebToken expiration(Date date) { + this.expiration = date; + return this; + } + + /** + * Builder method to set claims explicitly. + * + * @param extraClaims a Map of claims to set + * @return the updated JsonWebToken object + * @since 3.2.0 + */ + public JsonWebToken extraClaims(Map extraClaims) { + this.extraClaims = extraClaims; + return this; + } + + /** + * Builder method to set the value of the {@code idp} claim. + * + * @param identityProvider the {@link IdentityProvider} to set + * @return the updated JsonWebToken object + * @since 3.2.0 + */ + public JsonWebToken identityProvider(IdentityProvider identityProvider) { + this.identityProvider = identityProvider; + return this; + } + + /** + * Builder method to set the value of the {@code permissions} claim. + * + * @param permissions the list of {@link Permission}s to set + * @return the updated JsonWebToken object + * @since 3.2.0 + */ + public JsonWebToken permissions(List permissions) { + this.permissions = permissions; + return this; + } + + /** + * Builder method to set the value of the {@code sub} claim. + * + * @param subject the subject of the token + * @return the updated JsonWebToken object + * @since 3.2.0 + */ + public JsonWebToken subject(String subject) { + this.subject = subject; + return this; + } + /** * Creates a new JWT for the specified principal. Token is signed using * the SecretKey with an HMAC 256 algorithm. @@ -132,9 +218,10 @@ public String createToken(final Principal principal, final List perm * @return a String representation of the generated token * @since 1.8.0 */ - public String createToken(final Principal principal, final List permissions, + public String createToken( + final Principal principal, + final List permissions, final IdentityProvider identityProvider) { - final int ttl = Config.getInstance().getPropertyAsInt(Config.AlpineKey.AUTH_JWT_TTL_SECONDS); return createToken(principal, permissions, identityProvider, ttl); } @@ -149,31 +236,43 @@ public String createToken(final Principal principal, final List perm * @return a String representation of the generated token * @since 3.0.0 */ - public String createToken(final Principal principal, final List permissions, final IdentityProvider identityProvider, final int ttlSeconds) { - final Date now = new Date(); - final JwtBuilder jwtBuilder = Jwts.builder(); - jwtBuilder.subject(principal.getName()); - jwtBuilder.issuer(ISSUER); - jwtBuilder.issuedAt(now); - jwtBuilder.expiration(addSeconds(now, ttlSeconds)); - if (permissions != null) { - jwtBuilder.claim("permissions", permissions.stream() - .map(Permission::getName) - .collect(Collectors.joining(",")) - ); - } - if (identityProvider != null) { - jwtBuilder.claim(IDENTITY_PROVIDER_CLAIM, identityProvider.name()); - } else { - if (principal instanceof LdapUser) { - jwtBuilder.claim(IDENTITY_PROVIDER_CLAIM, IdentityProvider.LDAP.name()); - } else if (principal instanceof OidcUser) { - jwtBuilder.claim(IDENTITY_PROVIDER_CLAIM, IdentityProvider.OPENID_CONNECT.name()); - } else { - jwtBuilder.claim(IDENTITY_PROVIDER_CLAIM, IdentityProvider.LOCAL.name()); - } - } - return jwtBuilder.signWith(key).compact(); + public String createToken( + final Principal principal, + final List permissions, + final IdentityProvider identityProvider, + final int ttlSeconds) { + return createToken(principal, permissions, identityProvider, ttlSeconds, extraClaims); + } + + /** + * Creates a new JWT for the specified principal. Token is signed using + * the SecretKey with an HMAC 256 algorithm. + * + * @param principal the Principal to create the token for + * @param permissions the effective list of permissions for the principal + * @param identityProvider the identity provider the principal was authenticated with. If null, it will be derived from principal + * @param ttlSeconds the token time-to-live in seconds + * @param extraClaims map of additional claims to include + * @return a String representation of the generated token + * @since 3.2.0 + */ + public String createToken( + final Principal principal, + final List permissions, + final IdentityProvider identityProvider, + final int ttlSeconds, + final Map extraClaims) { + this.ttl = ttlSeconds; + + return this.subject(principal.getName()) + .permissions(permissions) + .identityProvider(Objects.requireNonNullElse(identityProvider, switch (principal) { + case LdapUser user -> IdentityProvider.LDAP; + case OidcUser user -> IdentityProvider.OPENID_CONNECT; + default -> IdentityProvider.LOCAL; + })) + .extraClaims(extraClaims) + .build(); } /** @@ -185,9 +284,10 @@ public String createToken(final Principal principal, final List perm * @since 1.0.0 */ public String createToken(final Map claims) { - final JwtBuilder jwtBuilder = Jwts.builder(); - jwtBuilder.claims(claims); - return jwtBuilder.signWith(key).compact(); + return Jwts.builder() + .claims(claims) + .signWith(key) + .compact(); } /** @@ -202,9 +302,12 @@ public boolean validateToken(final String token) { try { final JwtParser jwtParser = Jwts.parser().verifyWith(key).build(); final Jws claims = jwtParser.parseSignedClaims(token); - this.subject = claims.getPayload().getSubject(); - this.expiration = claims.getPayload().getExpiration(); - this.identityProvider = IdentityProvider.forName(claims.getPayload().get(IDENTITY_PROVIDER_CLAIM, String.class)); + final Claims payload = claims.getPayload(); + + this.subject(payload.getSubject()) + .expiration(payload.getExpiration()) + .identityProvider(IdentityProvider.forName(payload.get(IDENTITY_PROVIDER_CLAIM, String.class))); + return true; } catch (SignatureException e) { LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Received token that did not pass signature verification"); @@ -216,6 +319,7 @@ public boolean validateToken(final String token) { } catch (UnsupportedJwtException | IllegalArgumentException e) { LOGGER.error(SecurityMarkers.SECURITY_FAILURE, e.getMessage()); } + return false; } @@ -229,7 +333,7 @@ public boolean validateToken(final String token) { private Date addSeconds(final Date date, final int seconds) { final Calendar cal = Calendar.getInstance(); cal.setTime(date); - cal.add(Calendar.SECOND, seconds); //minus number would decrement the seconds + cal.add(Calendar.SECOND, seconds); // minus number would decrement the seconds return cal.getTime(); } @@ -257,4 +361,20 @@ public IdentityProvider getIdentityProvider() { return identityProvider; } + /** + * Returns the permissions of the token. + * @return list of {@link Permission}s + */ + public List getPermissions() { + return permissions; + } + + /** + * Returns the extra claims of the token. + * @return map of claims + */ + public Map getExtraClaims() { + return extraClaims; + } + } diff --git a/alpine/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationCustomizer.java b/alpine/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationCustomizer.java new file mode 100644 index 0000000000..029d8ac012 --- /dev/null +++ b/alpine/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationCustomizer.java @@ -0,0 +1,35 @@ +/* + * This file is part of Alpine. + * + * 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) Steve Springett. All Rights Reserved. + */ +package alpine.server.auth; + +import com.nimbusds.openid.connect.sdk.claims.ClaimsSet; + +import alpine.model.OidcUser; + +public interface OidcAuthenticationCustomizer { + + OidcProfile createProfile(ClaimsSet claimsSet); + + boolean isProfileComplete(OidcProfile profile, boolean teamSyncEnabled); + + OidcProfile mergeProfiles(OidcProfile left, OidcProfile right); + + OidcUser onAuthenticationSuccess(OidcUser user, OidcProfile profile, String idToken, String accessToken); + +} diff --git a/alpine/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationService.java b/alpine/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationService.java index 72d84115d6..9a4d5988f0 100644 --- a/alpine/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationService.java +++ b/alpine/alpine-server/src/main/java/alpine/server/auth/OidcAuthenticationService.java @@ -24,12 +24,13 @@ import alpine.model.OidcUser; import alpine.persistence.AlpineQueryManager; import alpine.server.util.OidcUtil; -import com.nimbusds.openid.connect.sdk.claims.UserInfo; import jakarta.annotation.Nonnull; + import java.security.Principal; import java.util.List; import java.util.Objects; +import java.util.ServiceLoader; /** * @since 1.8.0 @@ -44,6 +45,7 @@ public class OidcAuthenticationService implements AuthenticationService { private final OidcUserInfoAuthenticator userInfoAuthenticator; private final String idToken; private final String accessToken; + private final OidcAuthenticationCustomizer customizer; /** * @param accessToken The access token acquired by authenticating with an IdP @@ -87,6 +89,14 @@ public OidcAuthenticationService(final String idToken, final String accessToken) this.userInfoAuthenticator = userInfoAuthenticator; this.idToken = idToken; this.accessToken = accessToken; + + String customizerClassName = Config.getInstance().getProperty(Config.AlpineKey.OIDC_AUTH_CUSTOMIZER); + this.customizer = ServiceLoader.load(OidcAuthenticationCustomizer.class) + .stream() + .filter(provider -> provider.type().getName().equals(customizerClassName)) + .map(ServiceLoader.Provider::get) + .findFirst() + .orElseThrow(IllegalStateException::new); } @Override @@ -127,12 +137,7 @@ public Principal authenticate() throws AlpineAuthenticationException { } final OidcProfileCreator profileCreator = claims -> { - final var profile = new OidcProfile(); - profile.setSubject(claims.getStringClaim(UserInfo.SUB_CLAIM_NAME)); - profile.setUsername(claims.getStringClaim(usernameClaimName)); - profile.setGroups(claims.getStringListClaim(teamsClaimName)); - profile.setEmail(claims.getStringClaim(UserInfo.EMAIL_CLAIM_NAME)); - return profile; + return customizer.createProfile(claims); }; OidcProfile idTokenProfile = null; @@ -140,7 +145,7 @@ public Principal authenticate() throws AlpineAuthenticationException { idTokenProfile = idTokenAuthenticator.authenticate(idToken, profileCreator); LOGGER.debug("ID token profile: " + idTokenProfile); - if (isProfileComplete(idTokenProfile, teamSyncEnabled)) { + if (customizer.isProfileComplete(idTokenProfile, teamSyncEnabled)) { LOGGER.debug("ID token profile is complete, proceeding to authenticate"); return authenticateInternal(idTokenProfile); } @@ -151,7 +156,7 @@ public Principal authenticate() throws AlpineAuthenticationException { userInfoProfile = userInfoAuthenticator.authenticate(accessToken, profileCreator); LOGGER.debug("UserInfo profile: " + userInfoProfile); - if (isProfileComplete(userInfoProfile, teamSyncEnabled)) { + if (customizer.isProfileComplete(userInfoProfile, teamSyncEnabled)) { LOGGER.debug("UserInfo profile is complete, proceeding to authenticate"); return authenticateInternal(userInfoProfile); } @@ -159,10 +164,10 @@ public Principal authenticate() throws AlpineAuthenticationException { OidcProfile mergedProfile = null; if (idTokenProfile != null && userInfoProfile != null) { - mergedProfile = mergeProfiles(idTokenProfile, userInfoProfile); + mergedProfile = customizer.mergeProfiles(idTokenProfile, userInfoProfile); LOGGER.debug("Merged profile: " + mergedProfile); - if (isProfileComplete(mergedProfile, teamSyncEnabled)) { + if (customizer.isProfileComplete(mergedProfile, teamSyncEnabled)) { LOGGER.debug("Merged profile is complete, proceeding to authenticate"); return authenticateInternal(mergedProfile); } @@ -182,21 +187,29 @@ private OidcUser authenticateInternal(final OidcProfile profile) throws AlpineAu LOGGER.debug("Assigning subject identifier " + profile.getSubject() + " to user " + user.getUsername()); user.setSubjectIdentifier(profile.getSubject()); user.setEmail(profile.getEmail()); - return qm.updateOidcUser(user); + + return customizer.onAuthenticationSuccess(qm.updateOidcUser(user), profile, idToken, accessToken); } else if (!user.getSubjectIdentifier().equals(profile.getSubject())) { LOGGER.error("Refusing to authenticate user " + user.getUsername() + ": subject identifier has changed (" + user.getSubjectIdentifier() + " to " + profile.getSubject() + ")"); throw new AlpineAuthenticationException(AlpineAuthenticationException.CauseType.INVALID_CREDENTIALS); } + if (!Objects.equals(user.getEmail(), profile.getEmail())) { LOGGER.debug("Updating email of user " + user.getUsername() + ": " + user.getEmail() + " -> " + profile.getEmail()); user.setEmail(profile.getEmail()); user = qm.updateOidcUser(user); } + if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { - return qm.synchronizeTeamMembership(user, profile.getGroups()); + return customizer.onAuthenticationSuccess( + qm.synchronizeTeamMembership(user, profile.getGroups()), + profile, + idToken, + accessToken); } - return user; + + return customizer.onAuthenticationSuccess(user, profile, idToken, accessToken); } else if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_USER_PROVISIONING)) { LOGGER.debug("The user (" + profile.getUsername() + ") authenticated successfully but the account has not been provisioned"); return autoProvision(qm, profile); @@ -207,25 +220,6 @@ private OidcUser authenticateInternal(final OidcProfile profile) throws AlpineAu } } - private boolean isProfileComplete(final OidcProfile profile, final boolean teamSyncEnabled) { - return profile.getSubject() != null - && profile.getUsername() != null - && (!teamSyncEnabled || (profile.getGroups() != null)); - } - - private OidcProfile mergeProfiles(final OidcProfile left, final OidcProfile right) { - final var profile = new OidcProfile(); - profile.setSubject(selectProfileClaim(left.getSubject(), right.getSubject())); - profile.setUsername(selectProfileClaim(left.getUsername(), right.getUsername())); - profile.setGroups(selectProfileClaim(left.getGroups(), right.getGroups())); - profile.setEmail(selectProfileClaim(left.getEmail(), right.getEmail())); - return profile; - } - - private T selectProfileClaim(final T left, final T right) { - return (left != null) ? left : right; - } - private OidcUser autoProvision(final AlpineQueryManager qm, final OidcProfile profile) { var user = new OidcUser(); user.setUsername(profile.getUsername()); @@ -235,13 +229,21 @@ private OidcUser autoProvision(final AlpineQueryManager qm, final OidcProfile pr if (config.getPropertyAsBoolean(Config.AlpineKey.OIDC_TEAM_SYNCHRONIZATION)) { LOGGER.debug("Synchronizing teams for user " + user.getUsername()); - return qm.synchronizeTeamMembership(user, profile.getGroups()); + return customizer.onAuthenticationSuccess( + qm.synchronizeTeamMembership(user, profile.getGroups()), + profile, + idToken, + accessToken); } final List defaultTeams = config.getPropertyAsList(Config.AlpineKey.OIDC_TEAMS_DEFAULT); if (!defaultTeams.isEmpty()) { LOGGER.debug("Assigning default teams %s to user %s".formatted(defaultTeams, user.getUsername())); - return qm.addUserToTeams(user, defaultTeams); + return customizer.onAuthenticationSuccess( + qm.addUserToTeams(user, defaultTeams), + profile, + idToken, + accessToken); } return user; diff --git a/alpine/alpine-server/src/main/java/alpine/server/auth/OidcProfile.java b/alpine/alpine-server/src/main/java/alpine/server/auth/OidcProfile.java index c09519ea63..423cf39a45 100644 --- a/alpine/alpine-server/src/main/java/alpine/server/auth/OidcProfile.java +++ b/alpine/alpine-server/src/main/java/alpine/server/auth/OidcProfile.java @@ -21,56 +21,69 @@ import java.util.List; +import net.minidev.json.JSONObject; + /** * @since 1.10.0 */ -class OidcProfile { +public class OidcProfile { - private String subject; - private String username; + private String subject, username, email; private List groups; - private String email; + private JSONObject customValues = new JSONObject(); - String getSubject() { + public String getSubject() { return subject; } - void setSubject(final String subject) { + public void setSubject(final String subject) { this.subject = subject; } - String getUsername() { + public String getUsername() { return username; } - void setUsername(final String username) { + public void setUsername(final String username) { this.username = username; } - List getGroups() { + public List getGroups() { return groups; } - void setGroups(final List groups) { + public void setGroups(final List groups) { this.groups = groups; } - String getEmail() { + public String getEmail() { return email; } - void setEmail(final String email) { + public void setEmail(final String email) { this.email = email; } + public Object getCustomValue(final String key) { + return customValues.get(key); + } + + public Object putCustomValue(final String key, final Object value) { + return customValues.put(key, value); + } + + public JSONObject getCustomValues() { + return customValues; + } + + public void setCustomValues(JSONObject customValues) { + this.customValues = customValues; + } + @Override public String toString() { - return "OidcProfile{" + - "subject='" + subject + '\'' + - ", username='" + username + '\'' + - ", groups=" + groups + - ", email='" + email + '\'' + - '}'; + return "%s{subject='%s', username='%s', groups=%s, email='%s', customValues=%s".formatted( + getClass().getSimpleName(), subject, username, groups, email, customValues); } } diff --git a/alpine/alpine-server/src/main/resources/META-INF/services/alpine.server.auth.OidcAuthenticationCustomizer b/alpine/alpine-server/src/main/resources/META-INF/services/alpine.server.auth.OidcAuthenticationCustomizer new file mode 100644 index 0000000000..ad6c46bc33 --- /dev/null +++ b/alpine/alpine-server/src/main/resources/META-INF/services/alpine.server.auth.OidcAuthenticationCustomizer @@ -0,0 +1 @@ +alpine.server.auth.DefaultOidcAuthenticationCustomizer \ No newline at end of file From 0b2af0d634548f6fac4134dfa8863c4e59cb7e0d Mon Sep 17 00:00:00 2001 From: Alexis Lamb Date: Mon, 28 Apr 2025 10:48:33 -0400 Subject: [PATCH 138/181] Update ConfigPropertyConstants.java Update "SBOM_PUSH" to "GITLAB_SBOM_PUSH_ENABLED" Signed-off-by: Alexis Lamb --- .../java/org/dependencytrack/model/ConfigPropertyConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 8b840d245a..76f8c38444 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -106,7 +106,7 @@ public enum ConfigPropertyConstants { ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio", ConfigPropertyAccessMode.READ_WRITE, true), NOTIFICATION_TEMPLATE_BASE_DIR("notification", "template.baseDir", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_BASE_DIRECTORY", System.getProperty("user.home")), PropertyType.STRING, "The base directory to use when searching for notification templates", ConfigPropertyAccessMode.READ_WRITE), NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED("notification", "template.default.override.enabled", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_ENABLED", "false"), PropertyType.BOOLEAN, "Flag to enable/disable override of default notification templates", ConfigPropertyAccessMode.READ_WRITE), - SBOM_PUSH("integrations", "sbom.push.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_SBOM_PUSH_ENABLED("integrations", "sbom.push.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_LDAP_SYNC_CADENCE("task-scheduler", "ldap.sync.cadence", "6", PropertyType.INTEGER, "Sync cadence (in hours) for LDAP", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_GHSA_MIRROR_CADENCE("task-scheduler", "ghsa.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for Github Security Advisories", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_OSV_MIRROR_CADENCE("task-scheduler", "osv.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for OSV database", ConfigPropertyAccessMode.READ_WRITE), From 1579965fb847509490e7ea70d863e2e900d77626 Mon Sep 17 00:00:00 2001 From: Alexis Lamb Date: Mon, 28 Apr 2025 18:02:02 -0400 Subject: [PATCH 139/181] Update ConfigPropertyConstants.java Sort by alphabetical order Signed-off-by: Alexis Lamb --- .../java/org/dependencytrack/model/ConfigPropertyConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 76f8c38444..b082c8ea2f 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -92,6 +92,7 @@ public enum ConfigPropertyConstants { GITLAB_GROUPS("integrations", "gitlab.groups", "[]", PropertyType.STRING, "JSON array of GitLab group names for which to create teams/roles", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), GITLAB_INCLUDE_ARCHIVED("integrations", "gitlab.include.archived", "false", PropertyType.BOOLEAN, "Flag to enable/disable syncing of archived GitLab projects", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_SBOM_PUSH_ENABLED("integrations", "sbom.push.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_TOPICS("integrations", "gitlab.topics", "[]", PropertyType.STRING, "JSON array of topics to include when syncing GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_APP_ID("integrations", "gitlab.app.id", null, PropertyType.STRING, "ID for the configured GitLab application", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_ENABLED("integrations", "defectdojo.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo integration", ConfigPropertyAccessMode.READ_WRITE), @@ -106,7 +107,6 @@ public enum ConfigPropertyConstants { ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio", ConfigPropertyAccessMode.READ_WRITE, true), NOTIFICATION_TEMPLATE_BASE_DIR("notification", "template.baseDir", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_BASE_DIRECTORY", System.getProperty("user.home")), PropertyType.STRING, "The base directory to use when searching for notification templates", ConfigPropertyAccessMode.READ_WRITE), NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED("notification", "template.default.override.enabled", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_ENABLED", "false"), PropertyType.BOOLEAN, "Flag to enable/disable override of default notification templates", ConfigPropertyAccessMode.READ_WRITE), - GITLAB_SBOM_PUSH_ENABLED("integrations", "sbom.push.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_LDAP_SYNC_CADENCE("task-scheduler", "ldap.sync.cadence", "6", PropertyType.INTEGER, "Sync cadence (in hours) for LDAP", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_GHSA_MIRROR_CADENCE("task-scheduler", "ghsa.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for Github Security Advisories", ConfigPropertyAccessMode.READ_WRITE), TASK_SCHEDULER_OSV_MIRROR_CADENCE("task-scheduler", "osv.mirror.cadence", "24", PropertyType.INTEGER, "Mirror cadence (in hours) for OSV database", ConfigPropertyAccessMode.READ_WRITE), From a7071ab5fba532b391c92fb2e63b2bf9822cf2ae Mon Sep 17 00:00:00 2001 From: Alexis Lamb Date: Wed, 30 Apr 2025 14:52:15 -0400 Subject: [PATCH 140/181] Update createRole API to create a new role with permissions (#22) Signed-off-by: e388459 --- .../persistence/QueryManager.java | 4 ++ .../persistence/RoleQueryManager.java | 13 +++++ .../resources/v1/RoleResource.java | 53 ++++++++++++++----- .../resources/v1/vo/CreateRoleRequest.java | 47 ++++++++++++++++ 4 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/vo/CreateRoleRequest.java diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 1d66f354ab..b33f18d68c 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -901,6 +901,10 @@ public List getRoles() { return getRoleQueryManager().getRoles(); } + public Role getRoleByName(String name) { + return getRoleQueryManager().getRoleByName(name); + } + public Role getRole(String uuid) { return getRoleQueryManager().getRole(uuid); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 4fda1b840a..f6feea4f3e 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -33,6 +33,8 @@ import org.dependencytrack.persistence.jdbi.JdbiFactory; import org.dependencytrack.persistence.jdbi.RoleDao; +import org.apache.commons.lang3.StringUtils; + import alpine.common.logging.Logger; import alpine.model.LdapUser; import alpine.model.ManagedUser; @@ -87,6 +89,17 @@ public List getRoles() { return query.executeList(); } + @Override + public Role getRoleByName(final String name) { + final String role = StringUtils.lowerCase(StringUtils.trimToNull(name)); + final Query query = pm.newQuery(Role.class) + .filter("name.toLowerCase().trim() == :name") + .setNamedParameters(Map.of("name", role)) + .range(0, 1); + + return executeAndCloseUnique(query); + } + @Override public Role getRole(final String uuid) { return getObjectByUuid(Role.class, uuid, Role.FetchGroup.ALL.name()); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 157fada21d..58ad312d64 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -33,7 +33,7 @@ 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.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; @@ -51,14 +51,20 @@ import org.dependencytrack.model.ProjectRole; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.persistence.jdbi.RoleDao; +import org.dependencytrack.resources.v1.vo.CreateRoleRequest; + import org.jdbi.v3.core.Handle; import org.owasp.security.logging.SecurityMarkers; +import alpine.model.Permission; import alpine.model.UserPrincipal; import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; import java.util.List; +import java.util.Map; + +import javax.jdo.Query; /** * JAX-RS resources for processing roles. @@ -131,22 +137,41 @@ public Response getRole( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Creates a new role", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

        ") + @Operation(summary = "Creates a new role", description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

        ") @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "The created role", - content = @Content(schema = @Schema(implementation = Role.class))), - @ApiResponse(responseCode = "401", description = "Unauthorized") + @ApiResponse(responseCode = "201", description = "The created role", content = @Content(schema = @Schema(implementation = Role.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "409", description = "The Role already exists"), }) @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE }) - public Response createRole(Role jsonRole) { - failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); - + public Response createRole(@Valid CreateRoleRequest request) { try (QueryManager qm = new QueryManager()) { - final Role role = qm.createRole(jsonRole.getName(), jsonRole.getPermissions().stream().toList()); + boolean roleExists = qm.getRoleByName(request.name()) != null; + + if (roleExists) { + return Response.status(Response.Status.CONFLICT) + .entity(String.format("Role '%s' already exists", request.name())) + .build(); + } + + List permissionNames = request.permissions() + .stream() + .map(Permissions::name) + .toList(); + + final Query query = qm.getPersistenceManager().newQuery(Permission.class) + .filter(":permissions.contains(name)") + .setNamedParameters(Map.of("permissions", permissionNames)) + .orderBy("name asc"); + + final List requestedPermissions; + try { + requestedPermissions = List.copyOf(query.executeList()); + } finally { + query.closeAll(); + } + + final Role role = qm.createRole(request.name(), requestedPermissions); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Role created: " + role.getName()); return Response.status(Response.Status.CREATED).entity(role).build(); @@ -239,7 +264,7 @@ public Response getUserRoles( LOGGER.info("No roles found for user: " + username); return Response.ok(List.of()).build(); } - + return Response.ok(roles).build(); } } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/CreateRoleRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/CreateRoleRequest.java new file mode 100644 index 0000000000..fb8c89f981 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/CreateRoleRequest.java @@ -0,0 +1,47 @@ +/* + * 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.resources.v1.vo; + +import java.util.Set; + +import org.dependencytrack.auth.Permissions; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import alpine.common.validation.RegexSequence; +import alpine.server.json.TrimmedStringDeserializer; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; + +public record CreateRoleRequest( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The username may only contain printable characters") + String name, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + Set permissions) { +} From 4b91b832d0a4df9b060f892f292a655b9df96954 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 5 May 2025 14:18:33 -0600 Subject: [PATCH 141/181] remove role permissions Signed-off-by: Allen Shearin --- .../org/dependencytrack/auth/Permissions.java | 10 --------- .../resources/v1/RoleResource.java | 22 ++++++++++--------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/auth/Permissions.java b/apiserver/src/main/java/org/dependencytrack/auth/Permissions.java index a8e124a6b8..982eefe1a7 100644 --- a/apiserver/src/main/java/org/dependencytrack/auth/Permissions.java +++ b/apiserver/src/main/java/org/dependencytrack/auth/Permissions.java @@ -61,11 +61,6 @@ public enum Permissions { 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"), - ROLE_MANAGEMENT("Allows the creation, modification, and deletion of roles"), - ROLE_MANAGEMENT_CREATE("Allows the creation of roles"), - ROLE_MANAGEMENT_READ("Allows reading of roles"), - ROLE_MANAGEMENT_UPDATE("Allows update of roles"), - ROLE_MANAGEMENT_DELETE("Allows the deletion of roles"), TAG_MANAGEMENT("Allows the modification and deletion of tags"), TAG_MANAGEMENT_DELETE("Allows the deletion of a tag"), VIEW_BADGES("Provides the ability to view badges"); @@ -116,11 +111,6 @@ public static class Constants { public static final String POLICY_MANAGEMENT_READ = "POLICY_MANAGEMENT_READ"; public static final String POLICY_MANAGEMENT_UPDATE = "POLICY_MANAGEMENT_UPDATE"; public static final String POLICY_MANAGEMENT_DELETE = "POLICY_MANAGEMENT_DELETE"; - public static final String ROLE_MANAGEMENT = "ROLE_MANAGEMENT"; - public static final String ROLE_MANAGEMENT_CREATE = "ROLE_MANAGEMENT_CREATE"; - public static final String ROLE_MANAGEMENT_READ = "ROLE_MANAGEMENT_READ"; - public static final String ROLE_MANAGEMENT_UPDATE = "ROLE_MANAGEMENT_UPDATE"; - public static final String ROLE_MANAGEMENT_DELETE = "ROLE_MANAGEMENT_DELETE"; public static final String TAG_MANAGEMENT = "TAG_MANAGEMENT"; public static final String TAG_MANAGEMENT_DELETE = "TAG_MANAGEMENT_DELETE"; public static final String VIEW_BADGES = "VIEW_BADGES"; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 58ad312d64..2f13c1638a 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -86,7 +86,7 @@ public class RoleResource extends AlpineResource { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Returns a list of all roles", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

        ") + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_READ

        ") @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -98,7 +98,7 @@ public class RoleResource extends AlpineResource { content = @Content(array = @ArraySchema(schema = @Schema(implementation = Role.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) - @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) public Response getRoles() { try (QueryManager qm = new QueryManager()) { return Response.ok(qm.getRoles()).header(TOTAL_COUNT_HEADER, qm.getCount(Role.class)).build(); @@ -110,7 +110,7 @@ public Response getRoles() { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Returns a specific role", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_READ

        ") + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_READ

        ") @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -119,7 +119,7 @@ public Response getRoles() { @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_READ }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) public Response getRole( @Parameter( description = "The UUID of the role to retrieve", @@ -137,13 +137,15 @@ public Response getRole( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Creates a new role", description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_CREATE

        ") + @Operation( + summary = "Creates a new role", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_CREATE

        ") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "The created role", content = @Content(schema = @Schema(implementation = Role.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "409", description = "The Role already exists"), }) - @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_CREATE }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_CREATE }) public Response createRole(@Valid CreateRoleRequest request) { try (QueryManager qm = new QueryManager()) { boolean roleExists = qm.getRoleByName(request.name()) != null; @@ -183,7 +185,7 @@ public Response createRole(@Valid CreateRoleRequest request) { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Updates a role's fields", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_UPDATE

        ") + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

        ") @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -192,7 +194,7 @@ public Response createRole(@Valid CreateRoleRequest request) { @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_UPDATE }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE }) public Response updateRole(Role jsonRole) { failOnValidationError(super.getValidator().validateProperty(jsonRole, "name")); @@ -212,13 +214,13 @@ public Response updateRole(Role jsonRole) { @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Deletes a role", - description = "

        Requires permission ROLE_MANAGEMENT or ROLE_MANAGEMENT_DELETE

        ") + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_DELETE

        ") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Role removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The role could not be found") }) - @PermissionRequired({ Permissions.Constants.ROLE_MANAGEMENT, Permissions.Constants.ROLE_MANAGEMENT_DELETE }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_DELETE }) public Response deleteRole(Role jsonRole) { try (QueryManager qm = new QueryManager()) { final Role role = qm.getObjectByUuid(Role.class, jsonRole.getUuid(), Role.FetchGroup.ALL.name()); From d70f18bfaf4d5f05f174c5e9d1aae554cedc37a9 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 5 May 2025 16:34:07 -0500 Subject: [PATCH 142/181] chore: remove unused imports Signed-off-by: Jonathan Howard --- .../persistence/DefaultObjectGenerator.java | 2 -- .../dependencytrack/persistence/QueryManager.java | 9 ++++----- .../persistence/RoleQueryManager.java | 15 +-------------- .../org/dependencytrack/tasks/GitLabSyncTask.java | 3 +-- 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index b9264582ce..3b934d4c80 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -21,7 +21,6 @@ import alpine.Config; import alpine.common.logging.Logger; import alpine.model.ConfigProperty; -import alpine.model.LdapUser; import alpine.model.ManagedUser; import alpine.model.Permission; import alpine.server.auth.PasswordService; @@ -33,7 +32,6 @@ import org.dependencytrack.common.ConfigKey; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; -import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter; diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index f46d27cdaa..e2ae08895b 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -74,6 +74,9 @@ import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; +import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; +import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; @@ -518,7 +521,7 @@ protected Set getRoleIds(final Principal principal, final Project project) default -> { return Collections.emptySet(); } - }; + } Query query = pm.newQuery(cls) .filter("project.id == :projectId && %s.contains(:principal)".formatted(usersField)) @@ -901,10 +904,6 @@ public List getRoles() { return getRoleQueryManager().getRoles(); } - public Role getRoleByName(String name) { - return getRoleQueryManager().getRoleByName(name); - } - public Role getRole(String uuid) { return getRoleQueryManager().getRole(uuid); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index b9e7cd1583..b8a840b286 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -34,8 +34,6 @@ import org.dependencytrack.persistence.jdbi.JdbiFactory; import org.dependencytrack.persistence.jdbi.RoleDao; -import org.apache.commons.lang3.StringUtils; - import alpine.common.logging.Logger; import alpine.model.LdapUser; import alpine.model.ManagedUser; @@ -90,17 +88,6 @@ public List getRoles() { return query.executeList(); } - @Override - public Role getRoleByName(final String name) { - final String role = StringUtils.lowerCase(StringUtils.trimToNull(name)); - final Query query = pm.newQuery(Role.class) - .filter("name.toLowerCase().trim() == :name") - .setNamedParameters(Map.of("name", role)) - .range(0, 1); - - return executeAndCloseUnique(query); - } - @Override public Role getRole(final String uuid) { return getObjectByUuid(Role.class, uuid, Role.FetchGroup.ALL.name()); @@ -110,7 +97,7 @@ public Role getRole(final String uuid) { public Role getRoleByName(final String name) { final String role = StringUtils.lowerCase(StringUtils.trimToNull(name)); final Query query = pm.newQuery(Role.class) - .filter("name == :name") + .filter("name.toLowerCase().trim() == :name") .setNamedParameters(Map.of("name", role)) .range(0, 1); diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index bce16f6306..69655346af 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -23,11 +23,10 @@ import alpine.event.framework.LoggableSubscriber; import alpine.model.ConfigProperty; import alpine.model.OidcUser; -import alpine.model.Permission; + import net.minidev.json.JSONArray; import net.minidev.json.JSONValue; -import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.GitLabSyncEvent; import org.dependencytrack.integrations.gitlab.GitLabClient; import org.dependencytrack.integrations.gitlab.GitLabSyncer; From 3c539380f76ce13dd1fe659bc83da2095392a1a6 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 5 May 2025 18:52:29 -0500 Subject: [PATCH 143/181] chore: remove unused imports Signed-off-by: Jonathan Howard --- .../persistence/QueryManager.java | 3 --- .../v1/vo/PermissionsSetRequest.java | 27 ------------------- 2 files changed, 30 deletions(-) delete mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index e2ae08895b..dbe0342baf 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -74,9 +74,6 @@ import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ProjectRole; -import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; -import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; -import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java deleted file mode 100644 index f2d5cc2712..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java +++ /dev/null @@ -1,27 +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.resources.v1.vo; - -import java.util.Set; - -import jakarta.validation.constraints.NotNull; - -public record PermissionsSetRequest(@NotNull Set permissions) { -} From 4b9465e1b7dba5768a89961fadfa0c9b39295309 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 7 May 2025 10:06:15 -0600 Subject: [PATCH 144/181] remove OBE files Signed-off-by: Allen Shearin --- .../model/validation/ValidUuidList.java | 43 ----------------- .../validation/ValidUuidListValidator.java | 46 ------------------- .../v1/vo/PermissionsSetRequest.java | 27 ----------- 3 files changed, 116 deletions(-) delete mode 100644 apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidList.java delete mode 100644 apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidListValidator.java delete mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java diff --git a/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidList.java b/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidList.java deleted file mode 100644 index 6b8d2daef3..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidList.java +++ /dev/null @@ -1,43 +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.model.validation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - -@Documented -@Constraint(validatedBy = ValidUuidListValidator.class) -@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) - -public @interface ValidUuidList { - String message() default "Invalid UUID"; - - Class[] groups() default {}; - - Class[] payload() default {}; - -} diff --git a/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidListValidator.java b/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidListValidator.java deleted file mode 100644 index 3c5683eb18..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/model/validation/ValidUuidListValidator.java +++ /dev/null @@ -1,46 +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.model.validation; - -import java.util.Set; -import java.util.UUID; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; - -public class ValidUuidListValidator implements ConstraintValidator> { - - @Override - public boolean isValid(final Set uuidList, final ConstraintValidatorContext validatorContext) { - if (uuidList == null || uuidList.isEmpty()) { - // null or empty sets are considered valid - return true; - } - - for (String uuidString : uuidList) { - try { - UUID.fromString(uuidString); - } catch (IllegalArgumentException e) { - return false; - } - } - return true; - } -} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java deleted file mode 100644 index f2d5cc2712..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/PermissionsSetRequest.java +++ /dev/null @@ -1,27 +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.resources.v1.vo; - -import java.util.Set; - -import jakarta.validation.constraints.NotNull; - -public record PermissionsSetRequest(@NotNull Set permissions) { -} From c7f54562258e5868e4c157e24010bb99451e0dbe Mon Sep 17 00:00:00 2001 From: lmphil <126618132+lmphil@users.noreply.github.com> Date: Wed, 7 May 2025 13:59:22 -0400 Subject: [PATCH 145/181] test: add RoleResource unit tests (#20) * test: add roleresource unit tests Signed-off-by: Philippe * correct method name Signed-off-by: Philippe * fix createRoleTest Signed-off-by: Philippe * add license header Signed-off-by: Philippe * Update apiserver/src/test/java/org/dependencytrack/ResourceTest.java Co-authored-by: Allen Shearin Signed-off-by: lmphil <126618132+lmphil@users.noreply.github.com> * fix formatting Signed-off-by: Allen Shearin --------- Signed-off-by: Philippe Signed-off-by: lmphil <126618132+lmphil@users.noreply.github.com> Signed-off-by: Allen Shearin Co-authored-by: Allen Shearin --- .../persistence/DefaultObjectGenerator.java | 6 + .../org/dependencytrack/ResourceTest.java | 1 + .../resources/v1/RoleResourceTest.java | 192 ++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 3b934d4c80..fa79b5dc9e 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -324,6 +324,12 @@ private List getPermissionsByName(List names) { return names.stream().map(PERMISSIONS_MAP::get).filter(Objects::nonNull).toList(); } + public void loadDefaultRoles() { + try (final var qm = new QueryManager()) { + loadDefaultRoles(qm); + } + } + /** * Loads the default Roles */ diff --git a/apiserver/src/test/java/org/dependencytrack/ResourceTest.java b/apiserver/src/test/java/org/dependencytrack/ResourceTest.java index 204af31684..147e4a5b43 100644 --- a/apiserver/src/test/java/org/dependencytrack/ResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/ResourceTest.java @@ -73,6 +73,7 @@ public abstract class ResourceTest { protected final String V1_PROJECT = "/v1/project"; protected final String V1_PROJECT_LATEST = "/v1/project/latest/"; protected final String V1_REPOSITORY = "/v1/repository"; + protected final String V1_ROLE = "/v1/role"; protected final String V1_SCAN = "/v1/scan"; protected final String V1_SEARCH = "/v1/search"; protected final String V1_TEAM = "/v1/team"; diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java new file mode 100644 index 0000000000..ccc9e220ba --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java @@ -0,0 +1,192 @@ +/* + * 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.resources.v1; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Role; +import org.dependencytrack.ResourceTest; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.persistence.DefaultObjectGenerator; +import org.dependencytrack.persistence.jdbi.JdbiFactory; +import org.dependencytrack.persistence.jdbi.RoleDao; +import org.glassfish.jersey.client.ClientProperties; +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.AuthenticationFilter; +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; + +public class RoleResourceTest extends ResourceTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(RoleResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); + + @Before + @Override + public void before() throws Exception { + super.before(); + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultRoles(); + } + + @Test + public void getRolesTest() { + Response response = jersey.target(V1_ROLE).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(4, json.size()); + for (int i = 0; i < json.size(); i++) { + Assert.assertNotNull(json.getJsonObject(i).getString("name")); + Assert.assertNotNull(json.getJsonObject(i).getString("uuid")); + } + } + + @Test + public void getRoleTest() { + List rolePermissions = new ArrayList(); + Role role = qm.createRole("ABC", rolePermissions); + Response response = jersey.target(V1_ROLE + "/" + role.getUuid()).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("ABC", json.getString("name")); + } + + @Test + public void getRoleByInvalidUuidTest() { + Response response = jersey.target(V1_ROLE + "/" + UUID.randomUUID()) + .request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + String body = getPlainTextBody(response); + Assert.assertEquals("The role could not be found.", body); + } + + @Test + public void createRoleTest() { + initializeWithPermissions(Permissions.ROLE_MANAGEMENT_CREATE); + + Response response = jersey.target(V1_ROLE).request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "name": "ABC", + "permissions": [] + } + """)); + Assert.assertEquals(201, response.getStatus()); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("ABC", json.getString("name")); + Assert.assertTrue(UuidUtil.isValidUUID(json.getString("uuid"))); + } + + @Test + public void updateRoleTest() { + List rolePermissions = new ArrayList(); + Role role = qm.createRole("My Role", rolePermissions); + role.setName("My New Role Name"); + Response response = jersey.target(V1_ROLE).request() + .header(X_API_KEY, apiKey) + .post(Entity.entity(role, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("My New Role Name", json.getString("name")); + } + + @Test + public void deleteRoleTest() { + List rolePermissions = new ArrayList(); + Role role = qm.createRole("My Role", rolePermissions); + Response response = jersey.target(V1_ROLE).request() + .header(X_API_KEY, apiKey) + .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true) // HACK + .method("DELETE", Entity.entity(role, MediaType.APPLICATION_JSON)); // HACK + // Hack: Workaround to https://github.com/eclipse-ee4j/jersey/issues/3798 + Assert.assertEquals(204, response.getStatus(), 0); + } + + @Test + public void getUserRolesTest() throws ParseException { + final var testProject = new Project(); + testProject.setId(1); + testProject.setName("test-project"); + testProject.setVersion("1.0.0"); + qm.persist(testProject); + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername("test-user"); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + qm.persist(testUser); + + final var expectedRole = new Role(); + expectedRole.setId(1); + expectedRole.setName("maintainer"); + qm.persist(expectedRole); + + JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addRoleToUser( + testUser.getClass(), + testUser.getId(), + testProject.getId(), + expectedRole.getId())); + + Response response = jersey.target(V1_ROLE + "/test-user/roles").request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + JsonArray json = parseJsonArray(response); + Assert.assertNotNull(json); + Assert.assertEquals(1, json.size()); + Assert.assertEquals("maintainer", json.getJsonObject(0).getJsonObject("role").getString("name")); + } + +} From b4429b016279e07e3fc5ddef6e727ffd176c285a Mon Sep 17 00:00:00 2001 From: lmphil <126618132+lmphil@users.noreply.github.com> Date: Wed, 7 May 2025 18:38:35 -0400 Subject: [PATCH 146/181] test: add unit tests for RoleQueryManager class (#19) * roleQueryManagerTest Signed-off-by: Philippe * update role query manager test Signed-off-by: Philippe * fix RoleQueryManagerTest and the USER_PROJECT_EFFECTIVE_PERMISSIONS view Signed-off-by: Philippe * add remaining RoleQueryManager tests Signed-off-by: Philippe * add TODO to failing test Signed-off-by: Philippe * fix formatting Signed-off-by: Allen Shearin * fix RoleQueryManagerTest.testGetRole Signed-off-by: Philippe --------- Signed-off-by: Philippe Signed-off-by: Allen Shearin Co-authored-by: Allen Shearin --- .../persistence/RoleQueryManager.java | 4 +- .../persistence/jdbi/RoleDao.java | 12 + .../persistence/RoleQueryManagerTest.java | 421 ++++++++++++++++++ 3 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 apiserver/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index f6feea4f3e..d854b38fe2 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -169,7 +169,7 @@ public List getUserProjectPermissions(final String username, final S final String projectIds = executeAndCloseList(projectsQuery).stream() .map(Project::getId) .map(String::valueOf) - .collect(Collectors.joining(", ", "'{", "}'")); + .collect(Collectors.joining(", ", "(", ")")); // language=SQL final var queryString = """ @@ -182,7 +182,7 @@ public List getUserProjectPermissions(final String username, final S upep."PERMISSION_NAME" FROM "USER_PROJECT_EFFECTIVE_PERMISSIONS" upep WHERE upep."%s" = :userId - AND upep."PROJECT_ID" = ANY(%s) + AND upep."PROJECT_ID" IN %s """.formatted(columnName, projectIds); final Query query = pm.newQuery(Query.SQL, queryString); diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 7990b5ebea..9db1cd4652 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -46,6 +46,18 @@ public interface RoleDao { """) int deleteRole(@Bind final long roleId); + @SqlUpdate(/* language=sql */ """ + INSERT INTO "ROLES_PERMISSIONS" + ("ROLE_ID", "PERMISSION_ID") + VALUES + (:roleId, :permissionId) + ON CONFLICT DO NOTHING + """) + @DefineNamedBindings + int addPermissionToRole( + @Bind long roleId, + @Bind long permissionId); + @SqlUpdate(/* language=sql */ """ <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> <#assign prefix = userClass.getSimpleName()?upper_case> diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java new file mode 100644 index 0000000000..142269eb8e --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java @@ -0,0 +1,421 @@ +/* + * 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.model.Project; +import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.model.Role; +import org.dependencytrack.persistence.jdbi.JdbiFactory; +import org.dependencytrack.persistence.jdbi.RoleDao; +import org.jdbi.v3.core.Jdbi; + +import alpine.model.ManagedUser; +import alpine.model.Permission; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.dependencytrack.PersistenceCapableTest; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RoleQueryManagerTest extends PersistenceCapableTest { + + private PostgreSQLContainer postgresContainer; + private Jdbi jdbi; + + @Before + public void setUp() { + System.setProperty("javax.jdo.PersistenceManagerFactoryClass", + "org.datanucleus.api.jdo.JDOPersistenceManagerFactory"); + + postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:11-alpine")); + postgresContainer.start(); + + jdbi = Jdbi.create( + postgresContainer.getJdbcUrl(), + postgresContainer.getUsername(), + postgresContainer.getPassword()); + } + + @After + public void tearDown() { + if (postgresContainer != null) { + postgresContainer.stop(); + } + } + + @Test + public void testCreateRole() { + final var readPermission = new Permission(); + readPermission.setId(1); + readPermission.setName("read"); + readPermission.setDescription("permission to read"); + qm.persist(readPermission); + + final var writePermission = new Permission(); + writePermission.setId(2); + writePermission.setName("write"); + writePermission.setDescription("permission to write"); + qm.persist(writePermission); + + List expectedPermissionsList = Arrays.asList( + readPermission, + writePermission); + + assertThat(qm.createRole("maintainer", expectedPermissionsList)).satisfies( + roleCreated -> assertThat(roleCreated.getName()).isEqualTo("maintainer")); + } + + @Test + public void testGetRoles() { + final var maintainerRole = new Role(); + maintainerRole.setId(1); + maintainerRole.setName("maintainer"); + qm.persist(maintainerRole); + + final var ownerRole = new Role(); + ownerRole.setId(2); + ownerRole.setName("owner"); + qm.persist(ownerRole); + + List expectedRoles = Arrays.asList( + maintainerRole, + ownerRole); + + List actualRoles = qm.getRoles(); + List actualRolesMutable = new ArrayList(); + for (Role r : actualRoles) { + actualRolesMutable.add(r); + } + + Assert.assertEquals(expectedRoles, actualRolesMutable); + } + + @Test + public void testGetRole() { + final var wrongRole = new Role(); + wrongRole.setId(1); + wrongRole.setName("maintainer"); + qm.persist(wrongRole); + + final var expectedRole = new Role(); + expectedRole.setId(2); + expectedRole.setName("owner"); + qm.persist(expectedRole); + + String expectedRoleUuid = expectedRole.getUuid().toString(); + + Role actualRole = qm.getRole(expectedRoleUuid); + + Assert.assertEquals(expectedRole, actualRole); + } + + @Test + public void testGetUserRoles() throws ParseException { + final var testProject = new Project(); + testProject.setId(1); + testProject.setName("test-project"); + testProject.setVersion("1.0.0"); + qm.persist(testProject); + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername("test-user"); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + qm.persist(testUser); + + final var expectedRole = new Role(); + expectedRole.setId(1); + expectedRole.setName("maintainer"); + qm.persist(expectedRole); + + JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addRoleToUser( + testUser.getClass(), + testUser.getId(), + testProject.getId(), + expectedRole.getId())); + + List actualRoles = qm.getUserRoles(testUser); + + Assert.assertEquals(actualRoles.size(), 1); + Assert.assertEquals(expectedRole.toString(), actualRoles.get(0).getRole().toString()); + } + + @Test + public void testGetUnassignedProjects() throws ParseException { + String testUserName = "test-user"; + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername(testUserName); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + qm.persist(testUser); + + final var maintainerRole = new Role(); + maintainerRole.setId(1); + maintainerRole.setName("maintainer"); + qm.persist(maintainerRole); + + final var unassignedProject1 = new Project(); + unassignedProject1.setId(1); + unassignedProject1.setName("test-project-1"); + qm.persist(unassignedProject1); + + final var assignedProject = new Project(); + assignedProject.setId(2); + assignedProject.setName("test-project-2"); + + qm.persist(assignedProject); + + final var unassignedProject2 = new Project(); + unassignedProject2.setId(3); + unassignedProject2.setName("test-project-3"); + qm.persist(unassignedProject2); + + JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addRoleToUser( + testUser.getClass(), + testUser.getId(), + assignedProject.getId(), + maintainerRole.getId())); + + List expectedProjects = Arrays.asList( + unassignedProject1, + unassignedProject2); + + List actualProjects = qm.getUnassignedProjects(testUserName); + + Assert.assertEquals(expectedProjects.toString(), actualProjects.toString()); + } + + @Test + public void testGetUnassignedRolePermissions() throws ParseException { + final var readPermission = new Permission(); + readPermission.setId(1); + readPermission.setName("read"); + readPermission.setDescription("permission to read"); + qm.persist(readPermission); + + final var writePermission = new Permission(); + writePermission.setId(2); + writePermission.setName("write"); + writePermission.setDescription("permission to write"); + qm.persist(writePermission); + + final var partyPermission = new Permission(); + partyPermission.setId(3); + partyPermission.setName("party"); + partyPermission.setDescription("permission to party"); + qm.persist(partyPermission); + + List expectedPermissionsList = Arrays.asList( + readPermission, + writePermission); + + Set allPermissions = new HashSet<>(Arrays.asList( + writePermission, + writePermission, + partyPermission)); + + final var assistantRegionalManagerRole = new Role(); + assistantRegionalManagerRole.setId(1); + assistantRegionalManagerRole.setName("maintainer"); + assistantRegionalManagerRole.setPermissions(allPermissions); + qm.persist(assistantRegionalManagerRole); + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername("test-user"); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + testUser.setPermissions(expectedPermissionsList); + qm.persist(testUser); + + List actualPermissions = qm.getUnassignedRolePermissions(assistantRegionalManagerRole); + + Assert.assertEquals(actualPermissions.size(), 1); + Assert.assertEquals(expectedPermissionsList.get(0), actualPermissions.get(0)); + } + + @Test + public void testUpdateRole() { + + final var maintainerRole = new Role(); + maintainerRole.setId(1); + maintainerRole.setName("maintainer"); + qm.persist(maintainerRole); + + Role actualRole = qm.updateRole(maintainerRole); + + Assert.assertEquals(maintainerRole, actualRole); + + // TODO: Check requirements of `updateRole`. + } + + @Test + public void testGetUserProjectPermissions() throws ParseException { + final var testProject = new Project(); + testProject.setId(1); + testProject.setName("test-project"); + testProject.setVersion("1.0.0"); + qm.persist(testProject); + + final var readPermission = new Permission(); + readPermission.setId(1); + readPermission.setName("read"); + readPermission.setDescription("permission to read"); + qm.persist(readPermission); + + final var writePermission = new Permission(); + writePermission.setId(2); + writePermission.setName("write"); + writePermission.setDescription("permission to write"); + qm.persist(writePermission); + + List expectedPermissionsList = Arrays.asList( + readPermission, + writePermission); + + Set expectedPermissions = new HashSet<>(expectedPermissionsList); + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername("test-user"); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + testUser.setPermissions(expectedPermissionsList); + qm.persist(testUser); + + final var maintainerRole = new Role(); + maintainerRole.setId(1); + maintainerRole.setName("maintainer"); + maintainerRole.setPermissions(expectedPermissions); + qm.persist(maintainerRole); + + JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addPermissionToRole( + maintainerRole.getId(), + readPermission.getId())); + + JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addPermissionToRole( + maintainerRole.getId(), + writePermission.getId())); + + JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addRoleToUser( + testUser.getClass(), + testUser.getId(), + testProject.getId(), + maintainerRole.getId())); + + List actualPermissions = qm.getUserProjectPermissions("test-user", "test-project"); + List actualPermissionsSorted = new ArrayList(); + for (Permission p : actualPermissions) { + actualPermissionsSorted.add(p); + } + Collections.sort(actualPermissionsSorted, Comparator.comparing(Permission::getId)); + + Assert.assertEquals(expectedPermissionsList, actualPermissionsSorted); + } + + @Test + public void testAddRoleToUser() throws ParseException { + final var testProject = new Project(); + testProject.setId(1); + testProject.setName("test-project"); + testProject.setVersion("1.0.0"); + qm.persist(testProject); + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername("test-user"); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + qm.persist(testUser); + + final var maintainerRole = new Role(); + maintainerRole.setId(1); + maintainerRole.setName("maintainer"); + qm.persist(maintainerRole); + + qm.addRoleToUser(testUser, maintainerRole, testProject); + } + + @Test + public void testRemoveRoleFromUser() throws ParseException { + final var testProject = new Project(); + testProject.setId(1); + testProject.setName("test-project"); + testProject.setVersion("1.0.0"); + qm.persist(testProject); + + final var testUser = new ManagedUser(); + testUser.setFullname("test user created for testing"); + testUser.setId(1); + testUser.setUsername("test-user"); + DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); + testUser.setLastPasswordChange(dateFormatter.parse("20250324")); + testUser.setPassword("password"); + qm.persist(testUser); + + final var maintainerRole = new Role(); + maintainerRole.setId(1); + maintainerRole.setName("maintainer"); + qm.persist(maintainerRole); + + JdbiFactory.withJdbiHandle( + handle -> handle.attach(RoleDao.class).addRoleToUser( + testUser.getClass(), + testUser.getId(), + testProject.getId(), + maintainerRole.getId())); + + Assert.assertTrue(qm.removeRoleFromUser(testUser, maintainerRole, testProject)); + } +} From 479285e65c72765d0be7aa8ee1be0bef0298d658 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Fri, 9 May 2025 13:20:49 -0500 Subject: [PATCH 147/181] refactor: consolidate user tables Signed-off-by: Jonathan Howard --- .../persistence/AlpineQueryManager.java | 9 +- .../dependencytrack/model/ProjectRole.java | 180 ++--------- .../persistence/QueryManager.java | 35 +-- .../persistence/RoleQueryManager.java | 23 +- .../persistence/jdbi/RoleDao.java | 74 ++--- .../jdbi/mapping/ProjectRoleRowMapper.java | 36 ++- .../resources/v1/AccessControlResource.java | 12 +- .../resources/v1/PermissionResource.java | 1 + .../resources/v1/RoleResource.java | 8 +- .../resources/v1/UserResource.java | 42 +-- .../main/resources/META-INF/persistence.xml | 4 +- .../migration/changelog-v5.6.0-roles.xml | 294 ++++-------------- .../persistence/RoleQueryManagerTest.java | 11 +- .../resources/v1/RoleResourceTest.java | 3 +- 14 files changed, 191 insertions(+), 541 deletions(-) diff --git a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java index 840fef2b37..eee19833be 100644 --- a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java +++ b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Set; +import org.datanucleus.store.rdbms.query.JDOQLQuery; + /** * This QueryManager provides a concrete extension of {@link AbstractAlpineQueryManager} by * providing methods that operate on the default Alpine models such as ManagedUser and Team. @@ -537,8 +539,11 @@ public List getManagedUsers() { * @since 1.0.0 */ public User getUser(String username) { - final Query query = pm.newQuery(User.class, "username == :username"); - query.setParameters(username); + final Query query = pm.newQuery(User.class) + .filter("username == :username") + .setNamedParameters(Map.of("username", username)) + .extension(JDOQLQuery.EXTENSION_CANDIDATE_DONT_RESTRICT_DISCRIMINATOR, true); + return executeAndCloseUnique(query); } diff --git a/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java b/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java index f5f25e8e61..05dfccdd99 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ProjectRole.java @@ -20,9 +20,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; +import alpine.model.User; import java.io.Serializable; import java.util.ArrayList; @@ -36,7 +34,6 @@ import javax.jdo.annotations.Extension; import javax.jdo.annotations.FetchGroup; import javax.jdo.annotations.Order; -import javax.jdo.annotations.PersistenceAware; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.Unique; @@ -47,9 +44,15 @@ * @author Jonathan Howard * @since 5.6.0 */ -@PersistenceAware +@PersistenceCapable(table = "USERS_PROJECTS_ROLES") @JsonInclude(JsonInclude.Include.NON_NULL) -public abstract class ProjectRole implements Serializable { +@Unique(name = "USERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "users", "project", "role" }) +@FetchGroup(name = "ALL", members = { + @Persistent(name = "role"), + @Persistent(name = "project"), + @Persistent(name = "users") +}) +public class ProjectRole implements Serializable { @Persistent(defaultFetchGroup = "true") @Column(name = "ROLE_ID", allowsNull = "false") @@ -59,6 +62,27 @@ public abstract class ProjectRole implements Serializable { @Column(name = "PROJECT_ID", allowsNull = "false") private Project project; + @Persistent(defaultFetchGroup = "true") + @Column(name = "USER_ID", allowsNull = "false") + @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) + private List users; + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public void addUsers(User... users) { + this.users = Objects.requireNonNullElse(this.users, new ArrayList()); + this.users = Stream.concat(this.users.stream(), Arrays.stream(users)) + .distinct() + .sorted(Comparator.comparing(User::getUsername)) + .toList(); + } + public Role getRole() { return role; } @@ -75,148 +99,4 @@ public void setProject(Project project) { this.project = project; } - /** - * Model for associating a role on a given project with LDAP users. - * - * @author Allen Shearin - * @since 5.6.0 - */ - @PersistenceCapable(table = "LDAPUSERS_PROJECTS_ROLES") - @Unique(name = "LDAPUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "ldapUsers", "project", "role" }) - @FetchGroup(name = "ALL", members = { - @Persistent(name = "role"), - @Persistent(name = "project"), - @Persistent(name = "ldapUsers") - }) - @JsonInclude(JsonInclude.Include.NON_NULL) - public static class LdapUserProjectRole extends ProjectRole { - - private static final long serialVersionUID = 6018553054343647649L; - - /** - * Defines JDO fetch groups for this class. - */ - public enum FetchGroup { - ALL - } - - @Persistent(defaultFetchGroup = "true") - @Column(name = "LDAPUSER_ID", allowsNull = "false") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List ldapUsers; - - public List getLdapUsers() { - return ldapUsers; - } - - public void setLdapUsers(List ldapUsers) { - this.ldapUsers = ldapUsers; - } - - public void addLdapUsers(LdapUser... ldapUsers) { - this.ldapUsers = Objects.requireNonNullElse(this.ldapUsers, new ArrayList()); - this.ldapUsers = Stream.concat(this.ldapUsers.stream(), Arrays.stream(ldapUsers)) - .distinct() - .sorted(Comparator.comparing(LdapUser::getUsername)) - .toList(); - } - - } - - /** - * Model for associating a role on a given project with managed users. - * - * @author Allen Shearin - * @since 5.6.0 - */ - @PersistenceCapable(table = "MANAGEDUSERS_PROJECTS_ROLES") - @Unique(name = "MANAGEDUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "managedUsers", "project", "role" }) - @FetchGroup(name = "ALL", members = { - @Persistent(name = "role"), - @Persistent(name = "project"), - @Persistent(name = "managedUsers") - }) - @JsonInclude(JsonInclude.Include.NON_NULL) - public static class ManagedUserProjectRole extends ProjectRole { - - private static final long serialVersionUID = -380122087527236991L; - - /** - * Defines JDO fetch groups for this class. - */ - public enum FetchGroup { - ALL - } - - @Persistent(defaultFetchGroup = "true") - @Column(name = "MANAGEDUSER_ID", allowsNull = "false") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List managedUsers; - - public List getManagedUsers() { - return managedUsers; - } - - public void setManagedUsers(List managedUsers) { - this.managedUsers = managedUsers; - } - - public void addManagedUsers(ManagedUser... managedUsers) { - this.managedUsers = Objects.requireNonNullElse(this.managedUsers, new ArrayList()); - this.managedUsers = Stream.concat(this.managedUsers.stream(), Arrays.stream(managedUsers)) - .distinct() - .sorted(Comparator.comparing(ManagedUser::getUsername)) - .toList(); - } - - } - - /** - * Model for associating a role on a given project with OIDC users. - * - * @author Allen Shearin - * @since 5.6.0 - */ - @PersistenceCapable(table = "OIDCUSERS_PROJECTS_ROLES") - @Unique(name = "OIDCUSERS_PROJECTS_ROLES_COMPOSITE_IDX", members = { "oidcUsers", "project", "role" }) - @FetchGroup(name = "ALL", members = { - @Persistent(name = "role"), - @Persistent(name = "project"), - @Persistent(name = "oidcUsers") - }) - @JsonInclude(JsonInclude.Include.NON_NULL) - public static class OidcUserProjectRole extends ProjectRole { - - private static final long serialVersionUID = -5029209056240375886L; - - /** - * Defines JDO fetch groups for this class. - */ - public enum FetchGroup { - ALL - } - - @Persistent(defaultFetchGroup = "true") - @Column(name = "OIDCUSER_ID", allowsNull = "false") - @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "username ASC")) - private List oidcUsers; - - public List getOidcUsers() { - return oidcUsers; - } - - public void setOidcUsers(List oidcUsers) { - this.oidcUsers = oidcUsers; - } - - public void addOidcUsers(OidcUser... oidcUsers) { - this.oidcUsers = Objects.requireNonNullElse(this.oidcUsers, new ArrayList()); - this.oidcUsers = Stream.concat(this.oidcUsers.stream(), Arrays.stream(oidcUsers)) - .distinct() - .sorted(Comparator.comparing(OidcUser::getUsername)) - .toList(); - } - - } - } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 8d7213f58f..00d26e4914 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -499,29 +499,8 @@ public QueryManager withL2CacheDisabled() { * @return A {@link Set} of {@link ProjectRole} IDs */ protected Set getRoleIds(final Principal principal, final Project project) { - String usersField; - Class cls; - - switch (principal) { - case LdapUser ldapUser -> { - usersField = "ldapUsers"; - cls = ProjectRole.LdapUserProjectRole.class; - } - case ManagedUser managedUser -> { - usersField = "managedUsers"; - cls = ProjectRole.ManagedUserProjectRole.class; - } - case OidcUser oidcUser -> { - usersField = "oidcUsers"; - cls = ProjectRole.OidcUserProjectRole.class; - } - default -> { - return Collections.emptySet(); - } - }; - - Query query = pm.newQuery(cls) - .filter("project.id == :projectId && %s.contains(:principal)".formatted(usersField)) + final Query query = pm.newQuery(ProjectRole.class) + .filter("project.id == :projectId && users.contains(:principal)") .setNamedParameters(Map.ofEntries( Map.entry("principal", principal), Map.entry("projectId", project.getId()))); @@ -1192,15 +1171,15 @@ public synchronized RepositoryMetaComponent synchronizeRepositoryMetaComponent(f return getRepositoryQueryManager().synchronizeRepositoryMetaComponent(transientRepositoryMetaComponent); } - public boolean addRoleToUser(UserPrincipal principal, Role role, Project project){ - return getRoleQueryManager().addRoleToUser(principal, role, project); + public boolean addRoleToUser(User user, Role role, Project project){ + return getRoleQueryManager().addRoleToUser(user, role, project); } public List getUnassignedProjects(final String username) { return getRoleQueryManager().getUnassignedProjects(username); } - public List getUnassignedProjects(final UserPrincipal user) { + public List getUnassignedProjects(final User user) { return getRoleQueryManager().getUnassignedProjects(user); } @@ -1208,7 +1187,7 @@ public List getUnassignedRolePermissions(final Role role) { return getRoleQueryManager().getUnassignedRolePermissions(role); } - public List getUserRoles(final UserPrincipal user) { + public List getUserRoles(final User user) { return getRoleQueryManager().getUserRoles(user); } @@ -1216,7 +1195,7 @@ public List getUserProjectPermissions(final String username, final S return getRoleQueryManager().getUserProjectPermissions(username, projectName); } - public boolean removeRoleFromUser(final UserPrincipal user, final Role role, final Project project) { + public boolean removeRoleFromUser(final User user, final Role role, final Project project) { return getRoleQueryManager().removeRoleFromUser(user, role, project); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index d854b38fe2..fbc5c7a247 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -40,7 +40,7 @@ import alpine.model.ManagedUser; import alpine.model.OidcUser; import alpine.model.Permission; -import alpine.model.UserPrincipal; +import alpine.model.User; import alpine.resources.AlpineRequest; final class RoleQueryManager extends QueryManager implements IQueryManager { @@ -106,19 +106,18 @@ public Role getRole(final String uuid) { } @Override - public List getUserRoles(final UserPrincipal user) { + public List getUserRoles(final User user) { return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class) - .getUserRoles(user.getClass(), user.getUsername())); + .getUserRoles(user.getUsername())); } public List getUnassignedProjects(final String username) { - return getUnassignedProjects(getUserPrincipal(username)); + return getUnassignedProjects(getUser(username)); } - public List getUnassignedProjects(final UserPrincipal user) { - return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).getUserUnassignedProjects( - user.getClass(), - user.getUsername())); + public List getUnassignedProjects(final User user) { + return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class) + .getUserUnassignedProjects(user.getUsername())); } public List getUnassignedRolePermissions(final Role role) { @@ -150,7 +149,7 @@ public Role updateRole(final Role transientRole) { @Override public List getUserProjectPermissions(final String username, final String projectName) { - final UserPrincipal user = getUserPrincipal(username); + final User user = getUser(username); final String columnName; switch (user) { @@ -199,19 +198,17 @@ public List getUserProjectPermissions(final String username, final S } @Override - public boolean addRoleToUser(final UserPrincipal user, final Role role, final Project project) { + public boolean addRoleToUser(final User user, final Role role, final Project project) { return JdbiFactory.withJdbiHandle( handle -> handle.attach(RoleDao.class).addRoleToUser( - user.getClass(), user.getId(), project.getId(), role.getId())) == 1; } @Override - public boolean removeRoleFromUser(final UserPrincipal user, final Role role, final Project project) { + public boolean removeRoleFromUser(final User user, final Role role, final Project project) { return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class).removeRoleFromUser( - user.getClass(), user.getId(), project.getName(), role.getId())) > 0; diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 9db1cd4652..ca3bc84b73 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -27,13 +27,9 @@ import org.jdbi.v3.sqlobject.config.RegisterFieldMapper; import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.Bind; -import org.jdbi.v3.sqlobject.customizer.Define; -import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; -import alpine.model.UserPrincipal; - /** * @since 5.6.0 */ @@ -53,33 +49,21 @@ public interface RoleDao { (:roleId, :permissionId) ON CONFLICT DO NOTHING """) - @DefineNamedBindings - int addPermissionToRole( - @Bind long roleId, - @Bind long permissionId); + int addPermissionToRole(@Bind long roleId, @Bind long permissionId); @SqlUpdate(/* language=sql */ """ - <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = userClass.getSimpleName()?upper_case> - INSERT INTO "${prefix}S_PROJECTS_ROLES" - ("${prefix}_ID", "PROJECT_ID", "ROLE_ID") + INSERT INTO "USERS_PROJECTS_ROLES" + ("USER_ID", "PROJECT_ID", "ROLE_ID") VALUES (:userId, :projectId, :roleId) ON CONFLICT DO NOTHING """) - @DefineNamedBindings - int addRoleToUser( - @Define Class userClass, - @Bind long userId, - @Bind long projectId, - @Bind long roleId); + int addRoleToUser(@Bind long userId, @Bind long projectId, @Bind long roleId); @SqlUpdate(/* language=sql */ """ - <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = userClass.getSimpleName()?upper_case> DELETE - FROM "${prefix}S_PROJECTS_ROLES" - WHERE "${prefix}_ID" = :userId + FROM "USERS_PROJECTS_ROLES" + WHERE "USER_ID" = :userId AND "ROLE_ID" = :roleId AND "PROJECT_ID" IN ( SELECT "ID" @@ -87,52 +71,42 @@ int addRoleToUser( WHERE "NAME" = :projectName ) """) - @DefineNamedBindings - int removeRoleFromUser( - @Define Class userClass, - @Bind long userId, - @Bind String projectName, - @Bind long roleId); + int removeRoleFromUser(@Bind long userId, @Bind String projectName, @Bind long roleId); @SqlQuery(/* language=sql */ """ - <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = userClass.getSimpleName()?upper_case> SELECT - p."ID" AS "PROJECT_ID", - p."NAME" AS "PROJECT_NAME", - p."UUID" AS "PROJECT_UUID", - r."ID" AS "ROLE_ID", - r."NAME" AS "ROLE_NAME", - r."UUID" AS "ROLE_UUID", - u."ID" AS "${prefix}_ID" + p."ID" AS "PROJECT_ID", + p."NAME" AS "PROJECT_NAME", + p."UUID" AS "PROJECT_UUID", + r."ID" AS "ROLE_ID", + r."NAME" AS "ROLE_NAME", + r."UUID" AS "ROLE_UUID", + u."ID" AS "USER_ID", + u."USERNAME" AS "USER_NAME", + u."TYPE" AS "USER_TYPE" FROM "PROJECT" p - INNER JOIN "${prefix}S_PROJECTS_ROLES" pr + INNER JOIN "USERS_PROJECTS_ROLES" pr ON pr."PROJECT_ID" = p."ID" - INNER JOIN "${prefix}" u - ON u."ID" = pr."${prefix}_ID" + INNER JOIN "USER" u + ON u."ID" = pr."USER_ID" INNER JOIN "ROLE" r ON r."ID" = pr."ROLE_ID" WHERE u."USERNAME" = :username """) @RegisterRowMapper(ProjectRoleRowMapper.class) - @DefineNamedBindings - List getUserRoles(@Define Class userClass, @Bind String username); + List getUserRoles(@Bind String username); @SqlQuery(/* language=sql */ """ - <#-- @ftlvariable name="user" type="alpine.model.UserPrincipal" --> - <#assign prefix = userClass.getSimpleName()?upper_case> SELECT p."ID", p."NAME", p."UUID" FROM "PROJECT" p - LEFT JOIN "${prefix}S_PROJECTS_ROLES" pr + LEFT JOIN "USERS_PROJECTS_ROLES" pr ON pr."PROJECT_ID" = p."ID" - LEFT JOIN "${prefix}" u - ON u."ID" = pr."${prefix}_ID" + LEFT JOIN "USER" u + ON u."ID" = pr."USER_ID" WHERE u."USERNAME" != :username OR u."USERNAME" IS NULL """) @RegisterFieldMapper(Project.class) - List getUserUnassignedProjects( - @Define Class userClass, - @Bind String username); + List getUserUnassignedProjects(@Bind String username); } diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java index 013c5efade..d6bc2047c9 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRoleRowMapper.java @@ -18,11 +18,13 @@ */ package org.dependencytrack.persistence.jdbi.mapping; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; +import alpine.model.User; + import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectRole; -import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; -import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; -import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; import org.dependencytrack.model.Role; import org.jdbi.v3.core.mapper.RowMapper; @@ -32,26 +34,30 @@ import java.sql.SQLException; import java.util.UUID; -import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.hasColumn; import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; public class ProjectRoleRowMapper implements RowMapper { public ProjectRole map(final ResultSet resultSet, final StatementContext ctx) throws SQLException { - final ProjectRole projectRole; - - switch (resultSet) { - case ResultSet rs when hasColumn(rs, "LDAPUSER_ID") -> projectRole = new LdapUserProjectRole(); - case ResultSet rs when hasColumn(rs, "MANAGEDUSER_ID") -> projectRole = new ManagedUserProjectRole(); - case ResultSet rs when hasColumn(rs, "OIDCUSER_ID") -> projectRole = new OidcUserProjectRole(); - default -> { - return null; - } - } - + final ProjectRole projectRole = new ProjectRole(); projectRole.setProject(new Project()); projectRole.setRole(new Role()); + final String type = resultSet.getString("USER_TYPE"); + final User user = switch (type) { + case "LDAP" -> new LdapUser(); + case "MANAGED" -> new ManagedUser(); + case "OIDC" -> new OidcUser(); + default -> null; + }; + + if (user == null) + return projectRole; + + maybeSet(resultSet, "USER_ID", ResultSet::getLong, user::setId); + maybeSet(resultSet, "USER_NAME", ResultSet::getString, user::setUsername); + projectRole.addUsers(user); + maybeSet(resultSet, "PROJECT_ID", ResultSet::getLong, projectRole.getProject()::setId); maybeSet(resultSet, "PROJECT_NAME", ResultSet::getString, projectRole.getProject()::setName); maybeSet(resultSet, "PROJECT_UUID", ResultSet::getString, value -> { diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java index b95002fc38..a9cda5498b 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/AccessControlResource.java @@ -20,7 +20,7 @@ import alpine.common.logging.Logger; import alpine.model.Team; -import alpine.model.UserPrincipal; +import alpine.model.User; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -137,19 +137,17 @@ public Response retrieveUserProjects( @Parameter(description = "The username to retrieve projects for", required = true) @PathParam("username") String username) { try (QueryManager qm = new QueryManager()) { - UserPrincipal principal = qm.getUserPrincipal(username); + User user = qm.getUser(username); - if (principal == null) { + if (user == null) return Response.status(Response.Status.NOT_FOUND).build(); - } try (final Handle jdbiHandle = openJdbiHandle()) { var dao = jdbiHandle.attach(RoleDao.class); - List projects = dao.getUserUnassignedProjects(principal.getClass(), principal.getUsername()); + List projects = dao.getUserUnassignedProjects(user.getUsername()); - if (projects == null || projects.isEmpty()) { + if (projects == null || projects.isEmpty()) return Response.noContent().build(); - } return Response.ok(projects).header(TOTAL_COUNT_HEADER, projects.size()).build(); } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 49f1b831a5..9b38fdbec7 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -34,6 +34,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.Role; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.TeamPermissionsSetRequest; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java index 2f13c1638a..a639cbf4fc 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/RoleResource.java @@ -57,7 +57,7 @@ import org.owasp.security.logging.SecurityMarkers; import alpine.model.Permission; -import alpine.model.UserPrincipal; +import alpine.model.User; import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; @@ -255,13 +255,13 @@ public Response deleteRole(Role jsonRole) { public Response getUserRoles( @Parameter(description = "A valid username", required = true) @PathParam("username") String username) { try (QueryManager qm = new QueryManager()) { - UserPrincipal principal = qm.getUserPrincipal(username); - if (principal == null) { + User user = qm.getUser(username); + if (user == null) { LOGGER.warn("User not found: " + username); return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); } - List roles = qm.getUserRoles(principal); + List roles = qm.getUserRoles(user); if (roles == null || roles.isEmpty()) { LOGGER.info("No roles found for user: " + username); return Response.ok(List.of()).build(); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 3e2072bebf..081bb737ea 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -865,7 +865,7 @@ private UserSubject buildUserSubject(final String username, final String email) @ApiResponse( responseCode = "200", description = "Updated user with a specific role", - content = @Content(schema = @Schema(implementation = UserPrincipal.class)) + content = @Content(schema = @Schema(implementation = User.class)) ), @ApiResponse(responseCode = "304", description = "The user has already been assigned to this role."), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -882,23 +882,23 @@ public Response addRoleToUser( if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); - UserPrincipal principal = qm.getUserPrincipal(username); - if (principal == null) + User user = qm.getUser(username); + if (user == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); Project project = qm.getProject(roleProjectRequest.projectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); - final boolean modified = qm.addRoleToUser(principal, role, project); + final boolean modified = qm.addRoleToUser(user, role, project); if (!modified) return Response.notModified().entity("The user is already a member of the specified role.").build(); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added role membership for: %s / role: %s / project: %s" - .formatted(principal.getName(), role.getName(), project.getName())); + .formatted(user.getName(), role.getName(), project.getName())); - return Response.ok(principal).build(); + return Response.ok(user).build(); } } @@ -914,7 +914,7 @@ public Response addRoleToUser( @ApiResponse( responseCode = "200", description = "Updated user with a specific role removed", - content = @Content(schema = @Schema(implementation = UserPrincipal.class))), + content = @Content(schema = @Schema(implementation = User.class))), @ApiResponse(responseCode = "204", description = "The role has been successfully removed from the user"), @ApiResponse(responseCode = "304", description = "The user is not a member of the specified role"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -929,22 +929,22 @@ public Response removeRoleFromUser( if (role == null) return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); - UserPrincipal principal = qm.getUserPrincipal(username); - if (principal == null) + User user = qm.getUser(username); + if (user == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); Project project = qm.getProject(roleProjectRequest.projectUUID()); if (project == null) return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); - final boolean modified = qm.removeRoleFromUser(principal, role, project); + final boolean modified = qm.removeRoleFromUser(user, role, project); if (!modified) return Response.notModified().entity("The user is not a member of the specified role.").build(); - principal = qm.getObjectById(principal.getClass(), principal.getId()); + user = qm.getObjectById(user.getClass(), user.getId()); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed role membership for: %s / role: %s / project: %s" - .formatted(principal.getName(), role.getName(), project.getName())); + .formatted(user.getName(), role.getName(), project.getName())); return Response.noContent().build(); } @@ -956,7 +956,7 @@ public Response removeRoleFromUser( @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Adds the username to the specified team.", description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

        ") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "The updated user", content = @Content(schema = @Schema(implementation = UserPrincipal.class))), + @ApiResponse(responseCode = "200", description = "The updated user", content = @Content(schema = @Schema(implementation = User.class))), @ApiResponse(responseCode = "304", description = "The user is already a member of the specified team(s)"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The user or team(s) could not be found") @@ -966,14 +966,14 @@ public Response setUserTeams( @Parameter(description = "A valid username", required = true) @PathParam("username") String username, @Parameter(description = "The UUID(s) of the team(s) to associate username with", required = true) @Valid TeamsSetRequest request) { try (QueryManager qm = new QueryManager()) { - UserPrincipal principal = qm.getUserPrincipal(username); - if (principal == null) { + User user = qm.getUser(username); + if (user == null) { return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); } // Compare given team uuids against current user teams - final Set currentUserTeams = principal.getTeams() == null ? Collections.emptySet() - : principal.getTeams().stream() + final Set currentUserTeams = user.getTeams() == null ? Collections.emptySet() + : user.getTeams().stream() .map(Team::getUuid) .map(UUID::toString) .collect(Collectors.toSet()); @@ -1002,11 +1002,11 @@ public Response setUserTeams( return Response.status(Response.Status.BAD_REQUEST).entity(response).build(); } - principal.setTeams(requestedTeams); - qm.persist(principal); + user.setTeams(requestedTeams); + qm.persist(user); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, - "Added team membership for: " + principal.getName() + " / team: " + requestedTeams.toString()); - return Response.ok(principal).build(); + "Added team membership for: " + user.getName() + " / team: " + requestedTeams.toString()); + return Response.ok(user).build(); } } } diff --git a/apiserver/src/main/resources/META-INF/persistence.xml b/apiserver/src/main/resources/META-INF/persistence.xml index 511460ad2d..d462c5c9a1 100644 --- a/apiserver/src/main/resources/META-INF/persistence.xml +++ b/apiserver/src/main/resources/META-INF/persistence.xml @@ -45,9 +45,7 @@ org.dependencytrack.model.ProjectMetadata org.dependencytrack.model.ProjectMetrics org.dependencytrack.model.ProjectProperty - org.dependencytrack.model.ProjectRole$LdapUserProjectRole - org.dependencytrack.model.ProjectRole$ManagedUserProjectRole - org.dependencytrack.model.ProjectRole$OidcUserProjectRole + org.dependencytrack.model.ProjectRole org.dependencytrack.model.Repository org.dependencytrack.model.RepositoryMetaComponent org.dependencytrack.model.Role diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml index 4c89dcea3f..2eeccb916f 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -53,20 +53,20 @@ - - + + @@ -74,114 +74,26 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -197,25 +109,16 @@ DELETE FROM "USER_PROJECT_EFFECTIVE_PERMISSIONS" WHERE "PROJECT_ID" = ANY(project_ids); - -- Rebuild effective permissions for all user types - FOREACH tbl_prefix IN ARRAY ARRAY['LDAP', 'MANAGED', 'OIDC'] - LOOP - EXECUTE format($query$ - INSERT INTO "USER_PROJECT_EFFECTIVE_PERMISSIONS" - (%I, "PROJECT_ID", "PERMISSION_ID", "PERMISSION_NAME") - SELECT DISTINCT upr.%I, upr."PROJECT_ID", rp."PERMISSION_ID", p."NAME" - FROM %I upr - INNER JOIN "ROLES_PERMISSIONS" rp - ON rp."ROLE_ID" = upr."ROLE_ID" - INNER JOIN "PERMISSION" p - ON p."ID" = rp."PERMISSION_ID" - WHERE upr."PROJECT_ID" = ANY($1); - $query$, - tbl_prefix || 'USER_ID', - tbl_prefix || 'USER_ID', - tbl_prefix || 'USERS_PROJECTS_ROLES') - USING project_ids; - END LOOP; + -- Rebuild effective permissions for all users + INSERT INTO "USER_PROJECT_EFFECTIVE_PERMISSIONS" + ("USER_ID", "PROJECT_ID", "PERMISSION_ID", "PERMISSION_NAME") + SELECT DISTINCT upr."USER_ID", upr."PROJECT_ID", rp."PERMISSION_ID", p."NAME" + FROM "USERS_PROJECTS_ROLES" upr + INNER JOIN "ROLES_PERMISSIONS" rp + ON rp."ROLE_ID" = upr."ROLE_ID" + INNER JOIN "PERMISSION" p + ON p."ID" = rp."PERMISSION_ID" + WHERE upr."PROJECT_ID" = ANY(project_ids); END; $$ LANGUAGE plpgsql; @@ -235,29 +138,16 @@ SELECT ARRAY_AGG(sub."PROJECT_ID") INTO project_ids FROM ( - SELECT lpr."PROJECT_ID" - FROM "LDAPUSERS_PROJECTS_ROLES" lpr + SELECT upr."PROJECT_ID" + FROM "USERS_PROJECTS_ROLES" upr INNER JOIN old_table - ON old_table."ROLE_ID" = lpr."ROLE_ID" - UNION - SELECT mpr."PROJECT_ID" - FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr - INNER JOIN old_table - ON old_table."ROLE_ID" = mpr."ROLE_ID" - UNION - SELECT opr."PROJECT_ID" - FROM "OIDCUSERS_PROJECTS_ROLES" opr - INNER JOIN old_table - ON old_table."ROLE_ID" = opr."ROLE_ID" + ON old_table."ROLE_ID" = upr."ROLE_ID" ) sub; ELSE - EXECUTE format($query$ - SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") - FROM %I AS t - WHERE t."ROLE_ID" = ANY($1) - $query$, TG_TABLE_NAME) - USING role_ids - INTO project_ids; + SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") + INTO project_ids + FROM "USERS_PROJECTS_ROLES" + WHERE "ROLE_ID" = ANY(role_ids); END IF; PERFORM recalc_user_project_role_effective_permissions(project_ids); @@ -281,29 +171,16 @@ SELECT ARRAY_AGG(sub."PROJECT_ID") INTO project_ids FROM ( - SELECT lpr."PROJECT_ID" - FROM "LDAPUSERS_PROJECTS_ROLES" lpr + SELECT upr."PROJECT_ID" + FROM "USERS_PROJECTS_ROLES" upr INNER JOIN new_table - ON new_table."ROLE_ID" = lpr."ROLE_ID" - UNION - SELECT mpr."PROJECT_ID" - FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr - INNER JOIN new_table - ON new_table."ROLE_ID" = mpr."ROLE_ID" - UNION - SELECT opr."PROJECT_ID" - FROM "OIDCUSERS_PROJECTS_ROLES" opr - INNER JOIN new_table - ON new_table."ROLE_ID" = opr."ROLE_ID" + ON new_table."ROLE_ID" = upr."ROLE_ID" ) sub; ELSE - EXECUTE format($query$ - SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") - FROM %I AS t - WHERE t."ROLE_ID" = ANY($1) - $query$, TG_TABLE_NAME) - USING role_ids - INTO project_ids; + SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") + INTO project_ids + FROM "USERS_PROJECTS_ROLES" + WHERE "ROLE_ID" = ANY(role_ids); END IF; PERFORM recalc_user_project_role_effective_permissions(project_ids); @@ -331,35 +208,18 @@ SELECT ARRAY_AGG(sub."PROJECT_ID") INTO project_ids FROM ( - SELECT lpr."PROJECT_ID" - FROM "LDAPUSERS_PROJECTS_ROLES" lpr - INNER JOIN new_table - ON new_table."ROLE_ID" = lpr."ROLE_ID" - FULL OUTER JOIN old_table - ON new_table."ROLE_ID" = old_table."ROLE_ID" - UNION - SELECT mpr."PROJECT_ID" - FROM "MANAGEDUSERS_PROJECTS_ROLES" mpr + SELECT upr."PROJECT_ID" + FROM "USERS_PROJECTS_ROLES" upr INNER JOIN new_table - ON new_table."ROLE_ID" = mpr."ROLE_ID" - FULL OUTER JOIN old_table - ON new_table."ROLE_ID" = old_table."ROLE_ID" - UNION - SELECT opr."PROJECT_ID" - FROM "OIDCUSERS_PROJECTS_ROLES" opr - INNER JOIN new_table - ON new_table."ROLE_ID" = opr."ROLE_ID" + ON new_table."ROLE_ID" = upr."ROLE_ID" FULL OUTER JOIN old_table ON new_table."ROLE_ID" = old_table."ROLE_ID" ) sub; ELSE - EXECUTE format($query$ - SELECT ARRAY_AGG(DISTINCT t."PROJECT_ID") - FROM %I AS t - WHERE t."ROLE_ID" = ANY($1) - $query$, TG_TABLE_NAME) - USING role_ids - INTO project_ids; + SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") + INTO project_ids + FROM "USERS_PROJECTS_ROLES" + WHERE "ROLE_ID" = ANY(role_ids); END IF; PERFORM recalc_user_project_role_effective_permissions(project_ids); @@ -369,65 +229,23 @@ - -- INSERT trigger for LDAPUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_insert - AFTER INSERT ON "LDAPUSERS_PROJECTS_ROLES" - REFERENCING NEW TABLE AS new_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); - - -- DELETE trigger for LDAPUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_delete - AFTER DELETE ON "LDAPUSERS_PROJECTS_ROLES" - REFERENCING OLD TABLE AS old_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); - - -- UPDATE trigger for LDAPUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_ldapusers_roles_update - AFTER UPDATE ON "LDAPUSERS_PROJECTS_ROLES" - REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_update(); - - -- INSERT trigger for MANAGEDUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_insert - AFTER INSERT ON "MANAGEDUSERS_PROJECTS_ROLES" - REFERENCING NEW TABLE AS new_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); - - -- DELETE trigger for MANAGEDUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_delete - AFTER DELETE ON "MANAGEDUSERS_PROJECTS_ROLES" - REFERENCING OLD TABLE AS old_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); - - -- UPDATE trigger for MANAGEDUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_mgdusers_roles_update - AFTER UPDATE ON "MANAGEDUSERS_PROJECTS_ROLES" - REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_update(); - - -- INSERT trigger for OIDCUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_insert - AFTER INSERT ON "OIDCUSERS_PROJECTS_ROLES" + -- INSERT trigger for USERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_insert + AFTER INSERT ON "USERS_PROJECTS_ROLES" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); - -- DELETE trigger for OIDCUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_delete - AFTER DELETE ON "OIDCUSERS_PROJECTS_ROLES" + -- DELETE trigger for USERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_delete + AFTER DELETE ON "USERS_PROJECTS_ROLES" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); - -- UPDATE trigger for OIDCUSERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_oidcusers_roles_update - AFTER UPDATE ON "OIDCUSERS_PROJECTS_ROLES" + -- UPDATE trigger for USERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_update + AFTER UPDATE ON "USERS_PROJECTS_ROLES" REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION role_effective_permissions_mx_on_update(); diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java index 142269eb8e..9e1f24bea1 100644 --- a/apiserver/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java +++ b/apiserver/src/test/java/org/dependencytrack/persistence/RoleQueryManagerTest.java @@ -38,7 +38,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.UUID; import org.dependencytrack.PersistenceCapableTest; import org.junit.After; @@ -53,7 +52,6 @@ public class RoleQueryManagerTest extends PersistenceCapableTest { private PostgreSQLContainer postgresContainer; - private Jdbi jdbi; @Before public void setUp() { @@ -63,7 +61,7 @@ public void setUp() { postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:11-alpine")); postgresContainer.start(); - jdbi = Jdbi.create( + Jdbi.create( postgresContainer.getJdbcUrl(), postgresContainer.getUsername(), postgresContainer.getPassword()); @@ -166,12 +164,11 @@ public void testGetUserRoles() throws ParseException { JdbiFactory.withJdbiHandle( handle -> handle.attach(RoleDao.class).addRoleToUser( - testUser.getClass(), testUser.getId(), testProject.getId(), expectedRole.getId())); - List actualRoles = qm.getUserRoles(testUser); + List actualRoles = qm.getUserRoles(testUser); Assert.assertEquals(actualRoles.size(), 1); Assert.assertEquals(expectedRole.toString(), actualRoles.get(0).getRole().toString()); @@ -213,7 +210,6 @@ public void testGetUnassignedProjects() throws ParseException { JdbiFactory.withJdbiHandle( handle -> handle.attach(RoleDao.class).addRoleToUser( - testUser.getClass(), testUser.getId(), assignedProject.getId(), maintainerRole.getId())); @@ -347,7 +343,6 @@ public void testGetUserProjectPermissions() throws ParseException { JdbiFactory.withJdbiHandle( handle -> handle.attach(RoleDao.class).addRoleToUser( - testUser.getClass(), testUser.getId(), testProject.getId(), maintainerRole.getId())); @@ -411,11 +406,11 @@ public void testRemoveRoleFromUser() throws ParseException { JdbiFactory.withJdbiHandle( handle -> handle.attach(RoleDao.class).addRoleToUser( - testUser.getClass(), testUser.getId(), testProject.getId(), maintainerRole.getId())); Assert.assertTrue(qm.removeRoleFromUser(testUser, maintainerRole, testProject)); } + } 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 ccc9e220ba..e69d9bef78 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java @@ -107,7 +107,7 @@ public void getRoleByInvalidUuidTest() { @Test public void createRoleTest() { - initializeWithPermissions(Permissions.ROLE_MANAGEMENT_CREATE); + initializeWithPermissions(Permissions.ACCESS_MANAGEMENT_CREATE); Response response = jersey.target(V1_ROLE).request() .header(X_API_KEY, apiKey) @@ -174,7 +174,6 @@ public void getUserRolesTest() throws ParseException { JdbiFactory.withJdbiHandle( handle -> handle.attach(RoleDao.class).addRoleToUser( - testUser.getClass(), testUser.getId(), testProject.getId(), expectedRole.getId())); From d7fab40f7ae67cf8e6e008e88c42eba39cf574ce Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 12 May 2025 11:14:15 -0500 Subject: [PATCH 148/181] fix: unique constraint on USERS_PROJECTS_ROLES Signed-off-by: Jonathan Howard --- .../src/main/resources/migration/changelog-v5.6.0-roles.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml index 2eeccb916f..7b1d722d6e 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -73,12 +73,11 @@ - + validateForeignKey="true" /> From 02f45f13397789dd326c8325b1383dbc64ad08d3 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 12 May 2025 11:46:19 -0500 Subject: [PATCH 149/181] fix: unique constraint to unique index Signed-off-by: Jonathan Howard --- .../persistence/jdbi/RoleDao.java | 9 +++++++++ .../migration/changelog-v5.6.0-roles.xml | 20 ++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index ca3bc84b73..356c3d27a0 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -109,4 +109,13 @@ public interface RoleDao { @RegisterFieldMapper(Project.class) List getUserUnassignedProjects(@Bind String username); + @SqlUpdate(/* language=sql */ """ + INSERT INTO "USERS_PROJECTS_ROLES" + ("USER_ID", "PROJECT_ID", "ROLE_ID") + VALUES (:userId, (SELECT "ID" FROM "PROJECT" WHERE "NAME" = :projectName), :roleId) + ON CONFLICT ("USER_ID", "PROJECT_ID") DO + UPDATE SET "ROLE_ID" = EXCLUDED."ROLE_ID" + """) + int setUserProjectRole(@Bind long userId, @Bind String projectName, @Bind long roleId); + } diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml index 7b1d722d6e..6913a6c94a 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -55,21 +55,19 @@ - + validateForeignKey="true" /> - + validateForeignKey="true" /> @@ -81,18 +79,12 @@ - + - - - - + From f2b68540e22f20e371d9b3b5a7d6627d5d75255a Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 12 May 2025 12:51:23 -0500 Subject: [PATCH 150/181] fix: add conflict clause to DAO method Signed-off-by: Jonathan Howard --- .../dependencytrack/persistence/jdbi/RoleDao.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java index 356c3d27a0..583647bafc 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/RoleDao.java @@ -56,7 +56,8 @@ public interface RoleDao { ("USER_ID", "PROJECT_ID", "ROLE_ID") VALUES (:userId, :projectId, :roleId) - ON CONFLICT DO NOTHING + ON CONFLICT ("USER_ID", "PROJECT_ID") DO + UPDATE SET "ROLE_ID" = EXCLUDED."ROLE_ID" """) int addRoleToUser(@Bind long userId, @Bind long projectId, @Bind long roleId); @@ -109,13 +110,4 @@ public interface RoleDao { @RegisterFieldMapper(Project.class) List getUserUnassignedProjects(@Bind String username); - @SqlUpdate(/* language=sql */ """ - INSERT INTO "USERS_PROJECTS_ROLES" - ("USER_ID", "PROJECT_ID", "ROLE_ID") - VALUES (:userId, (SELECT "ID" FROM "PROJECT" WHERE "NAME" = :projectName), :roleId) - ON CONFLICT ("USER_ID", "PROJECT_ID") DO - UPDATE SET "ROLE_ID" = EXCLUDED."ROLE_ID" - """) - int setUserProjectRole(@Bind long userId, @Bind String projectName, @Bind long roleId); - } From a51c98aed02899de7a3117cd6fb33be7a1f0731a Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Wed, 14 May 2025 16:32:07 -0500 Subject: [PATCH 151/181] fix: cascade delete user foreign key (#24) Signed-off-by: Jonathan Howard --- .../src/main/resources/migration/changelog-v5.6.0-roles.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml index 6913a6c94a..9a10a2b4bd 100644 --- a/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml +++ b/apiserver/src/main/resources/migration/changelog-v5.6.0-roles.xml @@ -58,7 +58,7 @@ From f92992de2b2ed642a8e982efccb99ee38048227c Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 15 May 2025 10:19:31 -0600 Subject: [PATCH 152/181] add gitlab integration state toggle Signed-off-by: Allen Shearin --- .../event/EventSubsystemInitializer.java | 3 + .../event/GitLabIntegrationStateEvent.java | 29 ++++ .../integrations/gitlab/GitLabClient.java | 26 ++-- .../gitlab/GitLabIntegrationStateChanger.java | 129 ++++++++++++++++++ .../integrations/gitlab/GitLabRole.java | 20 +-- .../model/ConfigPropertyConstants.java | 1 - .../persistence/QueryManager.java | 56 ++++++++ .../resources/v1/IntegrationResource.java | 39 ++++++ .../tasks/GitLabIntegrationStateTask.java | 66 +++++++++ .../dependencytrack/tasks/GitLabSyncTask.java | 9 +- .../resources/graphql/gitlab-projects.graphql | 8 +- .../integrations/gitlab/GitLabClientTest.java | 2 +- 12 files changed, 356 insertions(+), 32 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/event/GitLabIntegrationStateEvent.java create mode 100644 apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java create mode 100644 apiserver/src/main/java/org/dependencytrack/tasks/GitLabIntegrationStateTask.java diff --git a/apiserver/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/apiserver/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index d7d1528c9b..41cc7783fd 100644 --- a/apiserver/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/apiserver/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -40,6 +40,7 @@ import org.dependencytrack.tasks.EpssMirrorTask; import org.dependencytrack.tasks.FortifySscUploadTask; import org.dependencytrack.tasks.GitHubAdvisoryMirrorTask; +import org.dependencytrack.tasks.GitLabIntegrationStateTask; import org.dependencytrack.tasks.GitLabSyncTask; import org.dependencytrack.tasks.IntegrityAnalysisTask; import org.dependencytrack.tasks.IntegrityMetaInitializerTask; @@ -97,6 +98,7 @@ public void contextInitialized(final ServletContextEvent event) { EVENT_SERVICE.subscribe(VexUploadEvent.class, VexUploadProcessingTask.class); EVENT_SERVICE.subscribe(LdapSyncEvent.class, LdapSyncTaskWrapper.class); EVENT_SERVICE.subscribe(GitHubAdvisoryMirrorEvent.class, GitHubAdvisoryMirrorTask.class); + EVENT_SERVICE.subscribe(GitLabIntegrationStateEvent.class, GitLabIntegrationStateTask.class); EVENT_SERVICE.subscribe(GitLabSyncEvent.class, GitLabSyncTask.class); EVENT_SERVICE.subscribe(OsvMirrorEvent.class, OsvMirrorTask.class); EVENT_SERVICE.subscribe(ProjectVulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class); @@ -145,6 +147,7 @@ public void contextDestroyed(final ServletContextEvent event) { EVENT_SERVICE.unsubscribe(VexUploadProcessingTask.class); EVENT_SERVICE.unsubscribe(LdapSyncTaskWrapper.class); EVENT_SERVICE.unsubscribe(GitHubAdvisoryMirrorTask.class); + EVENT_SERVICE.unsubscribe(GitLabIntegrationStateTask.class); EVENT_SERVICE.unsubscribe(GitLabSyncTask.class); EVENT_SERVICE.unsubscribe(OsvMirrorTask.class); EVENT_SERVICE.unsubscribe(VulnerabilityAnalysisTask.class); diff --git a/apiserver/src/main/java/org/dependencytrack/event/GitLabIntegrationStateEvent.java b/apiserver/src/main/java/org/dependencytrack/event/GitLabIntegrationStateEvent.java new file mode 100644 index 0000000000..c245c7c6ed --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/event/GitLabIntegrationStateEvent.java @@ -0,0 +1,29 @@ +/* + * 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.event; + +import alpine.event.framework.Event; + +/** + * Defines an event used to start a sync task of current user's GitLab groups. + * + * @author Allen Shearin + */ +public class GitLabIntegrationStateEvent implements Event { +} diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index a2ca80182a..8aef907164 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -57,6 +57,7 @@ public class GitLabClient { private final URI baseURL; private final Config config; private final List topics; + private final boolean includeArchived; private final Map> rolePermissions = Map.of( GitLabRole.GUEST, List.of( @@ -111,17 +112,19 @@ public class GitLabClient { Permissions.TAG_MANAGEMENT_DELETE)); public GitLabClient(final String accessToken) { - this(accessToken, Config.getInstance(), null); + this(accessToken, Config.getInstance(), null, false); } - public GitLabClient(final String accessToken, final List topics) { - this(accessToken, Config.getInstance(), topics); + public GitLabClient(final String accessToken, final List topics, final boolean includeArchived) { + this(accessToken, Config.getInstance(), topics, includeArchived); } - public GitLabClient(final String accessToken, final Config config, final List topics) { - this.config = config; + public GitLabClient(final String accessToken, final Config config, final List topics, + final boolean includeArchived) { this.accessToken = accessToken; this.baseURL = URI.create(config.getProperty(Config.AlpineKey.OIDC_ISSUER)); + this.config = config; + this.includeArchived = includeArchived; this.topics = topics; } @@ -131,11 +134,17 @@ public List getGitLabProjects() throws IOException, URISyntaxExce JSONObject variables = new JSONObject(); JSONObject queryObject = new JSONObject(); + variables.put("includeTopics", false); + if (topics != null && !topics.isEmpty()) { variables.put("includeTopics", true); variables.put("topics", topics); } + if (includeArchived) { + variables.put("archived", "INCLUDE"); + } + queryObject.put("query", resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8)); URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT); @@ -158,9 +167,10 @@ public List getGitLabProjects() throws IOException, URISyntaxExce String responseBody = EntityUtils.toString(responseEntity); JSONObject responseData = JSONValue.parse(responseBody, JSONObject.class); - JSONObject dataObject = (JSONObject) responseData.get("data"); - JSONObject projectsObject = (JSONObject) dataObject.get("projects"); - JSONArray nodes = (JSONArray) projectsObject.get("nodes"); + JSONObject dataObject = (JSONObject) responseData.getOrDefault("data", new JSONObject()); + JSONObject projectsObject = (JSONObject) dataObject.getOrDefault("withoutTopics", + dataObject.getOrDefault("withTopics", new JSONObject())); + JSONArray nodes = (JSONArray) projectsObject.getOrDefault("nodes", new JSONArray()); for (Object nodeObject : nodes) { JSONObject node = (JSONObject) nodeObject; diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java new file mode 100644 index 0000000000..00d14de56b --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -0,0 +1,129 @@ +/* + * 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.integrations.gitlab; + +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.openJdbiHandle; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.dependencytrack.integrations.AbstractIntegrationPoint; +import org.dependencytrack.model.Role; +import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.persistence.jdbi.RoleDao; +import org.jdbi.v3.core.Handle; + +import alpine.common.logging.Logger; +import alpine.model.Permission; + +public class GitLabIntegrationStateChanger extends AbstractIntegrationPoint { + + private static final Logger LOGGER = Logger.getLogger(GitLabIntegrationStateChanger.class); + private static final String INTEGRATIONS_GROUP = GITLAB_ENABLED.getGroupName(); + private final Map PERMISSIONS_MAP = new HashMap<>(); + + public GitLabIntegrationStateChanger() { + } + + @Override + public String name() { + return "GitLab Integration State Changer"; + } + + @Override + public String description() { + return "Executes GitLab integration enable and disable tasks"; + } + + public void setState(boolean isEnabled) { + try { + if (isEnabled) { + LOGGER.info("Enabling GitLab integration"); + createGitLabRoles(); + LOGGER.info("GitLab integration enabled"); + } else { + LOGGER.info("Disabling GitLab integration"); + removeGitlabRoles(); + LOGGER.info("GitLab integration disabled"); + } + + } catch (RuntimeException ex) { + LOGGER.error("An error occurred while changing Gitlab Integration State", ex); + handleException(LOGGER, ex); + } + } + + private void createGitLabRoles() { + if (PERMISSIONS_MAP.isEmpty()) { + populatePermissionsMap(qm); + } + + for (GitLabRole role : GitLabRole.values()) { + try { + if (qm.getRoleByName(role.getDescription()) == null) { + qm.createRole(role.getDescription(), qm.getPermissionsByName(role.getPermissions().toArray(String[]::new))); + LOGGER.info("Created GitLab role: " + role.getDescription()); + } else { + LOGGER.info("GitLab role already exists: " + role.getDescription()); + } + } catch (Exception ex) { + LOGGER.error("An error occurred while creating GitLab roles", ex); + throw new RuntimeException("Failed to create GitLab roles", ex); + } + } + } + + private void removeGitlabRoles() { + try (Handle jdbiHandle = openJdbiHandle()) { + for (GitLabRole role : GitLabRole.values()) { + Role targetRole = qm.getRoleByName(role.getDescription()); + if (targetRole == null) { + LOGGER.info("GitLab role does not exist: " + role.getDescription()); + continue; + } + + jdbiHandle.attach(RoleDao.class).deleteRole(targetRole.getId()); + LOGGER.info("Removed GitLab role: " + role.getDescription()); + } + + } catch (Exception ex) { + LOGGER.error("An error occurred while removing GitLab roles", ex); + throw new RuntimeException("Failed to remove GitLab roles", ex); + } + + } + + private void populatePermissionsMap(QueryManager qm) { + // Retrieve all permissions from the database + List allPermissions = Objects.requireNonNullElse(qm.getPermissions(), Collections.emptyList()); + + // Add all permissions to the PERMISSIONS_MAP + for (Permission permission : allPermissions) { + PERMISSIONS_MAP.put(permission.getName(), permission); + } + } + + private List getPermissionsByName(List names) { + return names.stream().map(PERMISSIONS_MAP::get).filter(Objects::nonNull).toList(); + } +} diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java index f632869946..97016075a5 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java @@ -18,9 +18,9 @@ */ package org.dependencytrack.integrations.gitlab; -import java.util.Collection; import java.util.LinkedHashSet; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.dependencytrack.auth.Permissions; @@ -94,16 +94,10 @@ public String getDescription() { * * @return A sorted set of permissions for this role. */ - public Set getPermissions() { - var permissions = Set.copyOf(this.permissions); - - permissions.addAll(Stream.of(GitLabRole.values()) - .filter(value -> value.getAccessLevel() <= getAccessLevel()) - .map(GitLabRole::getPermissions) - .flatMap(Collection::stream) - .toList()); - - return new LinkedHashSet<>(permissions.stream().distinct().sorted().toList()); - } - +public Set getPermissions() { + return Stream.of(GitLabRole.values()) + .filter(value -> value.getAccessLevel() <= this.accessLevel) // Include current and lower access levels + .flatMap(value -> value.permissions.stream()) // Flatten permissions from all roles + .collect(Collectors.toCollection(LinkedHashSet::new)); // Collect into a LinkedHashSet to maintain order +} } diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 7625605efe..6422f75a9b 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -87,7 +87,6 @@ public enum ConfigPropertyConstants { FORTIFY_SSC_URL("integrations", "fortify.ssc.url", null, PropertyType.URL, "Base URL to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), FORTIFY_SSC_TOKEN("integrations", "fortify.ssc.token", null, PropertyType.ENCRYPTEDSTRING, "The token to use to authenticate to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), GITLAB_ENABLED("integrations", "gitlab.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitLab integration", ConfigPropertyAccessMode.READ_WRITE), - GITLAB_GROUPS("integrations", "gitlab.groups", "[]", PropertyType.STRING, "JSON array of GitLab group names for which to create teams/roles", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), GITLAB_INCLUDE_ARCHIVED("integrations", "gitlab.include.archived", "false", PropertyType.BOOLEAN, "Flag to enable/disable syncing of archived GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_TOPICS("integrations", "gitlab.topics", "[]", PropertyType.STRING, "JSON array of topics to include when syncing GitLab projects", ConfigPropertyAccessMode.READ_WRITE), diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 6481cd9045..db5ec14d99 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -45,6 +45,7 @@ import org.apache.commons.lang3.ClassUtils; import org.datanucleus.PropertyNames; import org.datanucleus.api.jdo.JDOQuery; +import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.AffectedVersionAttribution; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalyzerIdentity; @@ -74,6 +75,9 @@ import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ProjectRole; +import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; +import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; +import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; @@ -98,6 +102,7 @@ import org.dependencytrack.notification.publisher.PublisherClass; import org.dependencytrack.persistence.jdbi.EffectivePermissionDao; import org.dependencytrack.persistence.jdbi.JdbiFactory; +import org.dependencytrack.persistence.jdbi.RoleDao; import org.dependencytrack.proto.vulnanalysis.v1.ScanResult; import org.dependencytrack.proto.vulnanalysis.v1.ScanStatus; import org.dependencytrack.proto.vulnanalysis.v1.ScannerResult; @@ -122,6 +127,8 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -493,6 +500,55 @@ public QueryManager withL2CacheDisabled() { return this; } + public List getPermissionsByName(String... permissionNames) { + final Query query = pm.newQuery(Permission.class) + .filter(":permissions.contains(name)") + .setNamedParameters(Map.of("permissions", Arrays.asList(permissionNames))) + .orderBy("name ASC"); + + return executeAndCloseList(query); + } + + @Override + public List getEffectivePermissions(UserPrincipal user) { + // Get effective permissions for the user principal, either + // directly assigned or based on their team membership + final String[] permissionNames = super.getEffectivePermissions((Principal) user).toArray(String[]::new); + final HashSet permissions = new LinkedHashSet<>(getPermissionsByName(permissionNames)); + + final Class cls; + final String fieldName; + + switch (user) { + case LdapUser ldapUser -> { + fieldName = "ldapUsers"; + cls = LdapUserProjectRole.class; + } + case ManagedUser managedUser -> { + fieldName = "managedUsers"; + cls = ManagedUserProjectRole.class; + } + case OidcUser oidcUser -> { + fieldName = "oidcUsers"; + cls = OidcUserProjectRole.class; + } + default -> { + return Collections.emptyList(); + } + } + + List userRoles = JdbiFactory.withJdbiHandle(request, handle -> handle.attach(RoleDao.class) + .getUserRoles(user.getClass(), user.getUsername())); + + final Permission viewPortfolio = getPermission(Permissions.Constants.VIEW_PORTFOLIO); + + // If a user has no permissions, assign view portfolio to allow access to dashboard + if (userRoles.isEmpty()) + permissions.add(viewPortfolio); + + return permissions.stream().toList(); + } + /** * Get the IDs of the {@link ProjectRole}s a given {@link Principal} is a member of. * diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java index f378b16091..448264affc 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java @@ -18,9 +18,12 @@ */ package org.dependencytrack.resources.v1; +import alpine.event.framework.Event; +import alpine.model.ConfigProperty; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -30,13 +33,19 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; 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.GitLabIntegrationStateEvent; +import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.tasks.OsvMirrorTask; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; + import java.util.List; import java.util.stream.Collectors; @@ -94,4 +103,34 @@ public Response getInactiveEcosystems() { .collect(Collectors.toList()); return Response.ok(ecosystems).build(); } + + @POST + @Path("gitlab/{state}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Set state of gitlab integration", description = "

        Requires permission SYSTEM_CONFIGURATION or SYSTEM_CONFIGURATION_CREATE

        ") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Gitlab state set successfully"), + @ApiResponse(responseCode = "304", description = "The GitLab integration is already in the desired state"), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired({ Permissions.Constants.SYSTEM_CONFIGURATION, + Permissions.Constants.SYSTEM_CONFIGURATION_CREATE }) // Require admin privileges due to system impact + public Response handleGitlabStateChange( + @Parameter(description = "A valid boolean", required = true) @PathParam("state") String state) { + try (QueryManager qm = new QueryManager()) { + final ConfigProperty property = qm.getConfigProperty(GITLAB_ENABLED.getGroupName(), + GITLAB_ENABLED.getPropertyName()); + + if (!property.getPropertyValue().equals(state)) { + if (!state.equalsIgnoreCase("true") && !state.equalsIgnoreCase("false")) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + property.setPropertyValue(state); + qm.persist(property); + Event.dispatch(new GitLabIntegrationStateEvent()); + } + } + + return Response.ok().build(); + } } diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabIntegrationStateTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabIntegrationStateTask.java new file mode 100644 index 0000000000..6feec71759 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabIntegrationStateTask.java @@ -0,0 +1,66 @@ +/* + * 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.tasks; + +import alpine.common.logging.Logger; +import alpine.event.framework.Event; +import alpine.event.framework.LoggableSubscriber; +import alpine.model.ConfigProperty; + +import org.dependencytrack.integrations.gitlab.GitLabIntegrationStateChanger; +import org.dependencytrack.persistence.QueryManager; + +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; + +import org.dependencytrack.event.GitLabIntegrationStateEvent; + +public class GitLabIntegrationStateTask implements LoggableSubscriber { + + private static final Logger LOGGER = Logger.getLogger(GitLabIntegrationStateTask.class); + private final boolean isEnabled; + + public GitLabIntegrationStateTask() { + try (final QueryManager qm = new QueryManager()) { + final ConfigProperty enabled = qm.getConfigProperty(GITLAB_ENABLED.getGroupName(), GITLAB_ENABLED.getPropertyName()); + + this.isEnabled = enabled != null && Boolean.parseBoolean(enabled.getPropertyValue()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void inform(final Event event) { + if (!(event instanceof GitLabIntegrationStateEvent)) { + return; + } + + LOGGER.info("Starting GitLab state change task"); + + try (QueryManager qm = new QueryManager()) { + GitLabIntegrationStateChanger stateChanger = new GitLabIntegrationStateChanger(); + stateChanger.setQueryManager(qm); + stateChanger.setState(this.isEnabled); + } + + LOGGER.info("GitLab state change complete"); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index 69655346af..d83b2b101c 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -23,7 +23,6 @@ import alpine.event.framework.LoggableSubscriber; import alpine.model.ConfigProperty; import alpine.model.OidcUser; - import net.minidev.json.JSONArray; import net.minidev.json.JSONValue; @@ -33,6 +32,7 @@ import org.dependencytrack.persistence.QueryManager; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_INCLUDE_ARCHIVED; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_TOPICS; import java.util.List; @@ -82,7 +82,12 @@ public void inform(final Event event) { GITLAB_TOPICS.getGroupName(), GITLAB_TOPICS.getPropertyName()).getPropertyValue(); List topics = List.of(JSONValue.parse(topicsProperty, JSONArray.class).toArray(String[]::new)); - GitLabClient gitLabClient = new GitLabClient(accessToken, topics); + String includeArchivedString = qm.getConfigProperty( + GITLAB_INCLUDE_ARCHIVED.getGroupName(), GITLAB_INCLUDE_ARCHIVED.getPropertyName()) + .getPropertyValue(); + boolean includeArchived = Boolean.parseBoolean(includeArchivedString); + + GitLabClient gitLabClient = new GitLabClient(accessToken, topics, includeArchived); GitLabSyncer syncer = new GitLabSyncer(user, gitLabClient); syncer.setQueryManager(qm); syncer.synchronize(); diff --git a/apiserver/src/main/resources/graphql/gitlab-projects.graphql b/apiserver/src/main/resources/graphql/gitlab-projects.graphql index e308d92ae1..058f383850 100644 --- a/apiserver/src/main/resources/graphql/gitlab-projects.graphql +++ b/apiserver/src/main/resources/graphql/gitlab-projects.graphql @@ -1,11 +1,5 @@ fragment projectFields on ProjectConnection { nodes { - group { - fullPath - maxAccessLevel { - humanAccess - } - } fullPath maxAccessLevel { humanAccess @@ -41,4 +35,4 @@ query ( ) @skip(if: $includeTopics) { ...projectFields } -} \ No newline at end of file +} diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index 886b821557..fe33c97f1a 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -86,7 +86,7 @@ public void testGetGitLabProjects() throws URISyntaxException, IOException { when(configMock.getProperty(eq(Config.AlpineKey.OIDC_ISSUER))).thenReturn(wireMockRule.baseUrl()); - GitLabClient gitLabClient = new GitLabClient(accessToken, configMock, null); + GitLabClient gitLabClient = new GitLabClient(accessToken, configMock, null, false); List gitLabProjects = gitLabClient.getGitLabProjects(); From 9c14b4da854ee0b44059813efa9bde8aa7988ccb Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 15 May 2025 10:19:41 -0600 Subject: [PATCH 153/181] update Event description Signed-off-by: Allen Shearin --- .../org/dependencytrack/event/GitLabIntegrationStateEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/event/GitLabIntegrationStateEvent.java b/apiserver/src/main/java/org/dependencytrack/event/GitLabIntegrationStateEvent.java index c245c7c6ed..59de3a8a63 100644 --- a/apiserver/src/main/java/org/dependencytrack/event/GitLabIntegrationStateEvent.java +++ b/apiserver/src/main/java/org/dependencytrack/event/GitLabIntegrationStateEvent.java @@ -21,7 +21,7 @@ import alpine.event.framework.Event; /** - * Defines an event used to start a sync task of current user's GitLab groups. + * Defines an event used to start a state change task for the GitLab Integration. * * @author Allen Shearin */ From afa7be52327846aa8a68cf29b70099b1cc8294c8 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 15 May 2025 10:19:41 -0600 Subject: [PATCH 154/181] add tests, general cleanup Signed-off-by: Allen Shearin --- .../gitlab/GitLabIntegrationStateChanger.java | 7 + .../model/ConfigPropertyConstants.java | 4 +- .../dependencytrack/tasks/GitLabSyncTask.java | 10 -- .../resources/graphql/gitlab-projects.graphql | 45 ++++--- .../GitLabIntegrationStateChangerTest.java | 127 ++++++++++++++++++ 5 files changed, 162 insertions(+), 31 deletions(-) create mode 100644 apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java index 00d14de56b..0687a267cc 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -126,4 +126,11 @@ private void populatePermissionsMap(QueryManager qm) { private List getPermissionsByName(List names) { return names.stream().map(PERMISSIONS_MAP::get).filter(Objects::nonNull).toList(); } + + public Map getPermissionsMap() { + if (PERMISSIONS_MAP.isEmpty()) { + populatePermissionsMap(qm); + } + return PERMISSIONS_MAP; + } } diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 6422f75a9b..e3422ac39b 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -86,11 +86,11 @@ public enum ConfigPropertyConstants { FORTIFY_SSC_SYNC_CADENCE("integrations", "fortify.ssc.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), FORTIFY_SSC_URL("integrations", "fortify.ssc.url", null, PropertyType.URL, "Base URL to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), FORTIFY_SSC_TOKEN("integrations", "fortify.ssc.token", null, PropertyType.ENCRYPTEDSTRING, "The token to use to authenticate to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_APP_ID("integrations", "gitlab.app.id", null, PropertyType.STRING, "ID for the configured GitLab application", ConfigPropertyAccessMode.READ_WRITE), GITLAB_ENABLED("integrations", "gitlab.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitLab integration", ConfigPropertyAccessMode.READ_WRITE), - GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), GITLAB_INCLUDE_ARCHIVED("integrations", "gitlab.include.archived", "false", PropertyType.BOOLEAN, "Flag to enable/disable syncing of archived GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_TOPICS("integrations", "gitlab.topics", "[]", PropertyType.STRING, "JSON array of topics to include when syncing GitLab projects", ConfigPropertyAccessMode.READ_WRITE), - GITLAB_APP_ID("integrations", "gitlab.app.id", null, PropertyType.STRING, "ID for the configured GitLab application", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_ENABLED("integrations", "defectdojo.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo integration", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_REIMPORT_ENABLED("integrations", "defectdojo.reimport.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo reimport-scan API endpoint", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_SYNC_CADENCE("integrations", "defectdojo.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to DefectDojo", ConfigPropertyAccessMode.READ_WRITE), diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index d83b2b101c..09dbadb043 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -94,16 +94,6 @@ public void inform(final Event event) { } LOGGER.info("GitLab sync complete"); - - // TODO: - // - [X] Assign authenticated OIDC user the VIEW_PORTFOLIO permission - // - [ ] Get user GitLab project memberships (use alpine.security.crypto.DataEncryption for request) - // - [X] Create Dependency-Track hierarchical project structure for user's GitLab projects - // - [X] Create Dependency-Track teams such as -maintainer - // - [ ] Come up with a set of Dependency-Track project permissions per GitLab role - // - [ ] Assign Dependency-Track permissions (TBD) to teams based on GitLab role - // - [ ] Map user OIDC groups to Dependency-Track teams - // - [ ] Configure portfolio access control (map team to project access) } } diff --git a/apiserver/src/main/resources/graphql/gitlab-projects.graphql b/apiserver/src/main/resources/graphql/gitlab-projects.graphql index 058f383850..7d65fde941 100644 --- a/apiserver/src/main/resources/graphql/gitlab-projects.graphql +++ b/apiserver/src/main/resources/graphql/gitlab-projects.graphql @@ -1,8 +1,31 @@ +fragment projectsWithTopics on Query { + withTopics: projects( + archived: $archived + topics: $topics + membership: true + first: 100 + after: $cursor + ) { + ...projectFields + } +} + +fragment projectsWithoutTopics on Query { + withoutTopics: projects( + archived: $archived + membership: true + first: 100 + after: $cursor + ) { + ...projectFields + } +} + fragment projectFields on ProjectConnection { nodes { fullPath maxAccessLevel { - humanAccess + stringValue } } pageInfo { @@ -17,22 +40,6 @@ query ( $topics: [String!] = [] $includeTopics: Boolean! = false ) { - withTopic: projects( - archived: $archived - topics: $topics - membership: true - first: 100 - after: $cursor - ) @include(if: $includeTopics) { - ...projectFields - } - - projects( - archived: $archived - membership: true - first: 100 - after: $cursor - ) @skip(if: $includeTopics) { - ...projectFields - } + ...projectsWithTopics @include(if: $includeTopics) + ...projectsWithoutTopics @skip(if: $includeTopics) } diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java new file mode 100644 index 0000000000..906b433d00 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java @@ -0,0 +1,127 @@ +/* + * 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.integrations.gitlab; + +import alpine.model.IConfigProperty; +import alpine.model.Permission; + +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Role; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GitLabIntegrationStateChangerTest extends PersistenceCapableTest { + + private GitLabIntegrationStateChanger stateChanger; + private List roles; + + @Before + public void setUp() { + stateChanger = new GitLabIntegrationStateChanger(); + } + + /** + * Validates that the metadata is correctly defined. + */ + @Test + public void testIntegrationStateChangerMetadata() { + Assert.assertEquals("GitLab Integration State Changer", stateChanger.name()); + Assert.assertEquals("Executes GitLab integration enable and disable tasks", stateChanger.description()); + } + + /** + * Validates that the when integration is enabled the roles are created. + */ + @Test + public void testEnable() { + stateChanger.setQueryManager(qm); + qm.createConfigProperty( + GITLAB_ENABLED.getGroupName(), + GITLAB_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null); + stateChanger.setState(true); + roles = qm.getRoles(); + Assert.assertEquals(roles.size(), GitLabRole.values().length); + for (GitLabRole role : GitLabRole.values()) { + Assert.assertNotNull(qm.getRoleByName(role.getDescription())); + } + } + + /** + * Validates that the when integration is disabled the roles are removed. + */ + @Test + public void testDisable() { + stateChanger.setQueryManager(qm); + qm.createConfigProperty( + GITLAB_ENABLED.getGroupName(), + GITLAB_ENABLED.getPropertyName(), + "false", + IConfigProperty.PropertyType.BOOLEAN, + null); + + // Create roles to be removed + stateChanger.setState(true); + roles = qm.getRoles(); + Assert.assertEquals(roles.size(), GitLabRole.values().length); + for (GitLabRole role : GitLabRole.values()) { + Assert.assertNotNull(qm.getRoleByName(role.getDescription())); + } + + // Disable the integration + // and verify that the roles are removed + stateChanger.setState(false); + roles = qm.getRoles(); + Assert.assertEquals(roles.size(), 0); + } + + @Test + public void testPopulatePermissionsMap() { + final var mockStateChanger = mock(GitLabIntegrationStateChanger.class); + + // Create Permission and add it to the map + Permission permission = new Permission(); + permission.setName("VIEW_PORTFOLIO"); + Map testPermissionsMap = new HashMap(); + testPermissionsMap.put("VIEW_PORTFOLIO", permission); + + // Set query manager and permissions map for mockStateChanger + mockStateChanger.setQueryManager(qm); + when(mockStateChanger.getPermissionsMap()).thenReturn(testPermissionsMap); + + // Verify that the permission was added to the map + Map permissionsMap = mockStateChanger.getPermissionsMap(); + Assert.assertEquals(permissionsMap.size(), 1); + Assert.assertTrue(permissionsMap.containsKey("VIEW_PORTFOLIO")); + Assert.assertTrue(permissionsMap.containsValue(permission)); + } + +} From 7dcf484a93c992f94a327a98b84c7f9f9967e66d Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 15 May 2025 10:19:41 -0600 Subject: [PATCH 155/181] Apply suggestions from code review Co-authored-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Signed-off-by: Allen Shearin --- .../gitlab/GitLabIntegrationStateChanger.java | 9 ++++----- .../dependencytrack/integrations/gitlab/GitLabRole.java | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java index 0687a267cc..05d7016cf6 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -60,13 +60,12 @@ public void setState(boolean isEnabled) { if (isEnabled) { LOGGER.info("Enabling GitLab integration"); createGitLabRoles(); - LOGGER.info("GitLab integration enabled"); - } else { - LOGGER.info("Disabling GitLab integration"); - removeGitlabRoles(); - LOGGER.info("GitLab integration disabled"); + + return; } + LOGGER.info("Disabling GitLab integration"); + removeGitlabRoles(); } catch (RuntimeException ex) { LOGGER.error("An error occurred while changing Gitlab Integration State", ex); handleException(LOGGER, ex); diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java index 97016075a5..18c6b63e84 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java @@ -96,7 +96,7 @@ public String getDescription() { */ public Set getPermissions() { return Stream.of(GitLabRole.values()) - .filter(value -> value.getAccessLevel() <= this.accessLevel) // Include current and lower access levels + .filter(value -> value.accessLevel <= this.accessLevel) // Include current and lower access levels .flatMap(value -> value.permissions.stream()) // Flatten permissions from all roles .collect(Collectors.toCollection(LinkedHashSet::new)); // Collect into a LinkedHashSet to maintain order } From bd3dbd14450ef09a2b523a68ea2d414c57f15836 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 15 May 2025 10:19:41 -0600 Subject: [PATCH 156/181] chore: cleanup to essentials, add archived default value Signed-off-by: Allen Shearin --- .../integrations/gitlab/GitLabClient.java | 2 + .../gitlab/GitLabIntegrationStateChanger.java | 4 -- .../persistence/QueryManager.java | 47 ------------------- 3 files changed, 2 insertions(+), 51 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index 8aef907164..ec9edf05b5 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -134,7 +134,9 @@ public List getGitLabProjects() throws IOException, URISyntaxExce JSONObject variables = new JSONObject(); JSONObject queryObject = new JSONObject(); + // Set the default values for the GraphQL query variables.put("includeTopics", false); + variables.put("archived", "EXCLUDE"); if (topics != null && !topics.isEmpty()) { variables.put("includeTopics", true); diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java index 05d7016cf6..426a1c74c2 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -122,10 +122,6 @@ private void populatePermissionsMap(QueryManager qm) { } } - private List getPermissionsByName(List names) { - return names.stream().map(PERMISSIONS_MAP::get).filter(Objects::nonNull).toList(); - } - public Map getPermissionsMap() { if (PERMISSIONS_MAP.isEmpty()) { populatePermissionsMap(qm); diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index db5ec14d99..6479f1c5ff 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -45,7 +45,6 @@ import org.apache.commons.lang3.ClassUtils; import org.datanucleus.PropertyNames; import org.datanucleus.api.jdo.JDOQuery; -import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.AffectedVersionAttribution; import org.dependencytrack.model.Analysis; import org.dependencytrack.model.AnalyzerIdentity; @@ -75,9 +74,6 @@ import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ProjectRole; -import org.dependencytrack.model.ProjectRole.LdapUserProjectRole; -import org.dependencytrack.model.ProjectRole.ManagedUserProjectRole; -import org.dependencytrack.model.ProjectRole.OidcUserProjectRole; import org.dependencytrack.model.Repository; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; @@ -102,7 +98,6 @@ import org.dependencytrack.notification.publisher.PublisherClass; import org.dependencytrack.persistence.jdbi.EffectivePermissionDao; import org.dependencytrack.persistence.jdbi.JdbiFactory; -import org.dependencytrack.persistence.jdbi.RoleDao; import org.dependencytrack.proto.vulnanalysis.v1.ScanResult; import org.dependencytrack.proto.vulnanalysis.v1.ScanStatus; import org.dependencytrack.proto.vulnanalysis.v1.ScannerResult; @@ -127,8 +122,6 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -509,46 +502,6 @@ public List getPermissionsByName(String... permissionNames) { return executeAndCloseList(query); } - @Override - public List getEffectivePermissions(UserPrincipal user) { - // Get effective permissions for the user principal, either - // directly assigned or based on their team membership - final String[] permissionNames = super.getEffectivePermissions((Principal) user).toArray(String[]::new); - final HashSet permissions = new LinkedHashSet<>(getPermissionsByName(permissionNames)); - - final Class cls; - final String fieldName; - - switch (user) { - case LdapUser ldapUser -> { - fieldName = "ldapUsers"; - cls = LdapUserProjectRole.class; - } - case ManagedUser managedUser -> { - fieldName = "managedUsers"; - cls = ManagedUserProjectRole.class; - } - case OidcUser oidcUser -> { - fieldName = "oidcUsers"; - cls = OidcUserProjectRole.class; - } - default -> { - return Collections.emptyList(); - } - } - - List userRoles = JdbiFactory.withJdbiHandle(request, handle -> handle.attach(RoleDao.class) - .getUserRoles(user.getClass(), user.getUsername())); - - final Permission viewPortfolio = getPermission(Permissions.Constants.VIEW_PORTFOLIO); - - // If a user has no permissions, assign view portfolio to allow access to dashboard - if (userRoles.isEmpty()) - permissions.add(viewPortfolio); - - return permissions.stream().toList(); - } - /** * Get the IDs of the {@link ProjectRole}s a given {@link Principal} is a member of. * From a8df3c868ee6f1cb3cd0fb955e7207bdd8b43fe7 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 15 May 2025 10:20:05 -0600 Subject: [PATCH 157/181] pull in new getPermissionByName Signed-off-by: Allen Shearin --- .../gitlab/GitLabIntegrationStateChanger.java | 2 +- .../org/dependencytrack/persistence/QueryManager.java | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java index 426a1c74c2..fe3cb4edc3 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -80,7 +80,7 @@ private void createGitLabRoles() { for (GitLabRole role : GitLabRole.values()) { try { if (qm.getRoleByName(role.getDescription()) == null) { - qm.createRole(role.getDescription(), qm.getPermissionsByName(role.getPermissions().toArray(String[]::new))); + qm.createRole(role.getDescription(), qm.getPermissionsByName(role.getPermissions())); LOGGER.info("Created GitLab role: " + role.getDescription()); } else { LOGGER.info("GitLab role already exists: " + role.getDescription()); diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 6479f1c5ff..6481cd9045 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -493,15 +493,6 @@ public QueryManager withL2CacheDisabled() { return this; } - public List getPermissionsByName(String... permissionNames) { - final Query query = pm.newQuery(Permission.class) - .filter(":permissions.contains(name)") - .setNamedParameters(Map.of("permissions", Arrays.asList(permissionNames))) - .orderBy("name ASC"); - - return executeAndCloseList(query); - } - /** * Get the IDs of the {@link ProjectRole}s a given {@link Principal} is a member of. * From 008c39a50aa06d6d7b9d3e205936f543149bacb4 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 15 May 2025 10:44:12 -0600 Subject: [PATCH 158/181] chore: rebase cleanup Signed-off-by: Allen Shearin --- .../java/org/dependencytrack/persistence/QueryManager.java | 3 --- .../org/dependencytrack/persistence/RoleQueryManager.java | 5 ----- 2 files changed, 8 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 6481cd9045..78805815af 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -23,9 +23,6 @@ import alpine.common.validation.RegexSequence; import alpine.model.ApiKey; import alpine.model.ConfigProperty; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; import alpine.model.IConfigProperty.PropertyType; import alpine.model.Permission; import alpine.model.Team; diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java index 745a3ee0bd..656e237263 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/RoleQueryManager.java @@ -104,11 +104,6 @@ public Role getRoleByName(final String name) { return executeAndCloseUnique(query); } - @Override - public Role getRole(final String uuid) { - return getObjectByUuid(Role.class, uuid, Role.FetchGroup.ALL.name()); - } - @Override public List getUserRoles(final User user) { return JdbiFactory.withJdbiHandle(handle -> handle.attach(RoleDao.class) From dde9fffb0c581e36e75b554f4377cdcc902e4762 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 19 May 2025 18:30:16 -0600 Subject: [PATCH 159/181] chore: includes cleanup Signed-off-by: Allen Shearin --- .../java/org/dependencytrack/persistence/QueryManager.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index 00d26e4914..3951cc9d5c 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -23,9 +23,6 @@ import alpine.common.validation.RegexSequence; import alpine.model.ApiKey; import alpine.model.ConfigProperty; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; import alpine.model.IConfigProperty.PropertyType; import alpine.model.Permission; import alpine.model.Team; From f4e880c949802dff98c6e815f875104450b081af Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Tue, 20 May 2025 12:38:00 -0500 Subject: [PATCH 160/181] chore: regenerate jooq with role tables Signed-off-by: Jonathan Howard --- .../main/resources/META-INF/persistence.xml | 1 - persistence-jooq/pom.xml | 36 +- .../jooq/generated/DefaultSchema.java | 23 +- .../persistence/jooq/generated/Indexes.java | 8 +- .../persistence/jooq/generated/Keys.java | 97 ++--- .../persistence/jooq/generated/Routines.java | 16 +- .../persistence/jooq/generated/Tables.java | 20 +- .../jooq/generated/enums/Severity.java | 2 +- .../generated/routines/CalcRiskScore.java | 2 +- .../generated/routines/HasProjectAccess.java | 2 +- .../generated/routines/JsonbVulnAliases.java | 2 +- ...RecalcUserProjectEffectivePermissions.java | 2 +- ...lcUserProjectRoleEffectivePermissions.java | 54 +++ .../routines/UpdateComponentMetrics.java | 2 +- .../routines/UpdatePortfolioMetrics.java | 2 +- .../routines/UpdateProjectMetrics.java | 2 +- .../tables/AffectedVersionAttribution.java | 4 +- .../jooq/generated/tables/Analysis.java | 14 +- .../generated/tables/AnalysisComment.java | 4 +- .../jooq/generated/tables/ApiKey.java | 4 +- .../jooq/generated/tables/ApiKeysTeams.java | 4 +- .../jooq/generated/tables/Bom.java | 8 +- .../jooq/generated/tables/Component.java | 22 +- .../generated/tables/ComponentOccurrence.java | 8 +- .../generated/tables/ComponentProperty.java | 8 +- .../tables/ComponentsVulnerabilities.java | 10 +- .../jooq/generated/tables/ConfigProperty.java | 2 +- .../generated/tables/DependencyMetrics.java | 4 +- .../jooq/generated/tables/Epss.java | 2 +- .../generated/tables/FindingAttribution.java | 4 +- .../generated/tables/IntegrityAnalysis.java | 8 +- .../tables/IntegrityMetaComponent.java | 2 +- .../jooq/generated/tables/License.java | 6 +- .../jooq/generated/tables/LicenseGroup.java | 4 +- .../generated/tables/LicenseGroupLicense.java | 4 +- .../generated/tables/MappedLdapGroup.java | 4 +- .../generated/tables/MappedOidcGroup.java | 4 +- .../tables/NotificationPublisher.java | 4 +- .../generated/tables/NotificationRule.java | 4 +- .../tables/NotificationRuleProjects.java | 4 +- .../tables/NotificationRuleTags.java | 4 +- .../tables/NotificationRuleTeams.java | 4 +- .../jooq/generated/tables/OidcGroup.java | 4 +- .../jooq/generated/tables/Permission.java | 34 +- .../jooq/generated/tables/Policy.java | 8 +- .../generated/tables/PolicyCondition.java | 4 +- .../jooq/generated/tables/PolicyProjects.java | 10 +- .../jooq/generated/tables/PolicyTags.java | 10 +- .../generated/tables/PolicyViolation.java | 4 +- .../generated/tables/PortfolioMetrics.java | 2 +- .../jooq/generated/tables/Project.java | 46 ++- .../generated/tables/ProjectAccessTeams.java | 10 +- .../generated/tables/ProjectHierarchy.java | 10 +- .../generated/tables/ProjectMetadata.java | 8 +- .../jooq/generated/tables/ProjectMetrics.java | 4 +- .../generated/tables/ProjectProperty.java | 8 +- .../jooq/generated/tables/ProjectsTags.java | 10 +- .../jooq/generated/tables/Repository.java | 2 +- .../tables/RepositoryMetaComponent.java | 2 +- .../jooq/generated/tables/Role.java | 333 ++++++++++++++++++ .../generated/tables/RolesPermissions.java | 317 +++++++++++++++++ .../generated/tables/ServiceComponent.java | 4 +- .../ServiceComponentsVulnerabilities.java | 4 +- .../jooq/generated/tables/Tag.java | 10 +- .../jooq/generated/tables/Team.java | 10 +- .../generated/tables/TeamsPermissions.java | 10 +- .../jooq/generated/tables/User.java | 24 +- .../UserProjectEffectivePermissions.java | 30 +- .../generated/tables/UsersPermissions.java | 10 +- .../generated/tables/UsersProjectsRoles.java | 329 +++++++++++++++++ .../jooq/generated/tables/UsersTeams.java | 10 +- .../jooq/generated/tables/Vex.java | 8 +- .../generated/tables/ViolationAnalysis.java | 4 +- .../tables/ViolationAnalysisComment.java | 4 +- .../generated/tables/VulnerabilitiesTags.java | 10 +- .../jooq/generated/tables/Vulnerability.java | 10 +- .../generated/tables/VulnerabilityAlias.java | 2 +- .../tables/VulnerabilityMetrics.java | 2 +- .../generated/tables/VulnerabilityPolicy.java | 6 +- .../tables/VulnerabilityPolicyBundle.java | 2 +- .../generated/tables/VulnerabilityScan.java | 2 +- .../generated/tables/VulnerableSoftware.java | 4 +- .../VulnerableSoftwareVulnerabilities.java | 4 +- .../jooq/generated/tables/WorkflowState.java | 8 +- .../AffectedVersionAttributionRecord.java | 2 +- .../tables/records/AnalysisCommentRecord.java | 2 +- .../tables/records/AnalysisRecord.java | 2 +- .../tables/records/ApiKeyRecord.java | 2 +- .../tables/records/ApiKeysTeamsRecord.java | 2 +- .../generated/tables/records/BomRecord.java | 2 +- .../records/ComponentOccurrenceRecord.java | 2 +- .../records/ComponentPropertyRecord.java | 2 +- .../tables/records/ComponentRecord.java | 2 +- .../ComponentsVulnerabilitiesRecord.java | 2 +- .../tables/records/ConfigPropertyRecord.java | 2 +- .../records/DependencyMetricsRecord.java | 2 +- .../generated/tables/records/EpssRecord.java | 2 +- .../records/FindingAttributionRecord.java | 2 +- .../records/IntegrityAnalysisRecord.java | 2 +- .../records/IntegrityMetaComponentRecord.java | 2 +- .../records/LicenseGroupLicenseRecord.java | 2 +- .../tables/records/LicenseGroupRecord.java | 2 +- .../tables/records/LicenseRecord.java | 2 +- .../tables/records/MappedLdapGroupRecord.java | 2 +- .../tables/records/MappedOidcGroupRecord.java | 2 +- .../records/NotificationPublisherRecord.java | 2 +- .../NotificationRuleProjectsRecord.java | 2 +- .../records/NotificationRuleRecord.java | 2 +- .../records/NotificationRuleTagsRecord.java | 2 +- .../records/NotificationRuleTeamsRecord.java | 2 +- .../tables/records/OidcGroupRecord.java | 2 +- .../tables/records/PermissionRecord.java | 2 +- .../tables/records/PolicyConditionRecord.java | 2 +- .../tables/records/PolicyProjectsRecord.java | 2 +- .../tables/records/PolicyRecord.java | 2 +- .../tables/records/PolicyTagsRecord.java | 2 +- .../tables/records/PolicyViolationRecord.java | 2 +- .../records/PortfolioMetricsRecord.java | 2 +- .../records/ProjectAccessTeamsRecord.java | 2 +- .../records/ProjectHierarchyRecord.java | 2 +- .../tables/records/ProjectMetadataRecord.java | 2 +- .../tables/records/ProjectMetricsRecord.java | 2 +- .../tables/records/ProjectPropertyRecord.java | 2 +- .../tables/records/ProjectRecord.java | 2 +- .../tables/records/ProjectsTagsRecord.java | 2 +- .../RepositoryMetaComponentRecord.java | 2 +- .../tables/records/RepositoryRecord.java | 2 +- .../generated/tables/records/RoleRecord.java | 108 ++++++ .../records/RolesPermissionsRecord.java | 80 +++++ .../records/ServiceComponentRecord.java | 2 +- ...erviceComponentsVulnerabilitiesRecord.java | 2 +- .../generated/tables/records/TagRecord.java | 2 +- .../generated/tables/records/TeamRecord.java | 2 +- .../records/TeamsPermissionsRecord.java | 2 +- ...UserProjectEffectivePermissionsRecord.java | 2 +- .../generated/tables/records/UserRecord.java | 2 +- .../records/UsersPermissionsRecord.java | 2 +- .../records/UsersProjectsRolesRecord.java | 96 +++++ .../tables/records/UsersTeamsRecord.java | 2 +- .../generated/tables/records/VexRecord.java | 2 +- .../ViolationAnalysisCommentRecord.java | 2 +- .../records/ViolationAnalysisRecord.java | 2 +- .../records/VulnerabilitiesTagsRecord.java | 2 +- .../records/VulnerabilityAliasRecord.java | 2 +- .../records/VulnerabilityMetricsRecord.java | 2 +- .../VulnerabilityPolicyBundleRecord.java | 2 +- .../records/VulnerabilityPolicyRecord.java | 2 +- .../tables/records/VulnerabilityRecord.java | 2 +- .../records/VulnerabilityScanRecord.java | 2 +- .../records/VulnerableSoftwareRecord.java | 2 +- ...lnerableSoftwareVulnerabilitiesRecord.java | 2 +- .../tables/records/WorkflowStateRecord.java | 2 +- .../resources/migration/changelog-main.xml | 1 - .../migration/changelog-v5.6.0-roles.xml | 266 -------------- .../resources/migration/changelog-v5.6.0.xml | 258 ++++++++++++++ 155 files changed, 2055 insertions(+), 638 deletions(-) create mode 100644 persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectRoleEffectivePermissions.java create mode 100644 persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Role.java create mode 100644 persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RolesPermissions.java create mode 100644 persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersProjectsRoles.java create mode 100644 persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RoleRecord.java create mode 100644 persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RolesPermissionsRecord.java create mode 100644 persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersProjectsRolesRecord.java delete mode 100644 persistence-migration/src/main/resources/migration/changelog-v5.6.0-roles.xml diff --git a/apiserver/src/main/resources/META-INF/persistence.xml b/apiserver/src/main/resources/META-INF/persistence.xml index df8eb98c8f..ceebdf4bd6 100644 --- a/apiserver/src/main/resources/META-INF/persistence.xml +++ b/apiserver/src/main/resources/META-INF/persistence.xml @@ -40,7 +40,6 @@ org.dependencytrack.model.Component org.dependencytrack.model.ComponentOccurrence org.dependencytrack.model.ComponentProperty - org.dependencytrack.model.IntegrityMetaComponent org.dependencytrack.model.Epss org.dependencytrack.model.FindingAttribution org.dependencytrack.model.IntegrityAnalysis diff --git a/persistence-jooq/pom.xml b/persistence-jooq/pom.xml index b1a90c0a00..e199b10c90 100644 --- a/persistence-jooq/pom.xml +++ b/persistence-jooq/pom.xml @@ -24,8 +24,8 @@ | IDENTITY | IMPACT | INCLUDING | INFO | INSTALLED | IS | KEYS? | LAST | LDAP | LIBRE | LICENSE | LIKELIHOOD | MANAGED | MAPPED | METRICS | NAME | NOTIFICATION | OCCURRENCE | OIDC | OPERATIONAL | OSI | OWASP | PARENT | PATCHED | PERMISSIONS? | POLICY - | PORTFOLIO | PROJECTS? | PROPERTY | PUBLISHER | PURL | REQUIRED | RISK | RR | RULE - | SCAN | SCHEMA | SCORE | SECURITY | SEE | SERVICE | SOFTWARE | START | STATE + | PORTFOLIO | PROJECTS? | PROPERTY | PUBLISHER | PURL | REQUIRED | RISK | ROLES? | RR + | RULE | SCAN | SCHEMA | SCORE | SECURITY | SEE | SERVICE | SOFTWARE | START | STATE | SUBSCRIBER | SW | SWID | TAGS? | TARGET | TEAMS? | TECHNICAL | TOTAL | TYPE | UNAUDITED | UPGRADES | USER(?!NAME)S? | V2 | V3 | VALUE | VECTOR | VERSIONS? | VIOLATIONS? | VULN | VULNERABILITIES | VULNERABILITY | VULNERABLE | WARN | WEIGHT @@ -165,6 +165,7 @@ $1_$2 + @@ -184,6 +185,7 @@
        + @@ -233,9 +235,6 @@ ^(${matcher.pattern})(${matcher.pattern})(${matcher.pattern})_(${matcher.pattern})_FK$ - - $0 - CAMEL $1_$2_$3 @@ -249,9 +248,6 @@ ^(${matcher.pattern})(${matcher.pattern})(${matcher.pattern})_(${matcher.pattern})(${matcher.pattern})_FK$ - - $0 - CAMEL $4_$5 @@ -269,9 +265,6 @@ ^(${matcher.pattern})(${matcher.pattern})_(${matcher.pattern})_FK$ - - $0 - CAMEL $1_$2 @@ -285,9 +278,6 @@ ^(${matcher.pattern})(${matcher.pattern})_(${matcher.pattern})(${matcher.pattern})_FK$ - - $0 - CAMEL $3_$4 @@ -305,9 +295,6 @@ ^(${matcher.pattern})(${matcher.pattern})_(${matcher.pattern})_(${matcher.pattern})(${matcher.pattern})_FK$ - - $0 - CAMEL $4_$5 @@ -325,9 +312,6 @@ ^(${matcher.pattern})(${matcher.pattern})_(${matcher.pattern})_(${matcher.pattern})_FK$ - - $0 - CAMEL $1_$2_$3 @@ -335,16 +319,18 @@ - ^USER_PROJECT_EFFECTIVE_PERMISSIONS_(LDAP|MANAGED|OIDC)USER_FK$ + ^USER_PROJECT_EFFECTIVE_PERMISSIONS_(.*)_FK$ - - $0 - CAMEL - $1_USER + $1 + + + $0 + + diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java index 45363fd859..ab0a3a9928 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java @@ -52,6 +52,8 @@ import org.dependencytrack.persistence.jooq.generated.tables.ProjectsTags; import org.dependencytrack.persistence.jooq.generated.tables.Repository; import org.dependencytrack.persistence.jooq.generated.tables.RepositoryMetaComponent; +import org.dependencytrack.persistence.jooq.generated.tables.Role; +import org.dependencytrack.persistence.jooq.generated.tables.RolesPermissions; import org.dependencytrack.persistence.jooq.generated.tables.ServiceComponent; import org.dependencytrack.persistence.jooq.generated.tables.ServiceComponentsVulnerabilities; import org.dependencytrack.persistence.jooq.generated.tables.Tag; @@ -60,6 +62,7 @@ import org.dependencytrack.persistence.jooq.generated.tables.User; import org.dependencytrack.persistence.jooq.generated.tables.UserProjectEffectivePermissions; import org.dependencytrack.persistence.jooq.generated.tables.UsersPermissions; +import org.dependencytrack.persistence.jooq.generated.tables.UsersProjectsRoles; import org.dependencytrack.persistence.jooq.generated.tables.UsersTeams; import org.dependencytrack.persistence.jooq.generated.tables.Vex; import org.dependencytrack.persistence.jooq.generated.tables.ViolationAnalysis; @@ -87,7 +90,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -316,6 +319,16 @@ public class DefaultSchema extends SchemaImpl { */ public final RepositoryMetaComponent REPOSITORY_META_COMPONENT = RepositoryMetaComponent.REPOSITORY_META_COMPONENT; + /** + * The table ROLE. + */ + public final Role ROLE = Role.ROLE; + + /** + * The table ROLES_PERMISSIONS. + */ + public final RolesPermissions ROLES_PERMISSIONS = RolesPermissions.ROLES_PERMISSIONS; + /** * The table SERVICECOMPONENT. */ @@ -356,6 +369,11 @@ public class DefaultSchema extends SchemaImpl { */ public final UsersPermissions USERS_PERMISSIONS = UsersPermissions.USERS_PERMISSIONS; + /** + * The table USERS_PROJECTS_ROLES. + */ + public final UsersProjectsRoles USERS_PROJECTS_ROLES = UsersProjectsRoles.USERS_PROJECTS_ROLES; + /** * The table USERS_TEAMS. */ @@ -485,6 +503,8 @@ public final List> getTables() { ProjectsTags.PROJECTS_TAGS, Repository.REPOSITORY, RepositoryMetaComponent.REPOSITORY_META_COMPONENT, + Role.ROLE, + RolesPermissions.ROLES_PERMISSIONS, ServiceComponent.SERVICECOMPONENT, ServiceComponentsVulnerabilities.SERVICECOMPONENTS_VULNERABILITIES, Tag.TAG, @@ -493,6 +513,7 @@ public final List> getTables() { User.USER, UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, UsersPermissions.USERS_PERMISSIONS, + UsersProjectsRoles.USERS_PROJECTS_ROLES, UsersTeams.USERS_TEAMS, Vex.VEX, ViolationAnalysis.VIOLATIONANALYSIS, diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Indexes.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Indexes.java index daa724832f..6182ea8d3a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Indexes.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Indexes.java @@ -34,10 +34,12 @@ import org.dependencytrack.persistence.jooq.generated.tables.ProjectMetadata; import org.dependencytrack.persistence.jooq.generated.tables.Repository; import org.dependencytrack.persistence.jooq.generated.tables.RepositoryMetaComponent; +import org.dependencytrack.persistence.jooq.generated.tables.RolesPermissions; import org.dependencytrack.persistence.jooq.generated.tables.ServiceComponent; import org.dependencytrack.persistence.jooq.generated.tables.ServiceComponentsVulnerabilities; import org.dependencytrack.persistence.jooq.generated.tables.Tag; import org.dependencytrack.persistence.jooq.generated.tables.User; +import org.dependencytrack.persistence.jooq.generated.tables.UsersProjectsRoles; import org.dependencytrack.persistence.jooq.generated.tables.Vex; import org.dependencytrack.persistence.jooq.generated.tables.ViolationAnalysis; import org.dependencytrack.persistence.jooq.generated.tables.ViolationAnalysisComment; @@ -60,7 +62,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -147,6 +149,8 @@ public class Indexes { public static final Index REPOSITORY_META_COMPONENT_COMPOUND_IDX = Internal.createIndex(DSL.name("REPOSITORY_META_COMPONENT_COMPOUND_IDX"), RepositoryMetaComponent.REPOSITORY_META_COMPONENT, new OrderField[] { RepositoryMetaComponent.REPOSITORY_META_COMPONENT.repositoryType, RepositoryMetaComponent.REPOSITORY_META_COMPONENT.namespace, RepositoryMetaComponent.REPOSITORY_META_COMPONENT.name }, true); public static final Index REPOSITORY_META_COMPONENT_LASTCHECK_IDX = Internal.createIndex(DSL.name("REPOSITORY_META_COMPONENT_LASTCHECK_IDX"), RepositoryMetaComponent.REPOSITORY_META_COMPONENT, new OrderField[] { RepositoryMetaComponent.REPOSITORY_META_COMPONENT.lastCheck }, false); public static final Index REPOSITORY_UUID_IDX = Internal.createIndex(DSL.name("REPOSITORY_UUID_IDX"), Repository.REPOSITORY, new OrderField[] { Repository.REPOSITORY.uuid }, false); + public static final Index ROLES_PERMISSIONS_PERMISSION_ID_IDX = Internal.createIndex(DSL.name("ROLES_PERMISSIONS_PERMISSION_ID_IDX"), RolesPermissions.ROLES_PERMISSIONS, new OrderField[] { RolesPermissions.ROLES_PERMISSIONS.permissionId }, false); + public static final Index ROLES_PERMISSIONS_ROLE_ID_IDX = Internal.createIndex(DSL.name("ROLES_PERMISSIONS_ROLE_ID_IDX"), RolesPermissions.ROLES_PERMISSIONS, new OrderField[] { RolesPermissions.ROLES_PERMISSIONS.roleId }, false); public static final Index SERVICECOMPONENT_LAST_RISKSCORE_IDX = Internal.createIndex(DSL.name("SERVICECOMPONENT_LAST_RISKSCORE_IDX"), ServiceComponent.SERVICECOMPONENT, new OrderField[] { ServiceComponent.SERVICECOMPONENT.lastRiskScore }, false); public static final Index SERVICECOMPONENT_PARENT_SERVICECOMPONENT_ID_IDX = Internal.createIndex(DSL.name("SERVICECOMPONENT_PARENT_SERVICECOMPONENT_ID_IDX"), ServiceComponent.SERVICECOMPONENT, new OrderField[] { ServiceComponent.SERVICECOMPONENT.parentServiceComponentId }, false); public static final Index SERVICECOMPONENT_PROJECT_ID_IDX = Internal.createIndex(DSL.name("SERVICECOMPONENT_PROJECT_ID_IDX"), ServiceComponent.SERVICECOMPONENT, new OrderField[] { ServiceComponent.SERVICECOMPONENT.projectId }, false); @@ -154,6 +158,8 @@ public class Indexes { public static final Index SERVICECOMPONENTS_VULNERABILITIES_VULNERABILITY_ID_IDX = Internal.createIndex(DSL.name("SERVICECOMPONENTS_VULNERABILITIES_VULNERABILITY_ID_IDX"), ServiceComponentsVulnerabilities.SERVICECOMPONENTS_VULNERABILITIES, new OrderField[] { ServiceComponentsVulnerabilities.SERVICECOMPONENTS_VULNERABILITIES.vulnerabilityId }, false); public static final Index TAG_NAME_IDX = Internal.createIndex(DSL.name("TAG_NAME_IDX"), Tag.TAG, new OrderField[] { Tag.TAG.name }, true); public static final Index USER_USERNAME_IDX = Internal.createIndex(DSL.name("USER_USERNAME_IDX"), User.USER, new OrderField[] { User.USER.username }, true); + public static final Index USERS_PROJECTS_ROLES_ROLE_ID_IDX = Internal.createIndex(DSL.name("USERS_PROJECTS_ROLES_ROLE_ID_IDX"), UsersProjectsRoles.USERS_PROJECTS_ROLES, new OrderField[] { UsersProjectsRoles.USERS_PROJECTS_ROLES.roleId }, false); + public static final Index USERS_PROJECTS_ROLES_USER_PROJECT_IDX = Internal.createIndex(DSL.name("USERS_PROJECTS_ROLES_USER_PROJECT_IDX"), UsersProjectsRoles.USERS_PROJECTS_ROLES, new OrderField[] { UsersProjectsRoles.USERS_PROJECTS_ROLES.userId, UsersProjectsRoles.USERS_PROJECTS_ROLES.projectId }, true); public static final Index VEX_PROJECT_ID_IDX = Internal.createIndex(DSL.name("VEX_PROJECT_ID_IDX"), Vex.VEX, new OrderField[] { Vex.VEX.projectId }, false); public static final Index VIOLATIONANALYSIS_COMPONENT_ID_IDX = Internal.createIndex(DSL.name("VIOLATIONANALYSIS_COMPONENT_ID_IDX"), ViolationAnalysis.VIOLATIONANALYSIS, new OrderField[] { ViolationAnalysis.VIOLATIONANALYSIS.componentId }, false); public static final Index VIOLATIONANALYSIS_POLICYVIOLATION_ID_IDX = Internal.createIndex(DSL.name("VIOLATIONANALYSIS_POLICYVIOLATION_ID_IDX"), ViolationAnalysis.VIOLATIONANALYSIS, new OrderField[] { ViolationAnalysis.VIOLATIONANALYSIS.policyViolationId }, false); diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Keys.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Keys.java index bccc8c33ef..6b98a520d2 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Keys.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Keys.java @@ -49,6 +49,8 @@ import org.dependencytrack.persistence.jooq.generated.tables.ProjectsTags; import org.dependencytrack.persistence.jooq.generated.tables.Repository; import org.dependencytrack.persistence.jooq.generated.tables.RepositoryMetaComponent; +import org.dependencytrack.persistence.jooq.generated.tables.Role; +import org.dependencytrack.persistence.jooq.generated.tables.RolesPermissions; import org.dependencytrack.persistence.jooq.generated.tables.ServiceComponent; import org.dependencytrack.persistence.jooq.generated.tables.ServiceComponentsVulnerabilities; import org.dependencytrack.persistence.jooq.generated.tables.Tag; @@ -57,6 +59,7 @@ import org.dependencytrack.persistence.jooq.generated.tables.User; import org.dependencytrack.persistence.jooq.generated.tables.UserProjectEffectivePermissions; import org.dependencytrack.persistence.jooq.generated.tables.UsersPermissions; +import org.dependencytrack.persistence.jooq.generated.tables.UsersProjectsRoles; import org.dependencytrack.persistence.jooq.generated.tables.UsersTeams; import org.dependencytrack.persistence.jooq.generated.tables.Vex; import org.dependencytrack.persistence.jooq.generated.tables.ViolationAnalysis; @@ -114,6 +117,8 @@ import org.dependencytrack.persistence.jooq.generated.tables.records.ProjectsTagsRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.RepositoryMetaComponentRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.RepositoryRecord; +import org.dependencytrack.persistence.jooq.generated.tables.records.RoleRecord; +import org.dependencytrack.persistence.jooq.generated.tables.records.RolesPermissionsRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.ServiceComponentRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.ServiceComponentsVulnerabilitiesRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.TagRecord; @@ -122,6 +127,7 @@ import org.dependencytrack.persistence.jooq.generated.tables.records.UserProjectEffectivePermissionsRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.UserRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.UsersPermissionsRecord; +import org.dependencytrack.persistence.jooq.generated.tables.records.UsersProjectsRolesRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.UsersTeamsRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.VexRecord; import org.dependencytrack.persistence.jooq.generated.tables.records.ViolationAnalysisCommentRecord; @@ -152,7 +158,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -225,6 +231,10 @@ public class Keys { public static final UniqueKey REPOSITORY_COMPOUND_IDX = Internal.createUniqueKey(Repository.REPOSITORY, DSL.name("REPOSITORY_COMPOUND_IDX"), new TableField[] { Repository.REPOSITORY.type, Repository.REPOSITORY.identifier }, true); public static final UniqueKey REPOSITORY_PK = Internal.createUniqueKey(Repository.REPOSITORY, DSL.name("REPOSITORY_PK"), new TableField[] { Repository.REPOSITORY.id }, true); public static final UniqueKey REPOSITORY_META_COMPONENT_PK = Internal.createUniqueKey(RepositoryMetaComponent.REPOSITORY_META_COMPONENT, DSL.name("REPOSITORY_META_COMPONENT_PK"), new TableField[] { RepositoryMetaComponent.REPOSITORY_META_COMPONENT.id }, true); + public static final UniqueKey ROLE_NAME_IDX = Internal.createUniqueKey(Role.ROLE, DSL.name("ROLE_NAME_IDX"), new TableField[] { Role.ROLE.name }, true); + public static final UniqueKey ROLE_PK = Internal.createUniqueKey(Role.ROLE, DSL.name("ROLE_PK"), new TableField[] { Role.ROLE.id }, true); + public static final UniqueKey ROLE_UUID_IDX = Internal.createUniqueKey(Role.ROLE, DSL.name("ROLE_UUID_IDX"), new TableField[] { Role.ROLE.uuid }, true); + public static final UniqueKey ROLES_PERMISSIONS_COMPOSITE_IDX = Internal.createUniqueKey(RolesPermissions.ROLES_PERMISSIONS, DSL.name("ROLES_PERMISSIONS_COMPOSITE_IDX"), new TableField[] { RolesPermissions.ROLES_PERMISSIONS.roleId, RolesPermissions.ROLES_PERMISSIONS.permissionId }, true); public static final UniqueKey SERVICECOMPONENT_PK = Internal.createUniqueKey(ServiceComponent.SERVICECOMPONENT, DSL.name("SERVICECOMPONENT_PK"), new TableField[] { ServiceComponent.SERVICECOMPONENT.id }, true); public static final UniqueKey SERVICECOMPONENT_UUID_IDX = Internal.createUniqueKey(ServiceComponent.SERVICECOMPONENT, DSL.name("SERVICECOMPONENT_UUID_IDX"), new TableField[] { ServiceComponent.SERVICECOMPONENT.uuid }, true); public static final UniqueKey TAG_PK = Internal.createUniqueKey(Tag.TAG, DSL.name("TAG_PK"), new TableField[] { Tag.TAG.id }, true); @@ -263,27 +273,27 @@ public class Keys { public static final ForeignKey AFFECTEDVERSIONATTRIBUTION_VULNERABILITY_FK = Internal.createForeignKey(AffectedVersionAttribution.AFFECTEDVERSIONATTRIBUTION, DSL.name("AFFECTEDVERSIONATTRIBUTION_VULNERABILITY_FK"), new TableField[] { AffectedVersionAttribution.AFFECTEDVERSIONATTRIBUTION.vulnerability }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey AFFECTEDVERSIONATTRIBUTION_VULNERABLESOFTWARE_FK = Internal.createForeignKey(AffectedVersionAttribution.AFFECTEDVERSIONATTRIBUTION, DSL.name("AFFECTEDVERSIONATTRIBUTION_VULNERABLESOFTWARE_FK"), new TableField[] { AffectedVersionAttribution.AFFECTEDVERSIONATTRIBUTION.vulnerableSoftware }, Keys.VULNERABLESOFTWARE_PK, new TableField[] { VulnerableSoftware.VULNERABLESOFTWARE.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey ANALYSIS__ANALYSIS_COMPONENT_FK = Internal.createForeignKey(Analysis.ANALYSIS, DSL.name("ANALYSIS_COMPONENT_FK"), new TableField[] { Analysis.ANALYSIS.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey ANALYSIS__ANALYSIS_PROJECT_FK = Internal.createForeignKey(Analysis.ANALYSIS, DSL.name("ANALYSIS_PROJECT_FK"), new TableField[] { Analysis.ANALYSIS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey ANALYSIS__ANALYSIS_VULNERABILITY_FK = Internal.createForeignKey(Analysis.ANALYSIS, DSL.name("ANALYSIS_VULNERABILITY_FK"), new TableField[] { Analysis.ANALYSIS.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey ANALYSIS__ANALYSIS_VULNERABILITY_POLICY_ID_FK = Internal.createForeignKey(Analysis.ANALYSIS, DSL.name("ANALYSIS_VULNERABILITY_POLICY_ID_FK"), new TableField[] { Analysis.ANALYSIS.vulnerabilityPolicyId }, Keys.VULNERABILITYPOLICY_PK, new TableField[] { VulnerabilityPolicy.VULNERABILITY_POLICY.id }, true, ForeignKeyRule.NO_ACTION, ForeignKeyRule.NO_ACTION); + public static final ForeignKey ANALYSIS_COMPONENT_FK = Internal.createForeignKey(Analysis.ANALYSIS, DSL.name("ANALYSIS_COMPONENT_FK"), new TableField[] { Analysis.ANALYSIS.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey ANALYSIS_PROJECT_FK = Internal.createForeignKey(Analysis.ANALYSIS, DSL.name("ANALYSIS_PROJECT_FK"), new TableField[] { Analysis.ANALYSIS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey ANALYSIS_VULNERABILITY_FK = Internal.createForeignKey(Analysis.ANALYSIS, DSL.name("ANALYSIS_VULNERABILITY_FK"), new TableField[] { Analysis.ANALYSIS.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey ANALYSIS_VULNERABILITY_POLICY_ID_FK = Internal.createForeignKey(Analysis.ANALYSIS, DSL.name("ANALYSIS_VULNERABILITY_POLICY_ID_FK"), new TableField[] { Analysis.ANALYSIS.vulnerabilityPolicyId }, Keys.VULNERABILITYPOLICY_PK, new TableField[] { VulnerabilityPolicy.VULNERABILITY_POLICY.id }, true, ForeignKeyRule.NO_ACTION, ForeignKeyRule.NO_ACTION); public static final ForeignKey ANALYSISCOMMENT_ANALYSIS_FK = Internal.createForeignKey(AnalysisComment.ANALYSISCOMMENT, DSL.name("ANALYSISCOMMENT_ANALYSIS_FK"), new TableField[] { AnalysisComment.ANALYSISCOMMENT.analysisId }, Keys.ANALYSIS_PK, new TableField[] { Analysis.ANALYSIS.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey APIKEYS_TEAMS_APIKEY_FK = Internal.createForeignKey(ApiKeysTeams.APIKEYS_TEAMS, DSL.name("APIKEYS_TEAMS_APIKEY_FK"), new TableField[] { ApiKeysTeams.APIKEYS_TEAMS.apiKeyId }, Keys.APIKEY_PK, new TableField[] { ApiKey.APIKEY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey APIKEYS_TEAMS_TEAM_FK = Internal.createForeignKey(ApiKeysTeams.APIKEYS_TEAMS, DSL.name("APIKEYS_TEAMS_TEAM_FK"), new TableField[] { ApiKeysTeams.APIKEYS_TEAMS.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey BOM__BOM_PROJECT_FK = Internal.createForeignKey(Bom.BOM, DSL.name("BOM_PROJECT_FK"), new TableField[] { Bom.BOM.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey COMPONENT__COMPONENT_COMPONENT_FK = Internal.createForeignKey(Component.COMPONENT, DSL.name("COMPONENT_COMPONENT_FK"), new TableField[] { Component.COMPONENT.parentComponentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey COMPONENT__COMPONENT_LICENSE_FK = Internal.createForeignKey(Component.COMPONENT, DSL.name("COMPONENT_LICENSE_FK"), new TableField[] { Component.COMPONENT.licenseId }, Keys.LICENSE_PK, new TableField[] { License.LICENSE.id }, true, ForeignKeyRule.NO_ACTION, ForeignKeyRule.NO_ACTION); - public static final ForeignKey COMPONENT__COMPONENT_PROJECT_FK = Internal.createForeignKey(Component.COMPONENT, DSL.name("COMPONENT_PROJECT_FK"), new TableField[] { Component.COMPONENT.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey COMPONENT_OCCURRENCE__COMPONENT_OCCURRENCE_COMPONENT_FK = Internal.createForeignKey(ComponentOccurrence.COMPONENT_OCCURRENCE, DSL.name("COMPONENT_OCCURRENCE_COMPONENT_FK"), new TableField[] { ComponentOccurrence.COMPONENT_OCCURRENCE.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey COMPONENT_PROPERTY__COMPONENT_PROPERTY_COMPONENT_ID_FK = Internal.createForeignKey(ComponentProperty.COMPONENT_PROPERTY, DSL.name("COMPONENT_PROPERTY_COMPONENT_ID_FK"), new TableField[] { ComponentProperty.COMPONENT_PROPERTY.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey COMPONENTS_VULNERABILITIES__COMPONENTS_VULNERABILITIES_COMPONENT_FK = Internal.createForeignKey(ComponentsVulnerabilities.COMPONENTS_VULNERABILITIES, DSL.name("COMPONENTS_VULNERABILITIES_COMPONENT_FK"), new TableField[] { ComponentsVulnerabilities.COMPONENTS_VULNERABILITIES.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey COMPONENTS_VULNERABILITIES__COMPONENTS_VULNERABILITIES_VULNERABILITY_FK = Internal.createForeignKey(ComponentsVulnerabilities.COMPONENTS_VULNERABILITIES, DSL.name("COMPONENTS_VULNERABILITIES_VULNERABILITY_FK"), new TableField[] { ComponentsVulnerabilities.COMPONENTS_VULNERABILITIES.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey BOM_PROJECT_FK = Internal.createForeignKey(Bom.BOM, DSL.name("BOM_PROJECT_FK"), new TableField[] { Bom.BOM.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey COMPONENT_COMPONENT_FK = Internal.createForeignKey(Component.COMPONENT, DSL.name("COMPONENT_COMPONENT_FK"), new TableField[] { Component.COMPONENT.parentComponentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey COMPONENT_LICENSE_FK = Internal.createForeignKey(Component.COMPONENT, DSL.name("COMPONENT_LICENSE_FK"), new TableField[] { Component.COMPONENT.licenseId }, Keys.LICENSE_PK, new TableField[] { License.LICENSE.id }, true, ForeignKeyRule.NO_ACTION, ForeignKeyRule.NO_ACTION); + public static final ForeignKey COMPONENT_PROJECT_FK = Internal.createForeignKey(Component.COMPONENT, DSL.name("COMPONENT_PROJECT_FK"), new TableField[] { Component.COMPONENT.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey COMPONENT_OCCURRENCE_COMPONENT_FK = Internal.createForeignKey(ComponentOccurrence.COMPONENT_OCCURRENCE, DSL.name("COMPONENT_OCCURRENCE_COMPONENT_FK"), new TableField[] { ComponentOccurrence.COMPONENT_OCCURRENCE.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey COMPONENT_PROPERTY_COMPONENT_ID_FK = Internal.createForeignKey(ComponentProperty.COMPONENT_PROPERTY, DSL.name("COMPONENT_PROPERTY_COMPONENT_ID_FK"), new TableField[] { ComponentProperty.COMPONENT_PROPERTY.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey COMPONENTS_VULNERABILITIES_COMPONENT_FK = Internal.createForeignKey(ComponentsVulnerabilities.COMPONENTS_VULNERABILITIES, DSL.name("COMPONENTS_VULNERABILITIES_COMPONENT_FK"), new TableField[] { ComponentsVulnerabilities.COMPONENTS_VULNERABILITIES.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey COMPONENTS_VULNERABILITIES_VULNERABILITY_FK = Internal.createForeignKey(ComponentsVulnerabilities.COMPONENTS_VULNERABILITIES, DSL.name("COMPONENTS_VULNERABILITIES_VULNERABILITY_FK"), new TableField[] { ComponentsVulnerabilities.COMPONENTS_VULNERABILITIES.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey DEPENDENCYMETRICS_COMPONENT_FK = Internal.createForeignKey(DependencyMetrics.DEPENDENCYMETRICS, DSL.name("DEPENDENCYMETRICS_COMPONENT_FK"), new TableField[] { DependencyMetrics.DEPENDENCYMETRICS.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey DEPENDENCYMETRICS_PROJECT_FK = Internal.createForeignKey(DependencyMetrics.DEPENDENCYMETRICS, DSL.name("DEPENDENCYMETRICS_PROJECT_FK"), new TableField[] { DependencyMetrics.DEPENDENCYMETRICS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey FINDINGATTRIBUTION_COMPONENT_FK = Internal.createForeignKey(FindingAttribution.FINDINGATTRIBUTION, DSL.name("FINDINGATTRIBUTION_COMPONENT_FK"), new TableField[] { FindingAttribution.FINDINGATTRIBUTION.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey FINDINGATTRIBUTION_PROJECT_FK = Internal.createForeignKey(FindingAttribution.FINDINGATTRIBUTION, DSL.name("FINDINGATTRIBUTION_PROJECT_FK"), new TableField[] { FindingAttribution.FINDINGATTRIBUTION.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey FINDINGATTRIBUTION_VULNERABILITY_FK = Internal.createForeignKey(FindingAttribution.FINDINGATTRIBUTION, DSL.name("FINDINGATTRIBUTION_VULNERABILITY_FK"), new TableField[] { FindingAttribution.FINDINGATTRIBUTION.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey INTEGRITY_ANALYSIS__INTEGRITY_ANALYSIS_COMPONENT_FK = Internal.createForeignKey(IntegrityAnalysis.INTEGRITY_ANALYSIS, DSL.name("INTEGRITY_ANALYSIS_COMPONENT_FK"), new TableField[] { IntegrityAnalysis.INTEGRITY_ANALYSIS.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey INTEGRITY_ANALYSIS_COMPONENT_FK = Internal.createForeignKey(IntegrityAnalysis.INTEGRITY_ANALYSIS, DSL.name("INTEGRITY_ANALYSIS_COMPONENT_FK"), new TableField[] { IntegrityAnalysis.INTEGRITY_ANALYSIS.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey LICENSEGROUP_LICENSE_LICENSE_FK = Internal.createForeignKey(LicenseGroupLicense.LICENSEGROUP_LICENSE, DSL.name("LICENSEGROUP_LICENSE_LICENSE_FK"), new TableField[] { LicenseGroupLicense.LICENSEGROUP_LICENSE.licenseId }, Keys.LICENSE_PK, new TableField[] { License.LICENSE.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey LICENSEGROUP_LICENSE_LICENSEGROUP_FK = Internal.createForeignKey(LicenseGroupLicense.LICENSEGROUP_LICENSE, DSL.name("LICENSEGROUP_LICENSE_LICENSEGROUP_FK"), new TableField[] { LicenseGroupLicense.LICENSEGROUP_LICENSE.licenseGroupId }, Keys.LICENSEGROUP_PK, new TableField[] { LicenseGroup.LICENSEGROUP.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey MAPPEDLDAPGROUP_TEAM_FK = Internal.createForeignKey(MappedLdapGroup.MAPPEDLDAPGROUP, DSL.name("MAPPEDLDAPGROUP_TEAM_FK"), new TableField[] { MappedLdapGroup.MAPPEDLDAPGROUP.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); @@ -296,46 +306,51 @@ public class Keys { public static final ForeignKey NOTIFICATIONRULE_TAGS_TAG_FK = Internal.createForeignKey(NotificationRuleTags.NOTIFICATIONRULE_TAGS, DSL.name("NOTIFICATIONRULE_TAGS_TAG_FK"), new TableField[] { NotificationRuleTags.NOTIFICATIONRULE_TAGS.tagId }, Keys.TAG_PK, new TableField[] { Tag.TAG.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey NOTIFICATIONRULE_TEAMS_NOTIFICATIONRULE_FK = Internal.createForeignKey(NotificationRuleTeams.NOTIFICATIONRULE_TEAMS, DSL.name("NOTIFICATIONRULE_TEAMS_NOTIFICATIONRULE_FK"), new TableField[] { NotificationRuleTeams.NOTIFICATIONRULE_TEAMS.notificationRuleId }, Keys.NOTIFICATIONRULE_PK, new TableField[] { NotificationRule.NOTIFICATIONRULE.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey NOTIFICATIONRULE_TEAMS_TEAM_FK = Internal.createForeignKey(NotificationRuleTeams.NOTIFICATIONRULE_TEAMS, DSL.name("NOTIFICATIONRULE_TEAMS_TEAM_FK"), new TableField[] { NotificationRuleTeams.NOTIFICATIONRULE_TEAMS.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey POLICY_PROJECTS__POLICY_PROJECTS_POLICY_FK = Internal.createForeignKey(PolicyProjects.POLICY_PROJECTS, DSL.name("POLICY_PROJECTS_POLICY_FK"), new TableField[] { PolicyProjects.POLICY_PROJECTS.policyId }, Keys.POLICY_PK, new TableField[] { Policy.POLICY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey POLICY_PROJECTS__POLICY_PROJECTS_PROJECT_FK = Internal.createForeignKey(PolicyProjects.POLICY_PROJECTS, DSL.name("POLICY_PROJECTS_PROJECT_FK"), new TableField[] { PolicyProjects.POLICY_PROJECTS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey POLICY_TAGS__POLICY_TAGS_POLICY_FK = Internal.createForeignKey(PolicyTags.POLICY_TAGS, DSL.name("POLICY_TAGS_POLICY_FK"), new TableField[] { PolicyTags.POLICY_TAGS.policyId }, Keys.POLICY_PK, new TableField[] { Policy.POLICY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey POLICY_TAGS__POLICY_TAGS_TAG_FK = Internal.createForeignKey(PolicyTags.POLICY_TAGS, DSL.name("POLICY_TAGS_TAG_FK"), new TableField[] { PolicyTags.POLICY_TAGS.tagId }, Keys.TAG_PK, new TableField[] { Tag.TAG.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey POLICY_PROJECTS_POLICY_FK = Internal.createForeignKey(PolicyProjects.POLICY_PROJECTS, DSL.name("POLICY_PROJECTS_POLICY_FK"), new TableField[] { PolicyProjects.POLICY_PROJECTS.policyId }, Keys.POLICY_PK, new TableField[] { Policy.POLICY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey POLICY_PROJECTS_PROJECT_FK = Internal.createForeignKey(PolicyProjects.POLICY_PROJECTS, DSL.name("POLICY_PROJECTS_PROJECT_FK"), new TableField[] { PolicyProjects.POLICY_PROJECTS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey POLICY_TAGS_POLICY_FK = Internal.createForeignKey(PolicyTags.POLICY_TAGS, DSL.name("POLICY_TAGS_POLICY_FK"), new TableField[] { PolicyTags.POLICY_TAGS.policyId }, Keys.POLICY_PK, new TableField[] { Policy.POLICY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey POLICY_TAGS_TAG_FK = Internal.createForeignKey(PolicyTags.POLICY_TAGS, DSL.name("POLICY_TAGS_TAG_FK"), new TableField[] { PolicyTags.POLICY_TAGS.tagId }, Keys.TAG_PK, new TableField[] { Tag.TAG.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey POLICYCONDITION_POLICY_FK = Internal.createForeignKey(PolicyCondition.POLICYCONDITION, DSL.name("POLICYCONDITION_POLICY_FK"), new TableField[] { PolicyCondition.POLICYCONDITION.policyId }, Keys.POLICY_PK, new TableField[] { Policy.POLICY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey POLICYVIOLATION_COMPONENT_FK = Internal.createForeignKey(PolicyViolation.POLICYVIOLATION, DSL.name("POLICYVIOLATION_COMPONENT_FK"), new TableField[] { PolicyViolation.POLICYVIOLATION.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey POLICYVIOLATION_POLICYCONDITION_FK = Internal.createForeignKey(PolicyViolation.POLICYVIOLATION, DSL.name("POLICYVIOLATION_POLICYCONDITION_FK"), new TableField[] { PolicyViolation.POLICYVIOLATION.policyConditionId }, Keys.POLICYCONDITION_PK, new TableField[] { PolicyCondition.POLICYCONDITION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey POLICYVIOLATION_PROJECT_FK = Internal.createForeignKey(PolicyViolation.POLICYVIOLATION, DSL.name("POLICYVIOLATION_PROJECT_FK"), new TableField[] { PolicyViolation.POLICYVIOLATION.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECT__PROJECT_PROJECT_FK = Internal.createForeignKey(Project.PROJECT, DSL.name("PROJECT_PROJECT_FK"), new TableField[] { Project.PROJECT.parentProjectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECT_ACCESS_TEAMS__PROJECT_ACCESS_TEAMS_PROJECT_FK = Internal.createForeignKey(ProjectAccessTeams.PROJECT_ACCESS_TEAMS, DSL.name("PROJECT_ACCESS_TEAMS_PROJECT_FK"), new TableField[] { ProjectAccessTeams.PROJECT_ACCESS_TEAMS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECT_ACCESS_TEAMS__PROJECT_ACCESS_TEAMS_TEAM_FK = Internal.createForeignKey(ProjectAccessTeams.PROJECT_ACCESS_TEAMS, DSL.name("PROJECT_ACCESS_TEAMS_TEAM_FK"), new TableField[] { ProjectAccessTeams.PROJECT_ACCESS_TEAMS.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECT_HIERARCHY__PROJECT_HIERARCHY_CHILD_PROJECT_FK = Internal.createForeignKey(ProjectHierarchy.PROJECT_HIERARCHY, DSL.name("PROJECT_HIERARCHY_CHILD_PROJECT_FK"), new TableField[] { ProjectHierarchy.PROJECT_HIERARCHY.childProjectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.NO_ACTION, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECT_HIERARCHY__PROJECT_HIERARCHY_PARENT_PROJECT_FK = Internal.createForeignKey(ProjectHierarchy.PROJECT_HIERARCHY, DSL.name("PROJECT_HIERARCHY_PARENT_PROJECT_FK"), new TableField[] { ProjectHierarchy.PROJECT_HIERARCHY.parentProjectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.NO_ACTION, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECT_METADATA__PROJECT_METADATA_PROJECT_ID_FK = Internal.createForeignKey(ProjectMetadata.PROJECT_METADATA, DSL.name("PROJECT_METADATA_PROJECT_ID_FK"), new TableField[] { ProjectMetadata.PROJECT_METADATA.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECT_PROPERTY__PROJECT_PROPERTY_PROJECT_FK = Internal.createForeignKey(ProjectProperty.PROJECT_PROPERTY, DSL.name("PROJECT_PROPERTY_PROJECT_FK"), new TableField[] { ProjectProperty.PROJECT_PROPERTY.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECT_PROJECT_FK = Internal.createForeignKey(Project.PROJECT, DSL.name("PROJECT_PROJECT_FK"), new TableField[] { Project.PROJECT.parentProjectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECT_ACCESS_TEAMS_PROJECT_FK = Internal.createForeignKey(ProjectAccessTeams.PROJECT_ACCESS_TEAMS, DSL.name("PROJECT_ACCESS_TEAMS_PROJECT_FK"), new TableField[] { ProjectAccessTeams.PROJECT_ACCESS_TEAMS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECT_ACCESS_TEAMS_TEAM_FK = Internal.createForeignKey(ProjectAccessTeams.PROJECT_ACCESS_TEAMS, DSL.name("PROJECT_ACCESS_TEAMS_TEAM_FK"), new TableField[] { ProjectAccessTeams.PROJECT_ACCESS_TEAMS.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECT_HIERARCHY_CHILD_PROJECT_FK = Internal.createForeignKey(ProjectHierarchy.PROJECT_HIERARCHY, DSL.name("PROJECT_HIERARCHY_CHILD_PROJECT_FK"), new TableField[] { ProjectHierarchy.PROJECT_HIERARCHY.childProjectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.NO_ACTION, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECT_HIERARCHY_PARENT_PROJECT_FK = Internal.createForeignKey(ProjectHierarchy.PROJECT_HIERARCHY, DSL.name("PROJECT_HIERARCHY_PARENT_PROJECT_FK"), new TableField[] { ProjectHierarchy.PROJECT_HIERARCHY.parentProjectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.NO_ACTION, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECT_METADATA_PROJECT_ID_FK = Internal.createForeignKey(ProjectMetadata.PROJECT_METADATA, DSL.name("PROJECT_METADATA_PROJECT_ID_FK"), new TableField[] { ProjectMetadata.PROJECT_METADATA.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECT_PROPERTY_PROJECT_FK = Internal.createForeignKey(ProjectProperty.PROJECT_PROPERTY, DSL.name("PROJECT_PROPERTY_PROJECT_FK"), new TableField[] { ProjectProperty.PROJECT_PROPERTY.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey PROJECTMETRICS_PROJECT_FK = Internal.createForeignKey(ProjectMetrics.PROJECTMETRICS, DSL.name("PROJECTMETRICS_PROJECT_FK"), new TableField[] { ProjectMetrics.PROJECTMETRICS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECTS_TAGS__PROJECTS_TAGS_PROJECT_FK = Internal.createForeignKey(ProjectsTags.PROJECTS_TAGS, DSL.name("PROJECTS_TAGS_PROJECT_FK"), new TableField[] { ProjectsTags.PROJECTS_TAGS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey PROJECTS_TAGS__PROJECTS_TAGS_TAG_FK = Internal.createForeignKey(ProjectsTags.PROJECTS_TAGS, DSL.name("PROJECTS_TAGS_TAG_FK"), new TableField[] { ProjectsTags.PROJECTS_TAGS.tagId }, Keys.TAG_PK, new TableField[] { Tag.TAG.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECTS_TAGS_PROJECT_FK = Internal.createForeignKey(ProjectsTags.PROJECTS_TAGS, DSL.name("PROJECTS_TAGS_PROJECT_FK"), new TableField[] { ProjectsTags.PROJECTS_TAGS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey PROJECTS_TAGS_TAG_FK = Internal.createForeignKey(ProjectsTags.PROJECTS_TAGS, DSL.name("PROJECTS_TAGS_TAG_FK"), new TableField[] { ProjectsTags.PROJECTS_TAGS.tagId }, Keys.TAG_PK, new TableField[] { Tag.TAG.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey ROLES_PERMISSIONS_PERMISSION_FK = Internal.createForeignKey(RolesPermissions.ROLES_PERMISSIONS, DSL.name("ROLES_PERMISSIONS_PERMISSION_FK"), new TableField[] { RolesPermissions.ROLES_PERMISSIONS.permissionId }, Keys.PERMISSION_PK, new TableField[] { Permission.PERMISSION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey ROLES_PERMISSIONS_ROLE_FK = Internal.createForeignKey(RolesPermissions.ROLES_PERMISSIONS, DSL.name("ROLES_PERMISSIONS_ROLE_FK"), new TableField[] { RolesPermissions.ROLES_PERMISSIONS.roleId }, Keys.ROLE_PK, new TableField[] { Role.ROLE.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey SERVICECOMPONENT_PROJECT_FK = Internal.createForeignKey(ServiceComponent.SERVICECOMPONENT, DSL.name("SERVICECOMPONENT_PROJECT_FK"), new TableField[] { ServiceComponent.SERVICECOMPONENT.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey SERVICECOMPONENT_SERVICECOMPONENT_FK = Internal.createForeignKey(ServiceComponent.SERVICECOMPONENT, DSL.name("SERVICECOMPONENT_SERVICECOMPONENT_FK"), new TableField[] { ServiceComponent.SERVICECOMPONENT.parentServiceComponentId }, Keys.SERVICECOMPONENT_PK, new TableField[] { ServiceComponent.SERVICECOMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey SERVICECOMPONENTS_VULNERABILITIES_SERVICECOMPONENT_FK = Internal.createForeignKey(ServiceComponentsVulnerabilities.SERVICECOMPONENTS_VULNERABILITIES, DSL.name("SERVICECOMPONENTS_VULNERABILITIES_SERVICECOMPONENT_FK"), new TableField[] { ServiceComponentsVulnerabilities.SERVICECOMPONENTS_VULNERABILITIES.serviceComponentId }, Keys.SERVICECOMPONENT_PK, new TableField[] { ServiceComponent.SERVICECOMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey SERVICECOMPONENTS_VULNERABILITIES_VULNERABILITY_FK = Internal.createForeignKey(ServiceComponentsVulnerabilities.SERVICECOMPONENTS_VULNERABILITIES, DSL.name("SERVICECOMPONENTS_VULNERABILITIES_VULNERABILITY_FK"), new TableField[] { ServiceComponentsVulnerabilities.SERVICECOMPONENTS_VULNERABILITIES.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey TEAMS_PERMISSIONS__TEAMS_PERMISSIONS_PERMISSION_FK = Internal.createForeignKey(TeamsPermissions.TEAMS_PERMISSIONS, DSL.name("TEAMS_PERMISSIONS_PERMISSION_FK"), new TableField[] { TeamsPermissions.TEAMS_PERMISSIONS.permissionId }, Keys.PERMISSION_PK, new TableField[] { Permission.PERMISSION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey TEAMS_PERMISSIONS__TEAMS_PERMISSIONS_TEAM_FK = Internal.createForeignKey(TeamsPermissions.TEAMS_PERMISSIONS, DSL.name("TEAMS_PERMISSIONS_TEAM_FK"), new TableField[] { TeamsPermissions.TEAMS_PERMISSIONS.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK = Internal.createForeignKey(UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, DSL.name("USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK"), new TableField[] { UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS.permissionId }, Keys.PERMISSION_PK, new TableField[] { Permission.PERMISSION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK = Internal.createForeignKey(UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, DSL.name("USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK"), new TableField[] { UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS.permissionName }, Keys.PERMISSION_IDX, new TableField[] { Permission.PERMISSION.name }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK = Internal.createForeignKey(UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, DSL.name("USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK"), new TableField[] { UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK = Internal.createForeignKey(UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, DSL.name("USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK"), new TableField[] { UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS.userId }, Keys.USER_PK, new TableField[] { User.USER.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey USERS_PERMISSIONS__USERS_PERMISSIONS_PERMISSION_FK = Internal.createForeignKey(UsersPermissions.USERS_PERMISSIONS, DSL.name("USERS_PERMISSIONS_PERMISSION_FK"), new TableField[] { UsersPermissions.USERS_PERMISSIONS.permissionId }, Keys.PERMISSION_PK, new TableField[] { Permission.PERMISSION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey USERS_PERMISSIONS__USERS_PERMISSIONS_USER_FK = Internal.createForeignKey(UsersPermissions.USERS_PERMISSIONS, DSL.name("USERS_PERMISSIONS_USER_FK"), new TableField[] { UsersPermissions.USERS_PERMISSIONS.userId }, Keys.USER_PK, new TableField[] { User.USER.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey USERS_TEAMS__USERS_TEAMS_TEAM_FK = Internal.createForeignKey(UsersTeams.USERS_TEAMS, DSL.name("USERS_TEAMS_TEAM_FK"), new TableField[] { UsersTeams.USERS_TEAMS.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey USERS_TEAMS__USERS_TEAMS_USER_FK = Internal.createForeignKey(UsersTeams.USERS_TEAMS, DSL.name("USERS_TEAMS_USER_FK"), new TableField[] { UsersTeams.USERS_TEAMS.userId }, Keys.USER_PK, new TableField[] { User.USER.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey VEX__VEX_PROJECT_FK = Internal.createForeignKey(Vex.VEX, DSL.name("VEX_PROJECT_FK"), new TableField[] { Vex.VEX.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey TEAMS_PERMISSIONS_PERMISSION_FK = Internal.createForeignKey(TeamsPermissions.TEAMS_PERMISSIONS, DSL.name("TEAMS_PERMISSIONS_PERMISSION_FK"), new TableField[] { TeamsPermissions.TEAMS_PERMISSIONS.permissionId }, Keys.PERMISSION_PK, new TableField[] { Permission.PERMISSION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey TEAMS_PERMISSIONS_TEAM_FK = Internal.createForeignKey(TeamsPermissions.TEAMS_PERMISSIONS, DSL.name("TEAMS_PERMISSIONS_TEAM_FK"), new TableField[] { TeamsPermissions.TEAMS_PERMISSIONS.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK = Internal.createForeignKey(UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, DSL.name("USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK"), new TableField[] { UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS.permissionId }, Keys.PERMISSION_PK, new TableField[] { Permission.PERMISSION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK = Internal.createForeignKey(UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, DSL.name("USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK"), new TableField[] { UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS.permissionName }, Keys.PERMISSION_IDX, new TableField[] { Permission.PERMISSION.name }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK = Internal.createForeignKey(UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, DSL.name("USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK"), new TableField[] { UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK = Internal.createForeignKey(UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS, DSL.name("USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK"), new TableField[] { UserProjectEffectivePermissions.USER_PROJECT_EFFECTIVE_PERMISSIONS.userId }, Keys.USER_PK, new TableField[] { User.USER.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USERS_PERMISSIONS_PERMISSION_FK = Internal.createForeignKey(UsersPermissions.USERS_PERMISSIONS, DSL.name("USERS_PERMISSIONS_PERMISSION_FK"), new TableField[] { UsersPermissions.USERS_PERMISSIONS.permissionId }, Keys.PERMISSION_PK, new TableField[] { Permission.PERMISSION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USERS_PERMISSIONS_USER_FK = Internal.createForeignKey(UsersPermissions.USERS_PERMISSIONS, DSL.name("USERS_PERMISSIONS_USER_FK"), new TableField[] { UsersPermissions.USERS_PERMISSIONS.userId }, Keys.USER_PK, new TableField[] { User.USER.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USERS_PROJECTS_ROLES_PROJECT_FK = Internal.createForeignKey(UsersProjectsRoles.USERS_PROJECTS_ROLES, DSL.name("USERS_PROJECTS_ROLES_PROJECT_FK"), new TableField[] { UsersProjectsRoles.USERS_PROJECTS_ROLES.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USERS_PROJECTS_ROLES_ROLE_FK = Internal.createForeignKey(UsersProjectsRoles.USERS_PROJECTS_ROLES, DSL.name("USERS_PROJECTS_ROLES_ROLE_FK"), new TableField[] { UsersProjectsRoles.USERS_PROJECTS_ROLES.roleId }, Keys.ROLE_PK, new TableField[] { Role.ROLE.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USERS_PROJECTS_ROLES_USER_FK = Internal.createForeignKey(UsersProjectsRoles.USERS_PROJECTS_ROLES, DSL.name("USERS_PROJECTS_ROLES_USER_FK"), new TableField[] { UsersProjectsRoles.USERS_PROJECTS_ROLES.userId }, Keys.USER_PK, new TableField[] { User.USER.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USERS_TEAMS_TEAM_FK = Internal.createForeignKey(UsersTeams.USERS_TEAMS, DSL.name("USERS_TEAMS_TEAM_FK"), new TableField[] { UsersTeams.USERS_TEAMS.teamId }, Keys.TEAM_PK, new TableField[] { Team.TEAM.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey USERS_TEAMS_USER_FK = Internal.createForeignKey(UsersTeams.USERS_TEAMS, DSL.name("USERS_TEAMS_USER_FK"), new TableField[] { UsersTeams.USERS_TEAMS.userId }, Keys.USER_PK, new TableField[] { User.USER.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey VEX_PROJECT_FK = Internal.createForeignKey(Vex.VEX, DSL.name("VEX_PROJECT_FK"), new TableField[] { Vex.VEX.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey VIOLATIONANALYSIS_COMPONENT_FK = Internal.createForeignKey(ViolationAnalysis.VIOLATIONANALYSIS, DSL.name("VIOLATIONANALYSIS_COMPONENT_FK"), new TableField[] { ViolationAnalysis.VIOLATIONANALYSIS.componentId }, Keys.COMPONENT_PK, new TableField[] { Component.COMPONENT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey VIOLATIONANALYSIS_POLICYVIOLATION_FK = Internal.createForeignKey(ViolationAnalysis.VIOLATIONANALYSIS, DSL.name("VIOLATIONANALYSIS_POLICYVIOLATION_FK"), new TableField[] { ViolationAnalysis.VIOLATIONANALYSIS.policyViolationId }, Keys.POLICYVIOLATION_PK, new TableField[] { PolicyViolation.POLICYVIOLATION.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey VIOLATIONANALYSIS_PROJECT_FK = Internal.createForeignKey(ViolationAnalysis.VIOLATIONANALYSIS, DSL.name("VIOLATIONANALYSIS_PROJECT_FK"), new TableField[] { ViolationAnalysis.VIOLATIONANALYSIS.projectId }, Keys.PROJECT_PK, new TableField[] { Project.PROJECT.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey VIOLATIONANALYSISCOMMENT_VIOLATIONANALYSIS_FK = Internal.createForeignKey(ViolationAnalysisComment.VIOLATIONANALYSISCOMMENT, DSL.name("VIOLATIONANALYSISCOMMENT_VIOLATIONANALYSIS_FK"), new TableField[] { ViolationAnalysisComment.VIOLATIONANALYSISCOMMENT.violationAnalysisId }, Keys.VIOLATIONANALYSIS_PK, new TableField[] { ViolationAnalysis.VIOLATIONANALYSIS.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey VULNERABILITIES_TAGS__VULNERABILITIES_TAGS_TAG_FK = Internal.createForeignKey(VulnerabilitiesTags.VULNERABILITIES_TAGS, DSL.name("VULNERABILITIES_TAGS_TAG_FK"), new TableField[] { VulnerabilitiesTags.VULNERABILITIES_TAGS.tagId }, Keys.TAG_PK, new TableField[] { Tag.TAG.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey VULNERABILITIES_TAGS__VULNERABILITIES_TAGS_VULNERABILITY_FK = Internal.createForeignKey(VulnerabilitiesTags.VULNERABILITIES_TAGS, DSL.name("VULNERABILITIES_TAGS_VULNERABILITY_FK"), new TableField[] { VulnerabilitiesTags.VULNERABILITIES_TAGS.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey VULNERABILITIES_TAGS_TAG_FK = Internal.createForeignKey(VulnerabilitiesTags.VULNERABILITIES_TAGS, DSL.name("VULNERABILITIES_TAGS_TAG_FK"), new TableField[] { VulnerabilitiesTags.VULNERABILITIES_TAGS.tagId }, Keys.TAG_PK, new TableField[] { Tag.TAG.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey VULNERABILITIES_TAGS_VULNERABILITY_FK = Internal.createForeignKey(VulnerabilitiesTags.VULNERABILITIES_TAGS, DSL.name("VULNERABILITIES_TAGS_VULNERABILITY_FK"), new TableField[] { VulnerabilitiesTags.VULNERABILITIES_TAGS.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey VULNERABLESOFTWARE_VULNERABILITIES_VULNERABILITY_FK = Internal.createForeignKey(VulnerableSoftwareVulnerabilities.VULNERABLESOFTWARE_VULNERABILITIES, DSL.name("VULNERABLESOFTWARE_VULNERABILITIES_VULNERABILITY_FK"), new TableField[] { VulnerableSoftwareVulnerabilities.VULNERABLESOFTWARE_VULNERABILITIES.vulnerabilityId }, Keys.VULNERABILITY_PK, new TableField[] { Vulnerability.VULNERABILITY.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); public static final ForeignKey VULNERABLESOFTWARE_VULNERABILITIES_VULNERABLESOFTWARE_FK = Internal.createForeignKey(VulnerableSoftwareVulnerabilities.VULNERABLESOFTWARE_VULNERABILITIES, DSL.name("VULNERABLESOFTWARE_VULNERABILITIES_VULNERABLESOFTWARE_FK"), new TableField[] { VulnerableSoftwareVulnerabilities.VULNERABLESOFTWARE_VULNERABILITIES.vulnerableSoftwareId }, Keys.VULNERABLESOFTWARE_PK, new TableField[] { VulnerableSoftware.VULNERABLESOFTWARE.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); - public static final ForeignKey WORKFLOW_STATE__WORKFLOW_STATE_WORKFLOW_STATE_FK = Internal.createForeignKey(WorkflowState.WORKFLOW_STATE, DSL.name("WORKFLOW_STATE_WORKFLOW_STATE_FK"), new TableField[] { WorkflowState.WORKFLOW_STATE.parentStepId }, Keys.WORKFLOW_STATE_PK, new TableField[] { WorkflowState.WORKFLOW_STATE.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); + public static final ForeignKey WORKFLOW_STATE_WORKFLOW_STATE_FK = Internal.createForeignKey(WorkflowState.WORKFLOW_STATE, DSL.name("WORKFLOW_STATE_WORKFLOW_STATE_FK"), new TableField[] { WorkflowState.WORKFLOW_STATE.parentStepId }, Keys.WORKFLOW_STATE_PK, new TableField[] { WorkflowState.WORKFLOW_STATE.id }, true, ForeignKeyRule.CASCADE, ForeignKeyRule.NO_ACTION); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Routines.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Routines.java index b2d71758d9..7ac47f6814 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Routines.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Routines.java @@ -13,6 +13,7 @@ import org.dependencytrack.persistence.jooq.generated.routines.HasProjectAccess; import org.dependencytrack.persistence.jooq.generated.routines.JsonbVulnAliases; import org.dependencytrack.persistence.jooq.generated.routines.RecalcUserProjectEffectivePermissions; +import org.dependencytrack.persistence.jooq.generated.routines.RecalcUserProjectRoleEffectivePermissions; import org.dependencytrack.persistence.jooq.generated.routines.UpdateComponentMetrics; import org.dependencytrack.persistence.jooq.generated.routines.UpdatePortfolioMetrics; import org.dependencytrack.persistence.jooq.generated.routines.UpdateProjectMetrics; @@ -29,7 +30,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -199,6 +200,19 @@ public static void recalcUserProjectEffectivePermissions( p.execute(configuration); } + /** + * Call recalc_user_project_role_effective_permissions + */ + public static void recalcUserProjectRoleEffectivePermissions( + Configuration configuration + , Long[] projectIds + ) { + RecalcUserProjectRoleEffectivePermissions p = new RecalcUserProjectRoleEffectivePermissions(); + p.setProjectIds(projectIds); + + p.execute(configuration); + } + /** * Call UPDATE_COMPONENT_METRICS */ diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Tables.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Tables.java index ca998f2e3c..e92954215b 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Tables.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/Tables.java @@ -49,6 +49,8 @@ import org.dependencytrack.persistence.jooq.generated.tables.ProjectsTags; import org.dependencytrack.persistence.jooq.generated.tables.Repository; import org.dependencytrack.persistence.jooq.generated.tables.RepositoryMetaComponent; +import org.dependencytrack.persistence.jooq.generated.tables.Role; +import org.dependencytrack.persistence.jooq.generated.tables.RolesPermissions; import org.dependencytrack.persistence.jooq.generated.tables.ServiceComponent; import org.dependencytrack.persistence.jooq.generated.tables.ServiceComponentsVulnerabilities; import org.dependencytrack.persistence.jooq.generated.tables.Tag; @@ -57,6 +59,7 @@ import org.dependencytrack.persistence.jooq.generated.tables.User; import org.dependencytrack.persistence.jooq.generated.tables.UserProjectEffectivePermissions; import org.dependencytrack.persistence.jooq.generated.tables.UsersPermissions; +import org.dependencytrack.persistence.jooq.generated.tables.UsersProjectsRoles; import org.dependencytrack.persistence.jooq.generated.tables.UsersTeams; import org.dependencytrack.persistence.jooq.generated.tables.Vex; import org.dependencytrack.persistence.jooq.generated.tables.ViolationAnalysis; @@ -80,7 +83,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -302,6 +305,16 @@ public class Tables { */ public static final RepositoryMetaComponent REPOSITORY_META_COMPONENT = RepositoryMetaComponent.REPOSITORY_META_COMPONENT; + /** + * The table ROLE. + */ + public static final Role ROLE = Role.ROLE; + + /** + * The table ROLES_PERMISSIONS. + */ + public static final RolesPermissions ROLES_PERMISSIONS = RolesPermissions.ROLES_PERMISSIONS; + /** * The table SERVICECOMPONENT. */ @@ -342,6 +355,11 @@ public class Tables { */ public static final UsersPermissions USERS_PERMISSIONS = UsersPermissions.USERS_PERMISSIONS; + /** + * The table USERS_PROJECTS_ROLES. + */ + public static final UsersProjectsRoles USERS_PROJECTS_ROLES = UsersProjectsRoles.USERS_PROJECTS_ROLES; + /** * The table USERS_TEAMS. */ diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/enums/Severity.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/enums/Severity.java index 54444a2170..633b2102fe 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/enums/Severity.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/enums/Severity.java @@ -19,7 +19,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/CalcRiskScore.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/CalcRiskScore.java index 29f56b39b9..c12fd9aeb1 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/CalcRiskScore.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/CalcRiskScore.java @@ -24,7 +24,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/HasProjectAccess.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/HasProjectAccess.java index 97ffb95d1d..9fa6af7fdc 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/HasProjectAccess.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/HasProjectAccess.java @@ -22,7 +22,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/JsonbVulnAliases.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/JsonbVulnAliases.java index e2c6f6a4d3..dbfd3ac7e8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/JsonbVulnAliases.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/JsonbVulnAliases.java @@ -23,7 +23,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectEffectivePermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectEffectivePermissions.java index d8eb0ae3ec..c300e10706 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectEffectivePermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectEffectivePermissions.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectRoleEffectivePermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectRoleEffectivePermissions.java new file mode 100644 index 0000000000..a7ba879f49 --- /dev/null +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectRoleEffectivePermissions.java @@ -0,0 +1,54 @@ +/* + * This file is generated by jOOQ. + */ +package org.dependencytrack.persistence.jooq.generated.routines; + + +import javax.annotation.processing.Generated; + +import org.dependencytrack.persistence.jooq.generated.DefaultSchema; +import org.jooq.Parameter; +import org.jooq.impl.AbstractRoutine; +import org.jooq.impl.DSL; +import org.jooq.impl.Internal; +import org.jooq.impl.SQLDataType; + + +/** + * This class is generated by jOOQ. + */ +@Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" +) +@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) +public class RecalcUserProjectRoleEffectivePermissions extends AbstractRoutine { + + private static final long serialVersionUID = 1L; + + /** + * The parameter + * recalc_user_project_role_effective_permissions.project_ids. + */ + public static final Parameter PROJECT_IDS = Internal.createParameter("project_ids", SQLDataType.BIGINT.array(), false, false); + + /** + * Create a new routine call instance + */ + public RecalcUserProjectRoleEffectivePermissions() { + super("recalc_user_project_role_effective_permissions", DefaultSchema.DEFAULT_SCHEMA, DSL.comment("")); + + addInParameter(PROJECT_IDS); + } + + /** + * Set the project_ids parameter IN value to the routine + */ + public void setProjectIds(Long[] value) { + setValue(PROJECT_IDS, value); + } +} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateComponentMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateComponentMetrics.java index 44b8b2ed67..f8d0a90b23 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateComponentMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateComponentMetrics.java @@ -23,7 +23,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java index 6a0f30353d..9f9250cff4 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateProjectMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateProjectMetrics.java index 5523b16a6e..cd0e55d97a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateProjectMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateProjectMetrics.java @@ -23,7 +23,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AffectedVersionAttribution.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AffectedVersionAttribution.java index cb1bbd715c..ab2749f25a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AffectedVersionAttribution.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AffectedVersionAttribution.java @@ -49,7 +49,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -146,7 +146,7 @@ public AffectedVersionAttribution(Table path, ForeignKey Analysis(Table path, ForeignKey value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -256,7 +256,7 @@ public List> getUniqueKeys() { @Override public List> getReferences() { - return Arrays.asList(Keys.ANALYSIS__ANALYSIS_COMPONENT_FK, Keys.ANALYSIS__ANALYSIS_PROJECT_FK, Keys.ANALYSIS__ANALYSIS_VULNERABILITY_FK, Keys.ANALYSIS__ANALYSIS_VULNERABILITY_POLICY_ID_FK); + return Arrays.asList(Keys.ANALYSIS_COMPONENT_FK, Keys.ANALYSIS_PROJECT_FK, Keys.ANALYSIS_VULNERABILITY_FK, Keys.ANALYSIS_VULNERABILITY_POLICY_ID_FK); } private transient ComponentPath _component; @@ -266,7 +266,7 @@ public List> getUniqueKeys() { */ public ComponentPath component() { if (_component == null) - _component = new ComponentPath(this, Keys.ANALYSIS__ANALYSIS_COMPONENT_FK, null); + _component = new ComponentPath(this, Keys.ANALYSIS_COMPONENT_FK, null); return _component; } @@ -278,7 +278,7 @@ public ComponentPath component() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.ANALYSIS__ANALYSIS_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.ANALYSIS_PROJECT_FK, null); return _project; } @@ -290,7 +290,7 @@ public ProjectPath project() { */ public VulnerabilityPath vulnerability() { if (_vulnerability == null) - _vulnerability = new VulnerabilityPath(this, Keys.ANALYSIS__ANALYSIS_VULNERABILITY_FK, null); + _vulnerability = new VulnerabilityPath(this, Keys.ANALYSIS_VULNERABILITY_FK, null); return _vulnerability; } @@ -303,7 +303,7 @@ public VulnerabilityPath vulnerability() { */ public VulnerabilityPolicyPath vulnerabilityPolicy() { if (_vulnerabilityPolicy == null) - _vulnerabilityPolicy = new VulnerabilityPolicyPath(this, Keys.ANALYSIS__ANALYSIS_VULNERABILITY_POLICY_ID_FK, null); + _vulnerabilityPolicy = new VulnerabilityPolicyPath(this, Keys.ANALYSIS_VULNERABILITY_POLICY_ID_FK, null); return _vulnerabilityPolicy; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AnalysisComment.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AnalysisComment.java index 795e0d25c2..ceda269163 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AnalysisComment.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AnalysisComment.java @@ -47,7 +47,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -134,7 +134,7 @@ public AnalysisComment(Table path, ForeignKey ApiKey(Table path, ForeignKey chil value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKeysTeams.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKeysTeams.java index 226d1a015c..b70612e22e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKeysTeams.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKeysTeams.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -116,7 +116,7 @@ public ApiKeysTeams(Table path, ForeignKey Bom(Table path, ForeignKey childPath, value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -212,7 +212,7 @@ public List> getUniqueKeys() { @Override public List> getReferences() { - return Arrays.asList(Keys.BOM__BOM_PROJECT_FK); + return Arrays.asList(Keys.BOM_PROJECT_FK); } private transient ProjectPath _project; @@ -222,7 +222,7 @@ public List> getUniqueKeys() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.BOM__BOM_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.BOM_PROJECT_FK, null); return _project; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Component.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Component.java index 19b5e75e14..378de6b4fc 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Component.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Component.java @@ -61,7 +61,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -323,7 +323,7 @@ public Component(Table path, ForeignKey> getUniqueKeys() { @Override public List> getReferences() { - return Arrays.asList(Keys.COMPONENT__COMPONENT_COMPONENT_FK, Keys.COMPONENT__COMPONENT_LICENSE_FK, Keys.COMPONENT__COMPONENT_PROJECT_FK); + return Arrays.asList(Keys.COMPONENT_COMPONENT_FK, Keys.COMPONENT_LICENSE_FK, Keys.COMPONENT_PROJECT_FK); } private transient ComponentPath _component; @@ -390,7 +390,7 @@ public List> getUniqueKeys() { */ public ComponentPath component() { if (_component == null) - _component = new ComponentPath(this, Keys.COMPONENT__COMPONENT_COMPONENT_FK, null); + _component = new ComponentPath(this, Keys.COMPONENT_COMPONENT_FK, null); return _component; } @@ -402,7 +402,7 @@ public ComponentPath component() { */ public LicensePath license() { if (_license == null) - _license = new LicensePath(this, Keys.COMPONENT__COMPONENT_LICENSE_FK, null); + _license = new LicensePath(this, Keys.COMPONENT_LICENSE_FK, null); return _license; } @@ -414,7 +414,7 @@ public LicensePath license() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.COMPONENT__COMPONENT_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.COMPONENT_PROJECT_FK, null); return _project; } @@ -426,7 +426,7 @@ public ProjectPath project() { */ public AnalysisPath analysis() { if (_analysis == null) - _analysis = new AnalysisPath(this, null, Keys.ANALYSIS__ANALYSIS_COMPONENT_FK.getInverseKey()); + _analysis = new AnalysisPath(this, null, Keys.ANALYSIS_COMPONENT_FK.getInverseKey()); return _analysis; } @@ -439,7 +439,7 @@ public AnalysisPath analysis() { */ public ComponentOccurrencePath componentOccurrence() { if (_componentOccurrence == null) - _componentOccurrence = new ComponentOccurrencePath(this, null, Keys.COMPONENT_OCCURRENCE__COMPONENT_OCCURRENCE_COMPONENT_FK.getInverseKey()); + _componentOccurrence = new ComponentOccurrencePath(this, null, Keys.COMPONENT_OCCURRENCE_COMPONENT_FK.getInverseKey()); return _componentOccurrence; } @@ -452,7 +452,7 @@ public ComponentOccurrencePath componentOccurrence() { */ public ComponentPropertyPath componentProperty() { if (_componentProperty == null) - _componentProperty = new ComponentPropertyPath(this, null, Keys.COMPONENT_PROPERTY__COMPONENT_PROPERTY_COMPONENT_ID_FK.getInverseKey()); + _componentProperty = new ComponentPropertyPath(this, null, Keys.COMPONENT_PROPERTY_COMPONENT_ID_FK.getInverseKey()); return _componentProperty; } @@ -465,7 +465,7 @@ public ComponentPropertyPath componentProperty() { */ public ComponentsVulnerabilitiesPath componentsVulnerabilities() { if (_componentsVulnerabilities == null) - _componentsVulnerabilities = new ComponentsVulnerabilitiesPath(this, null, Keys.COMPONENTS_VULNERABILITIES__COMPONENTS_VULNERABILITIES_COMPONENT_FK.getInverseKey()); + _componentsVulnerabilities = new ComponentsVulnerabilitiesPath(this, null, Keys.COMPONENTS_VULNERABILITIES_COMPONENT_FK.getInverseKey()); return _componentsVulnerabilities; } @@ -504,7 +504,7 @@ public FindingAttributionPath findingAttribution() { */ public IntegrityAnalysisPath integrityAnalysis() { if (_integrityAnalysis == null) - _integrityAnalysis = new IntegrityAnalysisPath(this, null, Keys.INTEGRITY_ANALYSIS__INTEGRITY_ANALYSIS_COMPONENT_FK.getInverseKey()); + _integrityAnalysis = new IntegrityAnalysisPath(this, null, Keys.INTEGRITY_ANALYSIS_COMPONENT_FK.getInverseKey()); return _integrityAnalysis; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentOccurrence.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentOccurrence.java index 003ac234ea..d23376bc8e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentOccurrence.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentOccurrence.java @@ -47,7 +47,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -144,7 +144,7 @@ public ComponentOccurrence(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.COMPONENT_OCCURRENCE__COMPONENT_OCCURRENCE_COMPONENT_FK); + return Arrays.asList(Keys.COMPONENT_OCCURRENCE_COMPONENT_FK); } private transient ComponentPath _component; @@ -201,7 +201,7 @@ public UniqueKey getPrimaryKey() { */ public ComponentPath component() { if (_component == null) - _component = new ComponentPath(this, Keys.COMPONENT_OCCURRENCE__COMPONENT_OCCURRENCE_COMPONENT_FK, null); + _component = new ComponentPath(this, Keys.COMPONENT_OCCURRENCE_COMPONENT_FK, null); return _component; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentProperty.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentProperty.java index 10aaf1802a..699621ebcc 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentProperty.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentProperty.java @@ -49,7 +49,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -151,7 +151,7 @@ public ComponentProperty(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.COMPONENT_PROPERTY__COMPONENT_PROPERTY_COMPONENT_ID_FK); + return Arrays.asList(Keys.COMPONENT_PROPERTY_COMPONENT_ID_FK); } private transient ComponentPath _component; @@ -213,7 +213,7 @@ public UniqueKey getPrimaryKey() { */ public ComponentPath component() { if (_component == null) - _component = new ComponentPath(this, Keys.COMPONENT_PROPERTY__COMPONENT_PROPERTY_COMPONENT_ID_FK, null); + _component = new ComponentPath(this, Keys.COMPONENT_PROPERTY_COMPONENT_ID_FK, null); return _component; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentsVulnerabilities.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentsVulnerabilities.java index 63cc49d2d2..42be6d3f0f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentsVulnerabilities.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentsVulnerabilities.java @@ -45,7 +45,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -117,7 +117,7 @@ public ComponentsVulnerabilities(Table path, ForeignKey getIndexes() { @Override public List> getReferences() { - return Arrays.asList(Keys.COMPONENTS_VULNERABILITIES__COMPONENTS_VULNERABILITIES_COMPONENT_FK, Keys.COMPONENTS_VULNERABILITIES__COMPONENTS_VULNERABILITIES_VULNERABILITY_FK); + return Arrays.asList(Keys.COMPONENTS_VULNERABILITIES_COMPONENT_FK, Keys.COMPONENTS_VULNERABILITIES_VULNERABILITY_FK); } private transient ComponentPath _component; @@ -169,7 +169,7 @@ public List getIndexes() { */ public ComponentPath component() { if (_component == null) - _component = new ComponentPath(this, Keys.COMPONENTS_VULNERABILITIES__COMPONENTS_VULNERABILITIES_COMPONENT_FK, null); + _component = new ComponentPath(this, Keys.COMPONENTS_VULNERABILITIES_COMPONENT_FK, null); return _component; } @@ -181,7 +181,7 @@ public ComponentPath component() { */ public VulnerabilityPath vulnerability() { if (_vulnerability == null) - _vulnerability = new VulnerabilityPath(this, Keys.COMPONENTS_VULNERABILITIES__COMPONENTS_VULNERABILITIES_VULNERABILITY_FK, null); + _vulnerability = new VulnerabilityPath(this, Keys.COMPONENTS_VULNERABILITIES_VULNERABILITY_FK, null); return _vulnerability; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ConfigProperty.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ConfigProperty.java index 9326897217..3ccf71ad28 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ConfigProperty.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ConfigProperty.java @@ -39,7 +39,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/DependencyMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/DependencyMetrics.java index d43bd939f2..098bd9db67 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/DependencyMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/DependencyMetrics.java @@ -45,7 +45,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -265,7 +265,7 @@ public DependencyMetrics(Table path, ForeignKey FindingAttribution(Table path, ForeignKey IntegrityAnalysis(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.INTEGRITY_ANALYSIS__INTEGRITY_ANALYSIS_COMPONENT_FK); + return Arrays.asList(Keys.INTEGRITY_ANALYSIS_COMPONENT_FK); } private transient ComponentPath _component; @@ -211,7 +211,7 @@ public UniqueKey getPrimaryKey() { */ public ComponentPath component() { if (_component == null) - _component = new ComponentPath(this, Keys.INTEGRITY_ANALYSIS__INTEGRITY_ANALYSIS_COMPONENT_FK, null); + _component = new ComponentPath(this, Keys.INTEGRITY_ANALYSIS_COMPONENT_FK, null); return _component; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityMetaComponent.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityMetaComponent.java index 70158897a1..6f11a64d26 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityMetaComponent.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityMetaComponent.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/License.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/License.java index 65a3fbb7e9..0deff1578e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/License.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/License.java @@ -48,7 +48,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -175,7 +175,7 @@ public License(Table path, ForeignKey ch value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -237,7 +237,7 @@ public List> getUniqueKeys() { */ public ComponentPath component() { if (_component == null) - _component = new ComponentPath(this, null, Keys.COMPONENT__COMPONENT_LICENSE_FK.getInverseKey()); + _component = new ComponentPath(this, null, Keys.COMPONENT_LICENSE_FK.getInverseKey()); return _component; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroup.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroup.java index 30b35b6e38..ed81170d79 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroup.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroup.java @@ -47,7 +47,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -129,7 +129,7 @@ public LicenseGroup(Table path, ForeignKey LicenseGroupLicense(Table path, ForeignKey MappedLdapGroup(Table path, ForeignKey MappedOidcGroup(Table path, ForeignKey NotificationPublisher(Table path, ForeignKey NotificationRule(Table path, ForeignKey NotificationRuleProjects(Table path, ForeignKey NotificationRuleTags(Table path, ForeignKey NotificationRuleTeams(Table path, ForeignKey OidcGroup(Table path, ForeignKey Permission(Table path, ForeignKey> getUniqueKeys() { return Arrays.asList(Keys.PERMISSION_IDX); } + private transient RolesPermissionsPath _rolesPermissions; + + /** + * Get the implicit to-many join path to the ROLES_PERMISSIONS + * table + */ + public RolesPermissionsPath rolesPermissions() { + if (_rolesPermissions == null) + _rolesPermissions = new RolesPermissionsPath(this, null, Keys.ROLES_PERMISSIONS_PERMISSION_FK.getInverseKey()); + + return _rolesPermissions; + } + private transient TeamsPermissionsPath _teamsPermissions; /** @@ -183,7 +198,7 @@ public List> getUniqueKeys() { */ public TeamsPermissionsPath teamsPermissions() { if (_teamsPermissions == null) - _teamsPermissions = new TeamsPermissionsPath(this, null, Keys.TEAMS_PERMISSIONS__TEAMS_PERMISSIONS_PERMISSION_FK.getInverseKey()); + _teamsPermissions = new TeamsPermissionsPath(this, null, Keys.TEAMS_PERMISSIONS_PERMISSION_FK.getInverseKey()); return _teamsPermissions; } @@ -197,7 +212,7 @@ public TeamsPermissionsPath teamsPermissions() { */ public UserProjectEffectivePermissionsPath userProjectEffectivePermissionsPermissionIdFk() { if (_userProjectEffectivePermissionsPermissionIdFk == null) - _userProjectEffectivePermissionsPermissionIdFk = new UserProjectEffectivePermissionsPath(this, null, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK.getInverseKey()); + _userProjectEffectivePermissionsPermissionIdFk = new UserProjectEffectivePermissionsPath(this, null, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK.getInverseKey()); return _userProjectEffectivePermissionsPermissionIdFk; } @@ -211,7 +226,7 @@ public UserProjectEffectivePermissionsPath userProjectEffectivePermissionsPermis */ public UserProjectEffectivePermissionsPath userProjectEffectivePermissionsPermissionNameFk() { if (_userProjectEffectivePermissionsPermissionNameFk == null) - _userProjectEffectivePermissionsPermissionNameFk = new UserProjectEffectivePermissionsPath(this, null, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK.getInverseKey()); + _userProjectEffectivePermissionsPermissionNameFk = new UserProjectEffectivePermissionsPath(this, null, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK.getInverseKey()); return _userProjectEffectivePermissionsPermissionNameFk; } @@ -224,11 +239,18 @@ public UserProjectEffectivePermissionsPath userProjectEffectivePermissionsPermis */ public UsersPermissionsPath usersPermissions() { if (_usersPermissions == null) - _usersPermissions = new UsersPermissionsPath(this, null, Keys.USERS_PERMISSIONS__USERS_PERMISSIONS_PERMISSION_FK.getInverseKey()); + _usersPermissions = new UsersPermissionsPath(this, null, Keys.USERS_PERMISSIONS_PERMISSION_FK.getInverseKey()); return _usersPermissions; } + /** + * Get the implicit many-to-many join path to the ROLE table + */ + public RolePath role() { + return rolesPermissions().role(); + } + /** * Get the implicit many-to-many join path to the TEAM table */ diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Policy.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Policy.java index 07c5f8d29a..2f4eab2c25 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Policy.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Policy.java @@ -50,7 +50,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -147,7 +147,7 @@ public Policy(Table path, ForeignKey chil value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -210,7 +210,7 @@ public List> getUniqueKeys() { */ public PolicyProjectsPath policyProjects() { if (_policyProjects == null) - _policyProjects = new PolicyProjectsPath(this, null, Keys.POLICY_PROJECTS__POLICY_PROJECTS_POLICY_FK.getInverseKey()); + _policyProjects = new PolicyProjectsPath(this, null, Keys.POLICY_PROJECTS_POLICY_FK.getInverseKey()); return _policyProjects; } @@ -222,7 +222,7 @@ public PolicyProjectsPath policyProjects() { */ public PolicyTagsPath policyTags() { if (_policyTags == null) - _policyTags = new PolicyTagsPath(this, null, Keys.POLICY_TAGS__POLICY_TAGS_POLICY_FK.getInverseKey()); + _policyTags = new PolicyTagsPath(this, null, Keys.POLICY_TAGS_POLICY_FK.getInverseKey()); return _policyTags; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyCondition.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyCondition.java index 85f14eb4ac..9b455d8cf0 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyCondition.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyCondition.java @@ -48,7 +48,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -145,7 +145,7 @@ public PolicyCondition(Table path, ForeignKey PolicyProjects(Table path, ForeignKey getIndexes() { @Override public List> getReferences() { - return Arrays.asList(Keys.POLICY_PROJECTS__POLICY_PROJECTS_POLICY_FK, Keys.POLICY_PROJECTS__POLICY_PROJECTS_PROJECT_FK); + return Arrays.asList(Keys.POLICY_PROJECTS_POLICY_FK, Keys.POLICY_PROJECTS_PROJECT_FK); } private transient PolicyPath _policy; @@ -169,7 +169,7 @@ public List getIndexes() { */ public PolicyPath policy() { if (_policy == null) - _policy = new PolicyPath(this, Keys.POLICY_PROJECTS__POLICY_PROJECTS_POLICY_FK, null); + _policy = new PolicyPath(this, Keys.POLICY_PROJECTS_POLICY_FK, null); return _policy; } @@ -181,7 +181,7 @@ public PolicyPath policy() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.POLICY_PROJECTS__POLICY_PROJECTS_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.POLICY_PROJECTS_PROJECT_FK, null); return _project; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyTags.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyTags.java index 029d960467..0e84e1ccbc 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyTags.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyTags.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -116,7 +116,7 @@ public PolicyTags(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.POLICY_TAGS__POLICY_TAGS_POLICY_FK, Keys.POLICY_TAGS__POLICY_TAGS_TAG_FK); + return Arrays.asList(Keys.POLICY_TAGS_POLICY_FK, Keys.POLICY_TAGS_TAG_FK); } private transient PolicyPath _policy; @@ -168,7 +168,7 @@ public UniqueKey getPrimaryKey() { */ public PolicyPath policy() { if (_policy == null) - _policy = new PolicyPath(this, Keys.POLICY_TAGS__POLICY_TAGS_POLICY_FK, null); + _policy = new PolicyPath(this, Keys.POLICY_TAGS_POLICY_FK, null); return _policy; } @@ -180,7 +180,7 @@ public PolicyPath policy() { */ public TagPath tag() { if (_tag == null) - _tag = new TagPath(this, Keys.POLICY_TAGS__POLICY_TAGS_TAG_FK, null); + _tag = new TagPath(this, Keys.POLICY_TAGS_TAG_FK, null); return _tag; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyViolation.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyViolation.java index dcd8ce1a8c..12a913fe24 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyViolation.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyViolation.java @@ -51,7 +51,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -153,7 +153,7 @@ public PolicyViolation(Table path, ForeignKey Project(Table path, ForeignKey ch value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -300,7 +301,7 @@ public List> getUniqueKeys() { @Override public List> getReferences() { - return Arrays.asList(Keys.PROJECT__PROJECT_PROJECT_FK); + return Arrays.asList(Keys.PROJECT_PROJECT_FK); } private transient ProjectPath _project; @@ -310,7 +311,7 @@ public List> getUniqueKeys() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.PROJECT__PROJECT_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.PROJECT_PROJECT_FK, null); return _project; } @@ -322,7 +323,7 @@ public ProjectPath project() { */ public AnalysisPath analysis() { if (_analysis == null) - _analysis = new AnalysisPath(this, null, Keys.ANALYSIS__ANALYSIS_PROJECT_FK.getInverseKey()); + _analysis = new AnalysisPath(this, null, Keys.ANALYSIS_PROJECT_FK.getInverseKey()); return _analysis; } @@ -334,7 +335,7 @@ public AnalysisPath analysis() { */ public BomPath bom() { if (_bom == null) - _bom = new BomPath(this, null, Keys.BOM__BOM_PROJECT_FK.getInverseKey()); + _bom = new BomPath(this, null, Keys.BOM_PROJECT_FK.getInverseKey()); return _bom; } @@ -346,7 +347,7 @@ public BomPath bom() { */ public ComponentPath component() { if (_component == null) - _component = new ComponentPath(this, null, Keys.COMPONENT__COMPONENT_PROJECT_FK.getInverseKey()); + _component = new ComponentPath(this, null, Keys.COMPONENT_PROJECT_FK.getInverseKey()); return _component; } @@ -398,7 +399,7 @@ public NotificationRuleProjectsPath notificationRuleProjects() { */ public PolicyProjectsPath policyProjects() { if (_policyProjects == null) - _policyProjects = new PolicyProjectsPath(this, null, Keys.POLICY_PROJECTS__POLICY_PROJECTS_PROJECT_FK.getInverseKey()); + _policyProjects = new PolicyProjectsPath(this, null, Keys.POLICY_PROJECTS_PROJECT_FK.getInverseKey()); return _policyProjects; } @@ -424,7 +425,7 @@ public PolicyViolationPath policyViolation() { */ public ProjectAccessTeamsPath projectAccessTeams() { if (_projectAccessTeams == null) - _projectAccessTeams = new ProjectAccessTeamsPath(this, null, Keys.PROJECT_ACCESS_TEAMS__PROJECT_ACCESS_TEAMS_PROJECT_FK.getInverseKey()); + _projectAccessTeams = new ProjectAccessTeamsPath(this, null, Keys.PROJECT_ACCESS_TEAMS_PROJECT_FK.getInverseKey()); return _projectAccessTeams; } @@ -437,7 +438,7 @@ public ProjectAccessTeamsPath projectAccessTeams() { */ public ProjectHierarchyPath projectHierarchyChildProjectFk() { if (_projectHierarchyChildProjectFk == null) - _projectHierarchyChildProjectFk = new ProjectHierarchyPath(this, null, Keys.PROJECT_HIERARCHY__PROJECT_HIERARCHY_CHILD_PROJECT_FK.getInverseKey()); + _projectHierarchyChildProjectFk = new ProjectHierarchyPath(this, null, Keys.PROJECT_HIERARCHY_CHILD_PROJECT_FK.getInverseKey()); return _projectHierarchyChildProjectFk; } @@ -450,7 +451,7 @@ public ProjectHierarchyPath projectHierarchyChildProjectFk() { */ public ProjectHierarchyPath projectHierarchyParentProjectFk() { if (_projectHierarchyParentProjectFk == null) - _projectHierarchyParentProjectFk = new ProjectHierarchyPath(this, null, Keys.PROJECT_HIERARCHY__PROJECT_HIERARCHY_PARENT_PROJECT_FK.getInverseKey()); + _projectHierarchyParentProjectFk = new ProjectHierarchyPath(this, null, Keys.PROJECT_HIERARCHY_PARENT_PROJECT_FK.getInverseKey()); return _projectHierarchyParentProjectFk; } @@ -463,7 +464,7 @@ public ProjectHierarchyPath projectHierarchyParentProjectFk() { */ public ProjectMetadataPath projectMetadata() { if (_projectMetadata == null) - _projectMetadata = new ProjectMetadataPath(this, null, Keys.PROJECT_METADATA__PROJECT_METADATA_PROJECT_ID_FK.getInverseKey()); + _projectMetadata = new ProjectMetadataPath(this, null, Keys.PROJECT_METADATA_PROJECT_ID_FK.getInverseKey()); return _projectMetadata; } @@ -476,7 +477,7 @@ public ProjectMetadataPath projectMetadata() { */ public ProjectPropertyPath projectProperty() { if (_projectProperty == null) - _projectProperty = new ProjectPropertyPath(this, null, Keys.PROJECT_PROPERTY__PROJECT_PROPERTY_PROJECT_FK.getInverseKey()); + _projectProperty = new ProjectPropertyPath(this, null, Keys.PROJECT_PROPERTY_PROJECT_FK.getInverseKey()); return _projectProperty; } @@ -502,7 +503,7 @@ public ProjectMetricsPath projectMetrics() { */ public ProjectsTagsPath projectsTags() { if (_projectsTags == null) - _projectsTags = new ProjectsTagsPath(this, null, Keys.PROJECTS_TAGS__PROJECTS_TAGS_PROJECT_FK.getInverseKey()); + _projectsTags = new ProjectsTagsPath(this, null, Keys.PROJECTS_TAGS_PROJECT_FK.getInverseKey()); return _projectsTags; } @@ -528,11 +529,24 @@ public ServiceComponentPath serviceComponent() { */ public UserProjectEffectivePermissionsPath userProjectEffectivePermissions() { if (_userProjectEffectivePermissions == null) - _userProjectEffectivePermissions = new UserProjectEffectivePermissionsPath(this, null, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK.getInverseKey()); + _userProjectEffectivePermissions = new UserProjectEffectivePermissionsPath(this, null, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK.getInverseKey()); return _userProjectEffectivePermissions; } + private transient UsersProjectsRolesPath _usersProjectsRoles; + + /** + * Get the implicit to-many join path to the + * USERS_PROJECTS_ROLES table + */ + public UsersProjectsRolesPath usersProjectsRoles() { + if (_usersProjectsRoles == null) + _usersProjectsRoles = new UsersProjectsRolesPath(this, null, Keys.USERS_PROJECTS_ROLES_PROJECT_FK.getInverseKey()); + + return _usersProjectsRoles; + } + private transient VexPath _vex; /** @@ -540,7 +554,7 @@ public UserProjectEffectivePermissionsPath userProjectEffectivePermissions() { */ public VexPath vex() { if (_vex == null) - _vex = new VexPath(this, null, Keys.VEX__VEX_PROJECT_FK.getInverseKey()); + _vex = new VexPath(this, null, Keys.VEX_PROJECT_FK.getInverseKey()); return _vex; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectAccessTeams.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectAccessTeams.java index 27d872e068..ce78ae5414 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectAccessTeams.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectAccessTeams.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -116,7 +116,7 @@ public ProjectAccessTeams(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.PROJECT_ACCESS_TEAMS__PROJECT_ACCESS_TEAMS_PROJECT_FK, Keys.PROJECT_ACCESS_TEAMS__PROJECT_ACCESS_TEAMS_TEAM_FK); + return Arrays.asList(Keys.PROJECT_ACCESS_TEAMS_PROJECT_FK, Keys.PROJECT_ACCESS_TEAMS_TEAM_FK); } private transient ProjectPath _project; @@ -168,7 +168,7 @@ public UniqueKey getPrimaryKey() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.PROJECT_ACCESS_TEAMS__PROJECT_ACCESS_TEAMS_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.PROJECT_ACCESS_TEAMS_PROJECT_FK, null); return _project; } @@ -180,7 +180,7 @@ public ProjectPath project() { */ public TeamPath team() { if (_team == null) - _team = new TeamPath(this, Keys.PROJECT_ACCESS_TEAMS__PROJECT_ACCESS_TEAMS_TEAM_FK, null); + _team = new TeamPath(this, Keys.PROJECT_ACCESS_TEAMS_TEAM_FK, null); return _team; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectHierarchy.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectHierarchy.java index b6807dd1a1..b59598c0d2 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectHierarchy.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectHierarchy.java @@ -43,7 +43,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -120,7 +120,7 @@ public ProjectHierarchy(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.PROJECT_HIERARCHY__PROJECT_HIERARCHY_CHILD_PROJECT_FK, Keys.PROJECT_HIERARCHY__PROJECT_HIERARCHY_PARENT_PROJECT_FK); + return Arrays.asList(Keys.PROJECT_HIERARCHY_CHILD_PROJECT_FK, Keys.PROJECT_HIERARCHY_PARENT_PROJECT_FK); } private transient ProjectPath _projectHierarchyChildProjectFk; @@ -173,7 +173,7 @@ public UniqueKey getPrimaryKey() { */ public ProjectPath projectHierarchyChildProjectFk() { if (_projectHierarchyChildProjectFk == null) - _projectHierarchyChildProjectFk = new ProjectPath(this, Keys.PROJECT_HIERARCHY__PROJECT_HIERARCHY_CHILD_PROJECT_FK, null); + _projectHierarchyChildProjectFk = new ProjectPath(this, Keys.PROJECT_HIERARCHY_CHILD_PROJECT_FK, null); return _projectHierarchyChildProjectFk; } @@ -186,7 +186,7 @@ public ProjectPath projectHierarchyChildProjectFk() { */ public ProjectPath projectHierarchyParentProjectFk() { if (_projectHierarchyParentProjectFk == null) - _projectHierarchyParentProjectFk = new ProjectPath(this, Keys.PROJECT_HIERARCHY__PROJECT_HIERARCHY_PARENT_PROJECT_FK, null); + _projectHierarchyParentProjectFk = new ProjectPath(this, Keys.PROJECT_HIERARCHY_PARENT_PROJECT_FK, null); return _projectHierarchyParentProjectFk; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetadata.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetadata.java index 2ebdbee501..bccb484757 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetadata.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetadata.java @@ -46,7 +46,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -133,7 +133,7 @@ public ProjectMetadata(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.PROJECT_METADATA__PROJECT_METADATA_PROJECT_ID_FK); + return Arrays.asList(Keys.PROJECT_METADATA_PROJECT_ID_FK); } private transient ProjectPath _project; @@ -195,7 +195,7 @@ public UniqueKey getPrimaryKey() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.PROJECT_METADATA__PROJECT_METADATA_PROJECT_ID_FK, null); + _project = new ProjectPath(this, Keys.PROJECT_METADATA_PROJECT_ID_FK, null); return _project; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetrics.java index 0ce4c72c88..9901df67e8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetrics.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -266,7 +266,7 @@ public ProjectMetrics(Table path, ForeignKey ProjectProperty(Table path, ForeignKey> getUniqueKeys() { @Override public List> getReferences() { - return Arrays.asList(Keys.PROJECT_PROPERTY__PROJECT_PROPERTY_PROJECT_FK); + return Arrays.asList(Keys.PROJECT_PROPERTY_PROJECT_FK); } private transient ProjectPath _project; @@ -203,7 +203,7 @@ public List> getUniqueKeys() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.PROJECT_PROPERTY__PROJECT_PROPERTY_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.PROJECT_PROPERTY_PROJECT_FK, null); return _project; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectsTags.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectsTags.java index 70b468ee1f..3aff1e32b0 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectsTags.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectsTags.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -116,7 +116,7 @@ public ProjectsTags(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.PROJECTS_TAGS__PROJECTS_TAGS_PROJECT_FK, Keys.PROJECTS_TAGS__PROJECTS_TAGS_TAG_FK); + return Arrays.asList(Keys.PROJECTS_TAGS_PROJECT_FK, Keys.PROJECTS_TAGS_TAG_FK); } private transient ProjectPath _project; @@ -168,7 +168,7 @@ public UniqueKey getPrimaryKey() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.PROJECTS_TAGS__PROJECTS_TAGS_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.PROJECTS_TAGS_PROJECT_FK, null); return _project; } @@ -180,7 +180,7 @@ public ProjectPath project() { */ public TagPath tag() { if (_tag == null) - _tag = new TagPath(this, Keys.PROJECTS_TAGS__PROJECTS_TAGS_TAG_FK, null); + _tag = new TagPath(this, Keys.PROJECTS_TAGS_TAG_FK, null); return _tag; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Repository.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Repository.java index 3b6cac5043..47d40b14b6 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Repository.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Repository.java @@ -42,7 +42,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RepositoryMetaComponent.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RepositoryMetaComponent.java index 544feceb25..763663bfb7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RepositoryMetaComponent.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RepositoryMetaComponent.java @@ -42,7 +42,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Role.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Role.java new file mode 100644 index 0000000000..48168398e9 --- /dev/null +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Role.java @@ -0,0 +1,333 @@ +/* + * This file is generated by jOOQ. + */ +package org.dependencytrack.persistence.jooq.generated.tables; + + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +import javax.annotation.processing.Generated; + +import org.dependencytrack.persistence.jooq.generated.DefaultSchema; +import org.dependencytrack.persistence.jooq.generated.Keys; +import org.dependencytrack.persistence.jooq.generated.tables.Permission.PermissionPath; +import org.dependencytrack.persistence.jooq.generated.tables.RolesPermissions.RolesPermissionsPath; +import org.dependencytrack.persistence.jooq.generated.tables.UsersProjectsRoles.UsersProjectsRolesPath; +import org.dependencytrack.persistence.jooq.generated.tables.records.RoleRecord; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.ForeignKey; +import org.jooq.Identity; +import org.jooq.InverseForeignKey; +import org.jooq.Name; +import org.jooq.Path; +import org.jooq.PlainSQL; +import org.jooq.QueryPart; +import org.jooq.Record; +import org.jooq.SQL; +import org.jooq.Schema; +import org.jooq.Select; +import org.jooq.Stringly; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.TableOptions; +import org.jooq.UniqueKey; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.jooq.impl.TableImpl; + + +/** + * This class is generated by jOOQ. + */ +@Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" +) +@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) +public class Role extends TableImpl { + + private static final long serialVersionUID = 1L; + + /** + * The reference instance of ROLE + */ + public static final Role ROLE = new Role(); + + /** + * The class holding records for this type + */ + @Override + public Class getRecordType() { + return RoleRecord.class; + } + + /** + * The column ROLE.ID. + */ + public final TableField id = createField(DSL.name("ID"), SQLDataType.BIGINT.nullable(false).identity(true), this, ""); + + /** + * The column ROLE.NAME. + */ + public final TableField name = createField(DSL.name("NAME"), SQLDataType.VARCHAR(255).nullable(false), this, ""); + + /** + * The column ROLE.UUID. + */ + public final TableField uuid = createField(DSL.name("UUID"), SQLDataType.UUID.nullable(false), this, ""); + + private Role(Name alias, Table aliased) { + this(alias, aliased, (Field[]) null, null); + } + + private Role(Name alias, Table aliased, Field[] parameters, Condition where) { + super(alias, null, aliased, parameters, DSL.comment(""), TableOptions.table(), where); + } + + /** + * Create an aliased ROLE table reference + */ + public Role(String alias) { + this(DSL.name(alias), ROLE); + } + + /** + * Create an aliased ROLE table reference + */ + public Role(Name alias) { + this(alias, ROLE); + } + + /** + * Create a ROLE table reference + */ + public Role() { + this(DSL.name("ROLE"), null); + } + + public Role(Table path, ForeignKey childPath, InverseForeignKey parentPath) { + super(path, childPath, parentPath, ROLE); + } + + /** + * A subtype implementing {@link Path} for simplified path-based joins. + */ + @Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" + ) + public static class RolePath extends Role implements Path { + + private static final long serialVersionUID = 1L; + public RolePath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { + super(path, childPath, parentPath); + } + private RolePath(Name alias, Table aliased) { + super(alias, aliased); + } + + @Override + public RolePath as(String alias) { + return new RolePath(DSL.name(alias), this); + } + + @Override + public RolePath as(Name alias) { + return new RolePath(alias, this); + } + + @Override + public RolePath as(Table alias) { + return new RolePath(alias.getQualifiedName(), this); + } + } + + @Override + public Schema getSchema() { + return aliased() ? null : DefaultSchema.DEFAULT_SCHEMA; + } + + @Override + public Identity getIdentity() { + return (Identity) super.getIdentity(); + } + + @Override + public UniqueKey getPrimaryKey() { + return Keys.ROLE_PK; + } + + @Override + public List> getUniqueKeys() { + return Arrays.asList(Keys.ROLE_NAME_IDX, Keys.ROLE_UUID_IDX); + } + + private transient RolesPermissionsPath _rolesPermissions; + + /** + * Get the implicit to-many join path to the ROLES_PERMISSIONS + * table + */ + public RolesPermissionsPath rolesPermissions() { + if (_rolesPermissions == null) + _rolesPermissions = new RolesPermissionsPath(this, null, Keys.ROLES_PERMISSIONS_ROLE_FK.getInverseKey()); + + return _rolesPermissions; + } + + private transient UsersProjectsRolesPath _usersProjectsRoles; + + /** + * Get the implicit to-many join path to the + * USERS_PROJECTS_ROLES table + */ + public UsersProjectsRolesPath usersProjectsRoles() { + if (_usersProjectsRoles == null) + _usersProjectsRoles = new UsersProjectsRolesPath(this, null, Keys.USERS_PROJECTS_ROLES_ROLE_FK.getInverseKey()); + + return _usersProjectsRoles; + } + + /** + * Get the implicit many-to-many join path to the PERMISSION + * table + */ + public PermissionPath permission() { + return rolesPermissions().permission(); + } + + @Override + public Role as(String alias) { + return new Role(DSL.name(alias), this); + } + + @Override + public Role as(Name alias) { + return new Role(alias, this); + } + + @Override + public Role as(Table alias) { + return new Role(alias.getQualifiedName(), this); + } + + /** + * Rename this table + */ + @Override + public Role rename(String name) { + return new Role(DSL.name(name), null); + } + + /** + * Rename this table + */ + @Override + public Role rename(Name name) { + return new Role(name, null); + } + + /** + * Rename this table + */ + @Override + public Role rename(Table name) { + return new Role(name.getQualifiedName(), null); + } + + /** + * Create an inline derived table from this table + */ + @Override + public Role where(Condition condition) { + return new Role(getQualifiedName(), aliased() ? this : null, null, condition); + } + + /** + * Create an inline derived table from this table + */ + @Override + public Role where(Collection conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public Role where(Condition... conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public Role where(Field condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public Role where(SQL condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public Role where(@Stringly.SQL String condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public Role where(@Stringly.SQL String condition, Object... binds) { + return where(DSL.condition(condition, binds)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public Role where(@Stringly.SQL String condition, QueryPart... parts) { + return where(DSL.condition(condition, parts)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public Role whereExists(Select select) { + return where(DSL.exists(select)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public Role whereNotExists(Select select) { + return where(DSL.notExists(select)); + } +} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RolesPermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RolesPermissions.java new file mode 100644 index 0000000000..1c3d96367c --- /dev/null +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RolesPermissions.java @@ -0,0 +1,317 @@ +/* + * This file is generated by jOOQ. + */ +package org.dependencytrack.persistence.jooq.generated.tables; + + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.annotation.processing.Generated; + +import org.dependencytrack.persistence.jooq.generated.DefaultSchema; +import org.dependencytrack.persistence.jooq.generated.Indexes; +import org.dependencytrack.persistence.jooq.generated.Keys; +import org.dependencytrack.persistence.jooq.generated.tables.Permission.PermissionPath; +import org.dependencytrack.persistence.jooq.generated.tables.Role.RolePath; +import org.dependencytrack.persistence.jooq.generated.tables.records.RolesPermissionsRecord; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.ForeignKey; +import org.jooq.Index; +import org.jooq.InverseForeignKey; +import org.jooq.Name; +import org.jooq.Path; +import org.jooq.PlainSQL; +import org.jooq.QueryPart; +import org.jooq.Record; +import org.jooq.SQL; +import org.jooq.Schema; +import org.jooq.Select; +import org.jooq.Stringly; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.TableOptions; +import org.jooq.UniqueKey; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.jooq.impl.TableImpl; + + +/** + * This class is generated by jOOQ. + */ +@Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" +) +@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) +public class RolesPermissions extends TableImpl { + + private static final long serialVersionUID = 1L; + + /** + * The reference instance of ROLES_PERMISSIONS + */ + public static final RolesPermissions ROLES_PERMISSIONS = new RolesPermissions(); + + /** + * The class holding records for this type + */ + @Override + public Class getRecordType() { + return RolesPermissionsRecord.class; + } + + /** + * The column ROLES_PERMISSIONS.ROLE_ID. + */ + public final TableField roleId = createField(DSL.name("ROLE_ID"), SQLDataType.BIGINT.nullable(false), this, ""); + + /** + * The column ROLES_PERMISSIONS.PERMISSION_ID. + */ + public final TableField permissionId = createField(DSL.name("PERMISSION_ID"), SQLDataType.BIGINT.nullable(false), this, ""); + + private RolesPermissions(Name alias, Table aliased) { + this(alias, aliased, (Field[]) null, null); + } + + private RolesPermissions(Name alias, Table aliased, Field[] parameters, Condition where) { + super(alias, null, aliased, parameters, DSL.comment(""), TableOptions.table(), where); + } + + /** + * Create an aliased ROLES_PERMISSIONS table reference + */ + public RolesPermissions(String alias) { + this(DSL.name(alias), ROLES_PERMISSIONS); + } + + /** + * Create an aliased ROLES_PERMISSIONS table reference + */ + public RolesPermissions(Name alias) { + this(alias, ROLES_PERMISSIONS); + } + + /** + * Create a ROLES_PERMISSIONS table reference + */ + public RolesPermissions() { + this(DSL.name("ROLES_PERMISSIONS"), null); + } + + public RolesPermissions(Table path, ForeignKey childPath, InverseForeignKey parentPath) { + super(path, childPath, parentPath, ROLES_PERMISSIONS); + } + + /** + * A subtype implementing {@link Path} for simplified path-based joins. + */ + @Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" + ) + public static class RolesPermissionsPath extends RolesPermissions implements Path { + + private static final long serialVersionUID = 1L; + public RolesPermissionsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { + super(path, childPath, parentPath); + } + private RolesPermissionsPath(Name alias, Table aliased) { + super(alias, aliased); + } + + @Override + public RolesPermissionsPath as(String alias) { + return new RolesPermissionsPath(DSL.name(alias), this); + } + + @Override + public RolesPermissionsPath as(Name alias) { + return new RolesPermissionsPath(alias, this); + } + + @Override + public RolesPermissionsPath as(Table alias) { + return new RolesPermissionsPath(alias.getQualifiedName(), this); + } + } + + @Override + public Schema getSchema() { + return aliased() ? null : DefaultSchema.DEFAULT_SCHEMA; + } + + @Override + public List getIndexes() { + return Arrays.asList(Indexes.ROLES_PERMISSIONS_PERMISSION_ID_IDX, Indexes.ROLES_PERMISSIONS_ROLE_ID_IDX); + } + + @Override + public List> getUniqueKeys() { + return Arrays.asList(Keys.ROLES_PERMISSIONS_COMPOSITE_IDX); + } + + @Override + public List> getReferences() { + return Arrays.asList(Keys.ROLES_PERMISSIONS_PERMISSION_FK, Keys.ROLES_PERMISSIONS_ROLE_FK); + } + + private transient PermissionPath _permission; + + /** + * Get the implicit join path to the PERMISSION table. + */ + public PermissionPath permission() { + if (_permission == null) + _permission = new PermissionPath(this, Keys.ROLES_PERMISSIONS_PERMISSION_FK, null); + + return _permission; + } + + private transient RolePath _role; + + /** + * Get the implicit join path to the ROLE table. + */ + public RolePath role() { + if (_role == null) + _role = new RolePath(this, Keys.ROLES_PERMISSIONS_ROLE_FK, null); + + return _role; + } + + @Override + public RolesPermissions as(String alias) { + return new RolesPermissions(DSL.name(alias), this); + } + + @Override + public RolesPermissions as(Name alias) { + return new RolesPermissions(alias, this); + } + + @Override + public RolesPermissions as(Table alias) { + return new RolesPermissions(alias.getQualifiedName(), this); + } + + /** + * Rename this table + */ + @Override + public RolesPermissions rename(String name) { + return new RolesPermissions(DSL.name(name), null); + } + + /** + * Rename this table + */ + @Override + public RolesPermissions rename(Name name) { + return new RolesPermissions(name, null); + } + + /** + * Rename this table + */ + @Override + public RolesPermissions rename(Table name) { + return new RolesPermissions(name.getQualifiedName(), null); + } + + /** + * Create an inline derived table from this table + */ + @Override + public RolesPermissions where(Condition condition) { + return new RolesPermissions(getQualifiedName(), aliased() ? this : null, null, condition); + } + + /** + * Create an inline derived table from this table + */ + @Override + public RolesPermissions where(Collection conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public RolesPermissions where(Condition... conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public RolesPermissions where(Field condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public RolesPermissions where(SQL condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public RolesPermissions where(@Stringly.SQL String condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public RolesPermissions where(@Stringly.SQL String condition, Object... binds) { + return where(DSL.condition(condition, binds)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public RolesPermissions where(@Stringly.SQL String condition, QueryPart... parts) { + return where(DSL.condition(condition, parts)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public RolesPermissions whereExists(Select select) { + return where(DSL.exists(select)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public RolesPermissions whereNotExists(Select select) { + return where(DSL.notExists(select)); + } +} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponent.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponent.java index 6eb69c956e..c58a5d23f8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponent.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponent.java @@ -49,7 +49,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -191,7 +191,7 @@ public ServiceComponent(Table path, ForeignKey ServiceComponentsVulnerabilities(Table path, Foreig value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Tag.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Tag.java index e91fbecbb1..71d2a95fa3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Tag.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Tag.java @@ -52,7 +52,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -124,7 +124,7 @@ public Tag(Table path, ForeignKey childPath, value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -194,7 +194,7 @@ public NotificationRuleTagsPath notificationRuleTags() { */ public PolicyTagsPath policyTags() { if (_policyTags == null) - _policyTags = new PolicyTagsPath(this, null, Keys.POLICY_TAGS__POLICY_TAGS_TAG_FK.getInverseKey()); + _policyTags = new PolicyTagsPath(this, null, Keys.POLICY_TAGS_TAG_FK.getInverseKey()); return _policyTags; } @@ -207,7 +207,7 @@ public PolicyTagsPath policyTags() { */ public ProjectsTagsPath projectsTags() { if (_projectsTags == null) - _projectsTags = new ProjectsTagsPath(this, null, Keys.PROJECTS_TAGS__PROJECTS_TAGS_TAG_FK.getInverseKey()); + _projectsTags = new ProjectsTagsPath(this, null, Keys.PROJECTS_TAGS_TAG_FK.getInverseKey()); return _projectsTags; } @@ -220,7 +220,7 @@ public ProjectsTagsPath projectsTags() { */ public VulnerabilitiesTagsPath vulnerabilitiesTags() { if (_vulnerabilitiesTags == null) - _vulnerabilitiesTags = new VulnerabilitiesTagsPath(this, null, Keys.VULNERABILITIES_TAGS__VULNERABILITIES_TAGS_TAG_FK.getInverseKey()); + _vulnerabilitiesTags = new VulnerabilitiesTagsPath(this, null, Keys.VULNERABILITIES_TAGS_TAG_FK.getInverseKey()); return _vulnerabilitiesTags; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Team.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Team.java index 1f8bba0de0..8f9fe5f8cd 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Team.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Team.java @@ -53,7 +53,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -130,7 +130,7 @@ public Team(Table path, ForeignKey childPat value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -240,7 +240,7 @@ public NotificationRuleTeamsPath notificationRuleTeams() { */ public ProjectAccessTeamsPath projectAccessTeams() { if (_projectAccessTeams == null) - _projectAccessTeams = new ProjectAccessTeamsPath(this, null, Keys.PROJECT_ACCESS_TEAMS__PROJECT_ACCESS_TEAMS_TEAM_FK.getInverseKey()); + _projectAccessTeams = new ProjectAccessTeamsPath(this, null, Keys.PROJECT_ACCESS_TEAMS_TEAM_FK.getInverseKey()); return _projectAccessTeams; } @@ -253,7 +253,7 @@ public ProjectAccessTeamsPath projectAccessTeams() { */ public TeamsPermissionsPath teamsPermissions() { if (_teamsPermissions == null) - _teamsPermissions = new TeamsPermissionsPath(this, null, Keys.TEAMS_PERMISSIONS__TEAMS_PERMISSIONS_TEAM_FK.getInverseKey()); + _teamsPermissions = new TeamsPermissionsPath(this, null, Keys.TEAMS_PERMISSIONS_TEAM_FK.getInverseKey()); return _teamsPermissions; } @@ -265,7 +265,7 @@ public TeamsPermissionsPath teamsPermissions() { */ public UsersTeamsPath usersTeams() { if (_usersTeams == null) - _usersTeams = new UsersTeamsPath(this, null, Keys.USERS_TEAMS__USERS_TEAMS_TEAM_FK.getInverseKey()); + _usersTeams = new UsersTeamsPath(this, null, Keys.USERS_TEAMS_TEAM_FK.getInverseKey()); return _usersTeams; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/TeamsPermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/TeamsPermissions.java index a7b25a0f9d..5f402d415c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/TeamsPermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/TeamsPermissions.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -116,7 +116,7 @@ public TeamsPermissions(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.TEAMS_PERMISSIONS__TEAMS_PERMISSIONS_PERMISSION_FK, Keys.TEAMS_PERMISSIONS__TEAMS_PERMISSIONS_TEAM_FK); + return Arrays.asList(Keys.TEAMS_PERMISSIONS_PERMISSION_FK, Keys.TEAMS_PERMISSIONS_TEAM_FK); } private transient PermissionPath _permission; @@ -168,7 +168,7 @@ public UniqueKey getPrimaryKey() { */ public PermissionPath permission() { if (_permission == null) - _permission = new PermissionPath(this, Keys.TEAMS_PERMISSIONS__TEAMS_PERMISSIONS_PERMISSION_FK, null); + _permission = new PermissionPath(this, Keys.TEAMS_PERMISSIONS_PERMISSION_FK, null); return _permission; } @@ -180,7 +180,7 @@ public PermissionPath permission() { */ public TeamPath team() { if (_team == null) - _team = new TeamPath(this, Keys.TEAMS_PERMISSIONS__TEAMS_PERMISSIONS_TEAM_FK, null); + _team = new TeamPath(this, Keys.TEAMS_PERMISSIONS_TEAM_FK, null); return _team; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/User.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/User.java index 425fd86d49..11d9dd7e76 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/User.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/User.java @@ -18,6 +18,7 @@ import org.dependencytrack.persistence.jooq.generated.tables.Team.TeamPath; import org.dependencytrack.persistence.jooq.generated.tables.UserProjectEffectivePermissions.UserProjectEffectivePermissionsPath; import org.dependencytrack.persistence.jooq.generated.tables.UsersPermissions.UsersPermissionsPath; +import org.dependencytrack.persistence.jooq.generated.tables.UsersProjectsRoles.UsersProjectsRolesPath; import org.dependencytrack.persistence.jooq.generated.tables.UsersTeams.UsersTeamsPath; import org.dependencytrack.persistence.jooq.generated.tables.records.UserRecord; import org.jooq.Check; @@ -53,7 +54,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -175,7 +176,7 @@ public User(Table path, ForeignKey childPat value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -233,7 +234,7 @@ public UniqueKey getPrimaryKey() { */ public UserProjectEffectivePermissionsPath userProjectEffectivePermissions() { if (_userProjectEffectivePermissions == null) - _userProjectEffectivePermissions = new UserProjectEffectivePermissionsPath(this, null, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK.getInverseKey()); + _userProjectEffectivePermissions = new UserProjectEffectivePermissionsPath(this, null, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK.getInverseKey()); return _userProjectEffectivePermissions; } @@ -246,11 +247,24 @@ public UserProjectEffectivePermissionsPath userProjectEffectivePermissions() { */ public UsersPermissionsPath usersPermissions() { if (_usersPermissions == null) - _usersPermissions = new UsersPermissionsPath(this, null, Keys.USERS_PERMISSIONS__USERS_PERMISSIONS_USER_FK.getInverseKey()); + _usersPermissions = new UsersPermissionsPath(this, null, Keys.USERS_PERMISSIONS_USER_FK.getInverseKey()); return _usersPermissions; } + private transient UsersProjectsRolesPath _usersProjectsRoles; + + /** + * Get the implicit to-many join path to the + * USERS_PROJECTS_ROLES table + */ + public UsersProjectsRolesPath usersProjectsRoles() { + if (_usersProjectsRoles == null) + _usersProjectsRoles = new UsersProjectsRolesPath(this, null, Keys.USERS_PROJECTS_ROLES_USER_FK.getInverseKey()); + + return _usersProjectsRoles; + } + private transient UsersTeamsPath _usersTeams; /** @@ -258,7 +272,7 @@ public UsersPermissionsPath usersPermissions() { */ public UsersTeamsPath usersTeams() { if (_usersTeams == null) - _usersTeams = new UsersTeamsPath(this, null, Keys.USERS_TEAMS__USERS_TEAMS_USER_FK.getInverseKey()); + _usersTeams = new UsersTeamsPath(this, null, Keys.USERS_TEAMS_USER_FK.getInverseKey()); return _usersTeams; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UserProjectEffectivePermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UserProjectEffectivePermissions.java index dba3c18426..799ff1e8d4 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UserProjectEffectivePermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UserProjectEffectivePermissions.java @@ -45,7 +45,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -130,7 +130,7 @@ public UserProjectEffectivePermissions(Table path, Foreign value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -172,33 +172,33 @@ public UniqueKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK); + return Arrays.asList(Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK); } - private transient PermissionPath _userProjectEffectivePermissionsPermissionIdFk; + private transient PermissionPath _permissionId; /** * Get the implicit join path to the PERMISSION table, via the * USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK key. */ - public PermissionPath userProjectEffectivePermissionsPermissionIdFk() { - if (_userProjectEffectivePermissionsPermissionIdFk == null) - _userProjectEffectivePermissionsPermissionIdFk = new PermissionPath(this, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK, null); + public PermissionPath permissionId() { + if (_permissionId == null) + _permissionId = new PermissionPath(this, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_ID_FK, null); - return _userProjectEffectivePermissionsPermissionIdFk; + return _permissionId; } - private transient PermissionPath _userProjectEffectivePermissionsPermissionNameFk; + private transient PermissionPath _permissionName; /** * Get the implicit join path to the PERMISSION table, via the * USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK key. */ - public PermissionPath userProjectEffectivePermissionsPermissionNameFk() { - if (_userProjectEffectivePermissionsPermissionNameFk == null) - _userProjectEffectivePermissionsPermissionNameFk = new PermissionPath(this, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK, null); + public PermissionPath permissionName() { + if (_permissionName == null) + _permissionName = new PermissionPath(this, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PERMISSION_NAME_FK, null); - return _userProjectEffectivePermissionsPermissionNameFk; + return _permissionName; } private transient ProjectPath _project; @@ -208,7 +208,7 @@ public PermissionPath userProjectEffectivePermissionsPermissionNameFk() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_PROJECT_FK, null); return _project; } @@ -220,7 +220,7 @@ public ProjectPath project() { */ public UserPath user() { if (_user == null) - _user = new UserPath(this, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS__USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK, null); + _user = new UserPath(this, Keys.USER_PROJECT_EFFECTIVE_PERMISSIONS_USER_FK, null); return _user; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersPermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersPermissions.java index e3456d7b63..583e0697f0 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersPermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersPermissions.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -116,7 +116,7 @@ public UsersPermissions(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.USERS_PERMISSIONS__USERS_PERMISSIONS_PERMISSION_FK, Keys.USERS_PERMISSIONS__USERS_PERMISSIONS_USER_FK); + return Arrays.asList(Keys.USERS_PERMISSIONS_PERMISSION_FK, Keys.USERS_PERMISSIONS_USER_FK); } private transient PermissionPath _permission; @@ -168,7 +168,7 @@ public UniqueKey getPrimaryKey() { */ public PermissionPath permission() { if (_permission == null) - _permission = new PermissionPath(this, Keys.USERS_PERMISSIONS__USERS_PERMISSIONS_PERMISSION_FK, null); + _permission = new PermissionPath(this, Keys.USERS_PERMISSIONS_PERMISSION_FK, null); return _permission; } @@ -180,7 +180,7 @@ public PermissionPath permission() { */ public UserPath user() { if (_user == null) - _user = new UserPath(this, Keys.USERS_PERMISSIONS__USERS_PERMISSIONS_USER_FK, null); + _user = new UserPath(this, Keys.USERS_PERMISSIONS_USER_FK, null); return _user; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersProjectsRoles.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersProjectsRoles.java new file mode 100644 index 0000000000..459fe04660 --- /dev/null +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersProjectsRoles.java @@ -0,0 +1,329 @@ +/* + * This file is generated by jOOQ. + */ +package org.dependencytrack.persistence.jooq.generated.tables; + + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.annotation.processing.Generated; + +import org.dependencytrack.persistence.jooq.generated.DefaultSchema; +import org.dependencytrack.persistence.jooq.generated.Indexes; +import org.dependencytrack.persistence.jooq.generated.Keys; +import org.dependencytrack.persistence.jooq.generated.tables.Project.ProjectPath; +import org.dependencytrack.persistence.jooq.generated.tables.Role.RolePath; +import org.dependencytrack.persistence.jooq.generated.tables.User.UserPath; +import org.dependencytrack.persistence.jooq.generated.tables.records.UsersProjectsRolesRecord; +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.ForeignKey; +import org.jooq.Index; +import org.jooq.InverseForeignKey; +import org.jooq.Name; +import org.jooq.Path; +import org.jooq.PlainSQL; +import org.jooq.QueryPart; +import org.jooq.Record; +import org.jooq.SQL; +import org.jooq.Schema; +import org.jooq.Select; +import org.jooq.Stringly; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.TableOptions; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.jooq.impl.TableImpl; + + +/** + * This class is generated by jOOQ. + */ +@Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" +) +@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) +public class UsersProjectsRoles extends TableImpl { + + private static final long serialVersionUID = 1L; + + /** + * The reference instance of USERS_PROJECTS_ROLES + */ + public static final UsersProjectsRoles USERS_PROJECTS_ROLES = new UsersProjectsRoles(); + + /** + * The class holding records for this type + */ + @Override + public Class getRecordType() { + return UsersProjectsRolesRecord.class; + } + + /** + * The column USERS_PROJECTS_ROLES.USER_ID. + */ + public final TableField userId = createField(DSL.name("USER_ID"), SQLDataType.BIGINT.nullable(false), this, ""); + + /** + * The column USERS_PROJECTS_ROLES.PROJECT_ID. + */ + public final TableField projectId = createField(DSL.name("PROJECT_ID"), SQLDataType.BIGINT.nullable(false), this, ""); + + /** + * The column USERS_PROJECTS_ROLES.ROLE_ID. + */ + public final TableField roleId = createField(DSL.name("ROLE_ID"), SQLDataType.BIGINT.nullable(false), this, ""); + + private UsersProjectsRoles(Name alias, Table aliased) { + this(alias, aliased, (Field[]) null, null); + } + + private UsersProjectsRoles(Name alias, Table aliased, Field[] parameters, Condition where) { + super(alias, null, aliased, parameters, DSL.comment(""), TableOptions.table(), where); + } + + /** + * Create an aliased USERS_PROJECTS_ROLES table reference + */ + public UsersProjectsRoles(String alias) { + this(DSL.name(alias), USERS_PROJECTS_ROLES); + } + + /** + * Create an aliased USERS_PROJECTS_ROLES table reference + */ + public UsersProjectsRoles(Name alias) { + this(alias, USERS_PROJECTS_ROLES); + } + + /** + * Create a USERS_PROJECTS_ROLES table reference + */ + public UsersProjectsRoles() { + this(DSL.name("USERS_PROJECTS_ROLES"), null); + } + + public UsersProjectsRoles(Table path, ForeignKey childPath, InverseForeignKey parentPath) { + super(path, childPath, parentPath, USERS_PROJECTS_ROLES); + } + + /** + * A subtype implementing {@link Path} for simplified path-based joins. + */ + @Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" + ) + public static class UsersProjectsRolesPath extends UsersProjectsRoles implements Path { + + private static final long serialVersionUID = 1L; + public UsersProjectsRolesPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { + super(path, childPath, parentPath); + } + private UsersProjectsRolesPath(Name alias, Table aliased) { + super(alias, aliased); + } + + @Override + public UsersProjectsRolesPath as(String alias) { + return new UsersProjectsRolesPath(DSL.name(alias), this); + } + + @Override + public UsersProjectsRolesPath as(Name alias) { + return new UsersProjectsRolesPath(alias, this); + } + + @Override + public UsersProjectsRolesPath as(Table alias) { + return new UsersProjectsRolesPath(alias.getQualifiedName(), this); + } + } + + @Override + public Schema getSchema() { + return aliased() ? null : DefaultSchema.DEFAULT_SCHEMA; + } + + @Override + public List getIndexes() { + return Arrays.asList(Indexes.USERS_PROJECTS_ROLES_ROLE_ID_IDX, Indexes.USERS_PROJECTS_ROLES_USER_PROJECT_IDX); + } + + @Override + public List> getReferences() { + return Arrays.asList(Keys.USERS_PROJECTS_ROLES_PROJECT_FK, Keys.USERS_PROJECTS_ROLES_ROLE_FK, Keys.USERS_PROJECTS_ROLES_USER_FK); + } + + private transient ProjectPath _project; + + /** + * Get the implicit join path to the PROJECT table. + */ + public ProjectPath project() { + if (_project == null) + _project = new ProjectPath(this, Keys.USERS_PROJECTS_ROLES_PROJECT_FK, null); + + return _project; + } + + private transient RolePath _role; + + /** + * Get the implicit join path to the ROLE table. + */ + public RolePath role() { + if (_role == null) + _role = new RolePath(this, Keys.USERS_PROJECTS_ROLES_ROLE_FK, null); + + return _role; + } + + private transient UserPath _user; + + /** + * Get the implicit join path to the USER table. + */ + public UserPath user() { + if (_user == null) + _user = new UserPath(this, Keys.USERS_PROJECTS_ROLES_USER_FK, null); + + return _user; + } + + @Override + public UsersProjectsRoles as(String alias) { + return new UsersProjectsRoles(DSL.name(alias), this); + } + + @Override + public UsersProjectsRoles as(Name alias) { + return new UsersProjectsRoles(alias, this); + } + + @Override + public UsersProjectsRoles as(Table alias) { + return new UsersProjectsRoles(alias.getQualifiedName(), this); + } + + /** + * Rename this table + */ + @Override + public UsersProjectsRoles rename(String name) { + return new UsersProjectsRoles(DSL.name(name), null); + } + + /** + * Rename this table + */ + @Override + public UsersProjectsRoles rename(Name name) { + return new UsersProjectsRoles(name, null); + } + + /** + * Rename this table + */ + @Override + public UsersProjectsRoles rename(Table name) { + return new UsersProjectsRoles(name.getQualifiedName(), null); + } + + /** + * Create an inline derived table from this table + */ + @Override + public UsersProjectsRoles where(Condition condition) { + return new UsersProjectsRoles(getQualifiedName(), aliased() ? this : null, null, condition); + } + + /** + * Create an inline derived table from this table + */ + @Override + public UsersProjectsRoles where(Collection conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public UsersProjectsRoles where(Condition... conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public UsersProjectsRoles where(Field condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public UsersProjectsRoles where(SQL condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public UsersProjectsRoles where(@Stringly.SQL String condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public UsersProjectsRoles where(@Stringly.SQL String condition, Object... binds) { + return where(DSL.condition(condition, binds)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public UsersProjectsRoles where(@Stringly.SQL String condition, QueryPart... parts) { + return where(DSL.condition(condition, parts)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public UsersProjectsRoles whereExists(Select select) { + return where(DSL.exists(select)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public UsersProjectsRoles whereNotExists(Select select) { + return where(DSL.notExists(select)); + } +} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersTeams.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersTeams.java index a1f6c254b4..7189a86ec3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersTeams.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersTeams.java @@ -44,7 +44,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -116,7 +116,7 @@ public UsersTeams(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.USERS_TEAMS__USERS_TEAMS_TEAM_FK, Keys.USERS_TEAMS__USERS_TEAMS_USER_FK); + return Arrays.asList(Keys.USERS_TEAMS_TEAM_FK, Keys.USERS_TEAMS_USER_FK); } private transient TeamPath _team; @@ -168,7 +168,7 @@ public UniqueKey getPrimaryKey() { */ public TeamPath team() { if (_team == null) - _team = new TeamPath(this, Keys.USERS_TEAMS__USERS_TEAMS_TEAM_FK, null); + _team = new TeamPath(this, Keys.USERS_TEAMS_TEAM_FK, null); return _team; } @@ -180,7 +180,7 @@ public TeamPath team() { */ public UserPath user() { if (_user == null) - _user = new UserPath(this, Keys.USERS_TEAMS__USERS_TEAMS_USER_FK, null); + _user = new UserPath(this, Keys.USERS_TEAMS_USER_FK, null); return _user; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vex.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vex.java index bc2ecd5cb7..3dad1e7c95 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vex.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vex.java @@ -48,7 +48,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -150,7 +150,7 @@ public Vex(Table path, ForeignKey childPath, value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -207,7 +207,7 @@ public List> getUniqueKeys() { @Override public List> getReferences() { - return Arrays.asList(Keys.VEX__VEX_PROJECT_FK); + return Arrays.asList(Keys.VEX_PROJECT_FK); } private transient ProjectPath _project; @@ -217,7 +217,7 @@ public List> getUniqueKeys() { */ public ProjectPath project() { if (_project == null) - _project = new ProjectPath(this, Keys.VEX__VEX_PROJECT_FK, null); + _project = new ProjectPath(this, Keys.VEX_PROJECT_FK, null); return _project; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysis.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysis.java index 431ae6a305..38f1b14bd1 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysis.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysis.java @@ -49,7 +49,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -141,7 +141,7 @@ public ViolationAnalysis(Table path, ForeignKey ViolationAnalysisComment(Table path, ForeignKey VulnerabilitiesTags(Table path, ForeignKey getPrimaryKey() { @Override public List> getReferences() { - return Arrays.asList(Keys.VULNERABILITIES_TAGS__VULNERABILITIES_TAGS_TAG_FK, Keys.VULNERABILITIES_TAGS__VULNERABILITIES_TAGS_VULNERABILITY_FK); + return Arrays.asList(Keys.VULNERABILITIES_TAGS_TAG_FK, Keys.VULNERABILITIES_TAGS_VULNERABILITY_FK); } private transient TagPath _tag; @@ -168,7 +168,7 @@ public UniqueKey getPrimaryKey() { */ public TagPath tag() { if (_tag == null) - _tag = new TagPath(this, Keys.VULNERABILITIES_TAGS__VULNERABILITIES_TAGS_TAG_FK, null); + _tag = new TagPath(this, Keys.VULNERABILITIES_TAGS_TAG_FK, null); return _tag; } @@ -180,7 +180,7 @@ public TagPath tag() { */ public VulnerabilityPath vulnerability() { if (_vulnerability == null) - _vulnerability = new VulnerabilityPath(this, Keys.VULNERABILITIES_TAGS__VULNERABILITIES_TAGS_VULNERABILITY_FK, null); + _vulnerability = new VulnerabilityPath(this, Keys.VULNERABILITIES_TAGS_VULNERABILITY_FK, null); return _vulnerability; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vulnerability.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vulnerability.java index 9facb1f311..dd2b0bbfc4 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vulnerability.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vulnerability.java @@ -57,7 +57,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -274,7 +274,7 @@ public Vulnerability(Table path, ForeignKey VulnerabilityPolicy(Table path, ForeignKey getPrimaryKey() { */ public AnalysisPath analysis() { if (_analysis == null) - _analysis = new AnalysisPath(this, null, Keys.ANALYSIS__ANALYSIS_VULNERABILITY_POLICY_ID_FK.getInverseKey()); + _analysis = new AnalysisPath(this, null, Keys.ANALYSIS_VULNERABILITY_POLICY_ID_FK.getInverseKey()); return _analysis; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicyBundle.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicyBundle.java index 4389dbf05c..266b319c4e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicyBundle.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicyBundle.java @@ -38,7 +38,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityScan.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityScan.java index c382523cee..8e788ef366 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityScan.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityScan.java @@ -41,7 +41,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftware.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftware.java index 44b8d7ec7f..df46792c81 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftware.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftware.java @@ -48,7 +48,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -245,7 +245,7 @@ public VulnerableSoftware(Table path, ForeignKey VulnerableSoftwareVulnerabilities(Table path, Forei value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/WorkflowState.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/WorkflowState.java index 7e02ccead2..d8f1da3e90 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/WorkflowState.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/WorkflowState.java @@ -50,7 +50,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) @@ -152,7 +152,7 @@ public WorkflowState(Table path, ForeignKey> getUniqueKeys() { @Override public List> getReferences() { - return Arrays.asList(Keys.WORKFLOW_STATE__WORKFLOW_STATE_WORKFLOW_STATE_FK); + return Arrays.asList(Keys.WORKFLOW_STATE_WORKFLOW_STATE_FK); } private transient WorkflowStatePath _workflowState; @@ -219,7 +219,7 @@ public List> getUniqueKeys() { */ public WorkflowStatePath workflowState() { if (_workflowState == null) - _workflowState = new WorkflowStatePath(this, Keys.WORKFLOW_STATE__WORKFLOW_STATE_WORKFLOW_STATE_FK, null); + _workflowState = new WorkflowStatePath(this, Keys.WORKFLOW_STATE_WORKFLOW_STATE_FK, null); return _workflowState; } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AffectedVersionAttributionRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AffectedVersionAttributionRecord.java index 4e01ad1e5e..026062679d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AffectedVersionAttributionRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AffectedVersionAttributionRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisCommentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisCommentRecord.java index 8e8cd0d112..37e3dcc00a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisCommentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisCommentRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisRecord.java index c68267fb8c..5dd2bcda49 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeyRecord.java index 8765a1c7d5..4684ad3ec6 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeyRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeysTeamsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeysTeamsRecord.java index ce7704d6e7..7d9bf9757c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeysTeamsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeysTeamsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/BomRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/BomRecord.java index b9b71ae10c..0480329d10 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/BomRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/BomRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentOccurrenceRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentOccurrenceRecord.java index aec5fb7463..1c5cfe6c9a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentOccurrenceRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentOccurrenceRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentPropertyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentPropertyRecord.java index 27c6938cd8..d11f49ce38 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentPropertyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentPropertyRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentRecord.java index 969eb6f679..efaa744751 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentsVulnerabilitiesRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentsVulnerabilitiesRecord.java index 4b33dc261f..8cc6cedc52 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentsVulnerabilitiesRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentsVulnerabilitiesRecord.java @@ -17,7 +17,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ConfigPropertyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ConfigPropertyRecord.java index f673175c9c..0d106adab3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ConfigPropertyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ConfigPropertyRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/DependencyMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/DependencyMetricsRecord.java index 0f7eacb832..bab2687e4c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/DependencyMetricsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/DependencyMetricsRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/EpssRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/EpssRecord.java index 91ada20894..5e9f6e18a0 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/EpssRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/EpssRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/FindingAttributionRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/FindingAttributionRecord.java index c0fa2bb695..82341aac80 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/FindingAttributionRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/FindingAttributionRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityAnalysisRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityAnalysisRecord.java index 97e595d11e..28990e9506 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityAnalysisRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityAnalysisRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityMetaComponentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityMetaComponentRecord.java index 7ab9cb04d1..4aaf565508 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityMetaComponentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityMetaComponentRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupLicenseRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupLicenseRecord.java index d789751d4f..ea4472d487 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupLicenseRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupLicenseRecord.java @@ -17,7 +17,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupRecord.java index 72d9d77c5d..81722d92be 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseRecord.java index 23865b65a7..6934ecf642 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedLdapGroupRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedLdapGroupRecord.java index fe93906012..7f5f32c2e0 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedLdapGroupRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedLdapGroupRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedOidcGroupRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedOidcGroupRecord.java index dcb84e5213..31adc62183 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedOidcGroupRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedOidcGroupRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationPublisherRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationPublisherRecord.java index 43c9c0efe1..6e7b384e6f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationPublisherRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationPublisherRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleProjectsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleProjectsRecord.java index 04594148b0..bbfaf01594 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleProjectsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleProjectsRecord.java @@ -17,7 +17,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleRecord.java index b125068143..cd327bca15 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTagsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTagsRecord.java index 9c2092bdfe..0ad66cf68b 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTagsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTagsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTeamsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTeamsRecord.java index 2ab66a5dd5..22bce91843 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTeamsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTeamsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/OidcGroupRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/OidcGroupRecord.java index 5456e00f7d..493d3e764b 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/OidcGroupRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/OidcGroupRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PermissionRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PermissionRecord.java index 0949668ffd..7d1c08af5c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PermissionRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PermissionRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyConditionRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyConditionRecord.java index e5d7317e94..6fabed5c20 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyConditionRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyConditionRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyProjectsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyProjectsRecord.java index f4237cadbd..02a86f18f2 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyProjectsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyProjectsRecord.java @@ -17,7 +17,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyRecord.java index dfd62c6bae..e4e88bd0ad 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyTagsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyTagsRecord.java index 1d6f20b0a9..644e89fdf0 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyTagsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyTagsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyViolationRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyViolationRecord.java index d9d81a63d6..6c484f80c6 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyViolationRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyViolationRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java index 5aecd99fe3..d81823aa7d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectAccessTeamsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectAccessTeamsRecord.java index 0aab0e50b7..87970df92a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectAccessTeamsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectAccessTeamsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectHierarchyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectHierarchyRecord.java index eeee02f6cb..090a0da713 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectHierarchyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectHierarchyRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetadataRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetadataRecord.java index cdb3f1fb0b..fe036ee092 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetadataRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetadataRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetricsRecord.java index 9fe8ebe78c..f56ff940ef 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetricsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetricsRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectPropertyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectPropertyRecord.java index a962b044b4..9ca3db8a4e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectPropertyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectPropertyRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectRecord.java index 67aaf0af4c..bbe3098fb3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectRecord.java @@ -22,7 +22,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectsTagsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectsTagsRecord.java index 6c26a178bc..9e7d5b41e9 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectsTagsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectsTagsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryMetaComponentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryMetaComponentRecord.java index 07e7be5e88..c0a6d7f107 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryMetaComponentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryMetaComponentRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryRecord.java index 90e61c62ce..91f5176280 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RoleRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RoleRecord.java new file mode 100644 index 0000000000..83cf180003 --- /dev/null +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RoleRecord.java @@ -0,0 +1,108 @@ +/* + * This file is generated by jOOQ. + */ +package org.dependencytrack.persistence.jooq.generated.tables.records; + + +import java.util.UUID; + +import javax.annotation.processing.Generated; + +import org.dependencytrack.persistence.jooq.generated.tables.Role; +import org.jooq.Record1; +import org.jooq.impl.UpdatableRecordImpl; + + +/** + * This class is generated by jOOQ. + */ +@Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" +) +@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) +public class RoleRecord extends UpdatableRecordImpl { + + private static final long serialVersionUID = 1L; + + /** + * Setter for ROLE.ID. + */ + public RoleRecord setId(Long value) { + set(0, value); + return this; + } + + /** + * Getter for ROLE.ID. + */ + public Long getId() { + return (Long) get(0); + } + + /** + * Setter for ROLE.NAME. + */ + public RoleRecord setName(String value) { + set(1, value); + return this; + } + + /** + * Getter for ROLE.NAME. + */ + public String getName() { + return (String) get(1); + } + + /** + * Setter for ROLE.UUID. + */ + public RoleRecord setUuid(UUID value) { + set(2, value); + return this; + } + + /** + * Getter for ROLE.UUID. + */ + public UUID getUuid() { + return (UUID) get(2); + } + + // ------------------------------------------------------------------------- + // Primary key information + // ------------------------------------------------------------------------- + + @Override + public Record1 key() { + return (Record1) super.key(); + } + + // ------------------------------------------------------------------------- + // Constructors + // ------------------------------------------------------------------------- + + /** + * Create a detached RoleRecord + */ + public RoleRecord() { + super(Role.ROLE); + } + + /** + * Create a detached, initialised RoleRecord + */ + public RoleRecord(Long id, String name, UUID uuid) { + super(Role.ROLE); + + setId(id); + setName(name); + setUuid(uuid); + resetTouchedOnNotNull(); + } +} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RolesPermissionsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RolesPermissionsRecord.java new file mode 100644 index 0000000000..f685b5d415 --- /dev/null +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RolesPermissionsRecord.java @@ -0,0 +1,80 @@ +/* + * This file is generated by jOOQ. + */ +package org.dependencytrack.persistence.jooq.generated.tables.records; + + +import javax.annotation.processing.Generated; + +import org.dependencytrack.persistence.jooq.generated.tables.RolesPermissions; +import org.jooq.impl.TableRecordImpl; + + +/** + * This class is generated by jOOQ. + */ +@Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" +) +@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) +public class RolesPermissionsRecord extends TableRecordImpl { + + private static final long serialVersionUID = 1L; + + /** + * Setter for ROLES_PERMISSIONS.ROLE_ID. + */ + public RolesPermissionsRecord setRoleId(Long value) { + set(0, value); + return this; + } + + /** + * Getter for ROLES_PERMISSIONS.ROLE_ID. + */ + public Long getRoleId() { + return (Long) get(0); + } + + /** + * Setter for ROLES_PERMISSIONS.PERMISSION_ID. + */ + public RolesPermissionsRecord setPermissionId(Long value) { + set(1, value); + return this; + } + + /** + * Getter for ROLES_PERMISSIONS.PERMISSION_ID. + */ + public Long getPermissionId() { + return (Long) get(1); + } + + // ------------------------------------------------------------------------- + // Constructors + // ------------------------------------------------------------------------- + + /** + * Create a detached RolesPermissionsRecord + */ + public RolesPermissionsRecord() { + super(RolesPermissions.ROLES_PERMISSIONS); + } + + /** + * Create a detached, initialised RolesPermissionsRecord + */ + public RolesPermissionsRecord(Long roleId, Long permissionId) { + super(RolesPermissions.ROLES_PERMISSIONS); + + setRoleId(roleId); + setPermissionId(permissionId); + resetTouchedOnNotNull(); + } +} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentRecord.java index 6048b71553..2d6288f73c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentsVulnerabilitiesRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentsVulnerabilitiesRecord.java index 77f405e1d2..e16956d546 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentsVulnerabilitiesRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentsVulnerabilitiesRecord.java @@ -17,7 +17,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TagRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TagRecord.java index b2e61434ef..5159cb6019 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TagRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TagRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamRecord.java index 02a0a50c0b..6f96e78229 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamsPermissionsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamsPermissionsRecord.java index 3aa97c9d38..f5c15a4670 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamsPermissionsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamsPermissionsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserProjectEffectivePermissionsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserProjectEffectivePermissionsRecord.java index 93360441a8..21f697d30e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserProjectEffectivePermissionsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserProjectEffectivePermissionsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserRecord.java index cb5d733bf4..ee7ad41cbb 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersPermissionsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersPermissionsRecord.java index 42ce54015e..fdc21ad9ad 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersPermissionsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersPermissionsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersProjectsRolesRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersProjectsRolesRecord.java new file mode 100644 index 0000000000..be61a662d6 --- /dev/null +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersProjectsRolesRecord.java @@ -0,0 +1,96 @@ +/* + * This file is generated by jOOQ. + */ +package org.dependencytrack.persistence.jooq.generated.tables.records; + + +import javax.annotation.processing.Generated; + +import org.dependencytrack.persistence.jooq.generated.tables.UsersProjectsRoles; +import org.jooq.impl.TableRecordImpl; + + +/** + * This class is generated by jOOQ. + */ +@Generated( + value = { + "https://www.jooq.org", + "jOOQ version:3.20.4", + "schema version:v5.6.0-27" + }, + comments = "This class is generated by jOOQ" +) +@SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) +public class UsersProjectsRolesRecord extends TableRecordImpl { + + private static final long serialVersionUID = 1L; + + /** + * Setter for USERS_PROJECTS_ROLES.USER_ID. + */ + public UsersProjectsRolesRecord setUserId(Long value) { + set(0, value); + return this; + } + + /** + * Getter for USERS_PROJECTS_ROLES.USER_ID. + */ + public Long getUserId() { + return (Long) get(0); + } + + /** + * Setter for USERS_PROJECTS_ROLES.PROJECT_ID. + */ + public UsersProjectsRolesRecord setProjectId(Long value) { + set(1, value); + return this; + } + + /** + * Getter for USERS_PROJECTS_ROLES.PROJECT_ID. + */ + public Long getProjectId() { + return (Long) get(1); + } + + /** + * Setter for USERS_PROJECTS_ROLES.ROLE_ID. + */ + public UsersProjectsRolesRecord setRoleId(Long value) { + set(2, value); + return this; + } + + /** + * Getter for USERS_PROJECTS_ROLES.ROLE_ID. + */ + public Long getRoleId() { + return (Long) get(2); + } + + // ------------------------------------------------------------------------- + // Constructors + // ------------------------------------------------------------------------- + + /** + * Create a detached UsersProjectsRolesRecord + */ + public UsersProjectsRolesRecord() { + super(UsersProjectsRoles.USERS_PROJECTS_ROLES); + } + + /** + * Create a detached, initialised UsersProjectsRolesRecord + */ + public UsersProjectsRolesRecord(Long userId, Long projectId, Long roleId) { + super(UsersProjectsRoles.USERS_PROJECTS_ROLES); + + setUserId(userId); + setProjectId(projectId); + setRoleId(roleId); + resetTouchedOnNotNull(); + } +} diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersTeamsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersTeamsRecord.java index 650418104c..f23a0154ff 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersTeamsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersTeamsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VexRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VexRecord.java index f64438defc..92bc30b222 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VexRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VexRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisCommentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisCommentRecord.java index 57bde459dc..60421d4025 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisCommentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisCommentRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisRecord.java index dc3bd03d99..7252aacba7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilitiesTagsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilitiesTagsRecord.java index 84c3f1411a..ce5991b490 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilitiesTagsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilitiesTagsRecord.java @@ -18,7 +18,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityAliasRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityAliasRecord.java index d1f10d08c5..525eac4a62 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityAliasRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityAliasRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityMetricsRecord.java index f5f27c0dc0..0ab3e15a9d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityMetricsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityMetricsRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyBundleRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyBundleRecord.java index 87a0a92363..b215b072f8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyBundleRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyBundleRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyRecord.java index 95466211a0..780fca9265 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityRecord.java index 3665113924..ac8d675a4c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityRecord.java @@ -23,7 +23,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityScanRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityScanRecord.java index 5313f282fd..b8676890d4 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityScanRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityScanRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareRecord.java index e9996a8745..8027d2b077 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareRecord.java @@ -20,7 +20,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareVulnerabilitiesRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareVulnerabilitiesRecord.java index db2e922740..2a49f5840a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareVulnerabilitiesRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareVulnerabilitiesRecord.java @@ -17,7 +17,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/WorkflowStateRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/WorkflowStateRecord.java index bda930c431..aa7abda325 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/WorkflowStateRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/WorkflowStateRecord.java @@ -21,7 +21,7 @@ value = { "https://www.jooq.org", "jOOQ version:3.20.4", - "schema version:v5.6.0-26" + "schema version:v5.6.0-27" }, comments = "This class is generated by jOOQ" ) diff --git a/persistence-migration/src/main/resources/migration/changelog-main.xml b/persistence-migration/src/main/resources/migration/changelog-main.xml index be507a195d..e55e42262c 100644 --- a/persistence-migration/src/main/resources/migration/changelog-main.xml +++ b/persistence-migration/src/main/resources/migration/changelog-main.xml @@ -13,6 +13,5 @@ - \ No newline at end of file diff --git a/persistence-migration/src/main/resources/migration/changelog-v5.6.0-roles.xml b/persistence-migration/src/main/resources/migration/changelog-v5.6.0-roles.xml deleted file mode 100644 index 9a10a2b4bd..0000000000 --- a/persistence-migration/src/main/resources/migration/changelog-v5.6.0-roles.xml +++ /dev/null @@ -1,266 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- Helper function to recalculate all user permissions for a project. - -- Called by trigger functions to update the values in the USER_PROJECT_EFFECTIVE_PERMISSIONS table. - CREATE OR REPLACE FUNCTION recalc_user_project_role_effective_permissions(project_ids BIGINT[]) - RETURNS void AS $$ - DECLARE - tbl_prefix TEXT; - BEGIN - -- Remove any existing effective permissions for this project - DELETE FROM "USER_PROJECT_EFFECTIVE_PERMISSIONS" - WHERE "PROJECT_ID" = ANY(project_ids); - - -- Rebuild effective permissions for all users - INSERT INTO "USER_PROJECT_EFFECTIVE_PERMISSIONS" - ("USER_ID", "PROJECT_ID", "PERMISSION_ID", "PERMISSION_NAME") - SELECT DISTINCT upr."USER_ID", upr."PROJECT_ID", rp."PERMISSION_ID", p."NAME" - FROM "USERS_PROJECTS_ROLES" upr - INNER JOIN "ROLES_PERMISSIONS" rp - ON rp."ROLE_ID" = upr."ROLE_ID" - INNER JOIN "PERMISSION" p - ON p."ID" = rp."PERMISSION_ID" - WHERE upr."PROJECT_ID" = ANY(project_ids); - END; - $$ LANGUAGE plpgsql; - - - - CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_delete() - RETURNS TRIGGER AS $$ - DECLARE - project_ids BIGINT[]; - role_ids BIGINT[]; - BEGIN - SELECT ARRAY_AGG(DISTINCT "ROLE_ID") - INTO role_ids - FROM old_table; - - IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN - SELECT ARRAY_AGG(sub."PROJECT_ID") - INTO project_ids - FROM ( - SELECT upr."PROJECT_ID" - FROM "USERS_PROJECTS_ROLES" upr - INNER JOIN old_table - ON old_table."ROLE_ID" = upr."ROLE_ID" - ) sub; - ELSE - SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") - INTO project_ids - FROM "USERS_PROJECTS_ROLES" - WHERE "ROLE_ID" = ANY(role_ids); - END IF; - - PERFORM recalc_user_project_role_effective_permissions(project_ids); - RETURN NULL; - END; - $$ LANGUAGE plpgsql; - - - - CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_insert() - RETURNS TRIGGER AS $$ - DECLARE - project_ids BIGINT[]; - role_ids BIGINT[]; - BEGIN - SELECT ARRAY_AGG(DISTINCT "ROLE_ID") - INTO role_ids - FROM new_table; - - IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN - SELECT ARRAY_AGG(sub."PROJECT_ID") - INTO project_ids - FROM ( - SELECT upr."PROJECT_ID" - FROM "USERS_PROJECTS_ROLES" upr - INNER JOIN new_table - ON new_table."ROLE_ID" = upr."ROLE_ID" - ) sub; - ELSE - SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") - INTO project_ids - FROM "USERS_PROJECTS_ROLES" - WHERE "ROLE_ID" = ANY(role_ids); - END IF; - - PERFORM recalc_user_project_role_effective_permissions(project_ids); - RETURN NULL; - END; - $$ LANGUAGE plpgsql; - - - - CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_update() - RETURNS TRIGGER AS $$ - DECLARE - project_ids BIGINT[]; - role_ids BIGINT[]; - BEGIN - SELECT ARRAY_AGG("ROLE_ID") - INTO role_ids - FROM ( - SELECT "ROLE_ID" FROM old_table - UNION - SELECT "ROLE_ID" FROM new_table - ) roles; - - IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN - SELECT ARRAY_AGG(sub."PROJECT_ID") - INTO project_ids - FROM ( - SELECT upr."PROJECT_ID" - FROM "USERS_PROJECTS_ROLES" upr - INNER JOIN new_table - ON new_table."ROLE_ID" = upr."ROLE_ID" - FULL OUTER JOIN old_table - ON new_table."ROLE_ID" = old_table."ROLE_ID" - ) sub; - ELSE - SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") - INTO project_ids - FROM "USERS_PROJECTS_ROLES" - WHERE "ROLE_ID" = ANY(role_ids); - END IF; - - PERFORM recalc_user_project_role_effective_permissions(project_ids); - RETURN NULL; - END; - $$ LANGUAGE plpgsql; - - - - -- INSERT trigger for USERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_insert - AFTER INSERT ON "USERS_PROJECTS_ROLES" - REFERENCING NEW TABLE AS new_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); - - -- DELETE trigger for USERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_delete - AFTER DELETE ON "USERS_PROJECTS_ROLES" - REFERENCING OLD TABLE AS old_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); - - -- UPDATE trigger for USERS_PROJECTS_ROLES - CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_update - AFTER UPDATE ON "USERS_PROJECTS_ROLES" - REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_update(); - - -- INSERT trigger for ROLES_PERMISSIONS - CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_insert - AFTER INSERT ON "ROLES_PERMISSIONS" - REFERENCING NEW TABLE AS new_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); - - -- DELETE trigger for ROLES_PERMISSIONS - CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_delete - AFTER DELETE ON "ROLES_PERMISSIONS" - REFERENCING OLD TABLE AS old_table - FOR EACH STATEMENT - EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); - - -- UPDATE trigger for ROLES_PERMISSIONS - CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_update - AFTER UPDATE ON "ROLES_PERMISSIONS" - REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table - FOR EACH STATEMENT - EXECUTE FUNCTION effective_permissions_mx_on_update(); - - - 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 5522cd1f49..d80df826f3 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 @@ -2286,4 +2286,262 @@
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- Helper function to recalculate all user permissions for a project. + -- Called by trigger functions to update the values in the USER_PROJECT_EFFECTIVE_PERMISSIONS table. + CREATE OR REPLACE FUNCTION recalc_user_project_role_effective_permissions(project_ids BIGINT[]) + RETURNS void AS $$ + DECLARE + tbl_prefix TEXT; + BEGIN + -- Remove any existing effective permissions for this project + DELETE FROM "USER_PROJECT_EFFECTIVE_PERMISSIONS" + WHERE "PROJECT_ID" = ANY(project_ids); + + -- Rebuild effective permissions for all users + INSERT INTO "USER_PROJECT_EFFECTIVE_PERMISSIONS" + ("USER_ID", "PROJECT_ID", "PERMISSION_ID", "PERMISSION_NAME") + SELECT DISTINCT upr."USER_ID", upr."PROJECT_ID", rp."PERMISSION_ID", p."NAME" + FROM "USERS_PROJECTS_ROLES" upr + INNER JOIN "ROLES_PERMISSIONS" rp + ON rp."ROLE_ID" = upr."ROLE_ID" + INNER JOIN "PERMISSION" p + ON p."ID" = rp."PERMISSION_ID" + WHERE upr."PROJECT_ID" = ANY(project_ids); + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_delete() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG(DISTINCT "ROLE_ID") + INTO role_ids + FROM old_table; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT upr."PROJECT_ID" + FROM "USERS_PROJECTS_ROLES" upr + INNER JOIN old_table + ON old_table."ROLE_ID" = upr."ROLE_ID" + ) sub; + ELSE + SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") + INTO project_ids + FROM "USERS_PROJECTS_ROLES" + WHERE "ROLE_ID" = ANY(role_ids); + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_insert() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG(DISTINCT "ROLE_ID") + INTO role_ids + FROM new_table; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT upr."PROJECT_ID" + FROM "USERS_PROJECTS_ROLES" upr + INNER JOIN new_table + ON new_table."ROLE_ID" = upr."ROLE_ID" + ) sub; + ELSE + SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") + INTO project_ids + FROM "USERS_PROJECTS_ROLES" + WHERE "ROLE_ID" = ANY(role_ids); + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + CREATE OR REPLACE FUNCTION role_effective_permissions_mx_on_update() + RETURNS TRIGGER AS $$ + DECLARE + project_ids BIGINT[]; + role_ids BIGINT[]; + BEGIN + SELECT ARRAY_AGG("ROLE_ID") + INTO role_ids + FROM ( + SELECT "ROLE_ID" FROM old_table + UNION + SELECT "ROLE_ID" FROM new_table + ) roles; + + IF TG_TABLE_NAME = 'ROLES_PERMISSIONS' THEN + SELECT ARRAY_AGG(sub."PROJECT_ID") + INTO project_ids + FROM ( + SELECT upr."PROJECT_ID" + FROM "USERS_PROJECTS_ROLES" upr + INNER JOIN new_table + ON new_table."ROLE_ID" = upr."ROLE_ID" + FULL OUTER JOIN old_table + ON new_table."ROLE_ID" = old_table."ROLE_ID" + ) sub; + ELSE + SELECT ARRAY_AGG(DISTINCT "PROJECT_ID") + INTO project_ids + FROM "USERS_PROJECTS_ROLES" + WHERE "ROLE_ID" = ANY(role_ids); + END IF; + + PERFORM recalc_user_project_role_effective_permissions(project_ids); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + + + -- INSERT trigger for USERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_insert + AFTER INSERT ON "USERS_PROJECTS_ROLES" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for USERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_delete + AFTER DELETE ON "USERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for USERS_PROJECTS_ROLES + CREATE TRIGGER trigger_effective_permissions_mx_on_users_roles_update + AFTER UPDATE ON "USERS_PROJECTS_ROLES" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_update(); + + -- INSERT trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_insert + AFTER INSERT ON "ROLES_PERMISSIONS" + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_insert(); + + -- DELETE trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_delete + AFTER DELETE ON "ROLES_PERMISSIONS" + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION role_effective_permissions_mx_on_delete(); + + -- UPDATE trigger for ROLES_PERMISSIONS + CREATE TRIGGER trigger_effective_permissions_mx_on_roles_permissions_update + AFTER UPDATE ON "ROLES_PERMISSIONS" + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT + EXECUTE FUNCTION effective_permissions_mx_on_update(); + + From 8cef6fd718dad98f37af7f2ff60ce6e457a44386 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Tue, 20 May 2025 13:13:40 -0500 Subject: [PATCH 161/181] chore: generate deterministic serialVersionUID values Signed-off-by: Jonathan Howard --- persistence-jooq/pom.xml | 3 +++ .../persistence/jooq/generated/DefaultCatalog.java | 2 +- .../persistence/jooq/generated/DefaultSchema.java | 2 +- .../persistence/jooq/generated/routines/CalcRiskScore.java | 2 +- .../persistence/jooq/generated/routines/HasProjectAccess.java | 2 +- .../persistence/jooq/generated/routines/JsonbVulnAliases.java | 2 +- .../routines/RecalcUserProjectEffectivePermissions.java | 2 +- .../routines/RecalcUserProjectRoleEffectivePermissions.java | 2 +- .../jooq/generated/routines/UpdateComponentMetrics.java | 2 +- .../jooq/generated/routines/UpdatePortfolioMetrics.java | 2 +- .../jooq/generated/routines/UpdateProjectMetrics.java | 2 +- .../jooq/generated/tables/AffectedVersionAttribution.java | 4 ++-- .../persistence/jooq/generated/tables/Analysis.java | 4 ++-- .../persistence/jooq/generated/tables/AnalysisComment.java | 4 ++-- .../persistence/jooq/generated/tables/ApiKey.java | 4 ++-- .../persistence/jooq/generated/tables/ApiKeysTeams.java | 4 ++-- .../persistence/jooq/generated/tables/Bom.java | 4 ++-- .../persistence/jooq/generated/tables/Component.java | 4 ++-- .../jooq/generated/tables/ComponentOccurrence.java | 4 ++-- .../persistence/jooq/generated/tables/ComponentProperty.java | 4 ++-- .../jooq/generated/tables/ComponentsVulnerabilities.java | 4 ++-- .../persistence/jooq/generated/tables/ConfigProperty.java | 2 +- .../persistence/jooq/generated/tables/DependencyMetrics.java | 4 ++-- .../persistence/jooq/generated/tables/Epss.java | 2 +- .../persistence/jooq/generated/tables/FindingAttribution.java | 4 ++-- .../persistence/jooq/generated/tables/IntegrityAnalysis.java | 4 ++-- .../jooq/generated/tables/IntegrityMetaComponent.java | 2 +- .../persistence/jooq/generated/tables/License.java | 4 ++-- .../persistence/jooq/generated/tables/LicenseGroup.java | 4 ++-- .../jooq/generated/tables/LicenseGroupLicense.java | 4 ++-- .../persistence/jooq/generated/tables/MappedLdapGroup.java | 4 ++-- .../persistence/jooq/generated/tables/MappedOidcGroup.java | 4 ++-- .../jooq/generated/tables/NotificationPublisher.java | 4 ++-- .../persistence/jooq/generated/tables/NotificationRule.java | 4 ++-- .../jooq/generated/tables/NotificationRuleProjects.java | 4 ++-- .../jooq/generated/tables/NotificationRuleTags.java | 4 ++-- .../jooq/generated/tables/NotificationRuleTeams.java | 4 ++-- .../persistence/jooq/generated/tables/OidcGroup.java | 4 ++-- .../persistence/jooq/generated/tables/Permission.java | 4 ++-- .../persistence/jooq/generated/tables/Policy.java | 4 ++-- .../persistence/jooq/generated/tables/PolicyCondition.java | 4 ++-- .../persistence/jooq/generated/tables/PolicyProjects.java | 4 ++-- .../persistence/jooq/generated/tables/PolicyTags.java | 4 ++-- .../persistence/jooq/generated/tables/PolicyViolation.java | 4 ++-- .../persistence/jooq/generated/tables/PortfolioMetrics.java | 2 +- .../persistence/jooq/generated/tables/Project.java | 4 ++-- .../persistence/jooq/generated/tables/ProjectAccessTeams.java | 4 ++-- .../persistence/jooq/generated/tables/ProjectHierarchy.java | 4 ++-- .../persistence/jooq/generated/tables/ProjectMetadata.java | 4 ++-- .../persistence/jooq/generated/tables/ProjectMetrics.java | 4 ++-- .../persistence/jooq/generated/tables/ProjectProperty.java | 4 ++-- .../persistence/jooq/generated/tables/ProjectsTags.java | 4 ++-- .../persistence/jooq/generated/tables/Repository.java | 2 +- .../jooq/generated/tables/RepositoryMetaComponent.java | 2 +- .../persistence/jooq/generated/tables/Role.java | 4 ++-- .../persistence/jooq/generated/tables/RolesPermissions.java | 4 ++-- .../persistence/jooq/generated/tables/ServiceComponent.java | 4 ++-- .../generated/tables/ServiceComponentsVulnerabilities.java | 4 ++-- .../persistence/jooq/generated/tables/Tag.java | 4 ++-- .../persistence/jooq/generated/tables/Team.java | 4 ++-- .../persistence/jooq/generated/tables/TeamsPermissions.java | 4 ++-- .../persistence/jooq/generated/tables/User.java | 4 ++-- .../generated/tables/UserProjectEffectivePermissions.java | 4 ++-- .../persistence/jooq/generated/tables/UsersPermissions.java | 4 ++-- .../persistence/jooq/generated/tables/UsersProjectsRoles.java | 4 ++-- .../persistence/jooq/generated/tables/UsersTeams.java | 4 ++-- .../persistence/jooq/generated/tables/Vex.java | 4 ++-- .../persistence/jooq/generated/tables/ViolationAnalysis.java | 4 ++-- .../jooq/generated/tables/ViolationAnalysisComment.java | 4 ++-- .../jooq/generated/tables/VulnerabilitiesTags.java | 4 ++-- .../persistence/jooq/generated/tables/Vulnerability.java | 4 ++-- .../persistence/jooq/generated/tables/VulnerabilityAlias.java | 2 +- .../jooq/generated/tables/VulnerabilityMetrics.java | 2 +- .../jooq/generated/tables/VulnerabilityPolicy.java | 4 ++-- .../jooq/generated/tables/VulnerabilityPolicyBundle.java | 2 +- .../persistence/jooq/generated/tables/VulnerabilityScan.java | 2 +- .../persistence/jooq/generated/tables/VulnerableSoftware.java | 4 ++-- .../generated/tables/VulnerableSoftwareVulnerabilities.java | 4 ++-- .../persistence/jooq/generated/tables/WorkflowState.java | 4 ++-- .../tables/records/AffectedVersionAttributionRecord.java | 2 +- .../jooq/generated/tables/records/AnalysisCommentRecord.java | 2 +- .../jooq/generated/tables/records/AnalysisRecord.java | 2 +- .../jooq/generated/tables/records/ApiKeyRecord.java | 2 +- .../jooq/generated/tables/records/ApiKeysTeamsRecord.java | 2 +- .../persistence/jooq/generated/tables/records/BomRecord.java | 2 +- .../generated/tables/records/ComponentOccurrenceRecord.java | 2 +- .../generated/tables/records/ComponentPropertyRecord.java | 2 +- .../jooq/generated/tables/records/ComponentRecord.java | 2 +- .../tables/records/ComponentsVulnerabilitiesRecord.java | 2 +- .../jooq/generated/tables/records/ConfigPropertyRecord.java | 2 +- .../generated/tables/records/DependencyMetricsRecord.java | 2 +- .../persistence/jooq/generated/tables/records/EpssRecord.java | 2 +- .../generated/tables/records/FindingAttributionRecord.java | 2 +- .../generated/tables/records/IntegrityAnalysisRecord.java | 2 +- .../tables/records/IntegrityMetaComponentRecord.java | 2 +- .../generated/tables/records/LicenseGroupLicenseRecord.java | 2 +- .../jooq/generated/tables/records/LicenseGroupRecord.java | 2 +- .../jooq/generated/tables/records/LicenseRecord.java | 2 +- .../jooq/generated/tables/records/MappedLdapGroupRecord.java | 2 +- .../jooq/generated/tables/records/MappedOidcGroupRecord.java | 2 +- .../generated/tables/records/NotificationPublisherRecord.java | 2 +- .../tables/records/NotificationRuleProjectsRecord.java | 2 +- .../jooq/generated/tables/records/NotificationRuleRecord.java | 2 +- .../generated/tables/records/NotificationRuleTagsRecord.java | 2 +- .../generated/tables/records/NotificationRuleTeamsRecord.java | 2 +- .../jooq/generated/tables/records/OidcGroupRecord.java | 2 +- .../jooq/generated/tables/records/PermissionRecord.java | 2 +- .../jooq/generated/tables/records/PolicyConditionRecord.java | 2 +- .../jooq/generated/tables/records/PolicyProjectsRecord.java | 2 +- .../jooq/generated/tables/records/PolicyRecord.java | 2 +- .../jooq/generated/tables/records/PolicyTagsRecord.java | 2 +- .../jooq/generated/tables/records/PolicyViolationRecord.java | 2 +- .../jooq/generated/tables/records/PortfolioMetricsRecord.java | 2 +- .../generated/tables/records/ProjectAccessTeamsRecord.java | 2 +- .../jooq/generated/tables/records/ProjectHierarchyRecord.java | 2 +- .../jooq/generated/tables/records/ProjectMetadataRecord.java | 2 +- .../jooq/generated/tables/records/ProjectMetricsRecord.java | 2 +- .../jooq/generated/tables/records/ProjectPropertyRecord.java | 2 +- .../jooq/generated/tables/records/ProjectRecord.java | 2 +- .../jooq/generated/tables/records/ProjectsTagsRecord.java | 2 +- .../tables/records/RepositoryMetaComponentRecord.java | 2 +- .../jooq/generated/tables/records/RepositoryRecord.java | 2 +- .../persistence/jooq/generated/tables/records/RoleRecord.java | 2 +- .../jooq/generated/tables/records/RolesPermissionsRecord.java | 2 +- .../jooq/generated/tables/records/ServiceComponentRecord.java | 2 +- .../records/ServiceComponentsVulnerabilitiesRecord.java | 2 +- .../persistence/jooq/generated/tables/records/TagRecord.java | 2 +- .../persistence/jooq/generated/tables/records/TeamRecord.java | 2 +- .../jooq/generated/tables/records/TeamsPermissionsRecord.java | 2 +- .../tables/records/UserProjectEffectivePermissionsRecord.java | 2 +- .../persistence/jooq/generated/tables/records/UserRecord.java | 2 +- .../jooq/generated/tables/records/UsersPermissionsRecord.java | 2 +- .../generated/tables/records/UsersProjectsRolesRecord.java | 2 +- .../jooq/generated/tables/records/UsersTeamsRecord.java | 2 +- .../persistence/jooq/generated/tables/records/VexRecord.java | 2 +- .../tables/records/ViolationAnalysisCommentRecord.java | 2 +- .../generated/tables/records/ViolationAnalysisRecord.java | 2 +- .../generated/tables/records/VulnerabilitiesTagsRecord.java | 2 +- .../generated/tables/records/VulnerabilityAliasRecord.java | 2 +- .../generated/tables/records/VulnerabilityMetricsRecord.java | 2 +- .../tables/records/VulnerabilityPolicyBundleRecord.java | 2 +- .../generated/tables/records/VulnerabilityPolicyRecord.java | 2 +- .../jooq/generated/tables/records/VulnerabilityRecord.java | 2 +- .../generated/tables/records/VulnerabilityScanRecord.java | 2 +- .../generated/tables/records/VulnerableSoftwareRecord.java | 2 +- .../records/VulnerableSoftwareVulnerabilitiesRecord.java | 2 +- .../jooq/generated/tables/records/WorkflowStateRecord.java | 2 +- 147 files changed, 207 insertions(+), 204 deletions(-) diff --git a/persistence-jooq/pom.xml b/persistence-jooq/pom.xml index e199b10c90..8b830a97ba 100644 --- a/persistence-jooq/pom.xml +++ b/persistence-jooq/pom.xml @@ -143,6 +143,7 @@ true + HASH @@ -230,6 +231,7 @@ + @@ -327,6 +329,7 @@ + $0 diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultCatalog.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultCatalog.java index e025e57b84..bcd691af0e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultCatalog.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultCatalog.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class DefaultCatalog extends CatalogImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -378744483; /** * The reference instance of DEFAULT_CATALOG diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java index ab0a3a9928..dcd69db81e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/DefaultSchema.java @@ -97,7 +97,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class DefaultSchema extends SchemaImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1575785433; /** * The reference instance of DEFAULT_SCHEMA diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/CalcRiskScore.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/CalcRiskScore.java index c12fd9aeb1..8993cf3f17 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/CalcRiskScore.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/CalcRiskScore.java @@ -31,7 +31,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class CalcRiskScore extends AbstractRoutine { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -2141815729; /** * The parameter CALC_RISK_SCORE.RETURN_VALUE. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/HasProjectAccess.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/HasProjectAccess.java index 9fa6af7fdc..23e62fcde4 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/HasProjectAccess.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/HasProjectAccess.java @@ -29,7 +29,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class HasProjectAccess extends AbstractRoutine { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1366074265; /** * The parameter has_project_access.RETURN_VALUE. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/JsonbVulnAliases.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/JsonbVulnAliases.java index dbfd3ac7e8..e74a0899a3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/JsonbVulnAliases.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/JsonbVulnAliases.java @@ -30,7 +30,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class JsonbVulnAliases extends AbstractRoutine { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1914958624; /** * The parameter jsonb_vuln_aliases.RETURN_VALUE. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectEffectivePermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectEffectivePermissions.java index c300e10706..9d8a1c1f33 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectEffectivePermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectEffectivePermissions.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class RecalcUserProjectEffectivePermissions extends AbstractRoutine { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -2142402630; /** * The parameter diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectRoleEffectivePermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectRoleEffectivePermissions.java index a7ba879f49..4f50b5d7d9 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectRoleEffectivePermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/RecalcUserProjectRoleEffectivePermissions.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class RecalcUserProjectRoleEffectivePermissions extends AbstractRoutine { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2073358932; /** * The parameter diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateComponentMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateComponentMetrics.java index f8d0a90b23..b761cba401 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateComponentMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateComponentMetrics.java @@ -30,7 +30,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UpdateComponentMetrics extends AbstractRoutine { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1702575277; /** * The parameter UPDATE_COMPONENT_METRICS.component_uuid. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java index 9f9250cff4..bde55628d8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdatePortfolioMetrics.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UpdatePortfolioMetrics extends AbstractRoutine { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1444263983; /** * Create a new routine call instance diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateProjectMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateProjectMetrics.java index cd0e55d97a..4a067a768d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateProjectMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/routines/UpdateProjectMetrics.java @@ -30,7 +30,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UpdateProjectMetrics extends AbstractRoutine { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -612851025; /** * The parameter UPDATE_PROJECT_METRICS.project_uuid. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AffectedVersionAttribution.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AffectedVersionAttribution.java index ab2749f25a..1c12933d5c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AffectedVersionAttribution.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AffectedVersionAttribution.java @@ -56,7 +56,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class AffectedVersionAttribution extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -938219356; /** * The reference instance of AFFECTEDVERSIONATTRIBUTION @@ -152,7 +152,7 @@ public AffectedVersionAttribution(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -938219356; public AffectedVersionAttributionPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Analysis.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Analysis.java index 37139f4811..79cc5cd650 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Analysis.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Analysis.java @@ -59,7 +59,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Analysis extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -676941835; /** * The reference instance of ANALYSIS @@ -205,7 +205,7 @@ public Analysis(Table path, ForeignKey ) public static class AnalysisPath extends Analysis implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -676941835; public AnalysisPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AnalysisComment.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AnalysisComment.java index ceda269163..e177fa9b31 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AnalysisComment.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/AnalysisComment.java @@ -54,7 +54,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class AnalysisComment extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 67920414; /** * The reference instance of ANALYSISCOMMENT @@ -140,7 +140,7 @@ public AnalysisComment(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 67920414; public AnalysisCommentPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKey.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKey.java index fc7e6788e9..8b8de0594f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKey.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKey.java @@ -55,7 +55,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ApiKey extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -711465365; /** * The reference instance of APIKEY @@ -151,7 +151,7 @@ public ApiKey(Table path, ForeignKey chil ) public static class ApiKeyPath extends ApiKey implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -711465365; public ApiKeyPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKeysTeams.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKeysTeams.java index b70612e22e..77c31c58e3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKeysTeams.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ApiKeysTeams.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ApiKeysTeams extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1570762444; /** * The reference instance of APIKEYS_TEAMS @@ -122,7 +122,7 @@ public ApiKeysTeams(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1570762444; public ApiKeysTeamsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Bom.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Bom.java index 5ae7ff239c..95b444ad30 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Bom.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Bom.java @@ -55,7 +55,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Bom extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -979504904; /** * The reference instance of BOM @@ -161,7 +161,7 @@ public Bom(Table path, ForeignKey childPath, ) public static class BomPath extends Bom implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -979504904; public BomPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Component.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Component.java index 378de6b4fc..f185bdb339 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Component.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Component.java @@ -68,7 +68,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Component extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1731235713; /** * The reference instance of COMPONENT @@ -329,7 +329,7 @@ public Component(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1731235713; public ComponentPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentOccurrence.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentOccurrence.java index d23376bc8e..1a6636480a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentOccurrence.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentOccurrence.java @@ -54,7 +54,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ComponentOccurrence extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1139580904; /** * The reference instance of COMPONENT_OCCURRENCE @@ -150,7 +150,7 @@ public ComponentOccurrence(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1139580904; public ComponentOccurrencePath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentProperty.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentProperty.java index 699621ebcc..385e93e1c7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentProperty.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentProperty.java @@ -56,7 +56,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ComponentProperty extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2142097110; /** * The reference instance of COMPONENT_PROPERTY @@ -157,7 +157,7 @@ public ComponentProperty(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2142097110; public ComponentPropertyPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentsVulnerabilities.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentsVulnerabilities.java index 42be6d3f0f..5b5b6e13ec 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentsVulnerabilities.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ComponentsVulnerabilities.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ComponentsVulnerabilities extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -2111707182; /** * The reference instance of COMPONENTS_VULNERABILITIES @@ -123,7 +123,7 @@ public ComponentsVulnerabilities(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -2111707182; public ComponentsVulnerabilitiesPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ConfigProperty.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ConfigProperty.java index 3ccf71ad28..865ddcbf2f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ConfigProperty.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ConfigProperty.java @@ -46,7 +46,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ConfigProperty extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1963189124; /** * The reference instance of CONFIGPROPERTY diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/DependencyMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/DependencyMetrics.java index 098bd9db67..1e8cff1cdf 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/DependencyMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/DependencyMetrics.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class DependencyMetrics extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -589450869; /** * The reference instance of DEPENDENCYMETRICS @@ -271,7 +271,7 @@ public DependencyMetrics(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -589450869; public DependencyMetricsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Epss.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Epss.java index 3b47d60042..126041a6c7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Epss.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Epss.java @@ -49,7 +49,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Epss extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1806497615; /** * The reference instance of EPSS diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/FindingAttribution.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/FindingAttribution.java index d4cdb908e7..ad6073cd40 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/FindingAttribution.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/FindingAttribution.java @@ -57,7 +57,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class FindingAttribution extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1832451837; /** * The reference instance of FINDINGATTRIBUTION @@ -163,7 +163,7 @@ public FindingAttribution(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1832451837; public FindingAttributionPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityAnalysis.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityAnalysis.java index 690e917632..dfccb4a670 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityAnalysis.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityAnalysis.java @@ -54,7 +54,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class IntegrityAnalysis extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1888012935; /** * The reference instance of INTEGRITY_ANALYSIS @@ -155,7 +155,7 @@ public IntegrityAnalysis(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1888012935; public IntegrityAnalysisPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityMetaComponent.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityMetaComponent.java index 6f11a64d26..84856f58c6 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityMetaComponent.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/IntegrityMetaComponent.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class IntegrityMetaComponent extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1317194047; /** * The reference instance of INTEGRITY_META_COMPONENT diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/License.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/License.java index 0deff1578e..011ae0778d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/License.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/License.java @@ -55,7 +55,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class License extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 660778313; /** * The reference instance of LICENSE @@ -181,7 +181,7 @@ public License(Table path, ForeignKey ch ) public static class LicensePath extends License implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 660778313; public LicensePath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroup.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroup.java index ed81170d79..ffcbe3ab51 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroup.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroup.java @@ -54,7 +54,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class LicenseGroup extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1648671036; /** * The reference instance of LICENSEGROUP @@ -135,7 +135,7 @@ public LicenseGroup(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1648671036; public LicenseGroupPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroupLicense.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroupLicense.java index c22099f947..fb731ddec5 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroupLicense.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/LicenseGroupLicense.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class LicenseGroupLicense extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 224650408; /** * The reference instance of LICENSEGROUP_LICENSE @@ -123,7 +123,7 @@ public LicenseGroupLicense(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 224650408; public LicenseGroupLicensePath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/MappedLdapGroup.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/MappedLdapGroup.java index 76ab8543b5..0ff959fd1c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/MappedLdapGroup.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/MappedLdapGroup.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class MappedLdapGroup extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1776230963; /** * The reference instance of MAPPEDLDAPGROUP @@ -132,7 +132,7 @@ public MappedLdapGroup(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1776230963; public MappedLdapGroupPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/MappedOidcGroup.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/MappedOidcGroup.java index 02faad4117..d46720a004 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/MappedOidcGroup.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/MappedOidcGroup.java @@ -54,7 +54,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class MappedOidcGroup extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 610435687; /** * The reference instance of MAPPEDOIDCGROUP @@ -135,7 +135,7 @@ public MappedOidcGroup(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 610435687; public MappedOidcGroupPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationPublisher.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationPublisher.java index 806cd72bec..d4ea29e6eb 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationPublisher.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationPublisher.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationPublisher extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1076695095; /** * The reference instance of NOTIFICATIONPUBLISHER @@ -153,7 +153,7 @@ public NotificationPublisher(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1076695095; public NotificationPublisherPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRule.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRule.java index e5fa243b32..9c0bae12af 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRule.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRule.java @@ -59,7 +59,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationRule extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1113696852; /** * The reference instance of NOTIFICATIONRULE @@ -180,7 +180,7 @@ public NotificationRule(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1113696852; public NotificationRulePath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleProjects.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleProjects.java index 25f5b13272..2eaf3bd7f3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleProjects.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleProjects.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationRuleProjects extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 810213248; /** * The reference instance of NOTIFICATIONRULE_PROJECTS @@ -123,7 +123,7 @@ public NotificationRuleProjects(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 810213248; public NotificationRuleProjectsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleTags.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleTags.java index b9ac2e0696..2209a6d143 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleTags.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleTags.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationRuleTags extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1998603530; /** * The reference instance of NOTIFICATIONRULE_TAGS @@ -122,7 +122,7 @@ public NotificationRuleTags(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1998603530; public NotificationRuleTagsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleTeams.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleTeams.java index 8ae7baef91..ff730d97df 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleTeams.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/NotificationRuleTeams.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationRuleTeams extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 802684418; /** * The reference instance of NOTIFICATIONRULE_TEAMS @@ -122,7 +122,7 @@ public NotificationRuleTeams(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 802684418; public NotificationRuleTeamsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/OidcGroup.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/OidcGroup.java index b99f4d00e6..23e91a6b06 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/OidcGroup.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/OidcGroup.java @@ -53,7 +53,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class OidcGroup extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -561573330; /** * The reference instance of OIDCGROUP @@ -129,7 +129,7 @@ public OidcGroup(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -561573330; public OidcGroupPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Permission.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Permission.java index 4a907c9ea1..35345c8419 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Permission.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Permission.java @@ -57,7 +57,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Permission extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1134556928; /** * The reference instance of PERMISSION @@ -133,7 +133,7 @@ public Permission(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1134556928; public PermissionPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Policy.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Policy.java index 2f4eab2c25..03cacc345f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Policy.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Policy.java @@ -57,7 +57,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Policy extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -763533389; /** * The reference instance of POLICY @@ -153,7 +153,7 @@ public Policy(Table path, ForeignKey chil ) public static class PolicyPath extends Policy implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -763533389; public PolicyPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyCondition.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyCondition.java index 9b455d8cf0..0419247592 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyCondition.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyCondition.java @@ -55,7 +55,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyCondition extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -765297543; /** * The reference instance of POLICYCONDITION @@ -151,7 +151,7 @@ public PolicyCondition(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -765297543; public PolicyConditionPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyProjects.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyProjects.java index 5806e0f1cd..ee93f6bbcf 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyProjects.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyProjects.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyProjects extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -599792149; /** * The reference instance of POLICY_PROJECTS @@ -123,7 +123,7 @@ public PolicyProjects(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -599792149; public PolicyProjectsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyTags.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyTags.java index 0e84e1ccbc..f4336355ee 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyTags.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyTags.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyTags extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -971385270; /** * The reference instance of POLICY_TAGS @@ -122,7 +122,7 @@ public PolicyTags(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -971385270; public PolicyTagsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyViolation.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyViolation.java index 12a913fe24..7e9758d98d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyViolation.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PolicyViolation.java @@ -58,7 +58,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyViolation extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1684309677; /** * The reference instance of POLICYVIOLATION @@ -159,7 +159,7 @@ public PolicyViolation(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1684309677; public PolicyViolationPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PortfolioMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PortfolioMetrics.java index 2324ce86af..9a0df136e8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PortfolioMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/PortfolioMetrics.java @@ -44,7 +44,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PortfolioMetrics extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1604183279; /** * The reference instance of PORTFOLIOMETRICS diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Project.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Project.java index 412a7f7ab9..244fa8918a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Project.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Project.java @@ -79,7 +79,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Project extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1297445188; /** * The reference instance of PROJECT @@ -250,7 +250,7 @@ public Project(Table path, ForeignKey ch ) public static class ProjectPath extends Project implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1297445188; public ProjectPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectAccessTeams.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectAccessTeams.java index ce78ae5414..009718437f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectAccessTeams.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectAccessTeams.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectAccessTeams extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 873948858; /** * The reference instance of PROJECT_ACCESS_TEAMS @@ -122,7 +122,7 @@ public ProjectAccessTeams(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 873948858; public ProjectAccessTeamsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectHierarchy.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectHierarchy.java index b59598c0d2..3f2cfd54ec 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectHierarchy.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectHierarchy.java @@ -50,7 +50,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectHierarchy extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 75786160; /** * The reference instance of PROJECT_HIERARCHY @@ -126,7 +126,7 @@ public ProjectHierarchy(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 75786160; public ProjectHierarchyPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetadata.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetadata.java index bccb484757..359b8b5a47 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetadata.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetadata.java @@ -53,7 +53,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectMetadata extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 228933682; /** * The reference instance of PROJECT_METADATA @@ -139,7 +139,7 @@ public ProjectMetadata(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 228933682; public ProjectMetadataPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetrics.java index 9901df67e8..12477b9928 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectMetrics.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectMetrics extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1382780670; /** * The reference instance of PROJECTMETRICS @@ -272,7 +272,7 @@ public ProjectMetrics(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1382780670; public ProjectMetricsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectProperty.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectProperty.java index e0de508fb6..c5b1e9ea29 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectProperty.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectProperty.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectProperty extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 44418381; /** * The reference instance of PROJECT_PROPERTY @@ -147,7 +147,7 @@ public ProjectProperty(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 44418381; public ProjectPropertyPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectsTags.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectsTags.java index 3aff1e32b0..346fb62f47 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectsTags.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ProjectsTags.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectsTags extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -163939250; /** * The reference instance of PROJECTS_TAGS @@ -122,7 +122,7 @@ public ProjectsTags(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -163939250; public ProjectsTagsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Repository.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Repository.java index 47d40b14b6..7ddf8ff687 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Repository.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Repository.java @@ -49,7 +49,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Repository extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1139153193; /** * The reference instance of REPOSITORY diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RepositoryMetaComponent.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RepositoryMetaComponent.java index 763663bfb7..c370ea530d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RepositoryMetaComponent.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RepositoryMetaComponent.java @@ -49,7 +49,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class RepositoryMetaComponent extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1187611547; /** * The reference instance of REPOSITORY_META_COMPONENT diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Role.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Role.java index 48168398e9..8092f464a2 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Role.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Role.java @@ -54,7 +54,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Role extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1832423169; /** * The reference instance of ROLE @@ -130,7 +130,7 @@ public Role(Table path, ForeignKey childPat ) public static class RolePath extends Role implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1832423169; public RolePath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RolesPermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RolesPermissions.java index 1c3d96367c..e783f8437e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RolesPermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/RolesPermissions.java @@ -53,7 +53,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class RolesPermissions extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 188042180; /** * The reference instance of ROLES_PERMISSIONS @@ -124,7 +124,7 @@ public RolesPermissions(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 188042180; public RolesPermissionsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponent.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponent.java index c58a5d23f8..c5628dfa8d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponent.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponent.java @@ -56,7 +56,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ServiceComponent extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1366507819; /** * The reference instance of SERVICECOMPONENT @@ -197,7 +197,7 @@ public ServiceComponent(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1366507819; public ServiceComponentPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponentsVulnerabilities.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponentsVulnerabilities.java index c68811db44..daa582f4e6 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponentsVulnerabilities.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ServiceComponentsVulnerabilities.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ServiceComponentsVulnerabilities extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -991095685; /** * The reference instance of SERVICECOMPONENTS_VULNERABILITIES @@ -127,7 +127,7 @@ public ServiceComponentsVulnerabilities(Table path, Foreig ) public static class ServiceComponentsVulnerabilitiesPath extends ServiceComponentsVulnerabilities implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -991095685; public ServiceComponentsVulnerabilitiesPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Tag.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Tag.java index 71d2a95fa3..a57b60bee6 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Tag.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Tag.java @@ -59,7 +59,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Tag extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -59691910; /** * The reference instance of TAG @@ -130,7 +130,7 @@ public Tag(Table path, ForeignKey childPath, ) public static class TagPath extends Tag implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -59691910; public TagPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Team.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Team.java index 8f9fe5f8cd..b73798a4f6 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Team.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Team.java @@ -60,7 +60,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Team extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 638132231; /** * The reference instance of TEAM @@ -136,7 +136,7 @@ public Team(Table path, ForeignKey childPat ) public static class TeamPath extends Team implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 638132231; public TeamPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/TeamsPermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/TeamsPermissions.java index 5f402d415c..10df9304d3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/TeamsPermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/TeamsPermissions.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class TeamsPermissions extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1593956964; /** * The reference instance of TEAMS_PERMISSIONS @@ -122,7 +122,7 @@ public TeamsPermissions(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1593956964; public TeamsPermissionsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/User.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/User.java index 11d9dd7e76..3505e8d1ab 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/User.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/User.java @@ -61,7 +61,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class User extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -712068352; /** * The reference instance of USER @@ -182,7 +182,7 @@ public User(Table path, ForeignKey childPat ) public static class UserPath extends User implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -712068352; public UserPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UserProjectEffectivePermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UserProjectEffectivePermissions.java index 799ff1e8d4..4a45ced47d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UserProjectEffectivePermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UserProjectEffectivePermissions.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UserProjectEffectivePermissions extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2035432985; /** * The reference instance of USER_PROJECT_EFFECTIVE_PERMISSIONS @@ -136,7 +136,7 @@ public UserProjectEffectivePermissions(Table path, Foreign ) public static class UserProjectEffectivePermissionsPath extends UserProjectEffectivePermissions implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2035432985; public UserProjectEffectivePermissionsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersPermissions.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersPermissions.java index 583e0697f0..da021e3340 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersPermissions.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersPermissions.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UsersPermissions extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1206928892; /** * The reference instance of USERS_PERMISSIONS @@ -122,7 +122,7 @@ public UsersPermissions(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1206928892; public UsersPermissionsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersProjectsRoles.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersProjectsRoles.java index 459fe04660..174bb3074f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersProjectsRoles.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersProjectsRoles.java @@ -53,7 +53,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UsersProjectsRoles extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 622585294; /** * The reference instance of USERS_PROJECTS_ROLES @@ -129,7 +129,7 @@ public UsersProjectsRoles(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 622585294; public UsersProjectsRolesPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersTeams.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersTeams.java index 7189a86ec3..06ebbc541f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersTeams.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/UsersTeams.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UsersTeams extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 274318204; /** * The reference instance of USERS_TEAMS @@ -122,7 +122,7 @@ public UsersTeams(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 274318204; public UsersTeamsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vex.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vex.java index 3dad1e7c95..df860ea400 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vex.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vex.java @@ -55,7 +55,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Vex extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -865420210; /** * The reference instance of VEX @@ -156,7 +156,7 @@ public Vex(Table path, ForeignKey childPath, ) public static class VexPath extends Vex implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -865420210; public VexPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysis.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysis.java index 38f1b14bd1..94a9914ec3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysis.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysis.java @@ -56,7 +56,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ViolationAnalysis extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1089999461; /** * The reference instance of VIOLATIONANALYSIS @@ -147,7 +147,7 @@ public ViolationAnalysis(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1089999461; public ViolationAnalysisPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysisComment.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysisComment.java index bc6d35b989..4bbea6eeb3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysisComment.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/ViolationAnalysisComment.java @@ -54,7 +54,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ViolationAnalysisComment extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 833389648; /** * The reference instance of VIOLATIONANALYSISCOMMENT @@ -140,7 +140,7 @@ public ViolationAnalysisComment(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 833389648; public ViolationAnalysisCommentPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilitiesTags.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilitiesTags.java index 578e9f0c93..90d1c2acac 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilitiesTags.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilitiesTags.java @@ -51,7 +51,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilitiesTags extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1626926470; /** * The reference instance of VULNERABILITIES_TAGS @@ -122,7 +122,7 @@ public VulnerabilitiesTags(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1626926470; public VulnerabilitiesTagsPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vulnerability.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vulnerability.java index dd2b0bbfc4..43bf41e95d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vulnerability.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/Vulnerability.java @@ -64,7 +64,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class Vulnerability extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 194354309; /** * The reference instance of VULNERABILITY @@ -280,7 +280,7 @@ public Vulnerability(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 194354309; public VulnerabilityPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityAlias.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityAlias.java index 002410d013..bc898db017 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityAlias.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityAlias.java @@ -49,7 +49,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityAlias extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2010552213; /** * The reference instance of VULNERABILITYALIAS diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityMetrics.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityMetrics.java index 6f92fde3ce..0f076f367b 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityMetrics.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityMetrics.java @@ -45,7 +45,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityMetrics extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1647962504; /** * The reference instance of VULNERABILITYMETRICS diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicy.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicy.java index 3edf69354b..d6c218b60e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicy.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicy.java @@ -55,7 +55,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityPolicy extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -751906294; /** * The reference instance of VULNERABILITY_POLICY @@ -176,7 +176,7 @@ public VulnerabilityPolicy(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -751906294; public VulnerabilityPolicyPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicyBundle.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicyBundle.java index 266b319c4e..823a2208db 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicyBundle.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityPolicyBundle.java @@ -45,7 +45,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityPolicyBundle extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -653983299; /** * The reference instance of VULNERABILITY_POLICY_BUNDLE diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityScan.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityScan.java index 8e788ef366..042b62043d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityScan.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerabilityScan.java @@ -48,7 +48,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityScan extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 936592231; /** * The reference instance of VULNERABILITYSCAN diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftware.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftware.java index df46792c81..7f6b561bf8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftware.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftware.java @@ -55,7 +55,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerableSoftware extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1199573646; /** * The reference instance of VULNERABLESOFTWARE @@ -251,7 +251,7 @@ public VulnerableSoftware(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1199573646; public VulnerableSoftwarePath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftwareVulnerabilities.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftwareVulnerabilities.java index a1311e6ae1..476f3c573e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftwareVulnerabilities.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/VulnerableSoftwareVulnerabilities.java @@ -52,7 +52,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerableSoftwareVulnerabilities extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -587467024; /** * The reference instance of VULNERABLESOFTWARE_VULNERABILITIES @@ -127,7 +127,7 @@ public VulnerableSoftwareVulnerabilities(Table path, Forei ) public static class VulnerableSoftwareVulnerabilitiesPath extends VulnerableSoftwareVulnerabilities implements Path { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -587467024; public VulnerableSoftwareVulnerabilitiesPath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/WorkflowState.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/WorkflowState.java index d8f1da3e90..97dab01600 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/WorkflowState.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/WorkflowState.java @@ -57,7 +57,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class WorkflowState extends TableImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 136710125; /** * The reference instance of WORKFLOW_STATE @@ -158,7 +158,7 @@ public WorkflowState(Table path, ForeignKey { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 136710125; public WorkflowStatePath(Table path, ForeignKey childPath, InverseForeignKey parentPath) { super(path, childPath, parentPath); } diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AffectedVersionAttributionRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AffectedVersionAttributionRecord.java index 026062679d..7d7ae2428d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AffectedVersionAttributionRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AffectedVersionAttributionRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class AffectedVersionAttributionRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 830189685; /** * Setter for AFFECTEDVERSIONATTRIBUTION.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisCommentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisCommentRecord.java index 37e3dcc00a..c387cdcb0e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisCommentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisCommentRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class AnalysisCommentRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 683571869; /** * Setter for ANALYSISCOMMENT.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisRecord.java index 5dd2bcda49..f72bc82c6f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/AnalysisRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class AnalysisRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1074618273; /** * Setter for ANALYSIS.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeyRecord.java index 4684ad3ec6..aef891db85 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeyRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ApiKeyRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -285420750; /** * Setter for APIKEY.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeysTeamsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeysTeamsRecord.java index 7d9bf9757c..9b5a605556 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeysTeamsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ApiKeysTeamsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ApiKeysTeamsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -2091283289; /** * Setter for APIKEYS_TEAMS.TEAM_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/BomRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/BomRecord.java index 0480329d10..e1920895e7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/BomRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/BomRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class BomRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1158088535; /** * Setter for BOM.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentOccurrenceRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentOccurrenceRecord.java index 1c5cfe6c9a..6ef69f9020 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentOccurrenceRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentOccurrenceRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ComponentOccurrenceRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1566639764; /** * Setter for COMPONENT_OCCURRENCE.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentPropertyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentPropertyRecord.java index d11f49ce38..2a261a2e3a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentPropertyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentPropertyRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ComponentPropertyRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1894747516; /** * Setter for COMPONENT_PROPERTY.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentRecord.java index efaa744751..7043b48983 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ComponentRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1813992192; /** * Setter for COMPONENT.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentsVulnerabilitiesRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentsVulnerabilitiesRecord.java index 8cc6cedc52..174c7519aa 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentsVulnerabilitiesRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ComponentsVulnerabilitiesRecord.java @@ -24,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ComponentsVulnerabilitiesRecord extends TableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1782821111; /** * Setter for COMPONENTS_VULNERABILITIES.COMPONENT_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ConfigPropertyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ConfigPropertyRecord.java index 0d106adab3..1613d9b137 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ConfigPropertyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ConfigPropertyRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ConfigPropertyRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1531833329; /** * Setter for CONFIGPROPERTY.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/DependencyMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/DependencyMetricsRecord.java index bab2687e4c..05ac2d25c7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/DependencyMetricsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/DependencyMetricsRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class DependencyMetricsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -78726026; /** * Setter for DEPENDENCYMETRICS.COMPONENT_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/EpssRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/EpssRecord.java index 5e9f6e18a0..023facaf15 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/EpssRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/EpssRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class EpssRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1881018208; /** * Setter for EPSS.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/FindingAttributionRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/FindingAttributionRecord.java index 82341aac80..ceb7e1d5b9 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/FindingAttributionRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/FindingAttributionRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class FindingAttributionRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1597754647; /** * Setter for FINDINGATTRIBUTION.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityAnalysisRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityAnalysisRecord.java index 28990e9506..89084b52c3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityAnalysisRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityAnalysisRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class IntegrityAnalysisRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1886133780; /** * Setter for INTEGRITY_ANALYSIS.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityMetaComponentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityMetaComponentRecord.java index 4aaf565508..9b181b743c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityMetaComponentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/IntegrityMetaComponentRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class IntegrityMetaComponentRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -2144778894; /** * Setter for INTEGRITY_META_COMPONENT.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupLicenseRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupLicenseRecord.java index ea4472d487..e036f8f951 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupLicenseRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupLicenseRecord.java @@ -24,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class LicenseGroupLicenseRecord extends TableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -449692016; /** * Setter for LICENSEGROUP_LICENSE.LICENSEGROUP_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupRecord.java index 81722d92be..baa58c753f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseGroupRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class LicenseGroupRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -2014332549; /** * Setter for LICENSEGROUP.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseRecord.java index 6934ecf642..83661cb1f8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/LicenseRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class LicenseRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -108552760; /** * Setter for LICENSE.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedLdapGroupRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedLdapGroupRecord.java index 7f5f32c2e0..ed8bf95d18 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedLdapGroupRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedLdapGroupRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class MappedLdapGroupRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1055111209; /** * Setter for MAPPEDLDAPGROUP.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedOidcGroupRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedOidcGroupRecord.java index 31adc62183..7ec4319b08 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedOidcGroupRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/MappedOidcGroupRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class MappedOidcGroupRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -235246095; /** * Setter for MAPPEDOIDCGROUP.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationPublisherRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationPublisherRecord.java index 6e7b384e6f..64e4242d48 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationPublisherRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationPublisherRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationPublisherRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -140065302; /** * Setter for NOTIFICATIONPUBLISHER.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleProjectsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleProjectsRecord.java index bbfaf01594..e1efa0e115 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleProjectsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleProjectsRecord.java @@ -24,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationRuleProjectsRecord extends TableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -458610639; /** * Setter for NOTIFICATIONRULE_PROJECTS.NOTIFICATIONRULE_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleRecord.java index cd327bca15..219d6fad16 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationRuleRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1490850195; /** * Setter for NOTIFICATIONRULE.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTagsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTagsRecord.java index 0ad66cf68b..c2b8f14a05 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTagsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTagsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationRuleTagsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2128918538; /** * Setter for NOTIFICATIONRULE_TAGS.NOTIFICATIONRULE_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTeamsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTeamsRecord.java index 22bce91843..fde1d95331 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTeamsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/NotificationRuleTeamsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class NotificationRuleTeamsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 631633464; /** * Setter for NOTIFICATIONRULE_TEAMS.NOTIFICATIONRULE_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/OidcGroupRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/OidcGroupRecord.java index 493d3e764b..26146fc65f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/OidcGroupRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/OidcGroupRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class OidcGroupRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1380876876; /** * Setter for OIDCGROUP.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PermissionRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PermissionRecord.java index 7d1c08af5c..ba13280d95 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PermissionRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PermissionRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PermissionRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1586304711; /** * Setter for PERMISSION.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyConditionRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyConditionRecord.java index 6fabed5c20..fcbf737aca 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyConditionRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyConditionRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyConditionRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1855718319; /** * Setter for POLICYCONDITION.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyProjectsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyProjectsRecord.java index 02a86f18f2..a864d6911c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyProjectsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyProjectsRecord.java @@ -24,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyProjectsRecord extends TableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1289508177; /** * Setter for POLICY_PROJECTS.POLICY_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyRecord.java index e4e88bd0ad..96d0d3311a 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1255213442; /** * Setter for POLICY.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyTagsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyTagsRecord.java index 644e89fdf0..4db995d17f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyTagsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyTagsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyTagsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -718175564; /** * Setter for POLICY_TAGS.POLICY_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyViolationRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyViolationRecord.java index 6c484f80c6..9fa503fbea 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyViolationRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PolicyViolationRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PolicyViolationRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -2130877628; /** * Setter for POLICYVIOLATION.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java index d81823aa7d..5a4a758dbc 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/PortfolioMetricsRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class PortfolioMetricsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -640386303; /** * Setter for PORTFOLIOMETRICS.COMPONENTS. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectAccessTeamsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectAccessTeamsRecord.java index 87970df92a..fc2c868a54 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectAccessTeamsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectAccessTeamsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectAccessTeamsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1036749248; /** * Setter for PROJECT_ACCESS_TEAMS.PROJECT_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectHierarchyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectHierarchyRecord.java index 090a0da713..400af2fd9e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectHierarchyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectHierarchyRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectHierarchyRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -407800101; /** * Setter for PROJECT_HIERARCHY.PARENT_PROJECT_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetadataRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetadataRecord.java index fe036ee092..638a3418c9 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetadataRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetadataRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectMetadataRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -462682031; /** * Setter for PROJECT_METADATA.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetricsRecord.java index f56ff940ef..bc05e10987 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetricsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectMetricsRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectMetricsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 492620514; /** * Setter for PROJECTMETRICS.COMPONENTS. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectPropertyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectPropertyRecord.java index 9ca3db8a4e..ae353606f7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectPropertyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectPropertyRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectPropertyRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 145578597; /** * Setter for PROJECT_PROPERTY.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectRecord.java index bbe3098fb3..18e45539ac 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectRecord.java @@ -29,7 +29,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1640806964; /** * Setter for PROJECT.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectsTagsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectsTagsRecord.java index 9e7d5b41e9..46ac5702e7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectsTagsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ProjectsTagsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ProjectsTagsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1673410517; /** * Setter for PROJECTS_TAGS.TAG_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryMetaComponentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryMetaComponentRecord.java index c0a6d7f107..65dedc05e2 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryMetaComponentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryMetaComponentRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class RepositoryMetaComponentRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 530816784; /** * Setter for REPOSITORY_META_COMPONENT.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryRecord.java index 91f5176280..df9f7ae905 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RepositoryRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class RepositoryRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 193876256; /** * Setter for REPOSITORY.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RoleRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RoleRecord.java index 83cf180003..6dbc946de9 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RoleRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RoleRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class RoleRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -404691591; /** * Setter for ROLE.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RolesPermissionsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RolesPermissionsRecord.java index f685b5d415..26f33eff91 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RolesPermissionsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/RolesPermissionsRecord.java @@ -24,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class RolesPermissionsRecord extends TableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -268299302; /** * Setter for ROLES_PERMISSIONS.ROLE_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentRecord.java index 2d6288f73c..f1a1bfa12f 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ServiceComponentRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -313015124; /** * Setter for SERVICECOMPONENT.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentsVulnerabilitiesRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentsVulnerabilitiesRecord.java index e16956d546..e80071bfc7 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentsVulnerabilitiesRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ServiceComponentsVulnerabilitiesRecord.java @@ -24,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ServiceComponentsVulnerabilitiesRecord extends TableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1453803659; /** * Setter for diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TagRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TagRecord.java index 5159cb6019..2c80cafc04 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TagRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TagRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class TagRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1486904060; /** * Setter for TAG.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamRecord.java index 6f96e78229..d7ae92976b 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class TeamRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1435240628; /** * Setter for TEAM.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamsPermissionsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamsPermissionsRecord.java index f5c15a4670..a87f56dd76 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamsPermissionsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/TeamsPermissionsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class TeamsPermissionsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1282451951; /** * Setter for TEAMS_PERMISSIONS.TEAM_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserProjectEffectivePermissionsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserProjectEffectivePermissionsRecord.java index 21f697d30e..dfad702be8 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserProjectEffectivePermissionsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserProjectEffectivePermissionsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UserProjectEffectivePermissionsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -934414569; /** * Setter for USER_PROJECT_EFFECTIVE_PERMISSIONS.PROJECT_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserRecord.java index ee7ad41cbb..ffe1e818ef 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UserRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UserRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1987326126; /** * Setter for USER.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersPermissionsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersPermissionsRecord.java index fdc21ad9ad..49b4bcd603 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersPermissionsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersPermissionsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UsersPermissionsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1191546457; /** * Setter for USERS_PERMISSIONS.USER_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersProjectsRolesRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersProjectsRolesRecord.java index be61a662d6..ff32879bcf 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersProjectsRolesRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersProjectsRolesRecord.java @@ -24,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UsersProjectsRolesRecord extends TableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -147063056; /** * Setter for USERS_PROJECTS_ROLES.USER_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersTeamsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersTeamsRecord.java index f23a0154ff..8b04fc466e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersTeamsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/UsersTeamsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class UsersTeamsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -800688593; /** * Setter for USERS_TEAMS.USER_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VexRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VexRecord.java index 92bc30b222..8e72850505 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VexRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VexRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VexRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 656810192; /** * Setter for VEX.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisCommentRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisCommentRecord.java index 60421d4025..9f9a7df49d 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisCommentRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisCommentRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ViolationAnalysisCommentRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 392460294; /** * Setter for VIOLATIONANALYSISCOMMENT.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisRecord.java index 7252aacba7..839867fd5c 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/ViolationAnalysisRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class ViolationAnalysisRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1891849923; /** * Setter for VIOLATIONANALYSIS.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilitiesTagsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilitiesTagsRecord.java index ce5991b490..8942679101 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilitiesTagsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilitiesTagsRecord.java @@ -25,7 +25,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilitiesTagsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 822864282; /** * Setter for VULNERABILITIES_TAGS.TAG_ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityAliasRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityAliasRecord.java index 525eac4a62..6009c692f9 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityAliasRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityAliasRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityAliasRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1497308906; /** * Setter for VULNERABILITYALIAS.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityMetricsRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityMetricsRecord.java index 0ab3e15a9d..6fa5df32a3 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityMetricsRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityMetricsRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityMetricsRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 336038556; /** * Setter for VULNERABILITYMETRICS.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyBundleRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyBundleRecord.java index b215b072f8..5e135259c6 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyBundleRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyBundleRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityPolicyBundleRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1375266263; /** * Setter for VULNERABILITY_POLICY_BUNDLE.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyRecord.java index 780fca9265..1d2efcb45e 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityPolicyRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityPolicyRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1120893212; /** * Setter for VULNERABILITY_POLICY.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityRecord.java index ac8d675a4c..04072978c9 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityRecord.java @@ -30,7 +30,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1739870155; /** * Setter for VULNERABILITY.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityScanRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityScanRecord.java index b8676890d4..a94a8bece1 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityScanRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerabilityScanRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerabilityScanRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 147284591; /** * Setter for VULNERABILITYSCAN.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareRecord.java index 8027d2b077..7f8e29a080 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareRecord.java @@ -27,7 +27,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerableSoftwareRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 638654342; /** * Setter for VULNERABLESOFTWARE.ID. diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareVulnerabilitiesRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareVulnerabilitiesRecord.java index 2a49f5840a..f4ab34c9d1 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareVulnerabilitiesRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/VulnerableSoftwareVulnerabilitiesRecord.java @@ -24,7 +24,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class VulnerableSoftwareVulnerabilitiesRecord extends TableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = -1792385758; /** * Setter for diff --git a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/WorkflowStateRecord.java b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/WorkflowStateRecord.java index aa7abda325..8974817812 100644 --- a/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/WorkflowStateRecord.java +++ b/persistence-jooq/src/main/java/org/dependencytrack/persistence/jooq/generated/tables/records/WorkflowStateRecord.java @@ -28,7 +28,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes", "this-escape" }) public class WorkflowStateRecord extends UpdatableRecordImpl { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1804473797; /** * Setter for WORKFLOW_STATE.ID. From 4e85b1e6e4ff1b5da5ecb862aeebbcb2340cc585 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:57:15 -0500 Subject: [PATCH 162/181] refactor: create default team for GitLab users (#6) Signed-off-by: Jonathan Howard --- .../persistence/AlpineQueryManager.java | 19 ++++- .../GitLabAuthenticationCustomizer.java | 14 ++-- .../integrations/gitlab/GitLabClient.java | 5 +- .../gitlab/GitLabIntegrationStateChanger.java | 80 ++++++++++++++++++- .../model/ConfigPropertyConstants.java | 1 + 5 files changed, 103 insertions(+), 16 deletions(-) diff --git a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java index eee19833be..18ed97e9b1 100644 --- a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java +++ b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java @@ -562,7 +562,7 @@ public Team createTeam(final String name, final boolean createApiKey) { } /** - * Creates a new Team with the specified name. + * Creates a new {@link Team} with the specified name. * @param name The name of the team * @return a Team * @since 3.2.0 @@ -571,7 +571,22 @@ public Team createTeam(final String name) { return callInTransaction(() -> { final var team = new Team(); team.setName(name); - //todo assign permissions + pm.makePersistent(team); + return team; + }); + } + + /** + * Creates a new {@link Team} with the specified name and initial {@link Permission}s. + * @param name The name of the team + * @return a Team + * @since 5.6.0 + */ + public Team createTeam(final String name, final List permissions) { + return callInTransaction(() -> { + final var team = new Team(); + team.setName(name); + team.setPermissions(permissions); pm.makePersistent(team); return team; }); diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index 1d86dc706d..be67e48508 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -23,17 +23,16 @@ import alpine.server.auth.DefaultOidcAuthenticationCustomizer; import alpine.server.auth.OidcProfile; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Objects; import org.dependencytrack.event.GitLabSyncEvent; import org.dependencytrack.persistence.QueryManager; public class GitLabAuthenticationCustomizer extends DefaultOidcAuthenticationCustomizer { - public GitLabAuthenticationCustomizer() { - - } + public GitLabAuthenticationCustomizer() {} @Override public boolean isProfileComplete(OidcProfile profile, boolean teamSyncEnabled) { @@ -43,12 +42,11 @@ public boolean isProfileComplete(OidcProfile profile, boolean teamSyncEnabled) { @Override public OidcUser onAuthenticationSuccess(OidcUser user, OidcProfile profile, String idToken, String accessToken) { try (QueryManager qm = new QueryManager()) { - List groups = profile.getGroups(); - groups = groups != null ? groups : new ArrayList(); + List groups = Objects.requireNonNullElse(profile.getGroups(), Collections.emptyList()); groups.stream() - .filter(groupName -> groupName == null) - .map(groupName -> qm.createOidcGroup(groupName)); + .filter(Objects::nonNull) + .forEach(qm::createOidcGroup); } Event.dispatch(new GitLabSyncEvent(accessToken, user)); diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index ec9edf05b5..b1ccb8ec85 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; +import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; @@ -45,8 +46,6 @@ import net.minidev.json.JSONObject; import net.minidev.json.JSONValue; -import static org.apache.commons.io.IOUtils.resourceToString; - public class GitLabClient { private static final Logger LOGGER = Logger.getLogger(GitLabClient.class); @@ -147,7 +146,7 @@ public List getGitLabProjects() throws IOException, URISyntaxExce variables.put("archived", "INCLUDE"); } - queryObject.put("query", resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8)); + queryObject.put("query", IOUtils.resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8)); URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT); diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java index 7c596140f9..db5dc378b0 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.integrations.gitlab; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_API_KEY; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; import java.util.Collections; @@ -26,17 +27,25 @@ import java.util.Map; import java.util.Objects; +import javax.jdo.Query; + +import org.dependencytrack.auth.Permissions; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.model.Role; import org.dependencytrack.persistence.QueryManager; import alpine.common.logging.Logger; +import alpine.model.ApiKey; +import alpine.model.ConfigProperty; import alpine.model.Permission; +import alpine.model.Team; +import alpine.security.crypto.DataEncryption; public class GitLabIntegrationStateChanger extends AbstractIntegrationPoint { private static final Logger LOGGER = Logger.getLogger(GitLabIntegrationStateChanger.class); private static final String INTEGRATIONS_GROUP = GITLAB_ENABLED.getGroupName(); + private static final String DEFAULT_TEAM = "GitLab Users"; private final Map PERMISSIONS_MAP = new HashMap<>(); public GitLabIntegrationStateChanger() { @@ -57,24 +66,51 @@ public void setState(boolean isEnabled) { if (isEnabled) { LOGGER.info("Enabling GitLab integration"); createGitLabRoles(); + createGitLabDefaultTeam(); return; } LOGGER.info("Disabling GitLab integration"); removeGitLabRoles(); + removeGitLabDefaultTeam(); } catch (RuntimeException ex) { LOGGER.error("An error occurred while changing GitLab Integration State", ex); handleException(LOGGER, ex); } } + private void createGitLabDefaultTeam() { + try { + if (qm.getTeam(DEFAULT_TEAM) != null) { + LOGGER.info("GitLab Users team already exists"); + return; + } + + final Team team = qm.createTeam(DEFAULT_TEAM, List.of( + qm.getPermission(Permissions.Constants.BOM_UPLOAD), + qm.getPermission(Permissions.Constants.VIEW_PORTFOLIO))); + + LOGGER.info("Created GitLab default user team"); + + final ApiKey apiKey = qm.createApiKey(team); + final ConfigProperty property = qm.getConfigProperty( + GITLAB_API_KEY.getGroupName(), + GITLAB_API_KEY.getPropertyName()); + + property.setPropertyValue(DataEncryption.encryptAsString(apiKey.getKey())); + qm.persist(property); + } catch (Exception ex) { + LOGGER.error("An error occurred while creating GitLab default user team", ex); + throw new RuntimeException("Failed to create default team for GitLab users", ex); + } + } + private void createGitLabRoles() { - if (PERMISSIONS_MAP.isEmpty()) { + if (PERMISSIONS_MAP.isEmpty()) populatePermissionsMap(qm); - } - for (GitLabRole role : GitLabRole.values()) { + for (GitLabRole role : GitLabRole.values()) try { if (qm.getRoleByName(role.getDescription()) == null) { qm.createRole(role.getDescription(), qm.getPermissionsByName(role.getPermissions())); @@ -86,7 +122,45 @@ private void createGitLabRoles() { LOGGER.error("An error occurred while creating GitLab roles", ex); throw new RuntimeException("Failed to create GitLab roles", ex); } + } + + private void removeGitLabDefaultTeam() { + try (final QueryManager qm = new QueryManager()) { + final Team team = qm.getTeam(DEFAULT_TEAM); + + if (team == null) { + LOGGER.info("GitLab default team does not exist"); + return; + } + + qm.delete(team); + LOGGER.info("Removed default GitLab team"); + + final ConfigProperty property = qm.getConfigProperty( + GITLAB_API_KEY.getGroupName(), + GITLAB_API_KEY.getPropertyName()); + + final Query query = qm.getPersistenceManager().newQuery(ApiKey.class) + .filter("key == :key") + .setParameters(DataEncryption.decryptAsString(property.getPropertyValue())); + + final ApiKey apiKey; + try{ + apiKey = query.executeUnique(); + } finally { + query.closeAll(); + } + + if (apiKey != null) + qm.delete(apiKey); + + property.setPropertyValue(null); + qm.persist(property); + } catch (Exception ex) { + LOGGER.error("An error occurred while removing GitLab roles", ex); + throw new RuntimeException("Failed to remove GitLab roles", ex); } + } private void removeGitLabRoles() { diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 5e4504c343..0fd7b5a279 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -94,6 +94,7 @@ public enum ConfigPropertyConstants { GITLAB_SBOM_PUSH_ENABLED("integrations", "sbom.push.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_TOPICS("integrations", "gitlab.topics", "[]", PropertyType.STRING, "JSON array of topics to include when syncing GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_API_KEY("integrations", "gitlab.api.key", null, PropertyType.ENCRYPTEDSTRING, "API Key for GitLab Users team", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_ENABLED("integrations", "defectdojo.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo integration", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_REIMPORT_ENABLED("integrations", "defectdojo.reimport.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo reimport-scan API endpoint", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_SYNC_CADENCE("integrations", "defectdojo.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to DefectDojo", ConfigPropertyAccessMode.READ_WRITE), From eb9351db6fea7dd69f9b64d5efd360d3935ba8b6 Mon Sep 17 00:00:00 2001 From: jmayer-lm Date: Thu, 5 Jun 2025 17:42:47 -0400 Subject: [PATCH 163/181] Add GitLab Key Constant (#5) * fix: rename variable Signed-off-by: johnny mayer * chore: resolve conflict Signed-off-by: johnny mayer --------- Signed-off-by: johnny mayer --- .../java/org/dependencytrack/model/ConfigPropertyConstants.java | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 0fd7b5a279..eec55dc786 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -95,6 +95,7 @@ public enum ConfigPropertyConstants { GITLAB_TOPICS("integrations", "gitlab.topics", "[]", PropertyType.STRING, "JSON array of topics to include when syncing GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), GITLAB_API_KEY("integrations", "gitlab.api.key", null, PropertyType.ENCRYPTEDSTRING, "API Key for GitLab Users team", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_JWKS_PATH("integrations", "gitlab.jwks.path", "/oauth/discovery/keys", PropertyType.STRING, "The URI path to the GitLab instance's JWKS endpoint", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_ENABLED("integrations", "defectdojo.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo integration", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_REIMPORT_ENABLED("integrations", "defectdojo.reimport.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable DefectDojo reimport-scan API endpoint", ConfigPropertyAccessMode.READ_WRITE), DEFECTDOJO_SYNC_CADENCE("integrations", "defectdojo.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to DefectDojo", ConfigPropertyAccessMode.READ_WRITE), From 94d3545392638ca356546ba0e99d40cab268bc52 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Thu, 5 Jun 2025 15:43:41 -0600 Subject: [PATCH 164/181] tests: add additional gitlab integration unit tests, codacy cleanup (#7) Signed-off-by: Allen Shearin --- .../GitLabAuthenticationCustomizer.java | 2 - .../integrations/gitlab/GitLabClient.java | 9 +- .../gitlab/GitLabIntegrationStateChanger.java | 30 ++--- .../model/ConfigPropertyConstants.java | 2 +- .../event/GitLabSyncEventTest.java | 98 +++++++++++++++++ .../integrations/PermissionsSyncerTest.java | 60 ++++++++++ .../GitLabAuthenticationCustomizerTest.java | 104 ++++++++++++++++++ .../integrations/gitlab/GitLabClientTest.java | 91 +++++++++++++-- .../GitLabIntegrationStateChangerTest.java | 55 ++++++++- .../integrations/gitlab/GitLabRoleTest.java | 83 ++++++++++++++ .../integrations/gitlab/GitLabSyncerTest.java | 32 +++--- ...api-getgitlabprojects-response-page-1.json | 4 +- ...api-getgitlabprojects-response-page-2.json | 6 +- ...api-getgitlabprojects-topics-response.json | 19 ++++ 14 files changed, 533 insertions(+), 62 deletions(-) create mode 100644 apiserver/src/test/java/org/dependencytrack/event/GitLabSyncEventTest.java create mode 100644 apiserver/src/test/java/org/dependencytrack/integrations/PermissionsSyncerTest.java create mode 100644 apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizerTest.java create mode 100644 apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabRoleTest.java create mode 100644 apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-topics-response.json diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index be67e48508..fbb52fbf69 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -32,8 +32,6 @@ public class GitLabAuthenticationCustomizer extends DefaultOidcAuthenticationCustomizer { - public GitLabAuthenticationCustomizer() {} - @Override public boolean isProfileComplete(OidcProfile profile, boolean teamSyncEnabled) { return super.isProfileComplete(profile, true); diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index b1ccb8ec85..4dfe029fe5 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -135,17 +135,13 @@ public List getGitLabProjects() throws IOException, URISyntaxExce // Set the default values for the GraphQL query variables.put("includeTopics", false); - variables.put("archived", "EXCLUDE"); + variables.put("archived", includeArchived ? "INCLUDE" : "EXCLUDE"); if (topics != null && !topics.isEmpty()) { variables.put("includeTopics", true); variables.put("topics", topics); } - if (includeArchived) { - variables.put("archived", "INCLUDE"); - } - queryObject.put("query", IOUtils.resourceToString("/graphql/gitlab-projects.graphql", StandardCharsets.UTF_8)); URIBuilder builder = new URIBuilder(baseURL.toString()).setPath(GRAPHQL_ENDPOINT); @@ -204,4 +200,7 @@ public ArrayList jsonToList(final JSONArray jsonArray) { return list; } + public Config getConfig() { + return config; + } } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java index db5dc378b0..2399518458 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -27,8 +27,6 @@ import java.util.Map; import java.util.Objects; -import javax.jdo.Query; - import org.dependencytrack.auth.Permissions; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.model.Role; @@ -48,9 +46,6 @@ public class GitLabIntegrationStateChanger extends AbstractIntegrationPoint { private static final String DEFAULT_TEAM = "GitLab Users"; private final Map PERMISSIONS_MAP = new HashMap<>(); - public GitLabIntegrationStateChanger() { - } - @Override public String name() { return "GitLab Integration State Changer"; @@ -120,7 +115,6 @@ private void createGitLabRoles() { } } catch (Exception ex) { LOGGER.error("An error occurred while creating GitLab roles", ex); - throw new RuntimeException("Failed to create GitLab roles", ex); } } @@ -133,6 +127,12 @@ private void removeGitLabDefaultTeam() { return; } + for (ApiKey key : team.getApiKeys()) + if (key != null) { + qm.delete(key); + LOGGER.info("Removed API key for GitLab team"); + } + qm.delete(team); LOGGER.info("Removed default GitLab team"); @@ -140,25 +140,11 @@ private void removeGitLabDefaultTeam() { GITLAB_API_KEY.getGroupName(), GITLAB_API_KEY.getPropertyName()); - final Query query = qm.getPersistenceManager().newQuery(ApiKey.class) - .filter("key == :key") - .setParameters(DataEncryption.decryptAsString(property.getPropertyValue())); - - final ApiKey apiKey; - try{ - apiKey = query.executeUnique(); - } finally { - query.closeAll(); - } - - if (apiKey != null) - qm.delete(apiKey); - property.setPropertyValue(null); qm.persist(property); } catch (Exception ex) { - LOGGER.error("An error occurred while removing GitLab roles", ex); - throw new RuntimeException("Failed to remove GitLab roles", ex); + LOGGER.error("An error occurred while removing GitLab team", ex); + throw new RuntimeException("Failed to remove GitLab team", ex); } } diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index eec55dc786..64ac676863 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -91,7 +91,7 @@ public enum ConfigPropertyConstants { GITLAB_AUTOCREATE_PROJECTS("integrations", "gitlab.autocreate.projects", "false", PropertyType.BOOLEAN, "Flag to enable/disable auto-creation of projects in GitLab", ConfigPropertyAccessMode.READ_WRITE), GITLAB_ENABLED("integrations", "gitlab.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitLab integration", ConfigPropertyAccessMode.READ_WRITE), GITLAB_INCLUDE_ARCHIVED("integrations", "gitlab.include.archived", "false", PropertyType.BOOLEAN, "Flag to enable/disable syncing of archived GitLab projects", ConfigPropertyAccessMode.READ_WRITE), - GITLAB_SBOM_PUSH_ENABLED("integrations", "sbom.push.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_SBOM_PUSH_ENABLED("integrations", "sbom.push.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_TOPICS("integrations", "gitlab.topics", "[]", PropertyType.STRING, "JSON array of topics to include when syncing GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), GITLAB_API_KEY("integrations", "gitlab.api.key", null, PropertyType.ENCRYPTEDSTRING, "API Key for GitLab Users team", ConfigPropertyAccessMode.READ_WRITE), diff --git a/apiserver/src/test/java/org/dependencytrack/event/GitLabSyncEventTest.java b/apiserver/src/test/java/org/dependencytrack/event/GitLabSyncEventTest.java new file mode 100644 index 0000000000..d4a3c8f005 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/event/GitLabSyncEventTest.java @@ -0,0 +1,98 @@ +/* + * 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.event; + +import alpine.model.OidcUser; +import org.junit.Assert; +import org.junit.Test; + +public class GitLabSyncEventTest { + + @Test + public void testDefaultConstructor() { + // Arrange and Act + GitLabSyncEvent event = new GitLabSyncEvent(); + + // Assert + Assert.assertNull(event.getAccessToken()); + Assert.assertNull(event.getUser()); + } + + @Test + public void testParameterizedConstructor() { + // Arrange + String accessToken = "test-access-token"; + OidcUser user = new OidcUser(); + + // Act + GitLabSyncEvent event = new GitLabSyncEvent(accessToken, user); + + // Assert + Assert.assertEquals(accessToken, event.getAccessToken()); + Assert.assertEquals(user, event.getUser()); + } + + @Test + public void testSettersAndGetters() { + // Arrange + GitLabSyncEvent event = new GitLabSyncEvent(); + String accessToken = "test-access-token"; + OidcUser user = new OidcUser(); + + // Act + event.setAccessToken(accessToken); + event.setUser(user); + + // Assert + Assert.assertEquals(accessToken, event.getAccessToken()); + Assert.assertEquals(user, event.getUser()); + } + + @Test + public void testToString() { + // Arrange + String accessToken = "test-access-token"; + OidcUser user = new OidcUser(); + GitLabSyncEvent event = new GitLabSyncEvent(accessToken, user); + + // Act + String toString = event.toString(); + + // Assert + Assert.assertNotNull(toString); + Assert.assertTrue(toString.contains("GitLabSyncEvent")); + Assert.assertTrue(toString.contains(accessToken)); + Assert.assertTrue(toString.contains(user.toString())); + } + + @Test + public void testToStringWithNullValues() { + // Arrange + GitLabSyncEvent event = new GitLabSyncEvent(); + + // Act + String toString = event.toString(); + + // Assert + Assert.assertNotNull(toString); + Assert.assertTrue(toString.contains("GitLabSyncEvent")); + Assert.assertTrue(toString.contains("null")); + } +} diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/PermissionsSyncerTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/PermissionsSyncerTest.java new file mode 100644 index 0000000000..fd153614f6 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/integrations/PermissionsSyncerTest.java @@ -0,0 +1,60 @@ +/* + * 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.integrations; + +import org.junit.Assert; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.dependencytrack.persistence.QueryManager; + +public class PermissionsSyncerTest { + + @Test + public final void testIsEnabled_True() { + PermissionsSyncer permissionsSyncer = mock(PermissionsSyncer.class); + when(permissionsSyncer.isEnabled()).thenReturn(true); + Assert.assertTrue(permissionsSyncer.isEnabled()); + } + + @Test + public final void testIsEnabled_False() { + PermissionsSyncer permissionsSyncer = mock(PermissionsSyncer.class); + when(permissionsSyncer.isEnabled()).thenReturn(false); + Assert.assertFalse(permissionsSyncer.isEnabled()); + } + + @Test + public final void testSetQueryManager() { + PermissionsSyncer permissionsSyncer = mock(PermissionsSyncer.class); + QueryManager queryManager = mock(QueryManager.class); + permissionsSyncer.setQueryManager(queryManager); + verify(permissionsSyncer).setQueryManager(queryManager); + } + + @Test + public final void testSynchronize() { + PermissionsSyncer permissionsSyncer = mock(PermissionsSyncer.class); + permissionsSyncer.synchronize(); + verify(permissionsSyncer).synchronize(); + } +} diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizerTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizerTest.java new file mode 100644 index 0000000000..00cc867ffd --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizerTest.java @@ -0,0 +1,104 @@ +/* + * 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.integrations.gitlab; + +import alpine.event.framework.Event; +import alpine.model.OidcUser; +import alpine.server.auth.OidcProfile; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class GitLabAuthenticationCustomizerTest { + + @Test + public void testIsProfileComplete() { + GitLabAuthenticationCustomizer customizer = new GitLabAuthenticationCustomizer(); + OidcProfile profile = mock(OidcProfile.class); + boolean teamSyncEnabled = true; + + boolean result = customizer.isProfileComplete(profile, teamSyncEnabled); + + Assert.assertTrue(result); + } + + @Test + public void testOnAuthenticationSuccess() { + GitLabAuthenticationCustomizer customizer = new GitLabAuthenticationCustomizer(); + OidcUser user = mock(OidcUser.class); + OidcProfile profile = mock(OidcProfile.class); + String idToken = "idToken"; + String accessToken = "accessToken"; + + when(profile.getGroups()).thenReturn(new ArrayList<>()); + + OidcUser result = customizer.onAuthenticationSuccess(user, profile, idToken, accessToken); + + Assert.assertEquals(user, result); + + // Verify that the GitLabSyncEvent was dispatched + verify(Event.class, Mockito.times(1)); + } + + @Test + public void testOnAuthenticationSuccess_Groups() { + GitLabAuthenticationCustomizer customizer = new GitLabAuthenticationCustomizer(); + OidcUser user = mock(OidcUser.class); + OidcProfile profile = mock(OidcProfile.class); + String idToken = "idToken"; + String accessToken = "accessToken"; + List groups = new ArrayList<>(); + groups.add("group1"); + groups.add("group2"); + + when(profile.getGroups()).thenReturn(groups); + + OidcUser result = customizer.onAuthenticationSuccess(user, profile, idToken, accessToken); + + Assert.assertEquals(user, result); + + // Verify that the GitLabSyncEvent was dispatched + verify(Event.class, Mockito.times(1)); + } + + @Test + public void testOnAuthenticationSuccess_NullGroups() { + GitLabAuthenticationCustomizer customizer = new GitLabAuthenticationCustomizer(); + OidcUser user = mock(OidcUser.class); + OidcProfile profile = mock(OidcProfile.class); + String idToken = "idToken"; + String accessToken = "accessToken"; + + when(profile.getGroups()).thenReturn(null); + + OidcUser result = customizer.onAuthenticationSuccess(user, profile, idToken, accessToken); + + Assert.assertEquals(user, result); + + // Verify that the GitLabSyncEvent was dispatched + verify(Event.class, Mockito.times(1)); + } +} diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index fe33c97f1a..07f9921086 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -19,7 +19,15 @@ package org.dependencytrack.integrations.gitlab; import alpine.Config; -import com.github.tomakehurst.wiremock.client.WireMock; +import jakarta.ws.rs.core.MediaType; +import net.minidev.json.JSONArray; + +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + import com.github.tomakehurst.wiremock.junit.WireMockRule; import com.github.tomakehurst.wiremock.stubbing.Scenario; @@ -35,6 +43,7 @@ import java.util.List; import org.apache.http.HttpHeaders; +import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaProducerInitializer; import org.junit.AfterClass; import org.junit.Assert; @@ -46,6 +55,9 @@ public class GitLabClientTest { + @Rule + public WireMockRule wireMockRule = new WireMockRule(); + @BeforeClass public static void beforeClass() { Config.enableUnitTests(); @@ -56,8 +68,21 @@ public static void after() { KafkaProducerInitializer.tearDown(); } - @Rule - public WireMockRule wireMockRule = new WireMockRule(); + @Test + public void testConstructorWithAccessToken() { + String accessToken = "my-access-token"; + GitLabClient client = new GitLabClient(accessToken); + Assert.assertNotNull(client); + } + + @Test + public void testConstructorWithAccessTokenAndConfig() { + String accessToken = "my-access-token"; + Config config = Config.getInstance(); + GitLabClient client = new GitLabClient(accessToken, config, null, false); + Assert.assertNotNull(client); + Assert.assertEquals("Dependency-Track", client.getConfig().getApplicationName()); + } @Test public void testGetGitLabProjects() throws URISyntaxException, IOException { @@ -68,17 +93,17 @@ public void testGetGitLabProjects() throws URISyntaxException, IOException { String page2Result = resourceToString("/unit/gitlab-api-getgitlabprojects-response-page-2.json", StandardCharsets.UTF_8); - WireMock.stubFor(WireMock.post("/api/graphql") + stubFor(post(urlPathEqualTo("/api/graphql")) .inScenario("test-get-gitlab-projects") .whenScenarioStateIs(Scenario.STARTED) - .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .willReturn(ok().withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .withBody(page1Result)) .willSetStateTo("second-page")); - WireMock.stubFor(WireMock.post("/api/graphql") + stubFor(post(urlPathEqualTo("/api/graphql")) .inScenario("test-get-gitlab-projects") .whenScenarioStateIs("second-page") - .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .willReturn(ok().withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .withBody(page2Result)) .willSetStateTo("Finished")); @@ -90,6 +115,9 @@ public void testGetGitLabProjects() throws URISyntaxException, IOException { List gitLabProjects = gitLabClient.getGitLabProjects(); + Assert.assertNotNull(gitLabProjects); + Assert.assertEquals(4, gitLabProjects.size()); + List actualProjectPaths = new ArrayList<>(); for (var project : gitLabProjects) actualProjectPaths.add(project.getFullPath()); @@ -103,4 +131,53 @@ public void testGetGitLabProjects() throws URISyntaxException, IOException { Assert.assertEquals(actualProjectPaths, expectedProjectPaths); } + @Test + public void testGetGitLabProjectsWithTopics() throws IOException, URISyntaxException { + String accessToken = "TEST_ACCESS_TOKEN"; + + String result = resourceToString("/unit/gitlab-api-getgitlabprojects-topics-response.json", + StandardCharsets.UTF_8); + + stubFor(post(urlPathEqualTo("/api/graphql")) + .willReturn(ok().withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .withBody(result))); + + final var configMock = mock(Config.class); + + when(configMock.getProperty(eq(Config.AlpineKey.OIDC_ISSUER))).thenReturn(wireMockRule.baseUrl()); + List topics = Arrays.asList("topic1"); + + GitLabClient gitLabClient = new GitLabClient(accessToken, configMock, topics, false); + + List gitLabProjects = gitLabClient.getGitLabProjects(); + + Assert.assertNotNull(gitLabProjects); + Assert.assertEquals(1, gitLabProjects.size()); + + Assert.assertEquals("project/with/topic", gitLabProjects.get(0).getFullPath()); + } + + @Test + public void testGetRolePermissions() { + String accessToken = "my-access-token"; + GitLabClient client = new GitLabClient(accessToken); + List permissions = client.getRolePermissions(GitLabRole.DEVELOPER); + Assert.assertNotNull(permissions); + Assert.assertEquals(6, permissions.size()); // assume some permissions are returned + } + + @Test + public void testJsonToList() { + String accessToken = "my-access-token"; + GitLabClient client = new GitLabClient(accessToken); + JSONArray jsonArray = new JSONArray(); + jsonArray.add("item1"); + jsonArray.add("item2"); + List list = client.jsonToList(jsonArray); + Assert.assertNotNull(list); + Assert.assertEquals(2, list.size()); // assume 2 items are returned + Assert.assertEquals("item1", list.get(0)); + Assert.assertEquals("item2", list.get(1)); + } + } diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java index 906b433d00..ecb1c89487 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java @@ -22,6 +22,7 @@ import alpine.model.Permission; import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Role; import org.junit.Assert; @@ -32,6 +33,7 @@ import java.util.List; import java.util.Map; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_API_KEY; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -60,6 +62,9 @@ public void testIntegrationStateChangerMetadata() { */ @Test public void testEnable() { + qm.createPermission(Permissions.Constants.BOM_UPLOAD, "upload BOMs"); + qm.createPermission(Permissions.Constants.VIEW_PORTFOLIO, "view portfolio"); + stateChanger.setQueryManager(qm); qm.createConfigProperty( GITLAB_ENABLED.getGroupName(), @@ -67,12 +72,22 @@ public void testEnable() { "true", IConfigProperty.PropertyType.BOOLEAN, null); + + qm.createConfigProperty( + GITLAB_API_KEY.getGroupName(), + GITLAB_API_KEY.getPropertyName(), + null, + GITLAB_API_KEY.getPropertyType(), + null); + stateChanger.setState(true); roles = qm.getRoles(); Assert.assertEquals(roles.size(), GitLabRole.values().length); for (GitLabRole role : GitLabRole.values()) { Assert.assertNotNull(qm.getRoleByName(role.getDescription())); } + Assert.assertEquals(qm.getTeams().size(), 1); + Assert.assertEquals(qm.getTeams().get(0).getName(), "GitLab Users"); } /** @@ -80,6 +95,9 @@ public void testEnable() { */ @Test public void testDisable() { + qm.createPermission(Permissions.Constants.BOM_UPLOAD, "upload BOMs"); + qm.createPermission(Permissions.Constants.VIEW_PORTFOLIO, "view portfolio"); + stateChanger.setQueryManager(qm); qm.createConfigProperty( GITLAB_ENABLED.getGroupName(), @@ -87,20 +105,29 @@ public void testDisable() { "false", IConfigProperty.PropertyType.BOOLEAN, null); + qm.createConfigProperty( + GITLAB_API_KEY.getGroupName(), + GITLAB_API_KEY.getPropertyName(), + "test_api_key", + GITLAB_API_KEY.getPropertyType(), + null); - // Create roles to be removed + // Create roles and team to be removed stateChanger.setState(true); roles = qm.getRoles(); Assert.assertEquals(roles.size(), GitLabRole.values().length); for (GitLabRole role : GitLabRole.values()) { Assert.assertNotNull(qm.getRoleByName(role.getDescription())); } + Assert.assertEquals(qm.getTeams().size(), 1); + Assert.assertEquals(qm.getTeams().get(0).getName(), "GitLab Users"); // Disable the integration - // and verify that the roles are removed + // and verify that the roles and team are removed stateChanger.setState(false); roles = qm.getRoles(); Assert.assertEquals(roles.size(), 0); + Assert.assertEquals(qm.getTeams().size(), 0); } @Test @@ -124,4 +151,28 @@ public void testPopulatePermissionsMap() { Assert.assertTrue(permissionsMap.containsValue(permission)); } + @Test + public void testGetPermissionsMap() { + stateChanger.setQueryManager(qm); + + // Test method call + Map permissionsMap = stateChanger.getPermissionsMap(); + + // Verify expected state + Assert.assertEquals(0, permissionsMap.size()); + } + + @Test + public void testGetPermissionsMapPopulated() { + qm.createPermission("testPermission", "Test Permission"); + + stateChanger.setQueryManager(qm); + + // Test method call + stateChanger.getPermissionsMap(); + + // Verify expected state + Assert.assertEquals(1, stateChanger.getPermissionsMap().size()); + Assert.assertTrue(stateChanger.getPermissionsMap().containsKey("testPermission")); + } } diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabRoleTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabRoleTest.java new file mode 100644 index 0000000000..ba54132500 --- /dev/null +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabRoleTest.java @@ -0,0 +1,83 @@ +/* + * 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.integrations.gitlab; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Set; + +public class GitLabRoleTest { + + @Test + public void testGetAccessLevel() { + Assert.assertEquals(10, GitLabRole.GUEST.getAccessLevel()); + Assert.assertEquals(15, GitLabRole.PLANNER.getAccessLevel()); + Assert.assertEquals(20, GitLabRole.REPORTER.getAccessLevel()); + Assert.assertEquals(30, GitLabRole.DEVELOPER.getAccessLevel()); + Assert.assertEquals(40, GitLabRole.MAINTAINER.getAccessLevel()); + Assert.assertEquals(50, GitLabRole.OWNER.getAccessLevel()); + } + + @Test + public void testGetDescription() { + Assert.assertEquals("GitLab Project Guest", GitLabRole.GUEST.getDescription()); + Assert.assertEquals("GitLab Project Planner", GitLabRole.PLANNER.getDescription()); + Assert.assertEquals("GitLab Project Reporter", GitLabRole.REPORTER.getDescription()); + Assert.assertEquals("GitLab Project Developer", GitLabRole.DEVELOPER.getDescription()); + Assert.assertEquals("GitLab Project Maintainer", GitLabRole.MAINTAINER.getDescription()); + Assert.assertEquals("GitLab Project Owner", GitLabRole.OWNER.getDescription()); + } + + @Test + public void testGetPermissionsForGuest() { + Set permissions = GitLabRole.GUEST.getPermissions(); + Assert.assertEquals(3, permissions.size()); + } + + @Test + public void testGetPermissionsForPlanner() { + Set permissions = GitLabRole.PLANNER.getPermissions(); + Assert.assertEquals(4, permissions.size()); + } + + @Test + public void testGetPermissionsForReporter() { + Set permissions = GitLabRole.REPORTER.getPermissions(); + Assert.assertEquals(4, permissions.size()); + } + + @Test + public void testGetPermissionsForDeveloper() { + Set permissions = GitLabRole.DEVELOPER.getPermissions(); + Assert.assertEquals(8, permissions.size()); + } + + @Test + public void testGetPermissionsForMaintainer() { + Set permissions = GitLabRole.MAINTAINER.getPermissions(); + Assert.assertEquals(20, permissions.size()); + } + + @Test + public void testGetPermissionsForOwner() { + Set permissions = GitLabRole.OWNER.getPermissions(); + Assert.assertEquals(32, permissions.size()); + } +} diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java index a0fae1842a..7f8c443064 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabSyncerTest.java @@ -21,12 +21,11 @@ import org.junit.Assert; import alpine.model.IConfigProperty; import alpine.model.OidcUser; -import alpine.model.Permission; -import alpine.model.Team; import java.net.URISyntaxException; import java.io.IOException; import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.model.Project; +import org.dependencytrack.model.UserProjectRole; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -106,6 +105,13 @@ public void testIsDisabled() { */ @Test public void testSynchronizeSuccess() { + qm.createRole("GitLab Project Guest", new ArrayList<>()); + qm.createRole("GitLab Project Maintainer", new ArrayList<>()); + qm.createRole("GitLab Project Reporter", new ArrayList<>()); + qm.createRole("GitLab Project Developer", new ArrayList<>()); + qm.createRole("GitLab Project Planner", new ArrayList<>()); + qm.createRole("GitLab Project Owner", new ArrayList<>()); + qm.createConfigProperty( GITLAB_ENABLED.getGroupName(), GITLAB_ENABLED.getPropertyName(), @@ -133,21 +139,11 @@ public void testSynchronizeSuccess() { Project testProject2 = qm.getProject("that/test/project2", null); Assert.assertFalse(testProject2.isActive()); - List testTeams = new ArrayList(); - Team team1 = qm.getTeam("this/test/project1_MAINTAINER"); - testTeams.add(team1); - List t1perm = team1.getPermissions(); - Assert.assertEquals(t1perm.size(), mockClient.getRolePermissions(GitLabRole.MAINTAINER).size()); - - Team team2 = qm.getTeam("that/test/project2_REPORTER"); - testTeams.add(team2); - List t2perm = team2.getPermissions(); - Assert.assertEquals(t2perm.size(), mockClient.getRolePermissions(GitLabRole.REPORTER).size()); - - for (Team team : testTeams) { - List testTeamUsers = team.getOidcUsers(); - Assert.assertEquals(testTeamUsers.size(), 1); - Assert.assertEquals(testTeamUsers.get(0).getUsername(), "test_user"); - } + List testRoles = qm.getUserRoles("test_user"); + Assert.assertEquals(2, testRoles.size()); + Assert.assertEquals("this/test/project1", testRoles.get(0).getProject().getName()); + Assert.assertEquals("GitLab Project Maintainer", testRoles.get(0).getRole().getName()); + Assert.assertEquals("that/test/project2", testRoles.get(1).getProject().getName()); + Assert.assertEquals("GitLab Project Reporter", testRoles.get(1).getRole().getName()); } } diff --git a/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-1.json b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-1.json index 8d4008e744..0665e36d9d 100644 --- a/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-1.json +++ b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-1.json @@ -1,6 +1,6 @@ { "data": { - "projects": { + "withoutTopics": { "nodes": [ { "name": "test-project-1", @@ -25,7 +25,7 @@ } ], "pageInfo": { - "endCursor": "eyJpZCI6IjE1ODU5NyJ9", + "endCursor": "eyJpZCI6Ijk3ODU2In0", "hasNextPage": true } } diff --git a/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-2.json b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-2.json index 6ef7d5a45a..53ceb1ba08 100644 --- a/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-2.json +++ b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-response-page-2.json @@ -1,14 +1,14 @@ { "data": { - "projects": { + "withoutTopics": { "nodes": [ { "name": "test-project-4", "fullPath": "test-group/test-subgroup-2/test-project-4", "maxAccessLevel": { "stringValue": "DEVELOPER" - } - }, + } + } ], "pageInfo": { "endCursor": "zzzzzzzzzzzzzzzzzzzz", diff --git a/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-topics-response.json b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-topics-response.json new file mode 100644 index 0000000000..c23aa9c50a --- /dev/null +++ b/apiserver/src/test/resources/unit/gitlab-api-getgitlabprojects-topics-response.json @@ -0,0 +1,19 @@ +{ + "data": { + "withTopics": { + "nodes": [ + { + "fullPath": "project/with/topic", + "maxAccessLevel": { + "stringValue": "MAINTAINER" + } + } + ], + "pageInfo": { + "endCursor": "eyJpZCI6IjgyMTY0In0", + "hasNextPage": false + } + } + }, + "correlationId": "01JX0Y94A7FMG49C7BWT9GERP7" +} From c544c111c95cdeb54b0f1ee982743c6c20c9dad4 Mon Sep 17 00:00:00 2001 From: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Date: Mon, 9 Jun 2025 10:52:35 -0500 Subject: [PATCH 165/181] refactor: enable portfolio access control on integration enable (#8) Signed-off-by: Jonathan Howard --- .../gitlab/GitLabIntegrationStateChanger.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java index 2399518458..1e1ec57d95 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -18,9 +18,6 @@ */ package org.dependencytrack.integrations.gitlab; -import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_API_KEY; -import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; - import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -29,6 +26,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.integrations.AbstractIntegrationPoint; +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Role; import org.dependencytrack.persistence.QueryManager; @@ -42,7 +40,6 @@ public class GitLabIntegrationStateChanger extends AbstractIntegrationPoint { private static final Logger LOGGER = Logger.getLogger(GitLabIntegrationStateChanger.class); - private static final String INTEGRATIONS_GROUP = GITLAB_ENABLED.getGroupName(); private static final String DEFAULT_TEAM = "GitLab Users"; private final Map PERMISSIONS_MAP = new HashMap<>(); @@ -60,6 +57,7 @@ public void setState(boolean isEnabled) { try { if (isEnabled) { LOGGER.info("Enabling GitLab integration"); + setConfigProperty(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED, "true"); createGitLabRoles(); createGitLabDefaultTeam(); @@ -89,12 +87,8 @@ private void createGitLabDefaultTeam() { LOGGER.info("Created GitLab default user team"); final ApiKey apiKey = qm.createApiKey(team); - final ConfigProperty property = qm.getConfigProperty( - GITLAB_API_KEY.getGroupName(), - GITLAB_API_KEY.getPropertyName()); - property.setPropertyValue(DataEncryption.encryptAsString(apiKey.getKey())); - qm.persist(property); + setConfigProperty(ConfigPropertyConstants.GITLAB_API_KEY, DataEncryption.encryptAsString(apiKey.getKey())); } catch (Exception ex) { LOGGER.error("An error occurred while creating GitLab default user team", ex); throw new RuntimeException("Failed to create default team for GitLab users", ex); @@ -136,12 +130,7 @@ private void removeGitLabDefaultTeam() { qm.delete(team); LOGGER.info("Removed default GitLab team"); - final ConfigProperty property = qm.getConfigProperty( - GITLAB_API_KEY.getGroupName(), - GITLAB_API_KEY.getPropertyName()); - - property.setPropertyValue(null); - qm.persist(property); + setConfigProperty(ConfigPropertyConstants.GITLAB_API_KEY, null); } catch (Exception ex) { LOGGER.error("An error occurred while removing GitLab team", ex); throw new RuntimeException("Failed to remove GitLab team", ex); @@ -179,6 +168,13 @@ private void populatePermissionsMap(QueryManager qm) { } } + private void setConfigProperty(ConfigPropertyConstants cpc, String value) { + final ConfigProperty property = qm.getConfigProperty(cpc.getGroupName(), cpc.getPropertyName()); + property.setPropertyValue(value); + + qm.persist(property); + } + public Map getPermissionsMap() { if (PERMISSIONS_MAP.isEmpty()) { populatePermissionsMap(qm); From 0fa5c8a6bdc67100dba5741636d9da345c920c7c Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 9 Jun 2025 13:15:10 -0600 Subject: [PATCH 166/181] Update GitLabClientTest.java (#10) Signed-off-by: Allen Shearin --- .../dependencytrack/integrations/gitlab/GitLabClientTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index 07f9921086..38db6e2a37 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -26,7 +26,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import com.github.tomakehurst.wiremock.junit.WireMockRule; import com.github.tomakehurst.wiremock.stubbing.Scenario; From 96845713ff2e2467e1eaf311a75709288b926c20 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Thu, 12 Jun 2025 16:34:17 -0500 Subject: [PATCH 167/181] fix: OIDC group unique constraint violation error Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitLabAuthenticationCustomizer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index fbb52fbf69..8902ab2926 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -39,11 +39,12 @@ public boolean isProfileComplete(OidcProfile profile, boolean teamSyncEnabled) { @Override public OidcUser onAuthenticationSuccess(OidcUser user, OidcProfile profile, String idToken, String accessToken) { - try (QueryManager qm = new QueryManager()) { - List groups = Objects.requireNonNullElse(profile.getGroups(), Collections.emptyList()); + try (final QueryManager qm = new QueryManager()) { + final List groups = Objects.requireNonNullElse(profile.getGroups(), Collections.emptyList()); groups.stream() .filter(Objects::nonNull) + .filter(group -> qm.getOidcGroup(group) == null) .forEach(qm::createOidcGroup); } From 8704dd8f0e02aa25abfdbb9ef651b457f0842444 Mon Sep 17 00:00:00 2001 From: emeremikwu-lm Date: Wed, 18 Jun 2025 10:49:30 -0500 Subject: [PATCH 168/181] Roles bulk (#9) * feat: add endpoint to set role permissions in bulk with validation Signed-off-by: Emmanuel Meremikwu * feat: add endpoint to retrieve users with optional filtering by type and username Signed-off-by: Emmanuel Meremikwu * refactor: optimize user retrieval logic and enhance permission handling Signed-off-by: Emmanuel Meremikwu * refactor: remove unused WireMockConfiguration import from GitLabClientTest causing checkstyle violation Signed-off-by: Emmanuel Meremikwu * refactor: rename variable 'principal' to 'user' for clarity in PermissionResource and UserResource Signed-off-by: Emmanuel Meremikwu * feat: add UserType enum and update user retrieval logic to support user type differentiation Signed-off-by: Emmanuel Meremikwu * test: add unit test for retrieving users by type in UserResourceAuthenticatedTest Signed-off-by: Emmanuel Meremikwu * refactor: cleanup Signed-off-by: Emmanuel Meremikwu * Update apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java Co-authored-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Signed-off-by: Emmanuel Meremikwu * PR Revisions Co-authored-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> Signed-off-by: Emmanuel Meremikwu * refactor: remove UserType enum and related deserialization logic, update user retrieval to use string type Signed-off-by: Emmanuel Meremikwu --------- Signed-off-by: Emmanuel Meremikwu Co-authored-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com> --- .../persistence/AlpineQueryManager.java | 27 ++++ .../resources/v1/PermissionResource.java | 134 ++++++++++++------ .../resources/v1/UserResource.java | 113 ++++++++++++--- .../v1/vo/RolePermissionsSetRequest.java | 41 ++++++ .../resources/v1/vo/TeamsSetRequest.java | 3 + .../v1/vo/UserPermissionsSetRequest.java | 3 + .../resources/v1/PermissionResourceTest.java | 78 ++++++++++ .../v1/UserResourceAuthenticatedTest.java | 39 +++++ 8 files changed, 376 insertions(+), 62 deletions(-) create mode 100644 apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RolePermissionsSetRequest.java diff --git a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java index 18ed97e9b1..b5c2c7a6de 100644 --- a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java +++ b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java @@ -344,6 +344,17 @@ public OidcUser addUserToTeams(final OidcUser user, final List teamNames }); } + /** + * Returns a complete list of all subclasses extending User.class, in ascending order by username. + * @return a list of all Users + * @since 1.0.0 + */ + public List getAllUsers() { + final Query query = pm.newQuery(User.class); + query.setOrdering("username asc"); + return executeAndCloseList(query); + } + /** * Retrieves an LdapUser containing the specified username. If the username * does not exist, returns null. @@ -547,6 +558,22 @@ public User getUser(String username) { return executeAndCloseUnique(query); } + /** + * Resolves a type of User. + * @param cls the class of the principal to retrieve + * @param username the username of the principal to retrieve + * @return a User if found, null if not found + * @since 1.0.0 + */ + public T getUser(String username, Class cls) { + final Query query = pm.newQuery(cls) + .filter("username == :username") + .setNamedParameters(Map.of("username", username)) + .extension(JDOQLQuery.EXTENSION_CANDIDATE_DONT_RESTRICT_DISCRIMINATOR, true); + + return (T) executeAndCloseUnique(query); + } + /** * Creates a new Team with the specified name. If createApiKey is true, * then {@link #createApiKey} is invoked and a cryptographically secure diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index e2d9cde396..caf62a7643 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -19,6 +19,9 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.Team; import alpine.model.User; @@ -33,10 +36,13 @@ 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 org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Role; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.resources.v1.vo.RolePermissionsSetRequest; import org.dependencytrack.resources.v1.vo.TeamPermissionsSetRequest; import org.dependencytrack.resources.v1.vo.UserPermissionsSetRequest; import org.owasp.security.logging.SecurityMarkers; @@ -50,11 +56,10 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import javax.jdo.Query; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -120,21 +125,21 @@ public Response addPermissionToUser( @Parameter(description = "A valid permission", required = true) @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { - User principal = qm.getUser(username); - if (principal == null) { + User user = qm.getUser(username); + if (user == null) { return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); } final Permission permission = qm.getPermission(permissionName); if (permission == null) { return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); } - final List permissions = principal.getPermissions(); + final List permissions = user.getPermissions(); if (permissions != null && !permissions.contains(permission)) { permissions.add(permission); - principal.setPermissions(permissions); - principal = qm.persist(principal); - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added permission for user: " + principal.getName() + " / permission: " + permission.getName()); - return Response.ok(principal).build(); + user.setPermissions(permissions); + user = qm.persist(user); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added permission for user: " + user.getName() + " / permission: " + permission.getName()); + return Response.ok(user).build(); } return Response.status(Response.Status.NOT_MODIFIED).build(); } @@ -163,24 +168,36 @@ public Response removePermissionFromUser( @Parameter(description = "A valid username", required = true) @PathParam("username") String username, @Parameter(description = "A valid permission", required = true) + @QueryParam("userType") String type, @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { - User principal = qm.getUser(username); - if (principal == null) { + User user = qm.getUser(username, (Class) switch (StringUtils.defaultString(type).toLowerCase()) { + case "managed" -> ManagedUser.class; + case "ldap" -> LdapUser.class; + case "oidc" -> OidcUser.class; + default -> User.class; + }); + + if (user == null) { return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); } + final Permission permission = qm.getPermission(permissionName); if (permission == null) { return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); } - final List permissions = principal.getPermissions(); + + final List permissions = user.getPermissions(); if (permissions != null && permissions.contains(permission)) { permissions.remove(permission); - principal.setPermissions(permissions); - principal = qm.persist(principal); - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed permission for user: " + principal.getName() + " / permission: " + permission.getName()); - return Response.ok(principal).build(); + user.setPermissions(permissions); + user = qm.persist(user); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Removed permission for user: " + user.getUsername() + " / permission: " + + permission.getName()); + return Response.ok(user).build(); } + return Response.status(Response.Status.NOT_MODIFIED).build(); } } @@ -213,10 +230,12 @@ public Response addPermissionToTeam( if (team == null) { return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); } + final Permission permission = qm.getPermission(permissionName); if (permission == null) { return Response.status(Response.Status.NOT_FOUND).entity("The permission could not be found.").build(); } + final List permissions = team.getPermissions(); if (permissions != null && !permissions.contains(permission)) { permissions.add(permission); @@ -225,6 +244,7 @@ public Response addPermissionToTeam( super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added permission for team: " + team.getName() + " / permission: " + permission.getName()); return Response.ok(team).build(); } + return Response.status(Response.Status.NOT_MODIFIED).build(); } } @@ -366,28 +386,24 @@ public Response removePermissionFromTeam( }) @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE }) public Response setUserPermissions( - @Parameter(description = "A username and valid list permission") @Valid UserPermissionsSetRequest request) { + @Parameter(description = "A username and valid list permission") @Valid final UserPermissionsSetRequest request) { try (QueryManager qm = new QueryManager()) { - User user = qm.getUser(request.username()); + User user = qm.getUser(request.username(), (Class) switch (StringUtils.defaultString(request.userType()).toLowerCase()) { + case "managed" -> ManagedUser.class; + case "ldap" -> LdapUser.class; + case "oidc" -> OidcUser.class; + default -> User.class; + }); + if (user == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); - List permissionNames = request.permissions() + final List permissionNames = request.permissions() .stream() .map(Permissions::name) .toList(); - final Query query = qm.getPersistenceManager().newQuery(Permission.class) - .filter(":permissions.contains(name)") - .setNamedParameters(Map.of("permissions", permissionNames)) - .orderBy("name asc"); - - final List requestedPermissions; - try { - requestedPermissions = List.copyOf(query.executeList()); - } finally { - query.closeAll(); - } + final List requestedPermissions = qm.getPermissionsByName(permissionNames); if (user.getPermissions().equals(requestedPermissions)) return Response.notModified() @@ -420,28 +436,18 @@ public Response setUserPermissions( @ApiResponse(responseCode = "404", description = "The team could not be found") }) @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE }) - public Response setTeamPermissions(@Parameter(description = "Team UUID and requested permissions") @Valid TeamPermissionsSetRequest request) { + public Response setTeamPermissions(@Parameter(description = "Team UUID and requested permissions") @Valid final TeamPermissionsSetRequest request) { try (QueryManager qm = new QueryManager()) { Team team = qm.getObjectByUuid(Team.class, request.team(), Team.FetchGroup.ALL.name()); if (team == null) return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); - List permissionNames = request.permissions() + final List permissionNames = request.permissions() .stream() .map(Permissions::name) .toList(); - final Query query = qm.getPersistenceManager().newQuery(Permission.class) - .filter(":permissions.contains(name)") - .setNamedParameters(Map.of("permissions", permissionNames)) - .orderBy("name asc"); - - final List requestedPermissions; - try { - requestedPermissions = List.copyOf(query.executeList()); - } finally { - query.closeAll(); - } + final List requestedPermissions = qm.getPermissionsByName(permissionNames); if (team.getPermissions().equals(requestedPermissions)) return Response.notModified().entity("Team already has selected permission(s).").build(); @@ -456,4 +462,46 @@ public Response setTeamPermissions(@Parameter(description = "Team UUID and reque } } + @PUT + @Path("/role") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Replaces a role's permissions with the specified list", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_UPDATE

        " + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "The updated role", content = @Content(schema = @Schema(implementation = Role.class))), + @ApiResponse(responseCode = "304", description = "The role already has the specified permission(s)"), + @ApiResponse(responseCode = "400", description = "Bad request"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The role could not be found") + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE }) + public Response setRolePermissions(@Parameter(description = "Role UUID and requested permissions") @Valid final RolePermissionsSetRequest request) { + try (QueryManager qm = new QueryManager()) { + Role role = qm.getObjectByUuid(Role.class, request.role(), Role.FetchGroup.ALL.name()); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + + final List permissionNames = request.permissions() + .stream() + .map(Permissions::name) + .toList(); + + final Set requestedPermissions = Set.copyOf(qm.getPermissionsByName(permissionNames)); + + if (role.getPermissions().equals(requestedPermissions)) + return Response.notModified().entity("Role already has selected permission(s).").build(); + + role.setPermissions(requestedPermissions); + role = qm.persist(role); + + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Set permissions for role: %s / permissions: %s" + .formatted(role.getName(), permissionNames)); + return Response.ok(role).build(); + } + } + } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index e83171cc7d..41c886cbb1 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -76,6 +76,7 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import javax.jdo.Query; @@ -256,6 +257,73 @@ public Response forceChangePassword(@FormParam("username") String username, @For } } + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Returns a list of users or a single user, optionally filtered by type and/or username.", + description = "

        Requires permission ACCESS_MANAGEMENT or ACCESS_MANAGEMENT_READ

        " + + "

        Note: To retrieve additional subclass-specific information (such as ManagedUser fields like suspended, forcePasswordChange, etc.), " + + "you must specify the type query parameter (e.g., type=managed, type=ldap, or type=oidc). " + + "If type is omitted, only base User fields will be included in the response.

        " + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "A list of users or a single user (may be filtered by type and/or username)", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of managed users", schema = @Schema(format = "integer")), + content = @Content( + schema = @Schema( + oneOf = {User.class, ManagedUser.class, LdapUser.class, OidcUser.class} + ), + array = @ArraySchema( + schema = @Schema(oneOf = {User.class, ManagedUser.class, LdapUser.class, OidcUser.class}) + ) + ) + ), + @ApiResponse(responseCode = "401", description = "Unauthorized") + }) + @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_READ }) + public Response getUsers( + @QueryParam("userType") String type, + @QueryParam("username") String username) { + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + final List users; + if (type == null) { + users = qm.getAllUsers(); + return Response.ok(users).header(TOTAL_COUNT_HEADER, users.size()).build(); + } + + Query query = qm.getPersistenceManager() + .newQuery((Class) switch (StringUtils.defaultString(type).toLowerCase()) { + case "managed" -> ManagedUser.class; + case "ldap" -> LdapUser.class; + case "oidc" -> OidcUser.class; + default -> User.class; + }); + + if (username != null) + query.filter("username == :username").setParameters(username); + + try { + users = List.copyOf(query.executeList()); + } finally { + query.closeAll(); + } + + if (users == null) + return Response.status(Response.Status.NOT_FOUND) + .entity("No user(s) found for the given criteria [type=%s, username=%s]" + .formatted(type, username)) + .build(); + + if (username != null && users.size() == 1) + return Response.ok(users.get(0)).build(); + + return Response.ok(users).header(TOTAL_COUNT_HEADER, users.size()).build(); + + } + } + @GET @Path("managed") @Produces(MediaType.APPLICATION_JSON) @@ -706,15 +774,15 @@ public Response addTeamToUser( if (team == null) { return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); } - User principal = qm.getUser(username); - if (principal == null) { + User user = qm.getUser(username); + if (user == null) { return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); } - final boolean modified = qm.addUserToTeam(principal, team); - principal = qm.getObjectById(principal.getClass(), principal.getId()); + final boolean modified = qm.addUserToTeam(user, team); + user = qm.getObjectById(user.getClass(), user.getId()); if (modified) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added team membership for: " + principal.getName() + " / team: " + team.getName()); - return Response.ok(principal).build(); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Added team membership for: %s / team: %s".formatted(user.getUsername(), team.getName())); + return Response.ok(user).build(); } else { return Response.status(Response.Status.NOT_MODIFIED).entity("The user is already a member of the specified team.").build(); } @@ -750,15 +818,15 @@ public Response removeTeamFromUser( if (team == null) { return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build(); } - User principal = qm.getUser(username); - if (principal == null) { + User user = qm.getUser(username); + if (user == null) { return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); } - final boolean modified = qm.removeUserFromTeam(principal, team); - principal = qm.getObjectById(principal.getClass(), principal.getId()); + final boolean modified = qm.removeUserFromTeam(user, team); + user = qm.getObjectById(user.getClass(), user.getId()); if (modified) { - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed team membership for: " + principal.getName() + " / team: " + team.getName()); - return Response.ok(principal).build(); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Removed team membership for: " + user.getName() + " / team: " + team.getName()); + return Response.ok(user).build(); } else { return Response.status(Response.Status.NOT_MODIFIED) .entity("The user was not a member of the specified team.") @@ -783,8 +851,15 @@ public Response removeTeamFromUser( public Response setUserTeams( @Parameter(description = "Username and list of UUIDs to assign to user", required = true) @Valid TeamsSetRequest request) { try (QueryManager qm = new QueryManager()) { - User principal = qm.getUser(request.username()); - if (principal == null) { + User user = qm.getUser(request.username(), + (Class) switch (StringUtils.defaultString(request.userType()).toLowerCase()) { + case "managed" -> ManagedUser.class; + case "ldap" -> LdapUser.class; + case "oidc" -> OidcUser.class; + default -> User.class; + }); + + if (user == null) { return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); } @@ -813,16 +888,16 @@ public Response setUserTeams( return problem.toResponse(); } - final List currentUserTeams = Objects.requireNonNullElse(principal.getTeams(), List.of()); + final List currentUserTeams = Objects.requireNonNullElse(user.getTeams(), List.of()); if (currentUserTeams.equals(requestedTeams)) { return Response.notModified().entity("The user is already a member of the selected team(s)").build(); } - principal.setTeams(requestedTeams); - qm.persist(principal); + user.setTeams(requestedTeams); + user = qm.persist(user); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, - "Added team membership for: " + principal.getName() + " / team: " + requestedTeams.toString()); - return Response.ok(principal).build(); + "Added team membership for: " + user.getName() + " / team: " + requestedTeams.toString()); + return Response.ok(user).build(); } } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RolePermissionsSetRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RolePermissionsSetRequest.java new file mode 100644 index 0000000000..cd1b215797 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/RolePermissionsSetRequest.java @@ -0,0 +1,41 @@ +/* + * 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.resources.v1.vo; + +import java.util.Set; + +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.validation.ValidUuid; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record RolePermissionsSetRequest( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank + @ValidUuid + String role, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull + Set permissions) { +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/TeamsSetRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/TeamsSetRequest.java index aefc8f1209..33474be159 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/TeamsSetRequest.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/TeamsSetRequest.java @@ -35,6 +35,9 @@ import jakarta.validation.constraints.Pattern; public record TeamsSetRequest( + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String userType, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank @JsonDeserialize(using = TrimmedStringDeserializer.class) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/UserPermissionsSetRequest.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/UserPermissionsSetRequest.java index 4cac17ee55..0d09cdb025 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/UserPermissionsSetRequest.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/vo/UserPermissionsSetRequest.java @@ -35,6 +35,9 @@ import jakarta.validation.constraints.Pattern; public record UserPermissionsSetRequest( + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String userType, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank @JsonDeserialize(using = TrimmedStringDeserializer.class) 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 adcafe3621..1ef2cf95db 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java @@ -563,4 +563,82 @@ public void removePermissionFromRoleNoChangesTest() { Assert.assertEquals(304, response.getStatus(), 0); Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); } + + @Test + public void setRolePermissionsTest() { + // Arrange: create a role and permissions + Role role = qm.createRole("testRole", Collections.emptyList()); + String roleUuid = role.getUuid().toString(); + + List permissionSet1 = List.of( + qm.getPermission("ACCESS_MANAGEMENT"), + qm.getPermission("ACCESS_MANAGEMENT_CREATE"), + qm.getPermission("ACCESS_MANAGEMENT_DELETE")); + + List permissionSet2 = List.of( + qm.getPermission("BOM_UPLOAD"), + qm.getPermission("VIEW_PORTFOLIO"), + qm.getPermission("PORTFOLIO_MANAGEMENT")); + + JsonObject request1 = Json.createObjectBuilder() + .add("role", roleUuid) + .add("permissions", Json.createArrayBuilder(permissionSet1.stream().map(Permission::getName).toList())) + .build(); + + JsonObject request2 = Json.createObjectBuilder() + .add("role", roleUuid) + .add("permissions", Json.createArrayBuilder(permissionSet2.stream().map(Permission::getName).toList())) + .build(); + + String endpoint = V1_PERMISSION + "/role"; + + // Act & Assert: assign first set + Response response = jersey.target(endpoint) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request1.toString(), MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus()); + Role updatedRole = qm.getObjectByUuid(Role.class, role.getUuid()); + Assert.assertTrue(updatedRole.getPermissions().containsAll(permissionSet1)); + + // Assign second set (replace) + response = jersey.target(endpoint) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request2.toString(), MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus()); + updatedRole = qm.getObjectByUuid(Role.class, role.getUuid()); + Assert.assertTrue(updatedRole.getPermissions().containsAll(permissionSet2)); + Assert.assertTrue(updatedRole.getPermissions().stream().noneMatch(p -> permissionSet1.contains(p))); + + // Assign same set again (should return 304) + response = jersey.target(endpoint) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request2.toString(), MediaType.APPLICATION_JSON)); + Assert.assertEquals(304, response.getStatus()); + + // Invalid role UUID + JsonObject invalidRoleRequest = Json.createObjectBuilder() + .add("role", UUID.randomUUID().toString()) + .add("permissions", Json.createArrayBuilder(permissionSet1.stream().map(Permission::getName).toList())) + .build(); + response = jersey.target(endpoint) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(invalidRoleRequest.toString(), MediaType.APPLICATION_JSON)); + Assert.assertEquals(404, response.getStatus()); + + // Invalid permissions + JsonObject invalidPermRequest = Json.createObjectBuilder() + .add("role", roleUuid) + .add("permissions", Json.createArrayBuilder().add("INVALID_PERMISSION")) + .build(); + response = jersey.target(endpoint) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(invalidPermRequest.toString(), MediaType.APPLICATION_JSON)); + Assert.assertEquals(400, response.getStatus()); + } + } diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java index de48f7a22c..aa0467f02d 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/UserResourceAuthenticatedTest.java @@ -47,6 +47,7 @@ import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; + import java.time.Duration; import java.util.Collections; import java.util.List; @@ -80,6 +81,44 @@ public void before() throws Exception { qm.addUserToTeam(testUser, team); } + @Test + public void getUsersTest() { + qm.createLdapUser("testldapuser"); + qm.createOidcUser("testoidcuser"); + Response responseAll = jersey.target(V1_USER) + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + + Response responseLdap = jersey.target(V1_USER) + .queryParam("type", "ldap") + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + + Response responseManaged = jersey.target(V1_USER) + .queryParam("type", "managed") + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + + Response responseOidc = jersey.target(V1_USER) + .queryParam("type", "oidc") + .request() + .header(X_API_KEY, apiKey) + .get(Response.class); + // add response values to a list called allResonses + List statuses = List.of( + responseAll.getStatus(), responseLdap.getStatus(), + responseManaged.getStatus(), responseOidc.getStatus() + ); + List expectedStatuses = List.of(200, 200, 200, 200); + Assert.assertEquals(expectedStatuses, statuses); + + JsonArray users = parseJsonArray(responseAll); + Assert.assertTrue(users.toArray().length >= 3); + } + @Test public void getManagedUsersTest() { for (int i=0; i<1000; i++) { From 148911ca5567d98f066b6c2adff057adadd66331 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Wed, 18 Jun 2025 12:03:04 -0600 Subject: [PATCH 169/181] fix: add error checking into graphql query (#11) Signed-off-by: Allen Shearin --- .../integrations/gitlab/GitLabClient.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index 4dfe029fe5..bf4f525b8e 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -157,6 +157,12 @@ public List getGitLabProjects() throws IOException, URISyntaxExce request.setEntity(entity); try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode < 200 || statusCode >= 300) { + LOGGER.warn("GitLab GraphQL query failed with status code: " + statusCode); + break; + } + HttpEntity responseEntity = response.getEntity(); if (responseEntity == null) @@ -164,6 +170,13 @@ public List getGitLabProjects() throws IOException, URISyntaxExce String responseBody = EntityUtils.toString(responseEntity); JSONObject responseData = JSONValue.parse(responseBody, JSONObject.class); + + // Check for GraphQL errors + if (responseData.containsKey("errors")) { + LOGGER.warn("GitLab GraphQL query returned errors: " + responseData.get("errors")); + break; + } + JSONObject dataObject = (JSONObject) responseData.getOrDefault("data", new JSONObject()); JSONObject projectsObject = (JSONObject) dataObject.getOrDefault("withoutTopics", dataObject.getOrDefault("withTopics", new JSONObject())); @@ -174,7 +187,7 @@ public List getGitLabProjects() throws IOException, URISyntaxExce projects.add(GitLabProject.parse(node.toJSONString())); } - JSONObject pageInfo = (JSONObject) projectsObject.get("pageInfo"); + JSONObject pageInfo = (JSONObject) projectsObject.getOrDefault("pageInfo", new JSONObject()); if (!(boolean) pageInfo.get("hasNextPage")) break; From 3ecc5353843e72cf51a77c93e042160ada2e223e Mon Sep 17 00:00:00 2001 From: emeremikwu-lm Date: Mon, 7 Jul 2025 10:52:44 -0500 Subject: [PATCH 170/181] fix: duplicate user error on sso (#13) * fix: duplicate user error on sso Signed-off-by: Emmanuel Meremikwu * Update apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java Co-authored-by: Allen Shearin Signed-off-by: emeremikwu-lm --------- Signed-off-by: Emmanuel Meremikwu Signed-off-by: emeremikwu-lm Co-authored-by: Allen Shearin --- .../org/dependencytrack/tasks/GitLabSyncTask.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java index 09dbadb043..46a361d323 100644 --- a/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java +++ b/apiserver/src/main/java/org/dependencytrack/tasks/GitLabSyncTask.java @@ -69,15 +69,15 @@ public void inform(final Event event) { return; } - OidcUser user = gitLabSyncEvent.getUser(); - if (user == null) { - LOGGER.warn("GitLab syncing is enabled, but no authenticated user was provided. Skipping."); - return; - } - LOGGER.info("Starting GitLab sync task"); try (QueryManager qm = new QueryManager()) { + final OidcUser user = qm.getUser(gitLabSyncEvent.getUser().getUsername(), OidcUser.class); + if (user == null) { + LOGGER.warn("GitLab syncing is enabled, but no authenticated user was provided. Skipping."); + return; + } + String topicsProperty = qm.getConfigProperty( GITLAB_TOPICS.getGroupName(), GITLAB_TOPICS.getPropertyName()).getPropertyValue(); List topics = List.of(JSONValue.parse(topicsProperty, JSONArray.class).toArray(String[]::new)); From f60656d2adcf970b70934e43c1ae9634b9f50f6f Mon Sep 17 00:00:00 2001 From: Alexis Lamb Date: Wed, 9 Jul 2025 11:50:58 -0400 Subject: [PATCH 171/181] feat: add functionality to upload a sbom from GitLab (#12) * feat: uploadBomGitLab validates a GitLab ID Token with a public JWKS and then uploads a bom file Signed-off-by: Alexis Lamb * update uploadBomGitLab and GitLabAuthenticationCustomizer Signed-off-by: Alexis Lamb * fix: remove build error Signed-off-by: Alexis Lamb * fix: remove unused imports Signed-off-by: Alexis Lamb * update autocreate flag usage and project creation logic Signed-off-by: Alexis Lamb * fix PR comments for GitLab SBOM Push Signed-off-by: Alexis Lamb * update updateNewProjectACL to add GitLabRole to user Signed-off-by: Alexis Lamb * update gitLabToken parameter name Signed-off-by: Alexis Lamb --------- Signed-off-by: Alexis Lamb --- .../GitLabAuthenticationCustomizer.java | 30 ++- .../integrations/gitlab/GitLabClient.java | 61 ++++++ .../gitlab/GitLabIntegrationStateChanger.java | 1 + .../model/ConfigPropertyConstants.java | 2 +- .../persistence/ProjectQueryManager.java | 34 ++-- .../persistence/QueryManager.java | 4 +- .../resources/v1/BomResource.java | 178 +++++++++++++++--- 7 files changed, 272 insertions(+), 38 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java index 8902ab2926..f1d7c85f7f 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabAuthenticationCustomizer.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.integrations.gitlab; +import alpine.Config; import alpine.event.framework.Event; import alpine.model.OidcUser; import alpine.server.auth.DefaultOidcAuthenticationCustomizer; @@ -30,11 +31,36 @@ import org.dependencytrack.event.GitLabSyncEvent; import org.dependencytrack.persistence.QueryManager; +import com.nimbusds.openid.connect.sdk.claims.ClaimsSet; +import com.nimbusds.openid.connect.sdk.claims.UserInfo; +import net.minidev.json.JSONObject; + public class GitLabAuthenticationCustomizer extends DefaultOidcAuthenticationCustomizer { @Override - public boolean isProfileComplete(OidcProfile profile, boolean teamSyncEnabled) { - return super.isProfileComplete(profile, true); + public OidcProfile createProfile(ClaimsSet claimsSet) { + final String teamsClaimName = Config.getInstance().getProperty(Config.AlpineKey.OIDC_TEAMS_CLAIM); + String usernameClaimName = Config.getInstance().getProperty(Config.AlpineKey.OIDC_USERNAME_CLAIM); + final var profile = new OidcProfile(); + + if (claimsSet.getStringClaim("user_login") != null) + usernameClaimName = "user_login"; + + profile.setSubject(Objects.requireNonNullElse(claimsSet.getStringClaim("user_id"), + claimsSet.getStringClaim(UserInfo.SUB_CLAIM_NAME))); + profile.setUsername(claimsSet.getStringClaim(usernameClaimName)); + profile.setEmail(Objects.requireNonNullElse(claimsSet.getStringClaim("user_email"), + claimsSet.getStringClaim(UserInfo.EMAIL_CLAIM_NAME))); + + JSONObject claimsObj = claimsSet.toJSONObject(); + claimsObj.remove(UserInfo.EMAIL_CLAIM_NAME); + claimsObj.remove(UserInfo.SUB_CLAIM_NAME); + claimsObj.remove(teamsClaimName); + claimsObj.remove(usernameClaimName); + + profile.setCustomValues(claimsObj); + + return profile; } @Override diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index bf4f525b8e..d4731a7117 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -20,18 +20,25 @@ package org.dependencytrack.integrations.gitlab; import java.io.IOException; +import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.RSAPublicKeySpec; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; @@ -58,6 +65,11 @@ public class GitLabClient { private final List topics; private final boolean includeArchived; + public static final String PROJECT_PATH_CLAIM = "project_path"; + public static final String REF_PATH_CLAIM = "ref_path"; + public static final String REF_TYPE_CLAIM = "ref_type"; + public static final String USER_ACCESS_LEVEL_CLAIM = "user_access_level"; + private final Map> rolePermissions = Map.of( GitLabRole.GUEST, List.of( Permissions.VIEW_PORTFOLIO, @@ -199,6 +211,55 @@ public List getGitLabProjects() throws IOException, URISyntaxExce return projects; } + private static JSONObject getJwks(String jwksUrl) throws IOException, InterruptedException, URISyntaxException { + URIBuilder builder = new URIBuilder(jwksUrl); + HttpGet request = new HttpGet(builder.build()); + request.setHeader("Accept", "application/json"); + + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + String jsonResponse = EntityUtils.toString(response.getEntity()); + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) + throw new IOException("Failed to fetch JWKS from URL: %s. Status code: %d".formatted(jwksUrl, response.getStatusLine().getStatusCode())); + + if (!jsonResponse.trim().startsWith("{")) + throw new IOException("Unexpected response: " + response.getEntity()); + + return JSONValue.parse(jsonResponse, JSONObject.class); + } + } + + public static PublicKey getPublicKeyFromJwks(String baseUrl, String jwksPath, String kid) throws Exception { + String gitLabJwksUrl = baseUrl + jwksPath; + Object keysObject = getJwks(gitLabJwksUrl).getOrDefault("keys", new JSONArray()); + if (!(keysObject instanceof List)) + throw new IllegalArgumentException("Invalid JWKS format: 'keys' is not a list"); + + @SuppressWarnings("unchecked") + List> keys = (List>) keysObject; + for (Map keyMap : keys) { + JSONObject jsonKey = new JSONObject(); + jsonKey.put("kty", keyMap.get("kty")); + jsonKey.put("alg", keyMap.get("alg")); + jsonKey.put("use", keyMap.get("use")); + jsonKey.put("kid", keyMap.get("kid")); + jsonKey.put("n", keyMap.get("n")); + jsonKey.put("e", keyMap.get("e")); + + if (jsonKey.get("kid").equals(kid)) { + if (!jsonKey.containsKey("n") || !jsonKey.containsKey("e")) + throw new IllegalArgumentException("Missing modulus 'n' or exponent 'e' in JWKS key: " + jsonKey); + + RSAPublicKeySpec spec = new RSAPublicKeySpec( + new BigInteger(1, Base64.getUrlDecoder().decode(jsonKey.get("n").toString())), + new BigInteger(1, Base64.getUrlDecoder().decode(jsonKey.get("e").toString())) + ); + + return KeyFactory.getInstance("RSA").generatePublic(spec); + } + } + throw new IllegalArgumentException("Public key not found for kid: " + kid); + } + public List getRolePermissions(final GitLabRole role) { return rolePermissions.get(role); } diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java index 1e1ec57d95..949541d7a1 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChanger.java @@ -67,6 +67,7 @@ public void setState(boolean isEnabled) { LOGGER.info("Disabling GitLab integration"); removeGitLabRoles(); removeGitLabDefaultTeam(); + setConfigProperty(ConfigPropertyConstants.GITLAB_SBOM_PUSH_ENABLED, "false"); } catch (RuntimeException ex) { LOGGER.error("An error occurred while changing GitLab Integration State", ex); handleException(LOGGER, ex); diff --git a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 64ac676863..8a719bb73b 100644 --- a/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -91,7 +91,7 @@ public enum ConfigPropertyConstants { GITLAB_AUTOCREATE_PROJECTS("integrations", "gitlab.autocreate.projects", "false", PropertyType.BOOLEAN, "Flag to enable/disable auto-creation of projects in GitLab", ConfigPropertyAccessMode.READ_WRITE), GITLAB_ENABLED("integrations", "gitlab.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitLab integration", ConfigPropertyAccessMode.READ_WRITE), GITLAB_INCLUDE_ARCHIVED("integrations", "gitlab.include.archived", "false", PropertyType.BOOLEAN, "Flag to enable/disable syncing of archived GitLab projects", ConfigPropertyAccessMode.READ_WRITE), - GITLAB_SBOM_PUSH_ENABLED("integrations", "sbom.push.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), + GITLAB_SBOM_PUSH_ENABLED("integrations", "gitlab.sbom.push.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable SBOM generation for GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_TOPICS("integrations", "gitlab.topics", "[]", PropertyType.STRING, "JSON array of topics to include when syncing GitLab projects", ConfigPropertyAccessMode.READ_WRITE), GITLAB_URL("integrations", "gitlab.url", "https://gitlab.com", PropertyType.URL, "Base URL to GitLab instance", ConfigPropertyAccessMode.READ_WRITE), GITLAB_API_KEY("integrations", "gitlab.api.key", null, PropertyType.ENCRYPTEDSTRING, "API Key for GitLab Users team", ConfigPropertyAccessMode.READ_WRITE), diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 04cef07467..05ad385761 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -48,6 +48,7 @@ import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.ProjectVersion; +import org.dependencytrack.model.Role; import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vulnerability; @@ -839,20 +840,31 @@ void preprocessACLs(final Query query, final String inputFilter, final Map { + final var apiTeam = apiKey.getTeams().stream().findFirst(); + if (apiTeam.isPresent()) { + LOGGER.debug("adding Team to ACL of newly created project"); + final Team team = getObjectByUuid(Team.class, apiTeam.get().getUuid()); + project.addAccessTeam(team); + persist(project); + return true; + } else { + LOGGER.warn("API Key without a Team, unable to assign team ACL to project."); + return false; + } + } + case User user when aclEnabled -> { + addRoleToUser(getUser(user.getUsername()), getRole(role.getUuid().toString()), project); return true; - } else { - LOGGER.warn("API Key without a Team, unable to assign team ACL to project."); + } + default -> { + // No ACL update for other principals + return false; } } - return false; } @Override diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java index e4022f436f..5896c4eabf 100644 --- a/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/apiserver/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -580,8 +580,8 @@ public Project updateProject(Project transientProject, boolean commitIndex) { return getProjectQueryManager().updateProject(transientProject, commitIndex); } - public boolean updateNewProjectACL(Project transientProject, Principal principal) { - return getProjectQueryManager().updateNewProjectACL(transientProject, principal); + public boolean updateNewProjectACL(Project transientProject, Principal principal, Role role) { + return getProjectQueryManager().updateNewProjectACL(transientProject, principal, role); } public Project clone(UUID from, String newVersion, boolean includeTags, boolean includeProperties, diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 7b70018bfd..406f5f2b74 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -24,6 +24,12 @@ import alpine.notification.Notification; import alpine.notification.NotificationLevel; import alpine.server.auth.PermissionRequired; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.SignatureException; import alpine.server.filters.ResourceAccessRequired; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -43,10 +49,13 @@ import org.dependencytrack.event.BomUploadEvent; import org.dependencytrack.event.kafka.KafkaEventDispatcher; import org.dependencytrack.filestorage.FileStorage; +import org.dependencytrack.integrations.gitlab.GitLabClient; +import org.dependencytrack.integrations.gitlab.GitLabRole; import org.dependencytrack.model.BomValidationMode; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; +import org.dependencytrack.model.Role; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; @@ -65,6 +74,9 @@ import org.glassfish.jersey.media.multipart.BodyPartEntity; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; +import org.owasp.security.logging.SecurityMarkers; + +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.Json; import jakarta.json.JsonArray; @@ -92,12 +104,18 @@ import java.util.Arrays; import java.util.Base64; import java.util.List; +import java.util.Map; import java.util.Set; - +import java.util.function.Function; import static java.util.function.Predicate.not; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_MODE; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE; import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_AUTOCREATE_PROJECTS; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_JWKS_PATH; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_SBOM_PUSH_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_URL; /** * JAX-RS resources for processing bill-of-material (bom) documents. @@ -335,18 +353,7 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) } requireAccess(qm, parent, "Access to the specified parent project is forbidden"); } - final String trimmedProjectName = StringUtils.trimToNull(request.getProjectName()); - if (request.isLatestProjectVersion()) { - final Project oldLatest = qm.getLatestProjectVersion(trimmedProjectName); - if(oldLatest != null) { - requireAccess(qm, oldLatest, "Access to the previous latest project version is forbidden"); - } - } - project = qm.createProject(trimmedProjectName, null, - StringUtils.trimToNull(request.getProjectVersion()), request.getProjectTags(), parent, - null, null, request.isLatestProjectVersion(), true); - Principal principal = getPrincipal(); - qm.updateNewProjectACL(project, principal); + createNewProject(request.getProjectName(), request.getProjectVersion(), request.getProjectTags(), parent, request.isLatestProjectVersion(), null); } else { return Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build(); } @@ -356,6 +363,120 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) } } + @POST + @Path("/gitlab") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Upload a supported bill of material from GitLab", description = "This endpoint processes input and delegates the request to the uploadBom method.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Token to be used for checking BOM processing progress", content = @Content(schema = @Schema(implementation = BomUploadResponse.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "The project could not be found") + }) + @PermissionRequired(Permissions.Constants.BOM_UPLOAD) + @ResourceAccessRequired + public Response uploadBomGitLab( + @FormDataParam("gitLabToken") String idToken, + @FormDataParam("bom") String bom, + @FormDataParam("isLatest") @DefaultValue("false") boolean isLatest) { + + try (QueryManager qm = new QueryManager()) { + Function propertyGetter = cpc -> qm.getConfigProperty( + cpc.getGroupName(), + cpc.getPropertyName()); + + ConfigProperty gitLabIntegrationConfigProperty = propertyGetter.apply(GITLAB_ENABLED); + if (gitLabIntegrationConfigProperty == null + || !Boolean.parseBoolean(gitLabIntegrationConfigProperty.getPropertyValue())) + return Response.notModified("GitLab integration not enabled").build(); + + ConfigProperty sbomPushConfigProperty = propertyGetter.apply(GITLAB_SBOM_PUSH_ENABLED); + if (sbomPushConfigProperty == null || !Boolean.parseBoolean(sbomPushConfigProperty.getPropertyValue())) + return Response.notModified("GitLab SBOM push functionality not enabled").build(); + + Boolean autoCreateProject = Boolean + .parseBoolean(propertyGetter.apply(GITLAB_AUTOCREATE_PROJECTS).getPropertyValue()); + + if (idToken == null || !idToken.matches("^[\\w-]+\\.[\\w-]+\\.[\\w-]+$")) + return Response.status(Response.Status.UNAUTHORIZED).entity("Invalid or missing GitLab idToken") + .build(); + + ConfigProperty gitLabUrlProperty = propertyGetter.apply(GITLAB_URL); + ConfigProperty gitLabJwksPathProperty = propertyGetter.apply(GITLAB_JWKS_PATH); + + // Get the key id (kid) from the JWT header + String headerJson = new String(Base64.getUrlDecoder().decode(idToken.split("\\.")[0])); + String kid = (String) new ObjectMapper().readValue(headerJson, Map.class).get("kid"); + + Claims claims = Jwts.parser() + .verifyWith(GitLabClient.getPublicKeyFromJwks(gitLabUrlProperty.getPropertyValue(), + gitLabJwksPathProperty.getPropertyValue(), kid)) + .build() + .parseSignedClaims(idToken) + .getPayload(); + + // If autoCreate is enabled and the project doesn't exist, create the project + final String projectName = List.of(claims.get(GitLabClient.PROJECT_PATH_CLAIM, String.class).split("/")) + .getLast(); + final String projectVersion = claims + .get(claims.get(GitLabClient.REF_TYPE_CLAIM, String.class).equals("tag") ? "ref" + : GitLabClient.REF_PATH_CLAIM, String.class); + Project project = qm.getProject(projectName, projectVersion); + + final GitLabRole gitLabRole = GitLabRole + .valueOf(claims.get(GitLabClient.USER_ACCESS_LEVEL_CLAIM, String.class).toUpperCase()); + Role role = (gitLabRole != null) + ? qm.getRoleByName(gitLabRole.getDescription()) + : null; + + if (project == null) { + if (autoCreateProject + && Set.of("owner", "maintainer") + .contains(claims.get(GitLabClient.USER_ACCESS_LEVEL_CLAIM, String.class))) + createNewProject(projectName, projectVersion, null, null, isLatest, role); + else + return Response.status(Response.Status.UNAUTHORIZED) + .entity("The principal does not have permission to create project.").build(); + } + + if (claims.get(GitLabClient.PROJECT_PATH_CLAIM, String.class) == null) + return Response.status(Response.Status.BAD_REQUEST).entity("Missing project_path claim").build(); + + if (!claims.get(GitLabClient.REF_TYPE_CLAIM, String.class).equals("tag") + && claims.get(GitLabClient.REF_PATH_CLAIM, String.class) == null) + return Response.status(Response.Status.BAD_REQUEST).entity("Invalid ref_type or missing ref_path claim") + .build(); + + BomSubmitRequest bomSubmitRequest = new BomSubmitRequest( + null, + projectName, + projectVersion, + null, + autoCreateProject, + isLatest, + bom); + + return uploadBom(bomSubmitRequest); + } catch (SignatureException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Received token that did not pass signature verification").build(); + } catch (ExpiredJwtException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Received expired token").build(); + } catch (MalformedJwtException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Received malformed token").build(); + } catch (UnsupportedJwtException | IllegalArgumentException e) { + LOGGER.error(SecurityMarkers.SECURITY_FAILURE, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Received unsupported JWT").build(); + } catch (IOException e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Error reading or parsing the JWT header or JWKS: " + e.getMessage()).build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("An error occured in uploadBomGitLab: " + e.getMessage()).build(); + } + } + @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) @@ -442,18 +563,10 @@ public Response uploadBom( } requireAccess(qm, parent, "Access to the specified parent project is forbidden"); } - if (isLatest) { - final Project oldLatest = qm.getLatestProjectVersion(trimmedProjectName); - if(oldLatest != null) { - requireAccess(qm, oldLatest, "Access to the previous latest project version is forbidden"); - } - } final List tags = (projectTags != null && !projectTags.isBlank()) ? Arrays.stream(projectTags.split(",")).map(String::trim).filter(not(String::isEmpty)).map(org.dependencytrack.model.Tag::new).toList() : null; - project = qm.createProject(trimmedProjectName, null, trimmedProjectVersion, tags, parent, null, null, isLatest, true); - Principal principal = getPrincipal(); - qm.updateNewProjectACL(project, principal); + createNewProject(projectName, projectVersion, tags, parent, isLatest, null); } else { return Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build(); } @@ -641,4 +754,25 @@ private static boolean shouldValidate(final Project project) { || (validationMode == BomValidationMode.DISABLED_FOR_TAGS && !doTagsMatch); } } + + private void createNewProject(String name, String version, + List tags, Project parent, + boolean isLatest, Role role) { + try (QueryManager qm = new QueryManager()) { + final String trimmedProjectName = StringUtils.trimToNull(name); + final String trimmedProjectVersion = StringUtils.trimToNull(version); + + if (isLatest) { + final Project oldLatest = qm.getLatestProjectVersion(trimmedProjectName); + if (oldLatest != null) { + requireAccess(qm, oldLatest, "Access to the previous latest project version is forbidden"); + } + } + Project project = qm.createProject(trimmedProjectName, null, + trimmedProjectVersion, tags, parent, + null, null, isLatest, true); + Principal principal = getPrincipal(); + qm.updateNewProjectACL(project, principal, role); + } + } } From 5343d9543659d1e5e5b86048b54b7d06efb646b3 Mon Sep 17 00:00:00 2001 From: Alexis Lamb Date: Wed, 9 Jul 2025 15:50:28 -0400 Subject: [PATCH 172/181] fix project creation error handling in BomResource (#14) Signed-off-by: Alexis Lamb --- .../org/dependencytrack/resources/v1/BomResource.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 605f970453..38602a65f4 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -357,9 +357,10 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) requireAccess(qm, parent, "Access to the specified parent project is forbidden"); } createNewProject(request.getProjectName(), request.getProjectVersion(), request.getProjectTags(), parent, request.isLatestProjectVersion(), null); + } else { + return Response.status(Response.Status.UNAUTHORIZED) + .entity("The principal does not have permission to create project.").build(); } - } else { - return Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build(); } return process(qm, project, request.getBom()); }); @@ -574,9 +575,10 @@ public Response uploadBom( ? Arrays.stream(projectTags.split(",")).map(String::trim).filter(not(String::isEmpty)).map(org.dependencytrack.model.Tag::new).toList() : null; createNewProject(projectName, projectVersion, tags, parent, isLatest, null); + } else { + return Response.status(Response.Status.UNAUTHORIZED) + .entity("The principal does not have permission to create project.").build(); } - } else { - return Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build(); } return process(qm, project, artifactParts); }); From aba8f5196ecdcbdc94da44c5cd1d53bd252c95c3 Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 14 Jul 2025 11:20:05 -0600 Subject: [PATCH 173/181] tests: update gitlab statechanger tests (#15) Signed-off-by: Allen Shearin --- .../GitLabIntegrationStateChangerTest.java | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java index ecb1c89487..9bdffb5372 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabIntegrationStateChangerTest.java @@ -20,6 +20,7 @@ import alpine.model.IConfigProperty; import alpine.model.Permission; +import alpine.model.Team; import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.auth.Permissions; @@ -33,8 +34,10 @@ import java.util.List; import java.util.Map; +import static org.dependencytrack.model.ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_API_KEY; import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.GITLAB_SBOM_PUSH_ENABLED; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -66,10 +69,17 @@ public void testEnable() { qm.createPermission(Permissions.Constants.VIEW_PORTFOLIO, "view portfolio"); stateChanger.setQueryManager(qm); + qm.createConfigProperty( + ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "false", + IConfigProperty.PropertyType.BOOLEAN, + null); + qm.createConfigProperty( GITLAB_ENABLED.getGroupName(), GITLAB_ENABLED.getPropertyName(), - "true", + "false", IConfigProperty.PropertyType.BOOLEAN, null); @@ -82,12 +92,14 @@ public void testEnable() { stateChanger.setState(true); roles = qm.getRoles(); - Assert.assertEquals(roles.size(), GitLabRole.values().length); + Assert.assertEquals(GitLabRole.values().length, roles.size()); for (GitLabRole role : GitLabRole.values()) { Assert.assertNotNull(qm.getRoleByName(role.getDescription())); } - Assert.assertEquals(qm.getTeams().size(), 1); - Assert.assertEquals(qm.getTeams().get(0).getName(), "GitLab Users"); + List teamsList = qm.getTeams().getList(Team.class); + + Assert.assertEquals(teamsList.size(), 1); + Assert.assertEquals(teamsList.get(0).getName(), "GitLab Users"); } /** @@ -102,9 +114,21 @@ public void testDisable() { qm.createConfigProperty( GITLAB_ENABLED.getGroupName(), GITLAB_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null); + qm.createConfigProperty( + ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), "false", IConfigProperty.PropertyType.BOOLEAN, null); + qm.createConfigProperty( + GITLAB_SBOM_PUSH_ENABLED.getGroupName(), + GITLAB_SBOM_PUSH_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + null); qm.createConfigProperty( GITLAB_API_KEY.getGroupName(), GITLAB_API_KEY.getPropertyName(), @@ -115,19 +139,21 @@ public void testDisable() { // Create roles and team to be removed stateChanger.setState(true); roles = qm.getRoles(); - Assert.assertEquals(roles.size(), GitLabRole.values().length); + Assert.assertEquals(GitLabRole.values().length, roles.size()); for (GitLabRole role : GitLabRole.values()) { Assert.assertNotNull(qm.getRoleByName(role.getDescription())); } - Assert.assertEquals(qm.getTeams().size(), 1); - Assert.assertEquals(qm.getTeams().get(0).getName(), "GitLab Users"); + List teamsList = qm.getTeams().getList(Team.class); + Assert.assertEquals(teamsList.size(), 1); + Assert.assertEquals(teamsList.get(0).getName(), "GitLab Users"); // Disable the integration // and verify that the roles and team are removed stateChanger.setState(false); roles = qm.getRoles(); - Assert.assertEquals(roles.size(), 0); - Assert.assertEquals(qm.getTeams().size(), 0); + teamsList = qm.getTeams().getList(Team.class); + Assert.assertEquals(0, roles.size()); + Assert.assertEquals(0, teamsList.size()); } @Test From de1fb9c0070655eb932c41fb11ef0727dc165aa6 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Mon, 28 Jul 2025 11:37:27 -0500 Subject: [PATCH 174/181] chore: address PR comments Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitLabRole.java | 2 + .../integrations/gitlab/GitLabSyncer.java | 4 +- .../jdbi/mapping/ProjectRowMapper.java | 88 ------------------- .../resources/v1/BomResource.java | 27 +++--- 4 files changed, 15 insertions(+), 106 deletions(-) delete mode 100644 apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java index 18c6b63e84..e9ca3fe9eb 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java @@ -27,6 +27,8 @@ /** * Definitions of access levels/roles as defined by GitLab. + * + * @see GitLab Roles */ public enum GitLabRole { diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 839b4324f9..0dcc694b3d 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -73,9 +73,7 @@ public String description() { @Override public boolean isEnabled() { - final ConfigProperty enabled = qm.getConfigProperty(INTEGRATIONS_GROUP, GITLAB_ENABLED.getPropertyName()); - - return enabled != null && Boolean.parseBoolean(enabled.getPropertyValue()); + return qm.isEnabled(GITLAB_ENABLED); } @Override diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java deleted file mode 100644 index ba435afed2..0000000000 --- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/mapping/ProjectRowMapper.java +++ /dev/null @@ -1,88 +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.jdbi.mapping; - -import org.cyclonedx.model.ExternalReference; -import org.dependencytrack.model.Classifier; -import org.dependencytrack.model.Project; -import org.dependencytrack.persistence.converter.OrganizationalContactsJsonConverter; -import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter; -import org.jdbi.v3.core.mapper.RowMapper; -import org.jdbi.v3.core.statement.StatementContext; - -import com.fasterxml.jackson.core.type.TypeReference; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.List; -import java.util.UUID; - -import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.deserializeJson; -import static org.dependencytrack.persistence.jdbi.mapping.RowMapperUtil.maybeSet; - -public class ProjectRowMapper implements RowMapper { - - private static final TypeReference> EXTERNAL_REFS_TYPE_REF = new TypeReference<>() { - }; - - @Override - public Project map(final ResultSet rs, final StatementContext ctx) throws SQLException { - final var project = new Project(); - - maybeSet(rs, "ID", ResultSet::getLong, project::setId); - maybeSet(rs, "CLASSIFIER", ResultSet::getString, Classifier::valueOf); - maybeSet(rs, "CPE", ResultSet::getString, project::setCpe); - maybeSet(rs, "DESCRIPTION", ResultSet::getString, project::setDescription); - maybeSet(rs, "DIRECT_DEPENDENCIES", ResultSet::getString, project::setDirectDependencies); - deserializeJson(rs, "EXTERNAL_REFERENCES", EXTERNAL_REFS_TYPE_REF); - maybeSet(rs, "GROUP", ResultSet::getString, project::setGroup); - maybeSet(rs, "LAST_BOM_IMPORTED", ResultSet::getDate, project::setLastBomImport); - maybeSet(rs, "LAST_BOM_IMPORTED_FORMAT", ResultSet::getString, project::setLastBomImportFormat); - maybeSet(rs, "LAST_RISKSCORE", ResultSet::getDouble, project::setLastInheritedRiskScore); - maybeSet(rs, "NAME", ResultSet::getString, project::setName); - maybeSet(rs, "PARENT_PROJECT_ID", ResultSet::getLong, value -> { - var parent = new Project(); - parent.setId(value); - project.setParent(parent); - }); - maybeSet(rs, "PUBLISHER", ResultSet::getString, project::setPublisher); - maybeSet(rs, "PURL", ResultSet::getString, project::setPurl); - maybeSet(rs, "SWIDTAGID", ResultSet::getString, project::setSwidTagId); - maybeSet(rs, "UUID", ResultSet::getString, value -> { - var uuid = UUID.fromString(value); - project.setUuid(uuid); - }); - maybeSet(rs, "VERSION", ResultSet::getString, project::setVersion); - maybeSet(rs, "SUPPLIER", ResultSet::getString, value -> { - var converter = new OrganizationalEntityJsonConverter(); - project.setSupplier(converter.convertToAttribute(value)); - }); - maybeSet(rs, "MANUFACTURER", ResultSet::getString, value -> { - var converter = new OrganizationalEntityJsonConverter(); - project.setManufacturer(converter.convertToAttribute(value)); - }); - maybeSet(rs, "AUTHORS", ResultSet::getString, values -> { - var converter = new OrganizationalContactsJsonConverter(); - project.setAuthors(converter.convertToAttribute(values)); - }); - - return project; - } - -} \ No newline at end of file diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java index e0865ab75a..25087f6373 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -273,7 +273,7 @@ public Response exportComponentAsCycloneDx( then the projectName and projectVersion must be specified. Optionally, if autoCreate is specified and true and the project does not exist, the project will be created. In this scenario, the principal making the request will - additionally need the PORTFOLIO_MANAGEMENT, PORTFOLIO_MANAGEMENT_CREATE, + additionally need the PORTFOLIO_MANAGEMENT, PORTFOLIO_MANAGEMENT_CREATE, or PROJECT_CREATION_UPLOAD permission.

        @@ -400,13 +400,10 @@ public Response uploadBomGitLab( cpc.getGroupName(), cpc.getPropertyName()); - ConfigProperty gitLabIntegrationConfigProperty = propertyGetter.apply(GITLAB_ENABLED); - if (gitLabIntegrationConfigProperty == null - || !Boolean.parseBoolean(gitLabIntegrationConfigProperty.getPropertyValue())) + if (qm.isEnabled(GITLAB_ENABLED)) return Response.notModified("GitLab integration not enabled").build(); - ConfigProperty sbomPushConfigProperty = propertyGetter.apply(GITLAB_SBOM_PUSH_ENABLED); - if (sbomPushConfigProperty == null || !Boolean.parseBoolean(sbomPushConfigProperty.getPropertyValue())) + if (qm.isEnabled(GITLAB_SBOM_PUSH_ENABLED)) return Response.notModified("GitLab SBOM push functionality not enabled").build(); Boolean autoCreateProject = Boolean @@ -473,21 +470,21 @@ public Response uploadBomGitLab( return uploadBom(bomSubmitRequest); } catch (SignatureException e) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + return Response.status(Response.Status.BAD_REQUEST) .entity("Received token that did not pass signature verification").build(); } catch (ExpiredJwtException e) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Received expired token").build(); + return Response.status(Response.Status.BAD_REQUEST).entity("Received expired token").build(); } catch (MalformedJwtException e) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Received malformed token").build(); + return Response.status(Response.Status.BAD_REQUEST).entity("Received malformed token").build(); } catch (UnsupportedJwtException | IllegalArgumentException e) { LOGGER.error(SecurityMarkers.SECURITY_FAILURE, e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Received unsupported JWT").build(); + return Response.status(Response.Status.BAD_REQUEST).entity("Received unsupported JWT").build(); } catch (IOException e) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("Error reading or parsing the JWT header or JWKS: " + e.getMessage()).build(); + LOGGER.error(SecurityMarkers.EVENT_FAILURE, "Error reading or parsing the JWT header or JWKS: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } catch (Exception e) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("An error occured in uploadBomGitLab: " + e.getMessage()).build(); + LOGGER.error(SecurityMarkers.EVENT_FAILURE, "An error occured in uploadBomGitLab: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } } @@ -502,7 +499,7 @@ public Response uploadBomGitLab( then the projectName and projectVersion must be specified. Optionally, if autoCreate is specified and true and the project does not exist, the project will be created. In this scenario, the principal making the request will - additionally need the PORTFOLIO_MANAGEMENT, PORTFOLIO_MANAGEMENT_CREATE, + additionally need the PORTFOLIO_MANAGEMENT, PORTFOLIO_MANAGEMENT_CREATE, or PROJECT_CREATION_UPLOAD permission.

        From da875310243daf2347982331d887fa1337105572 Mon Sep 17 00:00:00 2001 From: emeremikwu-lm Date: Mon, 28 Jul 2025 13:40:45 -0500 Subject: [PATCH 175/181] refactor: update GitLab JWT verification to use configurable issuer URL (#16) Signed-off-by: Emmanuel Meremikwu --- .../java/org/dependencytrack/resources/v1/BomResource.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 25087f6373..7184f2e1e8 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.resources.v1; +import alpine.Config; import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.model.ConfigProperty; @@ -414,6 +415,8 @@ public Response uploadBomGitLab( .build(); ConfigProperty gitLabUrlProperty = propertyGetter.apply(GITLAB_URL); + String alpineIssuerProperty = Config.getInstance().getProperty(Config.AlpineKey.OIDC_ISSUER); + String gitlabUrl = StringUtils.defaultIfBlank(alpineIssuerProperty, gitLabUrlProperty.getPropertyValue()); ConfigProperty gitLabJwksPathProperty = propertyGetter.apply(GITLAB_JWKS_PATH); // Get the key id (kid) from the JWT header @@ -421,8 +424,7 @@ public Response uploadBomGitLab( String kid = (String) new ObjectMapper().readValue(headerJson, Map.class).get("kid"); Claims claims = Jwts.parser() - .verifyWith(GitLabClient.getPublicKeyFromJwks(gitLabUrlProperty.getPropertyValue(), - gitLabJwksPathProperty.getPropertyValue(), kid)) + .verifyWith(GitLabClient.getPublicKeyFromJwks(gitlabUrl, gitLabJwksPathProperty.getPropertyValue(), kid)) .build() .parseSignedClaims(idToken) .getPayload(); From bd81710aac648dc6c656926e0004ba198a6cf87b Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Mon, 28 Jul 2025 15:03:05 -0600 Subject: [PATCH 176/181] fix: remove unused import (#19) Signed-off-by: Allen Shearin --- .../org/dependencytrack/integrations/gitlab/GitLabSyncer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 0dcc694b3d..928f9b1792 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -40,7 +40,6 @@ import org.dependencytrack.model.Role; import alpine.common.logging.Logger; -import alpine.model.ConfigProperty; import alpine.model.OidcUser; public class GitLabSyncer extends AbstractIntegrationPoint implements PermissionsSyncer { From acc0764b03c48542ba0a2b26296fb5b13401abfd Mon Sep 17 00:00:00 2001 From: Allen Shearin Date: Tue, 29 Jul 2025 10:16:48 -0600 Subject: [PATCH 177/181] fix: add null check for access level field in gitlab token (#18) * fix: add null check for access level field in gitlab token Signed-off-by: Allen Shearin * Update apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java Signed-off-by: Allen Shearin --------- Signed-off-by: Allen Shearin --- .../org/dependencytrack/resources/v1/BomResource.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 7184f2e1e8..cc12997553 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -437,8 +437,13 @@ public Response uploadBomGitLab( : GitLabClient.REF_PATH_CLAIM, String.class); Project project = qm.getProject(projectName, projectVersion); - final GitLabRole gitLabRole = GitLabRole - .valueOf(claims.get(GitLabClient.USER_ACCESS_LEVEL_CLAIM, String.class).toUpperCase()); + String accessLevel = claims.get(GitLabClient.USER_ACCESS_LEVEL_CLAIM, String.class); + if (accessLevel == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Missing user_access_level claim in token").build(); + } + + final GitLabRole gitLabRole = GitLabRole.valueOf(accessLevel.toUpperCase()); Role role = (gitLabRole != null) ? qm.getRoleByName(gitLabRole.getDescription()) : null; From df911e47239f1b0ed8c1a73c4f3761668834365e Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Thu, 31 Jul 2025 14:12:40 -0500 Subject: [PATCH 178/181] chore: address PR comments Signed-off-by: Jonathan Howard --- .../persistence/AlpineQueryManager.java | 16 +++++++-- .../src/main/java/alpine/model/LdapUser.java | 4 +-- .../main/java/alpine/model/ManagedUser.java | 4 +-- .../src/main/java/alpine/model/OidcUser.java | 4 +-- .../integrations/gitlab/GitLabSyncer.java | 23 +++++------- .../resources/v1/IntegrationResource.java | 31 +++++++++------- .../resources/v1/PermissionResource.java | 35 ++++++++++--------- .../resources/v1/UserResource.java | 7 ++-- 8 files changed, 70 insertions(+), 54 deletions(-) diff --git a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java index bf850e8e81..f37d475090 100644 --- a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java +++ b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java @@ -348,10 +348,20 @@ public OidcUser addUserToTeams(final OidcUser user, final List teamNames * @return a list of all Users * @since 1.0.0 */ - public List getAllUsers() { - final Query query = pm.newQuery(User.class); + public PaginatedResult getAllUsers() { + final Extent extent = pm.getExtent(User.class, true); + final Query query = pm.newQuery(extent) + .orderBy("username ASC"); + + if (filter != null) { + query.setFilter("username.toLowerCase().matches(:filter)"); + final String filterString = ".*" + filter.toLowerCase() + ".*"; + return execute(query, filterString); + } + query.setOrdering("username asc"); - return executeAndCloseList(query); + + return execute(query); } /** diff --git a/alpine/alpine-model/src/main/java/alpine/model/LdapUser.java b/alpine/alpine-model/src/main/java/alpine/model/LdapUser.java index 067094202b..a994390377 100644 --- a/alpine/alpine-model/src/main/java/alpine/model/LdapUser.java +++ b/alpine/alpine-model/src/main/java/alpine/model/LdapUser.java @@ -39,9 +39,9 @@ */ @PersistenceCapable @Inheritance(strategy = InheritanceStrategy.SUPERCLASS_TABLE) -@Discriminator(value = "LDAP") +@Discriminator("LDAP") @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder(value = { "username", "dn", "email", "teams", "permissions" }) +@JsonPropertyOrder({ "username", "dn", "email", "teams", "permissions" }) public class LdapUser extends User { private static final long serialVersionUID = 261924579887470488L; diff --git a/alpine/alpine-model/src/main/java/alpine/model/ManagedUser.java b/alpine/alpine-model/src/main/java/alpine/model/ManagedUser.java index 105b30b968..61f40318c9 100644 --- a/alpine/alpine-model/src/main/java/alpine/model/ManagedUser.java +++ b/alpine/alpine-model/src/main/java/alpine/model/ManagedUser.java @@ -42,9 +42,9 @@ */ @PersistenceCapable @Inheritance(strategy = InheritanceStrategy.SUPERCLASS_TABLE) -@Discriminator(value = "MANAGED") +@Discriminator("MANAGED") @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder(value = { +@JsonPropertyOrder({ "username", "lastPasswordChange", "fullname", diff --git a/alpine/alpine-model/src/main/java/alpine/model/OidcUser.java b/alpine/alpine-model/src/main/java/alpine/model/OidcUser.java index b382f5bd28..ea7a4c8e8c 100644 --- a/alpine/alpine-model/src/main/java/alpine/model/OidcUser.java +++ b/alpine/alpine-model/src/main/java/alpine/model/OidcUser.java @@ -38,9 +38,9 @@ */ @PersistenceCapable @Inheritance(strategy = InheritanceStrategy.SUPERCLASS_TABLE) -@Discriminator(value = "OIDC") +@Discriminator("OIDC") @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder(value = { "username", "subjectIdentifier", "email", "teams", "permissions" }) +@JsonPropertyOrder({ "username", "subjectIdentifier", "email", "teams", "permissions" }) public class OidcUser extends User { private static final long serialVersionUID = -6852825148699565269L; diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java index 928f9b1792..faea29b952 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabSyncer.java @@ -28,12 +28,9 @@ import java.util.Date; import java.util.List; import java.util.Map; -import java.util.concurrent.locks.Lock; import java.util.function.Function; import java.util.stream.Collectors; -import com.google.common.util.concurrent.Striped; - import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PermissionsSyncer; import org.dependencytrack.model.Project; @@ -50,12 +47,10 @@ public class GitLabSyncer extends AbstractIntegrationPoint implements Permission private static final String ROLE_CLAIM_PREFIX = "https://gitlab.org/claims/groups/"; private final OidcUser user; - private final Striped locks; private GitLabClient gitLabClient; public GitLabSyncer(final OidcUser user, final GitLabClient gitlabClient) { - this.locks = Striped.lock(128); this.user = user; this.gitLabClient = gitlabClient; } @@ -87,17 +82,19 @@ public void synchronize() { } } - private List createProjects(List gitLabProjects) { - List projects = new ArrayList<>(); + private List createProjects(final List gitLabProjects) { + final List projects = new ArrayList<>(); - Map roleMap = Arrays.stream(GitLabRole.values()) + final Map roleMap = Arrays.stream(GitLabRole.values()) .collect(Collectors.toMap(Function.identity(), role -> qm.getRoleByName(role.getDescription()))); for (var gitLabProject : gitLabProjects) { - final Lock lock = locks.get(gitLabProject.getFullPath()); - lock.lock(); + qm.runInTransaction(() -> { + if (!qm.tryAcquireAdvisoryLock(gitLabProject.getFullPath())) + throw new IllegalStateException("Failed to acquire advisory lock for GitLab project %s, " + + "likely because another sync for this project is already in progress" + .formatted(gitLabProject.getFullPath())); - try { Project project = qm.getProject(gitLabProject.getFullPath(), null); if (project == null) { @@ -114,9 +111,7 @@ private List createProjects(List gitLabProjects) { qm.addRoleToUser(user, roleMap.get(gitLabProject.getMaxAccessLevel().stringValue()), project); projects.add(qm.updateProject(project, false)); - } finally { - lock.unlock(); - } + }); } return projects; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java index b22dc87b1e..af93bb2a11 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/IntegrationResource.java @@ -107,7 +107,7 @@ public Response getInactiveEcosystems() { @POST @Path("gitlab/{state}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Set state of gitlab integration", description = "

        Requires permission SYSTEM_CONFIGURATION or SYSTEM_CONFIGURATION_CREATE

        ") + @Operation(summary = "Enable or disable GitLab integration", description = "

        Requires permission SYSTEM_CONFIGURATION or SYSTEM_CONFIGURATION_CREATE

        ") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "GitLab state set successfully"), @ApiResponse(responseCode = "304", description = "The GitLab integration is already in the desired state"), @@ -115,22 +115,29 @@ public Response getInactiveEcosystems() { }) @PermissionRequired({ Permissions.Constants.SYSTEM_CONFIGURATION, Permissions.Constants.SYSTEM_CONFIGURATION_CREATE }) // Require admin privileges due to system impact - public Response handleGitLabStateChange( + public Response setGitLabEnabledState( @Parameter(description = "A valid boolean", required = true) @PathParam("state") String state) { - try (QueryManager qm = new QueryManager()) { - final ConfigProperty property = qm.getConfigProperty(GITLAB_ENABLED.getGroupName(), - GITLAB_ENABLED.getPropertyName()); + try (final QueryManager qm = new QueryManager()) { + final Response response = qm.callInTransaction(() -> { + final ConfigProperty property = qm.getConfigProperty(GITLAB_ENABLED.getGroupName(), + GITLAB_ENABLED.getPropertyName()); - if (!property.getPropertyValue().equals(state)) { - if (!state.equalsIgnoreCase("true") && !state.equalsIgnoreCase("false")) { + if (property.getPropertyValue().equals(state)) + return Response.notModified().build(); + + if (!state.equalsIgnoreCase("true") && !state.equalsIgnoreCase("false")) return Response.status(Response.Status.BAD_REQUEST).build(); - } + property.setPropertyValue(state); - qm.persist(property); + + return Response.ok().entity(qm.persist(property)).build(); + }); + + if (response.getStatus() == Response.Status.OK.getStatusCode()) Event.dispatch(new GitLabIntegrationStateEvent()); - } - } - return Response.ok().build(); + return response; + } } + } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 5e2167800f..7a3c1f2d47 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -496,27 +496,30 @@ public Response setTeamPermissions(@Parameter(description = "Team UUID and reque @PermissionRequired({ Permissions.Constants.ACCESS_MANAGEMENT, Permissions.Constants.ACCESS_MANAGEMENT_UPDATE }) public Response setRolePermissions(@Parameter(description = "Role UUID and requested permissions") @Valid final RolePermissionsSetRequest request) { try (QueryManager qm = new QueryManager()) { - Role role = qm.getObjectByUuid(Role.class, request.role(), Role.FetchGroup.ALL.name()); - if (role == null) - return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); + return qm.callInTransaction(() -> { + Role role = qm.getObjectByUuid(Role.class, request.role(), Role.FetchGroup.ALL.name()); + if (role == null) + return Response.status(Response.Status.NOT_FOUND).entity("The role could not be found.").build(); - final List permissionNames = request.permissions() - .stream() - .map(Permissions::name) - .toList(); + final List permissionNames = request.permissions() + .stream() + .map(Permissions::name) + .toList(); + + final Set requestedPermissions = Set.copyOf(qm.getPermissionsByName(permissionNames)); - final Set requestedPermissions = Set.copyOf(qm.getPermissionsByName(permissionNames)); + if (role.getPermissions().equals(requestedPermissions)) + return Response.notModified().entity("Role already has selected permission(s).").build(); - if (role.getPermissions().equals(requestedPermissions)) - return Response.notModified().entity("Role already has selected permission(s).").build(); + role.setPermissions(requestedPermissions); + role = qm.persist(role); - role.setPermissions(requestedPermissions); - role = qm.persist(role); + super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, + "Set permissions for role: %s / permissions: %s" + .formatted(role.getName(), permissionNames)); - super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, - "Set permissions for role: %s / permissions: %s" - .formatted(role.getName(), permissionNames)); - return Response.ok(role).build(); + return Response.ok(role).build(); + }); } } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index 79bacaadfe..a3c42a2106 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -28,6 +28,7 @@ import alpine.model.User; import alpine.notification.Notification; import alpine.notification.NotificationLevel; +import alpine.persistence.PaginatedResult; import alpine.security.crypto.KeyManager; import alpine.server.auth.AlpineAuthenticationException; import alpine.server.auth.AuthenticationNotRequired; @@ -299,10 +300,10 @@ public Response getUsers( @QueryParam("userType") String type, @QueryParam("username") String username) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { - final List users; + final PaginatedResult result; if (type == null) { - users = qm.getAllUsers(); - return Response.ok(users).header(TOTAL_COUNT_HEADER, users.size()).build(); + result = qm.getAllUsers(); + return Response.ok(result).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } Query query = qm.getPersistenceManager() From 310fbd17f115e05e1adcb3aab2d1b550dc005557 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Thu, 31 Jul 2025 14:42:55 -0500 Subject: [PATCH 179/181] fix: add missing import Signed-off-by: Jonathan Howard --- .../src/main/java/alpine/persistence/AlpineQueryManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java index f37d475090..ddd28ba9c7 100644 --- a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java +++ b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java @@ -39,6 +39,7 @@ import alpine.security.ApiKeyGenerator; import org.datanucleus.store.rdbms.query.JDOQLQuery; +import javax.jdo.Extent; import javax.jdo.PersistenceManager; import javax.jdo.Query; import java.security.Principal; From 7b88d2202e936eddea8d9b6e916b5174a5407aa6 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Fri, 1 Aug 2025 12:59:51 -0500 Subject: [PATCH 180/181] fix: project-scoped permissions for role defaults Signed-off-by: Jonathan Howard --- .../integrations/gitlab/GitLabClient.java | 76 +++---------------- .../integrations/gitlab/GitLabRole.java | 48 ++++-------- .../resources/v1/UserResource.java | 15 ++-- .../integrations/gitlab/GitLabClientTest.java | 10 --- .../integrations/gitlab/GitLabRoleTest.java | 8 +- 5 files changed, 34 insertions(+), 123 deletions(-) diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java index d4731a7117..609099d434 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabClient.java @@ -43,7 +43,6 @@ import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; -import org.dependencytrack.auth.Permissions; import org.dependencytrack.common.HttpClientPool; import alpine.Config; @@ -70,58 +69,6 @@ public class GitLabClient { public static final String REF_TYPE_CLAIM = "ref_type"; public static final String USER_ACCESS_LEVEL_CLAIM = "user_access_level"; - private final Map> rolePermissions = Map.of( - GitLabRole.GUEST, List.of( - Permissions.VIEW_PORTFOLIO, - Permissions.VIEW_VULNERABILITY, - Permissions.VIEW_BADGES), - GitLabRole.PLANNER, List.of( - Permissions.VIEW_PORTFOLIO, - Permissions.VIEW_VULNERABILITY, - Permissions.VIEW_POLICY_VIOLATION, - Permissions.VIEW_BADGES), - GitLabRole.REPORTER, List.of( - Permissions.VIEW_PORTFOLIO, - Permissions.VIEW_VULNERABILITY, - Permissions.VIEW_POLICY_VIOLATION, - Permissions.VIEW_BADGES), - GitLabRole.DEVELOPER, List.of( - Permissions.BOM_UPLOAD, - Permissions.VIEW_PORTFOLIO, - Permissions.PORTFOLIO_MANAGEMENT_READ, - Permissions.VIEW_VULNERABILITY, - Permissions.VULNERABILITY_ANALYSIS_READ, - Permissions.PROJECT_CREATION_UPLOAD), - GitLabRole.MAINTAINER, List.of( - Permissions.BOM_UPLOAD, - Permissions.PORTFOLIO_MANAGEMENT, - Permissions.PORTFOLIO_MANAGEMENT_CREATE, - Permissions.PORTFOLIO_MANAGEMENT_READ, - Permissions.PORTFOLIO_MANAGEMENT_UPDATE, - Permissions.PORTFOLIO_MANAGEMENT_DELETE, - Permissions.VULNERABILITY_ANALYSIS, - Permissions.VULNERABILITY_ANALYSIS_CREATE, - Permissions.VULNERABILITY_ANALYSIS_READ, - Permissions.VULNERABILITY_ANALYSIS_UPDATE, - Permissions.POLICY_MANAGEMENT, - Permissions.POLICY_MANAGEMENT_CREATE, - Permissions.POLICY_MANAGEMENT_READ, - Permissions.POLICY_MANAGEMENT_UPDATE, - Permissions.POLICY_MANAGEMENT_DELETE), - GitLabRole.OWNER, List.of( - Permissions.ACCESS_MANAGEMENT, - Permissions.ACCESS_MANAGEMENT_CREATE, - Permissions.ACCESS_MANAGEMENT_READ, - Permissions.ACCESS_MANAGEMENT_UPDATE, - Permissions.ACCESS_MANAGEMENT_DELETE, - Permissions.SYSTEM_CONFIGURATION, - Permissions.SYSTEM_CONFIGURATION_CREATE, - Permissions.SYSTEM_CONFIGURATION_READ, - Permissions.SYSTEM_CONFIGURATION_UPDATE, - Permissions.SYSTEM_CONFIGURATION_DELETE, - Permissions.TAG_MANAGEMENT, - Permissions.TAG_MANAGEMENT_DELETE)); - public GitLabClient(final String accessToken) { this(accessToken, Config.getInstance(), null, false); } @@ -211,19 +158,19 @@ public List getGitLabProjects() throws IOException, URISyntaxExce return projects; } - private static JSONObject getJwks(String jwksUrl) throws IOException, InterruptedException, URISyntaxException { + private static JSONObject getJwks(String jwksUrl) throws IOException, InterruptedException, URISyntaxException { URIBuilder builder = new URIBuilder(jwksUrl); HttpGet request = new HttpGet(builder.build()); request.setHeader("Accept", "application/json"); - + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { String jsonResponse = EntityUtils.toString(response.getEntity()); - if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) throw new IOException("Failed to fetch JWKS from URL: %s. Status code: %d".formatted(jwksUrl, response.getStatusLine().getStatusCode())); - + if (!jsonResponse.trim().startsWith("{")) throw new IOException("Unexpected response: " + response.getEntity()); - + return JSONValue.parse(jsonResponse, JSONObject.class); } } @@ -244,24 +191,21 @@ public static PublicKey getPublicKeyFromJwks(String baseUrl, String jwksPath, St jsonKey.put("kid", keyMap.get("kid")); jsonKey.put("n", keyMap.get("n")); jsonKey.put("e", keyMap.get("e")); - + if (jsonKey.get("kid").equals(kid)) { if (!jsonKey.containsKey("n") || !jsonKey.containsKey("e")) throw new IllegalArgumentException("Missing modulus 'n' or exponent 'e' in JWKS key: " + jsonKey); - + RSAPublicKeySpec spec = new RSAPublicKeySpec( - new BigInteger(1, Base64.getUrlDecoder().decode(jsonKey.get("n").toString())), + new BigInteger(1, Base64.getUrlDecoder().decode(jsonKey.get("n").toString())), new BigInteger(1, Base64.getUrlDecoder().decode(jsonKey.get("e").toString())) ); - + return KeyFactory.getInstance("RSA").generatePublic(spec); } } - throw new IllegalArgumentException("Public key not found for kid: " + kid); - } - public List getRolePermissions(final GitLabRole role) { - return rolePermissions.get(role); + throw new IllegalArgumentException("Public key not found for kid: " + kid); } // JSONArray to ArrayList simple converter diff --git a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java index e9ca3fe9eb..5856c323c2 100644 --- a/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java +++ b/apiserver/src/main/java/org/dependencytrack/integrations/gitlab/GitLabRole.java @@ -33,44 +33,24 @@ public enum GitLabRole { GUEST(10, "GitLab Project Guest", Set.of( // Applies to private and internal projects only - Permissions.Constants.VIEW_PORTFOLIO, - Permissions.Constants.VIEW_VULNERABILITY, - Permissions.Constants.VIEW_BADGES)), + Permissions.Constants.VIEW_BADGES, + Permissions.Constants.VIEW_PORTFOLIO)), PLANNER(15, "GitLab Project Planner", Set.of( - Permissions.Constants.VIEW_POLICY_VIOLATION)), + Permissions.Constants.VIEW_VULNERABILITY)), REPORTER(20, "GitLab Project Reporter", Set.of( Permissions.Constants.VIEW_POLICY_VIOLATION)), DEVELOPER(30, "GitLab Project Developer", Set.of( Permissions.Constants.BOM_UPLOAD, Permissions.Constants.PORTFOLIO_MANAGEMENT_READ, - Permissions.Constants.VULNERABILITY_ANALYSIS_READ, - Permissions.Constants.PROJECT_CREATION_UPLOAD)), + Permissions.Constants.PROJECT_CREATION_UPLOAD, + Permissions.Constants.VULNERABILITY_ANALYSIS_READ)), MAINTAINER(40, "GitLab Project Maintainer", Set.of( - Permissions.Constants.PORTFOLIO_MANAGEMENT, - Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE, + Permissions.Constants.POLICY_VIOLATION_ANALYSIS, Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE, - Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE, - Permissions.Constants.VULNERABILITY_ANALYSIS, Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE, - Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE, - Permissions.Constants.POLICY_MANAGEMENT, - Permissions.Constants.POLICY_MANAGEMENT_CREATE, - Permissions.Constants.POLICY_MANAGEMENT_READ, - Permissions.Constants.POLICY_MANAGEMENT_UPDATE, - Permissions.Constants.POLICY_MANAGEMENT_DELETE)), + Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE)), OWNER(50, "GitLab Project Owner", Set.of( - Permissions.Constants.ACCESS_MANAGEMENT, - Permissions.Constants.ACCESS_MANAGEMENT_CREATE, - Permissions.Constants.ACCESS_MANAGEMENT_READ, - Permissions.Constants.ACCESS_MANAGEMENT_UPDATE, - Permissions.Constants.ACCESS_MANAGEMENT_DELETE, - Permissions.Constants.SYSTEM_CONFIGURATION, - Permissions.Constants.SYSTEM_CONFIGURATION_CREATE, - Permissions.Constants.SYSTEM_CONFIGURATION_READ, - Permissions.Constants.SYSTEM_CONFIGURATION_UPDATE, - Permissions.Constants.SYSTEM_CONFIGURATION_DELETE, - Permissions.Constants.TAG_MANAGEMENT, - Permissions.Constants.TAG_MANAGEMENT_DELETE)); + Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE)); private final int accessLevel; private final String description; @@ -96,10 +76,10 @@ public String getDescription() { * * @return A sorted set of permissions for this role. */ -public Set getPermissions() { - return Stream.of(GitLabRole.values()) - .filter(value -> value.accessLevel <= this.accessLevel) // Include current and lower access levels - .flatMap(value -> value.permissions.stream()) // Flatten permissions from all roles - .collect(Collectors.toCollection(LinkedHashSet::new)); // Collect into a LinkedHashSet to maintain order -} + public Set getPermissions() { + return Stream.of(GitLabRole.values()) + .filter(value -> value.accessLevel <= this.accessLevel) // Include current and lower access levels + .flatMap(value -> value.permissions.stream()) // Flatten permissions from all roles + .collect(Collectors.toCollection(LinkedHashSet::new)); // Collect into a LinkedHashSet to maintain order + } } diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java index a3c42a2106..e37570d1c2 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/UserResource.java @@ -317,23 +317,20 @@ public Response getUsers( if (username != null) query.filter("username == :username").setParameters(username); - try { - users = List.copyOf(query.executeList()); - } finally { - query.closeAll(); - } + result = qm.execute(query); + final List users = result.getList(User.class); + final long totalCount = result.getTotal(); - if (users == null) + if (result == null || totalCount == 0) return Response.status(Response.Status.NOT_FOUND) .entity("No user(s) found for the given criteria [type=%s, username=%s]" .formatted(type, username)) .build(); - if (username != null && users.size() == 1) + if (username != null && totalCount == 1) return Response.ok(users.get(0)).build(); - return Response.ok(users).header(TOTAL_COUNT_HEADER, users.size()).build(); - + return Response.ok(users).header(TOTAL_COUNT_HEADER, totalCount).build(); } } diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java index 38db6e2a37..cad008d9f4 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabClientTest.java @@ -42,7 +42,6 @@ import java.util.List; import org.apache.http.HttpHeaders; -import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.kafka.KafkaProducerInitializer; import org.junit.AfterClass; import org.junit.Assert; @@ -156,15 +155,6 @@ public void testGetGitLabProjectsWithTopics() throws IOException, URISyntaxExcep Assert.assertEquals("project/with/topic", gitLabProjects.get(0).getFullPath()); } - @Test - public void testGetRolePermissions() { - String accessToken = "my-access-token"; - GitLabClient client = new GitLabClient(accessToken); - List permissions = client.getRolePermissions(GitLabRole.DEVELOPER); - Assert.assertNotNull(permissions); - Assert.assertEquals(6, permissions.size()); // assume some permissions are returned - } - @Test public void testJsonToList() { String accessToken = "my-access-token"; diff --git a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabRoleTest.java b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabRoleTest.java index ba54132500..2d732e8c72 100644 --- a/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabRoleTest.java +++ b/apiserver/src/test/java/org/dependencytrack/integrations/gitlab/GitLabRoleTest.java @@ -48,13 +48,13 @@ public void testGetDescription() { @Test public void testGetPermissionsForGuest() { Set permissions = GitLabRole.GUEST.getPermissions(); - Assert.assertEquals(3, permissions.size()); + Assert.assertEquals(2, permissions.size()); } @Test public void testGetPermissionsForPlanner() { Set permissions = GitLabRole.PLANNER.getPermissions(); - Assert.assertEquals(4, permissions.size()); + Assert.assertEquals(3, permissions.size()); } @Test @@ -72,12 +72,12 @@ public void testGetPermissionsForDeveloper() { @Test public void testGetPermissionsForMaintainer() { Set permissions = GitLabRole.MAINTAINER.getPermissions(); - Assert.assertEquals(20, permissions.size()); + Assert.assertEquals(12, permissions.size()); } @Test public void testGetPermissionsForOwner() { Set permissions = GitLabRole.OWNER.getPermissions(); - Assert.assertEquals(32, permissions.size()); + Assert.assertEquals(13, permissions.size()); } } From 57edd1170fad0714791858544d06fd2a1d07db01 Mon Sep 17 00:00:00 2001 From: Jonathan Howard Date: Fri, 1 Aug 2025 15:49:49 -0500 Subject: [PATCH 181/181] fix: user subclass field population Signed-off-by: Jonathan Howard --- .../persistence/AlpineQueryManager.java | 15 ++++---------- .../resources/v1/PermissionResource.java | 20 ++----------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java index ddd28ba9c7..d4deee7008 100644 --- a/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java +++ b/alpine/alpine-infra/src/main/java/alpine/persistence/AlpineQueryManager.java @@ -350,19 +350,12 @@ public OidcUser addUserToTeams(final OidcUser user, final List teamNames * @since 1.0.0 */ public PaginatedResult getAllUsers() { - final Extent extent = pm.getExtent(User.class, true); - final Query query = pm.newQuery(extent) - .orderBy("username ASC"); + final Query query = pm.newQuery(User.class).orderBy("username ASC"); + final PaginatedResult result = execute(query); - if (filter != null) { - query.setFilter("username.toLowerCase().matches(:filter)"); - final String filterString = ".*" + filter.toLowerCase() + ".*"; - return execute(query, filterString); - } + pm.refreshAll(result.getObjects()); - query.setOrdering("username asc"); - - return execute(query); + return result; } /** diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java index 7a3c1f2d47..43f4ef15eb 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/PermissionResource.java @@ -19,9 +19,6 @@ package org.dependencytrack.resources.v1; import alpine.common.logging.Logger; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; import alpine.model.Permission; import alpine.model.Team; import alpine.model.User; @@ -37,7 +34,6 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; -import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Role; import org.dependencytrack.model.validation.ValidUuid; @@ -174,13 +170,7 @@ public Response removePermissionFromUser( @PathParam("permission") String permissionName) { try (QueryManager qm = new QueryManager()) { return qm.callInTransaction(() -> { - User user = qm.getUser(username, (Class) switch (StringUtils.defaultString(type).toLowerCase()) { - case "managed" -> ManagedUser.class; - case "ldap" -> LdapUser.class; - case "oidc" -> OidcUser.class; - default -> User.class; - }); - + User user = qm.getUser(username); if (user == null) { return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build(); } @@ -401,13 +391,7 @@ public Response setUserPermissions( @Parameter(description = "A username and valid list permission") @Valid final UserPermissionsSetRequest request) { try (QueryManager qm = new QueryManager()) { return qm.callInTransaction(() -> { - User user = qm.getUser(request.username(), (Class) switch (StringUtils.defaultString(request.userType()).toLowerCase()) { - case "managed" -> ManagedUser.class; - case "ldap" -> LdapUser.class; - case "oidc" -> OidcUser.class; - default -> User.class; - }); - + User user = qm.getUser(request.username()); if (user == null) return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build();