Skip to content

Commit

Permalink
feat(impersonate): add universe domain support (#2296)
Browse files Browse the repository at this point in the history
  • Loading branch information
quartzmo committed Jan 18, 2024
1 parent 6e77ef2 commit 6ef1144
Show file tree
Hide file tree
Showing 10 changed files with 450 additions and 65 deletions.
40 changes: 33 additions & 7 deletions impersonate/impersonate.go
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
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
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
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
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

0 comments on commit 6ef1144

Please sign in to comment.