Skip to content

Commit b9823f7

Browse files
liuchaorenelharo
andauthored
feat: add impersonation credentials to ADC (#613)
* ADC can load impersonation credentials * Add tests for new features in ImpersonationCredentials * Add tests for GoogleCredentials * Fix linter errors * Fix linter errors in ImpersonatedCredentialsTest * Fix issues after receiving comments * Fix lint errors * Handle ClassCastException in fromJson * Fix lint errors * minor refactoring * fix doc strings * fix lint errors * delegates can be missing from the json file * Mark test using @test() * Remove redundant methods and handle exceptions * add an empty file * remove an empty file * Fix docstring and move one variable to inner scope. * Refactor ImpersonatedCredentialsTest * Reformat the ImpersonatedCredentialsTest * Remove redundant checks in tests * Use VisibleForTesting annotation to limit visibility Co-authored-by: Elliotte Rusty Harold <[email protected]>
1 parent 3f90c81 commit b9823f7

File tree

5 files changed

+644
-181
lines changed

5 files changed

+644
-181
lines changed

oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ private ImpersonatedCredentials initializeImpersonatedCredentials() {
200200
.build();
201201
}
202202

203-
String targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
203+
String targetPrincipal =
204+
ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
204205
return ImpersonatedCredentials.newBuilder()
205206
.setSourceCredentials(sourceCredentials)
206207
.setHttpTransportFactory(transportFactory)
@@ -359,19 +360,6 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
359360
return response.getAccessToken();
360361
}
361362

362-
private static String extractTargetPrincipal(String serviceAccountImpersonationUrl) {
363-
// Extract the target principal.
364-
int startIndex = serviceAccountImpersonationUrl.lastIndexOf('/');
365-
int endIndex = serviceAccountImpersonationUrl.indexOf(":generateAccessToken");
366-
367-
if (startIndex != -1 && endIndex != -1 && startIndex < endIndex) {
368-
return serviceAccountImpersonationUrl.substring(startIndex + 1, endIndex);
369-
} else {
370-
throw new IllegalArgumentException(
371-
"Unable to determine target principal from service account impersonation URL.");
372-
}
373-
}
374-
375363
/**
376364
* Retrieves the external subject token to be exchanged for a GCP access token.
377365
*

oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,12 @@ public static GoogleCredentials create(AccessToken accessToken) {
7878
* <ol>
7979
* <li>Credentials file pointed to by the {@code GOOGLE_APPLICATION_CREDENTIALS} environment
8080
* variable
81-
* <li>Credentials provided by the Google Cloud SDK {@code gcloud auth application-default
82-
* login} command
81+
* <li>Credentials provided by the Google Cloud SDK.
82+
* <ol>
83+
* <li>{@code gcloud auth application-default login} for user account credentials.
84+
* <li>{@code gcloud auth application-default login --impersonate-service-account} for
85+
* impersonated service account credentials.
86+
* </ol>
8387
* <li>Google App Engine built-in credentials
8488
* <li>Google Cloud Shell built-in credentials
8589
* <li>Google Compute Engine built-in credentials
@@ -169,6 +173,9 @@ public static GoogleCredentials fromStream(
169173
if (ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE.equals(fileType)) {
170174
return ExternalAccountCredentials.fromJson(fileContents, transportFactory);
171175
}
176+
if ("impersonated_service_account".equals(fileType)) {
177+
return ImpersonatedCredentials.fromJson(fileContents, transportFactory);
178+
}
172179
throw new IOException(
173180
String.format(
174181
"Error reading credentials from stream, 'type' value '%s' not recognized."

oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java

Lines changed: 184 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
package com.google.auth.oauth2;
3333

3434
import static com.google.common.base.MoreObjects.firstNonNull;
35+
import static com.google.common.base.Preconditions.checkNotNull;
3536

3637
import com.google.api.client.http.GenericUrl;
3738
import com.google.api.client.http.HttpContent;
@@ -45,6 +46,7 @@
4546
import com.google.auth.ServiceAccountSigner;
4647
import com.google.auth.http.HttpCredentialsAdapter;
4748
import com.google.auth.http.HttpTransportFactory;
49+
import com.google.common.annotations.VisibleForTesting;
4850
import com.google.common.base.MoreObjects;
4951
import com.google.common.collect.ImmutableMap;
5052
import java.io.IOException;
@@ -53,6 +55,7 @@
5355
import java.text.SimpleDateFormat;
5456
import java.util.ArrayList;
5557
import java.util.Arrays;
58+
import java.util.Collection;
5659
import java.util.Date;
5760
import java.util.List;
5861
import java.util.Map;
@@ -85,7 +88,7 @@
8588
* </pre>
8689
*/
8790
public class ImpersonatedCredentials extends GoogleCredentials
88-
implements ServiceAccountSigner, IdTokenProvider {
91+
implements ServiceAccountSigner, IdTokenProvider, QuotaProjectIdProvider {
8992

9093
private static final long serialVersionUID = -2133257318957488431L;
9194
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
@@ -101,12 +104,14 @@ public class ImpersonatedCredentials extends GoogleCredentials
101104
private List<String> delegates;
102105
private List<String> scopes;
103106
private int lifetime;
107+
private String quotaProjectId;
104108
private final String transportFactoryClassName;
105109

106110
private transient HttpTransportFactory transportFactory;
107111

108112
/**
109-
* @param sourceCredentials the source credential used as to acquire the impersonated credentials
113+
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It
114+
* should be either a user account credential or a service account credential.
110115
* @param targetPrincipal the service account to impersonate
111116
* @param delegates the chained list of delegates required to grant the final access_token. If
112117
* set, the sequence of identities must have "Service Account Token Creator" capability
@@ -144,7 +149,52 @@ public static ImpersonatedCredentials create(
144149
}
145150

146151
/**
147-
* @param sourceCredentials the source credential used as to acquire the impersonated credentials
152+
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It
153+
* should be either a user account credential or a service account credential.
154+
* @param targetPrincipal the service account to impersonate
155+
* @param delegates the chained list of delegates required to grant the final access_token. If
156+
* set, the sequence of identities must have "Service Account Token Creator" capability
157+
* granted to the preceding identity. For example, if set to [serviceAccountB,
158+
* serviceAccountC], the sourceCredential must have the Token Creator role on serviceAccountB.
159+
* serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token
160+
* Creator on target_principal. If unset, sourceCredential must have that role on
161+
* targetPrincipal.
162+
* @param scopes scopes to request during the authorization grant
163+
* @param lifetime number of seconds the delegated credential should be valid. By default this
164+
* value should be at most 3600. However, you can follow <a
165+
* href='https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth'>these
166+
* instructions</a> to set up the service account and extend the maximum lifetime to 43200 (12
167+
* hours). If the given lifetime is 0, default value 3600 will be used instead when creating
168+
* the credentials.
169+
* @param transportFactory HTTP transport factory that creates the transport used to get access
170+
* tokens.
171+
* @param quotaProjectId the project used for quota and billing purposes. Should be null unless
172+
* the caller wants to use a project different from the one that owns the impersonated
173+
* credential for billing/quota purposes.
174+
* @return new credentials
175+
*/
176+
public static ImpersonatedCredentials create(
177+
GoogleCredentials sourceCredentials,
178+
String targetPrincipal,
179+
List<String> delegates,
180+
List<String> scopes,
181+
int lifetime,
182+
HttpTransportFactory transportFactory,
183+
String quotaProjectId) {
184+
return ImpersonatedCredentials.newBuilder()
185+
.setSourceCredentials(sourceCredentials)
186+
.setTargetPrincipal(targetPrincipal)
187+
.setDelegates(delegates)
188+
.setScopes(scopes)
189+
.setLifetime(lifetime)
190+
.setHttpTransportFactory(transportFactory)
191+
.setQuotaProjectId(quotaProjectId)
192+
.build();
193+
}
194+
195+
/**
196+
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It
197+
* should be either a user account credential or a service account credential.
148198
* @param targetPrincipal the service account to impersonate
149199
* @param delegates the chained list of delegates required to grant the final access_token. If
150200
* set, the sequence of identities must have "Service Account Token Creator" capability
@@ -179,6 +229,19 @@ public static ImpersonatedCredentials create(
179229
.build();
180230
}
181231

232+
static String extractTargetPrincipal(String serviceAccountImpersonationUrl) {
233+
// Extract the target principal.
234+
int startIndex = serviceAccountImpersonationUrl.lastIndexOf('/');
235+
int endIndex = serviceAccountImpersonationUrl.indexOf(":generateAccessToken");
236+
237+
if (startIndex != -1 && endIndex != -1 && startIndex < endIndex) {
238+
return serviceAccountImpersonationUrl.substring(startIndex + 1, endIndex);
239+
} else {
240+
throw new IllegalArgumentException(
241+
"Unable to determine target principal from service account impersonation URL.");
242+
}
243+
}
244+
182245
/**
183246
* Returns the email field of the serviceAccount that is being impersonated.
184247
*
@@ -189,10 +252,33 @@ public String getAccount() {
189252
return this.targetPrincipal;
190253
}
191254

255+
@Override
256+
public String getQuotaProjectId() {
257+
return this.quotaProjectId;
258+
}
259+
260+
@VisibleForTesting
261+
List<String> getDelegates() {
262+
return delegates;
263+
}
264+
265+
@VisibleForTesting
266+
List<String> getScopes() {
267+
return scopes;
268+
}
269+
270+
public GoogleCredentials getSourceCredentials() {
271+
return sourceCredentials;
272+
}
273+
192274
int getLifetime() {
193275
return this.lifetime;
194276
}
195277

278+
public void setTransportFactory(HttpTransportFactory httpTransportFactory) {
279+
this.transportFactory = httpTransportFactory;
280+
}
281+
196282
/**
197283
* Signs the provided bytes using the private key associated with the impersonated service account
198284
*
@@ -213,6 +299,89 @@ public byte[] sign(byte[] toSign) {
213299
ImmutableMap.of("delegates", this.delegates));
214300
}
215301

302+
/**
303+
* Returns impersonation account credentials defined by JSON using the format generated by gCloud.
304+
* The source credentials in the JSON should be either user account credentials or service account
305+
* credentials.
306+
*
307+
* @param json a map from the JSON representing the credentials
308+
* @param transportFactory HTTP transport factory, creates the transport used to get access tokens
309+
* @return the credentials defined by the JSON
310+
* @throws IOException if the credential cannot be created from the JSON.
311+
*/
312+
static ImpersonatedCredentials fromJson(
313+
Map<String, Object> json, HttpTransportFactory transportFactory) throws IOException {
314+
315+
checkNotNull(json);
316+
checkNotNull(transportFactory);
317+
318+
List<String> delegates = null;
319+
Map<String, Object> sourceCredentialsJson;
320+
String sourceCredentialsType;
321+
String quotaProjectId;
322+
String targetPrincipal;
323+
try {
324+
String serviceAccountImpersonationUrl =
325+
(String) json.get("service_account_impersonation_url");
326+
if (json.containsKey("delegates")) {
327+
delegates = (List<String>) json.get("delegates");
328+
}
329+
sourceCredentialsJson = (Map<String, Object>) json.get("source_credentials");
330+
sourceCredentialsType = (String) sourceCredentialsJson.get("type");
331+
quotaProjectId = (String) json.get("quota_project_id");
332+
targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
333+
} catch (ClassCastException | NullPointerException | IllegalArgumentException e) {
334+
throw new CredentialFormatException("An invalid input stream was provided.", e);
335+
}
336+
337+
GoogleCredentials sourceCredentials;
338+
if (GoogleCredentials.USER_FILE_TYPE.equals(sourceCredentialsType)) {
339+
sourceCredentials = UserCredentials.fromJson(sourceCredentialsJson, transportFactory);
340+
} else if (GoogleCredentials.SERVICE_ACCOUNT_FILE_TYPE.equals(sourceCredentialsType)) {
341+
sourceCredentials =
342+
ServiceAccountCredentials.fromJson(sourceCredentialsJson, transportFactory);
343+
} else {
344+
throw new IOException(
345+
String.format(
346+
"A credential of type %s is not supported as source credential for impersonation.",
347+
sourceCredentialsType));
348+
}
349+
return ImpersonatedCredentials.newBuilder()
350+
.setSourceCredentials(sourceCredentials)
351+
.setTargetPrincipal(targetPrincipal)
352+
.setDelegates(delegates)
353+
.setScopes(new ArrayList<String>())
354+
.setLifetime(DEFAULT_LIFETIME_IN_SECONDS)
355+
.setHttpTransportFactory(transportFactory)
356+
.setQuotaProjectId(quotaProjectId)
357+
.build();
358+
}
359+
360+
@Override
361+
public boolean createScopedRequired() {
362+
return this.scopes == null || this.scopes.isEmpty();
363+
}
364+
365+
@Override
366+
public GoogleCredentials createScoped(Collection<String> scopes) {
367+
return toBuilder()
368+
.setScopes((List<String>) scopes)
369+
.setLifetime(this.lifetime)
370+
.setDelegates(this.delegates)
371+
.setHttpTransportFactory(this.transportFactory)
372+
.setQuotaProjectId(this.quotaProjectId)
373+
.build();
374+
}
375+
376+
@Override
377+
protected Map<String, List<String>> getAdditionalHeaders() {
378+
Map<String, List<String>> headers = super.getAdditionalHeaders();
379+
if (quotaProjectId != null) {
380+
return addQuotaProjectIdToRequestMetadata(quotaProjectId, headers);
381+
}
382+
return headers;
383+
}
384+
216385
private ImpersonatedCredentials(Builder builder) {
217386
this.sourceCredentials = builder.getSourceCredentials();
218387
this.targetPrincipal = builder.getTargetPrincipal();
@@ -223,6 +392,7 @@ private ImpersonatedCredentials(Builder builder) {
223392
firstNonNull(
224393
builder.getHttpTransportFactory(),
225394
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
395+
this.quotaProjectId = builder.quotaProjectId;
226396
this.transportFactoryClassName = this.transportFactory.getClass().getName();
227397
if (this.delegates == null) {
228398
this.delegates = new ArrayList<String>();
@@ -318,7 +488,8 @@ public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.O
318488

319489
@Override
320490
public int hashCode() {
321-
return Objects.hash(sourceCredentials, targetPrincipal, delegates, scopes, lifetime);
491+
return Objects.hash(
492+
sourceCredentials, targetPrincipal, delegates, scopes, lifetime, quotaProjectId);
322493
}
323494

324495
@Override
@@ -330,6 +501,7 @@ public String toString() {
330501
.add("scopes", scopes)
331502
.add("lifetime", lifetime)
332503
.add("transportFactoryClassName", transportFactoryClassName)
504+
.add("quotaProjectId", quotaProjectId)
333505
.toString();
334506
}
335507

@@ -344,7 +516,8 @@ public boolean equals(Object obj) {
344516
&& Objects.equals(this.delegates, other.delegates)
345517
&& Objects.equals(this.scopes, other.scopes)
346518
&& Objects.equals(this.lifetime, other.lifetime)
347-
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName);
519+
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
520+
&& Objects.equals(this.quotaProjectId, other.quotaProjectId);
348521
}
349522

350523
public Builder toBuilder() {
@@ -363,6 +536,7 @@ public static class Builder extends GoogleCredentials.Builder {
363536
private List<String> scopes;
364537
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS;
365538
private HttpTransportFactory transportFactory;
539+
private String quotaProjectId;
366540

367541
protected Builder() {}
368542

@@ -425,6 +599,11 @@ public HttpTransportFactory getHttpTransportFactory() {
425599
return transportFactory;
426600
}
427601

602+
public Builder setQuotaProjectId(String quotaProjectId) {
603+
this.quotaProjectId = quotaProjectId;
604+
return this;
605+
}
606+
428607
public ImpersonatedCredentials build() {
429608
return new ImpersonatedCredentials(this);
430609
}

0 commit comments

Comments
 (0)