Skip to content

Commit

Permalink
feat(auth): add universe domain to impersonate and endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
quartzmo committed Apr 1, 2024
1 parent dd7c8e5 commit dc9a34b
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 65 deletions.
40 changes: 33 additions & 7 deletions auth/credentials/impersonate/impersonate.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ import (
)

var (
iamCredentialsEndpoint = "https://iamcredentials.googleapis.com"
oauth2Endpoint = "https://oauth2.googleapis.com"
iamCredentialsEndpoint = "https://iamcredentials.googleapis.com"
oauth2Endpoint = "https://oauth2.googleapis.com"
errMissingTargetPrincipal = errors.New("impersonate: 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")
)

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

// 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 opts.Subject != "" {
if !opts.isUniverseDomainGDU() {
return nil, errUniverseNotSupportedDomainWideDelegation
}
tp, err := user(opts, client, lifetime, isStaticToken)
if err != nil {
return nil, err
Expand Down Expand Up @@ -158,24 +166,42 @@ type CredentialsOptions struct {
// when fetching tokens. If provided the client should provide it's own
// credentials at call time. Optional.
Client *http.Client
// UniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com". Optional.
UniverseDomain string
}

func (o *CredentialsOptions) validate() error {
if o == nil {
return errors.New("impersonate: options must be provided")
}
if o.TargetPrincipal == "" {
return errors.New("impersonate: target service account must be provided")
return errMissingTargetPrincipal
}
if len(o.Scopes) == 0 {
return errors.New("impersonate: scopes must be provided")
return errMissingScopes
}
if o.Lifetime.Hours() > 12 {
return errors.New("impersonate: max lifetime is 12 hours")
return errLifetimeOverMax
}
return nil
}

// getUniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com".
func (o *CredentialsOptions) getUniverseDomain() string {
if o.UniverseDomain == "" {
return internal.DefaultUniverseDomain
}
return o.UniverseDomain
}

// isUniverseDomainGDU returns true if the universe domain is the default Google
// universe.
func (o *CredentialsOptions) isUniverseDomainGDU() bool {
return o.getUniverseDomain() == internal.DefaultUniverseDomain
}

func formatIAMServiceAccountName(name string) string {
return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
}
Expand Down
90 changes: 51 additions & 39 deletions auth/credentials/impersonate/impersonate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,38 +25,54 @@ import (
"time"

"github.com/google/go-cmp/cmp"
"google.golang.org/api/option"
)

func TestNewCredentials_serviceAccount(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
targetPrincipal string
scopes []string
lifetime time.Duration
wantErr bool
name string
config CredentialsOptions
opts option.ClientOption
wantErr error
}{
{
name: "missing targetPrincipal",
wantErr: true,
wantErr: errMissingTargetPrincipal,
},
{
name: "missing scopes",
targetPrincipal: "[email protected]",
wantErr: true,
name: "missing scopes",
config: CredentialsOptions{
TargetPrincipal: "[email protected]",
},
wantErr: errMissingScopes,
},
{
name: "lifetime over max",
targetPrincipal: "[email protected]",
scopes: []string{"scope"},
lifetime: 13 * time.Hour,
wantErr: true,
name: "lifetime over max",
config: CredentialsOptions{
TargetPrincipal: "[email protected]",
Scopes: []string{"scope"},
Lifetime: 13 * time.Hour,
},
wantErr: errLifetimeOverMax,
},
{
name: "works",
targetPrincipal: "[email protected]",
scopes: []string{"scope"},
wantErr: false,
name: "works",
config: CredentialsOptions{
TargetPrincipal: "[email protected]",
Scopes: []string{"scope"},
},
wantErr: nil,
},
{
name: "universe domain",
config: CredentialsOptions{
TargetPrincipal: "[email protected]",
Scopes: []string{"scope"},
Subject: "[email protected]",
},
opts: option.WithUniverseDomain("example.com"),
wantErr: errUniverseNotSupportedDomainWideDelegation,
},
}

Expand All @@ -76,11 +92,11 @@ func TestNewCredentials_serviceAccount(t *testing.T) {
if err := json.Unmarshal(b, &r); err != nil {
t.Error(err)
}
if !cmp.Equal(r.Scope, tt.scopes) {
t.Errorf("got %v, want %v", r.Scope, tt.scopes)
if !cmp.Equal(r.Scope, tt.config.Scopes) {
t.Errorf("got %v, want %v", r.Scope, tt.config.Scopes)
}
if !strings.Contains(req.URL.Path, tt.targetPrincipal) {
t.Errorf("got %q, want %q", req.URL.Path, tt.targetPrincipal)
if !strings.Contains(req.URL.Path, tt.config.TargetPrincipal) {
t.Errorf("got %q, want %q", req.URL.Path, tt.config.TargetPrincipal)
}

resp := generateAccessTokenResponse{
Expand All @@ -100,24 +116,20 @@ func TestNewCredentials_serviceAccount(t *testing.T) {
return nil
}),
}
ts, err := NewCredentials(&CredentialsOptions{
TargetPrincipal: tt.targetPrincipal,
Scopes: tt.scopes,
Lifetime: tt.lifetime,
Client: client,
})
if tt.wantErr && err != nil {
return
}
tt.config.Client = client
ts, err := NewCredentials(&tt.config)
if err != nil {
t.Fatal(err)
}
tok, err := ts.Token(ctx)
if err != nil {
t.Fatal(err)
}
if tok.Value != saTok {
t.Fatalf("got %q, want %q", tok.Value, saTok)
if err != tt.wantErr {
t.Fatalf("%s: err: %v", tt.name, err)
}
} else {
tok, err := ts.Token(ctx)
if err != nil {
t.Fatal(err)
}
if tok.Value != saTok {
t.Fatalf("got %q, want %q", tok.Value, saTok)
}
}
})
}
Expand Down
2 changes: 2 additions & 0 deletions auth/credentials/impersonate/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"cloud.google.com/go/auth/internal"
)

// user provides an auth flow for domain-wide delegation, setting
// CredentialsConfig.Subject to be the impersonated user.
func user(opts *CredentialsOptions, client *http.Client, lifetime time.Duration, isStaticToken bool) (auth.TokenProvider, error) {
u := userTokenProvider{
client: client,
Expand Down
12 changes: 12 additions & 0 deletions auth/credentials/impersonate/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func TestNewCredentials_user(t *testing.T) {
lifetime time.Duration
subject string
wantErr bool
universeDomain string
}{
{
name: "missing targetPrincipal",
Expand All @@ -61,6 +62,16 @@ func TestNewCredentials_user(t *testing.T) {
subject: "[email protected]",
wantErr: false,
},
{
name: "universeDomain",
targetPrincipal: "[email protected]",
scopes: []string{"scope"},
subject: "[email protected]",
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 @@ -132,6 +143,7 @@ func TestNewCredentials_user(t *testing.T) {
Lifetime: tt.lifetime,
Subject: tt.subject,
Client: client,
UniverseDomain: tt.universeDomain,
})
if tt.wantErr && err != nil {
return
Expand Down
Loading

0 comments on commit dc9a34b

Please sign in to comment.