From 407b668e1819412911fd483d9cf107f1284e998b Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 8 Dec 2023 16:17:55 -0700 Subject: [PATCH 01/19] feat(option): add universe domain support --- option/internaloption/internaloption.go | 15 +++++++++++++++ option/option.go | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/option/internaloption/internaloption.go b/option/internaloption/internaloption.go index 3fdee095c1b..1520ba3816d 100644 --- a/option/internaloption/internaloption.go +++ b/option/internaloption/internaloption.go @@ -26,6 +26,21 @@ func WithDefaultEndpoint(url string) option.ClientOption { return defaultEndpointOption(url) } +type defaultEndpointTemplateOption string + +func (o defaultEndpointTemplateOption) Apply(settings *internal.DialSettings) { + settings.DefaultEndpointTemplate = string(o) +} + +// WithDefaultEndpointTemplate provides a template for creating the endpoint using a universe domain. +// +// It should only be used internally by generated clients. +// +// TODO(chrisdsmith): Refs: CL-R3 (remove this note before publication) +func WithDefaultEndpointTemplate(url string) option.ClientOption { + return defaultEndpointTemplateOption(url) +} + type defaultMTLSEndpointOption string func (o defaultMTLSEndpointOption) Apply(settings *internal.DialSettings) { diff --git a/option/option.go b/option/option.go index c882c1eb482..1f951d137a2 100644 --- a/option/option.go +++ b/option/option.go @@ -23,6 +23,14 @@ type ClientOption interface { // WithTokenSource returns a ClientOption that specifies an OAuth2 token // source to be used as the basis for authentication. +// +// Specifying WithUniverseDomain is recommended when using this option. +// If WithUniverseDomain is not configured explicitly, no cross-checking of +// universe domain against credentials is possible. For backward compatibility +// if the end user does not configure WithUniverseDomain explicitly, the +// universe domain must be assumed to be googleapis.com. +// TODO(chrisdsmith): If possible, replace comment above with ability to set UniverseDomain on TokenSource, per CL-R8. +// TODO(chrisdsmith): Refs: CL-R8 (remove this note before publication) func WithTokenSource(s oauth2.TokenSource) ClientOption { return withTokenSource{s} } @@ -165,6 +173,13 @@ func (w withGRPCConnectionPool) Apply(o *internal.DialSettings) { // // API Keys can only be used for JSON-over-HTTP APIs, including those under // the import path google.golang.org/api/.... +// +// Specifying WithUniverseDomain is recommended when using this option. +// If WithUniverseDomain is not configured explicitly, no cross-checking of +// universe domain against credentials is possible. For backward compatibility +// if the end user does not configure WithUniverseDomain explicitly, the +// universe domain must be assumed to be googleapis.com. +// TODO(chrisdsmith): Refs: CL-R8 (remove this note before publication) func WithAPIKey(apiKey string) ClientOption { return withAPIKey(apiKey) } @@ -347,6 +362,7 @@ func WithCredentials(creds *google.Credentials) ClientOption { // WithUniverseDomain returns a ClientOption that sets the universe domain. // // This is an EXPERIMENTAL API and may be changed or removed in the future. +// TODO(chrisdsmith): Closes: CL-R7 (remove this note before publication) func WithUniverseDomain(ud string) ClientOption { return withUniverseDomain(ud) } From f850927d6770c3d8865d779cf9fd6cac89ff3476 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 8 Dec 2023 16:18:13 -0700 Subject: [PATCH 02/19] feat(internal): add universe domain support --- internal/cba.go | 21 ++++++ internal/cba_test.go | 160 +++++++++++++++++++++++++++++++++++++++++-- internal/settings.go | 18 ++++- 3 files changed, 193 insertions(+), 6 deletions(-) diff --git a/internal/cba.go b/internal/cba.go index 829383f55b5..3a30e834d56 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -35,6 +35,8 @@ package internal import ( "context" "crypto/tls" + "errors" + "fmt" "net" "net/url" "os" @@ -67,6 +69,15 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source, if err != nil { return nil, "", err } + // TODO(chrisdsmith): Closes: CL-R1 (remove this note before publication) + // TODO(chrisdsmith): Closes: CL-R3 (remove this note before publication) + // TODO(chrisdsmith): Use this composed endpoint everywhere to replace DialSettings.DefaultEndpoint + if settings.Endpoint == "" && settings.UniverseDomainNotGDU() { + endpoint = fmt.Sprintf(settings.DefaultEndpointTemplate, settings.GetUniverseDomain()) + if err != nil { + return nil, "", err + } + } return clientCertSource, endpoint, nil } @@ -94,6 +105,12 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) { if !shouldUseS2A(clientCertSource, settings) { return &defaultTransportConfig, nil } + // TODO(chrisdsmith): Closes: CL-R10 (remove this note before publication) + if settings.UniverseDomainNotGDU() { + return &transportConfig{ + clientCertSource: nil, endpoint: "", s2aAddress: "", s2aMTLSEndpoint: "", + }, errors.New("mTLS is not supported in any universe other than googleapis.com") + } s2aMTLSEndpoint := settings.DefaultMTLSEndpoint // If there is endpoint override, honor it. @@ -155,6 +172,10 @@ func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string, if settings.Endpoint == "" { mtlsMode := getMTLSMode() if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) { + // TODO(chrisdsmith): Closes: CL-R10 (remove this note before publication) + if settings.UniverseDomainNotGDU() { + return "", errors.New("mTLS is not supported in any universe other than googleapis.com") + } return settings.DefaultMTLSEndpoint, nil } return settings.DefaultEndpoint, nil diff --git a/internal/cba_test.go b/internal/cba_test.go index d6a783740e5..486674d8f31 100644 --- a/internal/cba_test.go +++ b/internal/cba_test.go @@ -13,9 +13,12 @@ import ( ) const ( - testMTLSEndpoint = "test.mtls.endpoint" - testRegularEndpoint = "test.endpoint" - testOverrideEndpoint = "test.override.endpoint" + testMTLSEndpoint = "https://test.mtls.googleapis.com/" + testRegularEndpoint = "https://test.googleapis.com/" + testEndpointTemplate = "https://test.%s/" + testOverrideEndpoint = "https://test.override.example.com/" + testUniverseDomain = "example.com" + testUniverseDomainEndpoint = "https://test.example.com/" ) var dummyClientCertSource = func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) { return nil, nil } @@ -213,7 +216,7 @@ func TestGetGRPCTransportConfigAndEndpoint(t *testing.T) { } } -func TestGetHTTPTransportConfigAndEndpoint(t *testing.T) { +func TestGetHTTPTransportConfigAndEndpoint_s2a(t *testing.T) { testCases := []struct { Desc string InputSettings *DialSettings @@ -325,7 +328,10 @@ func TestGetHTTPTransportConfigAndEndpoint(t *testing.T) { } else { os.Setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") } - _, dialFunc, endpoint, _ := GetHTTPTransportConfigAndEndpoint(tc.InputSettings) + _, dialFunc, endpoint, err := GetHTTPTransportConfigAndEndpoint(tc.InputSettings) + if err != nil { + t.Fatalf("%s: err: %v", tc.Desc, err) + } if tc.WantEndpoint != endpoint { t.Errorf("%s: want endpoint: [%s], got [%s]", tc.Desc, tc.WantEndpoint, endpoint) } @@ -355,3 +361,147 @@ func setupTest() func() { os.Setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", oldUseClientCert) } } + +func TestGetHTTPTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { + testCases := []struct { + name string + ds *DialSettings + wantEndpoint string + wantErr string + }{ + { + name: "google default universe (GDU), no client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + }, + wantEndpoint: testRegularEndpoint, + wantErr: "", + }, + { + name: "google default universe (GDU), client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + ClientCertSource: dummyClientCertSource, + }, + wantEndpoint: testMTLSEndpoint, + wantErr: "", + }, + { + name: "UniverseDomain, no client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + }, + wantEndpoint: testUniverseDomainEndpoint, + wantErr: "", + }, + { + name: "UniverseDomain, client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + ClientCertSource: dummyClientCertSource, + }, + wantEndpoint: testUniverseDomainEndpoint, + wantErr: "mTLS is not supported in any universe other than googleapis.com", + }, + } + + for _, tc := range testCases { + if tc.ds.ClientCertSource != nil { + os.Setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true") + } else { + os.Setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + } + _, _, endpoint, err := GetHTTPTransportConfigAndEndpoint(tc.ds) + if err != nil { + if err.Error() != tc.wantErr { + t.Fatalf("%s: err: %v", tc.name, err) + } + } else { + if tc.wantEndpoint != endpoint { + t.Errorf("%s: want endpoint: [%s], got [%s]", tc.name, tc.wantEndpoint, endpoint) + } + } + } +} + +func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { + testCases := []struct { + name string + ds *DialSettings + wantEndpoint string + wantErr string + }{ + { + name: "google default universe (GDU), no client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + }, + wantEndpoint: testRegularEndpoint, + wantErr: "", + }, + { + name: "google default universe (GDU), client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + ClientCertSource: dummyClientCertSource, + }, + wantEndpoint: testMTLSEndpoint, + wantErr: "", + }, + { + name: "UniverseDomain, no client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + }, + wantEndpoint: testUniverseDomainEndpoint, + wantErr: "", + }, + { + name: "UniverseDomain, client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + ClientCertSource: dummyClientCertSource, + }, + wantEndpoint: testUniverseDomainEndpoint, + wantErr: "mTLS is not supported in any universe other than googleapis.com", + }, + } + + for _, tc := range testCases { + if tc.ds.ClientCertSource != nil { + os.Setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true") + } else { + os.Setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + } + _, endpoint, err := GetGRPCTransportConfigAndEndpoint(tc.ds) + if err != nil { + if err.Error() != tc.wantErr { + t.Fatalf("%s: err: %v", tc.name, err) + } + } else { + if tc.wantEndpoint != endpoint { + t.Errorf("%s: want endpoint: [%s], got [%s]", tc.name, tc.wantEndpoint, endpoint) + } + } + } +} diff --git a/internal/settings.go b/internal/settings.go index 3356fa97b8f..a6838415313 100644 --- a/internal/settings.go +++ b/internal/settings.go @@ -19,7 +19,8 @@ import ( ) const ( - newAuthLibEnVar = "GOOGLE_API_GO_EXPERIMENTAL_USE_NEW_AUTH_LIB" + newAuthLibEnVar = "GOOGLE_API_GO_EXPERIMENTAL_USE_NEW_AUTH_LIB" + universeDomainDefault = "googleapis.com" ) // DialSettings holds information needed to establish a connection with a @@ -27,6 +28,7 @@ const ( type DialSettings struct { Endpoint string DefaultEndpoint string + DefaultEndpointTemplate string DefaultMTLSEndpoint string Scopes []string DefaultScopes []string @@ -160,3 +162,17 @@ func (ds *DialSettings) Validate() error { } return nil } + +// UniverseDomain returns the default service domain for a given Cloud universe. +// The default value is "googleapis.com". +// TODO(chrisdsmith): Refs: CL-R5.1 (remove this note before publication) +func (ds *DialSettings) GetUniverseDomain() string { + if ds.UniverseDomain == "" { + return universeDomainDefault + } + return ds.UniverseDomain +} + +func (ds *DialSettings) UniverseDomainNotGDU() bool { + return ds.GetUniverseDomain() != universeDomainDefault +} From 9e47d5ff6f799ad6921c7e9e409756f6e078e57b Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 8 Dec 2023 16:18:28 -0700 Subject: [PATCH 03/19] feat(transport): add universe domain support --- transport/http/dial.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/transport/http/dial.go b/transport/http/dial.go index 7e322a17c68..cad3b5d9264 100644 --- a/transport/http/dial.go +++ b/transport/http/dial.go @@ -11,6 +11,7 @@ import ( "context" "crypto/tls" "errors" + "fmt" "net" "net/http" "time" @@ -88,6 +89,11 @@ func newTransport(ctx context.Context, base http.RoundTripper, settings *interna if err != nil { return nil, err } + // TODO(chrisdsmith): Closes: CL-R5 for HTTP (remove this note before publication) + // TODO(chrisdsmith): Closes: CL-R5.2 (remove this note before publication) + if settings.GetUniverseDomain() != creds.UniverseDomain() { + return nil, fmt.Errorf("the configured universe domain (%s) does not match the universe domain found in the credentials (%s). If you haven't configured WithUniverseDomain explicitly, googleapis.com is the default", settings.GetUniverseDomain(), creds.UniverseDomain()) + } paramTransport.quotaProject = internal.GetQuotaProject(creds, settings.QuotaProject) ts := creds.TokenSource if settings.ImpersonationConfig == nil && settings.TokenSource != nil { From f327e1df694252ecbf9f77943c38d6cd261cf7f5 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 18 Dec 2023 14:18:09 -0700 Subject: [PATCH 04/19] add ErrUniverseNotSupportedMTLS to cba.go --- internal/cba.go | 8 ++++++-- internal/cba_test.go | 18 ++++++------------ option/option.go | 1 - transport/http/dial.go | 11 ++++++++++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/internal/cba.go b/internal/cba.go index 3a30e834d56..e541ede4860 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -57,6 +57,10 @@ const ( googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A" ) +var ( + ErrUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com") +) + // getClientCertificateSourceAndEndpoint is a convenience function that invokes // getClientCertificateSource and getEndpoint sequentially and returns the client // cert source and endpoint as a tuple. @@ -109,7 +113,7 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) { if settings.UniverseDomainNotGDU() { return &transportConfig{ clientCertSource: nil, endpoint: "", s2aAddress: "", s2aMTLSEndpoint: "", - }, errors.New("mTLS is not supported in any universe other than googleapis.com") + }, ErrUniverseNotSupportedMTLS } s2aMTLSEndpoint := settings.DefaultMTLSEndpoint @@ -174,7 +178,7 @@ func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string, if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) { // TODO(chrisdsmith): Closes: CL-R10 (remove this note before publication) if settings.UniverseDomainNotGDU() { - return "", errors.New("mTLS is not supported in any universe other than googleapis.com") + return "", ErrUniverseNotSupportedMTLS } return settings.DefaultMTLSEndpoint, nil } diff --git a/internal/cba_test.go b/internal/cba_test.go index 486674d8f31..453099def83 100644 --- a/internal/cba_test.go +++ b/internal/cba_test.go @@ -367,7 +367,7 @@ func TestGetHTTPTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { name string ds *DialSettings wantEndpoint string - wantErr string + wantErr error }{ { name: "google default universe (GDU), no client cert", @@ -377,7 +377,6 @@ func TestGetHTTPTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { DefaultMTLSEndpoint: testMTLSEndpoint, }, wantEndpoint: testRegularEndpoint, - wantErr: "", }, { name: "google default universe (GDU), client cert", @@ -388,7 +387,6 @@ func TestGetHTTPTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { ClientCertSource: dummyClientCertSource, }, wantEndpoint: testMTLSEndpoint, - wantErr: "", }, { name: "UniverseDomain, no client cert", @@ -399,7 +397,6 @@ func TestGetHTTPTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { UniverseDomain: testUniverseDomain, }, wantEndpoint: testUniverseDomainEndpoint, - wantErr: "", }, { name: "UniverseDomain, client cert", @@ -411,7 +408,7 @@ func TestGetHTTPTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { ClientCertSource: dummyClientCertSource, }, wantEndpoint: testUniverseDomainEndpoint, - wantErr: "mTLS is not supported in any universe other than googleapis.com", + wantErr: ErrUniverseNotSupportedMTLS, }, } @@ -423,7 +420,7 @@ func TestGetHTTPTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { } _, _, endpoint, err := GetHTTPTransportConfigAndEndpoint(tc.ds) if err != nil { - if err.Error() != tc.wantErr { + if err != tc.wantErr { t.Fatalf("%s: err: %v", tc.name, err) } } else { @@ -439,7 +436,7 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { name string ds *DialSettings wantEndpoint string - wantErr string + wantErr error }{ { name: "google default universe (GDU), no client cert", @@ -449,7 +446,6 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { DefaultMTLSEndpoint: testMTLSEndpoint, }, wantEndpoint: testRegularEndpoint, - wantErr: "", }, { name: "google default universe (GDU), client cert", @@ -460,7 +456,6 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { ClientCertSource: dummyClientCertSource, }, wantEndpoint: testMTLSEndpoint, - wantErr: "", }, { name: "UniverseDomain, no client cert", @@ -471,7 +466,6 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { UniverseDomain: testUniverseDomain, }, wantEndpoint: testUniverseDomainEndpoint, - wantErr: "", }, { name: "UniverseDomain, client cert", @@ -483,7 +477,7 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { ClientCertSource: dummyClientCertSource, }, wantEndpoint: testUniverseDomainEndpoint, - wantErr: "mTLS is not supported in any universe other than googleapis.com", + wantErr: ErrUniverseNotSupportedMTLS, }, } @@ -495,7 +489,7 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { } _, endpoint, err := GetGRPCTransportConfigAndEndpoint(tc.ds) if err != nil { - if err.Error() != tc.wantErr { + if err != tc.wantErr { t.Fatalf("%s: err: %v", tc.name, err) } } else { diff --git a/option/option.go b/option/option.go index 1f951d137a2..1251a613081 100644 --- a/option/option.go +++ b/option/option.go @@ -29,7 +29,6 @@ type ClientOption interface { // universe domain against credentials is possible. For backward compatibility // if the end user does not configure WithUniverseDomain explicitly, the // universe domain must be assumed to be googleapis.com. -// TODO(chrisdsmith): If possible, replace comment above with ability to set UniverseDomain on TokenSource, per CL-R8. // TODO(chrisdsmith): Refs: CL-R8 (remove this note before publication) func WithTokenSource(s oauth2.TokenSource) ClientOption { return withTokenSource{s} diff --git a/transport/http/dial.go b/transport/http/dial.go index cad3b5d9264..e2eb69f7a66 100644 --- a/transport/http/dial.go +++ b/transport/http/dial.go @@ -92,7 +92,7 @@ func newTransport(ctx context.Context, base http.RoundTripper, settings *interna // TODO(chrisdsmith): Closes: CL-R5 for HTTP (remove this note before publication) // TODO(chrisdsmith): Closes: CL-R5.2 (remove this note before publication) if settings.GetUniverseDomain() != creds.UniverseDomain() { - return nil, fmt.Errorf("the configured universe domain (%s) does not match the universe domain found in the credentials (%s). If you haven't configured WithUniverseDomain explicitly, googleapis.com is the default", settings.GetUniverseDomain(), creds.UniverseDomain()) + return nil, errUniverseNotMatch(settings.GetUniverseDomain(), creds.UniverseDomain()) } paramTransport.quotaProject = internal.GetQuotaProject(creds, settings.QuotaProject) ts := creds.TokenSource @@ -107,6 +107,15 @@ func newTransport(ctx context.Context, base http.RoundTripper, settings *interna return trans, nil } +func errUniverseNotMatch(settingsUD, credsUD string) error { + return fmt.Errorf( + "the configured universe domain (%s) does not match the universe "+ + "domain found in the credentials (%s). If you haven't configured "+ + "WithUniverseDomain explicitly, googleapis.com is the default", + settingsUD, + credsUD) +} + func newSettings(opts []option.ClientOption) (*internal.DialSettings, error) { var o internal.DialSettings for _, opt := range opts { From 32c2fe5e2e44f1f8f6fc3e1b894892cf661dba4c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 18 Dec 2023 14:37:11 -0700 Subject: [PATCH 05/19] remove CL-R8 descriptions from option.go, pending decision on CL-R8 --- option/option.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/option/option.go b/option/option.go index 1251a613081..67df7bf6173 100644 --- a/option/option.go +++ b/option/option.go @@ -24,11 +24,6 @@ type ClientOption interface { // WithTokenSource returns a ClientOption that specifies an OAuth2 token // source to be used as the basis for authentication. // -// Specifying WithUniverseDomain is recommended when using this option. -// If WithUniverseDomain is not configured explicitly, no cross-checking of -// universe domain against credentials is possible. For backward compatibility -// if the end user does not configure WithUniverseDomain explicitly, the -// universe domain must be assumed to be googleapis.com. // TODO(chrisdsmith): Refs: CL-R8 (remove this note before publication) func WithTokenSource(s oauth2.TokenSource) ClientOption { return withTokenSource{s} @@ -173,11 +168,6 @@ func (w withGRPCConnectionPool) Apply(o *internal.DialSettings) { // API Keys can only be used for JSON-over-HTTP APIs, including those under // the import path google.golang.org/api/.... // -// Specifying WithUniverseDomain is recommended when using this option. -// If WithUniverseDomain is not configured explicitly, no cross-checking of -// universe domain against credentials is possible. For backward compatibility -// if the end user does not configure WithUniverseDomain explicitly, the -// universe domain must be assumed to be googleapis.com. // TODO(chrisdsmith): Refs: CL-R8 (remove this note before publication) func WithAPIKey(apiKey string) ClientOption { return withAPIKey(apiKey) From f649422d1ba55dd05b40587c464aebce20242051 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 21 Dec 2023 10:03:53 -0700 Subject: [PATCH 06/19] feat(impersonate) add universe domain support * Raise error from user (DWD) auth flow if non-GDU universe domain --- impersonate/impersonate.go | 33 +++++++++++++++++++++++++++++---- impersonate/user.go | 2 ++ impersonate/user_test.go | 26 ++++++++++++++++++++------ 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/impersonate/impersonate.go b/impersonate/impersonate.go index 86d5eb82d58..f46a4227a28 100644 --- a/impersonate/impersonate.go +++ b/impersonate/impersonate.go @@ -8,20 +8,25 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" "time" "golang.org/x/oauth2" + "google.golang.org/api/internal" "google.golang.org/api/option" "google.golang.org/api/option/internaloption" htransport "google.golang.org/api/transport/http" ) var ( - iamCredentailsEndpoint = "https://iamcredentials.googleapis.com" - oauth2Endpoint = "https://oauth2.googleapis.com" + iamCredentailsEndpoint = "https://iamcredentials.googleapis.com" + oauth2Endpoint = "https://oauth2.googleapis.com" + ErrUniverseNotSupportedDomainWideDelegation = errors.New( + "impersonate: service account user is configured for the credential. Domain-wide " + + "delegation is not supported in universes other than googleapis.com") ) // CredentialsConfig for generating impersonated credentials. @@ -86,9 +91,17 @@ func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts if err != nil { return nil, err } - // If a subject is specified a different auth-flow is initiated to - // impersonate as the provided subject (user). + // If a subject is specified a domain-wide delegation auth-flow is initiated + // to impersonate as the provided subject (user). if config.Subject != "" { + settings, err := newSettings(clientOpts) + if err != nil { + return nil, err + } + // TODO(chrisdsmith): Closes: AL-9 (remove this note before publication) + if settings.UniverseDomainNotGDU() { + return nil, ErrUniverseNotSupportedDomainWideDelegation + } return user(ctx, config, client, lifetime, isStaticToken) } @@ -113,6 +126,18 @@ func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts return oauth2.ReuseTokenSource(nil, its), nil } +func newSettings(opts []option.ClientOption) (*internal.DialSettings, error) { + var o internal.DialSettings + for _, opt := range opts { + opt.Apply(&o) + } + if err := o.Validate(); err != nil { + return nil, err + } + + return &o, nil +} + func formatIAMServiceAccountName(name string) string { return fmt.Sprintf("projects/-/serviceAccounts/%s", name) } diff --git a/impersonate/user.go b/impersonate/user.go index c234abb8998..01bb42cc1af 100644 --- a/impersonate/user.go +++ b/impersonate/user.go @@ -18,6 +18,8 @@ import ( "golang.org/x/oauth2" ) +// user provides an auth flow for domain-wide delegation, setting +// CredentialsConfig.Subject to be the the impersonated user. func user(ctx context.Context, c CredentialsConfig, client *http.Client, lifetime time.Duration, isStaticToken bool) (oauth2.TokenSource, error) { u := userTokenSource{ client: client, diff --git a/impersonate/user_test.go b/impersonate/user_test.go index 54ec3068d3a..e4d84533e48 100644 --- a/impersonate/user_test.go +++ b/impersonate/user_test.go @@ -26,6 +26,7 @@ func TestTokenSource_user(t *testing.T) { lifetime time.Duration subject string wantErr bool + universeDomain string }{ { name: "missing targetPrincipal", @@ -50,6 +51,16 @@ func TestTokenSource_user(t *testing.T) { subject: "admin@example.com", wantErr: false, }, + { + name: "universeDomain", + targetPrincipal: "foo@project-id.iam.gserviceaccount.com", + scopes: []string{"scope"}, + subject: "admin@example.com", + wantErr: true, + // Non-GDU Universe Domain should result in error if + // CredentialsConfig.Subject is present for domain-wide delegation. + universeDomain: "example.com", + }, } for _, tt := range tests { @@ -92,12 +103,15 @@ func TestTokenSource_user(t *testing.T) { return nil }), } - ts, err := CredentialsTokenSource(ctx, CredentialsConfig{ - TargetPrincipal: tt.targetPrincipal, - Scopes: tt.scopes, - Lifetime: tt.lifetime, - Subject: tt.subject, - }, option.WithHTTPClient(client)) + ts, err := CredentialsTokenSource(ctx, + CredentialsConfig{ + TargetPrincipal: tt.targetPrincipal, + Scopes: tt.scopes, + Lifetime: tt.lifetime, + Subject: tt.subject, + }, + option.WithHTTPClient(client), + option.WithUniverseDomain(tt.universeDomain)) if tt.wantErr && err != nil { return } From 6f9cf15e90f50964de06d7d5a350b14e4dff5f6d Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 21 Dec 2023 12:42:22 -0700 Subject: [PATCH 07/19] feat(option) add universe domain support * Enforce self-signed JWT auth flow if non-GDU universe domain --- internal/creds.go | 6 ++ internal/creds_test.go | 75 +++++++++++++++++++++++++ option/internaloption/internaloption.go | 4 ++ 3 files changed, 85 insertions(+) diff --git a/internal/creds.go b/internal/creds.go index 05165f333b0..c74b99a3f7a 100644 --- a/internal/creds.go +++ b/internal/creds.go @@ -125,6 +125,12 @@ func credentialsFromJSON(ctx context.Context, data []byte, ds *DialSettings) (*g } func isSelfSignedJWTFlow(data []byte, ds *DialSettings) (bool, error) { + // For non-GDU universe domains, token exchange is impossible and services + // must support self-signed JWTs with scopes. + // TODO(chrisdsmith): closes: AL-8 (remove this note before publication) + if ds.UniverseDomainNotGDU() { + return true, nil + } if (ds.EnableJwtWithScope || ds.HasCustomAudience()) && ds.ImpersonationConfig == nil { // Check if JSON is a service account and if so create a self-signed JWT. diff --git a/internal/creds_test.go b/internal/creds_test.go index bcf623f6765..40208375ecf 100644 --- a/internal/creds_test.go +++ b/internal/creds_test.go @@ -120,6 +120,34 @@ func TestJWTWithScope(t *testing.T) { } } +func TestJWTWithScopeAndUniverseDomain(t *testing.T) { + ctx := context.Background() + + // Load a valid JSON file. No way to really test the contents; we just + // verify that there is no error. + ds := &DialSettings{ + CredentialsFile: "testdata/service-account.json", + Scopes: []string{"foo"}, + EnableJwtWithScope: true, + UniverseDomain: "example.com", + } + if _, err := Creds(ctx, ds); err != nil { + t.Errorf("got %v, wanted no error", err) + } + + // Load valid JSON. No way to really test the contents; we just + // verify that there is no error. + ds = &DialSettings{ + CredentialsJSON: []byte(validServiceAccountJSON), + Scopes: []string{"foo"}, + EnableJwtWithScope: true, + UniverseDomain: "example.com", + } + if _, err := Creds(ctx, ds); err != nil { + t.Errorf("got %v, wanted no error", err) + } +} + func TestJWTWithDefaultScopes(t *testing.T) { ctx := context.Background() @@ -337,3 +365,50 @@ func TestCredsWithCredentials(t *testing.T) { }) } } + +func TestIsSelfSignedJWTFlow(t *testing.T) { + tests := []struct { + name string + ds *DialSettings + want bool + }{ + { + name: "EnableJwtWithScope true", + ds: &DialSettings{ + CredentialsFile: "testdata/service-account.json", + Scopes: []string{"foo"}, + EnableJwtWithScope: true, + }, + want: true, + }, + { + name: "EnableJwtWithScope false", + ds: &DialSettings{ + CredentialsFile: "testdata/service-account.json", + Scopes: []string{"foo"}, + EnableJwtWithScope: false, + }, + want: false, + }, + { + name: "UniverseDomain", + ds: &DialSettings{ + CredentialsFile: "testdata/service-account.json", + Scopes: []string{"foo"}, + EnableJwtWithScope: false, + UniverseDomain: "example.com", + }, + want: true, + }, + } + + for _, tc := range tests { + isSSJ, err := isSelfSignedJWTFlow([]byte(validServiceAccountJSON), tc.ds) + if err != nil { + t.Errorf("[%s]: got %v, wanted no error", tc.name, err) + } + if isSSJ != tc.want { + t.Errorf("[%s]: got %t, wanted %t", tc.name, isSSJ, tc.want) + } + } +} diff --git a/option/internaloption/internaloption.go b/option/internaloption/internaloption.go index 2a9cb14ffd5..95cad64a1ca 100644 --- a/option/internaloption/internaloption.go +++ b/option/internaloption/internaloption.go @@ -165,6 +165,10 @@ func (w withDefaultUniverseDomain) Apply(o *internal.DialSettings) { // EnableJwtWithScope returns a ClientOption that specifies if scope can be used // with self-signed JWT. +// +// EnableJwtWithScope is ignored when option.WithUniverseDomain is set +// to a non-GDU universe domain. For non-GDU universe domains, token exchange is +// impossible and services must support self-signed JWTs with scopes. func EnableJwtWithScope() option.ClientOption { return enableJwtWithScope(true) } From e453234d62dcd22af78389f28b8dbcdc1770a044 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 9 Jan 2024 16:50:06 -0700 Subject: [PATCH 08/19] use universe domain placeholder UNIVERSE_DOMAIN --- internal/cba.go | 8 +++----- internal/cba_test.go | 2 +- option/internaloption/internaloption.go | 5 ++++- option/internaloption/internaloption_test.go | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/internal/cba.go b/internal/cba.go index e541ede4860..1782f2a0157 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -36,7 +36,6 @@ import ( "context" "crypto/tls" "errors" - "fmt" "net" "net/url" "os" @@ -55,6 +54,8 @@ const ( // Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false. googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A" + + universeDomainPlaceholder = "UNIVERSE_DOMAIN" ) var ( @@ -77,10 +78,7 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source, // TODO(chrisdsmith): Closes: CL-R3 (remove this note before publication) // TODO(chrisdsmith): Use this composed endpoint everywhere to replace DialSettings.DefaultEndpoint if settings.Endpoint == "" && settings.UniverseDomainNotGDU() { - endpoint = fmt.Sprintf(settings.DefaultEndpointTemplate, settings.GetUniverseDomain()) - if err != nil { - return nil, "", err - } + endpoint = strings.Replace(settings.DefaultEndpointTemplate, universeDomainPlaceholder, settings.GetUniverseDomain(), 1) } return clientCertSource, endpoint, nil } diff --git a/internal/cba_test.go b/internal/cba_test.go index 453099def83..c63a1491a29 100644 --- a/internal/cba_test.go +++ b/internal/cba_test.go @@ -15,7 +15,7 @@ import ( const ( testMTLSEndpoint = "https://test.mtls.googleapis.com/" testRegularEndpoint = "https://test.googleapis.com/" - testEndpointTemplate = "https://test.%s/" + testEndpointTemplate = "https://test.UNIVERSE_DOMAIN/" testOverrideEndpoint = "https://test.override.example.com/" testUniverseDomain = "example.com" testUniverseDomainEndpoint = "https://test.example.com/" diff --git a/option/internaloption/internaloption.go b/option/internaloption/internaloption.go index 95cad64a1ca..460b1751573 100644 --- a/option/internaloption/internaloption.go +++ b/option/internaloption/internaloption.go @@ -38,7 +38,10 @@ func (o defaultEndpointTemplateOption) Apply(settings *internal.DialSettings) { // WithDefaultEndpointTemplate provides a template for creating the endpoint // using a universe domain. See also WithDefaultUniverseDomain and -// option.WithUniverseDomain. +// option.WithUniverseDomain. The placeholder UNIVERSE_DOMAIN should be used +// instead of a concrete universe domain such as "googleapis.com". +// +// Example: WithDefaultEndpointTemplate("https://logging.UNIVERSE_DOMAIN/") // // It should only be used internally by generated clients. // diff --git a/option/internaloption/internaloption_test.go b/option/internaloption/internaloption_test.go index 773b5df5375..33037da25ce 100644 --- a/option/internaloption/internaloption_test.go +++ b/option/internaloption/internaloption_test.go @@ -35,7 +35,7 @@ func TestWithCredentials(t *testing.T) { func TestDefaultApply(t *testing.T) { opts := []option.ClientOption{ WithDefaultEndpoint("https://example.com:443"), - WithDefaultEndpointTemplate("https://foo.%s/"), + WithDefaultEndpointTemplate("https://foo.UNIVERSE_DOMAIN/"), WithDefaultMTLSEndpoint("http://mtls.example.com:445"), WithDefaultScopes("a"), WithDefaultUniverseDomain("foo.com"), @@ -48,7 +48,7 @@ func TestDefaultApply(t *testing.T) { want := internal.DialSettings{ DefaultScopes: []string{"a"}, DefaultEndpoint: "https://example.com:443", - DefaultEndpointTemplate: "https://foo.%s/", + DefaultEndpointTemplate: "https://foo.UNIVERSE_DOMAIN/", DefaultUniverseDomain: "foo.com", DefaultAudience: "audience", DefaultMTLSEndpoint: "http://mtls.example.com:445", From b698c143a270f03acc3e3eb7d56190dd04943b10 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 11 Jan 2024 12:00:58 -0700 Subject: [PATCH 09/19] fix isSelfSignedJWTFlow for AL-8 --- internal/creds.go | 29 ++++++++++++++++------------- internal/creds_test.go | 17 ++++++++++++++++- internal/testdata/user-account.json | 7 +++++++ 3 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 internal/testdata/user-account.json diff --git a/internal/creds.go b/internal/creds.go index c74b99a3f7a..6dd5322cb32 100644 --- a/internal/creds.go +++ b/internal/creds.go @@ -129,23 +129,26 @@ func isSelfSignedJWTFlow(data []byte, ds *DialSettings) (bool, error) { // must support self-signed JWTs with scopes. // TODO(chrisdsmith): closes: AL-8 (remove this note before publication) if ds.UniverseDomainNotGDU() { - return true, nil - } - if (ds.EnableJwtWithScope || ds.HasCustomAudience()) && - ds.ImpersonationConfig == nil { - // Check if JSON is a service account and if so create a self-signed JWT. - var f struct { - Type string `json:"type"` - // The rest JSON fields are omitted because they are not used. - } - if err := json.Unmarshal(data, &f); err != nil { - return false, err - } - return f.Type == serviceAccountKey, nil + return typeServiceAccount(data) + } + if (ds.EnableJwtWithScope || ds.HasCustomAudience()) && ds.ImpersonationConfig == nil { + return typeServiceAccount(data) } return false, nil } +// typeServiceAccount checks if JSON data is for a service account. +func typeServiceAccount(data []byte) (bool, error) { + var f struct { + Type string `json:"type"` + // The remaining JSON fields are omitted because they are not used. + } + if err := json.Unmarshal(data, &f); err != nil { + return false, err + } + return f.Type == serviceAccountKey, nil +} + func selfSignedJWTTokenSource(data []byte, ds *DialSettings) (oauth2.TokenSource, error) { if len(ds.GetScopes()) > 0 && !ds.HasCustomAudience() { // Scopes are preferred in self-signed JWT unless the scope is not available diff --git a/internal/creds_test.go b/internal/creds_test.go index ab0e13cd102..60b330034d3 100644 --- a/internal/creds_test.go +++ b/internal/creds_test.go @@ -400,10 +400,25 @@ func TestIsSelfSignedJWTFlow(t *testing.T) { }, want: true, }, + { + name: "UniverseDomainUserAccount", + ds: &DialSettings{ + CredentialsFile: "testdata/user-account.json", + Scopes: []string{"foo"}, + EnableJwtWithScope: false, + UniverseDomain: "example.com", + }, + want: false, + }, } for _, tc := range tests { - isSSJ, err := isSelfSignedJWTFlow([]byte(validServiceAccountJSON), tc.ds) + + bytes, err := os.ReadFile(tc.ds.CredentialsFile) + if err != nil { + t.Fatal(err) + } + isSSJ, err := isSelfSignedJWTFlow(bytes, tc.ds) if err != nil { t.Errorf("[%s]: got %v, wanted no error", tc.name, err) } diff --git a/internal/testdata/user-account.json b/internal/testdata/user-account.json new file mode 100644 index 00000000000..b06c2eadb18 --- /dev/null +++ b/internal/testdata/user-account.json @@ -0,0 +1,7 @@ +{ + "client_id": "", + "client_secret": "", + "quota_project_id": "", + "refresh_token": "", + "type": "authorized_user" +} \ No newline at end of file From c59f588d472d68fc53266c801835676b423b979f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 17 Jan 2024 11:22:31 -0700 Subject: [PATCH 10/19] restore transport --- transport/http/dial.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/transport/http/dial.go b/transport/http/dial.go index e2eb69f7a66..7e322a17c68 100644 --- a/transport/http/dial.go +++ b/transport/http/dial.go @@ -11,7 +11,6 @@ import ( "context" "crypto/tls" "errors" - "fmt" "net" "net/http" "time" @@ -89,11 +88,6 @@ func newTransport(ctx context.Context, base http.RoundTripper, settings *interna if err != nil { return nil, err } - // TODO(chrisdsmith): Closes: CL-R5 for HTTP (remove this note before publication) - // TODO(chrisdsmith): Closes: CL-R5.2 (remove this note before publication) - if settings.GetUniverseDomain() != creds.UniverseDomain() { - return nil, errUniverseNotMatch(settings.GetUniverseDomain(), creds.UniverseDomain()) - } paramTransport.quotaProject = internal.GetQuotaProject(creds, settings.QuotaProject) ts := creds.TokenSource if settings.ImpersonationConfig == nil && settings.TokenSource != nil { @@ -107,15 +101,6 @@ func newTransport(ctx context.Context, base http.RoundTripper, settings *interna return trans, nil } -func errUniverseNotMatch(settingsUD, credsUD string) error { - return fmt.Errorf( - "the configured universe domain (%s) does not match the universe "+ - "domain found in the credentials (%s). If you haven't configured "+ - "WithUniverseDomain explicitly, googleapis.com is the default", - settingsUD, - credsUD) -} - func newSettings(opts []option.ClientOption) (*internal.DialSettings, error) { var o internal.DialSettings for _, opt := range opts { From 56e635c3e0d26f4c6a24c7002f4fb9a9a4e4ec93 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 17 Jan 2024 12:00:01 -0700 Subject: [PATCH 11/19] restore option --- option/internaloption/internaloption.go | 11 +---------- option/internaloption/internaloption_test.go | 4 ++-- option/option.go | 5 ----- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/option/internaloption/internaloption.go b/option/internaloption/internaloption.go index 460b1751573..c15be9faa96 100644 --- a/option/internaloption/internaloption.go +++ b/option/internaloption/internaloption.go @@ -38,14 +38,9 @@ func (o defaultEndpointTemplateOption) Apply(settings *internal.DialSettings) { // WithDefaultEndpointTemplate provides a template for creating the endpoint // using a universe domain. See also WithDefaultUniverseDomain and -// option.WithUniverseDomain. The placeholder UNIVERSE_DOMAIN should be used -// instead of a concrete universe domain such as "googleapis.com". -// -// Example: WithDefaultEndpointTemplate("https://logging.UNIVERSE_DOMAIN/") +// option.WithUniverseDomain. // // It should only be used internally by generated clients. -// -// TODO(chrisdsmith): Refs: CL-R3 (remove this note before publication) func WithDefaultEndpointTemplate(url string) option.ClientOption { return defaultEndpointTemplateOption(url) } @@ -168,10 +163,6 @@ func (w withDefaultUniverseDomain) Apply(o *internal.DialSettings) { // EnableJwtWithScope returns a ClientOption that specifies if scope can be used // with self-signed JWT. -// -// EnableJwtWithScope is ignored when option.WithUniverseDomain is set -// to a non-GDU universe domain. For non-GDU universe domains, token exchange is -// impossible and services must support self-signed JWTs with scopes. func EnableJwtWithScope() option.ClientOption { return enableJwtWithScope(true) } diff --git a/option/internaloption/internaloption_test.go b/option/internaloption/internaloption_test.go index 33037da25ce..773b5df5375 100644 --- a/option/internaloption/internaloption_test.go +++ b/option/internaloption/internaloption_test.go @@ -35,7 +35,7 @@ func TestWithCredentials(t *testing.T) { func TestDefaultApply(t *testing.T) { opts := []option.ClientOption{ WithDefaultEndpoint("https://example.com:443"), - WithDefaultEndpointTemplate("https://foo.UNIVERSE_DOMAIN/"), + WithDefaultEndpointTemplate("https://foo.%s/"), WithDefaultMTLSEndpoint("http://mtls.example.com:445"), WithDefaultScopes("a"), WithDefaultUniverseDomain("foo.com"), @@ -48,7 +48,7 @@ func TestDefaultApply(t *testing.T) { want := internal.DialSettings{ DefaultScopes: []string{"a"}, DefaultEndpoint: "https://example.com:443", - DefaultEndpointTemplate: "https://foo.UNIVERSE_DOMAIN/", + DefaultEndpointTemplate: "https://foo.%s/", DefaultUniverseDomain: "foo.com", DefaultAudience: "audience", DefaultMTLSEndpoint: "http://mtls.example.com:445", diff --git a/option/option.go b/option/option.go index 67df7bf6173..c882c1eb482 100644 --- a/option/option.go +++ b/option/option.go @@ -23,8 +23,6 @@ type ClientOption interface { // WithTokenSource returns a ClientOption that specifies an OAuth2 token // source to be used as the basis for authentication. -// -// TODO(chrisdsmith): Refs: CL-R8 (remove this note before publication) func WithTokenSource(s oauth2.TokenSource) ClientOption { return withTokenSource{s} } @@ -167,8 +165,6 @@ func (w withGRPCConnectionPool) Apply(o *internal.DialSettings) { // // API Keys can only be used for JSON-over-HTTP APIs, including those under // the import path google.golang.org/api/.... -// -// TODO(chrisdsmith): Refs: CL-R8 (remove this note before publication) func WithAPIKey(apiKey string) ClientOption { return withAPIKey(apiKey) } @@ -351,7 +347,6 @@ func WithCredentials(creds *google.Credentials) ClientOption { // WithUniverseDomain returns a ClientOption that sets the universe domain. // // This is an EXPERIMENTAL API and may be changed or removed in the future. -// TODO(chrisdsmith): Closes: CL-R7 (remove this note before publication) func WithUniverseDomain(ud string) ClientOption { return withUniverseDomain(ud) } From 83b34ebae86059636d78395e70b4fe1e9b72fa3e Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 17 Jan 2024 12:35:43 -0700 Subject: [PATCH 12/19] add missing line end to user-account.json --- internal/testdata/user-account.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/testdata/user-account.json b/internal/testdata/user-account.json index b06c2eadb18..e231c19372b 100644 --- a/internal/testdata/user-account.json +++ b/internal/testdata/user-account.json @@ -4,4 +4,4 @@ "quota_project_id": "", "refresh_token": "", "type": "authorized_user" -} \ No newline at end of file +} From 007e93c91d2480c040cbcacd35fdeae976ef7248 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 17 Jan 2024 13:11:21 -0700 Subject: [PATCH 13/19] add temporary guard against missing DefaultEndpointTemplate in getClientCertificateSourceAndEndpoint --- internal/cba.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/cba.go b/internal/cba.go index 1782f2a0157..ac96f173e5d 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -77,7 +77,12 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source, // TODO(chrisdsmith): Closes: CL-R1 (remove this note before publication) // TODO(chrisdsmith): Closes: CL-R3 (remove this note before publication) // TODO(chrisdsmith): Use this composed endpoint everywhere to replace DialSettings.DefaultEndpoint - if settings.Endpoint == "" && settings.UniverseDomainNotGDU() { + // TODO(chrisdsmith): Remove settings.DefaultEndpointTemplate != "" condition after rollout of WithDefaultEndpointTemplate is complete. + if settings.Endpoint == "" && settings.UniverseDomainNotGDU() && settings.DefaultEndpointTemplate != "" { + // TODO(chrisdsmith): Uncomment error check below after rollout of WithDefaultEndpointTemplate is complete. + // if settings.DefaultEndpointTemplate == "" { + // return nil, "", errors.New("internaloption.WithDefaultEndpointTemplate is required if option.WithUniverseDomain is not googleapis.com") + // } endpoint = strings.Replace(settings.DefaultEndpointTemplate, universeDomainPlaceholder, settings.GetUniverseDomain(), 1) } return clientCertSource, endpoint, nil From b19015b8d1750768418e9cff8a5e873566f66eaa Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 17 Jan 2024 14:57:47 -0700 Subject: [PATCH 14/19] remove requirements tracking comments --- impersonate/impersonate.go | 1 - internal/cba.go | 4 ---- internal/creds.go | 1 - internal/settings.go | 1 - 4 files changed, 7 deletions(-) diff --git a/impersonate/impersonate.go b/impersonate/impersonate.go index f46a4227a28..ef60c1fddc4 100644 --- a/impersonate/impersonate.go +++ b/impersonate/impersonate.go @@ -98,7 +98,6 @@ func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts if err != nil { return nil, err } - // TODO(chrisdsmith): Closes: AL-9 (remove this note before publication) if settings.UniverseDomainNotGDU() { return nil, ErrUniverseNotSupportedDomainWideDelegation } diff --git a/internal/cba.go b/internal/cba.go index ac96f173e5d..8efe1a020e4 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -74,8 +74,6 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source, if err != nil { return nil, "", err } - // TODO(chrisdsmith): Closes: CL-R1 (remove this note before publication) - // TODO(chrisdsmith): Closes: CL-R3 (remove this note before publication) // TODO(chrisdsmith): Use this composed endpoint everywhere to replace DialSettings.DefaultEndpoint // TODO(chrisdsmith): Remove settings.DefaultEndpointTemplate != "" condition after rollout of WithDefaultEndpointTemplate is complete. if settings.Endpoint == "" && settings.UniverseDomainNotGDU() && settings.DefaultEndpointTemplate != "" { @@ -112,7 +110,6 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) { if !shouldUseS2A(clientCertSource, settings) { return &defaultTransportConfig, nil } - // TODO(chrisdsmith): Closes: CL-R10 (remove this note before publication) if settings.UniverseDomainNotGDU() { return &transportConfig{ clientCertSource: nil, endpoint: "", s2aAddress: "", s2aMTLSEndpoint: "", @@ -179,7 +176,6 @@ func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string, if settings.Endpoint == "" { mtlsMode := getMTLSMode() if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) { - // TODO(chrisdsmith): Closes: CL-R10 (remove this note before publication) if settings.UniverseDomainNotGDU() { return "", ErrUniverseNotSupportedMTLS } diff --git a/internal/creds.go b/internal/creds.go index 6dd5322cb32..437e4df6afb 100644 --- a/internal/creds.go +++ b/internal/creds.go @@ -127,7 +127,6 @@ func credentialsFromJSON(ctx context.Context, data []byte, ds *DialSettings) (*g func isSelfSignedJWTFlow(data []byte, ds *DialSettings) (bool, error) { // For non-GDU universe domains, token exchange is impossible and services // must support self-signed JWTs with scopes. - // TODO(chrisdsmith): closes: AL-8 (remove this note before publication) if ds.UniverseDomainNotGDU() { return typeServiceAccount(data) } diff --git a/internal/settings.go b/internal/settings.go index a6838415313..0ab6ea3c7b5 100644 --- a/internal/settings.go +++ b/internal/settings.go @@ -165,7 +165,6 @@ func (ds *DialSettings) Validate() error { // UniverseDomain returns the default service domain for a given Cloud universe. // The default value is "googleapis.com". -// TODO(chrisdsmith): Refs: CL-R5.1 (remove this note before publication) func (ds *DialSettings) GetUniverseDomain() string { if ds.UniverseDomain == "" { return universeDomainDefault From b9ed8b41df6a07d04758e91a9622b3ec73fc02a3 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 18 Jan 2024 10:43:23 -0700 Subject: [PATCH 15/19] return nil instead of empty struct from getTransportConfig when err --- impersonate/user.go | 2 +- internal/cba.go | 8 ++------ internal/cba_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/impersonate/user.go b/impersonate/user.go index 01bb42cc1af..fa4e0b43b97 100644 --- a/impersonate/user.go +++ b/impersonate/user.go @@ -19,7 +19,7 @@ import ( ) // user provides an auth flow for domain-wide delegation, setting -// CredentialsConfig.Subject to be the the impersonated user. +// CredentialsConfig.Subject to be the impersonated user. func user(ctx context.Context, c CredentialsConfig, client *http.Client, lifetime time.Duration, isStaticToken bool) (oauth2.TokenSource, error) { u := userTokenSource{ client: client, diff --git a/internal/cba.go b/internal/cba.go index 8efe1a020e4..cdca163bb5e 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -96,9 +96,7 @@ type transportConfig struct { func getTransportConfig(settings *DialSettings) (*transportConfig, error) { clientCertSource, endpoint, err := getClientCertificateSourceAndEndpoint(settings) if err != nil { - return &transportConfig{ - clientCertSource: nil, endpoint: "", s2aAddress: "", s2aMTLSEndpoint: "", - }, err + return nil, err } defaultTransportConfig := transportConfig{ clientCertSource: clientCertSource, @@ -111,9 +109,7 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) { return &defaultTransportConfig, nil } if settings.UniverseDomainNotGDU() { - return &transportConfig{ - clientCertSource: nil, endpoint: "", s2aAddress: "", s2aMTLSEndpoint: "", - }, ErrUniverseNotSupportedMTLS + return nil, ErrUniverseNotSupportedMTLS } s2aMTLSEndpoint := settings.DefaultMTLSEndpoint diff --git a/internal/cba_test.go b/internal/cba_test.go index c63a1491a29..1e2e0a51d4f 100644 --- a/internal/cba_test.go +++ b/internal/cba_test.go @@ -447,6 +447,16 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { }, wantEndpoint: testRegularEndpoint, }, + { + name: "google default universe (GDU), no client cert, endpoint", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + Endpoint: testOverrideEndpoint, + }, + wantEndpoint: testOverrideEndpoint, + }, { name: "google default universe (GDU), client cert", ds: &DialSettings{ @@ -457,6 +467,17 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { }, wantEndpoint: testMTLSEndpoint, }, + { + name: "google default universe (GDU), client cert, endpoint", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + ClientCertSource: dummyClientCertSource, + Endpoint: testOverrideEndpoint, + }, + wantEndpoint: testOverrideEndpoint, + }, { name: "UniverseDomain, no client cert", ds: &DialSettings{ @@ -467,6 +488,17 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { }, wantEndpoint: testUniverseDomainEndpoint, }, + { + name: "UniverseDomain, no client cert, endpoint", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + Endpoint: testOverrideEndpoint, + }, + wantEndpoint: testOverrideEndpoint, + }, { name: "UniverseDomain, client cert", ds: &DialSettings{ @@ -476,8 +508,19 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { UniverseDomain: testUniverseDomain, ClientCertSource: dummyClientCertSource, }, - wantEndpoint: testUniverseDomainEndpoint, - wantErr: ErrUniverseNotSupportedMTLS, + wantErr: ErrUniverseNotSupportedMTLS, + }, + { + name: "UniverseDomain, client cert, endpoint", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + ClientCertSource: dummyClientCertSource, + Endpoint: testOverrideEndpoint, + }, + wantEndpoint: testOverrideEndpoint, }, } From 7fb62d89e6fe95cbd44f1232aaac05e8a854c779 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 18 Jan 2024 12:43:30 -0700 Subject: [PATCH 16/19] rename UniverseDomainNotGDU to IsUniverseDomainGDU and invert logic --- impersonate/impersonate.go | 2 +- internal/cba.go | 6 +++--- internal/creds.go | 2 +- internal/settings.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/impersonate/impersonate.go b/impersonate/impersonate.go index ef60c1fddc4..683350cd1de 100644 --- a/impersonate/impersonate.go +++ b/impersonate/impersonate.go @@ -98,7 +98,7 @@ func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts if err != nil { return nil, err } - if settings.UniverseDomainNotGDU() { + if !settings.IsUniverseDomainGDU() { return nil, ErrUniverseNotSupportedDomainWideDelegation } return user(ctx, config, client, lifetime, isStaticToken) diff --git a/internal/cba.go b/internal/cba.go index cdca163bb5e..4dd478c5b81 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -76,7 +76,7 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source, } // TODO(chrisdsmith): Use this composed endpoint everywhere to replace DialSettings.DefaultEndpoint // TODO(chrisdsmith): Remove settings.DefaultEndpointTemplate != "" condition after rollout of WithDefaultEndpointTemplate is complete. - if settings.Endpoint == "" && settings.UniverseDomainNotGDU() && settings.DefaultEndpointTemplate != "" { + if settings.Endpoint == "" && !settings.IsUniverseDomainGDU() && settings.DefaultEndpointTemplate != "" { // TODO(chrisdsmith): Uncomment error check below after rollout of WithDefaultEndpointTemplate is complete. // if settings.DefaultEndpointTemplate == "" { // return nil, "", errors.New("internaloption.WithDefaultEndpointTemplate is required if option.WithUniverseDomain is not googleapis.com") @@ -108,7 +108,7 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) { if !shouldUseS2A(clientCertSource, settings) { return &defaultTransportConfig, nil } - if settings.UniverseDomainNotGDU() { + if !settings.IsUniverseDomainGDU() { return nil, ErrUniverseNotSupportedMTLS } @@ -172,7 +172,7 @@ func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string, if settings.Endpoint == "" { mtlsMode := getMTLSMode() if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) { - if settings.UniverseDomainNotGDU() { + if !settings.IsUniverseDomainGDU() { return "", ErrUniverseNotSupportedMTLS } return settings.DefaultMTLSEndpoint, nil diff --git a/internal/creds.go b/internal/creds.go index 437e4df6afb..9ce2d4ff141 100644 --- a/internal/creds.go +++ b/internal/creds.go @@ -127,7 +127,7 @@ func credentialsFromJSON(ctx context.Context, data []byte, ds *DialSettings) (*g func isSelfSignedJWTFlow(data []byte, ds *DialSettings) (bool, error) { // For non-GDU universe domains, token exchange is impossible and services // must support self-signed JWTs with scopes. - if ds.UniverseDomainNotGDU() { + if !ds.IsUniverseDomainGDU() { return typeServiceAccount(data) } if (ds.EnableJwtWithScope || ds.HasCustomAudience()) && ds.ImpersonationConfig == nil { diff --git a/internal/settings.go b/internal/settings.go index 0ab6ea3c7b5..3d86eee19b9 100644 --- a/internal/settings.go +++ b/internal/settings.go @@ -172,6 +172,6 @@ func (ds *DialSettings) GetUniverseDomain() string { return ds.UniverseDomain } -func (ds *DialSettings) UniverseDomainNotGDU() bool { - return ds.GetUniverseDomain() != universeDomainDefault +func (ds *DialSettings) IsUniverseDomainGDU() bool { + return ds.GetUniverseDomain() == universeDomainDefault } From 82ad920f340a8deed603496860b39b8b44243205 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 18 Jan 2024 12:58:46 -0700 Subject: [PATCH 17/19] Update errors to unexported types --- impersonate/impersonate.go | 16 +++++++++------- impersonate/impersonate_test.go | 33 +++++++++++++++++---------------- internal/cba.go | 6 +++--- internal/cba_test.go | 4 ++-- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/impersonate/impersonate.go b/impersonate/impersonate.go index 683350cd1de..a09698aded1 100644 --- a/impersonate/impersonate.go +++ b/impersonate/impersonate.go @@ -24,9 +24,11 @@ import ( var ( iamCredentailsEndpoint = "https://iamcredentials.googleapis.com" oauth2Endpoint = "https://oauth2.googleapis.com" - ErrUniverseNotSupportedDomainWideDelegation = errors.New( - "impersonate: service account user is configured for the credential. Domain-wide " + - "delegation is not supported in universes other than googleapis.com") + errMissingTargetPrincipal = errors.New("impersonate: a target service account must be provided") + errMissingScopes = errors.New("impersonate: scopes must be provided") + errLifetimeOverMax = errors.New("impersonate: max lifetime is 12 hours") + errUniverseNotSupportedDomainWideDelegation = errors.New("impersonate: service account user is configured for the credential. " + + "Domain-wide delegation is not supported in universes other than googleapis.com") ) // CredentialsConfig for generating impersonated credentials. @@ -67,13 +69,13 @@ func defaultClientOptions() []option.ClientOption { // the base credentials. func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts ...option.ClientOption) (oauth2.TokenSource, error) { if config.TargetPrincipal == "" { - return nil, fmt.Errorf("impersonate: a target service account must be provided") + return nil, errMissingTargetPrincipal } if len(config.Scopes) == 0 { - return nil, fmt.Errorf("impersonate: scopes must be provided") + return nil, errMissingScopes } if config.Lifetime.Hours() > 12 { - return nil, fmt.Errorf("impersonate: max lifetime is 12 hours") + return nil, errLifetimeOverMax } var isStaticToken bool @@ -99,7 +101,7 @@ func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts return nil, err } if !settings.IsUniverseDomainGDU() { - return nil, ErrUniverseNotSupportedDomainWideDelegation + return nil, errUniverseNotSupportedDomainWideDelegation } return user(ctx, config, client, lifetime, isStaticToken) } diff --git a/impersonate/impersonate_test.go b/impersonate/impersonate_test.go index 8e6a09ac9a2..0760698af09 100644 --- a/impersonate/impersonate_test.go +++ b/impersonate/impersonate_test.go @@ -24,29 +24,29 @@ func TestTokenSource_serviceAccount(t *testing.T) { targetPrincipal string scopes []string lifetime time.Duration - wantErr bool + wantErr error }{ { name: "missing targetPrincipal", - wantErr: true, + wantErr: errMissingTargetPrincipal, }, { name: "missing scopes", targetPrincipal: "foo@project-id.iam.gserviceaccount.com", - wantErr: true, + wantErr: errMissingScopes, }, { name: "lifetime over max", targetPrincipal: "foo@project-id.iam.gserviceaccount.com", scopes: []string{"scope"}, lifetime: 13 * time.Hour, - wantErr: true, + wantErr: errLifetimeOverMax, }, { name: "works", targetPrincipal: "foo@project-id.iam.gserviceaccount.com", scopes: []string{"scope"}, - wantErr: false, + wantErr: nil, }, } @@ -79,18 +79,19 @@ func TestTokenSource_serviceAccount(t *testing.T) { Scopes: tt.scopes, Lifetime: tt.lifetime, }, option.WithHTTPClient(client)) - if tt.wantErr && err != nil { - return - } - if err != nil { - t.Fatal(err) - } - tok, err := ts.Token() + if err != nil { - t.Fatal(err) - } - if tok.AccessToken != saTok { - t.Fatalf("got %q, want %q", tok.AccessToken, saTok) + if err != tt.wantErr { + t.Fatalf("%s: err: %v", tt.name, err) + } + } else { + tok, err := ts.Token() + if err != nil { + t.Fatal(err) + } + if tok.AccessToken != saTok { + t.Fatalf("got %q, want %q", tok.AccessToken, saTok) + } } }) } diff --git a/internal/cba.go b/internal/cba.go index 4dd478c5b81..28e8850028c 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -59,7 +59,7 @@ const ( ) var ( - ErrUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com") + errUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com") ) // getClientCertificateSourceAndEndpoint is a convenience function that invokes @@ -109,7 +109,7 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) { return &defaultTransportConfig, nil } if !settings.IsUniverseDomainGDU() { - return nil, ErrUniverseNotSupportedMTLS + return nil, errUniverseNotSupportedMTLS } s2aMTLSEndpoint := settings.DefaultMTLSEndpoint @@ -173,7 +173,7 @@ func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string, mtlsMode := getMTLSMode() if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) { if !settings.IsUniverseDomainGDU() { - return "", ErrUniverseNotSupportedMTLS + return "", errUniverseNotSupportedMTLS } return settings.DefaultMTLSEndpoint, nil } diff --git a/internal/cba_test.go b/internal/cba_test.go index 1e2e0a51d4f..942f55f5f1f 100644 --- a/internal/cba_test.go +++ b/internal/cba_test.go @@ -408,7 +408,7 @@ func TestGetHTTPTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { ClientCertSource: dummyClientCertSource, }, wantEndpoint: testUniverseDomainEndpoint, - wantErr: ErrUniverseNotSupportedMTLS, + wantErr: errUniverseNotSupportedMTLS, }, } @@ -508,7 +508,7 @@ func TestGetGRPCTransportConfigAndEndpoint_UniverseDomain(t *testing.T) { UniverseDomain: testUniverseDomain, ClientCertSource: dummyClientCertSource, }, - wantErr: ErrUniverseNotSupportedMTLS, + wantErr: errUniverseNotSupportedMTLS, }, { name: "UniverseDomain, client cert, endpoint", From 15b1c22723cc866eff3e947a6ebc32f71fea4ea9 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 18 Jan 2024 13:13:42 -0700 Subject: [PATCH 18/19] move TODO instructions to GH issue --- internal/cba.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/cba.go b/internal/cba.go index 28e8850028c..ee0408bd006 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -74,10 +74,9 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source, if err != nil { return nil, "", err } - // TODO(chrisdsmith): Use this composed endpoint everywhere to replace DialSettings.DefaultEndpoint - // TODO(chrisdsmith): Remove settings.DefaultEndpointTemplate != "" condition after rollout of WithDefaultEndpointTemplate is complete. + // TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359 if settings.Endpoint == "" && !settings.IsUniverseDomainGDU() && settings.DefaultEndpointTemplate != "" { - // TODO(chrisdsmith): Uncomment error check below after rollout of WithDefaultEndpointTemplate is complete. + // TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359 // if settings.DefaultEndpointTemplate == "" { // return nil, "", errors.New("internaloption.WithDefaultEndpointTemplate is required if option.WithUniverseDomain is not googleapis.com") // } From f37a18c8064eed10c306d8b4a7ef99e98aa0aaf4 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 18 Jan 2024 14:15:06 -0700 Subject: [PATCH 19/19] update impersonate test --- impersonate/impersonate_test.go | 61 +++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/impersonate/impersonate_test.go b/impersonate/impersonate_test.go index 0760698af09..026a7695ebb 100644 --- a/impersonate/impersonate_test.go +++ b/impersonate/impersonate_test.go @@ -20,33 +20,48 @@ import ( func TestTokenSource_serviceAccount(t *testing.T) { ctx := context.Background() tests := []struct { - name string - targetPrincipal string - scopes []string - lifetime time.Duration - wantErr error + name string + config CredentialsConfig + opts option.ClientOption + wantErr error }{ { name: "missing targetPrincipal", wantErr: errMissingTargetPrincipal, }, { - name: "missing scopes", - targetPrincipal: "foo@project-id.iam.gserviceaccount.com", - wantErr: errMissingScopes, + name: "missing scopes", + config: CredentialsConfig{ + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + }, + wantErr: errMissingScopes, }, { - name: "lifetime over max", - targetPrincipal: "foo@project-id.iam.gserviceaccount.com", - scopes: []string{"scope"}, - lifetime: 13 * time.Hour, - wantErr: errLifetimeOverMax, + name: "lifetime over max", + config: CredentialsConfig{ + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + Scopes: []string{"scope"}, + Lifetime: 13 * time.Hour, + }, + wantErr: errLifetimeOverMax, }, { - name: "works", - targetPrincipal: "foo@project-id.iam.gserviceaccount.com", - scopes: []string{"scope"}, - wantErr: nil, + name: "works", + config: CredentialsConfig{ + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + Scopes: []string{"scope"}, + }, + wantErr: nil, + }, + { + name: "universe domain", + config: CredentialsConfig{ + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + Scopes: []string{"scope"}, + Subject: "admin@example.com", + }, + opts: option.WithUniverseDomain("example.com"), + wantErr: errUniverseNotSupportedDomainWideDelegation, }, } @@ -74,11 +89,13 @@ func TestTokenSource_serviceAccount(t *testing.T) { return nil }), } - ts, err := CredentialsTokenSource(ctx, CredentialsConfig{ - TargetPrincipal: tt.targetPrincipal, - Scopes: tt.scopes, - Lifetime: tt.lifetime, - }, option.WithHTTPClient(client)) + opts := []option.ClientOption{ + option.WithHTTPClient(client), + } + if tt.opts != nil { + opts = append(opts, tt.opts) + } + ts, err := CredentialsTokenSource(ctx, tt.config, opts...) if err != nil { if err != tt.wantErr {