From 6ef1144a79d93982b56c81cf0116965a7c0bf93a Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 18 Jan 2024 15:27:12 -0700 Subject: [PATCH] feat(impersonate): add universe domain support (#2296) --- impersonate/impersonate.go | 40 +++++- impersonate/impersonate_test.go | 82 +++++++----- impersonate/user.go | 2 + impersonate/user_test.go | 26 +++- internal/cba.go | 25 +++- internal/cba_test.go | 197 +++++++++++++++++++++++++++- internal/creds.go | 30 +++-- internal/creds_test.go | 90 +++++++++++++ internal/settings.go | 16 ++- internal/testdata/user-account.json | 7 + 10 files changed, 450 insertions(+), 65 deletions(-) create mode 100644 internal/testdata/user-account.json diff --git a/impersonate/impersonate.go b/impersonate/impersonate.go index 86d5eb82d58..a09698aded1 100644 --- a/impersonate/impersonate.go +++ b/impersonate/impersonate.go @@ -8,20 +8,27 @@ 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" + 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. @@ -62,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 @@ -86,9 +93,16 @@ 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 + } + if !settings.IsUniverseDomainGDU() { + return nil, errUniverseNotSupportedDomainWideDelegation + } return user(ctx, config, client, lifetime, isStaticToken) } @@ -113,6 +127,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/impersonate_test.go b/impersonate/impersonate_test.go index 8e6a09ac9a2..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 bool + name string + config CredentialsConfig + opts option.ClientOption + wantErr error }{ { name: "missing targetPrincipal", - wantErr: true, + wantErr: errMissingTargetPrincipal, }, { - name: "missing scopes", - targetPrincipal: "foo@project-id.iam.gserviceaccount.com", - wantErr: true, + 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: true, + 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: false, + 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,23 +89,26 @@ 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)) - if tt.wantErr && err != nil { - return + opts := []option.ClientOption{ + option.WithHTTPClient(client), } - if err != nil { - t.Fatal(err) + if tt.opts != nil { + opts = append(opts, tt.opts) } - tok, err := ts.Token() + ts, err := CredentialsTokenSource(ctx, tt.config, opts...) + 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/impersonate/user.go b/impersonate/user.go index c234abb8998..fa4e0b43b97 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 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 } diff --git a/internal/cba.go b/internal/cba.go index 829383f55b5..ee0408bd006 100644 --- a/internal/cba.go +++ b/internal/cba.go @@ -35,6 +35,7 @@ package internal import ( "context" "crypto/tls" + "errors" "net" "net/url" "os" @@ -53,6 +54,12 @@ 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 ( + errUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com") ) // getClientCertificateSourceAndEndpoint is a convenience function that invokes @@ -67,6 +74,14 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source, if err != nil { return nil, "", err } + // TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359 + if settings.Endpoint == "" && !settings.IsUniverseDomainGDU() && settings.DefaultEndpointTemplate != "" { + // 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") + // } + endpoint = strings.Replace(settings.DefaultEndpointTemplate, universeDomainPlaceholder, settings.GetUniverseDomain(), 1) + } return clientCertSource, endpoint, nil } @@ -80,9 +95,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, @@ -94,6 +107,9 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) { if !shouldUseS2A(clientCertSource, settings) { return &defaultTransportConfig, nil } + if !settings.IsUniverseDomainGDU() { + return nil, errUniverseNotSupportedMTLS + } s2aMTLSEndpoint := settings.DefaultMTLSEndpoint // If there is endpoint override, honor it. @@ -155,6 +171,9 @@ func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string, if settings.Endpoint == "" { mtlsMode := getMTLSMode() if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) { + if !settings.IsUniverseDomainGDU() { + return "", errUniverseNotSupportedMTLS + } return settings.DefaultMTLSEndpoint, nil } return settings.DefaultEndpoint, nil diff --git a/internal/cba_test.go b/internal/cba_test.go index d6a783740e5..942f55f5f1f 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.UNIVERSE_DOMAIN/" + 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,184 @@ 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 error + }{ + { + name: "google default universe (GDU), no client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + }, + wantEndpoint: testRegularEndpoint, + }, + { + name: "google default universe (GDU), client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + ClientCertSource: dummyClientCertSource, + }, + wantEndpoint: testMTLSEndpoint, + }, + { + name: "UniverseDomain, no client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + }, + wantEndpoint: testUniverseDomainEndpoint, + }, + { + name: "UniverseDomain, client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + ClientCertSource: dummyClientCertSource, + }, + wantEndpoint: testUniverseDomainEndpoint, + wantErr: errUniverseNotSupportedMTLS, + }, + } + + 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 != 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 error + }{ + { + name: "google default universe (GDU), no client cert", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + }, + 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{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + ClientCertSource: dummyClientCertSource, + }, + 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{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + }, + 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{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + ClientCertSource: dummyClientCertSource, + }, + wantErr: errUniverseNotSupportedMTLS, + }, + { + name: "UniverseDomain, client cert, endpoint", + ds: &DialSettings{ + DefaultEndpoint: testRegularEndpoint, + DefaultEndpointTemplate: testEndpointTemplate, + DefaultMTLSEndpoint: testMTLSEndpoint, + UniverseDomain: testUniverseDomain, + ClientCertSource: dummyClientCertSource, + Endpoint: testOverrideEndpoint, + }, + wantEndpoint: testOverrideEndpoint, + }, + } + + 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 != 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/creds.go b/internal/creds.go index 05165f333b0..9ce2d4ff141 100644 --- a/internal/creds.go +++ b/internal/creds.go @@ -125,21 +125,29 @@ func credentialsFromJSON(ctx context.Context, data []byte, ds *DialSettings) (*g } func isSelfSignedJWTFlow(data []byte, ds *DialSettings) (bool, error) { - 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 + // For non-GDU universe domains, token exchange is impossible and services + // must support self-signed JWTs with scopes. + if !ds.IsUniverseDomainGDU() { + 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 0651567bcd0..60b330034d3 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,65 @@ 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, + }, + { + name: "UniverseDomainUserAccount", + ds: &DialSettings{ + CredentialsFile: "testdata/user-account.json", + Scopes: []string{"foo"}, + EnableJwtWithScope: false, + UniverseDomain: "example.com", + }, + want: false, + }, + } + + for _, tc := range tests { + + 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) + } + if isSSJ != tc.want { + t.Errorf("[%s]: got %t, wanted %t", tc.name, isSSJ, tc.want) + } + } +} diff --git a/internal/settings.go b/internal/settings.go index 285e6e04d39..3d86eee19b9 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 @@ -161,3 +162,16 @@ func (ds *DialSettings) Validate() error { } return nil } + +// UniverseDomain returns the default service domain for a given Cloud universe. +// The default value is "googleapis.com". +func (ds *DialSettings) GetUniverseDomain() string { + if ds.UniverseDomain == "" { + return universeDomainDefault + } + return ds.UniverseDomain +} + +func (ds *DialSettings) IsUniverseDomainGDU() bool { + return ds.GetUniverseDomain() == universeDomainDefault +} diff --git a/internal/testdata/user-account.json b/internal/testdata/user-account.json new file mode 100644 index 00000000000..e231c19372b --- /dev/null +++ b/internal/testdata/user-account.json @@ -0,0 +1,7 @@ +{ + "client_id": "", + "client_secret": "", + "quota_project_id": "", + "refresh_token": "", + "type": "authorized_user" +}