Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: GCS provisioner using ADC or existing service account for access tokens #111

Merged
Merged
16 changes: 11 additions & 5 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
maven/mavencentral/com.fasterxml.jackson.core/jackson-annotations/2.10.3, Apache-2.0, approved, CQ21280

Check warning on line 1 in DEPENDENCIES

View workflow job for this annotation

GitHub Actions / check / Dash-Verify-Licenses

Restricted Dependencies found

Some dependencies are marked 'restricted' - please review them
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-core/2.16.0, Apache-2.0 AND MIT, approved, #11602
Expand Down Expand Up @@ -28,6 +28,7 @@
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
Expand All @@ -52,14 +53,21 @@
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
Expand All @@ -77,6 +85,7 @@
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
Expand Down Expand Up @@ -117,9 +126,7 @@
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
Expand All @@ -128,7 +135,6 @@
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
Expand Down Expand Up @@ -232,7 +238,7 @@
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
Expand Down
1 change: 1 addition & 0 deletions extensions/common/gcp/gcp-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,81 +16,52 @@

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;
import org.eclipse.edc.gcp.common.GcpServiceAccount;
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<String> 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<IAMClient> iamClientSupplier;
private final Supplier<IamCredentialsClient> iamCredentialsClientSupplier;
private final Monitor monitor;
private final String gcpProjectId;
private Supplier<IAMClient> iamClientSupplier;
private Supplier<IamCredentialsClient> iamCredentialsClientSupplier;
private AccessTokenProvider applicationDefaultCredentials;

private IamServiceImpl(Monitor monitor,
String gcpProjectId,
Supplier<IAMClient> iamClientSupplier,
Supplier<IamCredentialsClient> 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);
}
}

Expand All @@ -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);
Expand All @@ -114,60 +85,56 @@ 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) {
return String.format("%s@%s.iam.gserviceaccount.com", name, project);
}

public static class Builder {
private final String gcpProjectId;
private final Monitor monitor;
private Supplier<IAMClient> iamClientSupplier;
private Supplier<IamCredentialsClient> 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) {
return new Builder(monitor, gcpProjectId);
}

public Builder iamClientSupplier(Supplier<IAMClient> iamClientSupplier) {
this.iamClientSupplier = iamClientSupplier;
iamServiceImpl.iamClientSupplier = iamClientSupplier;
return this;
}

public Builder iamCredentialsClientSupplier(Supplier<IamCredentialsClient> 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;
}

/**
Expand Down Expand Up @@ -196,4 +163,19 @@ private Supplier<IamCredentialsClient> 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;
}
}
}
}
Loading
Loading