diff --git a/DEPENDENCIES b/DEPENDENCIES index e135f731..d7e64850 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,19 +1,18 @@ -maven/mavencentral/com.apicatalog/carbon-did/0.0.2, Apache-2.0, approved, #9239 -maven/mavencentral/com.apicatalog/iron-verifiable-credentials/0.8.1, Apache-2.0, approved, #9234 +maven/mavencentral/com.apicatalog/carbon-did/0.3.0, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.apicatalog/copper-multibase/0.5.0, Apache-2.0, approved, #14501 +maven/mavencentral/com.apicatalog/copper-multicodec/0.1.1, Apache-2.0, approved, #14500 +maven/mavencentral/com.apicatalog/iron-verifiable-credentials/0.14.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.apicatalog/titanium-json-ld/1.0.0, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.apicatalog/titanium-json-ld/1.3.1, Apache-2.0, approved, #8912 +maven/mavencentral/com.apicatalog/titanium-json-ld/1.4.0, Apache-2.0, approved, #13683 maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.10.3, Apache-2.0, approved, CQ21280 -maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.15.3, Apache-2.0, approved, #7947 maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.16.1, Apache-2.0, approved, #11606 maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.17.0, Apache-2.0, approved, #13672 maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.16.1, Apache-2.0 AND MIT, approved, #11602 maven/mavencentral/com.fasterxml.jackson.core/jackson-core/2.17.0, , approved, #13665 -maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.15.3, Apache-2.0, approved, #7934 maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.16.1, Apache-2.0, approved, #11605 maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.17.0, Apache-2.0, approved, #13671 maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.16.1, Apache-2.0, approved, #11853 maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.17.0, Apache-2.0, approved, #14160 -maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.15.3, Apache-2.0, approved, #9241 maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.17.0, Apache-2.0, approved, #13668 maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.17.0, Apache-2.0, approved, #14162 maven/mavencentral/com.github.docker-java/docker-java-api/3.3.6, Apache-2.0, approved, #10346 @@ -72,7 +71,7 @@ maven/mavencentral/com.google.cloud/libraries-bom/26.33.0, Apache-2.0, approved, 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.crypto.tink/tink/1.12.0, Apache-2.0, approved, #12041 +maven/mavencentral/com.google.crypto.tink/tink/1.13.0, Apache-2.0, approved, #14502 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.21.1, Apache-2.0, approved, #9834 maven/mavencentral/com.google.errorprone/error_prone_annotations/2.22.0, Apache-2.0, approved, #10661 @@ -101,11 +100,11 @@ maven/mavencentral/com.google.http-client/google-http-client/1.44.1, Apache-2.0, maven/mavencentral/com.google.j2objc/j2objc-annotations/2.8, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.oauth-client/google-oauth-client/1.35.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.protobuf/protobuf-java-util/3.25.2, BSD-3-Clause, approved, clearlydefined -maven/mavencentral/com.google.protobuf/protobuf-java/3.24.3, BSD-3-Clause, approved, clearlydefined +maven/mavencentral/com.google.protobuf/protobuf-java/3.25.1, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.protobuf/protobuf-java/3.25.2, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.re2j/re2j/1.7, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37.3, Apache-2.0, approved, #11701 -maven/mavencentral/com.puppycrawl.tools/checkstyle/10.14.2, LGPL-2.1-or-later AND (Apache-2.0 AND LGPL-2.1-or-later) AND Apache-2.0, approved, #13562 +maven/mavencentral/com.puppycrawl.tools/checkstyle/10.15.0, LGPL-2.1-or-later, restricted, clearlydefined maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159 maven/mavencentral/com.squareup.okhttp3/okhttp/4.12.0, Apache-2.0, approved, #11156 maven/mavencentral/com.squareup.okhttp3/okhttp/4.9.3, Apache-2.0 AND MPL-2.0, approved, #3225 @@ -150,13 +149,13 @@ maven/mavencentral/io.opentelemetry/opentelemetry-api/1.32.0, Apache-2.0, approv maven/mavencentral/io.opentelemetry/opentelemetry-context/1.32.0, Apache-2.0, approved, #11683 maven/mavencentral/io.perfmark/perfmark-api/0.27.0, Apache-2.0, approved, clearlydefined maven/mavencentral/io.setl/rdf-urdna/1.1, 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.activation/jakarta.activation-api/2.1.3, 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 maven/mavencentral/jakarta.inject/jakarta.inject-api/2.0.1, Apache-2.0, approved, ee4j.cdi maven/mavencentral/jakarta.transaction/jakarta.transaction-api/2.0.0, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jta maven/mavencentral/jakarta.validation/jakarta.validation-api/3.0.2, Apache-2.0, approved, ee4j.validation maven/mavencentral/jakarta.ws.rs/jakarta.ws.rs-api/3.1.0, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.rest -maven/mavencentral/jakarta.xml.bind/jakarta.xml.bind-api/4.0.0, BSD-3-Clause, approved, ee4j.jaxb +maven/mavencentral/jakarta.xml.bind/jakarta.xml.bind-api/4.0.2, BSD-3-Clause, approved, ee4j.jaxb maven/mavencentral/javax.annotation/javax.annotation-api/1.3.2, CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, CQ16910 maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.12, Apache-2.0, approved, #7164 @@ -189,9 +188,9 @@ maven/mavencentral/org.apache.maven.doxia/doxia-sink-api/1.12.0, Apache-2.0, app maven/mavencentral/org.apache.xbean/xbean-reflect/3.7, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.assertj/assertj-core/3.25.3, Apache-2.0, approved, #12585 -maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.78, MIT, approved, #14235 -maven/mavencentral/org.bouncycastle/bcprov-jdk18on/1.78, MIT AND CC0-1.0, approved, #14237 -maven/mavencentral/org.bouncycastle/bcutil-jdk18on/1.78, MIT, approved, #14238 +maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.78, MIT, approved, #14434 +maven/mavencentral/org.bouncycastle/bcprov-jdk18on/1.78, MIT AND CC0-1.0, approved, #14433 +maven/mavencentral/org.bouncycastle/bcutil-jdk18on/1.78, MIT, approved, #14435 maven/mavencentral/org.checkerframework/checker-compat-qual/2.5.6, GPL-2.0-only with Classpath-Exception-2.0, approved, #11598 maven/mavencentral/org.checkerframework/checker-qual/3.37.0, MIT, approved, clearlydefined maven/mavencentral/org.checkerframework/checker-qual/3.42.0, MIT, approved, clearlydefined @@ -264,20 +263,20 @@ maven/mavencentral/org.eclipse.jetty/jetty-servlet/11.0.20, EPL-2.0 OR Apache-2. maven/mavencentral/org.eclipse.jetty/jetty-util/11.0.20, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty/jetty-webapp/11.0.20, EPL-2.0 OR Apache-2.0, approved, rt.jetty maven/mavencentral/org.eclipse.jetty/jetty-xml/11.0.20, EPL-2.0 OR Apache-2.0, approved, rt.jetty -maven/mavencentral/org.glassfish.hk2.external/aopalliance-repackaged/3.0.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish -maven/mavencentral/org.glassfish.hk2/hk2-api/3.0.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish -maven/mavencentral/org.glassfish.hk2/hk2-locator/3.0.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish -maven/mavencentral/org.glassfish.hk2/hk2-utils/3.0.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish -maven/mavencentral/org.glassfish.hk2/osgi-resource-locator/1.0.3, CDDL-1.0, approved, CQ10889 -maven/mavencentral/org.glassfish.jersey.containers/jersey-container-servlet-core/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey -maven/mavencentral/org.glassfish.jersey.containers/jersey-container-servlet/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey -maven/mavencentral/org.glassfish.jersey.core/jersey-client/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey -maven/mavencentral/org.glassfish.jersey.core/jersey-common/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey -maven/mavencentral/org.glassfish.jersey.core/jersey-server/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey -maven/mavencentral/org.glassfish.jersey.ext/jersey-entity-filtering/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey -maven/mavencentral/org.glassfish.jersey.inject/jersey-hk2/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey -maven/mavencentral/org.glassfish.jersey.media/jersey-media-json-jackson/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey -maven/mavencentral/org.glassfish.jersey.media/jersey-media-multipart/3.1.5, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.hk2.external/aopalliance-repackaged/3.0.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish +maven/mavencentral/org.glassfish.hk2/hk2-api/3.0.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish +maven/mavencentral/org.glassfish.hk2/hk2-locator/3.0.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish +maven/mavencentral/org.glassfish.hk2/hk2-utils/3.0.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish +maven/mavencentral/org.glassfish.hk2/osgi-resource-locator/1.0.3, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.glassfish +maven/mavencentral/org.glassfish.jersey.containers/jersey-container-servlet-core/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.jersey.containers/jersey-container-servlet/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.jersey.core/jersey-client/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.jersey.core/jersey-common/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.jersey.core/jersey-server/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.jersey.ext/jersey-entity-filtering/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.jersey.inject/jersey-hk2/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.jersey.media/jersey-media-json-jackson/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey +maven/mavencentral/org.glassfish.jersey.media/jersey-media-multipart/3.1.6, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jersey maven/mavencentral/org.glassfish/jakarta.json/2.0.1, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jsonp maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ11429 maven/mavencentral/org.jacoco/org.jacoco.agent/0.8.9, EPL-2.0, approved, CQ23285 @@ -285,7 +284,7 @@ maven/mavencentral/org.jacoco/org.jacoco.ant/0.8.9, EPL-2.0, approved, #1068 maven/mavencentral/org.jacoco/org.jacoco.core/0.8.9, EPL-2.0, approved, CQ23283 maven/mavencentral/org.jacoco/org.jacoco.report/0.8.9, EPL-2.0 AND Apache-2.0, approved, CQ23284 maven/mavencentral/org.javassist/javassist/3.28.0-GA, Apache-2.0 OR LGPL-2.1-or-later OR MPL-1.1, approved, #327 -maven/mavencentral/org.javassist/javassist/3.29.2-GA, Apache-2.0 AND LGPL-2.1-or-later AND MPL-1.1, approved, #6023 +maven/mavencentral/org.javassist/javassist/3.30.2-GA, Apache-2.0 AND LGPL-2.1-or-later AND MPL-1.1, approved, #12108 maven/mavencentral/org.jetbrains.kotlin/kotlin-stdlib-common/1.9.10, Apache-2.0, approved, #14186 maven/mavencentral/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.9.10, Apache-2.0, approved, #14193 maven/mavencentral/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.8.21, Apache-2.0, approved, #8919 diff --git a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/bigquery/service/BigQueryFactoryImpl.java b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/bigquery/service/BigQueryFactoryImpl.java index 4541a05f..319ce9e3 100644 --- a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/bigquery/service/BigQueryFactoryImpl.java +++ b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/bigquery/service/BigQueryFactoryImpl.java @@ -14,47 +14,26 @@ package org.eclipse.edc.gcp.bigquery.service; -import com.google.api.services.iam.v2.IamScopes; import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.ImpersonatedCredentials; import com.google.cloud.bigquery.BigQuery; import com.google.cloud.bigquery.BigQueryOptions; import org.eclipse.edc.gcp.common.GcpConfiguration; import org.eclipse.edc.gcp.common.GcpServiceAccount; import org.eclipse.edc.gcp.iam.IamService; -import org.eclipse.edc.spi.monitor.Monitor; - -import java.io.IOException; -import java.util.Arrays; public class BigQueryFactoryImpl implements BigQueryFactory { private final GcpConfiguration gcpConfiguration; - private final Monitor monitor; + private final IamService iamService; - public BigQueryFactoryImpl(GcpConfiguration gcpConfiguration, Monitor monitor) { + public BigQueryFactoryImpl(GcpConfiguration gcpConfiguration, IamService iamService) { this.gcpConfiguration = gcpConfiguration; - this.monitor = monitor; + this.iamService = iamService; } @Override - public BigQuery createBigQuery(GcpServiceAccount serviceAccount) throws IOException { - var credentials = GoogleCredentials.getApplicationDefault() - .createScoped(IamScopes.CLOUD_PLATFORM); - credentials.refreshIfExpired(); - - if (!serviceAccount.equals(IamService.ADC_SERVICE_ACCOUNT)) { - monitor.debug("BigQuery Service for project '" + gcpConfiguration.projectId() + - "' using service account '" + serviceAccount.getName() + "'"); - credentials = ImpersonatedCredentials.create( - credentials, - serviceAccount.getEmail(), - null, - Arrays.asList("https://www.googleapis.com/auth/bigquery"), - 3600); - } else { - monitor.warning("BigQuery Service for project '" + gcpConfiguration.projectId() + "' using ADC, NOT RECOMMENDED"); - } - + public BigQuery createBigQuery(GcpServiceAccount serviceAccount) { + var credentials = iamService.getCredentials(serviceAccount, + IamService.BQ_SCOPE); return createBigQuery(credentials); } diff --git a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/AccessTokenProvider.java b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/AccessTokenProvider.java deleted file mode 100644 index 9b0ca813..00000000 --- a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/AccessTokenProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2024 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 LLC - Initial implementation - * - */ - -package org.eclipse.edc.gcp.iam; - -import org.eclipse.edc.gcp.common.GcpAccessToken; - -/** - * Interface for credentials providing access tokens. - */ -public interface AccessTokenProvider { - /** - * Returns the access token generated for the credentials. - * - * @return the {@link GcpAccessToken} for the credentials, null if error occurs. - */ - GcpAccessToken getAccessToken(); -} diff --git a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/CredentialsManager.java b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/CredentialsManager.java new file mode 100644 index 00000000..7e3f778b --- /dev/null +++ b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/CredentialsManager.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 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 LLC - Initial implementation + * + */ + +package org.eclipse.edc.gcp.iam; + +import com.google.auth.oauth2.GoogleCredentials; +import org.eclipse.edc.gcp.common.GcpServiceAccount; + +/** + * Interface for credentials providing access tokens. + */ +public interface CredentialsManager { + /** + * Returns the default credentials. + * + * @return the {@link GoogleCredentials}. + */ + GoogleCredentials getApplicationDefaultCredentials(); + + /** + * Refresh the credentials if needed. + * + * @param credentials the credentials to be refreshed. + */ + void refreshCredentials(GoogleCredentials credentials); + + /** + * Returns the impersonated credentials. + * + * @param sourceCredentials the source credentials to start for impersonation. + * @param serviceAccount the service account to be impersonated. + * @param lifeTime lifetime of the credentials in seconds. + * @param scopes the list of scopes to be added to the credentials. + * @return the impersonated {@link GoogleCredentials}. + */ + GoogleCredentials createImpersonated(GoogleCredentials sourceCredentials, GcpServiceAccount serviceAccount, int lifeTime, String... scopes); +} diff --git a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/DefaultCredentialsManager.java b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/DefaultCredentialsManager.java new file mode 100644 index 00000000..3d83fd0a --- /dev/null +++ b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/DefaultCredentialsManager.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 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 LLC - Initial implementation + * + */ + +package org.eclipse.edc.gcp.iam; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ImpersonatedCredentials; +import org.eclipse.edc.gcp.common.GcpException; +import org.eclipse.edc.gcp.common.GcpServiceAccount; +import org.eclipse.edc.spi.monitor.Monitor; + +import java.io.IOException; +import java.util.Arrays; + +/** + * The DefaultCredentialsManager class provides the implementation of the CredentialsManager + * interface by means of the standard GCP API to fetch application-default credentials, refresh + * credentials, and impersonate service accounts. + */ +record DefaultCredentialsManager(Monitor monitor) implements CredentialsManager { + @Override + public GoogleCredentials getApplicationDefaultCredentials() { + try { + return GoogleCredentials.getApplicationDefault(); + } catch (IOException ioException) { + monitor.severe("Cannot get application default credentials", ioException); + throw new GcpException(ioException); + } + } + + @Override + public void refreshCredentials(GoogleCredentials credentials) { + try { + credentials.refreshIfExpired(); + } catch (IOException ioException) { + monitor.severe("Cannot get refresh the credentials", ioException); + throw new GcpException(ioException); + } + } + + @Override + public GoogleCredentials createImpersonated(GoogleCredentials sourceCredentials, GcpServiceAccount serviceAccount, int lifeTime, String... scopes) { + return ImpersonatedCredentials.create( + sourceCredentials, + serviceAccount.getEmail(), + null, + Arrays.asList(scopes), + lifeTime); + } +} diff --git a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamService.java b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamService.java index ed487ee2..259134b2 100644 --- a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamService.java +++ b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamService.java @@ -15,6 +15,7 @@ package org.eclipse.edc.gcp.iam; +import com.google.auth.oauth2.GoogleCredentials; import org.eclipse.edc.gcp.common.GcpAccessToken; import org.eclipse.edc.gcp.common.GcpServiceAccount; @@ -22,7 +23,22 @@ * Wrapper around GCP IAM-API for decoupling. */ public interface IamService { + /** + * Service account representing Application Default Credentials. + */ GcpServiceAccount ADC_SERVICE_ACCOUNT = new GcpServiceAccount("adc-email", "adc-name", "application default"); + /** + * OAUTH2 scope for BigQuery access. + */ + String BQ_SCOPE = "https://www.googleapis.com/auth/bigquery"; + /** + * OAUTH2 scope for GCS read/write access. + */ + String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.read_write"; + /** + * OAUTH2 scope for IAM access, used to then impersonate a service account. + */ + String IAM_SCOPE = "https://www.googleapis.com/auth/iam"; /** * Returns the existing service account with the matching name. @@ -33,11 +49,31 @@ public interface IamService { GcpServiceAccount getServiceAccount(String serviceAccountName); /** - * Creates a temporary valid OAunth2.0 access token for the service account + * Creates a temporary valid OAuth2.0 access token for the service account * * @param serviceAccount service account the token should be created for; * if ADC_SERVICE_ACCOUNT, access token from ADC is created. + * @param scopes list of scopes to be requested for the access token, see + * https://developers.google.com/identity/protocols/oauth2/scopes * @return {@link GcpAccessToken} */ - GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount); -} \ No newline at end of file + GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount, String... scopes); + + /** + * Generates the credentials from a temporary valid OAuth2.0 access token + * + * @param accessToken token created by e.g. provisioner + * @return {@link GoogleCredentials} corresponding to the given access token + */ + GoogleCredentials getCredentials(GcpAccessToken accessToken); + + /** + * Generates the credentials for a service account + * + * @param serviceAccount service account for the credentials, or null, to get default credentials + * @param scopes list of scopes to be requested for the access token, see + * https://developers.google.com/identity/protocols/oauth2/scopes + * @return {@link GoogleCredentials} corresponding to the give service account + */ + GoogleCredentials getCredentials(GcpServiceAccount serviceAccount, String... scopes); +} diff --git a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamServiceImpl.java b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamServiceImpl.java index 0b781562..242b26be 100644 --- a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamServiceImpl.java +++ b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamServiceImpl.java @@ -16,7 +16,7 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode; -import com.google.api.services.iam.v2.IamScopes; +import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.iam.admin.v1.IAMClient; import com.google.cloud.iam.credentials.v1.GenerateAccessTokenRequest; @@ -30,7 +30,8 @@ import org.eclipse.edc.spi.monitor.Monitor; import java.io.IOException; -import java.util.Collections; +import java.util.Arrays; +import java.util.Date; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -41,7 +42,7 @@ public class IamServiceImpl implements IamService { private final GcpConfiguration gcpConfiguration; private Supplier iamClientSupplier; private Supplier iamCredentialsClientSupplier; - private AccessTokenProvider applicationDefaultCredentials; + private CredentialsManager credentialsManager; private IamServiceImpl(Monitor monitor, GcpConfiguration gcpConfiguration) { this.monitor = monitor; @@ -75,9 +76,13 @@ public GcpServiceAccount getServiceAccount(String serviceAccountName) { } @Override - public GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount) { + public GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount, String... scopes) { if (serviceAccount.equals(ADC_SERVICE_ACCOUNT)) { - return applicationDefaultCredentials.getAccessToken(); + var credentials = credentialsManager.getApplicationDefaultCredentials() + .createScoped(scopes); + credentialsManager.refreshCredentials(credentials); + var token = credentials.getAccessToken(); + return new GcpAccessToken(token.getTokenValue(), token.getExpirationTime().getTime()); } try (var iamCredentialsClient = iamCredentialsClientSupplier.get()) { @@ -85,7 +90,7 @@ public GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount) { var lifetime = Duration.newBuilder().setSeconds(ONE_HOUR_IN_S).build(); var request = GenerateAccessTokenRequest.newBuilder() .setName(name.toString()) - .addAllScope(Collections.singleton(IamScopes.CLOUD_PLATFORM)) + .addAllScope(Arrays.asList(scopes)) .setLifetime(lifetime) .build(); var response = iamCredentialsClient.generateAccessToken(request); @@ -97,6 +102,36 @@ public GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount) { } } + @Override + public GoogleCredentials getCredentials(GcpAccessToken accessToken) { + return GoogleCredentials.create( + new AccessToken(accessToken.getToken(), new Date(accessToken.getExpiration())) + ); + } + + @Override + public GoogleCredentials getCredentials(GcpServiceAccount serviceAccount, String... scopes) { + var sourceCredentials = credentialsManager.getApplicationDefaultCredentials(); + credentialsManager.refreshCredentials(sourceCredentials); + + if (serviceAccount.equals(ADC_SERVICE_ACCOUNT)) { + var adcCredentials = sourceCredentials.createScoped(scopes); + monitor.debug( + "Credentials for project '" + gcpConfiguration.projectId() + "' using ADC"); + return adcCredentials; + } + + sourceCredentials = sourceCredentials.createScoped(IAM_SCOPE); + monitor.debug("Credentials for project '" + gcpConfiguration.projectId() + + "' using service account '" + serviceAccount.getName() + "'"); + + return credentialsManager.createImpersonated( + sourceCredentials, + serviceAccount, + 3600, + scopes); + } + private String getServiceAccountEmail(String name, String project) { return String.format("%s@%s.iam.gserviceaccount.com", name, project); } @@ -122,8 +157,8 @@ public Builder iamCredentialsClientSupplier(Supplier iamCr return this; } - public Builder applicationDefaultCredentials(AccessTokenProvider applicationDefaultCredentials) { - iamServiceImpl.applicationDefaultCredentials = applicationDefaultCredentials; + public Builder credentialUtil(CredentialsManager credentialUtil) { + iamServiceImpl.credentialsManager = credentialUtil; return this; } @@ -138,8 +173,8 @@ public IamServiceImpl build() { iamServiceImpl.iamCredentialsClientSupplier = defaultIamCredentialsClientSupplier(); } - if (iamServiceImpl.applicationDefaultCredentials == null) { - iamServiceImpl.applicationDefaultCredentials = new ApplicationDefaultCredentials(iamServiceImpl.monitor); + if (iamServiceImpl.credentialsManager == null) { + iamServiceImpl.credentialsManager = new DefaultCredentialsManager(iamServiceImpl.monitor); } return iamServiceImpl; @@ -171,19 +206,4 @@ private Supplier defaultIamCredentialsClientSupplier() { }; } } - - private record ApplicationDefaultCredentials(Monitor monitor) implements AccessTokenProvider { - @Override - public GcpAccessToken getAccessToken() { - try { - var credentials = GoogleCredentials.getApplicationDefault().createScoped(IamScopes.CLOUD_PLATFORM); - credentials.refreshIfExpired(); - var token = credentials.getAccessToken(); - return new GcpAccessToken(token.getTokenValue(), token.getExpirationTime().getTime()); - } catch (IOException ioException) { - monitor.severe("Cannot get application default access token", ioException); - return null; - } - } - } } diff --git a/extensions/common/gcp/gcp-core/src/test/java/org/eclipse/edc/gcp/iam/IamServiceImplTest.java b/extensions/common/gcp/gcp-core/src/test/java/org/eclipse/edc/gcp/iam/IamServiceImplTest.java index 88eb98b4..1fa18b57 100644 --- a/extensions/common/gcp/gcp-core/src/test/java/org/eclipse/edc/gcp/iam/IamServiceImplTest.java +++ b/extensions/common/gcp/gcp-core/src/test/java/org/eclipse/edc/gcp/iam/IamServiceImplTest.java @@ -17,12 +17,16 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ApiExceptionFactory; import com.google.api.gax.rpc.StatusCode; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.iam.admin.v1.IAMClient; import com.google.cloud.iam.credentials.v1.GenerateAccessTokenRequest; import com.google.cloud.iam.credentials.v1.GenerateAccessTokenResponse; import com.google.cloud.iam.credentials.v1.IamCredentialsClient; import com.google.cloud.iam.credentials.v1.ServiceAccountName; import com.google.iam.admin.v1.ServiceAccount; +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; import org.eclipse.edc.gcp.common.GcpAccessToken; import org.eclipse.edc.gcp.common.GcpConfiguration; import org.eclipse.edc.gcp.common.GcpException; @@ -31,15 +35,22 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; +import java.util.concurrent.TimeUnit; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.AdditionalMatchers.and; -import static org.mockito.Mockito.argThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class IamServiceImplTest { - + private final String testTokenValue = "test-access-token"; + private final long testTokenExpirationTime = 433454334; + private final String testScopes = "test-scopes"; private final String projectId = "test-project-Id"; private final String serviceAccountName = "test-service-account"; private final String serviceAccountEmail = String.format("%s@%s.iam.gserviceaccount.com", serviceAccountName, projectId); @@ -49,25 +60,39 @@ class IamServiceImplTest { private IamService iamApi; private IAMClient iamClient; private IamCredentialsClient iamCredentialsClient; - private AccessTokenProvider accessTokenProvider; + private CredentialsManager credentialsManager; private GcpServiceAccount testServiceAccount; private GcpConfiguration gcpConfiguration; - private final String iamServiceAccountName = "projects/" + projectId + "/serviceAccounts/" + serviceAccountEmail; @BeforeEach void setUp() { var monitor = mock(Monitor.class); iamClient = mock(); iamCredentialsClient = mock(); - accessTokenProvider = mock(); + credentialsManager = mock(); gcpConfiguration = mock(); testServiceAccount = new GcpServiceAccount(serviceAccountEmail, serviceAccountName, serviceAccountDescription); iamApi = IamServiceImpl.Builder.newInstance(monitor, gcpConfiguration) .iamClientSupplier(() -> iamClient) .iamCredentialsClientSupplier(() -> iamCredentialsClient) - .applicationDefaultCredentials(accessTokenProvider) + .credentialUtil(credentialsManager) .build(); + + var credentialAccessToken = AccessToken.newBuilder() + .setTokenValue(testTokenValue) + .setExpirationTime(new Date(testTokenExpirationTime)) + .build(); + var googleCredentials = mock(GoogleCredentials.class); + when(credentialsManager.getApplicationDefaultCredentials()) + .thenReturn(googleCredentials); + when(googleCredentials.createScoped(any(String[].class))).thenReturn(googleCredentials); + when(googleCredentials.getAccessToken()).thenReturn(credentialAccessToken); + when(credentialsManager.createImpersonated( + any(GoogleCredentials.class), + any(GcpServiceAccount.class), + anyInt(), + any(String[].class))).thenAnswer(i -> i.getArguments()[0]); } @Test @@ -124,36 +149,98 @@ void testGetServiceAccountThatDoesntExist() { @Test void testCreateAccessToken() { - var expectedTokenString = "test-access-token"; - var expectedKey = GenerateAccessTokenResponse.newBuilder().setAccessToken(expectedTokenString).build(); - GenerateAccessTokenRequest expectedRequest = and( - argThat(x -> x.getName().equals("projects/-/serviceAccounts/" + serviceAccountEmail)), - argThat(x -> x.getLifetime().getSeconds() == 3600) - ); + long timeout = 3600; + var expectedKey = GenerateAccessTokenResponse.newBuilder() + .setAccessToken(testTokenValue) + .setExpireTime(Timestamp.newBuilder().setSeconds(timeout)) + .build(); + String[] scope = {}; + var expectedRequest = GenerateAccessTokenRequest.newBuilder() + .setName("projects/-/serviceAccounts/" + serviceAccountEmail) + .addAllScope(Arrays.asList(scope)) + .setLifetime(Duration.newBuilder().setSeconds(TimeUnit.HOURS.toSeconds(1)).build()) + .build(); when(iamCredentialsClient.generateAccessToken(expectedRequest)).thenReturn(expectedKey); var accessToken = iamApi.createAccessToken(testServiceAccount); - assertThat(accessToken.getToken()).isEqualTo(expectedTokenString); + assertThat(accessToken.getToken()).isEqualTo(testTokenValue); + assertThat(accessToken.getExpiration()).isEqualTo(timeout * 1000); } @Test - void testCreateDefaultAccessToken() { - var expectedTokenString = "test-access-token"; + void testCreateAccessTokenWithScope() { long timeout = 3600; - when(accessTokenProvider.getAccessToken()).thenReturn(new GcpAccessToken(expectedTokenString, timeout)); + var expectedKey = GenerateAccessTokenResponse.newBuilder() + .setAccessToken(testTokenValue) + .setExpireTime(Timestamp.newBuilder().setSeconds(timeout)) + .build(); + var scope = "test_scope"; + var expectedRequest = GenerateAccessTokenRequest.newBuilder() + .setName("projects/-/serviceAccounts/" + serviceAccountEmail) + .addAllScope(Arrays.asList(scope)) + .setLifetime(Duration.newBuilder().setSeconds(TimeUnit.HOURS.toSeconds(1)).build()) + .build(); + + when(iamCredentialsClient.generateAccessToken(expectedRequest)).thenReturn(expectedKey); + + var accessToken = iamApi.createAccessToken(testServiceAccount, scope); + + assertThat(accessToken.getToken()).isEqualTo(testTokenValue); + assertThat(accessToken.getExpiration()).isEqualTo(timeout * 1000); + } + @Test + void testCreateDefaultAccessToken() { var accessToken = iamApi.createAccessToken(IamService.ADC_SERVICE_ACCOUNT); - assertThat(accessToken.getToken()).isEqualTo(expectedTokenString); - assertThat(accessToken.getExpiration()).isEqualTo(timeout); + assertThat(accessToken.getToken()).isEqualTo(testTokenValue); + assertThat(accessToken.getExpiration()).isEqualTo(testTokenExpirationTime); + } + + @Test + void testCreateDefaultAccessTokenWithScope() { + var scope = "test_scope"; + + var accessToken = iamApi.createAccessToken(IamService.ADC_SERVICE_ACCOUNT, scope); + assertThat(accessToken.getToken()).isEqualTo(testTokenValue); + assertThat(accessToken.getExpiration()).isEqualTo(testTokenExpirationTime); } @Test void testCreateDefaultAccessTokenError() { - when(accessTokenProvider.getAccessToken()).thenReturn(null); + when(credentialsManager.getApplicationDefaultCredentials()).thenThrow(new GcpException("Cannot get credentials")); + assertThatThrownBy(() -> iamApi.createAccessToken(IamService.ADC_SERVICE_ACCOUNT)).isInstanceOf(GcpException.class); + } - var accessToken = iamApi.createAccessToken(IamService.ADC_SERVICE_ACCOUNT); - assertThat(accessToken).isNull(); + @Test + void testGetCredentialsFromTokenSucceeds() { + var expectedTokenString = "test-access-token"; + long timeout = 3600; + var passedAccessToken = new GcpAccessToken(expectedTokenString, timeout); + var credentials = iamApi.getCredentials(passedAccessToken); + assertThat(credentials).isNotNull().extracting(GoogleCredentials::getAccessToken).satisfies(accessToken -> { + assertThat(accessToken.getTokenValue()).isEqualTo(expectedTokenString); + assertThat(accessToken.getExpirationTime()).isEqualTo(Instant.ofEpochMilli(timeout)); + }); + } + + @Test + void testGetCredentialsWithAdcSucceeds() { + var credentials = iamApi.getCredentials(IamService.ADC_SERVICE_ACCOUNT, testScopes); + assertThat(credentials).isNotNull().extracting(GoogleCredentials::getAccessToken).satisfies(accessToken -> { + assertThat(accessToken.getTokenValue()).isEqualTo(testTokenValue); + assertThat(accessToken.getExpirationTime()).isEqualTo(new Date(testTokenExpirationTime)); + }); + } + + @Test + void testGetCredentialsWithServiceAccountSucceeds() { + var serviceAccount = new GcpServiceAccount(serviceAccountEmail, serviceAccountName, serviceAccountDescription); + var credentials = iamApi.getCredentials(serviceAccount, testScopes); + assertThat(credentials).isNotNull().extracting(GoogleCredentials::getAccessToken).satisfies(accessToken -> { + assertThat(accessToken.getTokenValue()).isEqualTo(testTokenValue); + assertThat(accessToken.getExpirationTime()).isEqualTo(new Date(testTokenExpirationTime)); + }); } private ApiException apiExceptionWithStatusCode(StatusCode.Code code) { diff --git a/extensions/control-plane/provision/provision-bigquery/src/main/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisionExtension.java b/extensions/control-plane/provision/provision-bigquery/src/main/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisionExtension.java index eabd68a5..0739d09a 100644 --- a/extensions/control-plane/provision/provision-bigquery/src/main/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisionExtension.java +++ b/extensions/control-plane/provision/provision-bigquery/src/main/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisionExtension.java @@ -17,12 +17,14 @@ import org.eclipse.edc.connector.controlplane.transfer.spi.provision.ProvisionManager; import org.eclipse.edc.connector.controlplane.transfer.spi.provision.ResourceManifestGenerator; import org.eclipse.edc.gcp.bigquery.service.BigQueryFactoryImpl; +import org.eclipse.edc.gcp.common.GcpAccessToken; import org.eclipse.edc.gcp.common.GcpConfiguration; import org.eclipse.edc.gcp.iam.IamService; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; @Extension(value = BigQueryProvisionExtension.NAME) public class BigQueryProvisionExtension implements ServiceExtension { @@ -35,6 +37,8 @@ public class BigQueryProvisionExtension implements ServiceExtension { private GcpConfiguration gcpConfiguration; @Inject private IamService iamService; + @Inject + private TypeManager typeManager; @Override public String name() { @@ -44,11 +48,11 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { var monitor = context.getMonitor(); - var bqFactory = new BigQueryFactoryImpl(gcpConfiguration, monitor); - + var bqFactory = new BigQueryFactoryImpl(gcpConfiguration, iamService); var provisioner = new BigQueryProvisioner(gcpConfiguration, bqFactory, iamService, monitor); provisionManager.register(provisioner); manifestGenerator.registerGenerator(new BigQueryConsumerResourceDefinitionGenerator()); + typeManager.registerTypes(BigQueryProvisionedResource.class, BigQueryResourceDefinition.class, GcpAccessToken.class); } } diff --git a/extensions/control-plane/provision/provision-bigquery/src/main/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisioner.java b/extensions/control-plane/provision/provision-bigquery/src/main/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisioner.java index 935c19c7..674f1325 100644 --- a/extensions/control-plane/provision/provision-bigquery/src/main/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisioner.java +++ b/extensions/control-plane/provision/provision-bigquery/src/main/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisioner.java @@ -84,7 +84,7 @@ public CompletableFuture> provision( } monitor.info("BigQuery Provisioner table " + target.getTableName().toString() + " exists"); - var token = iamService.createAccessToken(serviceAccount); + var token = iamService.createAccessToken(serviceAccount, IamService.BQ_SCOPE); monitor.info("BigQuery Provisioner token ready"); var resource = getProvisionedResource(resourceDefinition, resourceName, tableName, serviceAccount); diff --git a/extensions/control-plane/provision/provision-bigquery/src/test/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisionerTest.java b/extensions/control-plane/provision/provision-bigquery/src/test/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisionerTest.java index 38343630..223d022a 100644 --- a/extensions/control-plane/provision/provision-bigquery/src/test/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisionerTest.java +++ b/extensions/control-plane/provision/provision-bigquery/src/test/java/org/eclipse/edc/connector/provision/gcp/BigQueryProvisionerTest.java @@ -125,7 +125,7 @@ void provisionSuccessUsingAdc() throws IOException { when(bqFactory.createBigQuery(serviceAccount)).thenReturn(bigQuery); when(iamService.getServiceAccount(null)).thenReturn(serviceAccount); - when(iamService.createAccessToken(serviceAccount)).thenReturn(token); + when(iamService.createAccessToken(serviceAccount, "https://www.googleapis.com/auth/bigquery")).thenReturn(token); var resourceDefinition = resourceDefinitionBuilder.build(); var expectedResource = BigQueryProvisionedResource.Builder.newInstance() @@ -151,7 +151,7 @@ void provisionSuccessUsingAdc() throws IOException { var result = bigQueryProvisioner.provision(resourceDefinition, Policy.Builder.newInstance().build()); // Assert. - verify(iamService).createAccessToken(serviceAccount); + verify(iamService).createAccessToken(serviceAccount, "https://www.googleapis.com/auth/bigquery"); assertThat(result).succeedsWithin(1, SECONDS) .extracting(StatusResult::getContent).satisfies(response -> { @@ -181,7 +181,7 @@ void provisionSuccessUsingServiceAccount() throws IOException { when(bqFactory.createBigQuery(serviceAccount)).thenReturn(bigQuery); when(iamService.getServiceAccount(TEST_SERVICE_ACCOUNT_NAME)).thenReturn(serviceAccount); - when(iamService.createAccessToken(serviceAccount)).thenReturn(token); + when(iamService.createAccessToken(serviceAccount, "https://www.googleapis.com/auth/bigquery")).thenReturn(token); var resourceDefinition = resourceDefinitionBuilder.build(); var expectedResource = BigQueryProvisionedResource.Builder.newInstance() @@ -208,7 +208,7 @@ void provisionSuccessUsingServiceAccount() throws IOException { // Assert. verify(iamService).getServiceAccount(TEST_SERVICE_ACCOUNT_NAME); - verify(iamService).createAccessToken(serviceAccount); + verify(iamService).createAccessToken(serviceAccount, "https://www.googleapis.com/auth/bigquery"); assertThat(result).succeedsWithin(1, SECONDS) .extracting(StatusResult::getContent).satisfies(response -> { diff --git a/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsProvisionExtension.java b/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsProvisionExtension.java index 32060334..d0e9dc95 100644 --- a/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsProvisionExtension.java +++ b/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsProvisionExtension.java @@ -18,13 +18,14 @@ import com.google.cloud.storage.StorageOptions; import org.eclipse.edc.connector.controlplane.transfer.spi.provision.ProvisionManager; import org.eclipse.edc.connector.controlplane.transfer.spi.provision.ResourceManifestGenerator; +import org.eclipse.edc.gcp.common.GcpAccessToken; import org.eclipse.edc.gcp.common.GcpConfiguration; import org.eclipse.edc.gcp.iam.IamService; import org.eclipse.edc.gcp.storage.StorageServiceImpl; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; - +import org.eclipse.edc.spi.types.TypeManager; public class GcsProvisionExtension implements ServiceExtension { @Inject @@ -44,6 +45,9 @@ public String name() { @Inject private GcpConfiguration gcpConfiguration; + @Inject + private TypeManager typeManager; + @Override public void initialize(ServiceExtensionContext context) { var monitor = context.getMonitor(); @@ -54,6 +58,8 @@ public void initialize(ServiceExtensionContext context) { provisionManager.register(provisioner); manifestGenerator.registerGenerator(new GcsConsumerResourceDefinitionGenerator()); + + typeManager.registerTypes(GcsProvisionedResource.class, GcsResourceDefinition.class, GcpAccessToken.class); } diff --git a/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsProvisioner.java b/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsProvisioner.java index 3a9910cd..93a7ae4d 100644 --- a/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsProvisioner.java +++ b/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsProvisioner.java @@ -78,7 +78,7 @@ public CompletableFuture> provision( try { var bucket = storageService.getOrCreateBucket(bucketName, bucketLocation); var serviceAccount = iamService.getServiceAccount(resourceDefinition.getServiceAccountName()); - var token = iamService.createAccessToken(serviceAccount); + var token = iamService.createAccessToken(serviceAccount, IamService.GCS_SCOPE); var resource = getProvisionedResource(resourceDefinition, resourceName, bucketName, serviceAccount); var response = ProvisionResponse.Builder.newInstance().resource(resource).secretToken(token).build(); diff --git a/extensions/control-plane/provision/provision-gcs/src/test/java/org/eclipse/edc/connector/provision/gcp/GcsProvisionerTest.java b/extensions/control-plane/provision/provision-gcs/src/test/java/org/eclipse/edc/connector/provision/gcp/GcsProvisionerTest.java index 2ab79d7c..71929a5c 100644 --- a/extensions/control-plane/provision/provision-gcs/src/test/java/org/eclipse/edc/connector/provision/gcp/GcsProvisionerTest.java +++ b/extensions/control-plane/provision/provision-gcs/src/test/java/org/eclipse/edc/connector/provision/gcp/GcsProvisionerTest.java @@ -33,6 +33,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -83,14 +84,14 @@ void provisionSuccess() { when(storageServiceMock.getOrCreateBucket(bucketName, bucketLocation)).thenReturn(bucket); when(storageServiceMock.isEmpty(bucketName)).thenReturn(true); when(iamServiceMock.getServiceAccount(null)).thenReturn(IamService.ADC_SERVICE_ACCOUNT); - when(iamServiceMock.createAccessToken(IamService.ADC_SERVICE_ACCOUNT)).thenReturn(token); + when(iamServiceMock.createAccessToken(IamService.ADC_SERVICE_ACCOUNT, "https://www.googleapis.com/auth/devstorage.read_write")).thenReturn(token); doNothing().when(storageServiceMock).addProviderPermissions(bucket, serviceAccount); var response = provisioner.provision(resourceDefinition, testPolicy).join().getContent(); verify(storageServiceMock).getOrCreateBucket(bucketName, bucketLocation); verify(iamServiceMock).getServiceAccount(null); - verify(iamServiceMock).createAccessToken(IamService.ADC_SERVICE_ACCOUNT); + verify(iamServiceMock).createAccessToken(IamService.ADC_SERVICE_ACCOUNT, "https://www.googleapis.com/auth/devstorage.read_write"); assertThat(response.getResource()).isInstanceOfSatisfying(GcsProvisionedResource.class, resource -> { assertThat(resource.getId()).isEqualTo(resourceDefinitionId); @@ -122,7 +123,7 @@ void provisionWithImpersonationSuccess() { when(storageServiceMock.getOrCreateBucket(bucketName, bucketLocation)).thenReturn(bucket); when(storageServiceMock.isEmpty(bucketName)).thenReturn(true); when(iamServiceMock.getServiceAccount(serviceAccount.getName())).thenReturn(serviceAccount); - when(iamServiceMock.createAccessToken(serviceAccount)).thenReturn(token); + when(iamServiceMock.createAccessToken(serviceAccount, "https://www.googleapis.com/auth/devstorage.read_write")).thenReturn(token); doNothing().when(storageServiceMock).addProviderPermissions(bucket, serviceAccount); var response = provisioner.provision(resourceDefinition, testPolicy).join().getContent(); @@ -138,7 +139,7 @@ void provisionWithImpersonationSuccess() { }); verify(storageServiceMock).getOrCreateBucket(bucketName, bucketLocation); - verify(iamServiceMock).createAccessToken(any()); + verify(iamServiceMock).createAccessToken(any(), eq("https://www.googleapis.com/auth/devstorage.read_write")); } @Test @@ -157,7 +158,7 @@ void provisionSucceedsIfBucketNotEmpty() { assertThat(response.failed()).isFalse(); verify(storageServiceMock).getOrCreateBucket(bucketName, bucketLocation); - verify(iamServiceMock).createAccessToken(IamService.ADC_SERVICE_ACCOUNT); + verify(iamServiceMock).createAccessToken(IamService.ADC_SERVICE_ACCOUNT, "https://www.googleapis.com/auth/devstorage.read_write"); } @Test diff --git a/extensions/data-plane/data-plane-google-storage/src/main/java/org/eclipse/edc/connector/dataplane/gcp/storage/DataPlaneGcsExtension.java b/extensions/data-plane/data-plane-google-storage/src/main/java/org/eclipse/edc/connector/dataplane/gcp/storage/DataPlaneGcsExtension.java index bc889fd0..26c60690 100644 --- a/extensions/data-plane/data-plane-google-storage/src/main/java/org/eclipse/edc/connector/dataplane/gcp/storage/DataPlaneGcsExtension.java +++ b/extensions/data-plane/data-plane-google-storage/src/main/java/org/eclipse/edc/connector/dataplane/gcp/storage/DataPlaneGcsExtension.java @@ -16,6 +16,7 @@ import org.eclipse.edc.connector.dataplane.spi.pipeline.DataTransferExecutorServiceContainer; import org.eclipse.edc.connector.dataplane.spi.pipeline.PipelineService; +import org.eclipse.edc.gcp.iam.IamService; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.security.Vault; @@ -40,6 +41,9 @@ public class DataPlaneGcsExtension implements ServiceExtension { @Inject private DataTransferExecutorServiceContainer executorContainer; + @Inject + private IamService iamService; + @Override public String name() { return NAME; @@ -53,7 +57,7 @@ public void initialize(ServiceExtensionContext context) { var sourceFactory = new GcsDataSourceFactory(monitor); pipelineService.registerFactory(sourceFactory); - var sinkFactory = new GcsDataSinkFactory(executorContainer.getExecutorService(), monitor, vault, typeManager); + var sinkFactory = new GcsDataSinkFactory(executorContainer.getExecutorService(), monitor, vault, typeManager, iamService); pipelineService.registerFactory(sinkFactory); } } diff --git a/extensions/data-plane/data-plane-google-storage/src/main/java/org/eclipse/edc/connector/dataplane/gcp/storage/GcsDataSinkFactory.java b/extensions/data-plane/data-plane-google-storage/src/main/java/org/eclipse/edc/connector/dataplane/gcp/storage/GcsDataSinkFactory.java index bbeb1503..8eba6593 100644 --- a/extensions/data-plane/data-plane-google-storage/src/main/java/org/eclipse/edc/connector/dataplane/gcp/storage/GcsDataSinkFactory.java +++ b/extensions/data-plane/data-plane-google-storage/src/main/java/org/eclipse/edc/connector/dataplane/gcp/storage/GcsDataSinkFactory.java @@ -14,8 +14,6 @@ package org.eclipse.edc.connector.dataplane.gcp.storage; - -import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageOptions; @@ -23,7 +21,7 @@ import org.eclipse.edc.connector.dataplane.spi.pipeline.DataSink; import org.eclipse.edc.connector.dataplane.spi.pipeline.DataSinkFactory; import org.eclipse.edc.gcp.common.GcpAccessToken; -import org.eclipse.edc.gcp.common.GcpException; +import org.eclipse.edc.gcp.iam.IamService; import org.eclipse.edc.gcp.storage.GcsStoreSchema; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; @@ -35,8 +33,6 @@ import org.eclipse.edc.validator.spi.Validator; import org.jetbrains.annotations.NotNull; -import java.io.IOException; -import java.util.Date; import java.util.concurrent.ExecutorService; public class GcsDataSinkFactory implements DataSinkFactory { @@ -46,13 +42,15 @@ public class GcsDataSinkFactory implements DataSinkFactory { private final Monitor monitor; private final Vault vault; private final TypeManager typeManager; + private final IamService iamService; - public GcsDataSinkFactory(ExecutorService executorService, Monitor monitor, Vault vault, TypeManager typeManager) { + public GcsDataSinkFactory(ExecutorService executorService, Monitor monitor, Vault vault, TypeManager typeManager, IamService iamService) { this.executorService = executorService; this.monitor = monitor; this.vault = vault; this.typeManager = typeManager; + this.iamService = iamService; } @Override @@ -74,7 +72,6 @@ public DataSink createSink(DataFlowStartMessage request) { } var destination = request.getDestinationDataAddress(); - var storageClient = createStorageClient(destination.getKeyName()); return GcsDataSink.Builder.newInstance() @@ -93,16 +90,9 @@ private Storage createStorageClient(String keyName) { if (keyName != null && !keyName.isEmpty()) { var credentialsContent = vault.resolveSecret(keyName); var gcsAccessToken = typeManager.readValue(credentialsContent, GcpAccessToken.class); - googleCredentials = GoogleCredentials.create( - new AccessToken(gcsAccessToken.getToken(), - new Date(gcsAccessToken.getExpiration())) - ); + googleCredentials = iamService.getCredentials(gcsAccessToken); } else { - try { - googleCredentials = GoogleCredentials.getApplicationDefault(); - } catch (IOException e) { - throw new GcpException("Error while getting the default credentials.", e); - } + googleCredentials = iamService.getCredentials(IamService.ADC_SERVICE_ACCOUNT, IamService.GCS_SCOPE); } return StorageOptions.newBuilder() diff --git a/extensions/data-plane/data-plane-google-storage/src/test/java/org/eclipse/edc/connector/dataplane/gcp/storage/GcsDataSinkFactoryTest.java b/extensions/data-plane/data-plane-google-storage/src/test/java/org/eclipse/edc/connector/dataplane/gcp/storage/GcsDataSinkFactoryTest.java index 909ae986..4b013a32 100644 --- a/extensions/data-plane/data-plane-google-storage/src/test/java/org/eclipse/edc/connector/dataplane/gcp/storage/GcsDataSinkFactoryTest.java +++ b/extensions/data-plane/data-plane-google-storage/src/test/java/org/eclipse/edc/connector/dataplane/gcp/storage/GcsDataSinkFactoryTest.java @@ -14,10 +14,15 @@ package org.eclipse.edc.connector.dataplane.gcp.storage; +import com.google.auth.oauth2.GoogleCredentials; +import org.eclipse.edc.gcp.common.GcpAccessToken; +import org.eclipse.edc.gcp.iam.IamService; import org.eclipse.edc.gcp.storage.GcsStoreSchema; import org.eclipse.edc.json.JacksonTypeManager; +import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.edc.spi.types.domain.DataAddress; import org.eclipse.edc.spi.types.domain.transfer.DataFlowStartMessage; import org.junit.jupiter.api.Test; @@ -32,51 +37,57 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class GcsDataSinkFactoryTest { - - private final GcsDataSinkFactory factory = new GcsDataSinkFactory( + private final TypeManager typeManager = new JacksonTypeManager(); + private Vault vault = mock(); + private IamService iamService = mock(); + private GcsDataSinkFactory factory = new GcsDataSinkFactory( mock(ExecutorService.class), mock(Monitor.class), - mock(Vault.class), - new JacksonTypeManager() - ); + vault, + typeManager, + iamService + ); @Test void canHandle_returnsTrueWhenExpectedType() { - var dataAddress = DataAddress.Builder + var destination = DataAddress.Builder .newInstance() .type(GcsStoreSchema.TYPE) .build(); - var result = factory.canHandle(createRequest(dataAddress)); + var result = factory.canHandle(createRequest(destination)); assertThat(result).isTrue(); } @Test void canHandle_returnsFalseWhenUnexpectedType() { - var dataAddress = DataAddress.Builder + var destination = DataAddress.Builder .newInstance() .type("Not Google Storage") .build(); - var result = factory.canHandle(createRequest(dataAddress)); + var result = factory.canHandle(createRequest(destination)); assertThat(result).isFalse(); } @Test void validate_ShouldSucceedIfPropertiesAreValid() { - var source = DataAddress.Builder + var destination = DataAddress.Builder .newInstance() .type(GcsStoreSchema.TYPE) .property(GcsStoreSchema.BUCKET_NAME, "validBucketName") .property(GcsStoreSchema.BLOB_NAME, "validBlobName") .build(); - var request = createRequest(source); + var request = createRequest(destination); var result = factory.validateRequest(request); @@ -86,19 +97,71 @@ void validate_ShouldSucceedIfPropertiesAreValid() { @ParameterizedTest @ArgumentsSource(InvalidInputProvider.class) void validate_shouldFailIfPropertiesAreMissing(String bucketName) { - var source = DataAddress.Builder + var destination = DataAddress.Builder .newInstance() .type(GcsStoreSchema.TYPE) .property(GcsStoreSchema.BUCKET_NAME, bucketName) .build(); - var request = createRequest(source); + var request = createRequest(destination); var result = factory.validateRequest(request); assertThat(result.failed()).isTrue(); } + @Test + void createSink_ShouldSucceedIfRequestIsValidWithAdc() { + var destination = DataAddress.Builder + .newInstance() + .type(GcsStoreSchema.TYPE) + .property(GcsStoreSchema.BUCKET_NAME, "validBucketName") + .property(GcsStoreSchema.BLOB_NAME, "validBlobName") + .build(); + + var request = createRequest(destination); + var credentials = mock(GoogleCredentials.class); + when(iamService.getCredentials(IamService.ADC_SERVICE_ACCOUNT, IamService.GCS_SCOPE)).thenReturn(credentials); + var dataSink = factory.createSink(request); + + assertThat(dataSink).isNotNull(); + } + + @Test + void createSink_ShouldSucceedIfRequestIsValidWithAccessToken() { + var destination = DataAddress.Builder + .newInstance() + .type(GcsStoreSchema.TYPE) + .property(DataAddress.EDC_DATA_ADDRESS_KEY_NAME, "testKeyName") + .property(GcsStoreSchema.BUCKET_NAME, "validBucketName") + .property(GcsStoreSchema.BLOB_NAME, "validBlobName") + .build(); + + var request = createRequest(destination); + var credentials = mock(GoogleCredentials.class); + var gcpAccessToken = new GcpAccessToken("tokenValue", 1000); + var accessToken = typeManager.writeValueAsString(gcpAccessToken); + + when(vault.resolveSecret("testKeyName")).thenReturn(accessToken); + when(iamService.getCredentials(any(GcpAccessToken.class))).thenReturn(credentials); + var dataSink = factory.createSink(request); + + assertThat(dataSink).isNotNull(); + } + + @Test + void createSink_ShouldThrowExceptionIfRequestIsInvalid() { + // Destination is missing the bucket. + var destination = DataAddress.Builder + .newInstance() + .type(GcsStoreSchema.TYPE) + .property(DataAddress.EDC_DATA_ADDRESS_KEY_NAME, "testKeyName") + .property(GcsStoreSchema.BLOB_NAME, "validBlobName") + .build(); + + var request = createRequest(destination); + assertThatThrownBy(() -> factory.createSink(request)).isInstanceOf(EdcException.class); + } private DataFlowStartMessage createRequest(DataAddress destination) { var source = DataAddress.Builder