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(auth): add universe domain to grpctransport and httptransport #9663

Merged
merged 13 commits into from
Apr 2, 2024
Merged
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: "[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]",
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: "[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
Loading