3232package com .google .auth .oauth2 ;
3333
3434import static com .google .common .base .MoreObjects .firstNonNull ;
35+ import static com .google .common .base .Preconditions .checkNotNull ;
3536
3637import com .google .api .client .http .GenericUrl ;
3738import com .google .api .client .http .HttpContent ;
4546import com .google .auth .ServiceAccountSigner ;
4647import com .google .auth .http .HttpCredentialsAdapter ;
4748import com .google .auth .http .HttpTransportFactory ;
49+ import com .google .common .annotations .VisibleForTesting ;
4850import com .google .common .base .MoreObjects ;
4951import com .google .common .collect .ImmutableMap ;
5052import java .io .IOException ;
5355import java .text .SimpleDateFormat ;
5456import java .util .ArrayList ;
5557import java .util .Arrays ;
58+ import java .util .Collection ;
5659import java .util .Date ;
5760import java .util .List ;
5861import java .util .Map ;
8588 * </pre>
8689 */
8790public 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