Skip to content

Commit

Permalink
Merge branch 'golang:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
cgostuff authored Nov 14, 2021
2 parents 968f208 + d3ed0bb commit d1280e9
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 49 deletions.
30 changes: 27 additions & 3 deletions google/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {

// JSON key file types.
const (
serviceAccountKey = "service_account"
userCredentialsKey = "authorized_user"
externalAccountKey = "external_account"
serviceAccountKey = "service_account"
userCredentialsKey = "authorized_user"
externalAccountKey = "external_account"
impersonatedServiceAccount = "impersonated_service_account"
)

// credentialsFile is the unmarshalled representation of a credentials file.
Expand All @@ -121,8 +122,13 @@ type credentialsFile struct {
TokenURLExternal string `json:"token_url"`
TokenInfoURL string `json:"token_info_url"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
Delegates []string `json:"delegates"`
CredentialSource externalaccount.CredentialSource `json:"credential_source"`
QuotaProjectID string `json:"quota_project_id"`
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`

// Service account impersonation
SourceCredentials *credentialsFile `json:"source_credentials"`
}

func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
Expand Down Expand Up @@ -176,8 +182,26 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
CredentialSource: f.CredentialSource,
QuotaProjectID: f.QuotaProjectID,
Scopes: params.Scopes,
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
}
return cfg.TokenSource(ctx)
case impersonatedServiceAccount:
if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
}

ts, err := f.SourceCredentials.tokenSource(ctx, params)
if err != nil {
return nil, err
}
imp := externalaccount.ImpersonateTokenSource{
Ctx: ctx,
URL: f.ServiceAccountImpersonationURL,
Scopes: params.Scopes,
Ts: ts,
Delegates: f.Delegates,
}
return oauth2.ReuseTokenSource(nil, imp), nil
case "":
return nil, errors.New("missing 'type' field in credentials")
default:
Expand Down
25 changes: 20 additions & 5 deletions google/internal/externalaccount/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,9 @@ func TestAwsCredential_BasicRequest(t *testing.T) {
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
getenv = setEnvironment(map[string]string{})
oldNow := now
defer func() { now = oldNow }()
now = setTime(defaultTime)

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

if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = %q, want %q", got, want)
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}

Expand All @@ -575,6 +578,9 @@ func TestAwsCredential_BasicRequestWithoutSecurityToken(t *testing.T) {
oldGetenv := getenv
defer func() { getenv = oldGetenv }()
getenv = setEnvironment(map[string]string{})
oldNow := now
defer func() { now = oldNow }()
now = setTime(defaultTime)

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

if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = %q, want %q", got, want)
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}

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

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

if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = %q, want %q", got, want)
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}

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

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

if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = %q, want %q", got, want)
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}

Expand All @@ -689,6 +701,9 @@ func TestAwsCredential_BasicRequestWithTwoRegions(t *testing.T) {
"AWS_REGION": "us-west-1",
"AWS_DEFAULT_REGION": "us-east-1",
})
oldNow := now
defer func() { now = oldNow }()
now = setTime(defaultTime)

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

if got, want := out, expected; !reflect.DeepEqual(got, want) {
t.Errorf("subjectToken = %q, want %q", got, want)
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
}
}

Expand Down
40 changes: 32 additions & 8 deletions google/internal/externalaccount/basecredentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ type Config struct {
QuotaProjectID string
// Scopes contains the desired scopes for the returned access token.
Scopes []string
// The optional workforce pool user project number when the credential
// corresponds to a workforce pool and not a workload identity pool.
// The underlying principal must still have serviceusage.services.use IAM
// permission to use the project for billing/quota.
WorkforcePoolUserProject string
}

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

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

for _, pattern := range patterns {

if valid := pattern.MatchString(toTest); valid {
if pattern.MatchString(toTest) {
return true
}
}
return false
}

func validateWorkforceAudience(input string) bool {
return validWorkforceAudiencePattern.MatchString(input)
}

// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
Expand All @@ -115,6 +124,13 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
}
}

if c.WorkforcePoolUserProject != "" {
valid := validateWorkforceAudience(c.Audience)
if !valid {
return nil, fmt.Errorf("oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials")
}
}

ts := tokenSource{
ctx: ctx,
conf: c,
Expand All @@ -124,11 +140,11 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
}
scopes := c.Scopes
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
imp := impersonateTokenSource{
ctx: ctx,
url: c.ServiceAccountImpersonationURL,
scopes: scopes,
ts: oauth2.ReuseTokenSource(nil, ts),
imp := ImpersonateTokenSource{
Ctx: ctx,
URL: c.ServiceAccountImpersonationURL,
Scopes: scopes,
Ts: oauth2.ReuseTokenSource(nil, ts),
}
return oauth2.ReuseTokenSource(nil, imp), nil
}
Expand Down Expand Up @@ -224,7 +240,15 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
ClientID: conf.ClientID,
ClientSecret: conf.ClientSecret,
}
stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil)
var options map[string]interface{}
// Do not pass workforce_pool_user_project when client authentication is used.
// The client ID is sufficient for determining the user project.
if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
options = map[string]interface{}{
"userProject": conf.WorkforcePoolUserProject,
}
}
stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit d1280e9

Please sign in to comment.