Skip to content

Commit

Permalink
Merge branch 'main' into chore-clean-go-1.21-residuals
Browse files Browse the repository at this point in the history
  • Loading branch information
codyoss authored Jan 7, 2025
2 parents e506b3d + 72add7e commit 2001223
Show file tree
Hide file tree
Showing 11 changed files with 394 additions and 159 deletions.
84 changes: 57 additions & 27 deletions auth/credentials/idtoken/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -44,38 +45,31 @@ func credsFromDefault(creds *auth.Credentials, opts *Options) (*auth.Credentials
if err != nil {
return nil, err
}
opts2LO := &auth.Options2LO{
Email: f.ClientEmail,
PrivateKey: []byte(f.PrivateKey),
PrivateKeyID: f.PrivateKeyID,
TokenURL: f.TokenURL,
UseIDToken: true,
Logger: internallog.New(opts.Logger),
}
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
var tp auth.TokenProvider
if resolveUniverseDomain(f) == internal.DefaultUniverseDomain {
tp, err = new2LOTokenProvider(f, opts)
if err != nil {
return nil, err
}
} else {
// In case of non-GDU universe domain, use IAM.
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),
ServiceAccountEmail: f.ClientEmail,
GenerateIDTokenRequest: intimpersonate.GenerateIDTokenRequest{
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 {
Expand Down Expand Up @@ -110,3 +104,39 @@ func credsFromDefault(creds *auth.Credentials, opts *Options) (*auth.Credentials
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,
Logger: internallog.New(opts.Logger),
}
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)
}

// 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
}
5 changes: 5 additions & 0 deletions auth/credentials/idtoken/idtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
Expand Down
108 changes: 106 additions & 2 deletions auth/credentials/idtoken/idtoken_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@
package idtoken

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"cloud.google.com/go/auth/credentials/internal/impersonate"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
)
Expand Down Expand Up @@ -66,7 +71,8 @@ func TestNewCredentials_Validate(t *testing.T) {
}
}

func TestNewCredentials_ServiceAccount_NoClient(t *testing.T) {
func TestNewCredentials_ServiceAccount(t *testing.T) {
ctx := context.Background()
wantTok, _ := createRS256JWT(t)
b, err := os.ReadFile("../../internal/testdata/sa.json")
if err != nil {
Expand Down Expand Up @@ -97,13 +103,110 @@ func TestNewCredentials_ServiceAccount_NoClient(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 impersonate.GenerateIDTokenRequest
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 := impersonate.GenerateIDTokenResponse{
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 {
Expand Down Expand Up @@ -147,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
Expand Down
Loading

0 comments on commit 2001223

Please sign in to comment.