Skip to content

Commit

Permalink
feat: Add iam endpoint override to ImpersonatedCredentials (googleapi…
Browse files Browse the repository at this point in the history
…s#910)

* feat: Added iam endpoint override to ImpersonatedCredentials

* fix: Fixed GoogleCredentialsTests that were broken by regional impersonated credential url change

* fix: Addressed code review comments

* fix: fixed createScoped method in impersonatedCredentials to use override endpoint correctly and added test

* fix: fixed linter errors

Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>
  • Loading branch information
aeitzman and lsirac committed Apr 26, 2022
1 parent 112bfc9 commit 97bfc4c
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 8 deletions.
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
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,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
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
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);

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

@Test
Expand Down
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

0 comments on commit 97bfc4c

Please sign in to comment.