32
32
package com .google .auth .oauth2 ;
33
33
34
34
import static com .google .common .base .MoreObjects .firstNonNull ;
35
+ import static com .google .common .base .Preconditions .checkNotNull ;
35
36
36
37
import com .google .api .client .http .GenericUrl ;
37
38
import com .google .api .client .http .HttpContent ;
45
46
import com .google .auth .ServiceAccountSigner ;
46
47
import com .google .auth .http .HttpCredentialsAdapter ;
47
48
import com .google .auth .http .HttpTransportFactory ;
49
+ import com .google .common .annotations .VisibleForTesting ;
48
50
import com .google .common .base .MoreObjects ;
49
51
import com .google .common .collect .ImmutableMap ;
50
52
import java .io .IOException ;
53
55
import java .text .SimpleDateFormat ;
54
56
import java .util .ArrayList ;
55
57
import java .util .Arrays ;
58
+ import java .util .Collection ;
56
59
import java .util .Date ;
57
60
import java .util .List ;
58
61
import java .util .Map ;
85
88
* </pre>
86
89
*/
87
90
public class ImpersonatedCredentials extends GoogleCredentials
88
- implements ServiceAccountSigner , IdTokenProvider {
91
+ implements ServiceAccountSigner , IdTokenProvider , QuotaProjectIdProvider {
89
92
90
93
private static final long serialVersionUID = -2133257318957488431L ;
91
94
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'" ;
@@ -101,12 +104,14 @@ public class ImpersonatedCredentials extends GoogleCredentials
101
104
private List <String > delegates ;
102
105
private List <String > scopes ;
103
106
private int lifetime ;
107
+ private String quotaProjectId ;
104
108
private final String transportFactoryClassName ;
105
109
106
110
private transient HttpTransportFactory transportFactory ;
107
111
108
112
/**
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.
110
115
* @param targetPrincipal the service account to impersonate
111
116
* @param delegates the chained list of delegates required to grant the final access_token. If
112
117
* set, the sequence of identities must have "Service Account Token Creator" capability
@@ -144,7 +149,52 @@ public static ImpersonatedCredentials create(
144
149
}
145
150
146
151
/**
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.
148
198
* @param targetPrincipal the service account to impersonate
149
199
* @param delegates the chained list of delegates required to grant the final access_token. If
150
200
* set, the sequence of identities must have "Service Account Token Creator" capability
@@ -179,6 +229,19 @@ public static ImpersonatedCredentials create(
179
229
.build ();
180
230
}
181
231
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
+
182
245
/**
183
246
* Returns the email field of the serviceAccount that is being impersonated.
184
247
*
@@ -189,10 +252,33 @@ public String getAccount() {
189
252
return this .targetPrincipal ;
190
253
}
191
254
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
+
192
274
int getLifetime () {
193
275
return this .lifetime ;
194
276
}
195
277
278
+ public void setTransportFactory (HttpTransportFactory httpTransportFactory ) {
279
+ this .transportFactory = httpTransportFactory ;
280
+ }
281
+
196
282
/**
197
283
* Signs the provided bytes using the private key associated with the impersonated service account
198
284
*
@@ -213,6 +299,89 @@ public byte[] sign(byte[] toSign) {
213
299
ImmutableMap .of ("delegates" , this .delegates ));
214
300
}
215
301
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
+
216
385
private ImpersonatedCredentials (Builder builder ) {
217
386
this .sourceCredentials = builder .getSourceCredentials ();
218
387
this .targetPrincipal = builder .getTargetPrincipal ();
@@ -223,6 +392,7 @@ private ImpersonatedCredentials(Builder builder) {
223
392
firstNonNull (
224
393
builder .getHttpTransportFactory (),
225
394
getFromServiceLoader (HttpTransportFactory .class , OAuth2Utils .HTTP_TRANSPORT_FACTORY ));
395
+ this .quotaProjectId = builder .quotaProjectId ;
226
396
this .transportFactoryClassName = this .transportFactory .getClass ().getName ();
227
397
if (this .delegates == null ) {
228
398
this .delegates = new ArrayList <String >();
@@ -318,7 +488,8 @@ public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.O
318
488
319
489
@ Override
320
490
public int hashCode () {
321
- return Objects .hash (sourceCredentials , targetPrincipal , delegates , scopes , lifetime );
491
+ return Objects .hash (
492
+ sourceCredentials , targetPrincipal , delegates , scopes , lifetime , quotaProjectId );
322
493
}
323
494
324
495
@ Override
@@ -330,6 +501,7 @@ public String toString() {
330
501
.add ("scopes" , scopes )
331
502
.add ("lifetime" , lifetime )
332
503
.add ("transportFactoryClassName" , transportFactoryClassName )
504
+ .add ("quotaProjectId" , quotaProjectId )
333
505
.toString ();
334
506
}
335
507
@@ -344,7 +516,8 @@ public boolean equals(Object obj) {
344
516
&& Objects .equals (this .delegates , other .delegates )
345
517
&& Objects .equals (this .scopes , other .scopes )
346
518
&& 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 );
348
521
}
349
522
350
523
public Builder toBuilder () {
@@ -363,6 +536,7 @@ public static class Builder extends GoogleCredentials.Builder {
363
536
private List <String > scopes ;
364
537
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS ;
365
538
private HttpTransportFactory transportFactory ;
539
+ private String quotaProjectId ;
366
540
367
541
protected Builder () {}
368
542
@@ -425,6 +599,11 @@ public HttpTransportFactory getHttpTransportFactory() {
425
599
return transportFactory ;
426
600
}
427
601
602
+ public Builder setQuotaProjectId (String quotaProjectId ) {
603
+ this .quotaProjectId = quotaProjectId ;
604
+ return this ;
605
+ }
606
+
428
607
public ImpersonatedCredentials build () {
429
608
return new ImpersonatedCredentials (this );
430
609
}
0 commit comments