From c4e9db19edf89affefa0c09847b151612a69b696 Mon Sep 17 00:00:00 2001 From: Andrea Marchini <123945788+man8pr@users.noreply.github.com> Date: Sat, 15 Jul 2023 14:55:28 +0200 Subject: [PATCH] feat: implement Vault extension using GCP Secret Manager (#6) (#10) * feat: implementation of Vault with GCP Secret Manager https://github.com/eclipse-edc/Technology-Gcp/issues/6 * style: build config cleanup - Case harmonized to lowercase - Libraries in version file sorted * refactor: fix review comments for #6 - Used Stream instead of String path for GcpSecretManagerVault .createWithServiceAccountCredentials - Switched to var - Fixed log messages to start with capital - Simplified reading of settings in GcpSecretManagerVaultExtension with ServiceExtensionContext.getSetting - Moved private method at the end of the file - Replaced assertTrue/False with assertThat(x).isTrue/isFalse - Used static imports in tests - Mocks reinstantiated at every test, not reset * refactor: fixed new comments for #6 - Used context.getConfig().getString for getting mandatory settings - Moved TestStatusCode class to private, at the end of the file - Fixed imports (removed unused, resorted) * refactor: typos and docs updated, member variables made final * refactor: removed jimfs dependency * chore: updated DEPENDENCIES, cleaned build dependencies * refactor: removed synchronized blocks from Vault implementation - as per review, EDC owns synchronization * chore: DEPENDENCIES - new attempt * chore: updated dependency-check.yml * chore: DEPENDENCIES updated with content generated by new workflow --- .github/workflows/dependency-check.yml | 49 +--- DEPENDENCIES | 48 ++- extensions/common/vault/vault-gcp/README.md | 8 + .../common/vault/vault-gcp/build.gradle.kts | 25 ++ .../edc/vault/gcp/GcpSecretManagerVault.java | 251 ++++++++++++++++ .../gcp/GcpSecretManagerVaultExtension.java | 92 ++++++ .../GcpSecretManagerVaultExtensionTest.java | 130 +++++++++ .../vault/gcp/GcpSecretManagerVaultTest.java | 275 ++++++++++++++++++ .../build.gradle.kts | 4 +- gradle.properties | 4 +- gradle/libs.versions.toml | 20 +- settings.gradle.kts | 7 +- 12 files changed, 840 insertions(+), 73 deletions(-) create mode 100644 extensions/common/vault/vault-gcp/README.md create mode 100644 extensions/common/vault/vault-gcp/build.gradle.kts create mode 100644 extensions/common/vault/vault-gcp/src/main/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVault.java create mode 100644 extensions/common/vault/vault-gcp/src/main/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultExtension.java create mode 100644 extensions/common/vault/vault-gcp/src/test/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultExtensionTest.java create mode 100644 extensions/common/vault/vault-gcp/src/test/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultTest.java diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index 1d67c04..3a570fe 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -9,50 +9,5 @@ permissions: contents: read jobs: - Check-Allowed-Licenses: - runs-on: ubuntu-latest - continue-on-error: false - if: ${{ github.event_name == 'pull_request' }} - steps: - - name: 'Checkout Repository' - uses: actions/checkout@v3 - - name: 'Dependency Review' - uses: actions/dependency-review-action@v3 - with: - fail-on-severity: critical - # Representation of this list: https://www.eclipse.org/legal/licenses.php# - # Expressed with the help of the following IDs: https://spdx.org/licenses/ - allow-licenses: >- - Adobe-Glyph, Apache-1.0, Apache-1.1, Apache-2.0, Artistic-2.0, BSD-2-Clause, BSD-3-Clause, - BSD-4-Clause, 0BSD, BSL-1.0, CDDL-1.0, CDDL-1.1, CPL-1.0, CC-BY-3.0, CC-BY-4.0, CC-BY-2.5, - CC-BY-SA-3.0, CC-BY-SA-4.0, CC0-1.0, EPL-1.0, EPL-2.0, FTL, GFDL-1.3-only, IPL-1.0, ISC, - MIT, MIT-0, MPL-1.1, MPL-2.0, NTP, OpenSSL, PHP-3.01, PostgreSQL, OFL-1.1, Unlicense, - Unicode-DFS-2015, Unicode-DFS-2016, Unicode-TOU, UPL-1.0, W3C-20150513, W3C-19980720, W3C, - WTFPL, X11, Zlib, ZPL-2.1 - - Dash-Dependency-Check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: ./.github/actions/setup-build - - name: Download latest Eclipse Dash - run: | - curl -L https://repo.eclipse.org/service/local/artifact/maven/redirect\?r\=dash-licenses\&g\=org.eclipse.dash\&a\=org.eclipse.dash.licenses\&v\=LATEST --output dash.jar - - name: Regenerate DEPENDENCIES - run: | - # dash returns a nonzero exit code if there are libs that need review. the "|| true" avoids that - ./gradlew allDependencies | grep -Poh "(?<=\s)[\w.-]+:[\w.-]+:[^:\s\[\]]+" | sort | uniq | java -jar dash.jar - -summary DEPENDENCIES-gen || true - - # log warning if restricted deps are found - grep -E 'restricted' DEPENDENCIES | if test $(wc -l) -gt 0; then - echo "::warning file=DEPENDENCIES,title=Restricted Dependencies found::Some dependencies are marked 'restricted' - please review them" - fi - - # log error and fail job if rejected deps are found - grep -E 'rejected' DEPENDENCIES | if test $(wc -l) -gt 0; then - echo "::error file=DEPENDENCIES,title=Rejected Dependencies found::Some dependencies are marked 'rejected', they cannot be used" - exit 1 - fi - - name: Check for differences - run: | - diff DEPENDENCIES DEPENDENCIES-gen \ No newline at end of file + check: + uses: eclipse-edc/.github/.github/workflows/dependency-check.yml@main diff --git a/DEPENDENCIES b/DEPENDENCIES index adf403b..2ecc48a 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -17,48 +17,71 @@ maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.0, A maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.0, Apache-2.0, approved, #7942 maven/mavencentral/com.google.android/annotations/4.1.1.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.api-client/google-api-client/2.2.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.google.api.grpc/gapic-google-cloud-storage-v2/2.22.4-alpha, , restricted, clearlydefined -maven/mavencentral/com.google.api.grpc/grpc-google-cloud-storage-v2/2.22.4-alpha, , restricted, clearlydefined +maven/mavencentral/com.google.api.grpc/gapic-google-cloud-storage-v2/2.22.4-alpha, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.api.grpc/grpc-google-cloud-storage-v2/2.22.4-alpha, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.api.grpc/proto-google-cloud-iamcredentials-v1/2.18.0, , restricted, clearlydefined -maven/mavencentral/com.google.api.grpc/proto-google-cloud-storage-v2/2.22.4-alpha, , restricted, clearlydefined +maven/mavencentral/com.google.api.grpc/proto-google-cloud-secretmanager-v1/2.20.0, , restricted, clearlydefined +maven/mavencentral/com.google.api.grpc/proto-google-cloud-secretmanager-v1beta1/2.20.0, , restricted, clearlydefined +maven/mavencentral/com.google.api.grpc/proto-google-cloud-storage-v2/2.22.4-alpha, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.api.grpc/proto-google-common-protos/2.19.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.api.grpc/proto-google-common-protos/2.20.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.api.grpc/proto-google-common-protos/2.21.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.api.grpc/proto-google-iam-admin-v1/3.14.0, , restricted, clearlydefined maven/mavencentral/com.google.api.grpc/proto-google-iam-v1/1.15.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.api.grpc/proto-google-iam-v1/1.16.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.api/api-common/2.11.1, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.api/api-common/2.12.0, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.google.api/api-common/2.13.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.api/gax-grpc/2.28.1, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.api/gax-grpc/2.29.0, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.google.api/gax-grpc/2.30.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.api/gax-httpjson/0.113.1, BSD-3-Clause, approved, clearlydefined -maven/mavencentral/com.google.api/gax-httpjson/0.114.0, , restricted, clearlydefined +maven/mavencentral/com.google.api/gax-httpjson/0.114.0, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.google.api/gax-httpjson/2.30.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.api/gax/2.28.1, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.api/gax/2.29.0, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.google.api/gax/2.30.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.apis/google-api-services-storage/v1-rev20230301-2.0.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.auth/google-auth-library-credentials/1.16.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.auth/google-auth-library-credentials/1.17.0, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.google.auth/google-auth-library-credentials/1.18.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.auth/google-auth-library-oauth2-http/1.16.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.auth/google-auth-library-oauth2-http/1.17.0, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.google.auth/google-auth-library-oauth2-http/1.18.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.auto.value/auto-value-annotations/1.10.1, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.google.cloud/google-cloud-core-grpc/2.19.0, , restricted, clearlydefined -maven/mavencentral/com.google.cloud/google-cloud-core-http/2.19.0, , restricted, clearlydefined +maven/mavencentral/com.google.cloud/google-cloud-core-grpc/2.19.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.cloud/google-cloud-core-http/2.19.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.cloud/google-cloud-core/2.19.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.cloud/google-cloud-core/2.20.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.cloud/google-cloud-iamcredentials/2.18.0, , restricted, clearlydefined +maven/mavencentral/com.google.cloud/google-cloud-secretmanager/2.20.0, , restricted, clearlydefined maven/mavencentral/com.google.cloud/google-cloud-storage/2.22.4, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.cloud/google-iam-admin/3.14.0, , restricted, clearlydefined maven/mavencentral/com.google.code.findbugs/jsr305/3.0.2, Apache-2.0, approved, #20 maven/mavencentral/com.google.code.gson/gson/2.10.1, Apache-2.0, approved, #6159 +maven/mavencentral/com.google.code.gson/gson/2.8.9, Apache-2.0, approved, CQ23496 +maven/mavencentral/com.google.errorprone/error_prone_annotations/2.11.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.errorprone/error_prone_annotations/2.18.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.errorprone/error_prone_annotations/2.7.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.guava/failureaccess/1.0.1, Apache-2.0, approved, CQ22654 +maven/mavencentral/com.google.guava/guava/29.0-android, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.guava/guava/30.1.1-android, Apache-2.0 AND CC0-1.0 AND LicenseRef-Public-Domain, approved, CQ23244 +maven/mavencentral/com.google.guava/guava/31.0.1-android, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.guava/guava/31.0.1-jre, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.guava/guava/31.1-jre, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.guava/guava/32.0.1-jre, Apache-2.0 AND CC0-1.0 AND CC-PDDC, approved, #8772 maven/mavencentral/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava, Apache-2.0, approved, CQ22657 maven/mavencentral/com.google.http-client/google-http-client-apache-v2/1.43.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.http-client/google-http-client-appengine/1.43.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.http-client/google-http-client-gson/1.42.3, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.http-client/google-http-client-gson/1.43.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.http-client/google-http-client-gson/1.43.3, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.http-client/google-http-client-jackson2/1.43.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.http-client/google-http-client/1.42.3, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.http-client/google-http-client/1.43.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.http-client/google-http-client/1.43.3, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.j2objc/j2objc-annotations/1.3, Apache-2.0, approved, CQ21195 +maven/mavencentral/com.google.j2objc/j2objc-annotations/2.8, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.oauth-client/google-oauth-client/1.34.1, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.protobuf/protobuf-java-util/3.23.1, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.protobuf/protobuf-java-util/3.23.2, BSD-3-Clause, approved, clearlydefined @@ -72,6 +95,7 @@ maven/mavencentral/com.squareup.okhttp3/okhttp/4.9.3, Apache-2.0 AND MPL-2.0, ap maven/mavencentral/com.squareup.okio/okio-jvm/3.2.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.squareup.okio/okio/3.2.0, Apache-2.0, approved, clearlydefined maven/mavencentral/commons-beanutils/commons-beanutils/1.9.4, Apache-2.0, approved, CQ12654 +maven/mavencentral/commons-codec/commons-codec/1.11, Apache-2.0 AND BSD-3-Clause, approved, CQ15971 maven/mavencentral/commons-codec/commons-codec/1.15, Apache-2.0 AND BSD-3-Clause AND LicenseRef-Public-Domain, approved, CQ22641 maven/mavencentral/commons-collections/commons-collections/3.2.2, Apache-2.0, approved, CQ10385 maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ10162 @@ -82,6 +106,7 @@ maven/mavencentral/info.picocli/picocli/4.6.3, Apache-2.0, approved, clearlydefi maven/mavencentral/io.grpc/grpc-alts/1.55.1, Apache-2.0, approved, clearlydefined maven/mavencentral/io.grpc/grpc-api/1.55.1, Apache-2.0, approved, clearlydefined maven/mavencentral/io.grpc/grpc-auth/1.55.1, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.grpc/grpc-context/1.27.2, Apache-2.0, approved, clearlydefined maven/mavencentral/io.grpc/grpc-context/1.55.1, Apache-2.0, approved, clearlydefined maven/mavencentral/io.grpc/grpc-core/1.55.1, Apache-2.0, approved, clearlydefined maven/mavencentral/io.grpc/grpc-googleapis/1.55.1, , restricted, clearlydefined @@ -96,9 +121,9 @@ maven/mavencentral/io.grpc/grpc-xds/1.55.1, Apache-2.0, approved, clearlydefined maven/mavencentral/io.opencensus/opencensus-api/0.31.1, Apache-2.0, approved, clearlydefined maven/mavencentral/io.opencensus/opencensus-contrib-http-util/0.31.1, Apache-2.0, approved, clearlydefined maven/mavencentral/io.opencensus/opencensus-proto/0.2.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.27.0, Apache-2.0, approved, #9270 -maven/mavencentral/io.opentelemetry/opentelemetry-api/1.27.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.opentelemetry/opentelemetry-context/1.27.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.28.0, , restricted, clearlydefined +maven/mavencentral/io.opentelemetry/opentelemetry-api/1.28.0, , restricted, clearlydefined +maven/mavencentral/io.opentelemetry/opentelemetry-context/1.28.0, , restricted, clearlydefined maven/mavencentral/io.perfmark/perfmark-api/0.26.0, Apache-2.0, approved, clearlydefined maven/mavencentral/jakarta.activation/jakarta.activation-api/2.1.0, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf maven/mavencentral/jakarta.annotation/jakarta.annotation-api/2.1.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.ca @@ -127,12 +152,15 @@ maven/mavencentral/org.bouncycastle/bcprov-jdk18on/1.75, MIT AND CC0-1.0, approv maven/mavencentral/org.bouncycastle/bcutil-jdk18on/1.75, MIT, approved, #9170 maven/mavencentral/org.checkerframework/checker-qual/3.12.0, MIT, approved, clearlydefined maven/mavencentral/org.checkerframework/checker-qual/3.32.0, MIT, approved, clearlydefined +maven/mavencentral/org.checkerframework/checker-qual/3.33.0, MIT, approved, clearlydefined maven/mavencentral/org.codehaus.mojo/animal-sniffer-annotations/1.23, MIT, approved, clearlydefined maven/mavencentral/org.conscrypt/conscrypt-openjdk-uber/2.5.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.eclipse.edc/aggregate-service-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/autodoc-processor/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/boot/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/catalog-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/connector-core/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/contract-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/control-plane-api-client-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/core-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/data-plane-core/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc @@ -150,10 +178,12 @@ maven/mavencentral/org.eclipse.edc/policy-engine-spi/0.1.4-SNAPSHOT, Apache-2.0, maven/mavencentral/org.eclipse.edc/policy-engine/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/policy-evaluator/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/policy-model/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/policy-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/runtime-metamodel/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transaction-datasource-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transaction-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transfer-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/transform-core/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transform-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/util/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/validator-spi/0.1.4-SNAPSHOT, Apache-2.0, approved, technology.edc diff --git a/extensions/common/vault/vault-gcp/README.md b/extensions/common/vault/vault-gcp/README.md new file mode 100644 index 0000000..1df44a8 --- /dev/null +++ b/extensions/common/vault/vault-gcp/README.md @@ -0,0 +1,8 @@ +# GCP Secret Manager Vault + +The vault-gcp extension is an implementation of the Vault interface based on GCP Secret Manager. +Arbitrary key names are possible through the key sanitation feature. + +## Decisions +- Secrets will not be overwritten if they exist to prevent potential leakage of credentials to third parties. +- Keys strings are sanitized to comply with key requirements of AWS Secrets Manager. Sanitizing replaces all illegal characters with '-' and appends the hash code of the original key to minimize the risk of key collision after the transformation, because the replacement operation is a many-to-one function. A warning will be logged if the key contains illegal characters. diff --git a/extensions/common/vault/vault-gcp/build.gradle.kts b/extensions/common/vault/vault-gcp/build.gradle.kts new file mode 100644 index 0000000..87acb66 --- /dev/null +++ b/extensions/common/vault/vault-gcp/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Google LCC - Initial implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + api(libs.edc.spi.core) + + implementation(libs.edc.util) + implementation(libs.googlecloud.core) + implementation(libs.googlecloud.secretmanager) +} diff --git a/extensions/common/vault/vault-gcp/src/main/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVault.java b/extensions/common/vault/vault-gcp/src/main/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVault.java new file mode 100644 index 0000000..294626d --- /dev/null +++ b/extensions/common/vault/vault-gcp/src/main/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVault.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2023 Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Google LCC - Initial implementation + * + */ + +package org.eclipse.edc.vault.gcp; + +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.rpc.AlreadyExistsException; +import com.google.api.gax.rpc.NotFoundException; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.secretmanager.v1.ProjectName; +import com.google.cloud.secretmanager.v1.Replication; +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; +import com.google.cloud.secretmanager.v1.SecretName; +import com.google.cloud.secretmanager.v1.SecretPayload; +import com.google.cloud.secretmanager.v1.SecretVersionName; +import com.google.protobuf.ByteString; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.security.Vault; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Vault extension implemented with GCP Secret Manager. + */ +public class GcpSecretManagerVault implements Vault { + + private final Monitor monitor; + private final String project; + private final String region; + private final SecretManagerServiceClient secretManagerServiceClient; + + private static final String LATEST_VERSION_ALIAS = "latest"; // alias for the latest version of a Secret + private static final int MAX_KEY_LENGTH = 255; // maximum Secret Manager key length + private static final int HASH_LENGTH = 8; // Object.hashCode() returns int, which is 32 bit, in hex results in 8 char + + /** Messages used for exception handling. */ + private static final String SECRET_NOT_FOUND_MSG = "Secret not found or has no version "; + private static final String RUNTIME_ERROR_MSG = "Runtime error "; + private static final String EXCEPTION_MSG = "Exception "; + private static final String SECRET_ALREADY_EXISTING_MSG = "Secret already exists "; + private static final String RESOLVE_SECRET_FUNCTION = "resolving secret"; + private static final String STORE_SECRET_FUNCTION = "storing secrect"; + private static final String DELETE_SECRET_FUNCTION = "deleting secrect"; + + + /** + * Factory helper constructing Vault object with default GCP credentials. + * + * @param monitor monitor object for logging. + * @param project GCP project name. + * @param region replica location for secrects created by the vault. + * @return the created Vault object backed by Secret Manager. + * @throws IOException if the creation of the Vault in GCP fails. + */ + public static GcpSecretManagerVault createWithDefaultSettings(Monitor monitor, String project, String region) throws IOException { + return new GcpSecretManagerVault(monitor, project, region, SecretManagerServiceClient.create()); + } + + /** + * Factory helper constructing Vault object with service account GCP credentials. + * + * @param monitor monitor object for logging. + * @param project GCP project name. + * @param region replica location for secrects created by the vault. + * @param credentialDataStream stream to service account credentials data. + * @return the created Vault object backed by Secret Manager. + * @throws IOException if the creation of the Vault in GCP fails. + */ + public static GcpSecretManagerVault createWithServiceAccountCredentials(Monitor monitor, String project, String region, InputStream credentialDataStream) throws IOException { + // TODO add proxy support. + var serviceAccountCredentials = ServiceAccountCredentials.fromStream(credentialDataStream); + var settings = SecretManagerServiceSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(serviceAccountCredentials)) + .build(); + return new GcpSecretManagerVault(monitor, project, region, SecretManagerServiceClient.create(settings)); + } + + /** + * Vault object constructor. + * + * @param monitor monitor object for logging. + * @param project GCP project name. + * @param region replica location for secrects created by the vault. + * @param secretClient GCP client to Secret Manager service (already authenticated). + */ + public GcpSecretManagerVault(Monitor monitor, String project, String region, SecretManagerServiceClient secretClient) { + this.monitor = monitor; + this.project = project; + this.region = region; + this.secretManagerServiceClient = secretClient; + } + + /** + * Retrieves the secret stored under a specific key. + * + * @param key string key identifying the secret to be fetched. + * @return the string if the secret is found, null otherwise. + */ + @Override + public @Nullable String resolveSecret(String key) { + try { + key = sanitizeKey(key); + var secretVersionName = SecretVersionName.of(project, key, LATEST_VERSION_ALIAS); + var response = secretManagerServiceClient.accessSecretVersion(secretVersionName); + String payload = response.getPayload().getData().toStringUtf8(); + return payload; + } catch (NotFoundException notFoundException) { + handleException(RESOLVE_SECRET_FUNCTION, SECRET_NOT_FOUND_MSG + key + ": ", notFoundException); + return null; + } catch (RuntimeException runtimeException) { + handleException(RESOLVE_SECRET_FUNCTION, RUNTIME_ERROR_MSG + key + ": ", runtimeException); + return null; + } catch (Exception exception) { + handleException(RESOLVE_SECRET_FUNCTION, EXCEPTION_MSG + key, exception); + return null; + } + } + + /** + * Saves a secret stored under a specific key. If the selected key is already in use by a secrect, error is + * returned and existing secret not overwritten. + * + * @param key string key identifying the secret to be stored. + * @param value string value of the secret. + * @return Result.success if the secret is stored. + */ + @Override + public Result storeSecret(String key, String value) { + try { + key = sanitizeKey(key); + var secret = + Secret.newBuilder() + .setReplication( + Replication.newBuilder() + .setUserManaged(Replication.UserManaged.newBuilder() + // TODO add multi-region replica support? + .addReplicas(Replication.UserManaged.Replica.newBuilder() + .setLocation(region) + .build()) + .build()) + .build()) + .build(); + + var parent = ProjectName.of(project); + var createdSecret = secretManagerServiceClient.createSecret(parent, key, secret); + var payload = SecretPayload.newBuilder().setData(ByteString.copyFromUtf8(value)).build(); + var addedVersion = secretManagerServiceClient.addSecretVersion(createdSecret.getName(), payload); + return Result.success(); + } catch (AlreadyExistsException alreadyExistsException) { + return handleException(STORE_SECRET_FUNCTION, SECRET_ALREADY_EXISTING_MSG + key, alreadyExistsException); + } catch (NotFoundException notFoundException) { + return handleException(STORE_SECRET_FUNCTION, SECRET_NOT_FOUND_MSG + key, notFoundException); + } catch (RuntimeException runtimeException) { + return handleException(STORE_SECRET_FUNCTION, RUNTIME_ERROR_MSG + key, runtimeException); + } catch (Exception exception) { + return handleException(STORE_SECRET_FUNCTION, EXCEPTION_MSG + key, exception); + } + } + + /** + * Deletes a secret stored under a specific key. If the selected key is not in use, error is returned. + * + * @param key string key identifying the secret to be deleted. + * @return Result.success if the secret is deleted. + */ + @Override + public Result deleteSecret(String key) { + try { + key = sanitizeKey(key); + var name = SecretName.of(project, key); + secretManagerServiceClient.deleteSecret(name); + return Result.success(); + } catch (NotFoundException notFoundException) { + return handleException(DELETE_SECRET_FUNCTION, SECRET_NOT_FOUND_MSG + key, notFoundException); + } catch (RuntimeException runtimeException) { + return handleException(DELETE_SECRET_FUNCTION, RUNTIME_ERROR_MSG + key, runtimeException); + } catch (Exception exception) { + return handleException(DELETE_SECRET_FUNCTION, EXCEPTION_MSG + key, exception); + } + } + + /** + * Checks if the given key parameter fits GCP requirements, if not it will: + * - any invalid character is replaced with dash '-' char. + * - if the key is too long it is truncated to max 255 chars. + * - if the key is changed in the process, original key is hashed into a 8-chars string appended at the end of + * the returned key. + * + * Secret key is a string with a maximum length of 255 characters and can contain uppercase and lowercase letters, + * digits, and the hyphen (`-`) and underscore (`_`) characters. + * + * @param key string key to be checked and sanitized. + * @return sanitized key. + */ + String sanitizeKey(String key) { + boolean modified = false; + var originalKey = key; + + var sb = new StringBuilder(); + for (int i = 0; i < key.length(); i++) { + var c = key.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '_' && c != '-') { + modified = true; + sb.append('-'); + } else { + sb.append(c); + } + + if (sb.length() > MAX_KEY_LENGTH || + (modified && sb.length() > (MAX_KEY_LENGTH - HASH_LENGTH - 1))) { + sb.setLength(MAX_KEY_LENGTH - HASH_LENGTH - 1); + modified = true; + i = key.length(); + } + } + + if (modified) { + var originalKeyHash = String.format("%08X", originalKey.hashCode()); + var fixedKey = sb.append('_').append(originalKeyHash).toString(); + monitor.warning("GCP Secret Manager vault sanitized the key, original:" + originalKey + " fixed:" + fixedKey); + return fixedKey; + } + + return key; + } + + private Result handleException(String function, String message, Exception exception) { + if (exception.getClass() == RuntimeException.class) { + monitor.severe(message, exception); + } else { + monitor.debug(message, exception); + } + return Result.failure("(" + function + ")" + message + ": " + exception.getMessage()); + } +} diff --git a/extensions/common/vault/vault-gcp/src/main/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultExtension.java b/extensions/common/vault/vault-gcp/src/main/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultExtension.java new file mode 100644 index 0000000..ad67124 --- /dev/null +++ b/extensions/common/vault/vault-gcp/src/main/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultExtension.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Google LCC - Initial implementation + * + */ + +package org.eclipse.edc.vault.gcp; + +import com.google.cloud.ServiceOptions; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.security.CertificateResolver; +import org.eclipse.edc.spi.security.PrivateKeyResolver; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.security.VaultCertificateResolver; +import org.eclipse.edc.spi.security.VaultPrivateKeyResolver; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static org.eclipse.edc.util.string.StringUtils.isNullOrEmpty; + +/** + * ServiceExtension instantiating and registering Vault object. + */ +@Provides({ Vault.class, PrivateKeyResolver.class, CertificateResolver.class }) +@Extension(value = GcpSecretManagerVaultExtension.NAME) +public class GcpSecretManagerVaultExtension implements ServiceExtension { + + public static final String NAME = "GCP Secret Manager"; + + @Setting(value = "GCP Project for Vault", required = false) + static final String VAULT_PROJECT = "edc.vault.gcp.project"; + + @Setting(value = "JSON file with Service Account credentials", required = false) + static final String VAULT_SACCOUNT_FILE = "edc.vault.gcp.saccount_file"; + + @Setting(value = "GCP Region for Vault Secret replication", required = true) + static final String VAULT_REGION = "edc.vault.gcp.region"; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var project = context.getSetting(VAULT_PROJECT, null); + if (isNullOrEmpty(project)) { + project = ServiceOptions.getDefaultProjectId(); + context.getMonitor().info("GCP Secret Manager vault extension: project loaded from default config " + project); + } else { + context.getMonitor().info("GCP Secret Manager vault extension: project loaded from settings " + project); + } + + var saccountFile = context.getSetting(VAULT_SACCOUNT_FILE, null); + + // TODO support multi-region replica. + var region = context.getConfig().getString(VAULT_REGION); + context.getMonitor().info("GCP Secret Manager vault extension: region selected " + region); + try { + GcpSecretManagerVault vault = null; + if (saccountFile == null) { + context.getMonitor().info("Creating GCP Secret Manager vault extension with default access settings"); + vault = GcpSecretManagerVault.createWithDefaultSettings(context.getMonitor(), project, region); + } else { + context.getMonitor().info("Creating GCP Secret Manager vault extension with Service Account credentials from file " + saccountFile); + var credentialDataStream = Files.newInputStream(Paths.get(saccountFile)); + vault = GcpSecretManagerVault.createWithServiceAccountCredentials(context.getMonitor(), project, region, credentialDataStream); + credentialDataStream.close(); + } + context.registerService(Vault.class, vault); + context.registerService(PrivateKeyResolver.class, new VaultPrivateKeyResolver(vault)); + context.registerService(CertificateResolver.class, new VaultCertificateResolver(vault)); + } catch (IOException ioException) { + throw new EdcException("Cannot create vault", ioException); + } + } +} diff --git a/extensions/common/vault/vault-gcp/src/test/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultExtensionTest.java b/extensions/common/vault/vault-gcp/src/test/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultExtensionTest.java new file mode 100644 index 0000000..5bbfede --- /dev/null +++ b/extensions/common/vault/vault-gcp/src/test/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultExtensionTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Google LCC - Initial implementation + * + */ + +package org.eclipse.edc.vault.gcp; + +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.ConfigFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +class GcpSecretManagerVaultExtensionTest { + + private final Monitor monitor = mock(Monitor.class); + private final GcpSecretManagerVaultExtension extension = new GcpSecretManagerVaultExtension(); + + private static final String TEST_REGION = "europe-west3"; + private static final String TEST_PROJECT = "project"; + private static final String TEST_FILE_PREFIX = "file"; + private static final String TEST_FILE_SUFFIX = ".json"; + + @BeforeEach + void resetMocks() { + reset(monitor); + } + + @Test + void noSettings_shouldThrowException() { + ServiceExtensionContext invalidContext = mock(ServiceExtensionContext.class); + when(invalidContext.getMonitor()).thenReturn(monitor); + when(invalidContext.getConfig()).thenReturn(ConfigFactory.empty()); + + EdcException exception = assertThrows(EdcException.class, () -> extension.initialize(invalidContext)); + assertEquals("No setting found for key edc.vault.gcp.region", exception.getMessage()); + } + + @Test + void onlyProjectSetting_shouldThrowException() { + ServiceExtensionContext invalidContext = mock(ServiceExtensionContext.class); + when(invalidContext.getMonitor()).thenReturn(monitor); + var settings = new HashMap(); + settings.put(GcpSecretManagerVaultExtension.VAULT_PROJECT, TEST_PROJECT); + when(invalidContext.getConfig()).thenReturn(ConfigFactory.fromMap(settings)); + + EdcException exception = assertThrows(EdcException.class, () -> extension.initialize(invalidContext)); + assertEquals("No setting found for key edc.vault.gcp.region", exception.getMessage()); + } + + @Test + void onlyRegionSetting_shouldNotThrowException() { + ServiceExtensionContext validContext = mock(ServiceExtensionContext.class); + when(validContext.getMonitor()).thenReturn(monitor); + var settings = new HashMap(); + settings.put(GcpSecretManagerVaultExtension.VAULT_REGION, TEST_REGION); + when(validContext.getConfig()).thenReturn(ConfigFactory.fromMap(settings)); + + try (MockedStatic utilities = Mockito.mockStatic(GcpSecretManagerVault.class)) { + utilities.when(() -> GcpSecretManagerVault.createWithDefaultSettings(monitor, TEST_PROJECT, TEST_REGION)) + .thenReturn(new GcpSecretManagerVault(null, null, null, null)); + extension.initialize(validContext); + } + } + + @Test + void mandatorySettings_shouldNotThrowException() { + ServiceExtensionContext validContext = mock(ServiceExtensionContext.class); + when(validContext.getMonitor()).thenReturn(monitor); + var settings = new HashMap(); + settings.put(GcpSecretManagerVaultExtension.VAULT_PROJECT, TEST_PROJECT); + settings.put(GcpSecretManagerVaultExtension.VAULT_REGION, TEST_REGION); + when(validContext.getConfig()).thenReturn(ConfigFactory.fromMap(settings)); + + try (MockedStatic utilities = Mockito.mockStatic(GcpSecretManagerVault.class)) { + utilities.when(() -> GcpSecretManagerVault.createWithDefaultSettings(monitor, TEST_PROJECT, TEST_REGION)) + .thenReturn(new GcpSecretManagerVault(null, null, null, null)); + extension.initialize(validContext); + } + } + + @Test + void mandatorySettingsWithServiceAccount_shouldNotThrowException() { + try { + var tempPath = Files.createTempFile(TEST_FILE_PREFIX, TEST_FILE_SUFFIX); + var accountFilePath = tempPath.toString(); + Files.write(tempPath, ("test account data").getBytes()); + ServiceExtensionContext validContext = mock(ServiceExtensionContext.class); + when(validContext.getMonitor()).thenReturn(monitor); + var settings = new HashMap(); + settings.put(GcpSecretManagerVaultExtension.VAULT_PROJECT, TEST_PROJECT); + settings.put(GcpSecretManagerVaultExtension.VAULT_REGION, TEST_REGION); + settings.put(GcpSecretManagerVaultExtension.VAULT_SACCOUNT_FILE, accountFilePath); + when(validContext.getConfig()).thenReturn(ConfigFactory.fromMap(settings)); + + try (MockedStatic utilities = Mockito.mockStatic(GcpSecretManagerVault.class)) { + utilities.when(() -> GcpSecretManagerVault.createWithServiceAccountCredentials(eq(monitor), eq(TEST_PROJECT), eq(TEST_REGION), Mockito.any(InputStream.class))) + .thenReturn(new GcpSecretManagerVault(null, null, null, null)); + extension.initialize(validContext); + } + } catch (IOException ioException) { + fail("Cannot create temporary file for testing"); + } + } +} diff --git a/extensions/common/vault/vault-gcp/src/test/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultTest.java b/extensions/common/vault/vault-gcp/src/test/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultTest.java new file mode 100644 index 0000000..0ade6f5 --- /dev/null +++ b/extensions/common/vault/vault-gcp/src/test/java/org/eclipse/edc/vault/gcp/GcpSecretManagerVaultTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2023 Google LLC + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Google LCC - Initial implementation + * + */ + +package org.eclipse.edc.vault.gcp; + +import com.google.api.gax.rpc.NotFoundException; +import com.google.api.gax.rpc.StatusCode; +import com.google.cloud.secretmanager.v1.ProjectName; +import com.google.cloud.secretmanager.v1.Replication; +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretName; +import com.google.cloud.secretmanager.v1.SecretVersionName; +import org.eclipse.edc.spi.monitor.Monitor; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.ArrayList; +import java.util.Random; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.TestInstance.Lifecycle; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@TestInstance(Lifecycle.PER_CLASS) +class GcpSecretManagerVaultTest { + private static final String TEST_REGION = "europe-west3"; + private static final String TEST_PROJECT = "project"; + private static final String VALID_KEY = "test"; + private static final String INVALID_KEY = "test="; + private static final String SANITIZED_KEY = "test-_06924DEB"; + + private static final int MAX_KEY_LENGTH = 255; + + private Monitor monitor; + private SecretManagerServiceClient secretClient; + private GcpSecretManagerVault vault; + private final Secret testSecret = + Secret.newBuilder() + .setReplication( + Replication.newBuilder() + .setUserManaged(Replication.UserManaged.newBuilder() + .addReplicas(Replication.UserManaged.Replica.newBuilder() + .setLocation(TEST_REGION) + .build()) + .build()) + .build()) + .build(); + + private final ArrayList validChars = new ArrayList(); + private final ArrayList invalidChars = new ArrayList(); + private final Random randGen = new Random(); + + @BeforeAll + void init() { + // Init the array with the chars allowed and not allowed in a Secret Key. + // Secret key is a string with a maximum length of 255 characters and can + // contain uppercase and lowercase letters, digits, and the hyphen (`-`) and + // underscore (`_`) characters. + for (char c = 'a'; c <= 'z'; c++) { + validChars.add(c); + } + for (char c = 'A'; c <= 'Z'; c++) { + validChars.add(c); + } + for (char c = '0'; c <= '9'; c++) { + validChars.add(c); + } + validChars.add('-'); + validChars.add('_'); + + for (char c = 32; c <= 126; c++) { + if (!Character.isLetterOrDigit(c) && c != '_' && c != '-') { + invalidChars.add(c); + } + } + + randGen.setSeed(System.currentTimeMillis()); + } + + @BeforeEach + void instantiateMocksAndVault() { + monitor = mock(); + secretClient = mock(); + vault = new GcpSecretManagerVault(monitor, TEST_PROJECT, TEST_REGION, secretClient); + } + + @Test + void storeSecret_shallSanitizeKey() { + var parent = ProjectName.of(TEST_PROJECT); + when(secretClient.createSecret(parent, SANITIZED_KEY, testSecret)) + .thenReturn(testSecret); + + var randomContent = UUID.randomUUID().toString(); + vault.storeSecret(INVALID_KEY, randomContent); + verify(secretClient).createSecret(parent, SANITIZED_KEY, testSecret); + } + + @Test + void resolveSecret_shallSanitizeKey() { + vault.resolveSecret(INVALID_KEY); + + var secretVersionName = SecretVersionName.of(TEST_PROJECT, SANITIZED_KEY, "latest"); + verify(secretClient).accessSecretVersion(secretVersionName); + } + + @Test + void deleteSecret_shallSanitizeKey() { + vault.deleteSecret(INVALID_KEY); + + verify(secretClient).deleteSecret(SecretName.of(TEST_PROJECT, SANITIZED_KEY)); + } + + @Test + void sanitizeKey_testValidKeys() { + for (int i = 0; i < 100; i++) { + int len = 1 + randGen.nextInt(MAX_KEY_LENGTH); + var validKey = getRandomKeyWithValidChars(len); + var sanitizedKey = vault.sanitizeKey(validKey); + assertThat(validKey.equals(sanitizedKey)).isTrue(); + assertThat(isValidKey(sanitizedKey)).isTrue(); + } + } + + @Test + void sanitizeKey_testLongKeyWithValidCharKey() { + for (int i = 0; i < 100; i++) { + int len = MAX_KEY_LENGTH + 1 + randGen.nextInt(MAX_KEY_LENGTH); + var longKey = getRandomKeyWithValidChars(len); + var sanitizedKey = vault.sanitizeKey(longKey); + assertThat(isValidKey(longKey)).isFalse(); + assertThat(isValidKey(sanitizedKey)).isTrue(); + } + } + + @Test + void sanitizeKey_testLongKeyWithInvalidCharKey() { + for (int i = 0; i < 100; i++) { + int len = MAX_KEY_LENGTH + 1 + randGen.nextInt(MAX_KEY_LENGTH); + int invCharCount = 1 + randGen.nextInt(len); + var longInvalidKey = getRandomKeyWithInvalidChars(len, invCharCount); + var sanitizedKey = vault.sanitizeKey(longInvalidKey); + assertThat(isValidKey(longInvalidKey)).isFalse(); + assertThat(isValidKey(sanitizedKey)).isTrue(); + } + } + + @Test + void sanitizeKey_longKeysDifferingAfter255CharsStillDifferent() { + for (int i = 0; i < 100; i++) { + int len = MAX_KEY_LENGTH + 1 + randGen.nextInt(MAX_KEY_LENGTH); + var longKey = getRandomKeyWithValidChars(len); + int extraLen = len - MAX_KEY_LENGTH; + int diffPosition = MAX_KEY_LENGTH + randGen.nextInt(extraLen); + var sb = new StringBuilder(longKey); + char c = sb.charAt(diffPosition); + do { + c = (char) (c + 1); + } while (!validChars.contains(c)); + + sb.setCharAt(diffPosition, (char) (c + 1)); + var longDiffKey = sb.toString(); + + var sanitizedKey = vault.sanitizeKey(longKey); + var sanitizedDiffKey = vault.sanitizeKey(longDiffKey); + assertThat(sanitizedKey.equals(sanitizedDiffKey)).isFalse(); + assertThat(isValidKey(sanitizedKey)).isTrue(); + assertThat(isValidKey(sanitizedDiffKey)).isTrue(); + } + } + + @Test + void resolveSecret_shallReturnNullAndLogDebugIfSecretNotFound() { + var status = new TestStatusCode(); + when(secretClient.accessSecretVersion(isA(SecretVersionName.class))) + .thenThrow(new NotFoundException("test", new Exception("test"), status, false)); + var result = vault.resolveSecret(VALID_KEY); + + assertThat(result).isNull(); + verify(monitor).debug(anyString(), isA(NotFoundException.class)); + } + + @Test + void resolveSecret_shallReturnNullAndLogSevereOnGenericException() { + when(secretClient.accessSecretVersion(isA(SecretVersionName.class))) + .thenThrow(new RuntimeException("test")); + + var result = vault.resolveSecret(VALID_KEY); + + assertThat(result).isNull(); + verify(monitor).severe(anyString(), isA(RuntimeException.class)); + } + + private boolean isValidKey(String key) { + if (key.length() > MAX_KEY_LENGTH) { + return false; + } + + for (int i = 0; i < key.length(); i++) { + var c = key.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '_' && c != '-') { + return false; + } + } + + return true; + } + + private char getRandomValidChar() { + return validChars.get(randGen.nextInt(validChars.size())); + } + + private char getRandomInvalidChar() { + return invalidChars.get(randGen.nextInt(invalidChars.size())); + } + + private String getRandomKeyWithValidChars(int length) { + var sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + var c = getRandomValidChar(); + sb.append(c); + } + return sb.toString(); + } + + private String getRandomKeyWithInvalidChars(int length, int errors) { + var key = getRandomKeyWithValidChars(length); + var keyBuilder = new StringBuilder(key); + ArrayList positions = new ArrayList(); + for (int i = 0; i < length; i++) { + positions.add(i); + } + + for (int i = 0; i < errors && positions.size() > 0; i++) { + int index = randGen.nextInt(positions.size()); + int position = positions.get(index); + positions.remove(index); + + keyBuilder.setCharAt(position, getRandomInvalidChar()); + } + + return keyBuilder.toString(); + } + + private class TestStatusCode implements StatusCode { + @Override + public Integer getTransportCode() { + return Integer.valueOf(0); + } + + @Override + public Code getCode() { + return Code.OK; + } + } +} + diff --git a/extensions/data-plane/data-plane-google-storage/build.gradle.kts b/extensions/data-plane/data-plane-google-storage/build.gradle.kts index 885aec8..0e7cf19 100644 --- a/extensions/data-plane/data-plane-google-storage/build.gradle.kts +++ b/extensions/data-plane/data-plane-google-storage/build.gradle.kts @@ -20,12 +20,10 @@ dependencies { api(libs.edc.spi.dataplane) implementation(libs.edc.util) implementation(project(":extensions:common:gcp:gcp-core")) - implementation(libs.edc.core.dataPlane.util) + implementation(libs.edc.core.dataplane.util) implementation(libs.googlecloud.storage) testImplementation(libs.edc.core.dataplane) testImplementation(libs.edc.junit) } - - diff --git a/gradle.properties b/gradle.properties index b2b213d..ce8363d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,5 +7,5 @@ edcGradlePluginsVersion=0.1.4-SNAPSHOT metaModelVersion=0.1.4-SNAPSHOT # used for publishing artifacts and plugins -techGcpScmConnection=scm:git:git@github.com:eclipse-edc/Technology-Aws.git -techGcpScmUrl=https://github.com/eclipse-edc/Technology-Aws.git +techGcpScmConnection=scm:git:git@github.com:eclipse-edc/Technology-Gcp.git +techGcpScmUrl=https://github.com/eclipse-edc/Technology-Gcp.git diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 327c5a5..7b26f8d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,36 +2,38 @@ format.version = "1.1" [versions] +edc = "0.1.4-SNAPSHOT" +failsafe = "3.3.1" googleCloudIamAdmin = "3.14.0" googleCloudIamCredentials = "2.18.0" googleCloudStorage = "2.22.4" -edc = "0.1.4-SNAPSHOT" -mockito = "5.2.0" -failsafe = "3.3.1" - +googleCloudSecretManager = "2.20.0" +googleCloudCore = "2.20.0" [libraries] assertj = { module = "org.assertj:assertj-core", version = "3.24.2" } -edc-core-dataPlane-util ={ module = "org.eclipse.edc:data-plane-util", version.ref = "edc" } -edc-core-dataplane ={ module = "org.eclipse.edc:data-plane-core", version.ref = "edc" } +edc-core-dataplane = { module = "org.eclipse.edc:data-plane-core", version.ref = "edc" } +edc-core-dataplane-util = { module = "org.eclipse.edc:data-plane-util", version.ref = "edc" } +edc-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } edc-spi-catalog = { module = "org.eclipse.edc:catalog-spi", version.ref = "edc" } edc-spi-contract = { module = "org.eclipse.edc:contract-spi", version.ref = "edc" } -edc-spi-policy = { module = "org.eclipse.edc:policy-spi", version.ref = "edc" } edc-spi-core = { module = "org.eclipse.edc:core-spi", version.ref = "edc" } +edc-spi-dataplane = { module = "org.eclipse.edc:data-plane-spi", version.ref = "edc" } +edc-spi-policy = { module = "org.eclipse.edc:policy-spi", version.ref = "edc" } edc-spi-transfer = { module = "org.eclipse.edc:transfer-spi", version.ref = "edc" } edc-spi-web = { module = "org.eclipse.edc:web-spi", version.ref = "edc" } -edc-spi-dataplane = { module = "org.eclipse.edc:data-plane-spi", version.ref = "edc" } edc-transfer-httppull-receiver-dynamic = { module = "org.eclipse.edc:transfer-pull-http-dynamic-receiver", version.ref = "edc" } edc-util = { module = "org.eclipse.edc:util", version.ref = "edc" } -edc-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } # third-party dependencies failsafe-core = { module = "dev.failsafe:failsafe", version.ref = "failsafe" } failsafe-okhttp = { module = "dev.failsafe:failsafe-okhttp", version.ref = "failsafe" } # Google dependencies +googlecloud-core = { module = "com.google.cloud:google-cloud-core", version.ref = "googleCloudCore"} googlecloud-iam-admin = { module = "com.google.cloud:google-iam-admin", version.ref = "googleCloudIamAdmin" } googlecloud-iam-credentials = { module = "com.google.cloud:google-cloud-iamcredentials", version.ref = "googleCloudIamCredentials" } +googlecloud-secretmanager = { module = "com.google.cloud:google-cloud-secretmanager", version.ref = "googleCloudSecretManager"} googlecloud-storage = { module = "com.google.cloud:google-cloud-storage", version.ref = "googleCloudStorage" } [bundles] diff --git a/settings.gradle.kts b/settings.gradle.kts index 14f2423..0d27fc8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,12 +36,13 @@ dependencyResolutionManagement { } } -// common modules +// common extensions include(":extensions:common:gcp:gcp-core") +include(":extensions:common:vault:vault-gcp") -// control plane modules +// control plane extensions include(":extensions:control-plane:provision:provision-gcs") -// data plane +// data plane extensions include(":extensions:data-plane:data-plane-google-storage") include(":version-catalog")