From 82aa92eca34f8ee4a249327d22931f42277aba69 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Mon, 18 Dec 2023 09:38:27 -0800 Subject: [PATCH 01/22] Adding programattic refreshable credentials --- google/internal/externalaccount/aws.go | 79 +++++++---- google/internal/externalaccount/aws_test.go | 124 +++++++++++++++++- .../externalaccount/basecredentials.go | 55 ++++++-- .../programmaticrefreshcredsource.go | 17 +++ .../programmaticrefreshcredsource_test.go | 71 ++++++++++ 5 files changed, 303 insertions(+), 43 deletions(-) create mode 100644 google/internal/externalaccount/programmaticrefreshcredsource.go create mode 100644 google/internal/externalaccount/programmaticrefreshcredsource_test.go diff --git a/google/internal/externalaccount/aws.go b/google/internal/externalaccount/aws.go index bd4efd19b..7ad9390ba 100644 --- a/google/internal/externalaccount/aws.go +++ b/google/internal/externalaccount/aws.go @@ -26,22 +26,28 @@ import ( "golang.org/x/oauth2" ) -type awsSecurityCredentials struct { - AccessKeyID string `json:"AccessKeyID"` +// Models AWS security credentials. +type AwsSecurityCredentials struct { + // AWS Access Key ID - Required. + AccessKeyID string `json:"AccessKeyID"` + // AWS Secret Access Key - Required. SecretAccessKey string `json:"SecretAccessKey"` - SecurityToken string `json:"Token"` + // AWS Session token - Optional. + SessionToken string `json:"Token"` } // awsRequestSigner is a utility class to sign http requests using a AWS V4 signature. type awsRequestSigner struct { RegionName string - AwsSecurityCredentials awsSecurityCredentials + AwsSecurityCredentials AwsSecurityCredentials } // getenv aliases os.Getenv for testing var getenv = os.Getenv const ( + defaultRegionalCredentialVerificationUrl = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" + // AWS Signature Version 4 signing algorithm identifier. awsAlgorithm = "AWS4-HMAC-SHA256" @@ -197,8 +203,8 @@ func (rs *awsRequestSigner) SignRequest(req *http.Request) error { signedRequest.Header.Add("host", requestHost(req)) - if rs.AwsSecurityCredentials.SecurityToken != "" { - signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SecurityToken) + if rs.AwsSecurityCredentials.SessionToken != "" { + signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SessionToken) } if signedRequest.Header.Get("date") == "" { @@ -251,16 +257,17 @@ func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp } type awsCredentialSource struct { - EnvironmentID string - RegionURL string - RegionalCredVerificationURL string - CredVerificationURL string - IMDSv2SessionTokenURL string - TargetResource string - requestSigner *awsRequestSigner - region string - ctx context.Context - client *http.Client + EnvironmentID string + RegionURL string + RegionalCredVerificationURL string + CredVerificationURL string + IMDSv2SessionTokenURL string + TargetResource string + requestSigner *awsRequestSigner + Region string + ctx context.Context + client *http.Client + AwsSecurityCredentialsSupplier func() (AwsSecurityCredentials, error) } type awsRequestHeader struct { @@ -292,18 +299,25 @@ func canRetrieveSecurityCredentialFromEnvironment() bool { return getenv(awsAccessKeyId) != "" && getenv(awsSecretAccessKey) != "" } -func shouldUseMetadataServer() bool { - return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment() +func (cs awsCredentialSource) shouldUseMetadataServer() bool { + return cs.AwsSecurityCredentialsSupplier == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()) } func (cs awsCredentialSource) credentialSourceType() string { + if cs.AwsSecurityCredentialsSupplier != nil { + return "programmatic" + } return "aws" } func (cs awsCredentialSource) subjectToken() (string, error) { + // Set Defaults + if cs.RegionalCredVerificationURL == "" { + cs.RegionalCredVerificationURL = defaultRegionalCredentialVerificationUrl + } if cs.requestSigner == nil { headers := make(map[string]string) - if shouldUseMetadataServer() { + if cs.shouldUseMetadataServer() { awsSessionToken, err := cs.getAWSSessionToken() if err != nil { return "", err @@ -318,20 +332,20 @@ func (cs awsCredentialSource) subjectToken() (string, error) { if err != nil { return "", err } - - if cs.region, err = cs.getRegion(headers); err != nil { + cs.Region, err = cs.getRegion(headers) + if err != nil { return "", err } cs.requestSigner = &awsRequestSigner{ - RegionName: cs.region, + RegionName: cs.Region, AwsSecurityCredentials: awsSecurityCredentials, } } // Generate the signed request to AWS STS GetCallerIdentity API. // Use the required regional endpoint. Otherwise, the request will fail. - req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.region, 1), nil) + req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.Region, 1), nil) if err != nil { return "", err } @@ -417,6 +431,12 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) { } func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) { + if cs.Region != "" { + return cs.Region, nil + } + if cs.AwsSecurityCredentialsSupplier != nil { + return "", errors.New("oauth2/google: AwsRegion must be provided when using an AwsSecurityCredentialsSupplier") + } if canRetrieveRegionFromEnvironment() { if envAwsRegion := getenv(awsRegion); envAwsRegion != "" { return envAwsRegion, nil @@ -461,12 +481,15 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err return string(respBody[:respBodyEnd]), nil } -func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result awsSecurityCredentials, err error) { +func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result AwsSecurityCredentials, err error) { + if cs.AwsSecurityCredentialsSupplier != nil { + return cs.AwsSecurityCredentialsSupplier() + } if canRetrieveSecurityCredentialFromEnvironment() { - return awsSecurityCredentials{ + return AwsSecurityCredentials{ AccessKeyID: getenv(awsAccessKeyId), SecretAccessKey: getenv(awsSecretAccessKey), - SecurityToken: getenv(awsSessionToken), + SessionToken: getenv(awsSessionToken), }, nil } @@ -491,8 +514,8 @@ func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) return credentials, nil } -func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (awsSecurityCredentials, error) { - var result awsSecurityCredentials +func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (AwsSecurityCredentials, error) { + var result AwsSecurityCredentials req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.CredVerificationURL, roleName), nil) if err != nil { diff --git a/google/internal/externalaccount/aws_test.go b/google/internal/externalaccount/aws_test.go index 28dc5284b..63eb1d913 100644 --- a/google/internal/externalaccount/aws_test.go +++ b/google/internal/externalaccount/aws_test.go @@ -7,6 +7,7 @@ package externalaccount import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" @@ -36,7 +37,7 @@ func setEnvironment(env map[string]string) func(string) string { var defaultRequestSigner = &awsRequestSigner{ RegionName: "us-east-1", - AwsSecurityCredentials: awsSecurityCredentials{ + AwsSecurityCredentials: AwsSecurityCredentials{ AccessKeyID: "AKIDEXAMPLE", SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, @@ -50,10 +51,10 @@ const ( var requestSignerWithToken = &awsRequestSigner{ RegionName: "us-east-2", - AwsSecurityCredentials: awsSecurityCredentials{ + AwsSecurityCredentials: AwsSecurityCredentials{ AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, - SecurityToken: securityToken, + SessionToken: securityToken, }, } @@ -388,7 +389,7 @@ func TestAWSv4Signature_PostRequestWithSecurityTokenAndAdditionalHeaders(t *test func TestAWSv4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) { var requestSigner = &awsRequestSigner{ RegionName: "us-east-2", - AwsSecurityCredentials: awsSecurityCredentials{ + AwsSecurityCredentials: AwsSecurityCredentials{ AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, }, @@ -541,10 +542,10 @@ func getExpectedSubjectToken(url, region, accessKeyID, secretAccessKey, security req.Header.Add("x-goog-cloud-target-resource", testFileConfig.Audience) signer := &awsRequestSigner{ RegionName: region, - AwsSecurityCredentials: awsSecurityCredentials{ + AwsSecurityCredentials: AwsSecurityCredentials{ AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, - SecurityToken: securityToken, + SessionToken: securityToken, }, } signer.SignRequest(req) @@ -1235,6 +1236,117 @@ func TestAWSCredential_ShouldCallMetadataEndpointWhenNoSecretAccessKey(t *testin } } +func TestAWSCredential_ProgrammaticAuth(t *testing.T) { + tfc := testFileConfig + securityCredentials := AwsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + SessionToken: securityToken, + } + + tfc.CredentialSource = CredentialSource{ + AwsSecurityCredentialsSupplier: func() (AwsSecurityCredentials, error) { + return securityCredentials, nil + }, + AwsRegion: "us-east-2", + } + + oldNow := now + defer func() { + now = oldNow + }() + now = setTime(defaultTime) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + expected := getExpectedSubjectToken( + "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", + "us-east-2", + accessKeyID, + secretAccessKey, + securityToken, + ) + + if got, want := out, expected; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = \n%q\n want \n%q", got, want) + } +} + +func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) { + tfc := testFileConfig + securityCredentials := AwsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + } + + tfc.CredentialSource = CredentialSource{ + AwsSecurityCredentialsSupplier: func() (AwsSecurityCredentials, error) { + return securityCredentials, nil + }, + AwsRegion: "us-east-2", + } + + oldNow := now + defer func() { + now = oldNow + }() + now = setTime(defaultTime) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + expected := getExpectedSubjectToken( + "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", + "us-east-2", + accessKeyID, + secretAccessKey, + "", + ) + + if got, want := out, expected; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = \n%q\n want \n%q", got, want) + } +} + +func TestAWSCredential_ProgrammaticAuthError(t *testing.T) { + tfc := testFileConfig + + tfc.CredentialSource = CredentialSource{ + AwsSecurityCredentialsSupplier: func() (AwsSecurityCredentials, error) { + return AwsSecurityCredentials{}, errors.New("test error") + }, + AwsRegion: "us-east-2", + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("subjectToken() should have failed") + } + if got, want := err.Error(), "test error"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} + func TestAwsCredential_CredentialSourceType(t *testing.T) { server := createDefaultAwsTestServer() ts := httptest.NewServer(server) diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 33288d367..5c6129690 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -107,8 +107,9 @@ func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSo // Subject token file types. const ( - fileTypeText = "text" - fileTypeJSON = "json" + fileTypeText = "text" + fileTypeJSON = "json" + defaultTokenUrl = "https://sts.googleapis.com/v1/token" ) type format struct { @@ -119,22 +120,42 @@ type format struct { } // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange. -// One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question. +// One field amongst File, URL, Executable, SubjectTokenSupplier, or AwsSecurityCredentialSupplier +// should be filled, depending on the kind of credential in question. // The EnvironmentID should start with AWS if being used for an AWS credential. type CredentialSource struct { + // File location for file sourced credentials. File string `json:"file"` - URL string `json:"url"` + // Url to call for URL sourced credentials. + URL string `json:"url"` + // Headers to attach to the request for URL sourced credentials. Headers map[string]string `json:"headers"` + // Configuration object for executable sourced credentials. Executable *ExecutableConfig `json:"executable"` - EnvironmentID string `json:"environment_id"` - RegionURL string `json:"region_url"` + // Environment ID used for AWS sourced credentials. + EnvironmentID string `json:"environment_id"` + // Metadata URL to retrieve the region from for EC2 AWS credentials. + RegionURL string `json:"region_url"` + // AWS regional credential verification URL, will default to "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided." RegionalCredVerificationURL string `json:"regional_cred_verification_url"` - CredVerificationURL string `json:"cred_verification_url"` - IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"` - Format format `json:"format"` + // DEPRECATED + CredVerificationURL string `json:"cred_verification_url"` + // URL to retrieve the session token when using IMDSv2 in AWS. + IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"` + // Format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json". + Format format `json:"format"` + // AWS region, required when an AwsSecurityCredentials Supplier is provided. + AwsRegion string `json:"-"` // Ignore for json. + + // Token supplier for OIDC/SAML credentials. This should be a function that returns + // a valid subject token as a string. + SubjectTokenSupplier func() (string, error) `json:"-"` // Ignore for json. + // AWS Security Credential supplier for AWS credentials. This should be a function + // that returns a valid AwsSecurityCredentials object. + AwsSecurityCredentialsSupplier func() (AwsSecurityCredentials, error) `json:"-"` // Ignore for json. } type ExecutableConfig struct { @@ -145,6 +166,11 @@ type ExecutableConfig struct { // parse determines the type of CredentialSource needed. func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { + //set Defaults + if c.TokenURL == "" { + c.TokenURL = defaultTokenUrl + } + if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { if awsVersion != 1 { @@ -157,6 +183,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, CredVerificationURL: c.CredentialSource.URL, TargetResource: c.Audience, + Region: c.CredentialSource.AwsRegion, ctx: ctx, } if c.CredentialSource.IMDSv2SessionTokenURL != "" { @@ -165,12 +192,22 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { return awsCredSource, nil } + } else if c.CredentialSource.AwsSecurityCredentialsSupplier != nil { + awsCredSource := awsCredentialSource{ + Region: c.CredentialSource.AwsRegion, + RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, + AwsSecurityCredentialsSupplier: c.CredentialSource.AwsSecurityCredentialsSupplier, + TargetResource: c.Audience, + } + return awsCredSource, nil } else if c.CredentialSource.File != "" { return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil } else if c.CredentialSource.URL != "" { return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil } else if c.CredentialSource.Executable != nil { return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c) + } else if c.CredentialSource.SubjectTokenSupplier != nil { + return programmaticRefreshCredentialSource{SubjectTokenSupplier: c.CredentialSource.SubjectTokenSupplier}, nil } return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } diff --git a/google/internal/externalaccount/programmaticrefreshcredsource.go b/google/internal/externalaccount/programmaticrefreshcredsource.go new file mode 100644 index 000000000..6808930e1 --- /dev/null +++ b/google/internal/externalaccount/programmaticrefreshcredsource.go @@ -0,0 +1,17 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +type programmaticRefreshCredentialSource struct { + SubjectTokenSupplier func() (string, error) +} + +func (cs programmaticRefreshCredentialSource) credentialSourceType() string { + return "programmatic" +} + +func (cs programmaticRefreshCredentialSource) subjectToken() (string, error) { + return cs.SubjectTokenSupplier() +} diff --git a/google/internal/externalaccount/programmaticrefreshcredsource_test.go b/google/internal/externalaccount/programmaticrefreshcredsource_test.go new file mode 100644 index 000000000..9e5454826 --- /dev/null +++ b/google/internal/externalaccount/programmaticrefreshcredsource_test.go @@ -0,0 +1,71 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "errors" + "reflect" + "testing" +) + +func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) { + tfc := testConfig + + tfc.CredentialSource = CredentialSource{ + SubjectTokenSupplier: func() (string, error) { + return "subjectToken", nil + }, + } + + oldNow := now + defer func() { + now = oldNow + }() + now = setTime(defaultTime) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + out, err := base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + + if got, want := out, "subjectToken"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = \n%q\n want \n%q", got, want) + } +} + +func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) { + tfc := testConfig + + tfc.CredentialSource = CredentialSource{ + SubjectTokenSupplier: func() (string, error) { + return "", errors.New("test error") + }, + } + + oldNow := now + defer func() { + now = oldNow + }() + now = setTime(defaultTime) + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("subjectToken() should have failed") + } + if got, want := err.Error(), "test error"; !reflect.DeepEqual(got, want) { + t.Errorf("subjectToken = %q, want %q", got, want) + } +} From 0d0666520c7bc1d0a3cb4eac1fa388524797583d Mon Sep 17 00:00:00 2001 From: aeitzman Date: Tue, 9 Jan 2024 13:50:17 -0800 Subject: [PATCH 02/22] Addressing PR comments --- google/internal/externalaccount/aws.go | 25 +++++----- .../externalaccount/basecredentials.go | 50 ++++++++++--------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/google/internal/externalaccount/aws.go b/google/internal/externalaccount/aws.go index 7ad9390ba..d550ed5a4 100644 --- a/google/internal/externalaccount/aws.go +++ b/google/internal/externalaccount/aws.go @@ -26,13 +26,13 @@ import ( "golang.org/x/oauth2" ) -// Models AWS security credentials. +// AwsSecurityCredentials models AWS security credentials. type AwsSecurityCredentials struct { - // AWS Access Key ID - Required. + // AccessKeyId is the AWS Access Key ID - Required. AccessKeyID string `json:"AccessKeyID"` - // AWS Secret Access Key - Required. + // SecretAccessKey is the AWS Secret Access Key - Required. SecretAccessKey string `json:"SecretAccessKey"` - // AWS Session token - Optional. + // SessionToken is the AWS Session token - Optional. SessionToken string `json:"Token"` } @@ -332,9 +332,11 @@ func (cs awsCredentialSource) subjectToken() (string, error) { if err != nil { return "", err } - cs.Region, err = cs.getRegion(headers) - if err != nil { - return "", err + if cs.Region == "" { + cs.Region, err = cs.getRegion(headers) + if err != nil { + return "", err + } } cs.requestSigner = &awsRequestSigner{ @@ -431,12 +433,6 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) { } func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) { - if cs.Region != "" { - return cs.Region, nil - } - if cs.AwsSecurityCredentialsSupplier != nil { - return "", errors.New("oauth2/google: AwsRegion must be provided when using an AwsSecurityCredentialsSupplier") - } if canRetrieveRegionFromEnvironment() { if envAwsRegion := getenv(awsRegion); envAwsRegion != "" { return envAwsRegion, nil @@ -483,6 +479,9 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result AwsSecurityCredentials, err error) { if cs.AwsSecurityCredentialsSupplier != nil { + if cs.Region == "" { + return result, errors.New("oauth2/google: AwsRegion must be provided when using an AwsSecurityCredentialsSupplier") + } return cs.AwsSecurityCredentialsSupplier() } if canRetrieveSecurityCredentialFromEnvironment() { diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 5c6129690..432f3c8c9 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -123,37 +123,39 @@ type format struct { // One field amongst File, URL, Executable, SubjectTokenSupplier, or AwsSecurityCredentialSupplier // should be filled, depending on the kind of credential in question. // The EnvironmentID should start with AWS if being used for an AWS credential. +// AwsRegion is required when AwsSecurityCredentialsSupplier is used. type CredentialSource struct { - // File location for file sourced credentials. + // File is the location for file sourced credentials. File string `json:"file"` - // Url to call for URL sourced credentials. + // Url is the URL to call for URL sourced credentials. URL string `json:"url"` - // Headers to attach to the request for URL sourced credentials. + // Headers are the Headers to attach to the request for URL sourced credentials. Headers map[string]string `json:"headers"` - // Configuration object for executable sourced credentials. + // Executable is the configuration object for executable sourced credentials. Executable *ExecutableConfig `json:"executable"` - // Environment ID used for AWS sourced credentials. + // EnvironmentID is the EnvironmentID used for AWS sourced credentials. EnvironmentID string `json:"environment_id"` - // Metadata URL to retrieve the region from for EC2 AWS credentials. + // RegionURL is the metadata URL to retrieve the region from for EC2 AWS credentials. RegionURL string `json:"region_url"` - // AWS regional credential verification URL, will default to "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided." + // RegionalCredVerificationURL is the AWS regional credential verification URL, will default to + // "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided." RegionalCredVerificationURL string `json:"regional_cred_verification_url"` - // DEPRECATED + // CredVerificationURL is deprecated and not used. Use RegionalCredVerificationURL instead. CredVerificationURL string `json:"cred_verification_url"` - // URL to retrieve the session token when using IMDSv2 in AWS. + // IMDSv2SessionTokenURL is the URL to retrieve the session token when using IMDSv2 in AWS. IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"` - // Format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json". + // Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json". Format format `json:"format"` - // AWS region, required when an AwsSecurityCredentials Supplier is provided. + // AwsRegion is the AWS region, required when an AwsSecurityCredentialsSupplier is provided. AwsRegion string `json:"-"` // Ignore for json. - // Token supplier for OIDC/SAML credentials. This should be a function that returns + // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. This should be a function that returns // a valid subject token as a string. SubjectTokenSupplier func() (string, error) `json:"-"` // Ignore for json. - // AWS Security Credential supplier for AWS credentials. This should be a function + // AwsSecurityCredentialsSupplier is an optional AWS Security Credential supplier for AWS credentials. This should be a function // that returns a valid AwsSecurityCredentials object. AwsSecurityCredentialsSupplier func() (AwsSecurityCredentials, error) `json:"-"` // Ignore for json. } @@ -171,7 +173,17 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { c.TokenURL = defaultTokenUrl } - if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { + if c.CredentialSource.AwsSecurityCredentialsSupplier != nil { + awsCredSource := awsCredentialSource{ + Region: c.CredentialSource.AwsRegion, + RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, + AwsSecurityCredentialsSupplier: c.CredentialSource.AwsSecurityCredentialsSupplier, + TargetResource: c.Audience, + } + return awsCredSource, nil + } else if c.CredentialSource.SubjectTokenSupplier != nil { + return programmaticRefreshCredentialSource{SubjectTokenSupplier: c.CredentialSource.SubjectTokenSupplier}, nil + } else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { if awsVersion != 1 { return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion) @@ -192,22 +204,12 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { return awsCredSource, nil } - } else if c.CredentialSource.AwsSecurityCredentialsSupplier != nil { - awsCredSource := awsCredentialSource{ - Region: c.CredentialSource.AwsRegion, - RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, - AwsSecurityCredentialsSupplier: c.CredentialSource.AwsSecurityCredentialsSupplier, - TargetResource: c.Audience, - } - return awsCredSource, nil } else if c.CredentialSource.File != "" { return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil } else if c.CredentialSource.URL != "" { return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil } else if c.CredentialSource.Executable != nil { return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c) - } else if c.CredentialSource.SubjectTokenSupplier != nil { - return programmaticRefreshCredentialSource{SubjectTokenSupplier: c.CredentialSource.SubjectTokenSupplier}, nil } return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } From 85ffd0280f7d5fd9df04718a4bde0ef56ff4c2e2 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Tue, 16 Jan 2024 16:21:42 -0800 Subject: [PATCH 03/22] Using struct for AWS supplier to better match java implementation --- google/internal/externalaccount/aws.go | 30 ++++++++++--------- google/internal/externalaccount/aws_test.go | 24 +++++++++------ .../externalaccount/basecredentials.go | 22 +++++++------- 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/google/internal/externalaccount/aws.go b/google/internal/externalaccount/aws.go index d550ed5a4..61e3c7fcc 100644 --- a/google/internal/externalaccount/aws.go +++ b/google/internal/externalaccount/aws.go @@ -264,10 +264,10 @@ type awsCredentialSource struct { IMDSv2SessionTokenURL string TargetResource string requestSigner *awsRequestSigner - Region string + region string ctx context.Context client *http.Client - AwsSecurityCredentialsSupplier func() (AwsSecurityCredentials, error) + awsSecurityCredentialsSupplier *AwsSecurityCredentialsSupplier } type awsRequestHeader struct { @@ -300,11 +300,11 @@ func canRetrieveSecurityCredentialFromEnvironment() bool { } func (cs awsCredentialSource) shouldUseMetadataServer() bool { - return cs.AwsSecurityCredentialsSupplier == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()) + return cs.awsSecurityCredentialsSupplier == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()) } func (cs awsCredentialSource) credentialSourceType() string { - if cs.AwsSecurityCredentialsSupplier != nil { + if cs.awsSecurityCredentialsSupplier != nil { return "programmatic" } return "aws" @@ -332,22 +332,20 @@ func (cs awsCredentialSource) subjectToken() (string, error) { if err != nil { return "", err } - if cs.Region == "" { - cs.Region, err = cs.getRegion(headers) - if err != nil { - return "", err - } + cs.region, err = cs.getRegion(headers) + if err != nil { + return "", err } cs.requestSigner = &awsRequestSigner{ - RegionName: cs.Region, + RegionName: cs.region, AwsSecurityCredentials: awsSecurityCredentials, } } // Generate the signed request to AWS STS GetCallerIdentity API. // Use the required regional endpoint. Otherwise, the request will fail. - req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.Region, 1), nil) + req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.region, 1), nil) if err != nil { return "", err } @@ -433,8 +431,12 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) { } func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) { + if cs.awsSecurityCredentialsSupplier != nil { + return cs.awsSecurityCredentialsSupplier.AwsRegion, nil + } if canRetrieveRegionFromEnvironment() { if envAwsRegion := getenv(awsRegion); envAwsRegion != "" { + cs.region = envAwsRegion return envAwsRegion, nil } return getenv("AWS_DEFAULT_REGION"), nil @@ -478,11 +480,11 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err } func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result AwsSecurityCredentials, err error) { - if cs.AwsSecurityCredentialsSupplier != nil { - if cs.Region == "" { + if cs.awsSecurityCredentialsSupplier != nil { + if cs.awsSecurityCredentialsSupplier.AwsRegion == "" { return result, errors.New("oauth2/google: AwsRegion must be provided when using an AwsSecurityCredentialsSupplier") } - return cs.AwsSecurityCredentialsSupplier() + return cs.awsSecurityCredentialsSupplier.GetAwsSecurityCredentials() } if canRetrieveSecurityCredentialFromEnvironment() { return AwsSecurityCredentials{ diff --git a/google/internal/externalaccount/aws_test.go b/google/internal/externalaccount/aws_test.go index 63eb1d913..cd35c5da8 100644 --- a/google/internal/externalaccount/aws_test.go +++ b/google/internal/externalaccount/aws_test.go @@ -1245,10 +1245,12 @@ func TestAWSCredential_ProgrammaticAuth(t *testing.T) { } tfc.CredentialSource = CredentialSource{ - AwsSecurityCredentialsSupplier: func() (AwsSecurityCredentials, error) { - return securityCredentials, nil + AwsSecurityCredentialsSupplier: &AwsSecurityCredentialsSupplier{ + GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { + return securityCredentials, nil + }, + AwsRegion: "us-east-2", }, - AwsRegion: "us-east-2", } oldNow := now @@ -1288,10 +1290,12 @@ func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) { } tfc.CredentialSource = CredentialSource{ - AwsSecurityCredentialsSupplier: func() (AwsSecurityCredentials, error) { - return securityCredentials, nil + AwsSecurityCredentialsSupplier: &AwsSecurityCredentialsSupplier{ + GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { + return securityCredentials, nil + }, + AwsRegion: "us-east-2", }, - AwsRegion: "us-east-2", } oldNow := now @@ -1327,10 +1331,12 @@ func TestAWSCredential_ProgrammaticAuthError(t *testing.T) { tfc := testFileConfig tfc.CredentialSource = CredentialSource{ - AwsSecurityCredentialsSupplier: func() (AwsSecurityCredentials, error) { - return AwsSecurityCredentials{}, errors.New("test error") + AwsSecurityCredentialsSupplier: &AwsSecurityCredentialsSupplier{ + GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { + return AwsSecurityCredentials{}, errors.New("test error") + }, + AwsRegion: "us-east-2", }, - AwsRegion: "us-east-2", } base, err := tfc.parse(context.Background()) diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 432f3c8c9..8eca02113 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -120,10 +120,9 @@ type format struct { } // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange. -// One field amongst File, URL, Executable, SubjectTokenSupplier, or AwsSecurityCredentialSupplier +// One field amongst File, URL, Executable, SubjectTokenSupplier, or AwsSecurityCredentialSupplierConfig. // should be filled, depending on the kind of credential in question. // The EnvironmentID should start with AWS if being used for an AWS credential. -// AwsRegion is required when AwsSecurityCredentialsSupplier is used. type CredentialSource struct { // File is the location for file sourced credentials. File string `json:"file"` @@ -149,15 +148,13 @@ type CredentialSource struct { IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"` // Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json". Format format `json:"format"` - // AwsRegion is the AWS region, required when an AwsSecurityCredentialsSupplier is provided. - AwsRegion string `json:"-"` // Ignore for json. // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. This should be a function that returns // a valid subject token as a string. SubjectTokenSupplier func() (string, error) `json:"-"` // Ignore for json. - // AwsSecurityCredentialsSupplier is an optional AWS Security Credential supplier for AWS credentials. This should be a function - // that returns a valid AwsSecurityCredentials object. - AwsSecurityCredentialsSupplier func() (AwsSecurityCredentials, error) `json:"-"` // Ignore for json. + // AwsSecurityCredentialsSupplier is an optional AWS Security Credential supplier. This should contain a + // function that returns valid AwsSecurityCredentials and a valid AwsRegion. + AwsSecurityCredentialsSupplier *AwsSecurityCredentialsSupplier `json:"-"` // Ignore for json. } type ExecutableConfig struct { @@ -166,6 +163,13 @@ type ExecutableConfig struct { OutputFile string `json:"output_file"` } +type AwsSecurityCredentialsSupplier struct { + // AwsRegion is the AWS region. + AwsRegion string + // GetAwsSecurityCredentials is a function that should return valid AwsSecurityCredentials. + GetAwsSecurityCredentials func() (AwsSecurityCredentials, error) +} + // parse determines the type of CredentialSource needed. func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { //set Defaults @@ -175,9 +179,8 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { if c.CredentialSource.AwsSecurityCredentialsSupplier != nil { awsCredSource := awsCredentialSource{ - Region: c.CredentialSource.AwsRegion, RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, - AwsSecurityCredentialsSupplier: c.CredentialSource.AwsSecurityCredentialsSupplier, + awsSecurityCredentialsSupplier: c.CredentialSource.AwsSecurityCredentialsSupplier, TargetResource: c.Audience, } return awsCredSource, nil @@ -195,7 +198,6 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, CredVerificationURL: c.CredentialSource.URL, TargetResource: c.Audience, - Region: c.CredentialSource.AwsRegion, ctx: ctx, } if c.CredentialSource.IMDSv2SessionTokenURL != "" { From c1ac924f83b8779b08e9510b764a6e242051c980 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 18 Jan 2024 11:24:19 -0800 Subject: [PATCH 04/22] moving externalaccount package out of internal --- google/{internal => }/externalaccount/aws.go | 0 .../{internal => }/externalaccount/aws_test.go | 0 .../externalaccount/basecredentials.go | 18 ++++++++---------- .../externalaccount/basecredentials_test.go | 12 ++++++------ google/{internal => }/externalaccount/err.go | 0 .../{internal => }/externalaccount/err_test.go | 0 .../externalaccount/executablecredsource.go | 4 ++-- .../executablecredsource_test.go | 12 ++++++------ .../externalaccount/filecredsource.go | 0 .../externalaccount/filecredsource_test.go | 2 +- .../{internal => }/externalaccount/header.go | 0 .../externalaccount/header_test.go | 0 .../externalaccount/impersonate.go | 0 .../externalaccount/impersonate_test.go | 6 +++--- .../programmaticrefreshcredsource.go | 0 .../programmaticrefreshcredsource_test.go | 0 .../externalaccount/testdata/3pi_cred.json | 0 .../externalaccount/testdata/3pi_cred.txt | 0 .../externalaccount/urlcredsource.go | 0 .../externalaccount/urlcredsource_test.go | 0 google/google.go | 4 ++-- 21 files changed, 28 insertions(+), 30 deletions(-) rename google/{internal => }/externalaccount/aws.go (100%) rename google/{internal => }/externalaccount/aws_test.go (100%) rename google/{internal => }/externalaccount/basecredentials.go (94%) rename google/{internal => }/externalaccount/basecredentials_test.go (97%) rename google/{internal => }/externalaccount/err.go (100%) rename google/{internal => }/externalaccount/err_test.go (100%) rename google/{internal => }/externalaccount/executablecredsource.go (98%) rename google/{internal => }/externalaccount/executablecredsource_test.go (98%) rename google/{internal => }/externalaccount/filecredsource.go (100%) rename google/{internal => }/externalaccount/filecredsource_test.go (97%) rename google/{internal => }/externalaccount/header.go (100%) rename google/{internal => }/externalaccount/header_test.go (100%) rename google/{internal => }/externalaccount/impersonate.go (100%) rename google/{internal => }/externalaccount/impersonate_test.go (98%) rename google/{internal => }/externalaccount/programmaticrefreshcredsource.go (100%) rename google/{internal => }/externalaccount/programmaticrefreshcredsource_test.go (100%) rename google/{internal => }/externalaccount/testdata/3pi_cred.json (100%) rename google/{internal => }/externalaccount/testdata/3pi_cred.txt (100%) rename google/{internal => }/externalaccount/urlcredsource.go (100%) rename google/{internal => }/externalaccount/urlcredsource_test.go (100%) diff --git a/google/internal/externalaccount/aws.go b/google/externalaccount/aws.go similarity index 100% rename from google/internal/externalaccount/aws.go rename to google/externalaccount/aws.go diff --git a/google/internal/externalaccount/aws_test.go b/google/externalaccount/aws_test.go similarity index 100% rename from google/internal/externalaccount/aws_test.go rename to google/externalaccount/aws_test.go diff --git a/google/internal/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go similarity index 94% rename from google/internal/externalaccount/basecredentials.go rename to google/externalaccount/basecredentials.go index 8eca02113..4c6bc662b 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -21,8 +21,8 @@ var now = func() time.Time { return time.Now().UTC() } -// Config stores the configuration for fetching tokens with external credentials. -type Config struct { +// ExternalAccountConfig is a config that stores the configuration for fetching tokens with external credentials. +type ExternalAccountConfig struct { // Audience is the Secure Token Service (STS) audience which contains the resource name for the workload // identity pool or the workforce pool and the provider identifier in that pool. Audience string @@ -71,14 +71,14 @@ func validateWorkforceAudience(input string) bool { } // 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) { +func (c *ExternalAccountConfig) TokenSource(ctx context.Context) (oauth2.TokenSource, error) { return c.tokenSource(ctx, "https") } // tokenSource is a private function that's directly called by some of the tests, // because the unit test URLs are mocked, and would otherwise fail the // validity check. -func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) { +func (c *ExternalAccountConfig) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) { if c.WorkforcePoolUserProject != "" { valid := validateWorkforceAudience(c.Audience) if !valid { @@ -142,8 +142,6 @@ type CredentialSource struct { // RegionalCredVerificationURL is the AWS regional credential verification URL, will default to // "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided." RegionalCredVerificationURL string `json:"regional_cred_verification_url"` - // CredVerificationURL is deprecated and not used. Use RegionalCredVerificationURL instead. - CredVerificationURL string `json:"cred_verification_url"` // IMDSv2SessionTokenURL is the URL to retrieve the session token when using IMDSv2 in AWS. IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"` // Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json". @@ -171,7 +169,7 @@ type AwsSecurityCredentialsSupplier struct { } // parse determines the type of CredentialSource needed. -func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { +func (c *ExternalAccountConfig) parse(ctx context.Context) (baseCredentialSource, error) { //set Defaults if c.TokenURL == "" { c.TokenURL = defaultTokenUrl @@ -211,7 +209,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { } else if c.CredentialSource.URL != "" { return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil } else if c.CredentialSource.Executable != nil { - return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c) + return createExecutableCredential(ctx, c.CredentialSource.Executable, c) } return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } @@ -224,10 +222,10 @@ type baseCredentialSource interface { // tokenSource is the source that handles external credentials. It is used to retrieve Tokens. type tokenSource struct { ctx context.Context - conf *Config + conf *ExternalAccountConfig } -func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string { +func getMetricsHeaderValue(conf *ExternalAccountConfig, credSource baseCredentialSource) string { return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), "unknown", diff --git a/google/internal/externalaccount/basecredentials_test.go b/google/externalaccount/basecredentials_test.go similarity index 97% rename from google/internal/externalaccount/basecredentials_test.go rename to google/externalaccount/basecredentials_test.go index 9bdf8e01d..51cc01d87 100644 --- a/google/internal/externalaccount/basecredentials_test.go +++ b/google/externalaccount/basecredentials_test.go @@ -26,7 +26,7 @@ var testBaseCredSource = CredentialSource{ Format: format{Type: fileTypeText}, } -var testConfig = Config{ +var testConfig = ExternalAccountConfig{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", TokenInfoURL: "http://localhost:8080/v1/tokeninfo", @@ -57,7 +57,7 @@ type testExchangeTokenServer struct { response string } -func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.Token, error) { +func run(t *testing.T, config *ExternalAccountConfig, tets *testExchangeTokenServer) (*oauth2.Token, error) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got, want := r.URL.String(), tets.url; got != want { t.Errorf("URL.String(): got %v but want %v", got, want) @@ -117,7 +117,7 @@ func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetim } func TestToken(t *testing.T) { - config := Config{ + config := ExternalAccountConfig{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", ClientSecret: "notsosecret", @@ -144,7 +144,7 @@ func TestToken(t *testing.T) { } func TestWorkforcePoolTokenWithClientID(t *testing.T) { - config := Config{ + config := ExternalAccountConfig{ Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", ClientSecret: "notsosecret", @@ -172,7 +172,7 @@ func TestWorkforcePoolTokenWithClientID(t *testing.T) { } func TestWorkforcePoolTokenWithoutClientID(t *testing.T) { - config := Config{ + config := ExternalAccountConfig{ Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", ClientSecret: "notsosecret", @@ -199,7 +199,7 @@ func TestWorkforcePoolTokenWithoutClientID(t *testing.T) { } func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) { - config := Config{ + config := ExternalAccountConfig{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", TokenURL: "https://sts.googleapis.com", diff --git a/google/internal/externalaccount/err.go b/google/externalaccount/err.go similarity index 100% rename from google/internal/externalaccount/err.go rename to google/externalaccount/err.go diff --git a/google/internal/externalaccount/err_test.go b/google/externalaccount/err_test.go similarity index 100% rename from google/internal/externalaccount/err_test.go rename to google/externalaccount/err_test.go diff --git a/google/internal/externalaccount/executablecredsource.go b/google/externalaccount/executablecredsource.go similarity index 98% rename from google/internal/externalaccount/executablecredsource.go rename to google/externalaccount/executablecredsource.go index 6497dc022..b15bc1bf2 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/externalaccount/executablecredsource.go @@ -140,13 +140,13 @@ type executableCredentialSource struct { Timeout time.Duration OutputFile string ctx context.Context - config *Config + config *ExternalAccountConfig env environment } // CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig. // It also performs defaulting and type conversions. -func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) { +func createExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *ExternalAccountConfig) (executableCredentialSource, error) { if ec.Command == "" { return executableCredentialSource{}, commandMissingError() } diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/externalaccount/executablecredsource_test.go similarity index 98% rename from google/internal/externalaccount/executablecredsource_test.go rename to google/externalaccount/executablecredsource_test.go index df8a906b9..80e3c3e31 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/externalaccount/executablecredsource_test.go @@ -128,7 +128,7 @@ var creationTests = []struct { func TestCreateExecutableCredential(t *testing.T) { for _, tt := range creationTests { t.Run(tt.name, func(t *testing.T) { - ecs, err := CreateExecutableCredential(context.Background(), &tt.executableConfig, nil) + ecs, err := createExecutableCredential(context.Background(), &tt.executableConfig, nil) if tt.expectedErr != nil { if err == nil { t.Fatalf("Expected error but found none") @@ -160,13 +160,13 @@ func TestCreateExecutableCredential(t *testing.T) { var getEnvironmentTests = []struct { name string - config Config + config ExternalAccountConfig environment testEnvironment expectedEnvironment []string }{ { name: "Minimal Executable Config", - config: Config{ + config: ExternalAccountConfig{ Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", CredentialSource: CredentialSource{ @@ -189,7 +189,7 @@ var getEnvironmentTests = []struct { }, { name: "Full Impersonation URL", - config: Config{ + config: ExternalAccountConfig{ Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", @@ -216,7 +216,7 @@ var getEnvironmentTests = []struct { }, { name: "Impersonation Email", - config: Config{ + config: ExternalAccountConfig{ Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", @@ -247,7 +247,7 @@ func TestExecutableCredentialGetEnvironment(t *testing.T) { t.Run(tt.name, func(t *testing.T) { config := tt.config - ecs, err := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) + ecs, err := createExecutableCredential(context.Background(), config.CredentialSource.Executable, &config) if err != nil { t.Fatalf("creation failed %v", err) } diff --git a/google/internal/externalaccount/filecredsource.go b/google/externalaccount/filecredsource.go similarity index 100% rename from google/internal/externalaccount/filecredsource.go rename to google/externalaccount/filecredsource.go diff --git a/google/internal/externalaccount/filecredsource_test.go b/google/externalaccount/filecredsource_test.go similarity index 97% rename from google/internal/externalaccount/filecredsource_test.go rename to google/externalaccount/filecredsource_test.go index c20700f1d..9c559c4d6 100644 --- a/google/internal/externalaccount/filecredsource_test.go +++ b/google/externalaccount/filecredsource_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -var testFileConfig = Config{ +var testFileConfig = ExternalAccountConfig{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", TokenURL: "http://localhost:8080/v1/token", diff --git a/google/internal/externalaccount/header.go b/google/externalaccount/header.go similarity index 100% rename from google/internal/externalaccount/header.go rename to google/externalaccount/header.go diff --git a/google/internal/externalaccount/header_test.go b/google/externalaccount/header_test.go similarity index 100% rename from google/internal/externalaccount/header_test.go rename to google/externalaccount/header_test.go diff --git a/google/internal/externalaccount/impersonate.go b/google/externalaccount/impersonate.go similarity index 100% rename from google/internal/externalaccount/impersonate.go rename to google/externalaccount/impersonate.go diff --git a/google/internal/externalaccount/impersonate_test.go b/google/externalaccount/impersonate_test.go similarity index 98% rename from google/internal/externalaccount/impersonate_test.go rename to google/externalaccount/impersonate_test.go index 0ab6d6190..01ff5972f 100644 --- a/google/internal/externalaccount/impersonate_test.go +++ b/google/externalaccount/impersonate_test.go @@ -73,13 +73,13 @@ func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Serv var impersonationTests = []struct { name string - config Config + config ExternalAccountConfig expectedImpersonationBody string expectedMetricsHeader string }{ { name: "Base Impersonation", - config: Config{ + config: ExternalAccountConfig{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", TokenInfoURL: "http://localhost:8080/v1/tokeninfo", @@ -93,7 +93,7 @@ var impersonationTests = []struct { }, { name: "With TokenLifetime Set", - config: Config{ + config: ExternalAccountConfig{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", TokenInfoURL: "http://localhost:8080/v1/tokeninfo", diff --git a/google/internal/externalaccount/programmaticrefreshcredsource.go b/google/externalaccount/programmaticrefreshcredsource.go similarity index 100% rename from google/internal/externalaccount/programmaticrefreshcredsource.go rename to google/externalaccount/programmaticrefreshcredsource.go diff --git a/google/internal/externalaccount/programmaticrefreshcredsource_test.go b/google/externalaccount/programmaticrefreshcredsource_test.go similarity index 100% rename from google/internal/externalaccount/programmaticrefreshcredsource_test.go rename to google/externalaccount/programmaticrefreshcredsource_test.go diff --git a/google/internal/externalaccount/testdata/3pi_cred.json b/google/externalaccount/testdata/3pi_cred.json similarity index 100% rename from google/internal/externalaccount/testdata/3pi_cred.json rename to google/externalaccount/testdata/3pi_cred.json diff --git a/google/internal/externalaccount/testdata/3pi_cred.txt b/google/externalaccount/testdata/3pi_cred.txt similarity index 100% rename from google/internal/externalaccount/testdata/3pi_cred.txt rename to google/externalaccount/testdata/3pi_cred.txt diff --git a/google/internal/externalaccount/urlcredsource.go b/google/externalaccount/urlcredsource.go similarity index 100% rename from google/internal/externalaccount/urlcredsource.go rename to google/externalaccount/urlcredsource.go diff --git a/google/internal/externalaccount/urlcredsource_test.go b/google/externalaccount/urlcredsource_test.go similarity index 100% rename from google/internal/externalaccount/urlcredsource_test.go rename to google/externalaccount/urlcredsource_test.go diff --git a/google/google.go b/google/google.go index c66c53527..96b6a416f 100644 --- a/google/google.go +++ b/google/google.go @@ -15,7 +15,7 @@ import ( "cloud.google.com/go/compute/metadata" "golang.org/x/oauth2" - "golang.org/x/oauth2/google/internal/externalaccount" + "golang.org/x/oauth2/google/externalaccount" "golang.org/x/oauth2/google/internal/externalaccountauthorizeduser" "golang.org/x/oauth2/jwt" ) @@ -191,7 +191,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar tok := &oauth2.Token{RefreshToken: f.RefreshToken} return cfg.TokenSource(ctx, tok), nil case externalAccountKey: - cfg := &externalaccount.Config{ + cfg := &externalaccount.ExternalAccountConfig{ Audience: f.Audience, SubjectTokenType: f.SubjectTokenType, TokenURL: f.TokenURLExternal, From c22d2a128eb29aa2622c657d4ffec3d83b95bf85 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Fri, 19 Jan 2024 10:12:11 -0800 Subject: [PATCH 05/22] Responding to CL comments --- google/externalaccount/aws_test.go | 31 +++++++--------- google/externalaccount/basecredentials.go | 35 +++++++++---------- .../programmaticrefreshcredsource_test.go | 12 +++---- 3 files changed, 33 insertions(+), 45 deletions(-) diff --git a/google/externalaccount/aws_test.go b/google/externalaccount/aws_test.go index cd35c5da8..577c02bc9 100644 --- a/google/externalaccount/aws_test.go +++ b/google/externalaccount/aws_test.go @@ -1243,14 +1243,11 @@ func TestAWSCredential_ProgrammaticAuth(t *testing.T) { SecretAccessKey: secretAccessKey, SessionToken: securityToken, } - - tfc.CredentialSource = CredentialSource{ - AwsSecurityCredentialsSupplier: &AwsSecurityCredentialsSupplier{ - GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { - return securityCredentials, nil - }, - AwsRegion: "us-east-2", + tfc.AwsSecurityCredentialsSupplier = &AwsSecurityCredentialsSupplier{ + GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { + return securityCredentials, nil }, + AwsRegion: "us-east-2", } oldNow := now @@ -1289,13 +1286,11 @@ func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) { SecretAccessKey: secretAccessKey, } - tfc.CredentialSource = CredentialSource{ - AwsSecurityCredentialsSupplier: &AwsSecurityCredentialsSupplier{ - GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { - return securityCredentials, nil - }, - AwsRegion: "us-east-2", + tfc.AwsSecurityCredentialsSupplier = &AwsSecurityCredentialsSupplier{ + GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { + return securityCredentials, nil }, + AwsRegion: "us-east-2", } oldNow := now @@ -1330,13 +1325,11 @@ func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) { func TestAWSCredential_ProgrammaticAuthError(t *testing.T) { tfc := testFileConfig - tfc.CredentialSource = CredentialSource{ - AwsSecurityCredentialsSupplier: &AwsSecurityCredentialsSupplier{ - GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { - return AwsSecurityCredentials{}, errors.New("test error") - }, - AwsRegion: "us-east-2", + tfc.AwsSecurityCredentialsSupplier = &AwsSecurityCredentialsSupplier{ + GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { + return AwsSecurityCredentials{}, errors.New("test error") }, + AwsRegion: "us-east-2", } base, err := tfc.parse(context.Background()) diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 4c6bc662b..38b8c7f19 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -24,12 +24,13 @@ var now = func() time.Time { // ExternalAccountConfig is a config that stores the configuration for fetching tokens with external credentials. type ExternalAccountConfig struct { // Audience is the Secure Token Service (STS) audience which contains the resource name for the workload - // identity pool or the workforce pool and the provider identifier in that pool. + // identity pool or the workforce pool and the provider identifier in that pool. Required. Audience string // SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec - // e.g. `urn:ietf:params:oauth:token-type:jwt`. + // e.g. `urn:ietf:params:oauth:token-type:jwt`. Required. SubjectTokenType string - // TokenURL is the STS token exchange endpoint. + // TokenURL is the STS token exchange endpoint. Optional, if not provided, will default to + // https://sts.googleapis.com/v1/token TokenURL string // TokenInfoURL is the token_info endpoint used to retrieve the account related information ( // user attributes like account identifier, eg. email, username, uid, etc). This is @@ -39,7 +40,7 @@ type ExternalAccountConfig struct { // required for workload identity pools when APIs to be accessed have not integrated with UberMint. ServiceAccountImpersonationURL string // ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation - // token will be valid for. + // token will be valid for. Optional, if not provided will default to 3600. ServiceAccountImpersonationLifetimeSeconds int // ClientSecret is currently only required if token_info endpoint also // needs to be called with the generated GCP access token. When provided, STS will be @@ -55,11 +56,17 @@ type ExternalAccountConfig 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 + // WorkforcePoolUserProject is 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 + // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. This should be a function that returns + // a valid subject token as a string. + SubjectTokenSupplier func() (string, error) `json:"-"` // Ignore for json. + // AwsSecurityCredentialsSupplier is an optional AWS Security Credential supplier. This should contain a + // function that returns valid AwsSecurityCredentials and a valid AwsRegion. + AwsSecurityCredentialsSupplier *AwsSecurityCredentialsSupplier `json:"-"` // Ignore for json. } var ( @@ -120,8 +127,7 @@ type format struct { } // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange. -// One field amongst File, URL, Executable, SubjectTokenSupplier, or AwsSecurityCredentialSupplierConfig. -// should be filled, depending on the kind of credential in question. +// One field amongst File, URL, Executable should be filled, depending on the kind of credential in question. // The EnvironmentID should start with AWS if being used for an AWS credential. type CredentialSource struct { // File is the location for file sourced credentials. @@ -146,13 +152,6 @@ type CredentialSource struct { IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"` // Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json". Format format `json:"format"` - - // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. This should be a function that returns - // a valid subject token as a string. - SubjectTokenSupplier func() (string, error) `json:"-"` // Ignore for json. - // AwsSecurityCredentialsSupplier is an optional AWS Security Credential supplier. This should contain a - // function that returns valid AwsSecurityCredentials and a valid AwsRegion. - AwsSecurityCredentialsSupplier *AwsSecurityCredentialsSupplier `json:"-"` // Ignore for json. } type ExecutableConfig struct { @@ -175,15 +174,15 @@ func (c *ExternalAccountConfig) parse(ctx context.Context) (baseCredentialSource c.TokenURL = defaultTokenUrl } - if c.CredentialSource.AwsSecurityCredentialsSupplier != nil { + if c.AwsSecurityCredentialsSupplier != nil { awsCredSource := awsCredentialSource{ RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, - awsSecurityCredentialsSupplier: c.CredentialSource.AwsSecurityCredentialsSupplier, + awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier, TargetResource: c.Audience, } return awsCredSource, nil - } else if c.CredentialSource.SubjectTokenSupplier != nil { - return programmaticRefreshCredentialSource{SubjectTokenSupplier: c.CredentialSource.SubjectTokenSupplier}, nil + } else if c.SubjectTokenSupplier != nil { + return programmaticRefreshCredentialSource{SubjectTokenSupplier: c.SubjectTokenSupplier}, nil } else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { if awsVersion != 1 { diff --git a/google/externalaccount/programmaticrefreshcredsource_test.go b/google/externalaccount/programmaticrefreshcredsource_test.go index 9e5454826..dc9d2333e 100644 --- a/google/externalaccount/programmaticrefreshcredsource_test.go +++ b/google/externalaccount/programmaticrefreshcredsource_test.go @@ -14,10 +14,8 @@ import ( func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) { tfc := testConfig - tfc.CredentialSource = CredentialSource{ - SubjectTokenSupplier: func() (string, error) { - return "subjectToken", nil - }, + tfc.SubjectTokenSupplier = func() (string, error) { + return "subjectToken", nil } oldNow := now @@ -44,10 +42,8 @@ func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) { func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) { tfc := testConfig - tfc.CredentialSource = CredentialSource{ - SubjectTokenSupplier: func() (string, error) { - return "", errors.New("test error") - }, + tfc.SubjectTokenSupplier = func() (string, error) { + return "", errors.New("test error") } oldNow := now From e15136c67a513233784a47fc379e02a4668a7791 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Tue, 23 Jan 2024 14:49:10 -0800 Subject: [PATCH 06/22] responding to CL comments --- google/externalaccount/aws.go | 36 +++++++++---------- google/externalaccount/basecredentials.go | 16 ++++----- google/externalaccount/err.go | 18 ---------- google/externalaccount/err_test.go | 19 ---------- .../programmaticrefreshcredsource.go | 2 +- .../programmaticrefreshcredsource_test.go | 8 +---- 6 files changed, 28 insertions(+), 71 deletions(-) delete mode 100644 google/externalaccount/err.go delete mode 100644 google/externalaccount/err_test.go diff --git a/google/externalaccount/aws.go b/google/externalaccount/aws.go index 61e3c7fcc..57c92d255 100644 --- a/google/externalaccount/aws.go +++ b/google/externalaccount/aws.go @@ -257,12 +257,12 @@ func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp } type awsCredentialSource struct { - EnvironmentID string - RegionURL string - RegionalCredVerificationURL string - CredVerificationURL string - IMDSv2SessionTokenURL string - TargetResource string + environmentID string + regionURL string + regionalCredVerificationURL string + credVerificationURL string + imdsv2SessionTokenURL string + targetResource string requestSigner *awsRequestSigner region string ctx context.Context @@ -312,8 +312,8 @@ func (cs awsCredentialSource) credentialSourceType() string { func (cs awsCredentialSource) subjectToken() (string, error) { // Set Defaults - if cs.RegionalCredVerificationURL == "" { - cs.RegionalCredVerificationURL = defaultRegionalCredentialVerificationUrl + if cs.regionalCredVerificationURL == "" { + cs.regionalCredVerificationURL = defaultRegionalCredentialVerificationUrl } if cs.requestSigner == nil { headers := make(map[string]string) @@ -345,7 +345,7 @@ func (cs awsCredentialSource) subjectToken() (string, error) { // Generate the signed request to AWS STS GetCallerIdentity API. // Use the required regional endpoint. Otherwise, the request will fail. - req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.region, 1), nil) + req, err := http.NewRequest("POST", strings.Replace(cs.regionalCredVerificationURL, "{region}", cs.region, 1), nil) if err != nil { return "", err } @@ -353,8 +353,8 @@ func (cs awsCredentialSource) subjectToken() (string, error) { // provider, with or without the HTTPS prefix. // Including this header as part of the signature is recommended to // ensure data integrity. - if cs.TargetResource != "" { - req.Header.Add("x-goog-cloud-target-resource", cs.TargetResource) + if cs.targetResource != "" { + req.Header.Add("x-goog-cloud-target-resource", cs.targetResource) } cs.requestSigner.SignRequest(req) @@ -401,11 +401,11 @@ func (cs awsCredentialSource) subjectToken() (string, error) { } func (cs *awsCredentialSource) getAWSSessionToken() (string, error) { - if cs.IMDSv2SessionTokenURL == "" { + if cs.imdsv2SessionTokenURL == "" { return "", nil } - req, err := http.NewRequest("PUT", cs.IMDSv2SessionTokenURL, nil) + req, err := http.NewRequest("PUT", cs.imdsv2SessionTokenURL, nil) if err != nil { return "", err } @@ -442,11 +442,11 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err return getenv("AWS_DEFAULT_REGION"), nil } - if cs.RegionURL == "" { + if cs.regionURL == "" { return "", errors.New("oauth2/google: unable to determine AWS region") } - req, err := http.NewRequest("GET", cs.RegionURL, nil) + req, err := http.NewRequest("GET", cs.regionURL, nil) if err != nil { return "", err } @@ -518,7 +518,7 @@ func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (AwsSecurityCredentials, error) { var result AwsSecurityCredentials - req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.CredVerificationURL, roleName), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.credVerificationURL, roleName), nil) if err != nil { return result, err } @@ -548,11 +548,11 @@ func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, h } func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (string, error) { - if cs.CredVerificationURL == "" { + if cs.credVerificationURL == "" { return "", errors.New("oauth2/google: unable to determine the AWS metadata server security credentials endpoint") } - req, err := http.NewRequest("GET", cs.CredVerificationURL, nil) + req, err := http.NewRequest("GET", cs.credVerificationURL, nil) if err != nil { return "", err } diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 38b8c7f19..96e6df98d 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -176,9 +176,9 @@ func (c *ExternalAccountConfig) parse(ctx context.Context) (baseCredentialSource if c.AwsSecurityCredentialsSupplier != nil { awsCredSource := awsCredentialSource{ - RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, + regionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier, - TargetResource: c.Audience, + targetResource: c.Audience, } return awsCredSource, nil } else if c.SubjectTokenSupplier != nil { @@ -190,15 +190,15 @@ func (c *ExternalAccountConfig) parse(ctx context.Context) (baseCredentialSource } awsCredSource := awsCredentialSource{ - EnvironmentID: c.CredentialSource.EnvironmentID, - RegionURL: c.CredentialSource.RegionURL, - RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, - CredVerificationURL: c.CredentialSource.URL, - TargetResource: c.Audience, + environmentID: c.CredentialSource.EnvironmentID, + regionURL: c.CredentialSource.RegionURL, + regionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, + credVerificationURL: c.CredentialSource.URL, + targetResource: c.Audience, ctx: ctx, } if c.CredentialSource.IMDSv2SessionTokenURL != "" { - awsCredSource.IMDSv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL + awsCredSource.imdsv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL } return awsCredSource, nil diff --git a/google/externalaccount/err.go b/google/externalaccount/err.go deleted file mode 100644 index 233a78cef..000000000 --- a/google/externalaccount/err.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package externalaccount - -import "fmt" - -// Error for handling OAuth related error responses as stated in rfc6749#5.2. -type Error struct { - Code string - URI string - Description string -} - -func (err *Error) Error() string { - return fmt.Sprintf("got error code %s from %s: %s", err.Code, err.URI, err.Description) -} diff --git a/google/externalaccount/err_test.go b/google/externalaccount/err_test.go deleted file mode 100644 index 687380d71..000000000 --- a/google/externalaccount/err_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package externalaccount - -import "testing" - -func TestError(t *testing.T) { - e := Error{ - "42", - "http:thisIsAPlaceholder", - "The Answer!", - } - want := "got error code 42 from http:thisIsAPlaceholder: The Answer!" - if got := e.Error(); got != want { - t.Errorf("Got error message %q; want %q", got, want) - } -} diff --git a/google/externalaccount/programmaticrefreshcredsource.go b/google/externalaccount/programmaticrefreshcredsource.go index 6808930e1..f7c795fea 100644 --- a/google/externalaccount/programmaticrefreshcredsource.go +++ b/google/externalaccount/programmaticrefreshcredsource.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Go Authors. All rights reserved. +// Copyright 2024 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/google/externalaccount/programmaticrefreshcredsource_test.go b/google/externalaccount/programmaticrefreshcredsource_test.go index dc9d2333e..ae9e4d801 100644 --- a/google/externalaccount/programmaticrefreshcredsource_test.go +++ b/google/externalaccount/programmaticrefreshcredsource_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Go Authors. All rights reserved. +// Copyright 2024 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -18,12 +18,6 @@ func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) { return "subjectToken", nil } - oldNow := now - defer func() { - now = oldNow - }() - now = setTime(defaultTime) - base, err := tfc.parse(context.Background()) if err != nil { t.Fatalf("parse() failed %v", err) From fc72581431b8e7db72582ff39c33b263f2ae8c1b Mon Sep 17 00:00:00 2001 From: aeitzman Date: Tue, 23 Jan 2024 15:46:12 -0800 Subject: [PATCH 07/22] fix test --- .../externalaccount/programmaticrefreshcredsource_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/google/externalaccount/programmaticrefreshcredsource_test.go b/google/externalaccount/programmaticrefreshcredsource_test.go index ae9e4d801..a1a19d349 100644 --- a/google/externalaccount/programmaticrefreshcredsource_test.go +++ b/google/externalaccount/programmaticrefreshcredsource_test.go @@ -40,12 +40,6 @@ func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) { return "", errors.New("test error") } - oldNow := now - defer func() { - now = oldNow - }() - now = setTime(defaultTime) - base, err := tfc.parse(context.Background()) if err != nil { t.Fatalf("parse() failed %v", err) From 89498bd47fb23885ba7388a5079d1c38d38c76ee Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 25 Jan 2024 13:37:50 -0800 Subject: [PATCH 08/22] Adding docs --- google/doc.go | 14 +-- google/externalaccount/basecredentials.go | 106 +++++++++++++++++++++- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/google/doc.go b/google/doc.go index 03c42c6f8..2dad55ad2 100644 --- a/google/doc.go +++ b/google/doc.go @@ -41,10 +41,10 @@ // OIDC identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#oidc // SAML 2.0 identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#saml // -// For OIDC and SAML providers, the library can retrieve tokens in three ways: +// For OIDC and SAML providers, the library can retrieve tokens in fours ways: // from a local file location (file-sourced credentials), from a server -// (URL-sourced credentials), or from a local executable (executable-sourced -// credentials). +// (URL-sourced credentials), from a local executable (executable-sourced +// credentials), or from a user defined function that returns an OIDC or SAML token. // For file-sourced credentials, a background process needs to be continuously // refreshing the file location with a new OIDC/SAML token prior to expiration. // For tokens with one hour lifetimes, the token needs to be updated in the file @@ -57,6 +57,7 @@ // For more information on how these work (and how to implement // executable-sourced credentials), please check out: // https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration +// For using a user definied function to supply the token, see the [golang.org/x/oauth2/google/externalaccount] package. // // Note that this library does not perform any validation on the token_url, token_info_url, // or service_account_impersonation_url fields of the credential configuration. @@ -84,10 +85,10 @@ // OIDC identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#oidc // SAML 2.0 identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#saml // -// For workforce identity federation, the library can retrieve tokens in three ways: +// For workforce identity federation, the library can retrieve tokens in four ways: // from a local file location (file-sourced credentials), from a server -// (URL-sourced credentials), or from a local executable (executable-sourced -// credentials). +// (URL-sourced credentials), from a local executable (executable-sourced +// credentials), or from a user supplied function that returns an OIDC or SAML token. // For file-sourced credentials, a background process needs to be continuously // refreshing the file location with a new OIDC/SAML token prior to expiration. // For tokens with one hour lifetimes, the token needs to be updated in the file @@ -100,6 +101,7 @@ // For more information on how these work (and how to implement // executable-sourced credentials), please check out: // https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in +// For using a user definied function to supply the token, see the [golang.org/x/oauth2/google/externalaccount] package. // // # Security considerations // diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 96e6df98d..20a26d9d3 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -2,6 +2,110 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +/* +Package externalaccount provides support for creating workload identity +federation and workforce identity federation token sources that can be +used to access Google Cloud resources from external identity providers. + +# Workload Identity Federation + +Using workload identity federation, your application can access Google Cloud +resources from Amazon Web Services (AWS), Microsoft Azure or any identity +provider that supports OpenID Connect (OIDC) or SAML 2.0. +Traditionally, applications running outside Google Cloud have used service +account keys to access Google Cloud resources. Using identity federation, +you can allow your workload to impersonate a service account. +This lets you access Google Cloud resources directly, eliminating the +maintenance and security burden associated with service account keys. + +Follow the detailed instructions on how to configure Workload Identity Federation +in various platforms: + +Amazon Web Services (AWS): https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#aws +Microsoft Azure: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#azure +OIDC identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#oidc +SAML 2.0 identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#saml + +For OIDC and SAML providers, the library can retrieve tokens in fours ways: +from a local file location (file-sourced credentials), from a server +(URL-sourced credentials), from a local executable (executable-sourced +credentials), or from a user defined function that returns an OIDC or SAML token. +For file-sourced credentials, a background process needs to be continuously +refreshing the file location with a new OIDC/SAML token prior to expiration. +For tokens with one hour lifetimes, the token needs to be updated in the file +every hour. The token can be stored directly as plain text or in JSON format. +For URL-sourced credentials, a local server needs to host a GET endpoint to +return the OIDC/SAML token. The response can be in plain text or JSON. +Additional required request headers can also be specified. +For executable-sourced credentials, an application needs to be available to +output the OIDC/SAML token and other information in a JSON format. +For more information on how these work (and how to implement +executable-sourced credentials), please check out: +https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration + +For using a user defined function to supply the token, define a function that can return +either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredential] +(for AWS providers). This function can then be used when building an [ExternalAccountConfig]. +The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google +Cloud resources. For instance, you can create a NewClient from thes +[cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) + +Note that this library does not perform any validation on the token_url, token_info_url, +or service_account_impersonation_url fields of the credential configuration. +It is not recommended to use a credential configuration that you did not generate with +the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain. + +# Workforce Identity Federation + +Workforce identity federation lets you use an external identity provider (IdP) to +authenticate and authorize a workforce—a group of users, such as employees, partners, +and contractors—using IAM, so that the users can access Google Cloud services. +Workforce identity federation extends Google Cloud's identity capabilities to support +syncless, attribute-based single sign on. + +With workforce identity federation, your workforce can access Google Cloud resources +using an external identity provider (IdP) that supports OpenID Connect (OIDC) or +SAML 2.0 such as Azure Active Directory (Azure AD), Active Directory Federation +Services (AD FS), Okta, and others. + +Follow the detailed instructions on how to configure Workload Identity Federation +in various platforms: + +Azure AD: https://cloud.google.com/iam/docs/workforce-sign-in-azure-ad +Okta: https://cloud.google.com/iam/docs/workforce-sign-in-okta +OIDC identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#oidc +SAML 2.0 identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#saml + +For workforce identity federation, the library can retrieve tokens in four ways: +from a local file location (file-sourced credentials), from a server +(URL-sourced credentials), from a local executable (executable-sourced +credentials), or from a user supplied function that returns an OIDC or SAML token. +For file-sourced credentials, a background process needs to be continuously +refreshing the file location with a new OIDC/SAML token prior to expiration. +For tokens with one hour lifetimes, the token needs to be updated in the file +every hour. The token can be stored directly as plain text or in JSON format. +For URL-sourced credentials, a local server needs to host a GET endpoint to +return the OIDC/SAML token. The response can be in plain text or JSON. +Additional required request headers can also be specified. +For executable-sourced credentials, an application needs to be available to +output the OIDC/SAML token and other information in a JSON format. +For more information on how these work (and how to implement +executable-sourced credentials), please check out: +https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in + +For using a user definied function to supply the token, define a function that can return +either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredential] +for AWS providers. This function can then be used when building an [ExternalAccountConfig]. +The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google +Cloud resources. For instance, you can create a NewClient from thes +[cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) + +# Security considerations +Note that this library does not perform any validation on the token_url, token_info_url, +or service_account_impersonation_url fields of the credential configuration. +It is not recommended to use a credential configuration that you did not generate with +the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain. +*/ package externalaccount import ( @@ -77,7 +181,7 @@ 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. +// TokenSource Returns an external account TokenSource. func (c *ExternalAccountConfig) TokenSource(ctx context.Context) (oauth2.TokenSource, error) { return c.tokenSource(ctx, "https") } From 64bdc08f03ce3700fef51d6d82e5936e78c0f368 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 25 Jan 2024 13:44:02 -0800 Subject: [PATCH 09/22] docs --- google/externalaccount/basecredentials.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 20a26d9d3..4de2477a7 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -44,7 +44,7 @@ executable-sourced credentials), please check out: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration For using a user defined function to supply the token, define a function that can return -either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredential] +either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] (for AWS providers). This function can then be used when building an [ExternalAccountConfig]. The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google Cloud resources. For instance, you can create a NewClient from thes @@ -94,7 +94,7 @@ executable-sourced credentials), please check out: https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in For using a user definied function to supply the token, define a function that can return -either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredential] +either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] for AWS providers. This function can then be used when building an [ExternalAccountConfig]. The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google Cloud resources. For instance, you can create a NewClient from thes @@ -264,10 +264,12 @@ type ExecutableConfig struct { OutputFile string `json:"output_file"` } +// AWSSecurityCredentialsSupplier is a struct that can be used to supply AwsSecurityCredentials to +// exchange for a GCP access token. type AwsSecurityCredentialsSupplier struct { // AwsRegion is the AWS region. AwsRegion string - // GetAwsSecurityCredentials is a function that should return valid AwsSecurityCredentials. + // GetAwsSecurityCredentials is a function that should return a valid set of AwsSecurityCredentials. GetAwsSecurityCredentials func() (AwsSecurityCredentials, error) } From 43ef8ba25196ac42055c73e2883286e45ea41514 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Mon, 29 Jan 2024 11:51:38 -0800 Subject: [PATCH 10/22] Fix docs --- google/externalaccount/basecredentials.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 4de2477a7..737081e10 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -43,7 +43,7 @@ For more information on how these work (and how to implement executable-sourced credentials), please check out: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration -For using a user defined function to supply the token, define a function that can return +For using a custom function to supply the token, define a function that can return either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] (for AWS providers). This function can then be used when building an [ExternalAccountConfig]. The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google @@ -97,10 +97,11 @@ For using a user definied function to supply the token, define a function that c either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] for AWS providers. This function can then be used when building an [ExternalAccountConfig]. The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google -Cloud resources. For instance, you can create a NewClient from thes +Cloud resources. For instance, you can create a NewClient from the [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) # Security considerations + Note that this library does not perform any validation on the token_url, token_info_url, or service_account_impersonation_url fields of the credential configuration. It is not recommended to use a credential configuration that you did not generate with From 50943bd1c1f8413107ccedfcab3dcc9822966dd5 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Wed, 31 Jan 2024 09:28:18 -0800 Subject: [PATCH 11/22] Addressing review comments --- google/externalaccount/aws.go | 17 ++- google/externalaccount/aws_test.go | 62 ++++++---- google/externalaccount/basecredentials.go | 112 +++++++++++------- .../externalaccount/basecredentials_test.go | 30 ++--- .../externalaccount/executablecredsource.go | 4 +- .../executablecredsource_test.go | 26 ++-- google/externalaccount/filecredsource.go | 2 +- google/externalaccount/filecredsource_test.go | 8 +- google/externalaccount/impersonate_test.go | 10 +- google/externalaccount/urlcredsource.go | 2 +- google/externalaccount/urlcredsource_test.go | 14 +-- google/google.go | 9 +- .../impersonate}/impersonate.go | 2 +- 13 files changed, 167 insertions(+), 131 deletions(-) rename google/{externalaccount => internal/impersonate}/impersonate.go (99%) diff --git a/google/externalaccount/aws.go b/google/externalaccount/aws.go index 57c92d255..4c67d3d65 100644 --- a/google/externalaccount/aws.go +++ b/google/externalaccount/aws.go @@ -39,7 +39,7 @@ type AwsSecurityCredentials struct { // awsRequestSigner is a utility class to sign http requests using a AWS V4 signature. type awsRequestSigner struct { RegionName string - AwsSecurityCredentials AwsSecurityCredentials + AwsSecurityCredentials *AwsSecurityCredentials } // getenv aliases os.Getenv for testing @@ -267,7 +267,7 @@ type awsCredentialSource struct { region string ctx context.Context client *http.Client - awsSecurityCredentialsSupplier *AwsSecurityCredentialsSupplier + awsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier } type awsRequestHeader struct { @@ -432,7 +432,7 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) { func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) { if cs.awsSecurityCredentialsSupplier != nil { - return cs.awsSecurityCredentialsSupplier.AwsRegion, nil + return cs.awsSecurityCredentialsSupplier.AwsRegion() } if canRetrieveRegionFromEnvironment() { if envAwsRegion := getenv(awsRegion); envAwsRegion != "" { @@ -479,15 +479,12 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err return string(respBody[:respBodyEnd]), nil } -func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result AwsSecurityCredentials, err error) { +func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result *AwsSecurityCredentials, err error) { if cs.awsSecurityCredentialsSupplier != nil { - if cs.awsSecurityCredentialsSupplier.AwsRegion == "" { - return result, errors.New("oauth2/google: AwsRegion must be provided when using an AwsSecurityCredentialsSupplier") - } - return cs.awsSecurityCredentialsSupplier.GetAwsSecurityCredentials() + return cs.awsSecurityCredentialsSupplier.AwsSecurityCredentials() } if canRetrieveSecurityCredentialFromEnvironment() { - return AwsSecurityCredentials{ + return &AwsSecurityCredentials{ AccessKeyID: getenv(awsAccessKeyId), SecretAccessKey: getenv(awsSecretAccessKey), SessionToken: getenv(awsSessionToken), @@ -512,7 +509,7 @@ func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) return result, errors.New("oauth2/google: missing SecretAccessKey credential") } - return credentials, nil + return &credentials, nil } func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (AwsSecurityCredentials, error) { diff --git a/google/externalaccount/aws_test.go b/google/externalaccount/aws_test.go index 577c02bc9..7f6a86388 100644 --- a/google/externalaccount/aws_test.go +++ b/google/externalaccount/aws_test.go @@ -37,7 +37,7 @@ func setEnvironment(env map[string]string) func(string) string { var defaultRequestSigner = &awsRequestSigner{ RegionName: "us-east-1", - AwsSecurityCredentials: AwsSecurityCredentials{ + AwsSecurityCredentials: &AwsSecurityCredentials{ AccessKeyID: "AKIDEXAMPLE", SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, @@ -51,7 +51,7 @@ const ( var requestSignerWithToken = &awsRequestSigner{ RegionName: "us-east-2", - AwsSecurityCredentials: AwsSecurityCredentials{ + AwsSecurityCredentials: &AwsSecurityCredentials{ AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, SessionToken: securityToken, @@ -389,7 +389,7 @@ func TestAWSv4Signature_PostRequestWithSecurityTokenAndAdditionalHeaders(t *test func TestAWSv4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) { var requestSigner = &awsRequestSigner{ RegionName: "us-east-2", - AwsSecurityCredentials: AwsSecurityCredentials{ + AwsSecurityCredentials: &AwsSecurityCredentials{ AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, }, @@ -527,8 +527,8 @@ func notFound(w http.ResponseWriter, r *http.Request) { func noHeaderValidation(r *http.Request) {} -func (server *testAwsServer) getCredentialSource(url string) CredentialSource { - return CredentialSource{ +func (server *testAwsServer) getCredentialSource(url string) *CredentialSource { + return &CredentialSource{ EnvironmentID: "aws1", URL: url + server.url, RegionURL: url + server.regionURL, @@ -542,7 +542,7 @@ func getExpectedSubjectToken(url, region, accessKeyID, secretAccessKey, security req.Header.Add("x-goog-cloud-target-resource", testFileConfig.Audience) signer := &awsRequestSigner{ RegionName: region, - AwsSecurityCredentials: AwsSecurityCredentials{ + AwsSecurityCredentials: &AwsSecurityCredentials{ AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, SessionToken: securityToken, @@ -589,7 +589,6 @@ func TestAWSCredential_BasicRequest(t *testing.T) { tfc := testFileConfig tfc.CredentialSource = server.getCredentialSource(ts.URL) - oldGetenv := getenv oldNow := now defer func() { @@ -1243,11 +1242,11 @@ func TestAWSCredential_ProgrammaticAuth(t *testing.T) { SecretAccessKey: secretAccessKey, SessionToken: securityToken, } - tfc.AwsSecurityCredentialsSupplier = &AwsSecurityCredentialsSupplier{ - GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { - return securityCredentials, nil - }, - AwsRegion: "us-east-2", + + tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{ + awsRegion: "us-east-2", + err: nil, + credentials: &securityCredentials, } oldNow := now @@ -1286,11 +1285,10 @@ func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) { SecretAccessKey: secretAccessKey, } - tfc.AwsSecurityCredentialsSupplier = &AwsSecurityCredentialsSupplier{ - GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { - return securityCredentials, nil - }, - AwsRegion: "us-east-2", + tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{ + awsRegion: "us-east-2", + err: nil, + credentials: &securityCredentials, } oldNow := now @@ -1324,12 +1322,10 @@ func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) { func TestAWSCredential_ProgrammaticAuthError(t *testing.T) { tfc := testFileConfig - - tfc.AwsSecurityCredentialsSupplier = &AwsSecurityCredentialsSupplier{ - GetAwsSecurityCredentials: func() (AwsSecurityCredentials, error) { - return AwsSecurityCredentials{}, errors.New("test error") - }, - AwsRegion: "us-east-2", + tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{ + awsRegion: "us-east-2", + err: errors.New("test error"), + credentials: nil, } base, err := tfc.parse(context.Background()) @@ -1362,3 +1358,23 @@ func TestAwsCredential_CredentialSourceType(t *testing.T) { t.Errorf("got %v but want %v", got, want) } } + +type testAwsSupplier struct { + err error + awsRegion string + credentials *AwsSecurityCredentials +} + +func (supp testAwsSupplier) AwsRegion() (string, error) { + if supp.err != nil { + return "", supp.err + } + return supp.awsRegion, nil +} + +func (supp testAwsSupplier) AwsSecurityCredentials() (*AwsSecurityCredentials, error) { + if supp.err != nil { + return nil, supp.err + } + return supp.credentials, nil +} diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 737081e10..609903930 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -45,7 +45,7 @@ https://cloud.google.com/iam/docs/workload-identity-federation-with-other-provid For using a custom function to supply the token, define a function that can return either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] -(for AWS providers). This function can then be used when building an [ExternalAccountConfig]. +(for AWS providers). This function can then be used when building an [Config]. The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google Cloud resources. For instance, you can create a NewClient from thes [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) @@ -95,7 +95,7 @@ https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#ge For using a user definied function to supply the token, define a function that can return either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] -for AWS providers. This function can then be used when building an [ExternalAccountConfig]. +for AWS providers. This function can then be used when building an [Config]. The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google Cloud resources. For instance, you can create a NewClient from the [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) @@ -118,6 +118,7 @@ import ( "time" "golang.org/x/oauth2" + "golang.org/x/oauth2/google/internal/impersonate" "golang.org/x/oauth2/google/internal/stsexchange" ) @@ -126,52 +127,52 @@ var now = func() time.Time { return time.Now().UTC() } -// ExternalAccountConfig is a config that stores the configuration for fetching tokens with external credentials. -type ExternalAccountConfig struct { +// Config stores the configuration for fetching tokens with external credentials. +type Config struct { // Audience is the Secure Token Service (STS) audience which contains the resource name for the workload // identity pool or the workforce pool and the provider identifier in that pool. Required. Audience string // SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec // e.g. `urn:ietf:params:oauth:token-type:jwt`. Required. SubjectTokenType string - // TokenURL is the STS token exchange endpoint. Optional, if not provided, will default to - // https://sts.googleapis.com/v1/token + // TokenURL is the STS token exchange endpoint. If not provided, will default to + // https://sts.googleapis.com/v1/token. Optional. TokenURL string // TokenInfoURL is the token_info endpoint used to retrieve the account related information ( // user attributes like account identifier, eg. email, username, uid, etc). This is - // needed for gCloud session account identification. + // needed for gCloud session account identification. Optional. TokenInfoURL string // ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only - // required for workload identity pools when APIs to be accessed have not integrated with UberMint. + // required for workload identity pools when APIs to be accessed have not integrated with UberMint. Optional. ServiceAccountImpersonationURL string // ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation - // token will be valid for. Optional, if not provided will default to 3600. + // token will be valid for. If not provided will default to 3600. Optional. ServiceAccountImpersonationLifetimeSeconds int // ClientSecret is currently only required if token_info endpoint also // needs to be called with the generated GCP access token. When provided, STS will be - // called with additional basic authentication using client_id as username and client_secret as password. + // called with additional basic authentication using client_id as username and client_secret as password. Optional. ClientSecret string - // ClientID is only required in conjunction with ClientSecret, as described above. + // ClientID is only required in conjunction with ClientSecret, as described above. Optional. ClientID string // CredentialSource contains the necessary information to retrieve the token itself, as well - // as some environmental information. - CredentialSource CredentialSource + // as some environmental information. Will be used unless a supplier is provided. Optional. + CredentialSource *CredentialSource // QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries - // will set the x-goog-user-project which overrides the project associated with the credentials. + // will set the x-goog-user-project which overrides the project associated with the credentials. Optional. QuotaProjectID string - // Scopes contains the desired scopes for the returned access token. + // Scopes contains the desired scopes for the returned access token. Optional. Scopes []string - // WorkforcePoolUserProject is the optional workforce pool user project number when the credential + // WorkforcePoolUserProject is the 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. + // permission to use the project for billing/quota. Optional. WorkforcePoolUserProject string // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. This should be a function that returns - // a valid subject token as a string. - SubjectTokenSupplier func() (string, error) `json:"-"` // Ignore for json. - // AwsSecurityCredentialsSupplier is an optional AWS Security Credential supplier. This should contain a - // function that returns valid AwsSecurityCredentials and a valid AwsRegion. - AwsSecurityCredentialsSupplier *AwsSecurityCredentialsSupplier `json:"-"` // Ignore for json. + // a valid subject token as a string. Optional. + SubjectTokenSupplier func() (string, error) + // AwsSecurityCredentialsSupplier is an AWS Security Credential supplier. This should contain a + // function that returns valid AwsSecurityCredentials and a valid AwsRegion. Optional. + AwsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier } var ( @@ -182,21 +183,43 @@ func validateWorkforceAudience(input string) bool { return validWorkforceAudiencePattern.MatchString(input) } -// TokenSource Returns an external account TokenSource. -func (c *ExternalAccountConfig) TokenSource(ctx context.Context) (oauth2.TokenSource, error) { - return c.tokenSource(ctx, "https") +// NewTokenSource Returns an external account TokenSource. +func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error) { + if conf.Audience == "" { + return nil, fmt.Errorf("oauth2/google: Audience must be set") + } + if conf.SubjectTokenType == "" { + return nil, fmt.Errorf("oauth2/google: Subject token type must be set") + } + if conf.WorkforcePoolUserProject != "" { + valid := validateWorkforceAudience(conf.Audience) + if !valid { + return nil, fmt.Errorf("oauth2/google: Workforce pool user project should not be set for non-workforce pool credentials") + } + } + count := 0 + if conf.CredentialSource != nil { + count++ + } + if conf.SubjectTokenSupplier != nil { + count++ + } + if conf.AwsSecurityCredentialsSupplier != nil { + count++ + } + if count == 0 { + return nil, fmt.Errorf("oauth2/google: One of CredentialSource, SubjectTokenSUpplier, or AwsSecurityCredentialsSupplier must be set") + } + if count > 1 { + return nil, fmt.Errorf("oauth2/google: Only one of CredentialSource, SubjectTokenSUpplier, or AwsSecurityCredentialsSupplier must be set") + } + return conf.tokenSource(ctx, "https") } // tokenSource is a private function that's directly called by some of the tests, // because the unit test URLs are mocked, and would otherwise fail the // validity check. -func (c *ExternalAccountConfig) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) { - 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") - } - } +func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) { ts := tokenSource{ ctx: ctx, @@ -207,7 +230,7 @@ func (c *ExternalAccountConfig) tokenSource(ctx context.Context, scheme string) } scopes := c.Scopes ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} - imp := ImpersonateTokenSource{ + imp := impersonate.ImpersonateTokenSource{ Ctx: ctx, URL: c.ServiceAccountImpersonationURL, Scopes: scopes, @@ -224,7 +247,7 @@ const ( defaultTokenUrl = "https://sts.googleapis.com/v1/token" ) -type format struct { +type Format struct { // Type is either "text" or "json". When not provided "text" type is assumed. Type string `json:"type"` // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure. @@ -256,7 +279,7 @@ type CredentialSource struct { // IMDSv2SessionTokenURL is the URL to retrieve the session token when using IMDSv2 in AWS. IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"` // Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json". - Format format `json:"format"` + Format Format `json:"format"` } type ExecutableConfig struct { @@ -265,17 +288,17 @@ type ExecutableConfig struct { OutputFile string `json:"output_file"` } -// AWSSecurityCredentialsSupplier is a struct that can be used to supply AwsSecurityCredentials to +// AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an Aws Region to // exchange for a GCP access token. -type AwsSecurityCredentialsSupplier struct { - // AwsRegion is the AWS region. - AwsRegion string - // GetAwsSecurityCredentials is a function that should return a valid set of AwsSecurityCredentials. - GetAwsSecurityCredentials func() (AwsSecurityCredentials, error) +type AwsSecurityCredentialsSupplier interface { + // AwsRegion should return the AWS region or an error. + AwsRegion() (string, error) + // GetAwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error. + AwsSecurityCredentials() (*AwsSecurityCredentials, error) } // parse determines the type of CredentialSource needed. -func (c *ExternalAccountConfig) parse(ctx context.Context) (baseCredentialSource, error) { +func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { //set Defaults if c.TokenURL == "" { c.TokenURL = defaultTokenUrl @@ -283,7 +306,6 @@ func (c *ExternalAccountConfig) parse(ctx context.Context) (baseCredentialSource if c.AwsSecurityCredentialsSupplier != nil { awsCredSource := awsCredentialSource{ - regionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier, targetResource: c.Audience, } @@ -328,10 +350,10 @@ type baseCredentialSource interface { // tokenSource is the source that handles external credentials. It is used to retrieve Tokens. type tokenSource struct { ctx context.Context - conf *ExternalAccountConfig + conf *Config } -func getMetricsHeaderValue(conf *ExternalAccountConfig, credSource baseCredentialSource) string { +func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string { return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), "unknown", diff --git a/google/externalaccount/basecredentials_test.go b/google/externalaccount/basecredentials_test.go index 51cc01d87..5747e07b0 100644 --- a/google/externalaccount/basecredentials_test.go +++ b/google/externalaccount/basecredentials_test.go @@ -23,16 +23,16 @@ const ( var testBaseCredSource = CredentialSource{ File: textBaseCredPath, - Format: format{Type: fileTypeText}, + Format: Format{Type: fileTypeText}, } -var testConfig = ExternalAccountConfig{ +var testConfig = Config{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", TokenInfoURL: "http://localhost:8080/v1/tokeninfo", ClientSecret: "notsosecret", ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: testBaseCredSource, + CredentialSource: &testBaseCredSource, Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, } @@ -57,7 +57,7 @@ type testExchangeTokenServer struct { response string } -func run(t *testing.T, config *ExternalAccountConfig, tets *testExchangeTokenServer) (*oauth2.Token, error) { +func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.Token, error) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got, want := r.URL.String(), tets.url; got != want { t.Errorf("URL.String(): got %v but want %v", got, want) @@ -117,12 +117,12 @@ func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetim } func TestToken(t *testing.T) { - config := ExternalAccountConfig{ + config := Config{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", ClientSecret: "notsosecret", ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: testBaseCredSource, + CredentialSource: &testBaseCredSource, Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, } @@ -144,12 +144,12 @@ func TestToken(t *testing.T) { } func TestWorkforcePoolTokenWithClientID(t *testing.T) { - config := ExternalAccountConfig{ + config := Config{ Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", ClientSecret: "notsosecret", ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: testBaseCredSource, + CredentialSource: &testBaseCredSource, Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, WorkforcePoolUserProject: "myProject", } @@ -172,11 +172,11 @@ func TestWorkforcePoolTokenWithClientID(t *testing.T) { } func TestWorkforcePoolTokenWithoutClientID(t *testing.T) { - config := ExternalAccountConfig{ + config := Config{ Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", ClientSecret: "notsosecret", - CredentialSource: testBaseCredSource, + CredentialSource: &testBaseCredSource, Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, WorkforcePoolUserProject: "myProject", } @@ -199,23 +199,23 @@ func TestWorkforcePoolTokenWithoutClientID(t *testing.T) { } func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) { - config := ExternalAccountConfig{ + config := Config{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", TokenURL: "https://sts.googleapis.com", ClientSecret: "notsosecret", ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: testBaseCredSource, + CredentialSource: &testBaseCredSource, Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, WorkforcePoolUserProject: "myProject", } - _, err := config.TokenSource(context.Background()) + _, err := NewTokenSource(context.Background(), config) if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials"; got != want { + if got, want := err.Error(), "oauth2/google: Workforce pool user project should not be set for non-workforce pool credentials"; got != want { t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) } } @@ -247,7 +247,7 @@ func TestWorkforcePoolCreation(t *testing.T) { config.ServiceAccountImpersonationURL = "https://iamcredentials.googleapis.com" config.Audience = tt.audience config.WorkforcePoolUserProject = "myProject" - _, err := config.TokenSource(ctx) + _, err := NewTokenSource(ctx, config) if tt.expectSuccess && err != nil { t.Errorf("got %v but want nil", err) diff --git a/google/externalaccount/executablecredsource.go b/google/externalaccount/executablecredsource.go index b86195f8c..d30f5f8b8 100644 --- a/google/externalaccount/executablecredsource.go +++ b/google/externalaccount/executablecredsource.go @@ -140,13 +140,13 @@ type executableCredentialSource struct { Timeout time.Duration OutputFile string ctx context.Context - config *ExternalAccountConfig + config *Config env environment } // CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig. // It also performs defaulting and type conversions. -func createExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *ExternalAccountConfig) (executableCredentialSource, error) { +func createExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) { if ec.Command == "" { return executableCredentialSource{}, commandMissingError() } diff --git a/google/externalaccount/executablecredsource_test.go b/google/externalaccount/executablecredsource_test.go index 25c534157..69ec21ae1 100644 --- a/google/externalaccount/executablecredsource_test.go +++ b/google/externalaccount/executablecredsource_test.go @@ -160,16 +160,16 @@ func TestCreateExecutableCredential(t *testing.T) { var getEnvironmentTests = []struct { name string - config ExternalAccountConfig + config Config environment testEnvironment expectedEnvironment []string }{ { name: "Minimal Executable Config", - config: ExternalAccountConfig{ + config: Config{ Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", - CredentialSource: CredentialSource{ + CredentialSource: &CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", }, @@ -189,11 +189,11 @@ var getEnvironmentTests = []struct { }, { name: "Full Impersonation URL", - config: ExternalAccountConfig{ + config: Config{ Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", - CredentialSource: CredentialSource{ + CredentialSource: &CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", OutputFile: "/path/to/generated/cached/credentials", @@ -216,11 +216,11 @@ var getEnvironmentTests = []struct { }, { name: "Impersonation Email", - config: ExternalAccountConfig{ + config: Config{ Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc", ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", - CredentialSource: CredentialSource{ + CredentialSource: &CredentialSource{ Executable: &ExecutableConfig{ Command: "blarg", OutputFile: "/path/to/generated/cached/credentials", @@ -471,7 +471,7 @@ func TestRetrieveExecutableSubjectTokenExecutableErrors(t *testing.T) { } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { @@ -578,7 +578,7 @@ func TestRetrieveExecutableSubjectTokenSuccesses(t *testing.T) { } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { @@ -629,7 +629,7 @@ func TestRetrieveOutputFileSubjectTokenNotJSON(t *testing.T) { } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { @@ -778,7 +778,7 @@ func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) { } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { @@ -881,7 +881,7 @@ func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) { } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { @@ -986,7 +986,7 @@ func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) { } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { diff --git a/google/externalaccount/filecredsource.go b/google/externalaccount/filecredsource.go index f35f73c5c..49fa101d9 100644 --- a/google/externalaccount/filecredsource.go +++ b/google/externalaccount/filecredsource.go @@ -16,7 +16,7 @@ import ( type fileCredentialSource struct { File string - Format format + Format Format } func (cs fileCredentialSource) credentialSourceType() string { diff --git a/google/externalaccount/filecredsource_test.go b/google/externalaccount/filecredsource_test.go index 9c559c4d6..dc561bd90 100644 --- a/google/externalaccount/filecredsource_test.go +++ b/google/externalaccount/filecredsource_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -var testFileConfig = ExternalAccountConfig{ +var testFileConfig = Config{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", TokenURL: "http://localhost:8080/v1/token", @@ -36,7 +36,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) { name: "TextFileSource", cs: CredentialSource{ File: textBaseCredPath, - Format: format{Type: fileTypeText}, + Format: Format{Type: fileTypeText}, }, want: "street123", }, @@ -44,7 +44,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) { name: "JSONFileSource", cs: CredentialSource{ File: jsonBaseCredPath, - Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, + Format: Format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, }, want: "321road", }, @@ -53,7 +53,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) { for _, test := range fileSourceTests { test := test tfc := testFileConfig - tfc.CredentialSource = test.cs + tfc.CredentialSource = &test.cs t.Run(test.name, func(t *testing.T) { base, err := tfc.parse(context.Background()) diff --git a/google/externalaccount/impersonate_test.go b/google/externalaccount/impersonate_test.go index 01ff5972f..930ac9c10 100644 --- a/google/externalaccount/impersonate_test.go +++ b/google/externalaccount/impersonate_test.go @@ -73,19 +73,19 @@ func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Serv var impersonationTests = []struct { name string - config ExternalAccountConfig + config Config expectedImpersonationBody string expectedMetricsHeader string }{ { name: "Base Impersonation", - config: ExternalAccountConfig{ + config: Config{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", TokenInfoURL: "http://localhost:8080/v1/tokeninfo", ClientSecret: "notsosecret", ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: testBaseCredSource, + CredentialSource: &testBaseCredSource, Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, }, expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", @@ -93,13 +93,13 @@ var impersonationTests = []struct { }, { name: "With TokenLifetime Set", - config: ExternalAccountConfig{ + config: Config{ Audience: "32555940559.apps.googleusercontent.com", SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", TokenInfoURL: "http://localhost:8080/v1/tokeninfo", ClientSecret: "notsosecret", ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: testBaseCredSource, + CredentialSource: &testBaseCredSource, Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, ServiceAccountImpersonationLifetimeSeconds: 10000, }, diff --git a/google/externalaccount/urlcredsource.go b/google/externalaccount/urlcredsource.go index 606bb4e80..a9a30ef7f 100644 --- a/google/externalaccount/urlcredsource.go +++ b/google/externalaccount/urlcredsource.go @@ -19,7 +19,7 @@ import ( type urlCredentialSource struct { URL string Headers map[string]string - Format format + Format Format ctx context.Context } diff --git a/google/externalaccount/urlcredsource_test.go b/google/externalaccount/urlcredsource_test.go index 699f7729e..4968db4dd 100644 --- a/google/externalaccount/urlcredsource_test.go +++ b/google/externalaccount/urlcredsource_test.go @@ -28,11 +28,11 @@ func TestRetrieveURLSubjectToken_Text(t *testing.T) { heads["Metadata"] = "True" cs := CredentialSource{ URL: ts.URL, - Format: format{Type: fileTypeText}, + Format: Format{Type: fileTypeText}, Headers: heads, } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { @@ -60,7 +60,7 @@ func TestRetrieveURLSubjectToken_Untyped(t *testing.T) { URL: ts.URL, } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { @@ -93,10 +93,10 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) { })) cs := CredentialSource{ URL: ts.URL, - Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, + Format: Format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { @@ -115,10 +115,10 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) { func TestURLCredential_CredentialSourceType(t *testing.T) { cs := CredentialSource{ URL: "http://example.com", - Format: format{Type: fileTypeText}, + Format: Format{Type: fileTypeText}, } tfc := testFileConfig - tfc.CredentialSource = cs + tfc.CredentialSource = &cs base, err := tfc.parse(context.Background()) if err != nil { diff --git a/google/google.go b/google/google.go index 96b6a416f..ba931c2c3 100644 --- a/google/google.go +++ b/google/google.go @@ -17,6 +17,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/google/externalaccount" "golang.org/x/oauth2/google/internal/externalaccountauthorizeduser" + "golang.org/x/oauth2/google/internal/impersonate" "golang.org/x/oauth2/jwt" ) @@ -191,7 +192,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar tok := &oauth2.Token{RefreshToken: f.RefreshToken} return cfg.TokenSource(ctx, tok), nil case externalAccountKey: - cfg := &externalaccount.ExternalAccountConfig{ + cfg := &externalaccount.Config{ Audience: f.Audience, SubjectTokenType: f.SubjectTokenType, TokenURL: f.TokenURLExternal, @@ -200,12 +201,12 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds, ClientSecret: f.ClientSecret, ClientID: f.ClientID, - CredentialSource: f.CredentialSource, + CredentialSource: &f.CredentialSource, QuotaProjectID: f.QuotaProjectID, Scopes: params.Scopes, WorkforcePoolUserProject: f.WorkforcePoolUserProject, } - return cfg.TokenSource(ctx) + return externalaccount.NewTokenSource(ctx, *cfg) case externalAccountAuthorizedUserKey: cfg := &externalaccountauthorizeduser.Config{ Audience: f.Audience, @@ -228,7 +229,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar if err != nil { return nil, err } - imp := externalaccount.ImpersonateTokenSource{ + imp := impersonate.ImpersonateTokenSource{ Ctx: ctx, URL: f.ServiceAccountImpersonationURL, Scopes: params.Scopes, diff --git a/google/externalaccount/impersonate.go b/google/internal/impersonate/impersonate.go similarity index 99% rename from google/externalaccount/impersonate.go rename to google/internal/impersonate/impersonate.go index 54c8f209f..6bc3af110 100644 --- a/google/externalaccount/impersonate.go +++ b/google/internal/impersonate/impersonate.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package externalaccount +package impersonate import ( "bytes" From 4bbff2ae46ad7d2ebdf031d4266115ddc4fb00df Mon Sep 17 00:00:00 2001 From: aeitzman Date: Wed, 31 Jan 2024 11:12:54 -0800 Subject: [PATCH 12/22] Added tests --- google/externalaccount/aws_test.go | 40 +++- google/externalaccount/basecredentials.go | 4 +- .../externalaccount/basecredentials_test.go | 201 +++++++++++++++++- google/externalaccount/impersonate_test.go | 144 ------------- 4 files changed, 236 insertions(+), 153 deletions(-) delete mode 100644 google/externalaccount/impersonate_test.go diff --git a/google/externalaccount/aws_test.go b/google/externalaccount/aws_test.go index 7f6a86388..fc387db15 100644 --- a/google/externalaccount/aws_test.go +++ b/google/externalaccount/aws_test.go @@ -1322,9 +1322,10 @@ func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) { func TestAWSCredential_ProgrammaticAuthError(t *testing.T) { tfc := testFileConfig + testErr := errors.New("test error") tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{ awsRegion: "us-east-2", - err: errors.New("test error"), + err: testErr, credentials: nil, } @@ -1337,8 +1338,36 @@ func TestAWSCredential_ProgrammaticAuthError(t *testing.T) { if err == nil { t.Fatalf("subjectToken() should have failed") } - if got, want := err.Error(), "test error"; !reflect.DeepEqual(got, want) { - t.Errorf("subjectToken = %q, want %q", got, want) + if err != testErr { + t.Errorf("error = %e, want %e", err, testErr) + } +} + +func TestAWSCredential_ProgrammaticAuthRegionError(t *testing.T) { + tfc := testFileConfig + securityCredentials := AwsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + } + + testErr := errors.New("test") + tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{ + awsRegion: "", + regionErr: testErr, + credentials: &securityCredentials, + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err == nil { + t.Fatalf("subjectToken() should have failed") + } + if err != testErr { + t.Errorf("error = %e, want %e", err, testErr) } } @@ -1361,13 +1390,14 @@ func TestAwsCredential_CredentialSourceType(t *testing.T) { type testAwsSupplier struct { err error + regionErr error awsRegion string credentials *AwsSecurityCredentials } func (supp testAwsSupplier) AwsRegion() (string, error) { - if supp.err != nil { - return "", supp.err + if supp.regionErr != nil { + return "", supp.regionErr } return supp.awsRegion, nil } diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 609903930..0131223c2 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -208,10 +208,10 @@ func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error count++ } if count == 0 { - return nil, fmt.Errorf("oauth2/google: One of CredentialSource, SubjectTokenSUpplier, or AwsSecurityCredentialsSupplier must be set") + return nil, fmt.Errorf("oauth2/google: One of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set") } if count > 1 { - return nil, fmt.Errorf("oauth2/google: Only one of CredentialSource, SubjectTokenSUpplier, or AwsSecurityCredentialsSupplier must be set") + return nil, fmt.Errorf("oauth2/google: Only one of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set") } return conf.tokenSource(ctx, "https") } diff --git a/google/externalaccount/basecredentials_test.go b/google/externalaccount/basecredentials_test.go index 5747e07b0..26caadedf 100644 --- a/google/externalaccount/basecredentials_test.go +++ b/google/externalaccount/basecredentials_test.go @@ -17,8 +17,10 @@ import ( ) const ( - textBaseCredPath = "testdata/3pi_cred.txt" - jsonBaseCredPath = "testdata/3pi_cred.json" + textBaseCredPath = "testdata/3pi_cred.txt" + jsonBaseCredPath = "testdata/3pi_cred.json" + baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" + baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}` ) var testBaseCredSource = CredentialSource{ @@ -112,6 +114,60 @@ func validateToken(t *testing.T, tok *oauth2.Token) { } } +func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.URL.String(), urlWanted; got != want { + t.Errorf("URL.String(): got %v but want %v", got, want) + } + headerAuth := r.Header.Get("Authorization") + if got, want := headerAuth, authWanted; got != want { + t.Errorf("got %v but want %v", got, want) + } + headerContentType := r.Header.Get("Content-Type") + if got, want := headerContentType, "application/json"; got != want { + t.Errorf("got %v but want %v", got, want) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("Failed reading request body: %v.", err) + } + if got, want := string(body), bodyWanted; got != want { + t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(response)) + })) +} + +func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.URL.String(), "/"; got != want { + t.Errorf("URL.String(): got %v but want %v", got, want) + } + headerAuth := r.Header.Get("Authorization") + if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want { + t.Errorf("got %v but want %v", got, want) + } + headerContentType := r.Header.Get("Content-Type") + if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want { + t.Errorf("got %v but want %v", got, want) + } + headerMetrics := r.Header.Get("x-goog-api-client") + if got, want := headerMetrics, metricsHeaderWanted; got != want { + t.Errorf("got %v but want %v", got, want) + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("Failed reading request body: %v.", err) + } + if got, want := string(body), baseImpersonateCredsReqBody; got != want { + t.Errorf("Unexpected exchange payload: got %v but want %v", got, want) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(baseCredsResponseBody)) + })) +} + func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string { return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime) } @@ -257,3 +313,144 @@ func TestWorkforcePoolCreation(t *testing.T) { }) } } + +var impersonationTests = []struct { + name string + config Config + expectedImpersonationBody string + expectedMetricsHeader string +}{ + { + name: "Base Impersonation", + config: Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + CredentialSource: &testBaseCredSource, + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + }, + expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", + expectedMetricsHeader: getExpectedMetricsHeader("file", true, false), + }, + { + name: "With TokenLifetime Set", + config: Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + CredentialSource: &testBaseCredSource, + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + ServiceAccountImpersonationLifetimeSeconds: 10000, + }, + expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", + expectedMetricsHeader: getExpectedMetricsHeader("file", true, true), + }, +} + +func TestImpersonation(t *testing.T) { + for _, tt := range impersonationTests { + t.Run(tt.name, func(t *testing.T) { + testImpersonateConfig := tt.config + impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t) + defer impersonateServer.Close() + testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL + + targetServer := createTargetServer(tt.expectedMetricsHeader, t) + defer targetServer.Close() + testImpersonateConfig.TokenURL = targetServer.URL + + ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http") + if err != nil { + t.Fatalf("Failed to create TokenSource: %v", err) + } + + oldNow := now + defer func() { now = oldNow }() + now = testNow + + tok, err := ourTS.Token() + if err != nil { + t.Fatalf("Unexpected error: %e", err) + } + if got, want := tok.AccessToken, "Second.Access.Token"; got != want { + t.Errorf("Unexpected access token: got %v, but wanted %v", got, want) + } + if got, want := tok.TokenType, "Bearer"; got != want { + t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want) + } + }) + } +} + +var newTokenTests = []struct { + name string + config Config +}{ + { + name: "Missing Audience", + config: Config{ + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + CredentialSource: &testBaseCredSource, + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + ServiceAccountImpersonationLifetimeSeconds: 10000, + }, + }, + { + name: "Missing Subject Token Type", + config: Config{ + Audience: "32555940559.apps.googleusercontent.com", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + CredentialSource: &testBaseCredSource, + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + ServiceAccountImpersonationLifetimeSeconds: 10000, + }, + }, + { + name: "No Cred Source", + config: Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + ServiceAccountImpersonationLifetimeSeconds: 10000, + }, + }, + { + name: "Cred Source and Supplier", + config: Config{ + Audience: "32555940559.apps.googleusercontent.com", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", + TokenInfoURL: "http://localhost:8080/v1/tokeninfo", + CredentialSource: &testBaseCredSource, + AwsSecurityCredentialsSupplier: testAwsSupplier{}, + ClientSecret: "notsosecret", + ClientID: "rbrgnognrhongo3bi4gb9ghg9g", + Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, + ServiceAccountImpersonationLifetimeSeconds: 10000, + }, + }, +} + +func TestNewToken(t *testing.T) { + for _, tt := range newTokenTests { + t.Run(tt.name, func(t *testing.T) { + testConfig := tt.config + + _, err := NewTokenSource(context.Background(), testConfig) + if err == nil { + t.Fatalf("expected error when calling NewToken()") + } + }) + } +} diff --git a/google/externalaccount/impersonate_test.go b/google/externalaccount/impersonate_test.go deleted file mode 100644 index 930ac9c10..000000000 --- a/google/externalaccount/impersonate_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package externalaccount - -import ( - "context" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" -) - -var ( - baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" - baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}` -) - -func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got, want := r.URL.String(), urlWanted; got != want { - t.Errorf("URL.String(): got %v but want %v", got, want) - } - headerAuth := r.Header.Get("Authorization") - if got, want := headerAuth, authWanted; got != want { - t.Errorf("got %v but want %v", got, want) - } - headerContentType := r.Header.Get("Content-Type") - if got, want := headerContentType, "application/json"; got != want { - t.Errorf("got %v but want %v", got, want) - } - body, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Fatalf("Failed reading request body: %v.", err) - } - if got, want := string(body), bodyWanted; got != want { - t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want) - } - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(response)) - })) -} - -func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got, want := r.URL.String(), "/"; got != want { - t.Errorf("URL.String(): got %v but want %v", got, want) - } - headerAuth := r.Header.Get("Authorization") - if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want { - t.Errorf("got %v but want %v", got, want) - } - headerContentType := r.Header.Get("Content-Type") - if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want { - t.Errorf("got %v but want %v", got, want) - } - headerMetrics := r.Header.Get("x-goog-api-client") - if got, want := headerMetrics, metricsHeaderWanted; got != want { - t.Errorf("got %v but want %v", got, want) - } - body, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Fatalf("Failed reading request body: %v.", err) - } - if got, want := string(body), baseImpersonateCredsReqBody; got != want { - t.Errorf("Unexpected exchange payload: got %v but want %v", got, want) - } - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(baseCredsResponseBody)) - })) -} - -var impersonationTests = []struct { - name string - config Config - expectedImpersonationBody string - expectedMetricsHeader string -}{ - { - name: "Base Impersonation", - config: Config{ - Audience: "32555940559.apps.googleusercontent.com", - SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", - TokenInfoURL: "http://localhost:8080/v1/tokeninfo", - ClientSecret: "notsosecret", - ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: &testBaseCredSource, - Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, - }, - expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", - expectedMetricsHeader: getExpectedMetricsHeader("file", true, false), - }, - { - name: "With TokenLifetime Set", - config: Config{ - Audience: "32555940559.apps.googleusercontent.com", - SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", - TokenInfoURL: "http://localhost:8080/v1/tokeninfo", - ClientSecret: "notsosecret", - ClientID: "rbrgnognrhongo3bi4gb9ghg9g", - CredentialSource: &testBaseCredSource, - Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, - ServiceAccountImpersonationLifetimeSeconds: 10000, - }, - expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", - expectedMetricsHeader: getExpectedMetricsHeader("file", true, true), - }, -} - -func TestImpersonation(t *testing.T) { - for _, tt := range impersonationTests { - t.Run(tt.name, func(t *testing.T) { - testImpersonateConfig := tt.config - impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t) - defer impersonateServer.Close() - testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL - - targetServer := createTargetServer(tt.expectedMetricsHeader, t) - defer targetServer.Close() - testImpersonateConfig.TokenURL = targetServer.URL - - ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http") - if err != nil { - t.Fatalf("Failed to create TokenSource: %v", err) - } - - oldNow := now - defer func() { now = oldNow }() - now = testNow - - tok, err := ourTS.Token() - if err != nil { - t.Fatalf("Unexpected error: %e", err) - } - if got, want := tok.AccessToken, "Second.Access.Token"; got != want { - t.Errorf("Unexpected access token: got %v, but wanted %v", got, want) - } - if got, want := tok.TokenType, "Bearer"; got != want { - t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want) - } - }) - } -} From 393ef15ec46380f5812da7750d24afadc21b364e Mon Sep 17 00:00:00 2001 From: aeitzman Date: Wed, 31 Jan 2024 12:14:43 -0800 Subject: [PATCH 13/22] Changing docs --- google/doc.go | 88 ++------------------------------------------------- 1 file changed, 2 insertions(+), 86 deletions(-) diff --git a/google/doc.go b/google/doc.go index 2dad55ad2..830d268c1 100644 --- a/google/doc.go +++ b/google/doc.go @@ -22,93 +22,9 @@ // the other by JWTConfigFromJSON. The returned Config can be used to obtain a TokenSource or // create an http.Client. // -// # Workload Identity Federation +// # Workload and Workforce Identity Federation // -// Using workload identity federation, your application can access Google Cloud -// resources from Amazon Web Services (AWS), Microsoft Azure or any identity -// provider that supports OpenID Connect (OIDC) or SAML 2.0. -// Traditionally, applications running outside Google Cloud have used service -// account keys to access Google Cloud resources. Using identity federation, -// you can allow your workload to impersonate a service account. -// This lets you access Google Cloud resources directly, eliminating the -// maintenance and security burden associated with service account keys. -// -// Follow the detailed instructions on how to configure Workload Identity Federation -// in various platforms: -// -// Amazon Web Services (AWS): https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#aws -// Microsoft Azure: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#azure -// OIDC identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#oidc -// SAML 2.0 identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#saml -// -// For OIDC and SAML providers, the library can retrieve tokens in fours ways: -// from a local file location (file-sourced credentials), from a server -// (URL-sourced credentials), from a local executable (executable-sourced -// credentials), or from a user defined function that returns an OIDC or SAML token. -// For file-sourced credentials, a background process needs to be continuously -// refreshing the file location with a new OIDC/SAML token prior to expiration. -// For tokens with one hour lifetimes, the token needs to be updated in the file -// every hour. The token can be stored directly as plain text or in JSON format. -// For URL-sourced credentials, a local server needs to host a GET endpoint to -// return the OIDC/SAML token. The response can be in plain text or JSON. -// Additional required request headers can also be specified. -// For executable-sourced credentials, an application needs to be available to -// output the OIDC/SAML token and other information in a JSON format. -// For more information on how these work (and how to implement -// executable-sourced credentials), please check out: -// https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration -// For using a user definied function to supply the token, see the [golang.org/x/oauth2/google/externalaccount] package. -// -// Note that this library does not perform any validation on the token_url, token_info_url, -// or service_account_impersonation_url fields of the credential configuration. -// It is not recommended to use a credential configuration that you did not generate with -// the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain. -// -// # Workforce Identity Federation -// -// Workforce identity federation lets you use an external identity provider (IdP) to -// authenticate and authorize a workforce—a group of users, such as employees, partners, -// and contractors—using IAM, so that the users can access Google Cloud services. -// Workforce identity federation extends Google Cloud's identity capabilities to support -// syncless, attribute-based single sign on. -// -// With workforce identity federation, your workforce can access Google Cloud resources -// using an external identity provider (IdP) that supports OpenID Connect (OIDC) or -// SAML 2.0 such as Azure Active Directory (Azure AD), Active Directory Federation -// Services (AD FS), Okta, and others. -// -// Follow the detailed instructions on how to configure Workload Identity Federation -// in various platforms: -// -// Azure AD: https://cloud.google.com/iam/docs/workforce-sign-in-azure-ad -// Okta: https://cloud.google.com/iam/docs/workforce-sign-in-okta -// OIDC identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#oidc -// SAML 2.0 identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#saml -// -// For workforce identity federation, the library can retrieve tokens in four ways: -// from a local file location (file-sourced credentials), from a server -// (URL-sourced credentials), from a local executable (executable-sourced -// credentials), or from a user supplied function that returns an OIDC or SAML token. -// For file-sourced credentials, a background process needs to be continuously -// refreshing the file location with a new OIDC/SAML token prior to expiration. -// For tokens with one hour lifetimes, the token needs to be updated in the file -// every hour. The token can be stored directly as plain text or in JSON format. -// For URL-sourced credentials, a local server needs to host a GET endpoint to -// return the OIDC/SAML token. The response can be in plain text or JSON. -// Additional required request headers can also be specified. -// For executable-sourced credentials, an application needs to be available to -// output the OIDC/SAML token and other information in a JSON format. -// For more information on how these work (and how to implement -// executable-sourced credentials), please check out: -// https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in -// For using a user definied function to supply the token, see the [golang.org/x/oauth2/google/externalaccount] package. -// -// # Security considerations -// -// Note that this library does not perform any validation on the token_url, token_info_url, -// or service_account_impersonation_url fields of the credential configuration. -// It is not recommended to use a credential configuration that you did not generate with -// the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain. +// For information on how to use Workload and Workforce Identity Federation, see [golang.org/x/oauth2/google/externalaccount]. // // # Credentials // From 93edc98a61add85aae4202457440f7f2881da727 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Wed, 31 Jan 2024 12:53:23 -0800 Subject: [PATCH 14/22] Adding context to supplier interfaces --- google/externalaccount/aws.go | 5 +- google/externalaccount/aws_test.go | 54 +++++++++++++++-- google/externalaccount/basecredentials.go | 46 +++++++++++---- .../programmaticrefreshcredsource.go | 5 +- .../programmaticrefreshcredsource_test.go | 59 ++++++++++++++++--- 5 files changed, 138 insertions(+), 31 deletions(-) diff --git a/google/externalaccount/aws.go b/google/externalaccount/aws.go index 4c67d3d65..e41ded149 100644 --- a/google/externalaccount/aws.go +++ b/google/externalaccount/aws.go @@ -268,6 +268,7 @@ type awsCredentialSource struct { ctx context.Context client *http.Client awsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier + supplierContext SupplierContext } type awsRequestHeader struct { @@ -432,7 +433,7 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) { func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) { if cs.awsSecurityCredentialsSupplier != nil { - return cs.awsSecurityCredentialsSupplier.AwsRegion() + return cs.awsSecurityCredentialsSupplier.AwsRegion(cs.supplierContext) } if canRetrieveRegionFromEnvironment() { if envAwsRegion := getenv(awsRegion); envAwsRegion != "" { @@ -481,7 +482,7 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result *AwsSecurityCredentials, err error) { if cs.awsSecurityCredentialsSupplier != nil { - return cs.awsSecurityCredentialsSupplier.AwsSecurityCredentials() + return cs.awsSecurityCredentialsSupplier.AwsSecurityCredentials(cs.supplierContext) } if canRetrieveSecurityCredentialFromEnvironment() { return &AwsSecurityCredentials{ diff --git a/google/externalaccount/aws_test.go b/google/externalaccount/aws_test.go index fc387db15..ceb989c95 100644 --- a/google/externalaccount/aws_test.go +++ b/google/externalaccount/aws_test.go @@ -1371,6 +1371,31 @@ func TestAWSCredential_ProgrammaticAuthRegionError(t *testing.T) { } } +func TestAWSCredential_ProgrammaticAuthContext(t *testing.T) { + tfc := testFileConfig + securityCredentials := AwsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + } + expectedContext := SupplierContext{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType} + + tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{ + awsRegion: "us-east-2", + credentials: &securityCredentials, + expectedContext: &expectedContext, + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err != nil { + t.Fatalf("subjectToken() failed %v", err) + } +} + func TestAwsCredential_CredentialSourceType(t *testing.T) { server := createDefaultAwsTestServer() ts := httptest.NewServer(server) @@ -1389,22 +1414,39 @@ func TestAwsCredential_CredentialSourceType(t *testing.T) { } type testAwsSupplier struct { - err error - regionErr error - awsRegion string - credentials *AwsSecurityCredentials + err error + regionErr error + awsRegion string + credentials *AwsSecurityCredentials + expectedContext *SupplierContext } -func (supp testAwsSupplier) AwsRegion() (string, error) { +func (supp testAwsSupplier) AwsRegion(ctx SupplierContext) (string, error) { if supp.regionErr != nil { return "", supp.regionErr } + if supp.expectedContext != nil { + if supp.expectedContext.Audience != ctx.Audience { + return "", errors.New("Audience does not match") + } + if supp.expectedContext.SubjectTokenType != ctx.SubjectTokenType { + return "", errors.New("Audience does not match") + } + } return supp.awsRegion, nil } -func (supp testAwsSupplier) AwsSecurityCredentials() (*AwsSecurityCredentials, error) { +func (supp testAwsSupplier) AwsSecurityCredentials(ctx SupplierContext) (*AwsSecurityCredentials, error) { if supp.err != nil { return nil, supp.err } + if supp.expectedContext != nil { + if supp.expectedContext.Audience != ctx.Audience { + return nil, errors.New("Audience does not match") + } + if supp.expectedContext.SubjectTokenType != ctx.SubjectTokenType { + return nil, errors.New("Audience does not match") + } + } return supp.credentials, nil } diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 0131223c2..0d1cf5a2e 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -93,7 +93,7 @@ For more information on how these work (and how to implement executable-sourced credentials), please check out: https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in -For using a user definied function to supply the token, define a function that can return +For using a user defined function to supply the token, define a function that can return either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] for AWS providers. This function can then be used when building an [Config]. The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google @@ -167,11 +167,9 @@ type Config struct { // The underlying principal must still have serviceusage.services.use IAM // permission to use the project for billing/quota. Optional. WorkforcePoolUserProject string - // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. This should be a function that returns - // a valid subject token as a string. Optional. - SubjectTokenSupplier func() (string, error) - // AwsSecurityCredentialsSupplier is an AWS Security Credential supplier. This should contain a - // function that returns valid AwsSecurityCredentials and a valid AwsRegion. Optional. + // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. Optional. + SubjectTokenSupplier SubjectTokenSupplier + // AwsSecurityCredentialsSupplier is an AWS Security Credential supplier for AWS credentials. Optional. AwsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier } @@ -247,6 +245,7 @@ const ( defaultTokenUrl = "https://sts.googleapis.com/v1/token" ) +// Format contains information needed to retireve a subject token for URL or File sourced credentials. type Format struct { // Type is either "text" or "json". When not provided "text" type is assumed. Type string `json:"type"` @@ -282,19 +281,40 @@ type CredentialSource struct { Format Format `json:"format"` } +// ExecutableConfig contains information needed for executable sourced credentials. type ExecutableConfig struct { - Command string `json:"command"` - TimeoutMillis *int `json:"timeout_millis"` - OutputFile string `json:"output_file"` + // Command is the the full command to run to retrieve the subject token. + // This can include arguments. Must be an absolute path for the program. Required. + Command string `json:"command"` + // TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30 seconds when not provided. Optional. + TimeoutMillis *int `json:"timeout_millis"` + // OutputFile is the absolute path to the output file where the executable will cache the response. + // If specified the auth libraries will first check this location before running the executable. Optional. + OutputFile string `json:"output_file"` +} + +// SubjectTokenSupplier can be used to supply a subject token to exchange for a GCP access token. +type SubjectTokenSupplier interface { + // AwsRegion should return a valid subject token or an error. + SubjectToken(context SupplierContext) (string, error) } // AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an Aws Region to // exchange for a GCP access token. type AwsSecurityCredentialsSupplier interface { // AwsRegion should return the AWS region or an error. - AwsRegion() (string, error) + AwsRegion(context SupplierContext) (string, error) // GetAwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error. - AwsSecurityCredentials() (*AwsSecurityCredentials, error) + AwsSecurityCredentials(context SupplierContext) (*AwsSecurityCredentials, error) +} + +// SupplierContext contains information about the requested subject token or Aws credentials from the +// Google external account credential. +type SupplierContext struct { + // Audience is the requested audience for the external account credential. + Audience string + // Subject token type is the requested subject token type fro the external account credential. + SubjectTokenType string } // parse determines the type of CredentialSource needed. @@ -303,15 +323,17 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { if c.TokenURL == "" { c.TokenURL = defaultTokenUrl } + supplierContext := SupplierContext{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType} if c.AwsSecurityCredentialsSupplier != nil { awsCredSource := awsCredentialSource{ awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier, targetResource: c.Audience, + supplierContext: supplierContext, } return awsCredSource, nil } else if c.SubjectTokenSupplier != nil { - return programmaticRefreshCredentialSource{SubjectTokenSupplier: c.SubjectTokenSupplier}, nil + return programmaticRefreshCredentialSource{subjectTokenSupplier: c.SubjectTokenSupplier, supplierContext: supplierContext}, nil } else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { if awsVersion != 1 { diff --git a/google/externalaccount/programmaticrefreshcredsource.go b/google/externalaccount/programmaticrefreshcredsource.go index f7c795fea..b49f1b785 100644 --- a/google/externalaccount/programmaticrefreshcredsource.go +++ b/google/externalaccount/programmaticrefreshcredsource.go @@ -5,7 +5,8 @@ package externalaccount type programmaticRefreshCredentialSource struct { - SubjectTokenSupplier func() (string, error) + supplierContext SupplierContext + subjectTokenSupplier SubjectTokenSupplier } func (cs programmaticRefreshCredentialSource) credentialSourceType() string { @@ -13,5 +14,5 @@ func (cs programmaticRefreshCredentialSource) credentialSourceType() string { } func (cs programmaticRefreshCredentialSource) subjectToken() (string, error) { - return cs.SubjectTokenSupplier() + return cs.subjectTokenSupplier.SubjectToken(cs.supplierContext) } diff --git a/google/externalaccount/programmaticrefreshcredsource_test.go b/google/externalaccount/programmaticrefreshcredsource_test.go index a1a19d349..85059a5c6 100644 --- a/google/externalaccount/programmaticrefreshcredsource_test.go +++ b/google/externalaccount/programmaticrefreshcredsource_test.go @@ -7,15 +7,14 @@ package externalaccount import ( "context" "errors" - "reflect" "testing" ) func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) { tfc := testConfig - tfc.SubjectTokenSupplier = func() (string, error) { - return "subjectToken", nil + tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ + subjectToken: "subjectToken", } base, err := tfc.parse(context.Background()) @@ -28,16 +27,17 @@ func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) { t.Fatalf("retrieveSubjectToken() failed: %v", err) } - if got, want := out, "subjectToken"; !reflect.DeepEqual(got, want) { - t.Errorf("subjectToken = \n%q\n want \n%q", got, want) + if out != "subjectToken" { + t.Errorf("subjectToken = \n%q\n want \nSubjectToken", out) } } func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) { tfc := testConfig + testError := errors.New("test error") - tfc.SubjectTokenSupplier = func() (string, error) { - return "", errors.New("test error") + tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ + err: testError, } base, err := tfc.parse(context.Background()) @@ -49,7 +49,48 @@ func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) { if err == nil { t.Fatalf("subjectToken() should have failed") } - if got, want := err.Error(), "test error"; !reflect.DeepEqual(got, want) { - t.Errorf("subjectToken = %q, want %q", got, want) + if testError != err { + t.Errorf("subjectToken = %e, want %e", err, testError) } } + +func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) { + tfc := testConfig + expectedContext := SupplierContext{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType} + + tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ + subjectToken: "subjectToken", + expectedContext: &expectedContext, + } + + base, err := tfc.parse(context.Background()) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } +} + +type testSubjectTokenSupplier struct { + err error + subjectToken string + expectedContext *SupplierContext +} + +func (supp testSubjectTokenSupplier) SubjectToken(ctx SupplierContext) (string, error) { + if supp.err != nil { + return "", supp.err + } + if supp.expectedContext != nil { + if supp.expectedContext.Audience != ctx.Audience { + return "", errors.New("Audience does not match") + } + if supp.expectedContext.SubjectTokenType != ctx.SubjectTokenType { + return "", errors.New("Audience does not match") + } + } + return supp.subjectToken, nil +} From 7023750e8d931290af7edf1aacdddd9e45249e2f Mon Sep 17 00:00:00 2001 From: aeitzman Date: Tue, 20 Feb 2024 15:12:34 -0800 Subject: [PATCH 15/22] Adds context to supplier methods and addresses review comments --- google/externalaccount/aws.go | 6 +-- google/externalaccount/aws_test.go | 22 +++++----- google/externalaccount/basecredentials.go | 43 ++++++++++++------- .../programmaticrefreshcredsource.go | 7 ++- .../programmaticrefreshcredsource_test.go | 12 +++--- 5 files changed, 53 insertions(+), 37 deletions(-) diff --git a/google/externalaccount/aws.go b/google/externalaccount/aws.go index e41ded149..68f7326b1 100644 --- a/google/externalaccount/aws.go +++ b/google/externalaccount/aws.go @@ -268,7 +268,7 @@ type awsCredentialSource struct { ctx context.Context client *http.Client awsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier - supplierContext SupplierContext + supplierOptions SupplierOptions } type awsRequestHeader struct { @@ -433,7 +433,7 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) { func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) { if cs.awsSecurityCredentialsSupplier != nil { - return cs.awsSecurityCredentialsSupplier.AwsRegion(cs.supplierContext) + return cs.awsSecurityCredentialsSupplier.AwsRegion(cs.ctx, cs.supplierOptions) } if canRetrieveRegionFromEnvironment() { if envAwsRegion := getenv(awsRegion); envAwsRegion != "" { @@ -482,7 +482,7 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result *AwsSecurityCredentials, err error) { if cs.awsSecurityCredentialsSupplier != nil { - return cs.awsSecurityCredentialsSupplier.AwsSecurityCredentials(cs.supplierContext) + return cs.awsSecurityCredentialsSupplier.AwsSecurityCredentials(cs.ctx, cs.supplierOptions) } if canRetrieveSecurityCredentialFromEnvironment() { return &AwsSecurityCredentials{ diff --git a/google/externalaccount/aws_test.go b/google/externalaccount/aws_test.go index ceb989c95..7590f8b70 100644 --- a/google/externalaccount/aws_test.go +++ b/google/externalaccount/aws_test.go @@ -1377,12 +1377,12 @@ func TestAWSCredential_ProgrammaticAuthContext(t *testing.T) { AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, } - expectedContext := SupplierContext{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType} + expectedOptions := SupplierOptions{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType} tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{ awsRegion: "us-east-2", credentials: &securityCredentials, - expectedContext: &expectedContext, + expectedOptions: &expectedOptions, } base, err := tfc.parse(context.Background()) @@ -1418,33 +1418,33 @@ type testAwsSupplier struct { regionErr error awsRegion string credentials *AwsSecurityCredentials - expectedContext *SupplierContext + expectedOptions *SupplierOptions } -func (supp testAwsSupplier) AwsRegion(ctx SupplierContext) (string, error) { +func (supp testAwsSupplier) AwsRegion(ctx context.Context, options SupplierOptions) (string, error) { if supp.regionErr != nil { return "", supp.regionErr } - if supp.expectedContext != nil { - if supp.expectedContext.Audience != ctx.Audience { + if supp.expectedOptions != nil { + if supp.expectedOptions.Audience != options.Audience { return "", errors.New("Audience does not match") } - if supp.expectedContext.SubjectTokenType != ctx.SubjectTokenType { + if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType { return "", errors.New("Audience does not match") } } return supp.awsRegion, nil } -func (supp testAwsSupplier) AwsSecurityCredentials(ctx SupplierContext) (*AwsSecurityCredentials, error) { +func (supp testAwsSupplier) AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error) { if supp.err != nil { return nil, supp.err } - if supp.expectedContext != nil { - if supp.expectedContext.Audience != ctx.Audience { + if supp.expectedOptions != nil { + if supp.expectedOptions.Audience != options.Audience { return nil, errors.New("Audience does not match") } - if supp.expectedContext.SubjectTokenType != ctx.SubjectTokenType { + if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType { return nil, errors.New("Audience does not match") } } diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 0d1cf5a2e..7f92f383b 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -132,8 +132,13 @@ type Config struct { // Audience is the Secure Token Service (STS) audience which contains the resource name for the workload // identity pool or the workforce pool and the provider identifier in that pool. Required. Audience string - // SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec - // e.g. `urn:ietf:params:oauth:token-type:jwt`. Required. + // SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec. + // Expected values include: + // “urn:ietf:params:oauth:token-type:jwt” + // “urn:ietf:params:oauth:token-type:id-token” + // “urn:ietf:params:oauth:token-type:saml2” + // “urn:ietf:params:aws:token-type:aws4_request” + // Required. SubjectTokenType string // TokenURL is the STS token exchange endpoint. If not provided, will default to // https://sts.googleapis.com/v1/token. Optional. @@ -146,7 +151,7 @@ type Config struct { // required for workload identity pools when APIs to be accessed have not integrated with UberMint. Optional. ServiceAccountImpersonationURL string // ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation - // token will be valid for. If not provided will default to 3600. Optional. + // token will be valid for. If not provided, it will default to 3600. Optional. ServiceAccountImpersonationLifetimeSeconds int // ClientSecret is currently only required if token_info endpoint also // needs to be called with the generated GCP access token. When provided, STS will be @@ -155,7 +160,8 @@ type Config struct { // ClientID is only required in conjunction with ClientSecret, as described above. Optional. ClientID string // CredentialSource contains the necessary information to retrieve the token itself, as well - // as some environmental information. Will be used unless a supplier is provided. Optional. + // as some environmental information. One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or + // CredentialSource must be provided. Optional. CredentialSource *CredentialSource // QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries // will set the x-goog-user-project which overrides the project associated with the credentials. Optional. @@ -167,9 +173,11 @@ type Config struct { // The underlying principal must still have serviceusage.services.use IAM // permission to use the project for billing/quota. Optional. WorkforcePoolUserProject string - // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. Optional. + // SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials. + // One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional. SubjectTokenSupplier SubjectTokenSupplier - // AwsSecurityCredentialsSupplier is an AWS Security Credential supplier for AWS credentials. Optional. + // AwsSecurityCredentialsSupplier is an AWS Security Credential supplier for AWS credentials. + // One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional. AwsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier } @@ -296,24 +304,28 @@ type ExecutableConfig struct { // SubjectTokenSupplier can be used to supply a subject token to exchange for a GCP access token. type SubjectTokenSupplier interface { // AwsRegion should return a valid subject token or an error. - SubjectToken(context SupplierContext) (string, error) + SubjectToken(ctx context.Context, options SupplierOptions) (string, error) } // AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an Aws Region to // exchange for a GCP access token. type AwsSecurityCredentialsSupplier interface { // AwsRegion should return the AWS region or an error. - AwsRegion(context SupplierContext) (string, error) + AwsRegion(ctx context.Context, options SupplierOptions) (string, error) // GetAwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error. - AwsSecurityCredentials(context SupplierContext) (*AwsSecurityCredentials, error) + AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error) } -// SupplierContext contains information about the requested subject token or Aws credentials from the +// SupplierOptions contains information about the requested subject token or Aws credentials from the // Google external account credential. -type SupplierContext struct { +type SupplierOptions struct { // Audience is the requested audience for the external account credential. Audience string - // Subject token type is the requested subject token type fro the external account credential. + // Subject token type is the requested subject token type for the external account credential. Expected values include: + // “urn:ietf:params:oauth:token-type:jwt” + // “urn:ietf:params:oauth:token-type:id-token” + // “urn:ietf:params:oauth:token-type:saml2” + // “urn:ietf:params:aws:token-type:aws4_request” SubjectTokenType string } @@ -323,17 +335,18 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { if c.TokenURL == "" { c.TokenURL = defaultTokenUrl } - supplierContext := SupplierContext{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType} + supplierOptions := SupplierOptions{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType} if c.AwsSecurityCredentialsSupplier != nil { awsCredSource := awsCredentialSource{ awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier, targetResource: c.Audience, - supplierContext: supplierContext, + supplierOptions: supplierOptions, + ctx: ctx, } return awsCredSource, nil } else if c.SubjectTokenSupplier != nil { - return programmaticRefreshCredentialSource{subjectTokenSupplier: c.SubjectTokenSupplier, supplierContext: supplierContext}, nil + return programmaticRefreshCredentialSource{subjectTokenSupplier: c.SubjectTokenSupplier, supplierOptions: supplierOptions, ctx: ctx}, nil } else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { if awsVersion != 1 { diff --git a/google/externalaccount/programmaticrefreshcredsource.go b/google/externalaccount/programmaticrefreshcredsource.go index b49f1b785..6c1abdf2d 100644 --- a/google/externalaccount/programmaticrefreshcredsource.go +++ b/google/externalaccount/programmaticrefreshcredsource.go @@ -4,9 +4,12 @@ package externalaccount +import "context" + type programmaticRefreshCredentialSource struct { - supplierContext SupplierContext + supplierOptions SupplierOptions subjectTokenSupplier SubjectTokenSupplier + ctx context.Context } func (cs programmaticRefreshCredentialSource) credentialSourceType() string { @@ -14,5 +17,5 @@ func (cs programmaticRefreshCredentialSource) credentialSourceType() string { } func (cs programmaticRefreshCredentialSource) subjectToken() (string, error) { - return cs.subjectTokenSupplier.SubjectToken(cs.supplierContext) + return cs.subjectTokenSupplier.SubjectToken(cs.ctx, cs.supplierOptions) } diff --git a/google/externalaccount/programmaticrefreshcredsource_test.go b/google/externalaccount/programmaticrefreshcredsource_test.go index 85059a5c6..7a954b081 100644 --- a/google/externalaccount/programmaticrefreshcredsource_test.go +++ b/google/externalaccount/programmaticrefreshcredsource_test.go @@ -56,11 +56,11 @@ func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) { func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) { tfc := testConfig - expectedContext := SupplierContext{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType} + expectedOptions := SupplierOptions{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType} tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ subjectToken: "subjectToken", - expectedContext: &expectedContext, + expectedContext: &expectedOptions, } base, err := tfc.parse(context.Background()) @@ -77,18 +77,18 @@ func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) { type testSubjectTokenSupplier struct { err error subjectToken string - expectedContext *SupplierContext + expectedContext *SupplierOptions } -func (supp testSubjectTokenSupplier) SubjectToken(ctx SupplierContext) (string, error) { +func (supp testSubjectTokenSupplier) SubjectToken(ctx context.Context, options SupplierOptions) (string, error) { if supp.err != nil { return "", supp.err } if supp.expectedContext != nil { - if supp.expectedContext.Audience != ctx.Audience { + if supp.expectedContext.Audience != options.Audience { return "", errors.New("Audience does not match") } - if supp.expectedContext.SubjectTokenType != ctx.SubjectTokenType { + if supp.expectedContext.SubjectTokenType != options.SubjectTokenType { return "", errors.New("Audience does not match") } } From 74e6f6e2c5b92456ca95fbcdcf1f0444edcd46d1 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Tue, 20 Feb 2024 15:23:04 -0800 Subject: [PATCH 16/22] update docs --- google/externalaccount/basecredentials.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 7f92f383b..028365a6a 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -43,10 +43,9 @@ For more information on how these work (and how to implement executable-sourced credentials), please check out: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration -For using a custom function to supply the token, define a function that can return -either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] -(for AWS providers). This function can then be used when building an [Config]. -The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google +To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers, +or one that implements [AwsSecurityCredentialsSupplier] for AWS providers. This can then be used when building a [Config]. +The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google Cloud resources. For instance, you can create a NewClient from thes [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) @@ -93,11 +92,10 @@ For more information on how these work (and how to implement executable-sourced credentials), please check out: https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in -For using a user defined function to supply the token, define a function that can return -either a token string (for OIDC/SAML providers), or one that returns an [AwsSecurityCredentials] -for AWS providers. This function can then be used when building an [Config]. -The [golang.org/x/oauth2.TokenSource] created from the config can then be used access Google -Cloud resources. For instance, you can create a NewClient from the +To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers. +This can then be used when building a [Config]. +The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google +Cloud resources. For instance, you can create a NewClient from thes [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) # Security considerations From 53cf7630498b3d284f9c619520b0a8f03cf6f9e0 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Wed, 21 Feb 2024 11:02:21 -0800 Subject: [PATCH 17/22] Adds context tests --- google/externalaccount/aws_test.go | 38 ++++++++++++++++++- .../programmaticrefreshcredsource_test.go | 38 ++++++++++++++++--- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/google/externalaccount/aws_test.go b/google/externalaccount/aws_test.go index 7590f8b70..8edc26c43 100644 --- a/google/externalaccount/aws_test.go +++ b/google/externalaccount/aws_test.go @@ -1371,7 +1371,7 @@ func TestAWSCredential_ProgrammaticAuthRegionError(t *testing.T) { } } -func TestAWSCredential_ProgrammaticAuthContext(t *testing.T) { +func TestAWSCredential_ProgrammaticAuthOptions(t *testing.T) { tfc := testFileConfig securityCredentials := AwsSecurityCredentials{ AccessKeyID: accessKeyID, @@ -1396,6 +1396,31 @@ func TestAWSCredential_ProgrammaticAuthContext(t *testing.T) { } } +func TestAWSCredential_ProgrammaticAuthContext(t *testing.T) { + tfc := testFileConfig + securityCredentials := AwsSecurityCredentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + } + ctx := context.Background() + + tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{ + awsRegion: "us-east-2", + credentials: &securityCredentials, + expectedContext: ctx, + } + + base, err := tfc.parse(ctx) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err != nil { + t.Fatalf("subjectToken() failed %v", err) + } +} + func TestAwsCredential_CredentialSourceType(t *testing.T) { server := createDefaultAwsTestServer() ts := httptest.NewServer(server) @@ -1419,6 +1444,7 @@ type testAwsSupplier struct { awsRegion string credentials *AwsSecurityCredentials expectedOptions *SupplierOptions + expectedContext context.Context } func (supp testAwsSupplier) AwsRegion(ctx context.Context, options SupplierOptions) (string, error) { @@ -1433,6 +1459,11 @@ func (supp testAwsSupplier) AwsRegion(ctx context.Context, options SupplierOptio return "", errors.New("Audience does not match") } } + if supp.expectedContext != nil { + if supp.expectedContext != ctx { + return "", errors.New("Context does not match") + } + } return supp.awsRegion, nil } @@ -1448,5 +1479,10 @@ func (supp testAwsSupplier) AwsSecurityCredentials(ctx context.Context, options return nil, errors.New("Audience does not match") } } + if supp.expectedContext != nil { + if supp.expectedContext != ctx { + return nil, errors.New("Context does not match") + } + } return supp.credentials, nil } diff --git a/google/externalaccount/programmaticrefreshcredsource_test.go b/google/externalaccount/programmaticrefreshcredsource_test.go index 7a954b081..7ec16c730 100644 --- a/google/externalaccount/programmaticrefreshcredsource_test.go +++ b/google/externalaccount/programmaticrefreshcredsource_test.go @@ -54,13 +54,13 @@ func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) { } } -func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) { +func TestRetrieveSubjectToken_ProgrammaticAuthOptions(t *testing.T) { tfc := testConfig expectedOptions := SupplierOptions{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType} tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ subjectToken: "subjectToken", - expectedContext: &expectedOptions, + expectedOptions: &expectedOptions, } base, err := tfc.parse(context.Background()) @@ -74,23 +74,49 @@ func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) { } } +func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) { + tfc := testConfig + ctx := context.Background() + + tfc.SubjectTokenSupplier = testSubjectTokenSupplier{ + subjectToken: "subjectToken", + expectedContext: ctx, + } + + base, err := tfc.parse(ctx) + if err != nil { + t.Fatalf("parse() failed %v", err) + } + + _, err = base.subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } +} + type testSubjectTokenSupplier struct { err error subjectToken string - expectedContext *SupplierOptions + expectedOptions *SupplierOptions + expectedContext context.Context } func (supp testSubjectTokenSupplier) SubjectToken(ctx context.Context, options SupplierOptions) (string, error) { if supp.err != nil { return "", supp.err } - if supp.expectedContext != nil { - if supp.expectedContext.Audience != options.Audience { + if supp.expectedOptions != nil { + if supp.expectedOptions.Audience != options.Audience { return "", errors.New("Audience does not match") } - if supp.expectedContext.SubjectTokenType != options.SubjectTokenType { + if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType { return "", errors.New("Audience does not match") } } + if supp.expectedContext != nil { + if supp.expectedContext != ctx { + return "", errors.New("Context does not match") + } + } return supp.subjectToken, nil } From 91a00fcf571ee0da7eb3ff620c0cf5e904aaef1b Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 22 Feb 2024 10:23:59 -0800 Subject: [PATCH 18/22] add more comments --- google/externalaccount/basecredentials.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 028365a6a..6c86a90bf 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -187,7 +187,7 @@ func validateWorkforceAudience(input string) bool { return validWorkforceAudiencePattern.MatchString(input) } -// NewTokenSource Returns an external account TokenSource. +// NewTokenSource Returns an external account TokenSource using the provided external account config. func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error) { if conf.Audience == "" { return nil, fmt.Errorf("oauth2/google: Audience must be set") @@ -253,14 +253,17 @@ const ( // Format contains information needed to retireve a subject token for URL or File sourced credentials. type Format struct { - // Type is either "text" or "json". When not provided "text" type is assumed. + // Type should be either "text" or "json". This determines whether the file or URL sourced credentials + // expect a simple text subject token or if the subject token will be contained in a JSON object. + // When not provided "text" type is assumed. Type string `json:"type"` - // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure. + // SubjectTokenFieldName is only required for JSON format. This is the field name that the credentials will check + // for the subject token in the file or URL response. This would be "access_token" for azure. SubjectTokenFieldName string `json:"subject_token_field_name"` } // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange. -// One field amongst File, URL, Executable should be filled, depending on the kind of credential in question. +// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. // The EnvironmentID should start with AWS if being used for an AWS credential. type CredentialSource struct { // File is the location for file sourced credentials. @@ -301,7 +304,9 @@ type ExecutableConfig struct { // SubjectTokenSupplier can be used to supply a subject token to exchange for a GCP access token. type SubjectTokenSupplier interface { - // AwsRegion should return a valid subject token or an error. + // SubjectToken should return a valid subject token or an error. + // The external account token source does not cache the returned subject token, so caching + // logic should be implemented in the supplier to prevent multiple requests for the same subject token. SubjectToken(ctx context.Context, options SupplierOptions) (string, error) } @@ -311,6 +316,8 @@ type AwsSecurityCredentialsSupplier interface { // AwsRegion should return the AWS region or an error. AwsRegion(ctx context.Context, options SupplierOptions) (string, error) // GetAwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error. + // The external account token source does not cache the returned security credentials, so caching + // logic should be implemented in the supplier to prevent multiple requests for the same security credentials. AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error) } From 80c14f3540bfcf8d4ee2134b088ba4ceb0d2a626 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 22 Feb 2024 11:54:31 -0800 Subject: [PATCH 19/22] Docs for session token --- google/externalaccount/aws.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/externalaccount/aws.go b/google/externalaccount/aws.go index 68f7326b1..1f1044fe0 100644 --- a/google/externalaccount/aws.go +++ b/google/externalaccount/aws.go @@ -32,7 +32,7 @@ type AwsSecurityCredentials struct { AccessKeyID string `json:"AccessKeyID"` // SecretAccessKey is the AWS Secret Access Key - Required. SecretAccessKey string `json:"SecretAccessKey"` - // SessionToken is the AWS Session token - Optional. + // SessionToken is the AWS Session token. This should be provided for temporary AWS security credentials - Optional. SessionToken string `json:"Token"` } From 353e887788db76bb2cd21717be27ba378401b16c Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 22 Feb 2024 14:19:51 -0800 Subject: [PATCH 20/22] Respond to comments --- google/externalaccount/aws.go | 16 ++++---- google/externalaccount/aws_test.go | 16 ++++---- google/externalaccount/basecredentials.go | 38 ++++++++++--------- .../externalaccount/basecredentials_test.go | 2 +- .../externalaccount/executablecredsource.go | 24 ++++++------ google/externalaccount/filecredsource.go | 12 +++--- google/externalaccount/urlcredsource.go | 16 ++++---- 7 files changed, 63 insertions(+), 61 deletions(-) diff --git a/google/externalaccount/aws.go b/google/externalaccount/aws.go index 1f1044fe0..da61d0c0e 100644 --- a/google/externalaccount/aws.go +++ b/google/externalaccount/aws.go @@ -425,7 +425,7 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) { } if resp.StatusCode != 200 { - return "", fmt.Errorf("oauth2/google: unable to retrieve AWS session token - %s", string(respBody)) + return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS session token - %s", string(respBody)) } return string(respBody), nil @@ -444,7 +444,7 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err } if cs.regionURL == "" { - return "", errors.New("oauth2/google: unable to determine AWS region") + return "", errors.New("oauth2/google/externalaccount: unable to determine AWS region") } req, err := http.NewRequest("GET", cs.regionURL, nil) @@ -468,7 +468,7 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err } if resp.StatusCode != 200 { - return "", fmt.Errorf("oauth2/google: unable to retrieve AWS region - %s", string(respBody)) + return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS region - %s", string(respBody)) } // This endpoint will return the region in format: us-east-2b. @@ -503,11 +503,11 @@ func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) } if credentials.AccessKeyID == "" { - return result, errors.New("oauth2/google: missing AccessKeyId credential") + return result, errors.New("oauth2/google/externalaccount: missing AccessKeyId credential") } if credentials.SecretAccessKey == "" { - return result, errors.New("oauth2/google: missing SecretAccessKey credential") + return result, errors.New("oauth2/google/externalaccount: missing SecretAccessKey credential") } return &credentials, nil @@ -538,7 +538,7 @@ func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, h } if resp.StatusCode != 200 { - return result, fmt.Errorf("oauth2/google: unable to retrieve AWS security credentials - %s", string(respBody)) + return result, fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS security credentials - %s", string(respBody)) } err = json.Unmarshal(respBody, &result) @@ -547,7 +547,7 @@ func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, h func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (string, error) { if cs.credVerificationURL == "" { - return "", errors.New("oauth2/google: unable to determine the AWS metadata server security credentials endpoint") + return "", errors.New("oauth2/google/externalaccount: unable to determine the AWS metadata server security credentials endpoint") } req, err := http.NewRequest("GET", cs.credVerificationURL, nil) @@ -571,7 +571,7 @@ func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (s } if resp.StatusCode != 200 { - return "", fmt.Errorf("oauth2/google: unable to retrieve AWS role name - %s", string(respBody)) + return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS role name - %s", string(respBody)) } return string(respBody), nil diff --git a/google/externalaccount/aws_test.go b/google/externalaccount/aws_test.go index 8edc26c43..4a2261bd8 100644 --- a/google/externalaccount/aws_test.go +++ b/google/externalaccount/aws_test.go @@ -846,7 +846,7 @@ func TestAWSCredential_RequestWithBadVersion(t *testing.T) { if err == nil { t.Fatalf("parse() should have failed") } - if got, want := err.Error(), "oauth2/google: aws version '3' is not supported in the current build"; !reflect.DeepEqual(got, want) { + if got, want := err.Error(), "oauth2/google/externalaccount: aws version '3' is not supported in the current build"; !reflect.DeepEqual(got, want) { t.Errorf("subjectToken = %q, want %q", got, want) } } @@ -875,7 +875,7 @@ func TestAWSCredential_RequestWithNoRegionURL(t *testing.T) { t.Fatalf("retrieveSubjectToken() should have failed") } - if got, want := err.Error(), "oauth2/google: unable to determine AWS region"; !reflect.DeepEqual(got, want) { + if got, want := err.Error(), "oauth2/google/externalaccount: unable to determine AWS region"; !reflect.DeepEqual(got, want) { t.Errorf("subjectToken = %q, want %q", got, want) } } @@ -905,7 +905,7 @@ func TestAWSCredential_RequestWithBadRegionURL(t *testing.T) { t.Fatalf("retrieveSubjectToken() should have failed") } - if got, want := err.Error(), "oauth2/google: unable to retrieve AWS region - Not Found"; !reflect.DeepEqual(got, want) { + if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS region - Not Found"; !reflect.DeepEqual(got, want) { t.Errorf("subjectToken = %q, want %q", got, want) } } @@ -937,7 +937,7 @@ func TestAWSCredential_RequestWithMissingCredential(t *testing.T) { t.Fatalf("retrieveSubjectToken() should have failed") } - if got, want := err.Error(), "oauth2/google: missing AccessKeyId credential"; !reflect.DeepEqual(got, want) { + if got, want := err.Error(), "oauth2/google/externalaccount: missing AccessKeyId credential"; !reflect.DeepEqual(got, want) { t.Errorf("subjectToken = %q, want %q", got, want) } } @@ -969,7 +969,7 @@ func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) { t.Fatalf("retrieveSubjectToken() should have failed") } - if got, want := err.Error(), "oauth2/google: missing SecretAccessKey credential"; !reflect.DeepEqual(got, want) { + if got, want := err.Error(), "oauth2/google/externalaccount: missing SecretAccessKey credential"; !reflect.DeepEqual(got, want) { t.Errorf("subjectToken = %q, want %q", got, want) } } @@ -998,7 +998,7 @@ func TestAWSCredential_RequestWithNoCredentialURL(t *testing.T) { t.Fatalf("retrieveSubjectToken() should have failed") } - if got, want := err.Error(), "oauth2/google: unable to determine the AWS metadata server security credentials endpoint"; !reflect.DeepEqual(got, want) { + if got, want := err.Error(), "oauth2/google/externalaccount: unable to determine the AWS metadata server security credentials endpoint"; !reflect.DeepEqual(got, want) { t.Errorf("subjectToken = %q, want %q", got, want) } } @@ -1027,7 +1027,7 @@ func TestAWSCredential_RequestWithBadCredentialURL(t *testing.T) { t.Fatalf("retrieveSubjectToken() should have failed") } - if got, want := err.Error(), "oauth2/google: unable to retrieve AWS role name - Not Found"; !reflect.DeepEqual(got, want) { + if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS role name - Not Found"; !reflect.DeepEqual(got, want) { t.Errorf("subjectToken = %q, want %q", got, want) } } @@ -1056,7 +1056,7 @@ func TestAWSCredential_RequestWithBadFinalCredentialURL(t *testing.T) { t.Fatalf("retrieveSubjectToken() should have failed") } - if got, want := err.Error(), "oauth2/google: unable to retrieve AWS security credentials - Not Found"; !reflect.DeepEqual(got, want) { + if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS security credentials - Not Found"; !reflect.DeepEqual(got, want) { t.Errorf("subjectToken = %q, want %q", got, want) } } diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 6c86a90bf..a000301cd 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -46,7 +46,7 @@ https://cloud.google.com/iam/docs/workload-identity-federation-with-other-provid To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers, or one that implements [AwsSecurityCredentialsSupplier] for AWS providers. This can then be used when building a [Config]. The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google -Cloud resources. For instance, you can create a NewClient from thes +Cloud resources. For instance, you can create a new client from the [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) Note that this library does not perform any validation on the token_url, token_info_url, @@ -153,7 +153,7 @@ type Config struct { ServiceAccountImpersonationLifetimeSeconds int // ClientSecret is currently only required if token_info endpoint also // needs to be called with the generated GCP access token. When provided, STS will be - // called with additional basic authentication using client_id as username and client_secret as password. Optional. + // called with additional basic authentication using ClientId as username and ClientSecret as password. Optional. ClientSecret string // ClientID is only required in conjunction with ClientSecret, as described above. Optional. ClientID string @@ -162,7 +162,7 @@ type Config struct { // CredentialSource must be provided. Optional. CredentialSource *CredentialSource // QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries - // will set the x-goog-user-project which overrides the project associated with the credentials. Optional. + // will set the x-goog-user-project header which overrides the project associated with the credentials. Optional. QuotaProjectID string // Scopes contains the desired scopes for the returned access token. Optional. Scopes []string @@ -190,15 +190,15 @@ func validateWorkforceAudience(input string) bool { // NewTokenSource Returns an external account TokenSource using the provided external account config. func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error) { if conf.Audience == "" { - return nil, fmt.Errorf("oauth2/google: Audience must be set") + return nil, fmt.Errorf("oauth2/google/externalaccount: Audience must be set") } if conf.SubjectTokenType == "" { - return nil, fmt.Errorf("oauth2/google: Subject token type must be set") + return nil, fmt.Errorf("oauth2/google/externalaccount: Subject token type must be set") } if conf.WorkforcePoolUserProject != "" { valid := validateWorkforceAudience(conf.Audience) if !valid { - return nil, fmt.Errorf("oauth2/google: Workforce pool user project should not be set for non-workforce pool credentials") + return nil, fmt.Errorf("oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials") } } count := 0 @@ -212,10 +212,10 @@ func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error count++ } if count == 0 { - return nil, fmt.Errorf("oauth2/google: One of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set") + return nil, fmt.Errorf("oauth2/google/externalaccount: One of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set") } if count > 1 { - return nil, fmt.Errorf("oauth2/google: Only one of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set") + return nil, fmt.Errorf("oauth2/google/externalaccount: Only one of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set") } return conf.tokenSource(ctx, "https") } @@ -263,21 +263,23 @@ type Format struct { } // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange. -// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. -// The EnvironmentID should start with AWS if being used for an AWS credential. type CredentialSource struct { // File is the location for file sourced credentials. + // One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. File string `json:"file"` // Url is the URL to call for URL sourced credentials. + // One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. URL string `json:"url"` - // Headers are the Headers to attach to the request for URL sourced credentials. + // Headers are the headers to attach to the request for URL sourced credentials. Headers map[string]string `json:"headers"` // Executable is the configuration object for executable sourced credentials. + // One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. Executable *ExecutableConfig `json:"executable"` - // EnvironmentID is the EnvironmentID used for AWS sourced credentials. + // EnvironmentID is the EnvironmentID used for AWS sourced credentials. This should start with "AWS". + // One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question. EnvironmentID string `json:"environment_id"` // RegionURL is the metadata URL to retrieve the region from for EC2 AWS credentials. RegionURL string `json:"region_url"` @@ -295,7 +297,7 @@ type ExecutableConfig struct { // Command is the the full command to run to retrieve the subject token. // This can include arguments. Must be an absolute path for the program. Required. Command string `json:"command"` - // TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30 seconds when not provided. Optional. + // TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30000 milliseconds when not provided. Optional. TimeoutMillis *int `json:"timeout_millis"` // OutputFile is the absolute path to the output file where the executable will cache the response. // If specified the auth libraries will first check this location before running the executable. Optional. @@ -310,7 +312,7 @@ type SubjectTokenSupplier interface { SubjectToken(ctx context.Context, options SupplierOptions) (string, error) } -// AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an Aws Region to +// AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an AWS Region to // exchange for a GCP access token. type AwsSecurityCredentialsSupplier interface { // AwsRegion should return the AWS region or an error. @@ -321,7 +323,7 @@ type AwsSecurityCredentialsSupplier interface { AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error) } -// SupplierOptions contains information about the requested subject token or Aws credentials from the +// SupplierOptions contains information about the requested subject token or AWS security credentials from the // Google external account credential. type SupplierOptions struct { // Audience is the requested audience for the external account credential. @@ -355,7 +357,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { } else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { if awsVersion != 1 { - return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion) + return nil, fmt.Errorf("oauth2/google/externalaccount: aws version '%d' is not supported in the current build", awsVersion) } awsCredSource := awsCredentialSource{ @@ -379,7 +381,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { } else if c.CredentialSource.Executable != nil { return createExecutableCredential(ctx, c.CredentialSource.Executable, c) } - return nil, fmt.Errorf("oauth2/google: unable to parse credential source") + return nil, fmt.Errorf("oauth2/google/externalaccount: unable to parse credential source") } type baseCredentialSource interface { @@ -449,7 +451,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) { TokenType: stsResp.TokenType, } if stsResp.ExpiresIn < 0 { - return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service") + return nil, fmt.Errorf("oauth2/google/externalaccount: got invalid expiry from security token service") } else if stsResp.ExpiresIn >= 0 { accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second) } diff --git a/google/externalaccount/basecredentials_test.go b/google/externalaccount/basecredentials_test.go index 26caadedf..5e896eed0 100644 --- a/google/externalaccount/basecredentials_test.go +++ b/google/externalaccount/basecredentials_test.go @@ -271,7 +271,7 @@ func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) { if err == nil { t.Fatalf("Expected error but found none") } - if got, want := err.Error(), "oauth2/google: Workforce pool user project should not be set for non-workforce pool credentials"; got != want { + if got, want := err.Error(), "oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials"; got != want { t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) } } diff --git a/google/externalaccount/executablecredsource.go b/google/externalaccount/executablecredsource.go index d30f5f8b8..dca5681a4 100644 --- a/google/externalaccount/executablecredsource.go +++ b/google/externalaccount/executablecredsource.go @@ -39,51 +39,51 @@ func (nce nonCacheableError) Error() string { } func missingFieldError(source, field string) error { - return fmt.Errorf("oauth2/google: %v missing `%q` field", source, field) + return fmt.Errorf("oauth2/google/externalaccount: %v missing `%q` field", source, field) } func jsonParsingError(source, data string) error { - return fmt.Errorf("oauth2/google: unable to parse %v\nResponse: %v", source, data) + return fmt.Errorf("oauth2/google/externalaccount: unable to parse %v\nResponse: %v", source, data) } func malformedFailureError() error { - return nonCacheableError{"oauth2/google: response must include `error` and `message` fields when unsuccessful"} + return nonCacheableError{"oauth2/google/externalaccount: response must include `error` and `message` fields when unsuccessful"} } func userDefinedError(code, message string) error { - return nonCacheableError{fmt.Sprintf("oauth2/google: response contains unsuccessful response: (%v) %v", code, message)} + return nonCacheableError{fmt.Sprintf("oauth2/google/externalaccount: response contains unsuccessful response: (%v) %v", code, message)} } func unsupportedVersionError(source string, version int) error { - return fmt.Errorf("oauth2/google: %v contains unsupported version: %v", source, version) + return fmt.Errorf("oauth2/google/externalaccount: %v contains unsupported version: %v", source, version) } func tokenExpiredError() error { - return nonCacheableError{"oauth2/google: the token returned by the executable is expired"} + return nonCacheableError{"oauth2/google/externalaccount: the token returned by the executable is expired"} } func tokenTypeError(source string) error { - return fmt.Errorf("oauth2/google: %v contains unsupported token type", source) + return fmt.Errorf("oauth2/google/externalaccount: %v contains unsupported token type", source) } func exitCodeError(exitCode int) error { - return fmt.Errorf("oauth2/google: executable command failed with exit code %v", exitCode) + return fmt.Errorf("oauth2/google/externalaccount: executable command failed with exit code %v", exitCode) } func executableError(err error) error { - return fmt.Errorf("oauth2/google: executable command failed: %v", err) + return fmt.Errorf("oauth2/google/externalaccount: executable command failed: %v", err) } func executablesDisallowedError() error { - return errors.New("oauth2/google: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run") + return errors.New("oauth2/google/externalaccount: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run") } func timeoutRangeError() error { - return errors.New("oauth2/google: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds") + return errors.New("oauth2/google/externalaccount: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds") } func commandMissingError() error { - return errors.New("oauth2/google: missing `command` field — executable command must be provided") + return errors.New("oauth2/google/externalaccount: missing `command` field — executable command must be provided") } type environment interface { diff --git a/google/externalaccount/filecredsource.go b/google/externalaccount/filecredsource.go index 49fa101d9..33766b972 100644 --- a/google/externalaccount/filecredsource.go +++ b/google/externalaccount/filecredsource.go @@ -26,12 +26,12 @@ func (cs fileCredentialSource) credentialSourceType() string { func (cs fileCredentialSource) subjectToken() (string, error) { tokenFile, err := os.Open(cs.File) if err != nil { - return "", fmt.Errorf("oauth2/google: failed to open credential file %q", cs.File) + return "", fmt.Errorf("oauth2/google/externalaccount: failed to open credential file %q", cs.File) } defer tokenFile.Close() tokenBytes, err := ioutil.ReadAll(io.LimitReader(tokenFile, 1<<20)) if err != nil { - return "", fmt.Errorf("oauth2/google: failed to read credential file: %v", err) + return "", fmt.Errorf("oauth2/google/externalaccount: failed to read credential file: %v", err) } tokenBytes = bytes.TrimSpace(tokenBytes) switch cs.Format.Type { @@ -39,15 +39,15 @@ func (cs fileCredentialSource) subjectToken() (string, error) { jsonData := make(map[string]interface{}) err = json.Unmarshal(tokenBytes, &jsonData) if err != nil { - return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err) + return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err) } val, ok := jsonData[cs.Format.SubjectTokenFieldName] if !ok { - return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials") + return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials") } token, ok := val.(string) if !ok { - return "", errors.New("oauth2/google: improperly formatted subject token") + return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token") } return token, nil case "text": @@ -55,7 +55,7 @@ func (cs fileCredentialSource) subjectToken() (string, error) { case "": return string(tokenBytes), nil default: - return "", errors.New("oauth2/google: invalid credential_source file format type") + return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type") } } diff --git a/google/externalaccount/urlcredsource.go b/google/externalaccount/urlcredsource.go index a9a30ef7f..71a7184e0 100644 --- a/google/externalaccount/urlcredsource.go +++ b/google/externalaccount/urlcredsource.go @@ -31,7 +31,7 @@ func (cs urlCredentialSource) subjectToken() (string, error) { client := oauth2.NewClient(cs.ctx, nil) req, err := http.NewRequest("GET", cs.URL, nil) if err != nil { - return "", fmt.Errorf("oauth2/google: HTTP request for URL-sourced credential failed: %v", err) + return "", fmt.Errorf("oauth2/google/externalaccount: HTTP request for URL-sourced credential failed: %v", err) } req = req.WithContext(cs.ctx) @@ -40,16 +40,16 @@ func (cs urlCredentialSource) subjectToken() (string, error) { } resp, err := client.Do(req) if err != nil { - return "", fmt.Errorf("oauth2/google: invalid response when retrieving subject token: %v", err) + return "", fmt.Errorf("oauth2/google/externalaccount: invalid response when retrieving subject token: %v", err) } defer resp.Body.Close() respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { - return "", fmt.Errorf("oauth2/google: invalid body in subject token URL query: %v", err) + return "", fmt.Errorf("oauth2/google/externalaccount: invalid body in subject token URL query: %v", err) } if c := resp.StatusCode; c < 200 || c > 299 { - return "", fmt.Errorf("oauth2/google: status code %d: %s", c, respBody) + return "", fmt.Errorf("oauth2/google/externalaccount: status code %d: %s", c, respBody) } switch cs.Format.Type { @@ -57,15 +57,15 @@ func (cs urlCredentialSource) subjectToken() (string, error) { jsonData := make(map[string]interface{}) err = json.Unmarshal(respBody, &jsonData) if err != nil { - return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err) + return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err) } val, ok := jsonData[cs.Format.SubjectTokenFieldName] if !ok { - return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials") + return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials") } token, ok := val.(string) if !ok { - return "", errors.New("oauth2/google: improperly formatted subject token") + return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token") } return token, nil case "text": @@ -73,7 +73,7 @@ func (cs urlCredentialSource) subjectToken() (string, error) { case "": return string(respBody), nil default: - return "", errors.New("oauth2/google: invalid credential_source file format type") + return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type") } } From 362176f4b08e571491fc4e32c962d720a6e53010 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 22 Feb 2024 14:21:16 -0800 Subject: [PATCH 21/22] fix typo --- google/externalaccount/basecredentials.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index a000301cd..794982bc1 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -45,7 +45,7 @@ https://cloud.google.com/iam/docs/workload-identity-federation-with-other-provid To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers, or one that implements [AwsSecurityCredentialsSupplier] for AWS providers. This can then be used when building a [Config]. -The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google +The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used to access Google Cloud resources. For instance, you can create a new client from the [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) From ac519b242f8315df572f1b205b0670f139bfc6c3 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 22 Feb 2024 14:29:46 -0800 Subject: [PATCH 22/22] fix type --- google/externalaccount/basecredentials.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go index 794982bc1..71342e42b 100644 --- a/google/externalaccount/basecredentials.go +++ b/google/externalaccount/basecredentials.go @@ -95,7 +95,7 @@ https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#ge To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers. This can then be used when building a [Config]. The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google -Cloud resources. For instance, you can create a NewClient from thes +Cloud resources. For instance, you can create a new client from the [cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource)) # Security considerations