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: Add iam endpoint override to ImpersonatedCredentials #910

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ private ImpersonatedCredentials initializeImpersonatedCredentials() {
.setTargetPrincipal(targetPrincipal)
.setScopes(new ArrayList<>(scopes))
.setLifetime(3600) // 1 hour in seconds
.setIamEndpointOverride(serviceAccountImpersonationUrl)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public class ImpersonatedCredentials extends GoogleCredentials
private List<String> scopes;
private int lifetime;
private String quotaProjectId;
private String iamEndpointOverride;
private final String transportFactoryClassName;

private transient HttpTransportFactory transportFactory;
Expand Down Expand Up @@ -192,6 +193,54 @@ public static ImpersonatedCredentials create(
.build();
}

/**
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It
* should be either a user account credential or a service account credential.
* @param targetPrincipal the service account to impersonate
* @param delegates the chained list of delegates required to grant the final access_token. If
* set, the sequence of identities must have "Service Account Token Creator" capability
* granted to the preceding identity. For example, if set to [serviceAccountB,
* serviceAccountC], the sourceCredential must have the Token Creator role on serviceAccountB.
* serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token
* Creator on target_principal. If unset, sourceCredential must have that role on
* targetPrincipal.
* @param scopes scopes to request during the authorization grant
* @param lifetime number of seconds the delegated credential should be valid. By default this
* value should be at most 3600. However, you can follow <a
* href='https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth'>these
* instructions</a> to set up the service account and extend the maximum lifetime to 43200 (12
* hours). If the given lifetime is 0, default value 3600 will be used instead when creating
* the credentials.
* @param transportFactory HTTP transport factory that creates the transport used to get access
* tokens.
* @param quotaProjectId the project used for quota and billing purposes. Should be null unless
* the caller wants to use a project different from the one that owns the impersonated
* credential for billing/quota purposes.
* @param iamEndpointOverride The full IAM endpoint override with the target_principal embedded.
* This is useful when supporting impersonation with regional endpoints.
lsirac marked this conversation as resolved.
Show resolved Hide resolved
* @return new credentials
*/
public static ImpersonatedCredentials create(
GoogleCredentials sourceCredentials,
String targetPrincipal,
List<String> delegates,
List<String> scopes,
int lifetime,
HttpTransportFactory transportFactory,
String quotaProjectId,
String iamEndpointOverride) {
return ImpersonatedCredentials.newBuilder()
.setSourceCredentials(sourceCredentials)
.setTargetPrincipal(targetPrincipal)
.setDelegates(delegates)
.setScopes(scopes)
.setLifetime(lifetime)
.setHttpTransportFactory(transportFactory)
.setQuotaProjectId(quotaProjectId)
.setIamEndpointOverride(iamEndpointOverride)
.build();
}

/**
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It
* should be either a user account credential or a service account credential.
Expand Down Expand Up @@ -257,6 +306,11 @@ public String getQuotaProjectId() {
return this.quotaProjectId;
}

@VisibleForTesting
String getIamEndpointOverride() {
return this.iamEndpointOverride;
}

@VisibleForTesting
List<String> getDelegates() {
return delegates;
Expand Down Expand Up @@ -320,9 +374,9 @@ static ImpersonatedCredentials fromJson(
String sourceCredentialsType;
String quotaProjectId;
String targetPrincipal;
String serviceAccountImpersonationUrl;
try {
String serviceAccountImpersonationUrl =
(String) json.get("service_account_impersonation_url");
serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
if (json.containsKey("delegates")) {
delegates = (List<String>) json.get("delegates");
}
Expand Down Expand Up @@ -354,6 +408,7 @@ static ImpersonatedCredentials fromJson(
.setLifetime(DEFAULT_LIFETIME_IN_SECONDS)
.setHttpTransportFactory(transportFactory)
.setQuotaProjectId(quotaProjectId)
.setIamEndpointOverride(serviceAccountImpersonationUrl)
.build();
}

Expand All @@ -370,6 +425,7 @@ public GoogleCredentials createScoped(Collection<String> scopes) {
.setDelegates(this.delegates)
.setHttpTransportFactory(this.transportFactory)
.setQuotaProjectId(this.quotaProjectId)
.setIamEndpointOverride(this.iamEndpointOverride)
.build();
}

Expand All @@ -393,6 +449,7 @@ private ImpersonatedCredentials(Builder builder) {
builder.getHttpTransportFactory(),
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
this.quotaProjectId = builder.quotaProjectId;
this.iamEndpointOverride = builder.iamEndpointOverride;
this.transportFactoryClassName = this.transportFactory.getClass().getName();
if (this.delegates == null) {
this.delegates = new ArrayList<String>();
Expand Down Expand Up @@ -424,7 +481,10 @@ public AccessToken refreshAccessToken() throws IOException {
HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(sourceCredentials);
HttpRequestFactory requestFactory = httpTransport.createRequestFactory();

String endpointUrl = String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
String endpointUrl =
this.iamEndpointOverride != null
? this.iamEndpointOverride
: String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
GenericUrl url = new GenericUrl(endpointUrl);

Map<String, Object> body =
Expand Down Expand Up @@ -489,7 +549,13 @@ public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.O
@Override
public int hashCode() {
return Objects.hash(
sourceCredentials, targetPrincipal, delegates, scopes, lifetime, quotaProjectId);
sourceCredentials,
targetPrincipal,
delegates,
scopes,
lifetime,
quotaProjectId,
iamEndpointOverride);
}

@Override
Expand All @@ -502,6 +568,7 @@ public String toString() {
.add("lifetime", lifetime)
.add("transportFactoryClassName", transportFactoryClassName)
.add("quotaProjectId", quotaProjectId)
.add("iamEndpointOverride", iamEndpointOverride)
.toString();
}

Expand All @@ -517,7 +584,8 @@ public boolean equals(Object obj) {
&& Objects.equals(this.scopes, other.scopes)
&& Objects.equals(this.lifetime, other.lifetime)
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
&& Objects.equals(this.quotaProjectId, other.quotaProjectId)
&& Objects.equals(this.iamEndpointOverride, other.iamEndpointOverride);
}

public Builder toBuilder() {
Expand All @@ -537,6 +605,7 @@ public static class Builder extends GoogleCredentials.Builder {
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
private HttpTransportFactory transportFactory;
private String quotaProjectId;
private String iamEndpointOverride;

protected Builder() {}

Expand Down Expand Up @@ -604,6 +673,11 @@ public Builder setQuotaProjectId(String quotaProjectId) {
return this;
}

public Builder setIamEndpointOverride(String iamEndpointOverride) {
this.iamEndpointOverride = iamEndpointOverride;
return this;
}

public ImpersonatedCredentials build() {
return new ImpersonatedCredentials(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOExceptio
ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL);
transportFactory.transport.setAccessToken(ImpersonatedCredentialsTest.ACCESS_TOKEN);
transportFactory.transport.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
transportFactory.transport.setAccessTokenEndpoint(
ImpersonatedCredentialsTest.IMPERSONATION_URL);

InputStream impersonationCredentialsStream =
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
Expand Down Expand Up @@ -307,6 +309,8 @@ void fromStream_Impersonation_providesToken_WithoutQuotaProject() throws IOExcep
ImpersonatedCredentialsTest.IMPERSONATED_CLIENT_EMAIL);
transportFactory.transport.setAccessToken(ImpersonatedCredentialsTest.ACCESS_TOKEN);
transportFactory.transport.setExpireTime(ImpersonatedCredentialsTest.getDefaultExpireTime());
transportFactory.transport.setAccessTokenEndpoint(
ImpersonatedCredentialsTest.IMPERSONATION_URL);

InputStream impersonationCredentialsStream =
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,14 @@ class ImpersonatedCredentialsTest extends BaseSerializationTest {
private static JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
public static final String IMPERSONATION_URL =
public static final String DEFAULT_IMPERSONATION_URL =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
+ IMPERSONATED_CLIENT_EMAIL
+ ":generateAccessToken";
public static final String IMPERSONATION_URL =
"https://us-east1-iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
+ IMPERSONATED_CLIENT_EMAIL
+ ":generateAccessToken";
private static final String USER_ACCOUNT_CLIENT_ID =
"76408650-6qr441hur.apps.googleusercontent.com";
private static final String USER_ACCOUNT_CLIENT_SECRET = "d-F499q74hFpdHD0T5";
Expand Down Expand Up @@ -180,6 +184,7 @@ void fromJson_userAsSource_WithQuotaProjectId() throws IOException {
ImpersonatedCredentials credentials =
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
assertEquals(IMPERSONATION_URL, credentials.getIamEndpointOverride());
assertEquals(QUOTA_PROJECT_ID, credentials.getQuotaProjectId());
assertEquals(DELEGATES, credentials.getDelegates());
assertEquals(new ArrayList<String>(), credentials.getScopes());
Expand All @@ -201,6 +206,7 @@ void fromJson_userAsSource_WithoutQuotaProjectId() throws IOException {
ImpersonatedCredentials credentials =
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
assertEquals(IMPERSONATION_URL, credentials.getIamEndpointOverride());
assertNull(credentials.getQuotaProjectId());
assertEquals(DELEGATES, credentials.getDelegates());
assertEquals(new ArrayList<String>(), credentials.getScopes());
Expand All @@ -223,6 +229,7 @@ void fromJson_userAsSource_MissingDelegatesField() throws IOException {
ImpersonatedCredentials credentials =
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
assertEquals(IMPERSONATION_URL, credentials.getIamEndpointOverride());
assertNull(credentials.getQuotaProjectId());
assertEquals(new ArrayList<String>(), credentials.getDelegates());
assertEquals(new ArrayList<String>(), credentials.getScopes());
Expand All @@ -238,6 +245,7 @@ void fromJson_ServiceAccountAsSource() throws IOException {
ImpersonatedCredentials credentials =
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
assertEquals(IMPERSONATION_URL, credentials.getIamEndpointOverride());
assertEquals(QUOTA_PROJECT_ID, credentials.getQuotaProjectId());
assertEquals(DELEGATES, credentials.getDelegates());
assertEquals(new ArrayList<String>(), credentials.getScopes());
Expand Down Expand Up @@ -329,6 +337,25 @@ void createScopedWithImmutableScopes() {
assertEquals(Arrays.asList("scope1", "scope2"), scoped_credentials.getScopes());
}

@Test
void createScopedWithIamEndpointOverride() {
ImpersonatedCredentials targetCredentials =
ImpersonatedCredentials.create(
sourceCredentials,
IMPERSONATED_CLIENT_EMAIL,
DELEGATES,
IMMUTABLE_SCOPES_LIST,
VALID_LIFETIME,
mockTransportFactory,
QUOTA_PROJECT_ID,
IMPERSONATION_URL);

ImpersonatedCredentials scoped_credentials =
(ImpersonatedCredentials) targetCredentials.createScoped(IMMUTABLE_SCOPES_SET);
assertEquals(
targetCredentials.getIamEndpointOverride(), scoped_credentials.getIamEndpointOverride());
}

@Test
void refreshAccessToken_unauthorized() throws IOException {

Expand Down Expand Up @@ -449,6 +476,29 @@ void refreshAccessToken_success() throws IOException, IllegalStateException {
mockTransportFactory);

assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue());
assertEquals(DEFAULT_IMPERSONATION_URL, mockTransportFactory.transport.getRequest().getUrl());
}

@Test
void refreshAccessToken_endpointOverride() throws IOException, IllegalStateException {
mockTransportFactory.transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
mockTransportFactory.transport.setAccessToken(ACCESS_TOKEN);
mockTransportFactory.transport.setExpireTime(getDefaultExpireTime());
mockTransportFactory.transport.setAccessTokenEndpoint(IMPERSONATION_URL);

ImpersonatedCredentials targetCredentials =
ImpersonatedCredentials.create(
sourceCredentials,
IMPERSONATED_CLIENT_EMAIL,
null,
IMMUTABLE_SCOPES_LIST,
VALID_LIFETIME,
mockTransportFactory,
QUOTA_PROJECT_ID,
IMPERSONATION_URL);
aeitzman marked this conversation as resolved.
Show resolved Hide resolved

assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue());
assertEquals(IMPERSONATION_URL, mockTransportFactory.transport.getRequest().getUrl());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
/** Transport that simulates the IAMCredentials server for access tokens. */
public class MockIAMCredentialsServiceTransport extends MockHttpTransport {

private static final String IAM_ACCESS_TOKEN_ENDPOINT =
private static final String DEFAULT_IAM_ACCESS_TOKEN_ENDPOINT =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken";
private static final String IAM_ID_TOKEN_ENDPOINT =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken";
Expand All @@ -58,6 +58,7 @@ public class MockIAMCredentialsServiceTransport extends MockHttpTransport {
private byte[] signedBlob;
private int responseCode = HttpStatusCodes.STATUS_CODE_OK;
private String errorMessage;
private String iamAccessTokenEndpoint;

private String accessToken;
private String expireTime;
Expand Down Expand Up @@ -101,6 +102,10 @@ public void setIdToken(String idToken) {
this.idToken = idToken;
}

public void setAccessTokenEndpoint(String accessTokenEndpoint) {
this.iamAccessTokenEndpoint = accessTokenEndpoint;
}

public MockLowLevelHttpRequest getRequest() {
return request;
}
Expand All @@ -109,7 +114,9 @@ public MockLowLevelHttpRequest getRequest() {
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {

String iamAccesssTokenformattedUrl =
String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
iamAccessTokenEndpoint != null
? iamAccessTokenEndpoint
: String.format(DEFAULT_IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
String iamSignBlobformattedUrl = String.format(IAM_SIGN_ENDPOINT, this.targetPrincipal);
String iamIdTokenformattedUrl = String.format(IAM_ID_TOKEN_ENDPOINT, this.targetPrincipal);
if (url.equals(iamAccesssTokenformattedUrl)) {
Expand Down