Skip to content

Commit

Permalink
Added iam endpoint override to ImpersonatedCredentials
Browse files Browse the repository at this point in the history
  • Loading branch information
aeitzman committed Apr 8, 2022
1 parent 6e59931 commit 0fea3f6
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 8 deletions.
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.
* @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,9 @@ public String getQuotaProjectId() {
return this.quotaProjectId;
}

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

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

Expand Down Expand Up @@ -393,6 +447,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 +479,8 @@ 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 +545,7 @@ 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 +558,7 @@ public String toString() {
.add("lifetime", lifetime)
.add("transportFactoryClassName", transportFactoryClassName)
.add("quotaProjectId", quotaProjectId)
.add("iamEndpointOverride", iamEndpointOverride)
.toString();
}

Expand All @@ -517,7 +574,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 +595,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 +663,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 @@ -120,7 +120,7 @@ class ImpersonatedCredentialsTest extends BaseSerializationTest {

private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
public static final String IMPERSONATION_URL =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
"https://us-east1-iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
+ IMPERSONATED_CLIENT_EMAIL
+ ":generateAccessToken";
private static final String USER_ACCOUNT_CLIENT_ID =
Expand Down Expand Up @@ -180,6 +180,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 +202,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 +225,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 +241,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 @@ -451,6 +455,28 @@ void refreshAccessToken_success() throws IOException, IllegalStateException {
assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue());
}

@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);

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

@Test
void getRequestMetadata_withQuotaProjectId() throws IOException, IllegalStateException {

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,15 +102,19 @@ public void setIdToken(String idToken) {
this.idToken = idToken;
}

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

public MockLowLevelHttpRequest getRequest() {
return request;
}

@Override
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {

String iamAccesssTokenformattedUrl =
String.format(IAM_ACCESS_TOKEN_ENDPOINT, this.targetPrincipal);
String iamAccesssTokenformattedUrl = 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

0 comments on commit 0fea3f6

Please sign in to comment.