Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add universe domain support #2296

Merged
merged 28 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
407b668
feat(option): add universe domain support
quartzmo Dec 8, 2023
f850927
feat(internal): add universe domain support
quartzmo Dec 8, 2023
9e47d5f
feat(transport): add universe domain support
quartzmo Dec 8, 2023
d851dd2
Merge branch 'main' into universe-domain
quartzmo Dec 18, 2023
f327e1d
add ErrUniverseNotSupportedMTLS to cba.go
quartzmo Dec 18, 2023
32c2fe5
remove CL-R8 descriptions from option.go, pending decision on CL-R8
quartzmo Dec 18, 2023
0f3cadd
Merge branch 'main' into universe-domain
quartzmo Dec 21, 2023
f649422
feat(impersonate) add universe domain support
quartzmo Dec 21, 2023
6f9cf15
feat(option) add universe domain support
quartzmo Dec 21, 2023
b93a905
Merge branch 'main' into universe-domain
quartzmo Dec 28, 2023
c3d8d0e
Merge branch 'main' into universe-domain
quartzmo Jan 9, 2024
e453234
use universe domain placeholder UNIVERSE_DOMAIN
quartzmo Jan 9, 2024
21289e8
Merge branch 'main' into universe-domain
quartzmo Jan 10, 2024
b698c14
fix isSelfSignedJWTFlow for AL-8
quartzmo Jan 11, 2024
dfc4c18
Merge branch 'main' into universe-domain
quartzmo Jan 17, 2024
c59f588
restore transport
quartzmo Jan 17, 2024
56e635c
restore option
quartzmo Jan 17, 2024
089f402
Merge branch 'main' into universe-domain
quartzmo Jan 17, 2024
83b34eb
add missing line end to user-account.json
quartzmo Jan 17, 2024
007e93c
add temporary guard against missing DefaultEndpointTemplate in getCli…
quartzmo Jan 17, 2024
b19015b
remove requirements tracking comments
quartzmo Jan 17, 2024
46ea4ed
Merge branch 'main' into universe-domain
quartzmo Jan 18, 2024
b9ed8b4
return nil instead of empty struct from getTransportConfig when err
quartzmo Jan 18, 2024
7fb62d8
rename UniverseDomainNotGDU to IsUniverseDomainGDU and invert logic
quartzmo Jan 18, 2024
82ad920
Update errors to unexported types
quartzmo Jan 18, 2024
15b1c22
move TODO instructions to GH issue
quartzmo Jan 18, 2024
f37a18c
update impersonate test
quartzmo Jan 18, 2024
11c12e6
Merge branch 'main' into universe-domain
quartzmo Jan 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 33 additions & 7 deletions impersonate/impersonate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,27 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"

"golang.org/x/oauth2"
"google.golang.org/api/internal"
"google.golang.org/api/option"
"google.golang.org/api/option/internaloption"
htransport "google.golang.org/api/transport/http"
)

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

// CredentialsConfig for generating impersonated credentials.
Expand Down Expand Up @@ -62,13 +69,13 @@ func defaultClientOptions() []option.ClientOption {
// the base credentials.
func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts ...option.ClientOption) (oauth2.TokenSource, error) {
if config.TargetPrincipal == "" {
return nil, fmt.Errorf("impersonate: a target service account must be provided")
return nil, errMissingTargetPrincipal
}
if len(config.Scopes) == 0 {
return nil, fmt.Errorf("impersonate: scopes must be provided")
return nil, errMissingScopes
}
if config.Lifetime.Hours() > 12 {
return nil, fmt.Errorf("impersonate: max lifetime is 12 hours")
return nil, errLifetimeOverMax
}

var isStaticToken bool
Expand All @@ -86,9 +93,16 @@ func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts
if err != nil {
return nil, err
}
// If a subject is specified a different auth-flow is initiated to
// impersonate as the provided subject (user).
// If a subject is specified a domain-wide delegation auth-flow is initiated
// to impersonate as the provided subject (user).
if config.Subject != "" {
settings, err := newSettings(clientOpts)
if err != nil {
return nil, err
}
if !settings.IsUniverseDomainGDU() {
return nil, errUniverseNotSupportedDomainWideDelegation
}
return user(ctx, config, client, lifetime, isStaticToken)
}

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

func newSettings(opts []option.ClientOption) (*internal.DialSettings, error) {
var o internal.DialSettings
for _, opt := range opts {
opt.Apply(&o)
}
if err := o.Validate(); err != nil {
return nil, err
}

return &o, nil
}

func formatIAMServiceAccountName(name string) string {
return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
}
Expand Down
82 changes: 50 additions & 32 deletions impersonate/impersonate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,48 @@ import (
func TestTokenSource_serviceAccount(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
targetPrincipal string
scopes []string
lifetime time.Duration
wantErr bool
name string
config CredentialsConfig
opts option.ClientOption
wantErr error
}{
{
name: "missing targetPrincipal",
wantErr: true,
wantErr: errMissingTargetPrincipal,
},
{
name: "missing scopes",
targetPrincipal: "foo@project-id.iam.gserviceaccount.com",
wantErr: true,
name: "missing scopes",
config: CredentialsConfig{
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
},
wantErr: errMissingScopes,
},
{
name: "lifetime over max",
targetPrincipal: "foo@project-id.iam.gserviceaccount.com",
scopes: []string{"scope"},
lifetime: 13 * time.Hour,
wantErr: true,
name: "lifetime over max",
config: CredentialsConfig{
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
Scopes: []string{"scope"},
Lifetime: 13 * time.Hour,
},
wantErr: errLifetimeOverMax,
},
{
name: "works",
targetPrincipal: "foo@project-id.iam.gserviceaccount.com",
scopes: []string{"scope"},
wantErr: false,
name: "works",
config: CredentialsConfig{
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
Scopes: []string{"scope"},
},
wantErr: nil,
},
{
name: "universe domain",
config: CredentialsConfig{
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
Scopes: []string{"scope"},
Subject: "admin@example.com",
},
opts: option.WithUniverseDomain("example.com"),
wantErr: errUniverseNotSupportedDomainWideDelegation,
},
}

Expand Down Expand Up @@ -74,23 +89,26 @@ func TestTokenSource_serviceAccount(t *testing.T) {
return nil
}),
}
ts, err := CredentialsTokenSource(ctx, CredentialsConfig{
TargetPrincipal: tt.targetPrincipal,
Scopes: tt.scopes,
Lifetime: tt.lifetime,
}, option.WithHTTPClient(client))
if tt.wantErr && err != nil {
return
opts := []option.ClientOption{
option.WithHTTPClient(client),
}
if err != nil {
t.Fatal(err)
if tt.opts != nil {
opts = append(opts, tt.opts)
}
tok, err := ts.Token()
ts, err := CredentialsTokenSource(ctx, tt.config, opts...)

if err != nil {
t.Fatal(err)
}
if tok.AccessToken != saTok {
t.Fatalf("got %q, want %q", tok.AccessToken, saTok)
if err != tt.wantErr {
t.Fatalf("%s: err: %v", tt.name, err)
}
} else {
tok, err := ts.Token()
if err != nil {
t.Fatal(err)
}
if tok.AccessToken != saTok {
t.Fatalf("got %q, want %q", tok.AccessToken, saTok)
}
}
})
}
Expand Down
2 changes: 2 additions & 0 deletions impersonate/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"golang.org/x/oauth2"
)

// user provides an auth flow for domain-wide delegation, setting
// CredentialsConfig.Subject to be the impersonated user.
func user(ctx context.Context, c CredentialsConfig, client *http.Client, lifetime time.Duration, isStaticToken bool) (oauth2.TokenSource, error) {
u := userTokenSource{
client: client,
Expand Down
26 changes: 20 additions & 6 deletions impersonate/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestTokenSource_user(t *testing.T) {
lifetime time.Duration
subject string
wantErr bool
universeDomain string
}{
{
name: "missing targetPrincipal",
Expand All @@ -50,6 +51,16 @@ func TestTokenSource_user(t *testing.T) {
subject: "admin@example.com",
wantErr: false,
},
{
name: "universeDomain",
targetPrincipal: "foo@project-id.iam.gserviceaccount.com",
scopes: []string{"scope"},
subject: "admin@example.com",
wantErr: true,
// Non-GDU Universe Domain should result in error if
// CredentialsConfig.Subject is present for domain-wide delegation.
universeDomain: "example.com",
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -92,12 +103,15 @@ func TestTokenSource_user(t *testing.T) {
return nil
}),
}
ts, err := CredentialsTokenSource(ctx, CredentialsConfig{
TargetPrincipal: tt.targetPrincipal,
Scopes: tt.scopes,
Lifetime: tt.lifetime,
Subject: tt.subject,
}, option.WithHTTPClient(client))
ts, err := CredentialsTokenSource(ctx,
CredentialsConfig{
TargetPrincipal: tt.targetPrincipal,
Scopes: tt.scopes,
Lifetime: tt.lifetime,
Subject: tt.subject,
},
option.WithHTTPClient(client),
option.WithUniverseDomain(tt.universeDomain))
if tt.wantErr && err != nil {
return
}
Expand Down
25 changes: 22 additions & 3 deletions internal/cba.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ package internal
import (
"context"
"crypto/tls"
"errors"
"net"
"net/url"
"os"
Expand All @@ -53,6 +54,12 @@ const (

// Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false.
googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A"

universeDomainPlaceholder = "UNIVERSE_DOMAIN"
)

var (
errUniverseNotSupportedMTLS = errors.New("mTLS is not supported in any universe other than googleapis.com")
)

// getClientCertificateSourceAndEndpoint is a convenience function that invokes
Expand All @@ -67,6 +74,14 @@ func getClientCertificateSourceAndEndpoint(settings *DialSettings) (cert.Source,
if err != nil {
return nil, "", err
}
// TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359
if settings.Endpoint == "" && !settings.IsUniverseDomainGDU() && settings.DefaultEndpointTemplate != "" {
// TODO(chrisdsmith): https://github.com/googleapis/google-api-go-client/issues/2359
// if settings.DefaultEndpointTemplate == "" {
// return nil, "", errors.New("internaloption.WithDefaultEndpointTemplate is required if option.WithUniverseDomain is not googleapis.com")
// }
endpoint = strings.Replace(settings.DefaultEndpointTemplate, universeDomainPlaceholder, settings.GetUniverseDomain(), 1)
}
return clientCertSource, endpoint, nil
}

Expand All @@ -80,9 +95,7 @@ type transportConfig struct {
func getTransportConfig(settings *DialSettings) (*transportConfig, error) {
clientCertSource, endpoint, err := getClientCertificateSourceAndEndpoint(settings)
if err != nil {
return &transportConfig{
clientCertSource: nil, endpoint: "", s2aAddress: "", s2aMTLSEndpoint: "",
}, err
return nil, err
}
defaultTransportConfig := transportConfig{
clientCertSource: clientCertSource,
Expand All @@ -94,6 +107,9 @@ func getTransportConfig(settings *DialSettings) (*transportConfig, error) {
if !shouldUseS2A(clientCertSource, settings) {
return &defaultTransportConfig, nil
}
if !settings.IsUniverseDomainGDU() {
return nil, errUniverseNotSupportedMTLS
}

s2aMTLSEndpoint := settings.DefaultMTLSEndpoint
// If there is endpoint override, honor it.
Expand Down Expand Up @@ -155,6 +171,9 @@ func getEndpoint(settings *DialSettings, clientCertSource cert.Source) (string,
if settings.Endpoint == "" {
mtlsMode := getMTLSMode()
if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
if !settings.IsUniverseDomainGDU() {
return "", errUniverseNotSupportedMTLS
}
return settings.DefaultMTLSEndpoint, nil
}
return settings.DefaultEndpoint, nil
Expand Down