Skip to content

Commit d1280e9

Browse files
authored
Merge branch 'golang:master' into master
2 parents 968f208 + d3ed0bb commit d1280e9

File tree

5 files changed

+275
-49
lines changed

5 files changed

+275
-49
lines changed

google/google.go

+27-3
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
9292

9393
// JSON key file types.
9494
const (
95-
serviceAccountKey = "service_account"
96-
userCredentialsKey = "authorized_user"
97-
externalAccountKey = "external_account"
95+
serviceAccountKey = "service_account"
96+
userCredentialsKey = "authorized_user"
97+
externalAccountKey = "external_account"
98+
impersonatedServiceAccount = "impersonated_service_account"
9899
)
99100

100101
// credentialsFile is the unmarshalled representation of a credentials file.
@@ -121,8 +122,13 @@ type credentialsFile struct {
121122
TokenURLExternal string `json:"token_url"`
122123
TokenInfoURL string `json:"token_info_url"`
123124
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
125+
Delegates []string `json:"delegates"`
124126
CredentialSource externalaccount.CredentialSource `json:"credential_source"`
125127
QuotaProjectID string `json:"quota_project_id"`
128+
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
129+
130+
// Service account impersonation
131+
SourceCredentials *credentialsFile `json:"source_credentials"`
126132
}
127133

128134
func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
@@ -176,8 +182,26 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
176182
CredentialSource: f.CredentialSource,
177183
QuotaProjectID: f.QuotaProjectID,
178184
Scopes: params.Scopes,
185+
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
179186
}
180187
return cfg.TokenSource(ctx)
188+
case impersonatedServiceAccount:
189+
if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
190+
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
191+
}
192+
193+
ts, err := f.SourceCredentials.tokenSource(ctx, params)
194+
if err != nil {
195+
return nil, err
196+
}
197+
imp := externalaccount.ImpersonateTokenSource{
198+
Ctx: ctx,
199+
URL: f.ServiceAccountImpersonationURL,
200+
Scopes: params.Scopes,
201+
Ts: ts,
202+
Delegates: f.Delegates,
203+
}
204+
return oauth2.ReuseTokenSource(nil, imp), nil
181205
case "":
182206
return nil, errors.New("missing 'type' field in credentials")
183207
default:

google/internal/externalaccount/aws_test.go

+20-5
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,9 @@ func TestAwsCredential_BasicRequest(t *testing.T) {
540540
oldGetenv := getenv
541541
defer func() { getenv = oldGetenv }()
542542
getenv = setEnvironment(map[string]string{})
543+
oldNow := now
544+
defer func() { now = oldNow }()
545+
now = setTime(defaultTime)
543546

544547
base, err := tfc.parse(context.Background())
545548
if err != nil {
@@ -560,7 +563,7 @@ func TestAwsCredential_BasicRequest(t *testing.T) {
560563
)
561564

562565
if got, want := out, expected; !reflect.DeepEqual(got, want) {
563-
t.Errorf("subjectToken = %q, want %q", got, want)
566+
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
564567
}
565568
}
566569

@@ -575,6 +578,9 @@ func TestAwsCredential_BasicRequestWithoutSecurityToken(t *testing.T) {
575578
oldGetenv := getenv
576579
defer func() { getenv = oldGetenv }()
577580
getenv = setEnvironment(map[string]string{})
581+
oldNow := now
582+
defer func() { now = oldNow }()
583+
now = setTime(defaultTime)
578584

579585
base, err := tfc.parse(context.Background())
580586
if err != nil {
@@ -595,7 +601,7 @@ func TestAwsCredential_BasicRequestWithoutSecurityToken(t *testing.T) {
595601
)
596602

597603
if got, want := out, expected; !reflect.DeepEqual(got, want) {
598-
t.Errorf("subjectToken = %q, want %q", got, want)
604+
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
599605
}
600606
}
601607

@@ -613,6 +619,9 @@ func TestAwsCredential_BasicRequestWithEnv(t *testing.T) {
613619
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
614620
"AWS_REGION": "us-west-1",
615621
})
622+
oldNow := now
623+
defer func() { now = oldNow }()
624+
now = setTime(defaultTime)
616625

617626
base, err := tfc.parse(context.Background())
618627
if err != nil {
@@ -633,7 +642,7 @@ func TestAwsCredential_BasicRequestWithEnv(t *testing.T) {
633642
)
634643

635644
if got, want := out, expected; !reflect.DeepEqual(got, want) {
636-
t.Errorf("subjectToken = %q, want %q", got, want)
645+
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
637646
}
638647
}
639648

@@ -651,6 +660,9 @@ func TestAwsCredential_BasicRequestWithDefaultEnv(t *testing.T) {
651660
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
652661
"AWS_DEFAULT_REGION": "us-west-1",
653662
})
663+
oldNow := now
664+
defer func() { now = oldNow }()
665+
now = setTime(defaultTime)
654666

655667
base, err := tfc.parse(context.Background())
656668
if err != nil {
@@ -670,7 +682,7 @@ func TestAwsCredential_BasicRequestWithDefaultEnv(t *testing.T) {
670682
)
671683

672684
if got, want := out, expected; !reflect.DeepEqual(got, want) {
673-
t.Errorf("subjectToken = %q, want %q", got, want)
685+
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
674686
}
675687
}
676688

@@ -689,6 +701,9 @@ func TestAwsCredential_BasicRequestWithTwoRegions(t *testing.T) {
689701
"AWS_REGION": "us-west-1",
690702
"AWS_DEFAULT_REGION": "us-east-1",
691703
})
704+
oldNow := now
705+
defer func() { now = oldNow }()
706+
now = setTime(defaultTime)
692707

693708
base, err := tfc.parse(context.Background())
694709
if err != nil {
@@ -708,7 +723,7 @@ func TestAwsCredential_BasicRequestWithTwoRegions(t *testing.T) {
708723
)
709724

710725
if got, want := out, expected; !reflect.DeepEqual(got, want) {
711-
t.Errorf("subjectToken = %q, want %q", got, want)
726+
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
712727
}
713728
}
714729

google/internal/externalaccount/basecredentials.go

+32-8
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ type Config struct {
5353
QuotaProjectID string
5454
// Scopes contains the desired scopes for the returned access token.
5555
Scopes []string
56+
// The optional workforce pool user project number when the credential
57+
// corresponds to a workforce pool and not a workload identity pool.
58+
// The underlying principal must still have serviceusage.services.use IAM
59+
// permission to use the project for billing/quota.
60+
WorkforcePoolUserProject string
5661
}
5762

5863
// Each element consists of a list of patterns. validateURLs checks for matches
@@ -73,6 +78,7 @@ var (
7378
regexp.MustCompile(`^iamcredentials\.[^\.\s\/\\]+\.googleapis\.com$`),
7479
regexp.MustCompile(`^[^\.\s\/\\]+-iamcredentials\.googleapis\.com$`),
7580
}
81+
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
7682
)
7783

7884
func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
@@ -86,14 +92,17 @@ func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
8692
toTest := parsed.Host
8793

8894
for _, pattern := range patterns {
89-
90-
if valid := pattern.MatchString(toTest); valid {
95+
if pattern.MatchString(toTest) {
9196
return true
9297
}
9398
}
9499
return false
95100
}
96101

102+
func validateWorkforceAudience(input string) bool {
103+
return validWorkforceAudiencePattern.MatchString(input)
104+
}
105+
97106
// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
98107
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
99108
return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
@@ -115,6 +124,13 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
115124
}
116125
}
117126

127+
if c.WorkforcePoolUserProject != "" {
128+
valid := validateWorkforceAudience(c.Audience)
129+
if !valid {
130+
return nil, fmt.Errorf("oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials")
131+
}
132+
}
133+
118134
ts := tokenSource{
119135
ctx: ctx,
120136
conf: c,
@@ -124,11 +140,11 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
124140
}
125141
scopes := c.Scopes
126142
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
127-
imp := impersonateTokenSource{
128-
ctx: ctx,
129-
url: c.ServiceAccountImpersonationURL,
130-
scopes: scopes,
131-
ts: oauth2.ReuseTokenSource(nil, ts),
143+
imp := ImpersonateTokenSource{
144+
Ctx: ctx,
145+
URL: c.ServiceAccountImpersonationURL,
146+
Scopes: scopes,
147+
Ts: oauth2.ReuseTokenSource(nil, ts),
132148
}
133149
return oauth2.ReuseTokenSource(nil, imp), nil
134150
}
@@ -224,7 +240,15 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
224240
ClientID: conf.ClientID,
225241
ClientSecret: conf.ClientSecret,
226242
}
227-
stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil)
243+
var options map[string]interface{}
244+
// Do not pass workforce_pool_user_project when client authentication is used.
245+
// The client ID is sufficient for determining the user project.
246+
if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
247+
options = map[string]interface{}{
248+
"userProject": conf.WorkforcePoolUserProject,
249+
}
250+
}
251+
stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
228252
if err != nil {
229253
return nil, err
230254
}

0 commit comments

Comments
 (0)