diff --git a/go.mod b/go.mod index 299ec4484..85ac2cbf8 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,6 @@ require ( require ( cloud.google.com/go/compute v1.20.1 // indirect github.com/golang/protobuf v1.5.3 // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/net v0.21.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index cfc19e8cb..ab5659465 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go index 3d4b5532d..ca1f35462 100644 --- a/google/downscope/downscoping.go +++ b/google/downscope/downscoping.go @@ -42,13 +42,16 @@ import ( "io/ioutil" "net/http" "net/url" + "strings" "time" "golang.org/x/oauth2" ) -var ( - identityBindingEndpoint = "https://sts.googleapis.com/v1/token" +const ( + universeDomainPlaceholder = "UNIVERSE_DOMAIN" + identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token" + universeDomainDefault = "googleapis.com" ) type accessBoundary struct { @@ -105,6 +108,18 @@ type DownscopingConfig struct { // access (or set of accesses) that the new token has to a given resource. // There can be a maximum of 10 AccessBoundaryRules. Rules []AccessBoundaryRule + // UniverseDomain is the default service domain for a given Cloud universe. + // The default value is "googleapis.com". Optional. + UniverseDomain string +} + +// identityBindingEndpoint returns the identity binding endpoint with the +// configured universe domain. +func (dc *DownscopingConfig) identityBindingEndpoint() string { + if dc.UniverseDomain == "" { + return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, universeDomainDefault, 1) + } + return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, dc.UniverseDomain, 1) } // A downscopingTokenSource is used to retrieve a downscoped token with restricted @@ -114,6 +129,9 @@ type downscopingTokenSource struct { ctx context.Context // config holds the information necessary to generate a downscoped Token. config DownscopingConfig + // identityBindingEndpoint is the identity binding endpoint with the + // configured universe domain. + identityBindingEndpoint string } // NewTokenSource returns a configured downscopingTokenSource. @@ -135,7 +153,11 @@ func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSo return nil, fmt.Errorf("downscope: all rules must provide at least one permission: %+v", val) } } - return downscopingTokenSource{ctx: ctx, config: conf}, nil + return downscopingTokenSource{ + ctx: ctx, + config: conf, + identityBindingEndpoint: conf.identityBindingEndpoint(), + }, nil } // Token() uses a downscopingTokenSource to generate an oauth2 Token. @@ -171,7 +193,7 @@ func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { form.Add("options", string(b)) myClient := oauth2.NewClient(dts.ctx, nil) - resp, err := myClient.PostForm(identityBindingEndpoint, form) + resp, err := myClient.PostForm(dts.identityBindingEndpoint, form) if err != nil { return nil, fmt.Errorf("unable to generate POST Request %v", err) } diff --git a/google/downscope/downscoping_test.go b/google/downscope/downscoping_test.go index d5adda19c..ecdd98691 100644 --- a/google/downscope/downscoping_test.go +++ b/google/downscope/downscoping_test.go @@ -38,18 +38,43 @@ func Test_DownscopedTokenSource(t *testing.T) { w.Write([]byte(standardRespBody)) })) - new := []AccessBoundaryRule{ + myTok := oauth2.Token{AccessToken: "Mellon"} + tmpSrc := oauth2.StaticTokenSource(&myTok) + rules := []AccessBoundaryRule{ { AvailableResource: "test1", AvailablePermissions: []string{"Perm1", "Perm2"}, }, } - myTok := oauth2.Token{AccessToken: "Mellon"} - tmpSrc := oauth2.StaticTokenSource(&myTok) - dts := downscopingTokenSource{context.Background(), DownscopingConfig{tmpSrc, new}} - identityBindingEndpoint = ts.URL + dts := downscopingTokenSource{ + ctx: context.Background(), + config: DownscopingConfig{ + RootSource: tmpSrc, + Rules: rules, + }, + identityBindingEndpoint: ts.URL, + } _, err := dts.Token() if err != nil { t.Fatalf("NewDownscopedTokenSource failed with error: %v", err) } } + +func Test_DownscopingConfig(t *testing.T) { + tests := []struct { + universeDomain string + want string + }{ + {"", "https://sts.googleapis.com/v1/token"}, + {"googleapis.com", "https://sts.googleapis.com/v1/token"}, + {"example.com", "https://sts.example.com/v1/token"}, + } + for _, tt := range tests { + c := DownscopingConfig{ + UniverseDomain: tt.universeDomain, + } + if got := c.identityBindingEndpoint(); got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + } +} diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go index 6497dc022..843d1c330 100644 --- a/google/internal/externalaccount/executablecredsource.go +++ b/google/internal/externalaccount/executablecredsource.go @@ -19,7 +19,7 @@ import ( "time" ) -var serviceAccountImpersonationRE = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken") +var serviceAccountImpersonationRE = regexp.MustCompile("https://iamcredentials\\..+/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken") const ( executableSupportedMaxVersion = 1 diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go index df8a906b9..18ee049ff 100644 --- a/google/internal/externalaccount/executablecredsource_test.go +++ b/google/internal/externalaccount/executablecredsource_test.go @@ -1021,3 +1021,37 @@ func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) { }) } } + +func TestServiceAccountImpersonationRE(t *testing.T) { + tests := []struct { + name string + serviceAccountImpersonationURL string + want string + }{ + { + name: "universe domain Google Default Universe (GDU) googleapis.com", + serviceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", + want: "test@project.iam.gserviceaccount.com", + }, + { + name: "email does not match", + serviceAccountImpersonationURL: "test@project.iam.gserviceaccount.com", + want: "", + }, + { + name: "universe domain non-GDU", + serviceAccountImpersonationURL: "https://iamcredentials.apis-tpclp.goog/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken", + want: "test@project.iam.gserviceaccount.com", + }, + } + for _, tt := range tests { + matches := serviceAccountImpersonationRE.FindStringSubmatch(tt.serviceAccountImpersonationURL) + if matches == nil { + if tt.want != "" { + t.Errorf("%q: got nil, want %q", tt.name, tt.want) + } + } else if matches[1] != tt.want { + t.Errorf("%q: got %q, want %q", tt.name, matches[1], tt.want) + } + } +}