Skip to content

Commit 6449ef0

Browse files
authored
fix: service account impersonation with workforce credentials (googleapis#770)
* fix: service account impersonation with workforce credentials * fix: add old constructors * fix: add one test for service account impersonation with a workforce IdentityPoolCredential * fix: code review * fix: remove workforce methods from IdentityPoolCredentials * fix: can't remove setWorkforcePoolUserProject in Builder
1 parent ff399e7 commit 6449ef0

File tree

6 files changed

+404
-257
lines changed

6 files changed

+404
-257
lines changed

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

Lines changed: 6 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import com.google.api.client.http.HttpResponse;
3838
import com.google.api.client.json.GenericJson;
3939
import com.google.api.client.json.JsonParser;
40-
import com.google.auth.http.HttpTransportFactory;
4140
import com.google.common.annotations.VisibleForTesting;
4241
import java.io.IOException;
4342
import java.io.UnsupportedEncodingException;
@@ -49,7 +48,6 @@
4948
import java.util.Map;
5049
import java.util.regex.Matcher;
5150
import java.util.regex.Pattern;
52-
import javax.annotation.Nullable;
5351

5452
/**
5553
* AWS credentials representing a third-party identity for calling Google APIs.
@@ -114,39 +112,10 @@ static class AwsCredentialSource extends CredentialSource {
114112

115113
private final AwsCredentialSource awsCredentialSource;
116114

117-
/**
118-
* Internal constructor. See {@link
119-
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
120-
* String, CredentialSource, String, String, String, String, String, Collection,
121-
* EnvironmentProvider)}
122-
*/
123-
AwsCredentials(
124-
HttpTransportFactory transportFactory,
125-
String audience,
126-
String subjectTokenType,
127-
String tokenUrl,
128-
AwsCredentialSource credentialSource,
129-
@Nullable String tokenInfoUrl,
130-
@Nullable String serviceAccountImpersonationUrl,
131-
@Nullable String quotaProjectId,
132-
@Nullable String clientId,
133-
@Nullable String clientSecret,
134-
@Nullable Collection<String> scopes,
135-
@Nullable EnvironmentProvider environmentProvider) {
136-
super(
137-
transportFactory,
138-
audience,
139-
subjectTokenType,
140-
tokenUrl,
141-
credentialSource,
142-
tokenInfoUrl,
143-
serviceAccountImpersonationUrl,
144-
quotaProjectId,
145-
clientId,
146-
clientSecret,
147-
scopes,
148-
environmentProvider);
149-
this.awsCredentialSource = credentialSource;
115+
/** Internal constructor. See {@link AwsCredentials.Builder}. */
116+
AwsCredentials(Builder builder) {
117+
super(builder);
118+
this.awsCredentialSource = (AwsCredentialSource) builder.credentialSource;
150119
}
151120

152121
@Override
@@ -192,19 +161,7 @@ public String retrieveSubjectToken() throws IOException {
192161
/** Clones the AwsCredentials with the specified scopes. */
193162
@Override
194163
public GoogleCredentials createScoped(Collection<String> newScopes) {
195-
return new AwsCredentials(
196-
transportFactory,
197-
getAudience(),
198-
getSubjectTokenType(),
199-
getTokenUrl(),
200-
awsCredentialSource,
201-
getTokenInfoUrl(),
202-
getServiceAccountImpersonationUrl(),
203-
getQuotaProjectId(),
204-
getClientId(),
205-
getClientSecret(),
206-
newScopes,
207-
getEnvironmentProvider());
164+
return new AwsCredentials((AwsCredentials.Builder) newBuilder(this).setScopes(newScopes));
208165
}
209166

210167
private String retrieveResource(String url, String resourceName) throws IOException {
@@ -342,19 +299,7 @@ public static class Builder extends ExternalAccountCredentials.Builder {
342299

343300
@Override
344301
public AwsCredentials build() {
345-
return new AwsCredentials(
346-
transportFactory,
347-
audience,
348-
subjectTokenType,
349-
tokenUrl,
350-
(AwsCredentialSource) credentialSource,
351-
tokenInfoUrl,
352-
serviceAccountImpersonationUrl,
353-
quotaProjectId,
354-
clientId,
355-
clientSecret,
356-
scopes,
357-
environmentProvider);
302+
return new AwsCredentials(this);
358303
}
359304
}
360305
}

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

Lines changed: 142 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,19 @@ abstract static class CredentialSource {
8989
@Nullable private final String clientId;
9090
@Nullable private final String clientSecret;
9191

92+
// This is used for Workforce Pools. It is passed to STS during token exchange in the
93+
// `options` param and will be embedded in the token by STS.
94+
@Nullable private final String workforcePoolUserProject;
95+
9296
protected transient HttpTransportFactory transportFactory;
9397

9498
@Nullable protected final ImpersonatedCredentials impersonatedCredentials;
9599

96100
private EnvironmentProvider environmentProvider;
97101

98102
/**
99-
* Constructor with minimum identifying information and custom HTTP transport.
103+
* Constructor with minimum identifying information and custom HTTP transport. Does not support
104+
* workforce credentials.
100105
*
101106
* @param transportFactory HTTP transport factory, creates the transport used to get access tokens
102107
* @param audience the STS audience which is usually the fully specified resource name of the
@@ -181,6 +186,49 @@ protected ExternalAccountCredentials(
181186
(scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes;
182187
this.environmentProvider =
183188
environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;
189+
this.workforcePoolUserProject = null;
190+
191+
validateTokenUrl(tokenUrl);
192+
if (serviceAccountImpersonationUrl != null) {
193+
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
194+
}
195+
196+
this.impersonatedCredentials = initializeImpersonatedCredentials();
197+
}
198+
199+
/**
200+
* Internal constructor with minimum identifying information and custom HTTP transport. See {@link
201+
* ExternalAccountCredentials.Builder}.
202+
*/
203+
protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) {
204+
this.transportFactory =
205+
MoreObjects.firstNonNull(
206+
builder.transportFactory,
207+
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
208+
this.transportFactoryClassName = checkNotNull(this.transportFactory.getClass().getName());
209+
this.audience = checkNotNull(builder.audience);
210+
this.subjectTokenType = checkNotNull(builder.subjectTokenType);
211+
this.tokenUrl = checkNotNull(builder.tokenUrl);
212+
this.credentialSource = checkNotNull(builder.credentialSource);
213+
this.tokenInfoUrl = builder.tokenInfoUrl;
214+
this.serviceAccountImpersonationUrl = builder.serviceAccountImpersonationUrl;
215+
this.quotaProjectId = builder.quotaProjectId;
216+
this.clientId = builder.clientId;
217+
this.clientSecret = builder.clientSecret;
218+
this.scopes =
219+
(builder.scopes == null || builder.scopes.isEmpty())
220+
? Arrays.asList(CLOUD_PLATFORM_SCOPE)
221+
: builder.scopes;
222+
this.environmentProvider =
223+
builder.environmentProvider == null
224+
? SystemEnvironmentProvider.getInstance()
225+
: builder.environmentProvider;
226+
227+
this.workforcePoolUserProject = builder.workforcePoolUserProject;
228+
if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
229+
throw new IllegalArgumentException(
230+
"The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.");
231+
}
184232

185233
validateTokenUrl(tokenUrl);
186234
if (serviceAccountImpersonationUrl != null) {
@@ -312,23 +360,21 @@ static ExternalAccountCredentials fromJson(
312360
String userProject = (String) json.get("workforce_pool_user_project");
313361

314362
if (isAwsCredential(credentialSourceMap)) {
315-
return new AwsCredentials(
316-
transportFactory,
317-
audience,
318-
subjectTokenType,
319-
tokenUrl,
320-
new AwsCredentialSource(credentialSourceMap),
321-
tokenInfoUrl,
322-
serviceAccountImpersonationUrl,
323-
quotaProjectId,
324-
clientId,
325-
clientSecret,
326-
/* scopes= */ null,
327-
/* environmentProvider= */ null);
363+
return AwsCredentials.newBuilder()
364+
.setHttpTransportFactory(transportFactory)
365+
.setAudience(audience)
366+
.setSubjectTokenType(subjectTokenType)
367+
.setTokenUrl(tokenUrl)
368+
.setTokenInfoUrl(tokenInfoUrl)
369+
.setCredentialSource(new AwsCredentialSource(credentialSourceMap))
370+
.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
371+
.setQuotaProjectId(quotaProjectId)
372+
.setClientId(clientId)
373+
.setClientSecret(clientSecret)
374+
.build();
328375
}
329376

330377
return IdentityPoolCredentials.newBuilder()
331-
.setWorkforcePoolUserProject(userProject)
332378
.setHttpTransportFactory(transportFactory)
333379
.setAudience(audience)
334380
.setSubjectTokenType(subjectTokenType)
@@ -339,6 +385,7 @@ static ExternalAccountCredentials fromJson(
339385
.setQuotaProjectId(quotaProjectId)
340386
.setClientId(clientId)
341387
.setClientSecret(clientSecret)
388+
.setWorkforcePoolUserProject(userProject)
342389
.build();
343390
}
344391

@@ -361,13 +408,25 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
361408
return impersonatedCredentials.refreshAccessToken();
362409
}
363410

364-
StsRequestHandler requestHandler =
411+
StsRequestHandler.Builder requestHandler =
365412
StsRequestHandler.newBuilder(
366-
tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory())
367-
.setInternalOptions(stsTokenExchangeRequest.getInternalOptions())
368-
.build();
413+
tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory());
414+
415+
// If this credential was initialized with a Workforce configuration then the
416+
// workforcePoolUserProject must passed to STS via the the internal options param.
417+
if (isWorkforcePoolConfiguration()) {
418+
GenericJson options = new GenericJson();
419+
options.setFactory(OAuth2Utils.JSON_FACTORY);
420+
options.put("userProject", workforcePoolUserProject);
421+
requestHandler.setInternalOptions(options.toString());
422+
}
423+
424+
if (stsTokenExchangeRequest.getInternalOptions() != null) {
425+
// Overwrite internal options. Let subclass handle setting options.
426+
requestHandler.setInternalOptions(stsTokenExchangeRequest.getInternalOptions());
427+
}
369428

370-
StsTokenExchangeResponse response = requestHandler.exchangeToken();
429+
StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
371430
return response.getAccessToken();
372431
}
373432

@@ -427,10 +486,26 @@ public Collection<String> getScopes() {
427486
return scopes;
428487
}
429488

489+
@Nullable
490+
public String getWorkforcePoolUserProject() {
491+
return workforcePoolUserProject;
492+
}
493+
430494
EnvironmentProvider getEnvironmentProvider() {
431495
return environmentProvider;
432496
}
433497

498+
/**
499+
* Returns whether or not the current configuration is for Workforce Pools (which enable 3p user
500+
* identities, rather than workloads).
501+
*/
502+
public boolean isWorkforcePoolConfiguration() {
503+
Pattern workforceAudiencePattern =
504+
Pattern.compile("^//iam.googleapis.com/locations/.+/workforcePools/.+/providers/.+$");
505+
return workforcePoolUserProject != null
506+
&& workforceAudiencePattern.matcher(getAudience()).matches();
507+
}
508+
434509
static void validateTokenUrl(String tokenUrl) {
435510
List<Pattern> patterns = new ArrayList<>();
436511
patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\.sts\\.googleapis\\.com$"));
@@ -501,6 +576,7 @@ public abstract static class Builder extends GoogleCredentials.Builder {
501576
@Nullable protected String clientId;
502577
@Nullable protected String clientSecret;
503578
@Nullable protected Collection<String> scopes;
579+
@Nullable protected String workforcePoolUserProject;
504580

505581
protected Builder() {}
506582

@@ -517,60 +593,95 @@ protected Builder(ExternalAccountCredentials credentials) {
517593
this.clientSecret = credentials.clientSecret;
518594
this.scopes = credentials.scopes;
519595
this.environmentProvider = credentials.environmentProvider;
596+
this.workforcePoolUserProject = credentials.workforcePoolUserProject;
520597
}
521598

599+
/** Sets the HTTP transport factory, creates the transport used to get access tokens. */
600+
public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
601+
this.transportFactory = transportFactory;
602+
return this;
603+
}
604+
605+
/**
606+
* Sets the STS audience which is usually the fully specified resource name of the
607+
* workload/workforce pool provider.
608+
*/
522609
public Builder setAudience(String audience) {
523610
this.audience = audience;
524611
return this;
525612
}
526613

614+
/**
615+
* Sets the STS subject token type based on the OAuth 2.0 token exchange spec. Indicates the
616+
* type of the security token in the credential file.
617+
*/
527618
public Builder setSubjectTokenType(String subjectTokenType) {
528619
this.subjectTokenType = subjectTokenType;
529620
return this;
530621
}
531622

623+
/** Sets the STS token exchange endpoint. */
532624
public Builder setTokenUrl(String tokenUrl) {
533625
this.tokenUrl = tokenUrl;
534626
return this;
535627
}
536628

537-
public Builder setTokenInfoUrl(String tokenInfoUrl) {
538-
this.tokenInfoUrl = tokenInfoUrl;
629+
/** Sets the external credential source. */
630+
public Builder setCredentialSource(CredentialSource credentialSource) {
631+
this.credentialSource = credentialSource;
539632
return this;
540633
}
541634

635+
/**
636+
* Sets the optional URL used for service account impersonation. This is only required when APIs
637+
* to be accessed have not integrated with UberMint. If this is not available, the STS returned
638+
* GCP access token is directly used.
639+
*/
542640
public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) {
543641
this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
544642
return this;
545643
}
546644

547-
public Builder setCredentialSource(CredentialSource credentialSource) {
548-
this.credentialSource = credentialSource;
549-
return this;
550-
}
551-
552-
public Builder setScopes(Collection<String> scopes) {
553-
this.scopes = scopes;
645+
/**
646+
* Sets the optional endpoint used to retrieve account related information. Required for gCloud
647+
* session account identification.
648+
*/
649+
public Builder setTokenInfoUrl(String tokenInfoUrl) {
650+
this.tokenInfoUrl = tokenInfoUrl;
554651
return this;
555652
}
556653

654+
/** Sets the optional project used for quota and billing purposes. */
557655
public Builder setQuotaProjectId(String quotaProjectId) {
558656
this.quotaProjectId = quotaProjectId;
559657
return this;
560658
}
561659

660+
/** Sets the optional client ID of the service account from the console. */
562661
public Builder setClientId(String clientId) {
563662
this.clientId = clientId;
564663
return this;
565664
}
566665

666+
/** Sets the optional client secret of the service account from the console. */
567667
public Builder setClientSecret(String clientSecret) {
568668
this.clientSecret = clientSecret;
569669
return this;
570670
}
571671

572-
public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
573-
this.transportFactory = transportFactory;
672+
/** Sets the optional scopes to request during the authorization grant. */
673+
public Builder setScopes(Collection<String> scopes) {
674+
this.scopes = scopes;
675+
return this;
676+
}
677+
678+
/**
679+
* Sets the optional workforce pool user project number when the credential corresponds to a
680+
* workforce pool and not a workload identity pool. The underlying principal must still have
681+
* serviceusage.services.use IAM permission to use the project for billing/quota.
682+
*/
683+
public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
684+
this.workforcePoolUserProject = workforcePoolUserProject;
574685
return this;
575686
}
576687

0 commit comments

Comments
 (0)