Skip to content

Commit

Permalink
feat(auth): add universe domain to grpctransport and httptransport (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
quartzmo authored Apr 2, 2024
1 parent 8b3aa92 commit 67d353b
Show file tree
Hide file tree
Showing 20 changed files with 661 additions and 211 deletions.
8 changes: 4 additions & 4 deletions auth/credentials/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ func TestDefaultCredentials_ExternalAccountAuthorizedUserKey(t *testing.T) {
}

func TestDefaultCredentials_Fails(t *testing.T) {
t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "nothingToSeeHere")
t.Setenv(credsfile.GoogleAppCredsEnvVar, "nothingToSeeHere")
t.Setenv("HOME", "nothingToSeeHere")
t.Setenv("APPDATA", "nothingToSeeHere")
allowOnGCECheck = false
Expand Down Expand Up @@ -890,14 +890,14 @@ func TestDefaultCredentials_UniverseDomain(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
creds, err := DetectDefault(tt.opts)
if err != nil {
t.Fatalf("%s: %v", tt.name, err)
t.Fatalf("%v", err)
}
ud, err := creds.UniverseDomain(ctx)
if err != nil {
t.Fatalf("%s: %v", tt.name, err)
t.Fatal(err)
}
if ud != tt.want {
t.Fatalf("%s: got %q, want %q", tt.name, ud, tt.want)
t.Fatalf("got %q, want %q", ud, tt.want)
}
})
}
Expand Down
12 changes: 6 additions & 6 deletions auth/credentials/downscope/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ import (
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/credentials/downscope"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/auth/internal/testutil"
"cloud.google.com/go/auth/internal/testutil/testgcs"
)

const (
rootTokenScope = "https://www.googleapis.com/auth/cloud-platform"
envServiceAccountFile = "GOOGLE_APPLICATION_CREDENTIALS"
object1 = "cab-first-c45wknuy.txt"
object2 = "cab-second-c45wknuy.txt"
bucket = "dulcet-port-762"
rootTokenScope = "https://www.googleapis.com/auth/cloud-platform"
object1 = "cab-first-c45wknuy.txt"
object2 = "cab-second-c45wknuy.txt"
bucket = "dulcet-port-762"
)

func TestDownscopedToken(t *testing.T) {
testutil.IntegrationTestCheck(t)
creds, err := credentials.DetectDefault(&credentials.DetectOptions{
CredentialsFile: os.Getenv(envServiceAccountFile),
CredentialsFile: os.Getenv(credsfile.GoogleAppCredsEnvVar),
Scopes: []string{rootTokenScope},
})
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions auth/credentials/idtoken/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ import (

"cloud.google.com/go/auth/credentials/idtoken"
"cloud.google.com/go/auth/httptransport"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/auth/internal/testutil"
)

const (
envCredentialFile = "GOOGLE_APPLICATION_CREDENTIALS"
aud = "http://example.com"
aud = "http://example.com"
)

func TestNewCredentials_CredentialsFile(t *testing.T) {
testutil.IntegrationTestCheck(t)
ctx := context.Background()
ts, err := idtoken.NewCredentials(&idtoken.Options{
Audience: "http://example.com",
CredentialsFile: os.Getenv(envCredentialFile),
CredentialsFile: os.Getenv(credsfile.GoogleAppCredsEnvVar),
})
if err != nil {
t.Fatalf("unable to create credentials: %v", err)
Expand All @@ -63,7 +63,7 @@ func TestNewCredentials_CredentialsFile(t *testing.T) {
func TestNewCredentials_CredentialsJSON(t *testing.T) {
testutil.IntegrationTestCheck(t)
ctx := context.Background()
b, err := os.ReadFile(os.Getenv(envCredentialFile))
b, err := os.ReadFile(os.Getenv(credsfile.GoogleAppCredsEnvVar))
if err != nil {
log.Fatal(err)
}
Expand Down
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
130 changes: 91 additions & 39 deletions auth/credentials/impersonate/impersonate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,47 @@ import (
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
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: CredentialsOptions{
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: CredentialsOptions{
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: CredentialsOptions{
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
Scopes: []string{"scope"},
},
wantErr: nil,
},
{
name: "universe domain",
config: CredentialsOptions{
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
Scopes: []string{"scope"},
Subject: "admin@example.com",
UniverseDomain: "example.com",
},
wantErr: errUniverseNotSupportedDomainWideDelegation,
},
}

Expand All @@ -76,11 +90,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 +114,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
}
if err != nil {
t.Fatal(err)
}
tok, err := ts.Token(ctx)
tt.config.Client = client
ts, err := NewCredentials(&tt.config)
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("err: %v", 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 All @@ -126,3 +136,45 @@ func TestNewCredentials_serviceAccount(t *testing.T) {
type RoundTripFn func(req *http.Request) *http.Response

func (f RoundTripFn) RoundTrip(req *http.Request) (*http.Response, error) { return f(req), nil }

func TestCredentialsOptions_UniverseDomain(t *testing.T) {
testCases := []struct {
name string
opts *CredentialsOptions
wantUniverseDomain string
wantIsGDU bool
}{
{
name: "empty",
opts: &CredentialsOptions{},
wantUniverseDomain: "googleapis.com",
wantIsGDU: true,
},
{
name: "defaults",
opts: &CredentialsOptions{
UniverseDomain: "googleapis.com",
},
wantUniverseDomain: "googleapis.com",
wantIsGDU: true,
},
{
name: "non-GDU",
opts: &CredentialsOptions{
UniverseDomain: "example.com",
},
wantUniverseDomain: "example.com",
wantIsGDU: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.opts.getUniverseDomain(); got != tc.wantUniverseDomain {
t.Errorf("got %v, want %v", got, tc.wantUniverseDomain)
}
if got := tc.opts.isUniverseDomainGDU(); got != tc.wantIsGDU {
t.Errorf("got %v, want %v", got, tc.wantIsGDU)
}
})
}
}
4 changes: 2 additions & 2 deletions auth/credentials/impersonate/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ import (
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/credentials/idtoken"
"cloud.google.com/go/auth/credentials/impersonate"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/auth/internal/testutil"
"cloud.google.com/go/auth/internal/testutil/testgcs"
)

const (
envAppCreds = "GOOGLE_APPLICATION_CREDENTIALS"
envProjectID = "GCLOUD_TESTS_GOLANG_PROJECT_ID"
envReaderCreds = "GCLOUD_TESTS_IMPERSONATE_READER_KEY"
envReaderEmail = "GCLOUD_TESTS_IMPERSONATE_READER_EMAIL"
Expand All @@ -52,7 +52,7 @@ var (
func TestMain(m *testing.M) {
flag.Parse()
random = rand.New(rand.NewSource(time.Now().UnixNano()))
baseKeyFile = os.Getenv(envAppCreds)
baseKeyFile = os.Getenv(credsfile.GoogleAppCredsEnvVar)
projectID = os.Getenv(envProjectID)
readerKeyFile = os.Getenv(envReaderCreds)
readerEmail = os.Getenv(envReaderEmail)
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: "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 @@ -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 67d353b

Please sign in to comment.