diff --git a/DEPENDENCIES b/DEPENDENCIES index ab34992..30f547d 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -28,6 +28,7 @@ maven/mavencentral/com.google.api/api-common/2.21.0, BSD-3-Clause, approved, cle maven/mavencentral/com.google.api/gax-grpc/2.38.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.api/gax-httpjson/2.38.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.api/gax/2.38.0, BSD-3-Clause, approved, #12035 +maven/mavencentral/com.google.apis/google-api-services-iam/v2-rev20240108-2.0.0, , restricted, clearlydefined maven/mavencentral/com.google.apis/google-api-services-storage/v1-rev20231117-2.0.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.auth/google-auth-library-credentials/1.20.0, BSD-3-Clause, approved, clearlydefined maven/mavencentral/com.google.auth/google-auth-library-oauth2-http/1.20.0, BSD-3-Clause, approved, clearlydefined @@ -52,14 +53,21 @@ maven/mavencentral/com.google.guava/failureaccess/1.0.1, Apache-2.0, approved, C 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-jre, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.guava/guava/31.1-android, 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.0-android, Apache-2.0 AND CC0-1.0 AND CC-PDDC, approved, #8772 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/guava/32.1.3-jre, Apache-2.0 AND CC0-1.0 AND LicenseRef-Public-Domain, approved, #9229 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.42.3, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.http-client/google-http-client-apache-v2/1.43.3, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.http-client/google-http-client-appengine/1.43.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.http-client/google-http-client-gson/1.42.0, 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.3, Apache-2.0, approved, clearlydefined maven/mavencentral/com.google.http-client/google-http-client-jackson2/1.43.3, Apache-2.0, approved, clearlydefined +maven/mavencentral/com.google.http-client/google-http-client/1.42.0, 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.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 @@ -77,6 +85,7 @@ maven/mavencentral/com.squareup.okio/okio-jvm/3.6.0, Apache-2.0, approved, #1115 maven/mavencentral/com.squareup.okio/okio/3.6.0, Apache-2.0, approved, #11155 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-codec/commons-codec/1.16.0, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #9157 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 @@ -117,9 +126,7 @@ maven/mavencentral/jakarta.ws.rs/jakarta.ws.rs-api/3.1.0, EPL-2.0 OR GPL-2.0-onl maven/mavencentral/jakarta.xml.bind/jakarta.xml.bind-api/4.0.0, 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.10, Apache-2.0, approved, #7164 -maven/mavencentral/net.bytebuddy/byte-buddy/1.12.21, Apache-2.0 AND BSD-3-Clause, approved, #1811 -maven/mavencentral/net.bytebuddy/byte-buddy/1.14.10, Apache-2.0 AND BSD-3-Clause, approved, #7163 +maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.11, Apache-2.0, approved, #7164 maven/mavencentral/net.bytebuddy/byte-buddy/1.14.11, Apache-2.0 AND BSD-3-Clause, approved, #7163 maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #6709 maven/mavencentral/net.sf.saxon/Saxon-HE/10.6, MPL-2.0 AND W3C, approved, #7945 @@ -128,7 +135,6 @@ maven/mavencentral/org.apache.commons/commons-compress/1.24.0, Apache-2.0 AND BS maven/mavencentral/org.apache.httpcomponents/httpclient/4.5.14, Apache-2.0 AND LicenseRef-Public-Domain, approved, CQ23527 maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.16, Apache-2.0, approved, CQ23528 maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.assertj/assertj-core/3.24.2, Apache-2.0, approved, #6161 maven/mavencentral/org.assertj/assertj-core/3.25.1, Apache-2.0, approved, #12585 maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.77, MIT, approved, #11593 maven/mavencentral/org.bouncycastle/bcprov-jdk18on/1.77, MIT AND CC0-1.0, approved, #11595 @@ -232,7 +238,7 @@ maven/mavencentral/org.junit/junit-bom/5.10.1, EPL-2.0, approved, #9844 maven/mavencentral/org.junit/junit-bom/5.9.2, EPL-2.0, approved, #4711 maven/mavencentral/org.jvnet.mimepull/mimepull/1.9.15, CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0, approved, CQ21484 maven/mavencentral/org.mockito/mockito-core/5.2.0, MIT AND (Apache-2.0 AND MIT) AND Apache-2.0, approved, #7401 -maven/mavencentral/org.mockito/mockito-core/5.8.0, MIT AND (Apache-2.0 AND MIT) AND Apache-2.0, approved, #11787 +maven/mavencentral/org.mockito/mockito-core/5.9.0, MIT AND (Apache-2.0 AND MIT) AND Apache-2.0, approved, #12774 maven/mavencentral/org.objenesis/objenesis/3.3, Apache-2.0, approved, clearlydefined maven/mavencentral/org.opentest4j/opentest4j/1.3.0, Apache-2.0, approved, #9713 maven/mavencentral/org.ow2.asm/asm-commons/9.5, BSD-3-Clause, approved, #7553 diff --git a/extensions/common/gcp/gcp-core/build.gradle.kts b/extensions/common/gcp/gcp-core/build.gradle.kts index 85e0a82..59af934 100644 --- a/extensions/common/gcp/gcp-core/build.gradle.kts +++ b/extensions/common/gcp/gcp-core/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(libs.googlecloud.iam.admin) implementation(libs.googlecloud.storage) implementation(libs.googlecloud.iam.credentials) + implementation(libs.googleapis.iam) testImplementation(libs.edc.junit) } diff --git a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/common/GcpConfiguration.java b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/common/GcpConfiguration.java index 8522fc6..f31fa82 100644 --- a/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/common/GcpConfiguration.java +++ b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/common/GcpConfiguration.java @@ -41,9 +41,9 @@ public class GcpConfiguration { public GcpConfiguration(ServiceExtensionContext context) { projectId = context.getSetting(PROJECT_ID, ServiceOptions.getDefaultProjectId()); - serviceAccountName = context.getSetting(SACCOUNT_NAME, ""); - serviceAccountFile = context.getSetting(SACCOUNT_FILE, ""); - universeDomain = context.getSetting(UNIVERSE_DOMAIN, ""); + serviceAccountName = context.getSetting(SACCOUNT_NAME, null); + serviceAccountFile = context.getSetting(SACCOUNT_FILE, null); + universeDomain = context.getSetting(UNIVERSE_DOMAIN, null); } /** 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 new file mode 100644 index 0000000..9e88666 --- /dev/null +++ b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/AccessTokenProvider.java @@ -0,0 +1,29 @@ +/* + * 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 LCC - 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/IamService.java b/extensions/common/gcp/gcp-core/src/main/java/org/eclipse/edc/gcp/iam/IamService.java index 0dd5a74..c552fb2 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 @@ -23,13 +23,12 @@ */ public interface IamService { /** - * Creates or returns the service account with the matching name and description. + * Returns the existing service account with the matching name. * * @param serviceAccountName the name for the service account. Limited to 30 chars - * @param serviceAccountDescription the unique description for the service account that is used to avoid reuse of service accounts * @return the {@link GcpServiceAccount} describing the service account */ - GcpServiceAccount getOrCreateServiceAccount(String serviceAccountName, String serviceAccountDescription); + GcpServiceAccount getServiceAccount(String serviceAccountName); /** * Creates a temporary valid OAunth2.0 access token for the service account @@ -40,10 +39,9 @@ public interface IamService { GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount); /** - * Delete the specified service account if it exists. - * Do nothing in case it doesn't exist (anymore) + * Creates a temporary valid OAunth2.0 access token using the application default account credentials. * - * @param serviceAccount The service account that should be deleted + * @return {@link GcpAccessToken} */ - void deleteServiceAccountIfExists(GcpServiceAccount serviceAccount); + GcpAccessToken createDefaultAccessToken(); } \ No newline at end of file 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 801e173..17001e5 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,14 +16,12 @@ 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.GoogleCredentials; import com.google.cloud.iam.admin.v1.IAMClient; import com.google.cloud.iam.credentials.v1.GenerateAccessTokenRequest; import com.google.cloud.iam.credentials.v1.IamCredentialsClient; import com.google.cloud.iam.credentials.v1.ServiceAccountName; -import com.google.common.collect.ImmutableList; -import com.google.iam.admin.v1.CreateServiceAccountRequest; -import com.google.iam.admin.v1.ProjectName; -import com.google.iam.admin.v1.ServiceAccount; import com.google.protobuf.Duration; import org.eclipse.edc.gcp.common.GcpAccessToken; import org.eclipse.edc.gcp.common.GcpException; @@ -31,66 +29,39 @@ import org.eclipse.edc.spi.monitor.Monitor; import java.io.IOException; +import java.util.Collections; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; public class IamServiceImpl implements IamService { - - private static final ImmutableList OAUTH_SCOPE = ImmutableList.of("https://www.googleapis.com/auth/cloud-platform"); private static final long ONE_HOUR_IN_S = TimeUnit.HOURS.toSeconds(1); - private final String gcpProjectId; - private final Supplier iamClientSupplier; - private final Supplier iamCredentialsClientSupplier; private final Monitor monitor; + private final String gcpProjectId; + private Supplier iamClientSupplier; + private Supplier iamCredentialsClientSupplier; + private AccessTokenProvider applicationDefaultCredentials; - private IamServiceImpl(Monitor monitor, - String gcpProjectId, - Supplier iamClientSupplier, - Supplier iamCredentialsClientSupplier - ) { + private IamServiceImpl(Monitor monitor, String gcpProjectId) { this.monitor = monitor; this.gcpProjectId = gcpProjectId; - this.iamClientSupplier = iamClientSupplier; - this.iamCredentialsClientSupplier = iamCredentialsClientSupplier; } @Override - public GcpServiceAccount getOrCreateServiceAccount(String serviceAccountName, String serviceAccountDescription) { - var requestedServiceAccount = ServiceAccount.newBuilder() - .setDisplayName(serviceAccountName) - .setDescription(serviceAccountDescription) - .build(); - var request = CreateServiceAccountRequest.newBuilder() - .setName(ProjectName.of(gcpProjectId).toString()) - .setAccountId(serviceAccountName) - .setServiceAccount(requestedServiceAccount) - .build(); - - try (var client = iamClientSupplier.get()) { - var serviceAccount = client.createServiceAccount(request); - monitor.debug("Created service account: " + serviceAccount.getEmail()); - return new GcpServiceAccount(serviceAccount.getEmail(), serviceAccount.getName(), serviceAccountDescription); - } catch (ApiException e) { - if (e.getStatusCode().getCode() == StatusCode.Code.ALREADY_EXISTS) { - return getServiceAccount(serviceAccountName, serviceAccountDescription); - } - monitor.severe("Unable to create service account", e); - throw new GcpException("Unable to create service account", e); - } - } - - private GcpServiceAccount getServiceAccount(String serviceAccountName, String serviceAccountDescription) { + public GcpServiceAccount getServiceAccount(String serviceAccountName) { try (var client = iamClientSupplier.get()) { var serviceAccountEmail = getServiceAccountEmail(serviceAccountName, gcpProjectId); var name = ServiceAccountName.of(gcpProjectId, serviceAccountEmail).toString(); var response = client.getServiceAccount(name); - if (!response.getDescription().equals(serviceAccountDescription)) { - String errorMessage = "A service account with the same name but different description existed already. Please ensure a unique name is used for every transfer process"; - monitor.severe(errorMessage); - throw new GcpException(errorMessage); - } + return new GcpServiceAccount(response.getEmail(), response.getName(), response.getDescription()); + } catch (ApiException e) { + if (e.getStatusCode().getCode() == StatusCode.Code.NOT_FOUND) { + monitor.severe("Service account '" + serviceAccountName + "'not found", e); + throw new GcpException("Service account '" + serviceAccountName + "'not found", e); + } + monitor.severe("Unable to get service account '" + serviceAccountName + "'", e); + throw new GcpException("Unable to get service account '" + serviceAccountName + "'", e); } } @@ -101,7 +72,7 @@ public GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount) { var lifetime = Duration.newBuilder().setSeconds(ONE_HOUR_IN_S).build(); var request = GenerateAccessTokenRequest.newBuilder() .setName(name.toString()) - .addAllScope(OAUTH_SCOPE) + .addAllScope(Collections.singleton(IamScopes.CLOUD_PLATFORM)) .setLifetime(lifetime) .build(); var response = iamCredentialsClient.generateAccessToken(request); @@ -114,19 +85,8 @@ public GcpAccessToken createAccessToken(GcpServiceAccount serviceAccount) { } @Override - public void deleteServiceAccountIfExists(GcpServiceAccount serviceAccount) { - try (var client = iamClientSupplier.get()) { - var serviceAccountName = ServiceAccountName.of(gcpProjectId, serviceAccount.getEmail()); - client.deleteServiceAccount(serviceAccountName.toString()); - monitor.debug("Deleted service account: " + serviceAccount.getEmail()); - } catch (ApiException e) { - if (e.getStatusCode().getCode() == StatusCode.Code.NOT_FOUND) { - monitor.severe("Service account not found", e); - return; - } - monitor.severe("Unable to delete service account", e); - throw new GcpException(e); - } + public GcpAccessToken createDefaultAccessToken() { + return applicationDefaultCredentials.getAccessToken(); } private String getServiceAccountEmail(String name, String project) { @@ -134,14 +94,10 @@ private String getServiceAccountEmail(String name, String project) { } public static class Builder { - private final String gcpProjectId; - private final Monitor monitor; - private Supplier iamClientSupplier; - private Supplier iamCredentialsClientSupplier; + private IamServiceImpl iamServiceImpl; private Builder(Monitor monitor, String gcpProjectId) { - this.gcpProjectId = gcpProjectId; - this.monitor = monitor; + iamServiceImpl = new IamServiceImpl(monitor, gcpProjectId); } public static IamServiceImpl.Builder newInstance(Monitor monitor, String gcpProjectId) { @@ -149,25 +105,36 @@ public static IamServiceImpl.Builder newInstance(Monitor monitor, String gcpProj } public Builder iamClientSupplier(Supplier iamClientSupplier) { - this.iamClientSupplier = iamClientSupplier; + iamServiceImpl.iamClientSupplier = iamClientSupplier; return this; } public Builder iamCredentialsClientSupplier(Supplier iamCredentialsClientSupplier) { - this.iamCredentialsClientSupplier = iamCredentialsClientSupplier; + iamServiceImpl.iamCredentialsClientSupplier = iamCredentialsClientSupplier; + return this; + } + + public Builder applicationDefaultCredentials(AccessTokenProvider applicationDefaultCredentials) { + iamServiceImpl.applicationDefaultCredentials = applicationDefaultCredentials; return this; } public IamServiceImpl build() { - Objects.requireNonNull(gcpProjectId, "gcpProjectId"); - Objects.requireNonNull(monitor, "monitor"); - if (iamClientSupplier == null) { - iamClientSupplier = defaultIamClientSupplier(); + Objects.requireNonNull(iamServiceImpl.gcpProjectId, "gcpProjectId"); + Objects.requireNonNull(iamServiceImpl.monitor, "monitor"); + + if (iamServiceImpl.iamClientSupplier == null) { + iamServiceImpl.iamClientSupplier = defaultIamClientSupplier(); } - if (iamCredentialsClientSupplier == null) { - iamCredentialsClientSupplier = defaultIamCredentialsClientSupplier(); + if (iamServiceImpl.iamCredentialsClientSupplier == null) { + iamServiceImpl.iamCredentialsClientSupplier = defaultIamCredentialsClientSupplier(); } - return new IamServiceImpl(monitor, gcpProjectId, iamClientSupplier, iamCredentialsClientSupplier); + + if (iamServiceImpl.applicationDefaultCredentials == null) { + iamServiceImpl.applicationDefaultCredentials = new ApplicationDefaultCredentials(iamServiceImpl.monitor); + } + + return iamServiceImpl; } /** @@ -196,4 +163,19 @@ 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 57b6827..9f11dda 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 @@ -21,25 +21,20 @@ 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.iam.admin.v1.CreateServiceAccountRequest; +import com.google.cloud.iam.credentials.v1.ServiceAccountName; import com.google.iam.admin.v1.ServiceAccount; +import org.eclipse.edc.gcp.common.GcpAccessToken; import org.eclipse.edc.gcp.common.GcpException; import org.eclipse.edc.gcp.common.GcpServiceAccount; import org.eclipse.edc.spi.monitor.Monitor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; 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.any; import static org.mockito.Mockito.argThat; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class IamServiceImplTest { @@ -51,53 +46,51 @@ class IamServiceImplTest { private IamService iamApi; private IAMClient iamClient; private IamCredentialsClient iamCredentialsClient; + + private AccessTokenProvider accessTokenProvider; private GcpServiceAccount testServiceAccount; private final String iamServiceAccountName = "projects/" + projectId + "/serviceAccounts/" + serviceAccountEmail; @BeforeEach void setUp() { - var monitor = Mockito.mock(Monitor.class); - iamClient = Mockito.mock(IAMClient.class); - iamCredentialsClient = Mockito.mock(IamCredentialsClient.class); + var monitor = mock(Monitor.class); + iamClient = mock(); + iamCredentialsClient = mock(); + accessTokenProvider = mock(); testServiceAccount = new GcpServiceAccount(serviceAccountEmail, serviceAccountName, serviceAccountDescription); iamApi = IamServiceImpl.Builder.newInstance(monitor, projectId) .iamClientSupplier(() -> iamClient) .iamCredentialsClientSupplier(() -> iamCredentialsClient) + .applicationDefaultCredentials(accessTokenProvider) .build(); } @Test - void testCreateServiceAccount() { - var serviceAccount = ServiceAccount.newBuilder().setEmail(serviceAccountEmail).build(); - when(iamClient.createServiceAccount(any(CreateServiceAccountRequest.class))).thenReturn(serviceAccount); - - GcpServiceAccount createdServiceAccount = iamApi.getOrCreateServiceAccount(serviceAccountName, serviceAccountDescription); - - assertThat(createdServiceAccount.getEmail()).isEqualTo(serviceAccountEmail); - assertThat(createdServiceAccount.getDescription()).isEqualTo(serviceAccountDescription); - } - - @Test - void testCreateServiceAccountThatExistsAlreadyWithMatchingDescription() { - var serviceAccount = ServiceAccount.newBuilder().setEmail(serviceAccountEmail).setDescription(serviceAccountDescription).build(); - var createError = apiExceptionWithStatusCode(StatusCode.Code.ALREADY_EXISTS); - when(iamClient.createServiceAccount(any(CreateServiceAccountRequest.class))).thenThrow(createError); - when(iamClient.getServiceAccount(eq(iamServiceAccountName))).thenReturn(serviceAccount); + void testGetServiceAccount() { + var name = ServiceAccountName.of(projectId, serviceAccountEmail).toString(); + var serviceAccount = ServiceAccount.newBuilder() + .setEmail(serviceAccountEmail) + .setDescription(serviceAccountDescription) + .build(); + when(iamClient.getServiceAccount(name)).thenReturn(serviceAccount); - var createdServiceAccount = iamApi.getOrCreateServiceAccount(serviceAccountName, serviceAccountDescription); + var createdServiceAccount = iamApi.getServiceAccount(serviceAccountName); assertThat(createdServiceAccount.getEmail()).isEqualTo(serviceAccountEmail); assertThat(createdServiceAccount.getDescription()).isEqualTo(serviceAccountDescription); } @Test - void testCreateServiceAccountThatExistsAlreadyWithOtherDescription() { - var serviceAccount = ServiceAccount.newBuilder().setEmail(serviceAccountEmail).setDescription("some-other-description").build(); - var createError = apiExceptionWithStatusCode(StatusCode.Code.ALREADY_EXISTS); - when(iamClient.createServiceAccount(any(CreateServiceAccountRequest.class))).thenThrow(createError); - when(iamClient.getServiceAccount(eq(iamServiceAccountName))).thenReturn(serviceAccount); + void testGetServiceAccountThatDoesntExist() { + var name = ServiceAccountName.of(projectId, serviceAccountEmail).toString(); + var serviceAccount = ServiceAccount.newBuilder() + .setEmail(serviceAccountEmail) + .setDescription(serviceAccountDescription) + .build(); + var getError = apiExceptionWithStatusCode(StatusCode.Code.NOT_FOUND); + when(iamClient.getServiceAccount(name)).thenThrow(getError); - assertThatThrownBy(() -> iamApi.getOrCreateServiceAccount(serviceAccountName, serviceAccountDescription)).isInstanceOf(GcpException.class); + assertThatThrownBy(() -> iamApi.getServiceAccount(serviceAccountName)).isInstanceOf(GcpException.class); } @Test @@ -116,35 +109,22 @@ void testCreateAccessToken() { } @Test - void testDeleteServiceAccount() { - var serviceAccount = new GcpServiceAccount(serviceAccountEmail, serviceAccountName, serviceAccountDescription); - doNothing().when(iamClient).deleteServiceAccount(serviceAccountName); - - iamApi.deleteServiceAccountIfExists(serviceAccount); - - verify(iamClient, times(1)).deleteServiceAccount(iamServiceAccountName); - } - - @Test - void testDeleteServiceAccountThatAlreadyExistsSucceeds() { - var exception = apiExceptionWithStatusCode(StatusCode.Code.NOT_FOUND); - doThrow(exception) - .when(iamClient).deleteServiceAccount(serviceAccountName); - - iamApi.deleteServiceAccountIfExists(testServiceAccount); + void testCreateDefaultAccessToken() { + var expectedTokenString = "test-access-token"; + long timeout = 3600; + when(accessTokenProvider.getAccessToken()).thenReturn(new GcpAccessToken(expectedTokenString, timeout)); - verify(iamClient, times(1)).deleteServiceAccount(iamServiceAccountName); + var accessToken = iamApi.createDefaultAccessToken(); + assertThat(accessToken.getToken()).isEqualTo(expectedTokenString); + assertThat(accessToken.getExpiration()).isEqualTo(timeout); } @Test - void testDeleteServiceAccountFails() { - var exception = apiExceptionWithStatusCode(StatusCode.Code.INTERNAL); - - doThrow(exception) - .when(iamClient).deleteServiceAccount(iamServiceAccountName); + void testCreateDefaultAccessTokenError() { + when(accessTokenProvider.getAccessToken()).thenReturn(null); - assertThatThrownBy(() -> iamApi.deleteServiceAccountIfExists(testServiceAccount)) - .isInstanceOf(GcpException.class); + var accessToken = iamApi.createDefaultAccessToken(); + assertThat(accessToken).isNull(); } private ApiException apiExceptionWithStatusCode(StatusCode.Code code) { diff --git a/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsConsumerResourceDefinitionGenerator.java b/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsConsumerResourceDefinitionGenerator.java index e8b271b..1462c13 100644 --- a/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsConsumerResourceDefinitionGenerator.java +++ b/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsConsumerResourceDefinitionGenerator.java @@ -35,9 +35,10 @@ ResourceDefinition generate(DataRequest dataRequest, Policy policy) { var location = destination.getStringProperty(GcsStoreSchema.LOCATION); var storageClass = destination.getStringProperty(GcsStoreSchema.STORAGE_CLASS); var bucketName = destination.getStringProperty(GcsStoreSchema.BUCKET_NAME); + var serviceAccount = destination.getStringProperty(GcsStoreSchema.SERVICE_ACCOUNT_NAME); return GcsResourceDefinition.Builder.newInstance().id(id).location(location) - .storageClass(storageClass).bucketName(bucketName).build(); + .storageClass(storageClass).bucketName(bucketName).serviceAccountName(serviceAccount).build(); } @Override 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 59385fa..91df161 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 @@ -50,7 +50,7 @@ public void initialize(ServiceExtensionContext context) { var storageClient = createDefaultStorageClient(gcpConfiguration.getProjectId()); var storageService = new StorageServiceImpl(storageClient, monitor); - var provisioner = new GcsProvisioner(monitor, storageService, iamService); + var provisioner = new GcsProvisioner(gcpConfiguration, monitor, storageService, iamService); provisionManager.register(provisioner); manifestGenerator.registerGenerator(new GcsConsumerResourceDefinitionGenerator()); 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 7ad3caf..e58c882 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 @@ -14,38 +14,42 @@ package org.eclipse.edc.connector.provision.gcp; +import com.google.common.collect.ImmutableList; import org.eclipse.edc.connector.transfer.spi.provision.Provisioner; import org.eclipse.edc.connector.transfer.spi.types.DeprovisionedResource; import org.eclipse.edc.connector.transfer.spi.types.ProvisionResponse; import org.eclipse.edc.connector.transfer.spi.types.ProvisionedResource; import org.eclipse.edc.connector.transfer.spi.types.ResourceDefinition; import org.eclipse.edc.gcp.common.GcpAccessToken; +import org.eclipse.edc.gcp.common.GcpConfiguration; import org.eclipse.edc.gcp.common.GcpException; import org.eclipse.edc.gcp.common.GcpServiceAccount; -import org.eclipse.edc.gcp.common.GcsBucket; import org.eclipse.edc.gcp.iam.IamService; import org.eclipse.edc.gcp.storage.StorageService; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.response.ResponseStatus; import org.eclipse.edc.spi.response.StatusResult; -import org.jetbrains.annotations.NotNull; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import static java.util.concurrent.CompletableFuture.completedFuture; public class GcsProvisioner implements Provisioner { - + private static final ImmutableList OAUTH_SCOPE = ImmutableList.of("https://www.googleapis.com/auth/cloud-platform"); + private static final long ONE_HOUR_IN_S = TimeUnit.HOURS.toSeconds(1); private final Monitor monitor; private final StorageService storageService; private final IamService iamService; + private final GcpConfiguration gcpConfiguration; - public GcsProvisioner(Monitor monitor, StorageService storageService, IamService iamService) { + public GcsProvisioner(GcpConfiguration gcpConfiguration, Monitor monitor, StorageService storageService, IamService iamService) { this.monitor = monitor; this.storageService = storageService; this.iamService = iamService; + this.gcpConfiguration = gcpConfiguration; } @Override @@ -76,9 +80,17 @@ public CompletableFuture> provision( try { var bucket = storageService.getOrCreateBucket(bucketName, bucketLocation); - // TODO use service account from transfer request, in case defined. - var serviceAccount = createServiceAccount(processId, bucketName); - var token = createBucketAccessToken(bucket, serviceAccount); + GcpServiceAccount serviceAccount = null; + GcpAccessToken token = null; + + var serviceAccountName = getServiceAccountName(resourceDefinition); + if (serviceAccountName != null) { + serviceAccount = iamService.getServiceAccount(serviceAccountName); + token = iamService.createAccessToken(serviceAccount); + } else { + serviceAccount = new GcpServiceAccount("adc-email", "adc-name", "application default"); + token = iamService.createDefaultAccessToken(); + } var resource = getProvisionedResource(resourceDefinition, resourceName, bucketName, serviceAccount); @@ -89,47 +101,23 @@ public CompletableFuture> provision( } } + private String getServiceAccountName(GcsResourceDefinition resourceDefinition) { + if (resourceDefinition.getServiceAccountName() != null) { + // TODO verify service account name from resource definition before returning. + return resourceDefinition.getServiceAccountName(); + } + + return gcpConfiguration.getServiceAccountName(); + } + @Override public CompletableFuture> deprovision( GcsProvisionedResource provisionedResource, Policy policy) { - try { - iamService.deleteServiceAccountIfExists( - new GcpServiceAccount(provisionedResource.getServiceAccountEmail(), - provisionedResource.getServiceAccountName(), "")); - } catch (GcpException e) { - return completedFuture(StatusResult.failure(ResponseStatus.FATAL_ERROR, - String.format("Deprovision failed with: %s", e.getMessage()))); - } return CompletableFuture.completedFuture(StatusResult.success( DeprovisionedResource.Builder.newInstance() .provisionedResourceId(provisionedResource.getId()).build())); } - private GcpServiceAccount createServiceAccount(String processId, String buckedName) { - var serviceAccountName = sanitizeServiceAccountName(processId); - var uniqueServiceAccountDescription = generateUniqueServiceAccountDescription(processId, buckedName); - return iamService.getOrCreateServiceAccount(serviceAccountName, uniqueServiceAccountDescription); - } - - @NotNull - private String sanitizeServiceAccountName(String processId) { - // service account ID must be between 6 and 30 characters and can contain lowercase alphanumeric characters and dashes - String processIdWithoutConstantChars = processId.replace("-", ""); - var maxAllowedSubstringLength = Math.min(26, processIdWithoutConstantChars.length()); - var uniqueId = processIdWithoutConstantChars.substring(0, maxAllowedSubstringLength); - return "edc-" + uniqueId; - } - - @NotNull - private String generateUniqueServiceAccountDescription(String transferProcessId, String bucketName) { - return String.format("transferProcess:%s\nbucket:%s", transferProcessId, bucketName); - } - - private GcpAccessToken createBucketAccessToken(GcsBucket bucket, GcpServiceAccount serviceAccount) { - storageService.addProviderPermissions(bucket, serviceAccount); - return iamService.createAccessToken(serviceAccount); - } - private GcsProvisionedResource getProvisionedResource(GcsResourceDefinition resourceDefinition, String resourceName, String bucketName, GcpServiceAccount serviceAccount) { String serviceAccountEmail = null; String serviceAccountName = null; diff --git a/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsResourceDefinition.java b/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsResourceDefinition.java index 73f7386..4748bd5 100644 --- a/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsResourceDefinition.java +++ b/extensions/control-plane/provision/provision-gcs/src/main/java/org/eclipse/edc/connector/provision/gcp/GcsResourceDefinition.java @@ -23,6 +23,7 @@ public class GcsResourceDefinition extends ResourceDefinition { private String location; private String storageClass; private String bucketName; + private String serviceAccountName; private GcsResourceDefinition() { } @@ -39,12 +40,17 @@ public String getBucketName() { return this.bucketName; } + public String getServiceAccountName() { + return this.serviceAccountName; + } + @Override public Builder toBuilder() { return initializeBuilder(new Builder()) .location(location) .storageClass(storageClass) - .bucketName(bucketName); + .bucketName(bucketName) + .serviceAccountName(serviceAccountName); } public static class Builder extends ResourceDefinition.Builder { @@ -72,6 +78,11 @@ public Builder bucketName(String bucketName) { return this; } + public Builder serviceAccountName(String serviceAccountName) { + resourceDefinition.serviceAccountName = serviceAccountName; + return this; + } + @Override protected void verify() { super.verify(); 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 fb100c5..56912d6 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 @@ -15,6 +15,7 @@ package org.eclipse.edc.connector.provision.gcp; import org.eclipse.edc.gcp.common.GcpAccessToken; +import org.eclipse.edc.gcp.common.GcpConfiguration; import org.eclipse.edc.gcp.common.GcpException; import org.eclipse.edc.gcp.common.GcpServiceAccount; import org.eclipse.edc.gcp.common.GcsBucket; @@ -22,7 +23,6 @@ import org.eclipse.edc.gcp.storage.StorageService; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.response.ResponseStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; @@ -31,8 +31,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -47,13 +45,15 @@ class GcsProvisionerTest { private StorageService storageServiceMock; private IamService iamServiceMock; private Policy testPolicy; + private GcpConfiguration gcpConfiguration; @BeforeEach void setUp() { - storageServiceMock = mock(StorageService.class); - iamServiceMock = mock(IamService.class); + storageServiceMock = mock(); + iamServiceMock = mock(); + gcpConfiguration = mock(); testPolicy = Policy.Builder.newInstance().build(); - provisioner = new GcsProvisioner(mock(Monitor.class), storageServiceMock, iamServiceMock); + provisioner = new GcsProvisioner(gcpConfiguration, mock(Monitor.class), storageServiceMock, iamServiceMock); } @Test @@ -79,11 +79,50 @@ void provisionSuccess() { var serviceAccount = new GcpServiceAccount("test-sa", "sa-name", "description"); var token = new GcpAccessToken("token", 123); + when(gcpConfiguration.getServiceAccountName()).thenReturn(null); + when(storageServiceMock.getOrCreateBucket(bucketName, bucketLocation)).thenReturn(bucket); when(storageServiceMock.isEmpty(bucketName)).thenReturn(true); - when(iamServiceMock.getOrCreateServiceAccount(anyString(), anyString())).thenReturn(serviceAccount); + when(iamServiceMock.createDefaultAccessToken()).thenReturn(token); doNothing().when(storageServiceMock).addProviderPermissions(bucket, serviceAccount); + + var response = provisioner.provision(resourceDefinition, testPolicy).join().getContent(); + + assertThat(response.getResource()).isInstanceOfSatisfying(GcsProvisionedResource.class, resource -> { + assertThat(resource.getId()).isEqualTo(resourceDefinitionId); + assertThat(resource.getTransferProcessId()).isEqualTo(transferProcessId); + assertThat(resource.getLocation()).isEqualTo(location); + assertThat(resource.getStorageClass()).isEqualTo(storageClass); + }); + assertThat(response.getSecretToken()).isInstanceOfSatisfying(GcpAccessToken.class, secretToken -> { + assertThat(secretToken.getToken()).isEqualTo("token"); + }); + + verify(storageServiceMock).getOrCreateBucket(bucketName, bucketLocation); + verify(iamServiceMock).createDefaultAccessToken(); + } + + @Test + void provisionWithImpersonationSuccess() { + var resourceDefinitionId = "id"; + var location = "location"; + var storageClass = "storage-class"; + var serviceAccount = new GcpServiceAccount("test-sa", "sa-name", "description"); + var token = new GcpAccessToken("token", 123); + var transferProcessId = UUID.randomUUID().toString(); + var resourceDefinition = createResourceDefinition(resourceDefinitionId, location, + storageClass, transferProcessId, serviceAccount.getName()); + var bucketName = resourceDefinition.getId(); + var bucket = new GcsBucket(bucketName); + var bucketLocation = resourceDefinition.getLocation(); + + when(gcpConfiguration.getServiceAccountName()).thenReturn(serviceAccount.getName()); + + 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); + doNothing().when(storageServiceMock).addProviderPermissions(bucket, serviceAccount); var response = provisioner.provision(resourceDefinition, testPolicy).join().getContent(); @@ -98,8 +137,7 @@ void provisionSuccess() { }); verify(storageServiceMock).getOrCreateBucket(bucketName, bucketLocation); - verify(storageServiceMock).addProviderPermissions(bucket, serviceAccount); - verify(iamServiceMock).createAccessToken(serviceAccount); + verify(iamServiceMock).createAccessToken(any()); } @Test @@ -108,6 +146,7 @@ void provisionSucceedsIfBucketNotEmpty() { var bucketName = resourceDefinition.getId(); var bucketLocation = resourceDefinition.getLocation(); + when(gcpConfiguration.getServiceAccountName()).thenReturn(null); when(storageServiceMock.getOrCreateBucket(bucketName, bucketLocation)).thenReturn(new GcsBucket(bucketName)); when(storageServiceMock.isEmpty(bucketName)).thenReturn(false); @@ -116,8 +155,7 @@ void provisionSucceedsIfBucketNotEmpty() { assertThat(response.failed()).isFalse(); verify(storageServiceMock).getOrCreateBucket(bucketName, bucketLocation); - verify(storageServiceMock, times(1)).addProviderPermissions(any(), any()); - verify(iamServiceMock, times(1)).createAccessToken(any()); + verify(iamServiceMock, times(1)).createDefaultAccessToken(); } @Test @@ -147,11 +185,9 @@ void deprovisionSuccess() { var description = "sa-description"; var serviceAccount = new GcpServiceAccount(email, name, description); - doNothing().when(iamServiceMock).deleteServiceAccountIfExists(argThat(matches(serviceAccount))); var resource = createGcsProvisionedResource(email, name, id); var response = provisioner.deprovision(resource, testPolicy).join().getContent(); - verify(iamServiceMock).deleteServiceAccountIfExists(argThat(matches(serviceAccount))); assertThat(response.getProvisionedResourceId()).isEqualTo(id); } @@ -179,23 +215,12 @@ private GcsResourceDefinition createResourceDefinition(String id, String locatio .transferProcessId(transferProcessId).build(); } - @Test - void deprovisionFails() { - var email = "test-email"; - var name = "test-name"; - var id = "test-id"; - var description = "sa-description"; - var serviceAccount = new GcpServiceAccount(email, name, description); - GcsProvisionedResource resource = createGcsProvisionedResource(email, name, id); - - doThrow(new GcpException("some error")) - .when(iamServiceMock) - .deleteServiceAccountIfExists(argThat(matches(serviceAccount))); - var response = provisioner.deprovision(resource, testPolicy).join(); - - verify(iamServiceMock).deleteServiceAccountIfExists(argThat(matches(serviceAccount))); - assertThat(response.failed()).isTrue(); - assertThat(response.getFailure().status()).isEqualTo(ResponseStatus.FATAL_ERROR); + private GcsResourceDefinition createResourceDefinition(String id, String location, String storageClass, String transferProcessId, String serviceAccountName) { + return GcsResourceDefinition.Builder.newInstance().id(id) + .location(location).storageClass(storageClass) + .transferProcessId(transferProcessId) + .serviceAccountName(serviceAccountName) + .build(); } private ArgumentMatcher matches(GcpServiceAccount serviceAccount) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dbade88..ad051f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ googleCloudIamCredentials = "2.32.0" googleCloudStorage = "2.30.1" googleCloudSecretManager = "2.32.0" googleCloudCore = "2.28.0" +googleApisIam = "v2-rev20240108-2.0.0" [libraries] assertj = { module = "org.assertj:assertj-core", version = "3.24.2" } @@ -36,6 +37,7 @@ googlecloud-iam-admin = { module = "com.google.cloud:google-iam-admin", version. 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" } +googleapis-iam = { module = "com.google.apis:google-api-services-iam", version.ref = "googleApisIam" } [bundles]