Skip to content

Commit 67d353b

Browse files
authored
feat(auth): add universe domain to grpctransport and httptransport (#9663)
* add universe domain to grpctransport and endpoint * replace deprecated DefaultEndpoint usages with DefaultEndpointTemplate * remove DefaultUniverseDomain usages * fix EXPERIMENTAL_GOOGLE_API_USE_S2A env var detection fixes: #9670
1 parent 8b3aa92 commit 67d353b

20 files changed

+661
-211
lines changed

auth/credentials/detect_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ func TestDefaultCredentials_ExternalAccountAuthorizedUserKey(t *testing.T) {
667667
}
668668

669669
func TestDefaultCredentials_Fails(t *testing.T) {
670-
t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "nothingToSeeHere")
670+
t.Setenv(credsfile.GoogleAppCredsEnvVar, "nothingToSeeHere")
671671
t.Setenv("HOME", "nothingToSeeHere")
672672
t.Setenv("APPDATA", "nothingToSeeHere")
673673
allowOnGCECheck = false
@@ -890,14 +890,14 @@ func TestDefaultCredentials_UniverseDomain(t *testing.T) {
890890
t.Run(tt.name, func(t *testing.T) {
891891
creds, err := DetectDefault(tt.opts)
892892
if err != nil {
893-
t.Fatalf("%s: %v", tt.name, err)
893+
t.Fatalf("%v", err)
894894
}
895895
ud, err := creds.UniverseDomain(ctx)
896896
if err != nil {
897-
t.Fatalf("%s: %v", tt.name, err)
897+
t.Fatal(err)
898898
}
899899
if ud != tt.want {
900-
t.Fatalf("%s: got %q, want %q", tt.name, ud, tt.want)
900+
t.Fatalf("got %q, want %q", ud, tt.want)
901901
}
902902
})
903903
}

auth/credentials/downscope/integration_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@ import (
2525
"cloud.google.com/go/auth"
2626
"cloud.google.com/go/auth/credentials"
2727
"cloud.google.com/go/auth/credentials/downscope"
28+
"cloud.google.com/go/auth/internal/credsfile"
2829
"cloud.google.com/go/auth/internal/testutil"
2930
"cloud.google.com/go/auth/internal/testutil/testgcs"
3031
)
3132

3233
const (
33-
rootTokenScope = "https://www.googleapis.com/auth/cloud-platform"
34-
envServiceAccountFile = "GOOGLE_APPLICATION_CREDENTIALS"
35-
object1 = "cab-first-c45wknuy.txt"
36-
object2 = "cab-second-c45wknuy.txt"
37-
bucket = "dulcet-port-762"
34+
rootTokenScope = "https://www.googleapis.com/auth/cloud-platform"
35+
object1 = "cab-first-c45wknuy.txt"
36+
object2 = "cab-second-c45wknuy.txt"
37+
bucket = "dulcet-port-762"
3838
)
3939

4040
func TestDownscopedToken(t *testing.T) {
4141
testutil.IntegrationTestCheck(t)
4242
creds, err := credentials.DetectDefault(&credentials.DetectOptions{
43-
CredentialsFile: os.Getenv(envServiceAccountFile),
43+
CredentialsFile: os.Getenv(credsfile.GoogleAppCredsEnvVar),
4444
Scopes: []string{rootTokenScope},
4545
})
4646
if err != nil {

auth/credentials/idtoken/integration_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,20 @@ import (
2424

2525
"cloud.google.com/go/auth/credentials/idtoken"
2626
"cloud.google.com/go/auth/httptransport"
27+
"cloud.google.com/go/auth/internal/credsfile"
2728
"cloud.google.com/go/auth/internal/testutil"
2829
)
2930

3031
const (
31-
envCredentialFile = "GOOGLE_APPLICATION_CREDENTIALS"
32-
aud = "http://example.com"
32+
aud = "http://example.com"
3333
)
3434

3535
func TestNewCredentials_CredentialsFile(t *testing.T) {
3636
testutil.IntegrationTestCheck(t)
3737
ctx := context.Background()
3838
ts, err := idtoken.NewCredentials(&idtoken.Options{
3939
Audience: "http://example.com",
40-
CredentialsFile: os.Getenv(envCredentialFile),
40+
CredentialsFile: os.Getenv(credsfile.GoogleAppCredsEnvVar),
4141
})
4242
if err != nil {
4343
t.Fatalf("unable to create credentials: %v", err)
@@ -63,7 +63,7 @@ func TestNewCredentials_CredentialsFile(t *testing.T) {
6363
func TestNewCredentials_CredentialsJSON(t *testing.T) {
6464
testutil.IntegrationTestCheck(t)
6565
ctx := context.Background()
66-
b, err := os.ReadFile(os.Getenv(envCredentialFile))
66+
b, err := os.ReadFile(os.Getenv(credsfile.GoogleAppCredsEnvVar))
6767
if err != nil {
6868
log.Fatal(err)
6969
}

auth/credentials/impersonate/impersonate.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@ import (
3030
)
3131

3232
var (
33-
iamCredentialsEndpoint = "https://iamcredentials.googleapis.com"
34-
oauth2Endpoint = "https://oauth2.googleapis.com"
33+
iamCredentialsEndpoint = "https://iamcredentials.googleapis.com"
34+
oauth2Endpoint = "https://oauth2.googleapis.com"
35+
errMissingTargetPrincipal = errors.New("impersonate: target service account must be provided")
36+
errMissingScopes = errors.New("impersonate: scopes must be provided")
37+
errLifetimeOverMax = errors.New("impersonate: max lifetime is 12 hours")
38+
errUniverseNotSupportedDomainWideDelegation = errors.New("impersonate: service account user is configured for the credential. " +
39+
"Domain-wide delegation is not supported in universes other than googleapis.com")
3540
)
3641

3742
// TODO(codyoss): plumb through base for this and idtoken
@@ -82,9 +87,12 @@ func NewCredentials(opts *CredentialsOptions) (*auth.Credentials, error) {
8287
client = opts.Client
8388
}
8489

85-
// If a subject is specified a different auth-flow is initiated to
86-
// impersonate as the provided subject (user).
90+
// If a subject is specified a domain-wide delegation auth-flow is initiated
91+
// to impersonate as the provided subject (user).
8792
if opts.Subject != "" {
93+
if !opts.isUniverseDomainGDU() {
94+
return nil, errUniverseNotSupportedDomainWideDelegation
95+
}
8896
tp, err := user(opts, client, lifetime, isStaticToken)
8997
if err != nil {
9098
return nil, err
@@ -158,24 +166,42 @@ type CredentialsOptions struct {
158166
// when fetching tokens. If provided the client should provide it's own
159167
// credentials at call time. Optional.
160168
Client *http.Client
169+
// UniverseDomain is the default service domain for a given Cloud universe.
170+
// The default value is "googleapis.com". Optional.
171+
UniverseDomain string
161172
}
162173

163174
func (o *CredentialsOptions) validate() error {
164175
if o == nil {
165176
return errors.New("impersonate: options must be provided")
166177
}
167178
if o.TargetPrincipal == "" {
168-
return errors.New("impersonate: target service account must be provided")
179+
return errMissingTargetPrincipal
169180
}
170181
if len(o.Scopes) == 0 {
171-
return errors.New("impersonate: scopes must be provided")
182+
return errMissingScopes
172183
}
173184
if o.Lifetime.Hours() > 12 {
174-
return errors.New("impersonate: max lifetime is 12 hours")
185+
return errLifetimeOverMax
175186
}
176187
return nil
177188
}
178189

190+
// getUniverseDomain is the default service domain for a given Cloud universe.
191+
// The default value is "googleapis.com".
192+
func (o *CredentialsOptions) getUniverseDomain() string {
193+
if o.UniverseDomain == "" {
194+
return internal.DefaultUniverseDomain
195+
}
196+
return o.UniverseDomain
197+
}
198+
199+
// isUniverseDomainGDU returns true if the universe domain is the default Google
200+
// universe.
201+
func (o *CredentialsOptions) isUniverseDomainGDU() bool {
202+
return o.getUniverseDomain() == internal.DefaultUniverseDomain
203+
}
204+
179205
func formatIAMServiceAccountName(name string) string {
180206
return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
181207
}

auth/credentials/impersonate/impersonate_test.go

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -30,33 +30,47 @@ import (
3030
func TestNewCredentials_serviceAccount(t *testing.T) {
3131
ctx := context.Background()
3232
tests := []struct {
33-
name string
34-
targetPrincipal string
35-
scopes []string
36-
lifetime time.Duration
37-
wantErr bool
33+
name string
34+
config CredentialsOptions
35+
wantErr error
3836
}{
3937
{
4038
name: "missing targetPrincipal",
41-
wantErr: true,
39+
wantErr: errMissingTargetPrincipal,
4240
},
4341
{
44-
name: "missing scopes",
45-
targetPrincipal: "[email protected]",
46-
wantErr: true,
42+
name: "missing scopes",
43+
config: CredentialsOptions{
44+
TargetPrincipal: "[email protected]",
45+
},
46+
wantErr: errMissingScopes,
4747
},
4848
{
49-
name: "lifetime over max",
50-
targetPrincipal: "[email protected]",
51-
scopes: []string{"scope"},
52-
lifetime: 13 * time.Hour,
53-
wantErr: true,
49+
name: "lifetime over max",
50+
config: CredentialsOptions{
51+
TargetPrincipal: "[email protected]",
52+
Scopes: []string{"scope"},
53+
Lifetime: 13 * time.Hour,
54+
},
55+
wantErr: errLifetimeOverMax,
5456
},
5557
{
56-
name: "works",
57-
targetPrincipal: "[email protected]",
58-
scopes: []string{"scope"},
59-
wantErr: false,
58+
name: "works",
59+
config: CredentialsOptions{
60+
TargetPrincipal: "[email protected]",
61+
Scopes: []string{"scope"},
62+
},
63+
wantErr: nil,
64+
},
65+
{
66+
name: "universe domain",
67+
config: CredentialsOptions{
68+
TargetPrincipal: "[email protected]",
69+
Scopes: []string{"scope"},
70+
Subject: "[email protected]",
71+
UniverseDomain: "example.com",
72+
},
73+
wantErr: errUniverseNotSupportedDomainWideDelegation,
6074
},
6175
}
6276

@@ -76,11 +90,11 @@ func TestNewCredentials_serviceAccount(t *testing.T) {
7690
if err := json.Unmarshal(b, &r); err != nil {
7791
t.Error(err)
7892
}
79-
if !cmp.Equal(r.Scope, tt.scopes) {
80-
t.Errorf("got %v, want %v", r.Scope, tt.scopes)
93+
if !cmp.Equal(r.Scope, tt.config.Scopes) {
94+
t.Errorf("got %v, want %v", r.Scope, tt.config.Scopes)
8195
}
82-
if !strings.Contains(req.URL.Path, tt.targetPrincipal) {
83-
t.Errorf("got %q, want %q", req.URL.Path, tt.targetPrincipal)
96+
if !strings.Contains(req.URL.Path, tt.config.TargetPrincipal) {
97+
t.Errorf("got %q, want %q", req.URL.Path, tt.config.TargetPrincipal)
8498
}
8599

86100
resp := generateAccessTokenResponse{
@@ -100,24 +114,20 @@ func TestNewCredentials_serviceAccount(t *testing.T) {
100114
return nil
101115
}),
102116
}
103-
ts, err := NewCredentials(&CredentialsOptions{
104-
TargetPrincipal: tt.targetPrincipal,
105-
Scopes: tt.scopes,
106-
Lifetime: tt.lifetime,
107-
Client: client,
108-
})
109-
if tt.wantErr && err != nil {
110-
return
111-
}
112-
if err != nil {
113-
t.Fatal(err)
114-
}
115-
tok, err := ts.Token(ctx)
117+
tt.config.Client = client
118+
ts, err := NewCredentials(&tt.config)
116119
if err != nil {
117-
t.Fatal(err)
118-
}
119-
if tok.Value != saTok {
120-
t.Fatalf("got %q, want %q", tok.Value, saTok)
120+
if err != tt.wantErr {
121+
t.Fatalf("err: %v", err)
122+
}
123+
} else {
124+
tok, err := ts.Token(ctx)
125+
if err != nil {
126+
t.Fatal(err)
127+
}
128+
if tok.Value != saTok {
129+
t.Fatalf("got %q, want %q", tok.Value, saTok)
130+
}
121131
}
122132
})
123133
}
@@ -126,3 +136,45 @@ func TestNewCredentials_serviceAccount(t *testing.T) {
126136
type RoundTripFn func(req *http.Request) *http.Response
127137

128138
func (f RoundTripFn) RoundTrip(req *http.Request) (*http.Response, error) { return f(req), nil }
139+
140+
func TestCredentialsOptions_UniverseDomain(t *testing.T) {
141+
testCases := []struct {
142+
name string
143+
opts *CredentialsOptions
144+
wantUniverseDomain string
145+
wantIsGDU bool
146+
}{
147+
{
148+
name: "empty",
149+
opts: &CredentialsOptions{},
150+
wantUniverseDomain: "googleapis.com",
151+
wantIsGDU: true,
152+
},
153+
{
154+
name: "defaults",
155+
opts: &CredentialsOptions{
156+
UniverseDomain: "googleapis.com",
157+
},
158+
wantUniverseDomain: "googleapis.com",
159+
wantIsGDU: true,
160+
},
161+
{
162+
name: "non-GDU",
163+
opts: &CredentialsOptions{
164+
UniverseDomain: "example.com",
165+
},
166+
wantUniverseDomain: "example.com",
167+
wantIsGDU: false,
168+
},
169+
}
170+
for _, tc := range testCases {
171+
t.Run(tc.name, func(t *testing.T) {
172+
if got := tc.opts.getUniverseDomain(); got != tc.wantUniverseDomain {
173+
t.Errorf("got %v, want %v", got, tc.wantUniverseDomain)
174+
}
175+
if got := tc.opts.isUniverseDomainGDU(); got != tc.wantIsGDU {
176+
t.Errorf("got %v, want %v", got, tc.wantIsGDU)
177+
}
178+
})
179+
}
180+
}

auth/credentials/impersonate/integration_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ import (
2828
"cloud.google.com/go/auth/credentials"
2929
"cloud.google.com/go/auth/credentials/idtoken"
3030
"cloud.google.com/go/auth/credentials/impersonate"
31+
"cloud.google.com/go/auth/internal/credsfile"
3132
"cloud.google.com/go/auth/internal/testutil"
3233
"cloud.google.com/go/auth/internal/testutil/testgcs"
3334
)
3435

3536
const (
36-
envAppCreds = "GOOGLE_APPLICATION_CREDENTIALS"
3737
envProjectID = "GCLOUD_TESTS_GOLANG_PROJECT_ID"
3838
envReaderCreds = "GCLOUD_TESTS_IMPERSONATE_READER_KEY"
3939
envReaderEmail = "GCLOUD_TESTS_IMPERSONATE_READER_EMAIL"
@@ -52,7 +52,7 @@ var (
5252
func TestMain(m *testing.M) {
5353
flag.Parse()
5454
random = rand.New(rand.NewSource(time.Now().UnixNano()))
55-
baseKeyFile = os.Getenv(envAppCreds)
55+
baseKeyFile = os.Getenv(credsfile.GoogleAppCredsEnvVar)
5656
projectID = os.Getenv(envProjectID)
5757
readerKeyFile = os.Getenv(envReaderCreds)
5858
readerEmail = os.Getenv(envReaderEmail)

auth/credentials/impersonate/user.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"cloud.google.com/go/auth/internal"
2929
)
3030

31+
// user provides an auth flow for domain-wide delegation, setting
32+
// CredentialsConfig.Subject to be the impersonated user.
3133
func user(opts *CredentialsOptions, client *http.Client, lifetime time.Duration, isStaticToken bool) (auth.TokenProvider, error) {
3234
u := userTokenProvider{
3335
client: client,

auth/credentials/impersonate/user_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func TestNewCredentials_user(t *testing.T) {
3737
lifetime time.Duration
3838
subject string
3939
wantErr bool
40+
universeDomain string
4041
}{
4142
{
4243
name: "missing targetPrincipal",
@@ -61,6 +62,16 @@ func TestNewCredentials_user(t *testing.T) {
6162
subject: "[email protected]",
6263
wantErr: false,
6364
},
65+
{
66+
name: "universeDomain",
67+
targetPrincipal: "[email protected]",
68+
scopes: []string{"scope"},
69+
subject: "[email protected]",
70+
wantErr: true,
71+
// Non-GDU Universe Domain should result in error if
72+
// CredentialsConfig.Subject is present for domain-wide delegation.
73+
universeDomain: "example.com",
74+
},
6475
}
6576

6677
for _, tt := range tests {
@@ -132,6 +143,7 @@ func TestNewCredentials_user(t *testing.T) {
132143
Lifetime: tt.lifetime,
133144
Subject: tt.subject,
134145
Client: client,
146+
UniverseDomain: tt.universeDomain,
135147
})
136148
if tt.wantErr && err != nil {
137149
return

0 commit comments

Comments
 (0)