Skip to content

Commit 6ef1144

Browse files
authored
feat(impersonate): add universe domain support (#2296)
1 parent 6e77ef2 commit 6ef1144

File tree

10 files changed

+450
-65
lines changed

10 files changed

+450
-65
lines changed

‎impersonate/impersonate.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,27 @@ import (
88
"bytes"
99
"context"
1010
"encoding/json"
11+
"errors"
1112
"fmt"
1213
"io"
1314
"net/http"
1415
"time"
1516

1617
"golang.org/x/oauth2"
18+
"google.golang.org/api/internal"
1719
"google.golang.org/api/option"
1820
"google.golang.org/api/option/internaloption"
1921
htransport "google.golang.org/api/transport/http"
2022
)
2123

2224
var (
23-
iamCredentailsEndpoint = "https://iamcredentials.googleapis.com"
24-
oauth2Endpoint = "https://oauth2.googleapis.com"
25+
iamCredentailsEndpoint = "https://iamcredentials.googleapis.com"
26+
oauth2Endpoint = "https://oauth2.googleapis.com"
27+
errMissingTargetPrincipal = errors.New("impersonate: a target service account must be provided")
28+
errMissingScopes = errors.New("impersonate: scopes must be provided")
29+
errLifetimeOverMax = errors.New("impersonate: max lifetime is 12 hours")
30+
errUniverseNotSupportedDomainWideDelegation = errors.New("impersonate: service account user is configured for the credential. " +
31+
"Domain-wide delegation is not supported in universes other than googleapis.com")
2532
)
2633

2734
// CredentialsConfig for generating impersonated credentials.
@@ -62,13 +69,13 @@ func defaultClientOptions() []option.ClientOption {
6269
// the base credentials.
6370
func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts ...option.ClientOption) (oauth2.TokenSource, error) {
6471
if config.TargetPrincipal == "" {
65-
return nil, fmt.Errorf("impersonate: a target service account must be provided")
72+
return nil, errMissingTargetPrincipal
6673
}
6774
if len(config.Scopes) == 0 {
68-
return nil, fmt.Errorf("impersonate: scopes must be provided")
75+
return nil, errMissingScopes
6976
}
7077
if config.Lifetime.Hours() > 12 {
71-
return nil, fmt.Errorf("impersonate: max lifetime is 12 hours")
78+
return nil, errLifetimeOverMax
7279
}
7380

7481
var isStaticToken bool
@@ -86,9 +93,16 @@ func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts
8693
if err != nil {
8794
return nil, err
8895
}
89-
// If a subject is specified a different auth-flow is initiated to
90-
// impersonate as the provided subject (user).
96+
// If a subject is specified a domain-wide delegation auth-flow is initiated
97+
// to impersonate as the provided subject (user).
9198
if config.Subject != "" {
99+
settings, err := newSettings(clientOpts)
100+
if err != nil {
101+
return nil, err
102+
}
103+
if !settings.IsUniverseDomainGDU() {
104+
return nil, errUniverseNotSupportedDomainWideDelegation
105+
}
92106
return user(ctx, config, client, lifetime, isStaticToken)
93107
}
94108

@@ -113,6 +127,18 @@ func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts
113127
return oauth2.ReuseTokenSource(nil, its), nil
114128
}
115129

130+
func newSettings(opts []option.ClientOption) (*internal.DialSettings, error) {
131+
var o internal.DialSettings
132+
for _, opt := range opts {
133+
opt.Apply(&o)
134+
}
135+
if err := o.Validate(); err != nil {
136+
return nil, err
137+
}
138+
139+
return &o, nil
140+
}
141+
116142
func formatIAMServiceAccountName(name string) string {
117143
return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
118144
}

‎impersonate/impersonate_test.go

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,48 @@ import (
2020
func TestTokenSource_serviceAccount(t *testing.T) {
2121
ctx := context.Background()
2222
tests := []struct {
23-
name string
24-
targetPrincipal string
25-
scopes []string
26-
lifetime time.Duration
27-
wantErr bool
23+
name string
24+
config CredentialsConfig
25+
opts option.ClientOption
26+
wantErr error
2827
}{
2928
{
3029
name: "missing targetPrincipal",
31-
wantErr: true,
30+
wantErr: errMissingTargetPrincipal,
3231
},
3332
{
34-
name: "missing scopes",
35-
targetPrincipal: "foo@project-id.iam.gserviceaccount.com",
36-
wantErr: true,
33+
name: "missing scopes",
34+
config: CredentialsConfig{
35+
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
36+
},
37+
wantErr: errMissingScopes,
3738
},
3839
{
39-
name: "lifetime over max",
40-
targetPrincipal: "foo@project-id.iam.gserviceaccount.com",
41-
scopes: []string{"scope"},
42-
lifetime: 13 * time.Hour,
43-
wantErr: true,
40+
name: "lifetime over max",
41+
config: CredentialsConfig{
42+
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
43+
Scopes: []string{"scope"},
44+
Lifetime: 13 * time.Hour,
45+
},
46+
wantErr: errLifetimeOverMax,
4447
},
4548
{
46-
name: "works",
47-
targetPrincipal: "foo@project-id.iam.gserviceaccount.com",
48-
scopes: []string{"scope"},
49-
wantErr: false,
49+
name: "works",
50+
config: CredentialsConfig{
51+
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
52+
Scopes: []string{"scope"},
53+
},
54+
wantErr: nil,
55+
},
56+
{
57+
name: "universe domain",
58+
config: CredentialsConfig{
59+
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
60+
Scopes: []string{"scope"},
61+
Subject: "admin@example.com",
62+
},
63+
opts: option.WithUniverseDomain("example.com"),
64+
wantErr: errUniverseNotSupportedDomainWideDelegation,
5065
},
5166
}
5267

@@ -74,23 +89,26 @@ func TestTokenSource_serviceAccount(t *testing.T) {
7489
return nil
7590
}),
7691
}
77-
ts, err := CredentialsTokenSource(ctx, CredentialsConfig{
78-
TargetPrincipal: tt.targetPrincipal,
79-
Scopes: tt.scopes,
80-
Lifetime: tt.lifetime,
81-
}, option.WithHTTPClient(client))
82-
if tt.wantErr && err != nil {
83-
return
92+
opts := []option.ClientOption{
93+
option.WithHTTPClient(client),
8494
}
85-
if err != nil {
86-
t.Fatal(err)
95+
if tt.opts != nil {
96+
opts = append(opts, tt.opts)
8797
}
88-
tok, err := ts.Token()
98+
ts, err := CredentialsTokenSource(ctx, tt.config, opts...)
99+
89100
if err != nil {
90-
t.Fatal(err)
91-
}
92-
if tok.AccessToken != saTok {
93-
t.Fatalf("got %q, want %q", tok.AccessToken, saTok)
101+
if err != tt.wantErr {
102+
t.Fatalf("%s: err: %v", tt.name, err)
103+
}
104+
} else {
105+
tok, err := ts.Token()
106+
if err != nil {
107+
t.Fatal(err)
108+
}
109+
if tok.AccessToken != saTok {
110+
t.Fatalf("got %q, want %q", tok.AccessToken, saTok)
111+
}
94112
}
95113
})
96114
}

‎impersonate/user.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"golang.org/x/oauth2"
1919
)
2020

21+
// user provides an auth flow for domain-wide delegation, setting
22+
// CredentialsConfig.Subject to be the impersonated user.
2123
func user(ctx context.Context, c CredentialsConfig, client *http.Client, lifetime time.Duration, isStaticToken bool) (oauth2.TokenSource, error) {
2224
u := userTokenSource{
2325
client: client,

‎impersonate/user_test.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestTokenSource_user(t *testing.T) {
2626
lifetime time.Duration
2727
subject string
2828
wantErr bool
29+
universeDomain string
2930
}{
3031
{
3132
name: "missing targetPrincipal",
@@ -50,6 +51,16 @@ func TestTokenSource_user(t *testing.T) {
5051
subject: "admin@example.com",
5152
wantErr: false,
5253
},
54+
{
55+
name: "universeDomain",
56+
targetPrincipal: "foo@project-id.iam.gserviceaccount.com",
57+
scopes: []string{"scope"},
58+
subject: "admin@example.com",
59+
wantErr: true,
60+
// Non-GDU Universe Domain should result in error if
61+
// CredentialsConfig.Subject is present for domain-wide delegation.
62+
universeDomain: "example.com",
63+
},
5364
}
5465

5566
for _, tt := range tests {
@@ -92,12 +103,15 @@ func TestTokenSource_user(t *testing.T) {
92103
return nil
93104
}),
94105
}
95-
ts, err := CredentialsTokenSource(ctx, CredentialsConfig{
96-
TargetPrincipal: tt.targetPrincipal,
97-
Scopes: tt.scopes,
98-
Lifetime: tt.lifetime,
99-
Subject: tt.subject,
100-
}, option.WithHTTPClient(client))
106+
ts, err := CredentialsTokenSource(ctx,
107+
CredentialsConfig{
108+
TargetPrincipal: tt.targetPrincipal,
109+
Scopes: tt.scopes,
110+
Lifetime: tt.lifetime,
111+
Subject: tt.subject,
112+
},
113+
option.WithHTTPClient(client),
114+
option.WithUniverseDomain(tt.universeDomain))
101115
if tt.wantErr && err != nil {
102116
return
103117
}

‎internal/cba.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ package internal
3535
import (
3636
"context"
3737
"crypto/tls"
38+
"errors"
3839
"net"
3940
"net/url"
4041
"os"
@@ -53,6 +54,12 @@ const (
5354

5455
// Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false.
5556
googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A"
57+
58+
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
59+
)
60+
61+
var (
62+
errUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com")
5663
)
5764

5865
// getClientCertificateSourceAndEndpoint is a convenience function that invokes
@@ -67,6 +74,14 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source,
6774
if err != nil {
6875
return nil, "", err
6976
}
77+
// TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359
78+
if settings.Endpoint == "" && !settings.IsUniverseDomainGDU() && settings.DefaultEndpointTemplate != "" {
79+
// TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359
80+
// if settings.DefaultEndpointTemplate == "" {
81+
// return nil, "", errors.New("internaloption.WithDefaultEndpointTemplate is required if option.WithUniverseDomain is not googleapis.com")
82+
// }
83+
endpoint = strings.Replace(settings.DefaultEndpointTemplate, universeDomainPlaceholder, settings.GetUniverseDomain(), 1)
84+
}
7085
return clientCertSource, endpoint, nil
7186
}
7287

@@ -80,9 +95,7 @@ type transportConfig struct {
8095
func getTransportConfig(settings *DialSettings) (*transportConfig, error) {
8196
clientCertSource, endpoint, err := getClientCertificateSourceAndEndpoint(settings)
8297
if err != nil {
83-
return &transportConfig{
84-
clientCertSource: nil, endpoint: "", s2aAddress: "", s2aMTLSEndpoint: "",
85-
}, err
98+
return nil, err
8699
}
87100
defaultTransportConfig := transportConfig{
88101
clientCertSource: clientCertSource,
@@ -94,6 +107,9 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) {
94107
if !shouldUseS2A(clientCertSource, settings) {
95108
return &defaultTransportConfig, nil
96109
}
110+
if !settings.IsUniverseDomainGDU() {
111+
return nil, errUniverseNotSupportedMTLS
112+
}
97113

98114
s2aMTLSEndpoint := settings.DefaultMTLSEndpoint
99115
// If there is endpoint override, honor it.
@@ -155,6 +171,9 @@ func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string,
155171
if settings.Endpoint == "" {
156172
mtlsMode := getMTLSMode()
157173
if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
174+
if !settings.IsUniverseDomainGDU() {
175+
return "", errUniverseNotSupportedMTLS
176+
}
158177
return settings.DefaultMTLSEndpoint, nil
159178
}
160179
return settings.DefaultEndpoint, nil

0 commit comments

Comments
 (0)