From 260aaaa98282ad7547c360e4cbc5dad9c2dfc1d5 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 29 Oct 2024 16:52:01 -0600 Subject: [PATCH 01/21] feat(auth): add universe domain support to idtoken --- auth/credentials/idtoken/file.go | 109 +++++++++++++++----- auth/credentials/idtoken/idtoken.go | 5 + auth/credentials/impersonate/idtoken.go | 4 +- auth/credentials/impersonate/impersonate.go | 8 +- auth/credentials/impersonate/user.go | 4 +- auth/internal/internal.go | 6 ++ 6 files changed, 103 insertions(+), 33 deletions(-) diff --git a/auth/credentials/idtoken/file.go b/auth/credentials/idtoken/file.go index 333521c91940..9d57d5937274 100644 --- a/auth/credentials/idtoken/file.go +++ b/auth/credentials/idtoken/file.go @@ -17,12 +17,14 @@ package idtoken import ( "encoding/json" "fmt" + "net/http" "path/filepath" "strings" "cloud.google.com/go/auth" "cloud.google.com/go/auth/credentials" "cloud.google.com/go/auth/credentials/impersonate" + "cloud.google.com/go/auth/httptransport" "cloud.google.com/go/auth/internal" "cloud.google.com/go/auth/internal/credsfile" ) @@ -50,38 +52,23 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) { if err != nil { return nil, err } - opts2LO := &auth.Options2LO{ - Email: f.ClientEmail, - PrivateKey: []byte(f.PrivateKey), - PrivateKeyID: f.PrivateKeyID, - TokenURL: f.TokenURL, - UseIDToken: true, + var tp auth.TokenProvider + if opts.UseIAMEndpoint { + tp, err = newIAMIDTokenProvider(b, f, opts) + } else { + tp, err = new2LOTokenProvider(f, opts) } - if opts2LO.TokenURL == "" { - opts2LO.TokenURL = jwtTokenURL - } - - var customClaims map[string]interface{} - if opts != nil { - customClaims = opts.CustomClaims - } - if customClaims == nil { - customClaims = make(map[string]interface{}) - } - customClaims["target_audience"] = opts.Audience - - opts2LO.PrivateClaims = customClaims - tp, err := auth.New2LOTokenProvider(opts2LO) if err != nil { return nil, err } tp = auth.NewCachedTokenProvider(tp, nil) - return auth.NewCredentials(&auth.CredentialsOptions{ + creds := auth.NewCredentials(&auth.CredentialsOptions{ TokenProvider: tp, JSON: b, ProjectIDProvider: internal.StaticCredentialsProperty(f.ProjectID), UniverseDomainProvider: internal.StaticCredentialsProperty(f.UniverseDomain), - }), nil + }) + return creds, nil case credsfile.ImpersonatedServiceAccountKey, credsfile.ExternalAccountKey: type url struct { ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` @@ -125,3 +112,79 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) { return nil, fmt.Errorf("idtoken: unsupported credentials type: %v", t) } } + +func new2LOTokenProvider(f *credsfile.ServiceAccountFile, opts *Options) (auth.TokenProvider, error) { + opts2LO := &auth.Options2LO{ + Email: f.ClientEmail, + PrivateKey: []byte(f.PrivateKey), + PrivateKeyID: f.PrivateKeyID, + TokenURL: f.TokenURL, + UseIDToken: true, + } + if opts2LO.TokenURL == "" { + opts2LO.TokenURL = jwtTokenURL + } + + var customClaims map[string]interface{} + if opts != nil { + customClaims = opts.CustomClaims + } + if customClaims == nil { + customClaims = make(map[string]interface{}) + } + customClaims["target_audience"] = opts.Audience + + opts2LO.PrivateClaims = customClaims + return auth.New2LOTokenProvider(opts2LO) +} + +// newIAMIDTokenProvider creates a TokenProvider that performs an authenticated +// RPC with the IAM service to obtain an ID token. The provided service account +// must have the iam.serviceAccountTokenCreator role. If a fully-authenticated +// client is not provided, the service account must support a self-signed JWT. +// This TokenProvider is primarily intended for use in non-GDU universes, which +// do not have access to the oauth2.googleapis.com/token endpoint, and thus must +// use IAM generateIdToken instead. +func newIAMIDTokenProvider(b []byte, f *credsfile.ServiceAccountFile, opts *Options) (auth.TokenProvider, error) { + var client *http.Client + var creds *auth.Credentials + var err error + universeDomain := resolveUniverseDomain(f) + if opts.Client == nil { + creds, err = credentials.DetectDefault(&credentials.DetectOptions{ + CredentialsJSON: b, + Scopes: []string{"https://www.googleapis.com/auth/iam"}, + UseSelfSignedJWT: true, + UniverseDomain: universeDomain, + }) + if err != nil { + return nil, err + } + client, err = httptransport.NewClient(&httptransport.Options{ + Credentials: creds, + UniverseDomain: universeDomain, + }) + if err != nil { + return nil, err + } + } else { + client = opts.Client + } + its := iamIDTokenProvider{ + client: client, + universeDomain: universeDomain, + signerEmail: f.ClientEmail, + audience: opts.Audience, + } + return its, nil +} + +// resolveUniverseDomain returns the default service domain for a given +// Cloud universe. This is the universe domain configured for the credentials, +// which will be used in endpoint. +func resolveUniverseDomain(f *credsfile.ServiceAccountFile) string { + if f.UniverseDomain != "" { + return f.UniverseDomain + } + return internal.DefaultUniverseDomain +} diff --git a/auth/credentials/idtoken/idtoken.go b/auth/credentials/idtoken/idtoken.go index b66c6551e6ee..86e1a9ccaacc 100644 --- a/auth/credentials/idtoken/idtoken.go +++ b/auth/credentials/idtoken/idtoken.go @@ -74,6 +74,11 @@ type Options struct { // when fetching tokens. If provided this should be a fully authenticated // client. Optional. Client *http.Client + // UseIAMEndpoint configures whether the IAM generateIdToken endpoint will + // be used instead of the oauth2.googleapis.com/token endpoint. Note that + // the iam.serviceAccountTokenCreator role is required to use the IAM + // generateIdToken endpoint. The default value is false. Optional. + UseIAMEndpoint bool } func (o *Options) client() *http.Client { diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index e51bee7d8764..e8921ff1dff1 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -118,7 +118,7 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { includeEmail: opts.IncludeEmail, } for _, v := range opts.Delegates { - itp.delegates = append(itp.delegates, formatIAMServiceAccountName(v)) + itp.delegates = append(itp.delegates, internal.FormatIAMServiceAccountName(v)) } var udp auth.CredentialsPropertyProvider @@ -161,7 +161,7 @@ func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, er return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) } - url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, formatIAMServiceAccountName(i.targetPrincipal)) + url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal)) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) if err != nil { return nil, fmt.Errorf("impersonate: unable to create request: %w", err) diff --git a/auth/credentials/impersonate/impersonate.go b/auth/credentials/impersonate/impersonate.go index df306057b49c..eb2f5d1b809e 100644 --- a/auth/credentials/impersonate/impersonate.go +++ b/auth/credentials/impersonate/impersonate.go @@ -111,7 +111,7 @@ func NewCredentials(opts *CredentialsOptions) (*auth.Credentials, error) { universeDomainProvider: universeDomainProvider, } for _, v := range opts.Delegates { - its.delegates = append(its.delegates, formatIAMServiceAccountName(v)) + its.delegates = append(its.delegates, internal.FormatIAMServiceAccountName(v)) } its.scopes = make([]string, len(opts.Scopes)) copy(its.scopes, opts.Scopes) @@ -197,10 +197,6 @@ func (o *CredentialsOptions) validate() error { return nil } -func formatIAMServiceAccountName(name string) string { - return fmt.Sprintf("projects/-/serviceAccounts/%s", name) -} - type generateAccessTokenRequest struct { Delegates []string `json:"delegates,omitempty"` Lifetime string `json:"lifetime,omitempty"` @@ -238,7 +234,7 @@ func (i impersonatedTokenProvider) Token(ctx context.Context) (*auth.Token, erro return nil, err } endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1) - url := fmt.Sprintf("%s/v1/%s:generateAccessToken", endpoint, formatIAMServiceAccountName(i.targetPrincipal)) + url := fmt.Sprintf("%s/v1/%s:generateAccessToken", endpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal)) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b)) if err != nil { return nil, fmt.Errorf("impersonate: unable to create request: %w", err) diff --git a/auth/credentials/impersonate/user.go b/auth/credentials/impersonate/user.go index b5e5fc8f6645..73180222a5aa 100644 --- a/auth/credentials/impersonate/user.go +++ b/auth/credentials/impersonate/user.go @@ -44,7 +44,7 @@ func user(opts *CredentialsOptions, client *http.Client, lifetime time.Duration, } u.delegates = make([]string, len(opts.Delegates)) for i, v := range opts.Delegates { - u.delegates[i] = formatIAMServiceAccountName(v) + u.delegates[i] = internal.FormatIAMServiceAccountName(v) } u.scopes = make([]string, len(opts.Scopes)) copy(u.scopes, opts.Scopes) @@ -139,7 +139,7 @@ func (u userTokenProvider) signJWT(ctx context.Context) (string, error) { if err != nil { return "", fmt.Errorf("impersonate: unable to marshal request: %w", err) } - reqURL := fmt.Sprintf("%s/v1/%s:signJwt", iamCredentialsEndpoint, formatIAMServiceAccountName(u.targetPrincipal)) + reqURL := fmt.Sprintf("%s/v1/%s:signJwt", iamCredentialsEndpoint, internal.FormatIAMServiceAccountName(u.targetPrincipal)) req, err := http.NewRequestWithContext(ctx, "POST", reqURL, bytes.NewReader(bodyBytes)) if err != nil { return "", fmt.Errorf("impersonate: unable to create request: %w", err) diff --git a/auth/internal/internal.go b/auth/internal/internal.go index d8c16119180e..8e610800fc3a 100644 --- a/auth/internal/internal.go +++ b/auth/internal/internal.go @@ -214,3 +214,9 @@ func getMetadataUniverseDomain(ctx context.Context) (string, error) { } return "", err } + +// FormatIAMServiceAccountName sets a service account name in an IAM resource +// name. +func FormatIAMServiceAccountName(name string) string { + return fmt.Sprintf("projects/-/serviceAccounts/%s", name) +} From 2a8bf0b5f23413abf248150d047c1e4659f2dd35 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 30 Oct 2024 12:40:50 -0600 Subject: [PATCH 02/21] use base creds universe_domain instead of UseIAMEndpoint flag --- auth/credentials/idtoken/file.go | 3 ++- auth/credentials/idtoken/idtoken.go | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/auth/credentials/idtoken/file.go b/auth/credentials/idtoken/file.go index 9d57d5937274..d57a02495562 100644 --- a/auth/credentials/idtoken/file.go +++ b/auth/credentials/idtoken/file.go @@ -53,7 +53,8 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) { return nil, err } var tp auth.TokenProvider - if opts.UseIAMEndpoint { + universeDomain := resolveUniverseDomain(f) + if universeDomain != internal.DefaultUniverseDomain { tp, err = newIAMIDTokenProvider(b, f, opts) } else { tp, err = new2LOTokenProvider(f, opts) diff --git a/auth/credentials/idtoken/idtoken.go b/auth/credentials/idtoken/idtoken.go index 86e1a9ccaacc..b66c6551e6ee 100644 --- a/auth/credentials/idtoken/idtoken.go +++ b/auth/credentials/idtoken/idtoken.go @@ -74,11 +74,6 @@ type Options struct { // when fetching tokens. If provided this should be a fully authenticated // client. Optional. Client *http.Client - // UseIAMEndpoint configures whether the IAM generateIdToken endpoint will - // be used instead of the oauth2.googleapis.com/token endpoint. Note that - // the iam.serviceAccountTokenCreator role is required to use the IAM - // generateIdToken endpoint. The default value is false. Optional. - UseIAMEndpoint bool } func (o *Options) client() *http.Client { From 285045cf93fec038894f63ea5166e3461b32c0cb Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 30 Oct 2024 13:03:09 -0600 Subject: [PATCH 03/21] add impersonate.IDTokenOptions.UniverseDomain --- auth/credentials/idtoken/file.go | 8 ++- auth/credentials/impersonate/idtoken.go | 61 ++++++++++++--------- auth/credentials/impersonate/impersonate.go | 1 - auth/credentials/impersonate/user.go | 4 ++ 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/auth/credentials/idtoken/file.go b/auth/credentials/idtoken/file.go index d57a02495562..ab92bdffe716 100644 --- a/auth/credentials/idtoken/file.go +++ b/auth/credentials/idtoken/file.go @@ -15,6 +15,7 @@ package idtoken import ( + "context" "encoding/json" "fmt" "net/http" @@ -90,13 +91,18 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) { if err != nil { return nil, err } - + // Hard pull of the provider is OK with file-based creds. + universeDomain, err := baseCreds.UniverseDomain(context.Background()) + if err != nil { + return nil, err + } config := impersonate.IDTokenOptions{ Audience: opts.Audience, TargetPrincipal: account, IncludeEmail: true, Client: opts.client(), Credentials: baseCreds, + UniverseDomain: universeDomain, } creds, err := impersonate.NewIDTokenCredentials(&config) if err != nil { diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index e8921ff1dff1..24d43d214ce6 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "cloud.google.com/go/auth" @@ -55,6 +56,11 @@ type IDTokenOptions struct { // when fetching tokens. If provided the client should provide it's own // base credentials at call time. Optional. Client *http.Client + // UniverseDomain is the default service domain for a given Cloud universe. + // The default value is "googleapis.com". This is the universe domain + // configured for the client, which will be compared to the universe domain + // that is separately configured for the credentials. Optional. + UniverseDomain string } func (o *IDTokenOptions) validate() error { @@ -85,49 +91,45 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { } var client *http.Client var creds *auth.Credentials - if opts.Client == nil && opts.Credentials == nil { + if opts.Client == nil { var err error - // TODO: test not signed jwt more - creds, err = credentials.DetectDefault(&credentials.DetectOptions{ - Scopes: []string{defaultScope}, - UseSelfSignedJWT: true, - }) - if err != nil { - return nil, err + if opts.Credentials == nil { + creds, err = credentials.DetectDefault(&credentials.DetectOptions{ + Scopes: []string{defaultScope}, + UseSelfSignedJWT: true, + }) + if err != nil { + return nil, err + } + } else { + creds = opts.Credentials } client, err = httptransport.NewClient(&httptransport.Options{ - Credentials: creds, + Credentials: creds, + UniverseDomain: opts.UniverseDomain, }) if err != nil { return nil, err } - } else if opts.Client == nil { - creds = opts.Credentials - client = internal.DefaultClient() - if err := httptransport.AddAuthorizationMiddleware(client, opts.Credentials); err != nil { - return nil, err - } } else { client = opts.Client } + universeDomainProvider := resolveUniverseDomainProvider(creds) itp := impersonatedIDTokenProvider{ - client: client, - targetPrincipal: opts.TargetPrincipal, - audience: opts.Audience, - includeEmail: opts.IncludeEmail, + client: client, + universeDomainProvider: universeDomainProvider, + targetPrincipal: opts.TargetPrincipal, + audience: opts.Audience, + includeEmail: opts.IncludeEmail, } for _, v := range opts.Delegates { itp.delegates = append(itp.delegates, internal.FormatIAMServiceAccountName(v)) } - var udp auth.CredentialsPropertyProvider - if creds != nil { - udp = auth.CredentialsPropertyFunc(creds.UniverseDomain) - } return auth.NewCredentials(&auth.CredentialsOptions{ TokenProvider: auth.NewCachedTokenProvider(itp, nil), - UniverseDomainProvider: udp, + UniverseDomainProvider: universeDomainProvider, }), nil } @@ -142,7 +144,8 @@ type generateIDTokenResponse struct { } type impersonatedIDTokenProvider struct { - client *http.Client + client *http.Client + universeDomainProvider auth.CredentialsPropertyProvider targetPrincipal string audience string @@ -160,8 +163,12 @@ func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, er if err != nil { return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) } - - url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal)) + universeDomain, err := i.universeDomainProvider.GetProperty(ctx) + if err != nil { + return nil, err + } + endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1) + url := fmt.Sprintf("%s/v1/%s:generateIdToken", endpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal)) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) if err != nil { return nil, fmt.Errorf("impersonate: unable to create request: %w", err) diff --git a/auth/credentials/impersonate/impersonate.go b/auth/credentials/impersonate/impersonate.go index eb2f5d1b809e..d99032a2ada5 100644 --- a/auth/credentials/impersonate/impersonate.go +++ b/auth/credentials/impersonate/impersonate.go @@ -32,7 +32,6 @@ import ( var ( universeDomainPlaceholder = "UNIVERSE_DOMAIN" - iamCredentialsEndpoint = "https://iamcredentials.googleapis.com" iamCredentialsUniverseDomainEndpoint = "https://iamcredentials.UNIVERSE_DOMAIN" oauth2Endpoint = "https://oauth2.googleapis.com" errMissingTargetPrincipal = errors.New("impersonate: target service account must be provided") diff --git a/auth/credentials/impersonate/user.go b/auth/credentials/impersonate/user.go index 73180222a5aa..17f44fc53e69 100644 --- a/auth/credentials/impersonate/user.go +++ b/auth/credentials/impersonate/user.go @@ -29,6 +29,10 @@ import ( "cloud.google.com/go/auth/internal" ) +var ( + iamCredentialsEndpoint = "https://iamcredentials.googleapis.com" +) + // user provides an auth flow for domain-wide delegation, setting // CredentialsConfig.Subject to be the impersonated user. func user(opts *CredentialsOptions, client *http.Client, lifetime time.Duration, isStaticToken bool, universeDomainProvider auth.CredentialsPropertyProvider) (auth.TokenProvider, error) { From 17a875216522d42be160042a68b91716baeeb35b Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 30 Oct 2024 13:41:09 -0600 Subject: [PATCH 04/21] add universe domain tests to impersonate/idtoken_test.go --- auth/credentials/impersonate/idtoken_test.go | 83 +++++++++++++++----- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/auth/credentials/impersonate/idtoken_test.go b/auth/credentials/impersonate/idtoken_test.go index 70dfbc8d44eb..717cfa9739b3 100644 --- a/auth/credentials/impersonate/idtoken_test.go +++ b/auth/credentials/impersonate/idtoken_test.go @@ -20,32 +20,67 @@ import ( "encoding/json" "io" "net/http" + "strings" "testing" ) func TestNewIDTokenCredentials(t *testing.T) { ctx := context.Background() tests := []struct { - name string - aud string - targetPrincipal string - wantErr bool + name string + config IDTokenOptions + wantErr bool + wantUniverseDomain string }{ { - name: "missing aud", - targetPrincipal: "foo@project-id.iam.gserviceaccount.com", - wantErr: true, + name: "missing aud", + config: IDTokenOptions{ + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + }, + wantErr: true, }, { - name: "missing targetPrincipal", - aud: "http://example.com/", + name: "missing targetPrincipal", + config: IDTokenOptions{ + Audience: "http://example.com/", + }, wantErr: true, }, { - name: "works", - aud: "http://example.com/", - targetPrincipal: "foo@project-id.iam.gserviceaccount.com", - wantErr: false, + name: "works", + config: IDTokenOptions{ + Audience: "http://example.com/", + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + }, + wantUniverseDomain: "googleapis.com", + }, + { + name: "universe domain from options", + config: IDTokenOptions{ + Audience: "http://example.com/", + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + UniverseDomain: "example.com", + }, + wantUniverseDomain: "googleapis.com", // From creds, not CredentialsOptions.UniverseDomain + }, + { + name: "universe domain from options and credentials", + config: IDTokenOptions{ + Audience: "http://example.com/", + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + UniverseDomain: "NOT.example.com", + Credentials: staticCredentials("example.com"), + }, + wantUniverseDomain: "example.com", // From creds, not CredentialsOptions.UniverseDomain + }, + { + name: "universe domain from credentials", + config: IDTokenOptions{ + Audience: "http://example.com/", + TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", + Credentials: staticCredentials("example.com"), + }, + wantUniverseDomain: "example.com", }, } @@ -64,8 +99,14 @@ func TestNewIDTokenCredentials(t *testing.T) { if err := json.Unmarshal(b, &r); err != nil { t.Error(err) } - if r.Audience != tt.aud { - t.Errorf("got %q, want %q", r.Audience, tt.aud) + if r.Audience != tt.config.Audience { + t.Errorf("got %q, want %q", r.Audience, tt.config.Audience) + } + if !strings.Contains(req.URL.Path, tt.config.TargetPrincipal) { + t.Errorf("got %q, want %q", req.URL.Path, tt.config.TargetPrincipal) + } + if !strings.Contains(req.URL.Hostname(), tt.wantUniverseDomain) { + t.Errorf("got %q, want %q", req.URL.Hostname(), tt.wantUniverseDomain) } resp := generateIDTokenResponse{ @@ -82,12 +123,9 @@ func TestNewIDTokenCredentials(t *testing.T) { } }), } - creds, err := NewIDTokenCredentials(&IDTokenOptions{ - Audience: tt.aud, - TargetPrincipal: tt.targetPrincipal, - Client: client, - }, - ) + opts := &tt.config + opts.Client = client + creds, err := NewIDTokenCredentials(opts) if tt.wantErr && err != nil { return } @@ -101,6 +139,9 @@ func TestNewIDTokenCredentials(t *testing.T) { if tok.Value != idTok { t.Fatalf("got %q, want %q", tok.Value, idTok) } + if got, _ := creds.UniverseDomain(ctx); got != tt.wantUniverseDomain { + t.Errorf("got %q, want %q", got, tt.wantUniverseDomain) + } }) } } From 8baddf929b7153a827e5d79c8804ef3ca337bc22 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 30 Oct 2024 14:01:55 -0600 Subject: [PATCH 05/21] fix idtoken_test --- auth/credentials/impersonate/idtoken_test.go | 36 +++++++++++-------- .../impersonate/impersonate_test.go | 6 ++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/auth/credentials/impersonate/idtoken_test.go b/auth/credentials/impersonate/idtoken_test.go index 717cfa9739b3..c705b317c448 100644 --- a/auth/credentials/impersonate/idtoken_test.go +++ b/auth/credentials/impersonate/idtoken_test.go @@ -108,6 +108,9 @@ func TestNewIDTokenCredentials(t *testing.T) { if !strings.Contains(req.URL.Hostname(), tt.wantUniverseDomain) { t.Errorf("got %q, want %q", req.URL.Hostname(), tt.wantUniverseDomain) } + if !strings.Contains(req.URL.Path, "generateIdToken") { + t.Fatal("path must contain 'generateIdToken'") + } resp := generateIDTokenResponse{ Token: idTok, @@ -126,21 +129,24 @@ func TestNewIDTokenCredentials(t *testing.T) { opts := &tt.config opts.Client = client creds, err := NewIDTokenCredentials(opts) - if tt.wantErr && err != nil { - return - } - if err != nil { - t.Fatal(err) - } - tok, err := creds.Token(ctx) - if err != nil { - t.Fatal(err) - } - if tok.Value != idTok { - t.Fatalf("got %q, want %q", tok.Value, idTok) - } - if got, _ := creds.UniverseDomain(ctx); got != tt.wantUniverseDomain { - t.Errorf("got %q, want %q", got, tt.wantUniverseDomain) + if err != nil && !tt.wantErr { + t.Errorf("err: %v", err) + } else if tt.config.Credentials != nil { + // config.Credentials is invalid for Token request, just assert universe domain. + if got, _ := creds.UniverseDomain(ctx); got != tt.wantUniverseDomain { + t.Errorf("got %q, want %q", got, tt.wantUniverseDomain) + } + } else { + tok, err := creds.Token(ctx) + if err != nil { + t.Error(err) + } + if tok.Value != idTok { + t.Errorf("got %q, want %q", tok.Value, idTok) + } + if got, _ := creds.UniverseDomain(ctx); got != tt.wantUniverseDomain { + t.Errorf("got %q, want %q", got, tt.wantUniverseDomain) + } } }) } diff --git a/auth/credentials/impersonate/impersonate_test.go b/auth/credentials/impersonate/impersonate_test.go index 777b3a1e02c6..4cd3a7f11340 100644 --- a/auth/credentials/impersonate/impersonate_test.go +++ b/auth/credentials/impersonate/impersonate_test.go @@ -115,9 +115,6 @@ func TestNewCredentials_serviceAccount(t *testing.T) { saTok := "sa-token" client := &http.Client{ Transport: RoundTripFn(func(req *http.Request) *http.Response { - if !strings.Contains(req.URL.Path, "generateAccessToken") { - t.Fatal("path must contain 'generateAccessToken'") - } defer req.Body.Close() b, err := io.ReadAll(req.Body) if err != nil { @@ -136,6 +133,9 @@ func TestNewCredentials_serviceAccount(t *testing.T) { if !strings.Contains(req.URL.Hostname(), tt.wantUniverseDomain) { t.Errorf("got %q, want %q", req.URL.Hostname(), tt.wantUniverseDomain) } + if !strings.Contains(req.URL.Path, "generateAccessToken") { + t.Fatal("path must contain 'generateAccessToken'") + } resp := generateAccessTokenResponse{ AccessToken: saTok, From 599a9cefe9aafc84be09b47ef234da4c2927e33b Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 30 Oct 2024 15:25:06 -0600 Subject: [PATCH 06/21] Add idtoken.Options.UniverseDomain --- auth/credentials/idtoken/file.go | 17 ++++------------- auth/credentials/idtoken/iam.go | 3 ++- auth/credentials/idtoken/idtoken.go | 5 +++++ auth/credentials/impersonate/impersonate.go | 3 ++- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/auth/credentials/idtoken/file.go b/auth/credentials/idtoken/file.go index ab92bdffe716..dee47eca3d91 100644 --- a/auth/credentials/idtoken/file.go +++ b/auth/credentials/idtoken/file.go @@ -15,7 +15,6 @@ package idtoken import ( - "context" "encoding/json" "fmt" "net/http" @@ -54,8 +53,7 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) { return nil, err } var tp auth.TokenProvider - universeDomain := resolveUniverseDomain(f) - if universeDomain != internal.DefaultUniverseDomain { + if resolveUniverseDomain(f) != internal.DefaultUniverseDomain { tp, err = newIAMIDTokenProvider(b, f, opts) } else { tp, err = new2LOTokenProvider(f, opts) @@ -91,18 +89,13 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) { if err != nil { return nil, err } - // Hard pull of the provider is OK with file-based creds. - universeDomain, err := baseCreds.UniverseDomain(context.Background()) - if err != nil { - return nil, err - } config := impersonate.IDTokenOptions{ Audience: opts.Audience, TargetPrincipal: account, IncludeEmail: true, Client: opts.client(), Credentials: baseCreds, - UniverseDomain: universeDomain, + UniverseDomain: opts.UniverseDomain, } creds, err := impersonate.NewIDTokenCredentials(&config) if err != nil { @@ -156,20 +149,18 @@ func newIAMIDTokenProvider(b []byte, f *credsfile.ServiceAccountFile, opts *Opti var client *http.Client var creds *auth.Credentials var err error - universeDomain := resolveUniverseDomain(f) if opts.Client == nil { creds, err = credentials.DetectDefault(&credentials.DetectOptions{ CredentialsJSON: b, Scopes: []string{"https://www.googleapis.com/auth/iam"}, UseSelfSignedJWT: true, - UniverseDomain: universeDomain, }) if err != nil { return nil, err } client, err = httptransport.NewClient(&httptransport.Options{ Credentials: creds, - UniverseDomain: universeDomain, + UniverseDomain: opts.UniverseDomain, }) if err != nil { return nil, err @@ -179,7 +170,7 @@ func newIAMIDTokenProvider(b []byte, f *credsfile.ServiceAccountFile, opts *Opti } its := iamIDTokenProvider{ client: client, - universeDomain: universeDomain, + universeDomain: resolveUniverseDomain(f), signerEmail: f.ClientEmail, audience: opts.Audience, } diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index b8a220ac3f47..75b0cc2725bd 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -49,7 +49,8 @@ type generateIAMIDTokenResponse struct { // do not have access to the oauth2.googleapis.com/token endpoint, and thus must // use IAM generateIdToken instead. type iamIDTokenProvider struct { - client *http.Client + client *http.Client + // universeDomain is used for endpoint construction. universeDomain string // signerEmail is the service account client email used to form the IAM generateIdToken endpoint. signerEmail string diff --git a/auth/credentials/idtoken/idtoken.go b/auth/credentials/idtoken/idtoken.go index b66c6551e6ee..b4e9a806ecc9 100644 --- a/auth/credentials/idtoken/idtoken.go +++ b/auth/credentials/idtoken/idtoken.go @@ -74,6 +74,11 @@ type Options struct { // when fetching tokens. If provided this should be a fully authenticated // client. Optional. Client *http.Client + // UniverseDomain is the default service domain for a given Cloud universe. + // The default value is "googleapis.com". This is the universe domain + // configured for the client, which will be compared to the universe domain + // that is separately configured for the credentials. Optional. + UniverseDomain string } func (o *Options) client() *http.Client { diff --git a/auth/credentials/impersonate/impersonate.go b/auth/credentials/impersonate/impersonate.go index d99032a2ada5..9d9caf506675 100644 --- a/auth/credentials/impersonate/impersonate.go +++ b/auth/credentials/impersonate/impersonate.go @@ -208,7 +208,8 @@ type generateAccessTokenResponse struct { } type impersonatedTokenProvider struct { - client *http.Client + client *http.Client + // universeDomain is used for endpoint construction. universeDomainProvider auth.CredentialsPropertyProvider targetPrincipal string From 4bdb8fce22f0d7d3e0f0fdd24fe9bb921f97d42c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 30 Oct 2024 16:03:30 -0600 Subject: [PATCH 07/21] fix test error handling --- auth/credentials/impersonate/idtoken_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/auth/credentials/impersonate/idtoken_test.go b/auth/credentials/impersonate/idtoken_test.go index c705b317c448..b1ec4a9afacc 100644 --- a/auth/credentials/impersonate/idtoken_test.go +++ b/auth/credentials/impersonate/idtoken_test.go @@ -129,8 +129,10 @@ func TestNewIDTokenCredentials(t *testing.T) { opts := &tt.config opts.Client = client creds, err := NewIDTokenCredentials(opts) - if err != nil && !tt.wantErr { - t.Errorf("err: %v", err) + if err != nil { + if !tt.wantErr { + t.Errorf("err: %v", err) + } } else if tt.config.Credentials != nil { // config.Credentials is invalid for Token request, just assert universe domain. if got, _ := creds.UniverseDomain(ctx); got != tt.wantUniverseDomain { From 6af13c6f53630eadf67ecac1e6eefdf27a571086 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 31 Oct 2024 09:02:02 -0600 Subject: [PATCH 08/21] refactor idtoken_test.go --- auth/credentials/impersonate/idtoken_test.go | 22 +++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/auth/credentials/impersonate/idtoken_test.go b/auth/credentials/impersonate/idtoken_test.go index b1ec4a9afacc..84ac76d4d575 100644 --- a/auth/credentials/impersonate/idtoken_test.go +++ b/auth/credentials/impersonate/idtoken_test.go @@ -61,7 +61,7 @@ func TestNewIDTokenCredentials(t *testing.T) { TargetPrincipal: "foo@project-id.iam.gserviceaccount.com", UniverseDomain: "example.com", }, - wantUniverseDomain: "googleapis.com", // From creds, not CredentialsOptions.UniverseDomain + wantUniverseDomain: "googleapis.com", // From creds, not IDTokenOptions.UniverseDomain }, { name: "universe domain from options and credentials", @@ -71,7 +71,7 @@ func TestNewIDTokenCredentials(t *testing.T) { UniverseDomain: "NOT.example.com", Credentials: staticCredentials("example.com"), }, - wantUniverseDomain: "example.com", // From creds, not CredentialsOptions.UniverseDomain + wantUniverseDomain: "example.com", // From creds, not IDTokenOptions.UniverseDomain }, { name: "universe domain from credentials", @@ -109,7 +109,7 @@ func TestNewIDTokenCredentials(t *testing.T) { t.Errorf("got %q, want %q", req.URL.Hostname(), tt.wantUniverseDomain) } if !strings.Contains(req.URL.Path, "generateIdToken") { - t.Fatal("path must contain 'generateIdToken'") + t.Error("path must contain 'generateIdToken'") } resp := generateIDTokenResponse{ @@ -133,12 +133,10 @@ func TestNewIDTokenCredentials(t *testing.T) { if !tt.wantErr { t.Errorf("err: %v", err) } - } else if tt.config.Credentials != nil { - // config.Credentials is invalid for Token request, just assert universe domain. - if got, _ := creds.UniverseDomain(ctx); got != tt.wantUniverseDomain { - t.Errorf("got %q, want %q", got, tt.wantUniverseDomain) - } - } else { + return + } + // Static config.Credentials is invalid for Token request, skip. + if tt.config.Credentials != nil { tok, err := creds.Token(ctx) if err != nil { t.Error(err) @@ -146,9 +144,9 @@ func TestNewIDTokenCredentials(t *testing.T) { if tok.Value != idTok { t.Errorf("got %q, want %q", tok.Value, idTok) } - if got, _ := creds.UniverseDomain(ctx); got != tt.wantUniverseDomain { - t.Errorf("got %q, want %q", got, tt.wantUniverseDomain) - } + } + if got, _ := creds.UniverseDomain(ctx); got != tt.wantUniverseDomain { + t.Errorf("got %q, want %q", got, tt.wantUniverseDomain) } }) } From 3a4a039718df54d315e29e873c774e4c1974d381 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 31 Oct 2024 09:27:05 -0600 Subject: [PATCH 09/21] fix idtoken_test.go --- auth/credentials/impersonate/idtoken_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/auth/credentials/impersonate/idtoken_test.go b/auth/credentials/impersonate/idtoken_test.go index 84ac76d4d575..c5948127b4a2 100644 --- a/auth/credentials/impersonate/idtoken_test.go +++ b/auth/credentials/impersonate/idtoken_test.go @@ -126,9 +126,10 @@ func TestNewIDTokenCredentials(t *testing.T) { } }), } - opts := &tt.config - opts.Client = client - creds, err := NewIDTokenCredentials(opts) + if tt.config.Credentials == nil { + tt.config.Client = client + } + creds, err := NewIDTokenCredentials(&tt.config) if err != nil { if !tt.wantErr { t.Errorf("err: %v", err) @@ -136,7 +137,7 @@ func TestNewIDTokenCredentials(t *testing.T) { return } // Static config.Credentials is invalid for Token request, skip. - if tt.config.Credentials != nil { + if tt.config.Credentials == nil { tok, err := creds.Token(ctx) if err != nil { t.Error(err) From eb264cedbcfd9e9e577734cf0627de5097ce3011 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 31 Oct 2024 10:37:43 -0600 Subject: [PATCH 10/21] update idtoken/idtoken_test.go --- auth/credentials/idtoken/idtoken_test.go | 103 ++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/auth/credentials/idtoken/idtoken_test.go b/auth/credentials/idtoken/idtoken_test.go index bc14b1b5c3a6..655ef5c421c9 100644 --- a/auth/credentials/idtoken/idtoken_test.go +++ b/auth/credentials/idtoken/idtoken_test.go @@ -15,12 +15,15 @@ package idtoken import ( + "bytes" "context" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "os" + "strings" "testing" "cloud.google.com/go/auth/internal" @@ -28,6 +31,7 @@ import ( ) func TestNewCredentials_ServiceAccount(t *testing.T) { + ctx := context.Background() wantTok, _ := createRS256JWT(t) b, err := os.ReadFile("../../internal/testdata/sa.json") if err != nil { @@ -58,13 +62,110 @@ func TestNewCredentials_ServiceAccount(t *testing.T) { if err != nil { t.Fatal(err) } - tok, err := creds.Token(context.Background()) + tok, err := creds.Token(ctx) if err != nil { t.Fatalf("tp.Token() = %v", err) } if tok.Value != wantTok { t.Errorf("got %q, want %q", tok.Value, wantTok) } + if got, _ := creds.UniverseDomain(ctx); got != internal.DefaultUniverseDomain { + t.Errorf("got %q, want %q", got, internal.DefaultUniverseDomain) + } +} + +func TestNewCredentials_ServiceAccount_UniverseDomain(t *testing.T) { + wantAudience := "aud" + wantClientEmail := "gopher@fake_project.iam.gserviceaccount.com" + wantUniverseDomain := "example.com" + wantTok := "id-token" + client := &http.Client{ + Transport: RoundTripFn(func(req *http.Request) *http.Response { + defer req.Body.Close() + b, err := io.ReadAll(req.Body) + if err != nil { + t.Error(err) + } + var r generateIAMIDTokenRequest + if err := json.Unmarshal(b, &r); err != nil { + t.Error(err) + } + if r.Audience != wantAudience { + t.Errorf("got %q, want %q", r.Audience, wantAudience) + } + if !r.IncludeEmail { + t.Errorf("got %t, want %t", r.IncludeEmail, false) + } + if !strings.Contains(req.URL.Path, wantClientEmail) { + t.Errorf("got %q, want %q", req.URL.Path, wantClientEmail) + } + if !strings.Contains(req.URL.Hostname(), wantUniverseDomain) { + t.Errorf("got %q, want %q", req.URL.Hostname(), wantUniverseDomain) + } + if !strings.Contains(req.URL.Path, "generateIdToken") { + t.Fatal("path must contain 'generateIdToken'") + } + + resp := generateIAMIDTokenResponse{ + Token: wantTok, + } + b, err = json.Marshal(&resp) + if err != nil { + t.Fatalf("unable to marshal response: %v", err) + } + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader(b)), + Header: http.Header{}, + } + }), + } + + ctx := context.Background() + creds, err := NewCredentials(&Options{ + Audience: wantAudience, + CredentialsFile: "../../internal/testdata/sa_universe_domain.json", + Client: client, + UniverseDomain: wantUniverseDomain, + }) + if err != nil { + t.Fatal(err) + } + tok, err := creds.Token(ctx) + if err != nil { + t.Fatalf("tp.Token() = %v", err) + } + if tok.Value != wantTok { + t.Errorf("got %q, want %q", tok.Value, wantTok) + } + if got, _ := creds.UniverseDomain(ctx); got != wantUniverseDomain { + t.Errorf("got %q, want %q", got, wantUniverseDomain) + } +} + +func TestNewCredentials_ServiceAccount_UniverseDomain_NoClient(t *testing.T) { + wantUniverseDomain := "example.com" + ctx := context.Background() + creds, err := NewCredentials(&Options{ + Audience: "aud", + CredentialsFile: "../../internal/testdata/sa_universe_domain.json", + UniverseDomain: wantUniverseDomain, + }) + if err != nil { + t.Fatal(err) + } + // To test client creation and usage without a mock client, we must expect a failed token request. + _, err = creds.Token(ctx) + if err == nil { + t.Fatal("token call to example.com did not fail") + } + // Assert that the failed token request targeted the universe domain. + if !strings.Contains(err.Error(), wantUniverseDomain) { + t.Errorf("got %q, want %q", err.Error(), wantUniverseDomain) + } + if got, _ := creds.UniverseDomain(ctx); got != wantUniverseDomain { + t.Errorf("got %q, want %q", got, wantUniverseDomain) + } } type mockTransport struct { From 111992e55216ce5ccf1460145bda5ee3312482bb Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 31 Oct 2024 11:01:22 -0600 Subject: [PATCH 11/21] refactor JSON request handling to internal --- auth/credentials/idtoken/iam.go | 20 +++++--------------- auth/credentials/impersonate/idtoken.go | 19 +++++-------------- auth/internal/internal.go | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index 75b0cc2725bd..608de2a226a4 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -15,7 +15,6 @@ package idtoken import ( - "bytes" "context" "encoding/json" "fmt" @@ -62,26 +61,17 @@ func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { Audience: i.audience, IncludeEmail: true, } - bodyBytes, err := json.Marshal(tokenReq) - if err != nil { - return nil, fmt.Errorf("idtoken: unable to marshal request: %w", err) - } - endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, i.universeDomain, 1) url := fmt.Sprintf("%s/v1/%s:generateIdToken", endpoint, internal.FormatIAMServiceAccountName(i.signerEmail)) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) + + bodyBytes, err := json.Marshal(tokenReq) if err != nil { - return nil, fmt.Errorf("idtoken: unable to create request: %w", err) + return nil, fmt.Errorf("idtoken: unable to marshal request: %w", err) } - req.Header.Set("Content-Type", "application/json") - resp, body, err := internal.DoRequest(i.client, req) + body, err := internal.DoJSONRequest(ctx, i.client, url, bodyBytes, "idtoken") if err != nil { - return nil, fmt.Errorf("idtoken: unable to generate ID token: %w", err) + return nil, err } - if c := resp.StatusCode; c < 200 || c > 299 { - return nil, fmt.Errorf("idtoken: status code %d: %s", c, body) - } - var tokenResp generateIAMIDTokenResponse if err := json.Unmarshal(body, &tokenResp); err != nil { return nil, fmt.Errorf("idtoken: unable to parse response: %w", err) diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index 24d43d214ce6..6d872b3d04dd 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -15,7 +15,6 @@ package impersonate import ( - "bytes" "context" "encoding/json" "errors" @@ -159,29 +158,21 @@ func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, er IncludeEmail: i.includeEmail, Delegates: i.delegates, } - bodyBytes, err := json.Marshal(genIDTokenReq) - if err != nil { - return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) - } universeDomain, err := i.universeDomainProvider.GetProperty(ctx) if err != nil { return nil, err } endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1) url := fmt.Sprintf("%s/v1/%s:generateIdToken", endpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal)) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) + + bodyBytes, err := json.Marshal(genIDTokenReq) if err != nil { - return nil, fmt.Errorf("impersonate: unable to create request: %w", err) + return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) } - req.Header.Set("Content-Type", "application/json") - resp, body, err := internal.DoRequest(i.client, req) + body, err := internal.DoJSONRequest(ctx, i.client, url, bodyBytes, "impersonate") if err != nil { - return nil, fmt.Errorf("impersonate: unable to generate ID token: %w", err) - } - if c := resp.StatusCode; c < 200 || c > 299 { - return nil, fmt.Errorf("impersonate: status code %d: %s", c, body) + return nil, err } - var generateIDTokenResp generateIDTokenResponse if err := json.Unmarshal(body, &generateIDTokenResp); err != nil { return nil, fmt.Errorf("impersonate: unable to parse response: %w", err) diff --git a/auth/internal/internal.go b/auth/internal/internal.go index 8e610800fc3a..6c24763dd8d0 100644 --- a/auth/internal/internal.go +++ b/auth/internal/internal.go @@ -15,6 +15,7 @@ package internal import ( + "bytes" "context" "crypto/rsa" "crypto/x509" @@ -141,6 +142,24 @@ func GetProjectID(b []byte, override string) string { return v.Project } +// DoJSONRequest sends the provided JSON bytes with the client. It reads the response +// body, checks the status code for errors, and returns the body. +func DoJSONRequest(ctx context.Context, c *http.Client, url string, bodyBytes []byte, pkg string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) + if err != nil { + return nil, fmt.Errorf("%s: unable to create request: %w", pkg, err) + } + req.Header.Set("Content-Type", "application/json") + resp, body, err := DoRequest(c, req) + if err != nil { + return nil, fmt.Errorf("%s: request to %s failed: %w", pkg, url, err) + } + if c := resp.StatusCode; c < 200 || c > 299 { + return nil, fmt.Errorf("%s: status code %d: %s", pkg, c, body) + } + return body, nil +} + // DoRequest executes the provided req with the client. It reads the response // body, closes it, and returns it. func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) { From 11139ff4950806326cb010d70a8aaf1a6bdc8b2c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 31 Oct 2024 12:30:33 -0600 Subject: [PATCH 12/21] small fixes per codyoss --- auth/credentials/idtoken/file.go | 21 +++++++++------------ auth/credentials/idtoken/iam.go | 2 +- auth/credentials/impersonate/idtoken.go | 17 +++++++---------- auth/internal/internal.go | 4 ++-- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/auth/credentials/idtoken/file.go b/auth/credentials/idtoken/file.go index dee47eca3d91..718a1e0c985d 100644 --- a/auth/credentials/idtoken/file.go +++ b/auth/credentials/idtoken/file.go @@ -17,7 +17,6 @@ package idtoken import ( "encoding/json" "fmt" - "net/http" "path/filepath" "strings" @@ -53,22 +52,21 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) { return nil, err } var tp auth.TokenProvider - if resolveUniverseDomain(f) != internal.DefaultUniverseDomain { - tp, err = newIAMIDTokenProvider(b, f, opts) - } else { + if resolveUniverseDomain(f) == internal.DefaultUniverseDomain { tp, err = new2LOTokenProvider(f, opts) + } else { + tp, err = newIAMIDTokenProvider(b, f, opts) } if err != nil { return nil, err } tp = auth.NewCachedTokenProvider(tp, nil) - creds := auth.NewCredentials(&auth.CredentialsOptions{ + return auth.NewCredentials(&auth.CredentialsOptions{ TokenProvider: tp, JSON: b, ProjectIDProvider: internal.StaticCredentialsProperty(f.ProjectID), UniverseDomainProvider: internal.StaticCredentialsProperty(f.UniverseDomain), - }) - return creds, nil + }), nil case credsfile.ImpersonatedServiceAccountKey, credsfile.ExternalAccountKey: type url struct { ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` @@ -146,10 +144,10 @@ func new2LOTokenProvider(f *credsfile.ServiceAccountFile, opts *Options) (auth.T // do not have access to the oauth2.googleapis.com/token endpoint, and thus must // use IAM generateIdToken instead. func newIAMIDTokenProvider(b []byte, f *credsfile.ServiceAccountFile, opts *Options) (auth.TokenProvider, error) { - var client *http.Client + client := opts.Client var creds *auth.Credentials var err error - if opts.Client == nil { + if client == nil { creds, err = credentials.DetectDefault(&credentials.DetectOptions{ CredentialsJSON: b, Scopes: []string{"https://www.googleapis.com/auth/iam"}, @@ -165,11 +163,10 @@ func newIAMIDTokenProvider(b []byte, f *credsfile.ServiceAccountFile, opts *Opti if err != nil { return nil, err } - } else { - client = opts.Client } its := iamIDTokenProvider{ - client: client, + client: client, + // Pass the credentials universe domain to configure the endpoint. universeDomain: resolveUniverseDomain(f), signerEmail: f.ClientEmail, audience: opts.Audience, diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index 608de2a226a4..b89b59c04e83 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -68,7 +68,7 @@ func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { if err != nil { return nil, fmt.Errorf("idtoken: unable to marshal request: %w", err) } - body, err := internal.DoJSONRequest(ctx, i.client, url, bodyBytes, "idtoken") + body, err := internal.DoJSONRequest(ctx, i.client, url, "POST", bodyBytes, "idtoken") if err != nil { return nil, err } diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index 6d872b3d04dd..ceb46edce713 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -88,11 +88,11 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { if err := opts.validate(); err != nil { return nil, err } - var client *http.Client - var creds *auth.Credentials - if opts.Client == nil { + client := opts.Client + creds := opts.Credentials + if client == nil { var err error - if opts.Credentials == nil { + if creds == nil { creds, err = credentials.DetectDefault(&credentials.DetectOptions{ Scopes: []string{defaultScope}, UseSelfSignedJWT: true, @@ -100,8 +100,6 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { if err != nil { return nil, err } - } else { - creds = opts.Credentials } client, err = httptransport.NewClient(&httptransport.Options{ Credentials: creds, @@ -110,13 +108,12 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { if err != nil { return nil, err } - } else { - client = opts.Client } universeDomainProvider := resolveUniverseDomainProvider(creds) itp := impersonatedIDTokenProvider{ - client: client, + client: client, + // Pass the credentials universe domain provider to configure the endpoint. universeDomainProvider: universeDomainProvider, targetPrincipal: opts.TargetPrincipal, audience: opts.Audience, @@ -169,7 +166,7 @@ func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, er if err != nil { return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) } - body, err := internal.DoJSONRequest(ctx, i.client, url, bodyBytes, "impersonate") + body, err := internal.DoJSONRequest(ctx, i.client, url, "POST", bodyBytes, "impersonate") if err != nil { return nil, err } diff --git a/auth/internal/internal.go b/auth/internal/internal.go index 6c24763dd8d0..327d8c93d7ce 100644 --- a/auth/internal/internal.go +++ b/auth/internal/internal.go @@ -144,8 +144,8 @@ func GetProjectID(b []byte, override string) string { // DoJSONRequest sends the provided JSON bytes with the client. It reads the response // body, checks the status code for errors, and returns the body. -func DoJSONRequest(ctx context.Context, c *http.Client, url string, bodyBytes []byte, pkg string) ([]byte, error) { - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) +func DoJSONRequest(ctx context.Context, c *http.Client, url string, method string, bodyBytes []byte, pkg string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(bodyBytes)) if err != nil { return nil, fmt.Errorf("%s: unable to create request: %w", pkg, err) } From 95ebfa9621e6a0db5767df6de431adaf08761aa8 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 31 Oct 2024 16:13:59 -0600 Subject: [PATCH 13/21] refactor idtoken RPC duplication to internal --- auth/credentials/idtoken/iam.go | 51 +++--------- auth/credentials/idtoken/idtoken_test.go | 5 +- auth/credentials/impersonate/idtoken.go | 55 +++---------- auth/credentials/impersonate/idtoken_test.go | 6 +- auth/credentials/impersonate/impersonate.go | 4 +- auth/credentials/impersonate/user.go | 4 +- .../internal/impersonate/idtoken.go | 78 +++++++++++++++++++ auth/internal/internal.go | 4 +- 8 files changed, 113 insertions(+), 94 deletions(-) create mode 100644 auth/credentials/internal/impersonate/idtoken.go diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index b89b59c04e83..3814c7f61bbe 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -16,30 +16,13 @@ package idtoken import ( "context" - "encoding/json" - "fmt" "net/http" - "strings" - "time" "cloud.google.com/go/auth" "cloud.google.com/go/auth/internal" + "cloud.google.com/go/auth/credentials/internal/impersonate" ) -var ( - universeDomainPlaceholder = "UNIVERSE_DOMAIN" - iamCredentialsUniverseDomainEndpoint = "https://iamcredentials.UNIVERSE_DOMAIN" -) - -type generateIAMIDTokenRequest struct { - Audience string `json:"audience"` - IncludeEmail bool `json:"includeEmail"` -} - -type generateIAMIDTokenResponse struct { - Token string `json:"token"` -} - // iamIDTokenProvider performs an authenticated RPC with the IAM service to // obtain an ID token. The provided client must be fully authenticated and // authorized with the iam.serviceAccountTokenCreator role. @@ -57,28 +40,14 @@ type iamIDTokenProvider struct { } func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { - tokenReq := generateIAMIDTokenRequest{ - Audience: i.audience, - IncludeEmail: true, - } - endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, i.universeDomain, 1) - url := fmt.Sprintf("%s/v1/%s:generateIdToken", endpoint, internal.FormatIAMServiceAccountName(i.signerEmail)) - - bodyBytes, err := json.Marshal(tokenReq) - if err != nil { - return nil, fmt.Errorf("idtoken: unable to marshal request: %w", err) - } - body, err := internal.DoJSONRequest(ctx, i.client, url, "POST", bodyBytes, "idtoken") - if err != nil { - return nil, err - } - var tokenResp generateIAMIDTokenResponse - if err := json.Unmarshal(body, &tokenResp); err != nil { - return nil, fmt.Errorf("idtoken: unable to parse response: %w", err) + opts := impersonate.IDTokenOptions{ + Client: i.client, + UniverseDomain: internal.StaticCredentialsProperty(i.universeDomain), + ServiceAccountEmail: i.signerEmail, + GenerateIDTokenRequest: impersonate.GenerateIDTokenRequest{ + Audience: i.audience, + IncludeEmail: true, + }, } - return &auth.Token{ - Value: tokenResp.Token, - // Generated ID tokens are good for one hour. - Expiry: time.Now().Add(1 * time.Hour), - }, nil + return opts.DoRequest(ctx) } diff --git a/auth/credentials/idtoken/idtoken_test.go b/auth/credentials/idtoken/idtoken_test.go index 655ef5c421c9..209d69b9e774 100644 --- a/auth/credentials/idtoken/idtoken_test.go +++ b/auth/credentials/idtoken/idtoken_test.go @@ -28,6 +28,7 @@ import ( "cloud.google.com/go/auth/internal" "cloud.google.com/go/auth/internal/credsfile" + "cloud.google.com/go/auth/credentials/internal/impersonate" ) func TestNewCredentials_ServiceAccount(t *testing.T) { @@ -86,7 +87,7 @@ func TestNewCredentials_ServiceAccount_UniverseDomain(t *testing.T) { if err != nil { t.Error(err) } - var r generateIAMIDTokenRequest + var r impersonate.GenerateIDTokenRequest if err := json.Unmarshal(b, &r); err != nil { t.Error(err) } @@ -106,7 +107,7 @@ func TestNewCredentials_ServiceAccount_UniverseDomain(t *testing.T) { t.Fatal("path must contain 'generateIdToken'") } - resp := generateIAMIDTokenResponse{ + resp := impersonate.GenerateIDTokenResponse{ Token: wantTok, } b, err = json.Marshal(&resp) diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index ceb46edce713..7d8c7c8e70bf 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -16,15 +16,12 @@ package impersonate import ( "context" - "encoding/json" "errors" - "fmt" "net/http" - "strings" - "time" "cloud.google.com/go/auth" "cloud.google.com/go/auth/credentials" + "cloud.google.com/go/auth/credentials/internal/impersonate" "cloud.google.com/go/auth/httptransport" "cloud.google.com/go/auth/internal" ) @@ -120,7 +117,7 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { includeEmail: opts.IncludeEmail, } for _, v := range opts.Delegates { - itp.delegates = append(itp.delegates, internal.FormatIAMServiceAccountName(v)) + itp.delegates = append(itp.delegates, internal.FormatIAMServiceAccountResource(v)) } return auth.NewCredentials(&auth.CredentialsOptions{ @@ -129,16 +126,6 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { }), nil } -type generateIDTokenRequest struct { - Audience string `json:"audience"` - IncludeEmail bool `json:"includeEmail"` - Delegates []string `json:"delegates,omitempty"` -} - -type generateIDTokenResponse struct { - Token string `json:"token"` -} - type impersonatedIDTokenProvider struct { client *http.Client universeDomainProvider auth.CredentialsPropertyProvider @@ -150,33 +137,15 @@ type impersonatedIDTokenProvider struct { } func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { - genIDTokenReq := generateIDTokenRequest{ - Audience: i.audience, - IncludeEmail: i.includeEmail, - Delegates: i.delegates, - } - universeDomain, err := i.universeDomainProvider.GetProperty(ctx) - if err != nil { - return nil, err - } - endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1) - url := fmt.Sprintf("%s/v1/%s:generateIdToken", endpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal)) - - bodyBytes, err := json.Marshal(genIDTokenReq) - if err != nil { - return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) - } - body, err := internal.DoJSONRequest(ctx, i.client, url, "POST", bodyBytes, "impersonate") - if err != nil { - return nil, err - } - var generateIDTokenResp generateIDTokenResponse - if err := json.Unmarshal(body, &generateIDTokenResp); err != nil { - return nil, fmt.Errorf("impersonate: unable to parse response: %w", err) + opts := impersonate.IDTokenOptions{ + Client: i.client, + UniverseDomain: i.universeDomainProvider, + ServiceAccountEmail: i.targetPrincipal, + GenerateIDTokenRequest: impersonate.GenerateIDTokenRequest{ + Audience: i.audience, + IncludeEmail: i.includeEmail, + Delegates: i.delegates, + }, } - return &auth.Token{ - Value: generateIDTokenResp.Token, - // Generated ID tokens are good for one hour. - Expiry: time.Now().Add(1 * time.Hour), - }, nil + return opts.DoRequest(ctx) } diff --git a/auth/credentials/impersonate/idtoken_test.go b/auth/credentials/impersonate/idtoken_test.go index c5948127b4a2..06fdb552e484 100644 --- a/auth/credentials/impersonate/idtoken_test.go +++ b/auth/credentials/impersonate/idtoken_test.go @@ -22,6 +22,8 @@ import ( "net/http" "strings" "testing" + + "cloud.google.com/go/auth/credentials/internal/impersonate" ) func TestNewIDTokenCredentials(t *testing.T) { @@ -95,7 +97,7 @@ func TestNewIDTokenCredentials(t *testing.T) { if err != nil { t.Error(err) } - var r generateIDTokenRequest + var r impersonate.GenerateIDTokenRequest if err := json.Unmarshal(b, &r); err != nil { t.Error(err) } @@ -112,7 +114,7 @@ func TestNewIDTokenCredentials(t *testing.T) { t.Error("path must contain 'generateIdToken'") } - resp := generateIDTokenResponse{ + resp := impersonate.GenerateIDTokenResponse{ Token: idTok, } b, err = json.Marshal(&resp) diff --git a/auth/credentials/impersonate/impersonate.go b/auth/credentials/impersonate/impersonate.go index 9d9caf506675..933494efceba 100644 --- a/auth/credentials/impersonate/impersonate.go +++ b/auth/credentials/impersonate/impersonate.go @@ -110,7 +110,7 @@ func NewCredentials(opts *CredentialsOptions) (*auth.Credentials, error) { universeDomainProvider: universeDomainProvider, } for _, v := range opts.Delegates { - its.delegates = append(its.delegates, internal.FormatIAMServiceAccountName(v)) + its.delegates = append(its.delegates, internal.FormatIAMServiceAccountResource(v)) } its.scopes = make([]string, len(opts.Scopes)) copy(its.scopes, opts.Scopes) @@ -234,7 +234,7 @@ func (i impersonatedTokenProvider) Token(ctx context.Context) (*auth.Token, erro return nil, err } endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1) - url := fmt.Sprintf("%s/v1/%s:generateAccessToken", endpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal)) + url := fmt.Sprintf("%s/v1/%s:generateAccessToken", endpoint, internal.FormatIAMServiceAccountResource(i.targetPrincipal)) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b)) if err != nil { return nil, fmt.Errorf("impersonate: unable to create request: %w", err) diff --git a/auth/credentials/impersonate/user.go b/auth/credentials/impersonate/user.go index 17f44fc53e69..2eb57802b133 100644 --- a/auth/credentials/impersonate/user.go +++ b/auth/credentials/impersonate/user.go @@ -48,7 +48,7 @@ func user(opts *CredentialsOptions, client *http.Client, lifetime time.Duration, } u.delegates = make([]string, len(opts.Delegates)) for i, v := range opts.Delegates { - u.delegates[i] = internal.FormatIAMServiceAccountName(v) + u.delegates[i] = internal.FormatIAMServiceAccountResource(v) } u.scopes = make([]string, len(opts.Scopes)) copy(u.scopes, opts.Scopes) @@ -143,7 +143,7 @@ func (u userTokenProvider) signJWT(ctx context.Context) (string, error) { if err != nil { return "", fmt.Errorf("impersonate: unable to marshal request: %w", err) } - reqURL := fmt.Sprintf("%s/v1/%s:signJwt", iamCredentialsEndpoint, internal.FormatIAMServiceAccountName(u.targetPrincipal)) + reqURL := fmt.Sprintf("%s/v1/%s:signJwt", iamCredentialsEndpoint, internal.FormatIAMServiceAccountResource(u.targetPrincipal)) req, err := http.NewRequestWithContext(ctx, "POST", reqURL, bytes.NewReader(bodyBytes)) if err != nil { return "", fmt.Errorf("impersonate: unable to create request: %w", err) diff --git a/auth/credentials/internal/impersonate/idtoken.go b/auth/credentials/internal/impersonate/idtoken.go new file mode 100644 index 000000000000..c6ecbe8d3c7f --- /dev/null +++ b/auth/credentials/internal/impersonate/idtoken.go @@ -0,0 +1,78 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package impersonate + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "cloud.google.com/go/auth" + "cloud.google.com/go/auth/internal" +) + +var ( + universeDomainPlaceholder = "UNIVERSE_DOMAIN" + iamCredentialsUniverseDomainEndpoint = "https://iamcredentials.UNIVERSE_DOMAIN" +) + +type IDTokenOptions struct { + Client *http.Client + UniverseDomain auth.CredentialsPropertyProvider + ServiceAccountEmail string + GenerateIDTokenRequest +} + +// GenerateIDTokenRequest holds the request to the IAM generateIdToken RPC. +type GenerateIDTokenRequest struct { + Audience string `json:"audience"` + IncludeEmail bool `json:"includeEmail"` + Delegates []string `json:"delegates,omitempty"` +} + +// GenerateIDTokenResponse holds the response from the IAM generateIdToken RPC. +type GenerateIDTokenResponse struct { + Token string `json:"token"` +} + +func (o IDTokenOptions) DoRequest(ctx context.Context) (*auth.Token, error) { + universeDomain, err := o.UniverseDomain.GetProperty(ctx) + if err != nil { + return nil, err + } + endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1) + url := fmt.Sprintf("%s/v1/%s:generateIdToken", endpoint, internal.FormatIAMServiceAccountResource(o.ServiceAccountEmail)) + + bodyBytes, err := json.Marshal(o.GenerateIDTokenRequest) + if err != nil { + return nil, fmt.Errorf("credentials: unable to marshal request: %w", err) + } + body, err := internal.DoJSONRequest(ctx, o.Client, url, "POST", bodyBytes, "credentials") + if err != nil { + return nil, err + } + var tokenResp GenerateIDTokenResponse + if err := json.Unmarshal(body, &tokenResp); err != nil { + return nil, fmt.Errorf("credentials: unable to parse response: %w", err) + } + return &auth.Token{ + Value: tokenResp.Token, + // Generated ID tokens are good for one hour. + Expiry: time.Now().Add(1 * time.Hour), + }, nil +} diff --git a/auth/internal/internal.go b/auth/internal/internal.go index 327d8c93d7ce..2e3597a9cc8a 100644 --- a/auth/internal/internal.go +++ b/auth/internal/internal.go @@ -234,8 +234,8 @@ func getMetadataUniverseDomain(ctx context.Context) (string, error) { return "", err } -// FormatIAMServiceAccountName sets a service account name in an IAM resource +// FormatIAMServiceAccountResource sets a service account name in an IAM resource // name. -func FormatIAMServiceAccountName(name string) string { +func FormatIAMServiceAccountResource(name string) string { return fmt.Sprintf("projects/-/serviceAccounts/%s", name) } From 8da4df39ffacd77188dd89ed3dd549c5c5602f7a Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 31 Oct 2024 16:19:09 -0600 Subject: [PATCH 14/21] fix vet --- auth/credentials/idtoken/iam.go | 2 +- auth/credentials/idtoken/idtoken_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index 3814c7f61bbe..c04d40573b4d 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -19,8 +19,8 @@ import ( "net/http" "cloud.google.com/go/auth" - "cloud.google.com/go/auth/internal" "cloud.google.com/go/auth/credentials/internal/impersonate" + "cloud.google.com/go/auth/internal" ) // iamIDTokenProvider performs an authenticated RPC with the IAM service to diff --git a/auth/credentials/idtoken/idtoken_test.go b/auth/credentials/idtoken/idtoken_test.go index 209d69b9e774..ee491cd6d65e 100644 --- a/auth/credentials/idtoken/idtoken_test.go +++ b/auth/credentials/idtoken/idtoken_test.go @@ -26,9 +26,9 @@ import ( "strings" "testing" + "cloud.google.com/go/auth/credentials/internal/impersonate" "cloud.google.com/go/auth/internal" "cloud.google.com/go/auth/internal/credsfile" - "cloud.google.com/go/auth/credentials/internal/impersonate" ) func TestNewCredentials_ServiceAccount(t *testing.T) { From 3be948048ab99fe90121fb9321dc3f94e5435479 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 31 Oct 2024 16:58:08 -0600 Subject: [PATCH 15/21] fix docs for vet --- auth/credentials/idtoken/iam.go | 2 +- auth/credentials/impersonate/idtoken.go | 2 +- auth/credentials/internal/impersonate/idtoken.go | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index c04d40573b4d..32ca1a3e6ab5 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -49,5 +49,5 @@ func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { IncludeEmail: true, }, } - return opts.DoRequest(ctx) + return opts.Token(ctx) } diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index 7d8c7c8e70bf..5c45cab1b272 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -147,5 +147,5 @@ func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, er Delegates: i.delegates, }, } - return opts.DoRequest(ctx) + return opts.Token(ctx) } diff --git a/auth/credentials/internal/impersonate/idtoken.go b/auth/credentials/internal/impersonate/idtoken.go index c6ecbe8d3c7f..320a3eaa09c4 100644 --- a/auth/credentials/internal/impersonate/idtoken.go +++ b/auth/credentials/internal/impersonate/idtoken.go @@ -31,6 +31,7 @@ var ( iamCredentialsUniverseDomainEndpoint = "https://iamcredentials.UNIVERSE_DOMAIN" ) +// IDTokenOptions provides configuration for [IDTokenOptions.Token]. type IDTokenOptions struct { Client *http.Client UniverseDomain auth.CredentialsPropertyProvider @@ -50,7 +51,8 @@ type GenerateIDTokenResponse struct { Token string `json:"token"` } -func (o IDTokenOptions) DoRequest(ctx context.Context) (*auth.Token, error) { +// Token call IAM generateIdToken with the configuration provided in [IDTokenOptions]. +func (o IDTokenOptions) Token(ctx context.Context) (*auth.Token, error) { universeDomain, err := o.UniverseDomain.GetProperty(ctx) if err != nil { return nil, err From 24f58a450e6c3080ce11ed85b5c471a952d980c7 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 6 Nov 2024 17:19:09 -0700 Subject: [PATCH 16/21] fix credentials/idtoken/file.go --- auth/credentials/idtoken/file.go | 58 +++++++------------------------- auth/credentials/idtoken/iam.go | 5 ++- 2 files changed, 15 insertions(+), 48 deletions(-) diff --git a/auth/credentials/idtoken/file.go b/auth/credentials/idtoken/file.go index d90d0b68aa17..43c8f854128f 100644 --- a/auth/credentials/idtoken/file.go +++ b/auth/credentials/idtoken/file.go @@ -22,7 +22,6 @@ import ( "cloud.google.com/go/auth" "cloud.google.com/go/auth/credentials/impersonate" - "cloud.google.com/go/auth/httptransport" "cloud.google.com/go/auth/internal" "cloud.google.com/go/auth/internal/credsfile" ) @@ -47,18 +46,25 @@ func credsFromDefault(creds *auth.Credentials, opts *Options) (*auth.Credentials var tp auth.TokenProvider if resolveUniverseDomain(f) == internal.DefaultUniverseDomain { tp, err = new2LOTokenProvider(f, opts) + if err != nil { + return nil, err + } } else { - tp, err = newIAMIDTokenProvider(b, f, opts) - } - if err != nil { - return nil, err + // In case of non-GDU universe domain, use IAM. + tp = iamIDTokenProvider{ + client: opts.client(), + // Pass the credentials universe domain to configure the endpoint. + universeDomain: auth.CredentialsPropertyFunc(creds.UniverseDomain), + signerEmail: f.ClientEmail, + audience: opts.Audience, + } } tp = auth.NewCachedTokenProvider(tp, nil) return auth.NewCredentials(&auth.CredentialsOptions{ TokenProvider: tp, JSON: b, - ProjectIDProvider: internal.StaticCredentialsProperty(f.ProjectID), - UniverseDomainProvider: internal.StaticCredentialsProperty(f.UniverseDomain), + ProjectIDProvider: auth.CredentialsPropertyFunc(creds.ProjectID), + UniverseDomainProvider: auth.CredentialsPropertyFunc(creds.UniverseDomain), }), nil case credsfile.ImpersonatedServiceAccountKey, credsfile.ExternalAccountKey: type url struct { @@ -119,44 +125,6 @@ func new2LOTokenProvider(f *credsfile.ServiceAccountFile, opts *Options) (auth.T return auth.New2LOTokenProvider(opts2LO) } -// newIAMIDTokenProvider creates a TokenProvider that performs an authenticated -// RPC with the IAM service to obtain an ID token. The provided service account -// must have the iam.serviceAccountTokenCreator role. If a fully-authenticated -// client is not provided, the service account must support a self-signed JWT. -// This TokenProvider is primarily intended for use in non-GDU universes, which -// do not have access to the oauth2.googleapis.com/token endpoint, and thus must -// use IAM generateIdToken instead. -func newIAMIDTokenProvider(b []byte, f *credsfile.ServiceAccountFile, opts *Options) (auth.TokenProvider, error) { - client := opts.Client - var creds *auth.Credentials - var err error - if client == nil { - creds, err = credentials.DetectDefault(&credentials.DetectOptions{ - CredentialsJSON: b, - Scopes: []string{"https://www.googleapis.com/auth/iam"}, - UseSelfSignedJWT: true, - }) - if err != nil { - return nil, err - } - client, err = httptransport.NewClient(&httptransport.Options{ - Credentials: creds, - UniverseDomain: opts.UniverseDomain, - }) - if err != nil { - return nil, err - } - } - its := iamIDTokenProvider{ - client: client, - // Pass the credentials universe domain to configure the endpoint. - universeDomain: resolveUniverseDomain(f), - signerEmail: f.ClientEmail, - audience: opts.Audience, - } - return its, nil -} - // resolveUniverseDomain returns the default service domain for a given // Cloud universe. This is the universe domain configured for the credentials, // which will be used in endpoint. diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index 32ca1a3e6ab5..e6dc74224792 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -20,7 +20,6 @@ import ( "cloud.google.com/go/auth" "cloud.google.com/go/auth/credentials/internal/impersonate" - "cloud.google.com/go/auth/internal" ) // iamIDTokenProvider performs an authenticated RPC with the IAM service to @@ -33,7 +32,7 @@ import ( type iamIDTokenProvider struct { client *http.Client // universeDomain is used for endpoint construction. - universeDomain string + universeDomain auth.CredentialsPropertyProvider // signerEmail is the service account client email used to form the IAM generateIdToken endpoint. signerEmail string audience string @@ -42,7 +41,7 @@ type iamIDTokenProvider struct { func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { opts := impersonate.IDTokenOptions{ Client: i.client, - UniverseDomain: internal.StaticCredentialsProperty(i.universeDomain), + UniverseDomain: i.universeDomain, ServiceAccountEmail: i.signerEmail, GenerateIDTokenRequest: impersonate.GenerateIDTokenRequest{ Audience: i.audience, From 3e5e31e9d08fe8f23bfd2b273e42a3c7623fcd26 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 3 Jan 2025 16:00:23 -0700 Subject: [PATCH 17/21] add logging support --- auth/credentials/idtoken/file.go | 1 + auth/credentials/idtoken/iam.go | 3 ++ auth/credentials/idtoken/idtoken_test.go | 2 + auth/credentials/impersonate/idtoken.go | 39 +++---------------- .../internal/impersonate/idtoken.go | 28 ++++++++++--- 5 files changed, 34 insertions(+), 39 deletions(-) diff --git a/auth/credentials/idtoken/file.go b/auth/credentials/idtoken/file.go index a8167bbc6df5..f7966c35f998 100644 --- a/auth/credentials/idtoken/file.go +++ b/auth/credentials/idtoken/file.go @@ -58,6 +58,7 @@ func credsFromDefault(creds *auth.Credentials, opts *Options) (*auth.Credentials universeDomain: auth.CredentialsPropertyFunc(creds.UniverseDomain), signerEmail: f.ClientEmail, audience: opts.Audience, + logger: internallog.New(opts.Logger), } } tp = auth.NewCachedTokenProvider(tp, nil) diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index e6dc74224792..4f491e2816bb 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -16,6 +16,7 @@ package idtoken import ( "context" + "log/slog" "net/http" "cloud.google.com/go/auth" @@ -36,6 +37,7 @@ type iamIDTokenProvider struct { // signerEmail is the service account client email used to form the IAM generateIdToken endpoint. signerEmail string audience string + logger *slog.Logger } func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { @@ -43,6 +45,7 @@ func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { Client: i.client, UniverseDomain: i.universeDomain, ServiceAccountEmail: i.signerEmail, + Logger: i.logger, GenerateIDTokenRequest: impersonate.GenerateIDTokenRequest{ Audience: i.audience, IncludeEmail: true, diff --git a/auth/credentials/idtoken/idtoken_test.go b/auth/credentials/idtoken/idtoken_test.go index fda2f72b918f..8c186f71418c 100644 --- a/auth/credentials/idtoken/idtoken_test.go +++ b/auth/credentials/idtoken/idtoken_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "net/http/httptest" "os" @@ -249,6 +250,7 @@ func TestNewCredentials_ImpersonatedAndExternal(t *testing.T) { "foo": "bar", }, Client: client, + Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), } if tt.file != "" { opts.CredentialsFile = tt.file diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index aafc030cab9f..98bfb6620103 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -17,7 +17,6 @@ package impersonate import ( "context" "errors" - "fmt" "log/slog" "net/http" @@ -111,7 +110,7 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { client, err = httptransport.NewClient(&httptransport.Options{ Credentials: creds, UniverseDomain: opts.UniverseDomain, - Logger: logger, + Logger: logger, }) if err != nil { return nil, err @@ -126,7 +125,7 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { targetPrincipal: opts.TargetPrincipal, audience: opts.Audience, includeEmail: opts.IncludeEmail, - logger: logger, + logger: logger, } for _, v := range opts.Delegates { itp.delegates = append(itp.delegates, internal.FormatIAMServiceAccountResource(v)) @@ -141,7 +140,7 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { type impersonatedIDTokenProvider struct { client *http.Client universeDomainProvider auth.CredentialsPropertyProvider - logger *slog.Logger + logger *slog.Logger targetPrincipal string audience string @@ -152,6 +151,7 @@ type impersonatedIDTokenProvider struct { func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { opts := impersonate.IDTokenOptions{ Client: i.client, + Logger: i.logger, UniverseDomain: i.universeDomainProvider, ServiceAccountEmail: i.targetPrincipal, GenerateIDTokenRequest: impersonate.GenerateIDTokenRequest{ @@ -160,34 +160,5 @@ func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, er Delegates: i.delegates, }, } - bodyBytes, err := json.Marshal(genIDTokenReq) - if err != nil { - return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) - } - - url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, formatIAMServiceAccountName(i.targetPrincipal)) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) - if err != nil { - return nil, fmt.Errorf("impersonate: unable to create request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - i.logger.DebugContext(ctx, "impersonated idtoken request", "request", internallog.HTTPRequest(req, bodyBytes)) - resp, body, err := internal.DoRequest(i.client, req) - if err != nil { - return nil, fmt.Errorf("impersonate: unable to generate ID token: %w", err) - } - i.logger.DebugContext(ctx, "impersonated idtoken response", "response", internallog.HTTPResponse(resp, body)) - if c := resp.StatusCode; c < 200 || c > 299 { - return nil, fmt.Errorf("impersonate: status code %d: %s", c, body) - } - - var generateIDTokenResp generateIDTokenResponse - if err := json.Unmarshal(body, &generateIDTokenResp); err != nil { - return nil, fmt.Errorf("impersonate: unable to parse response: %w", err) - } - return &auth.Token{ - Value: generateIDTokenResp.Token, - // Generated ID tokens are good for one hour. - Expiry: time.Now().Add(1 * time.Hour), - }, nil + return opts.Token(ctx) } diff --git a/auth/credentials/internal/impersonate/idtoken.go b/auth/credentials/internal/impersonate/idtoken.go index 320a3eaa09c4..c5898e862537 100644 --- a/auth/credentials/internal/impersonate/idtoken.go +++ b/auth/credentials/internal/impersonate/idtoken.go @@ -15,15 +15,18 @@ package impersonate import ( + "bytes" "context" "encoding/json" "fmt" + "log/slog" "net/http" "strings" "time" "cloud.google.com/go/auth" "cloud.google.com/go/auth/internal" + "github.com/googleapis/gax-go/v2/internallog" ) var ( @@ -33,7 +36,10 @@ var ( // IDTokenOptions provides configuration for [IDTokenOptions.Token]. type IDTokenOptions struct { - Client *http.Client + // Client is required. + Client *http.Client + // Logger is required. + Logger *slog.Logger UniverseDomain auth.CredentialsPropertyProvider ServiceAccountEmail string GenerateIDTokenRequest @@ -62,15 +68,27 @@ func (o IDTokenOptions) Token(ctx context.Context) (*auth.Token, error) { bodyBytes, err := json.Marshal(o.GenerateIDTokenRequest) if err != nil { - return nil, fmt.Errorf("credentials: unable to marshal request: %w", err) + return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err) } - body, err := internal.DoJSONRequest(ctx, o.Client, url, "POST", bodyBytes, "credentials") + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes)) if err != nil { - return nil, err + return nil, fmt.Errorf("impersonate: unable to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + o.Logger.DebugContext(ctx, "impersonated idtoken request", "request", internallog.HTTPRequest(req, bodyBytes)) + resp, body, err := internal.DoRequest(o.Client, req) + if err != nil { + return nil, fmt.Errorf("impersonate: unable to generate ID token: %w", err) + } + o.Logger.DebugContext(ctx, "impersonated idtoken response", "response", internallog.HTTPResponse(resp, body)) + if c := resp.StatusCode; c < 200 || c > 299 { + return nil, fmt.Errorf("impersonate: status code %d: %s", c, body) } + var tokenResp GenerateIDTokenResponse if err := json.Unmarshal(body, &tokenResp); err != nil { - return nil, fmt.Errorf("credentials: unable to parse response: %w", err) + return nil, fmt.Errorf("impersonate: unable to parse response: %w", err) } return &auth.Token{ Value: tokenResp.Token, From deefd9df97ac10c840181bd1bdbdbff0cb63edea Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 3 Jan 2025 16:25:59 -0700 Subject: [PATCH 18/21] fix header year --- auth/credentials/idtoken/iam.go | 2 +- auth/credentials/internal/impersonate/idtoken.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index 4f491e2816bb..c5ab707fa4d1 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/credentials/internal/impersonate/idtoken.go b/auth/credentials/internal/impersonate/idtoken.go index c5898e862537..ee549400ffad 100644 --- a/auth/credentials/internal/impersonate/idtoken.go +++ b/auth/credentials/internal/impersonate/idtoken.go @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 8a5c39c8179682de71190392a763d464b27767a7 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 6 Jan 2025 12:45:24 -0700 Subject: [PATCH 19/21] remove unused func --- auth/internal/internal.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/auth/internal/internal.go b/auth/internal/internal.go index 84e46a551fb2..6a8eab6eb99f 100644 --- a/auth/internal/internal.go +++ b/auth/internal/internal.go @@ -15,7 +15,6 @@ package internal import ( - "bytes" "context" "crypto" "crypto/x509" @@ -144,24 +143,6 @@ func GetProjectID(b []byte, override string) string { return v.Project } -// DoJSONRequest sends the provided JSON bytes with the client. It reads the response -// body, checks the status code for errors, and returns the body. -func DoJSONRequest(ctx context.Context, c *http.Client, url string, method string, bodyBytes []byte, pkg string) ([]byte, error) { - req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(bodyBytes)) - if err != nil { - return nil, fmt.Errorf("%s: unable to create request: %w", pkg, err) - } - req.Header.Set("Content-Type", "application/json") - resp, body, err := DoRequest(c, req) - if err != nil { - return nil, fmt.Errorf("%s: request to %s failed: %w", pkg, url, err) - } - if c := resp.StatusCode; c < 200 || c > 299 { - return nil, fmt.Errorf("%s: status code %d: %s", pkg, c, body) - } - return body, nil -} - // DoRequest executes the provided req with the client. It reads the response // body, closes it, and returns it. func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) { From 221739ccb79a54c41ba282c2160d848127356abe Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 6 Jan 2025 12:48:43 -0700 Subject: [PATCH 20/21] rename IDTokenOptions to IDTokenIAMOptions --- auth/credentials/idtoken/iam.go | 2 +- auth/credentials/impersonate/idtoken.go | 2 +- auth/credentials/internal/impersonate/idtoken.go | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go index c5ab707fa4d1..0f134b734d6a 100644 --- a/auth/credentials/idtoken/iam.go +++ b/auth/credentials/idtoken/iam.go @@ -41,7 +41,7 @@ type iamIDTokenProvider struct { } func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { - opts := impersonate.IDTokenOptions{ + opts := impersonate.IDTokenIAMOptions{ Client: i.client, UniverseDomain: i.universeDomain, ServiceAccountEmail: i.signerEmail, diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index 98bfb6620103..442b94597b6a 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -149,7 +149,7 @@ type impersonatedIDTokenProvider struct { } func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { - opts := impersonate.IDTokenOptions{ + opts := impersonate.IDTokenIAMOptions{ Client: i.client, Logger: i.logger, UniverseDomain: i.universeDomainProvider, diff --git a/auth/credentials/internal/impersonate/idtoken.go b/auth/credentials/internal/impersonate/idtoken.go index ee549400ffad..864b02cf104c 100644 --- a/auth/credentials/internal/impersonate/idtoken.go +++ b/auth/credentials/internal/impersonate/idtoken.go @@ -34,8 +34,8 @@ var ( iamCredentialsUniverseDomainEndpoint = "https://iamcredentials.UNIVERSE_DOMAIN" ) -// IDTokenOptions provides configuration for [IDTokenOptions.Token]. -type IDTokenOptions struct { +// IDTokenIAMOptions provides configuration for [IDTokenIAMOptions.Token]. +type IDTokenIAMOptions struct { // Client is required. Client *http.Client // Logger is required. @@ -57,8 +57,8 @@ type GenerateIDTokenResponse struct { Token string `json:"token"` } -// Token call IAM generateIdToken with the configuration provided in [IDTokenOptions]. -func (o IDTokenOptions) Token(ctx context.Context) (*auth.Token, error) { +// Token call IAM generateIdToken with the configuration provided in [IDTokenIAMOptions]. +func (o IDTokenIAMOptions) Token(ctx context.Context) (*auth.Token, error) { universeDomain, err := o.UniverseDomain.GetProperty(ctx) if err != nil { return nil, err From 80f0aa43b135df90cf0976d6bdbb6821ec9f4dce Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 6 Jan 2025 16:27:56 -0700 Subject: [PATCH 21/21] refactor to IDTokenIAMOptions as TokenProvider --- auth/credentials/idtoken/file.go | 15 ++++--- auth/credentials/idtoken/iam.go | 55 ------------------------ auth/credentials/idtoken/idtoken_test.go | 2 +- auth/credentials/impersonate/idtoken.go | 54 +++++++---------------- go.work.sum | 8 ++-- 5 files changed, 28 insertions(+), 106 deletions(-) delete mode 100644 auth/credentials/idtoken/iam.go diff --git a/auth/credentials/idtoken/file.go b/auth/credentials/idtoken/file.go index f7966c35f998..87fab751fbdc 100644 --- a/auth/credentials/idtoken/file.go +++ b/auth/credentials/idtoken/file.go @@ -22,6 +22,7 @@ import ( "cloud.google.com/go/auth" "cloud.google.com/go/auth/credentials/impersonate" + intimpersonate "cloud.google.com/go/auth/credentials/internal/impersonate" "cloud.google.com/go/auth/internal" "cloud.google.com/go/auth/internal/credsfile" "github.com/googleapis/gax-go/v2/internallog" @@ -52,13 +53,15 @@ func credsFromDefault(creds *auth.Credentials, opts *Options) (*auth.Credentials } } else { // In case of non-GDU universe domain, use IAM. - tp = iamIDTokenProvider{ - client: opts.client(), + tp = intimpersonate.IDTokenIAMOptions{ + Client: opts.client(), + Logger: internallog.New(opts.Logger), // Pass the credentials universe domain to configure the endpoint. - universeDomain: auth.CredentialsPropertyFunc(creds.UniverseDomain), - signerEmail: f.ClientEmail, - audience: opts.Audience, - logger: internallog.New(opts.Logger), + UniverseDomain: auth.CredentialsPropertyFunc(creds.UniverseDomain), + ServiceAccountEmail: f.ClientEmail, + GenerateIDTokenRequest: intimpersonate.GenerateIDTokenRequest{ + Audience: opts.Audience, + }, } } tp = auth.NewCachedTokenProvider(tp, nil) diff --git a/auth/credentials/idtoken/iam.go b/auth/credentials/idtoken/iam.go deleted file mode 100644 index 0f134b734d6a..000000000000 --- a/auth/credentials/idtoken/iam.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package idtoken - -import ( - "context" - "log/slog" - "net/http" - - "cloud.google.com/go/auth" - "cloud.google.com/go/auth/credentials/internal/impersonate" -) - -// iamIDTokenProvider performs an authenticated RPC with the IAM service to -// obtain an ID token. The provided client must be fully authenticated and -// authorized with the iam.serviceAccountTokenCreator role. -// -// This TokenProvider is primarily intended for use in non-GDU universes, which -// do not have access to the oauth2.googleapis.com/token endpoint, and thus must -// use IAM generateIdToken instead. -type iamIDTokenProvider struct { - client *http.Client - // universeDomain is used for endpoint construction. - universeDomain auth.CredentialsPropertyProvider - // signerEmail is the service account client email used to form the IAM generateIdToken endpoint. - signerEmail string - audience string - logger *slog.Logger -} - -func (i iamIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { - opts := impersonate.IDTokenIAMOptions{ - Client: i.client, - UniverseDomain: i.universeDomain, - ServiceAccountEmail: i.signerEmail, - Logger: i.logger, - GenerateIDTokenRequest: impersonate.GenerateIDTokenRequest{ - Audience: i.audience, - IncludeEmail: true, - }, - } - return opts.Token(ctx) -} diff --git a/auth/credentials/idtoken/idtoken_test.go b/auth/credentials/idtoken/idtoken_test.go index 8c186f71418c..c63d1291d2a7 100644 --- a/auth/credentials/idtoken/idtoken_test.go +++ b/auth/credentials/idtoken/idtoken_test.go @@ -134,7 +134,7 @@ func TestNewCredentials_ServiceAccount_UniverseDomain(t *testing.T) { if r.Audience != wantAudience { t.Errorf("got %q, want %q", r.Audience, wantAudience) } - if !r.IncludeEmail { + if r.IncludeEmail { t.Errorf("got %t, want %t", r.IncludeEmail, false) } if !strings.Contains(req.URL.Path, wantClientEmail) { diff --git a/auth/credentials/impersonate/idtoken.go b/auth/credentials/impersonate/idtoken.go index 442b94597b6a..1b7671f663a5 100644 --- a/auth/credentials/impersonate/idtoken.go +++ b/auth/credentials/impersonate/idtoken.go @@ -15,7 +15,6 @@ package impersonate import ( - "context" "errors" "log/slog" "net/http" @@ -118,47 +117,24 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) { } universeDomainProvider := resolveUniverseDomainProvider(creds) - itp := impersonatedIDTokenProvider{ - client: client, - // Pass the credentials universe domain provider to configure the endpoint. - universeDomainProvider: universeDomainProvider, - targetPrincipal: opts.TargetPrincipal, - audience: opts.Audience, - includeEmail: opts.IncludeEmail, - logger: logger, - } + delegates := make([]string, len(opts.Delegates)) for _, v := range opts.Delegates { - itp.delegates = append(itp.delegates, internal.FormatIAMServiceAccountResource(v)) + delegates = append(delegates, internal.FormatIAMServiceAccountResource(v)) } - - return auth.NewCredentials(&auth.CredentialsOptions{ - TokenProvider: auth.NewCachedTokenProvider(itp, nil), - UniverseDomainProvider: universeDomainProvider, - }), nil -} - -type impersonatedIDTokenProvider struct { - client *http.Client - universeDomainProvider auth.CredentialsPropertyProvider - logger *slog.Logger - - targetPrincipal string - audience string - includeEmail bool - delegates []string -} - -func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) { - opts := impersonate.IDTokenIAMOptions{ - Client: i.client, - Logger: i.logger, - UniverseDomain: i.universeDomainProvider, - ServiceAccountEmail: i.targetPrincipal, + iamOpts := impersonate.IDTokenIAMOptions{ + Client: client, + Logger: logger, + // Pass the credentials universe domain provider to configure the endpoint. + UniverseDomain: universeDomainProvider, + ServiceAccountEmail: opts.TargetPrincipal, GenerateIDTokenRequest: impersonate.GenerateIDTokenRequest{ - Audience: i.audience, - IncludeEmail: i.includeEmail, - Delegates: i.delegates, + Audience: opts.Audience, + IncludeEmail: opts.IncludeEmail, + Delegates: delegates, }, } - return opts.Token(ctx) + return auth.NewCredentials(&auth.CredentialsOptions{ + TokenProvider: auth.NewCachedTokenProvider(iamOpts, nil), + UniverseDomainProvider: universeDomainProvider, + }), nil } diff --git a/go.work.sum b/go.work.sum index 69c20ee9946c..eeaf465ad29b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -5,9 +5,6 @@ cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.18.0 h1:ugYJK/neZQtQeh2jc5xNoDFiMQojlAkoqJMRb7vTu1U= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.18.0/go.mod h1:Xx0VKh7GJ4si3rmElbh19Mejxz68ibWg/J30ZOMrqzU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.23.0/go.mod h1:p2puVVSKjQ84Qb1gzw2XHLs34WQyHTYFZLaVxypAFYs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.0/go.mod h1:p2puVVSKjQ84Qb1gzw2XHLs34WQyHTYFZLaVxypAFYs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= @@ -25,6 +22,7 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.16.12/go.mod h1:b53qpmhHk7mTL2J/tfG6 github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/bazelbuild/rules_go v0.49.0/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= @@ -63,7 +61,6 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= go.opentelemetry.io/contrib/detectors/gcp v1.27.0/go.mod h1:amd+4uZxqJAUx7zI1JvygUtAc2EVWtQeyz8D+3161SQ= -go.opentelemetry.io/contrib/detectors/gcp v1.28.0/go.mod h1:9BIqH22qyHWAiZxQh0whuJygro59z+nbMVuc7ciiGug= go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA= go.opentelemetry.io/otel/bridge/opencensus v0.40.0 h1:pqDiayRhBgoqy1vwnscik+TizcImJ58l053NScJyZso= go.opentelemetry.io/otel/bridge/opencensus v0.40.0/go.mod h1:1NvVHb6tLTe5A9qCYz+eErW0t8iPn4ZfR6tDKcqlGTM= @@ -73,12 +70,12 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Ev go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI= go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= -go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -87,6 +84,7 @@ golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= google.golang.org/api v0.174.0/go.mod h1:aC7tB6j0HR1Nl0ni5ghpx6iLasmAX78Zkh/wgxAAjLg= google.golang.org/genproto v0.0.0-20230725213213-b022f6e96895/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=