Skip to content

Commit

Permalink
Support CORS on Direct Response (#8432)
Browse files Browse the repository at this point in the history
* update plugin to set typedPerFilterConfig instead of route.cors/vhost.cors

* update plugin.go

* update route plugin tests

* update route plugin tests

* update vhost tests

* add e2e tests

* add changelog entry

* Adding changelog file to new location

* Deleting changelog file from old location

* Adding changelog file to new location

* Deleting changelog file from old location

---------

Co-authored-by: soloio-bulldozer[bot] <48420018+soloio-bulldozer[bot]@users.noreply.github.com>
Co-authored-by: changelog-bot <changelog-bot>
  • Loading branch information
ben-taussig-solo and soloio-bulldozer[bot] authored Jul 7, 2023
1 parent 35ba257 commit eadaa63
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 63 deletions.
10 changes: 10 additions & 0 deletions changelog/v1.15.0-beta18/cors-on-direct-response.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
changelog:
- type: NON_USER_FACING
description: >
Update CORS plugin to no longer use deprecated configuration options.
- type: NEW_FEATURE
description: >
Add support for CORS on direct response by configuring ResponseHeadersToAdd.
issueLink: https://github.com/solo-io/gloo/issues/7336
resolvesIssue: true

118 changes: 102 additions & 16 deletions projects/gloo/pkg/plugins/cors/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"

envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
Expand All @@ -20,6 +21,7 @@ import (
v1 "github.com/solo-io/gloo/projects/gloo/pkg/api/v1"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/options/cors"
"github.com/solo-io/gloo/projects/gloo/pkg/plugins"
"github.com/solo-io/gloo/projects/gloo/pkg/plugins/pluginutils"
)

var (
Expand Down Expand Up @@ -72,16 +74,29 @@ func (p *plugin) ProcessVirtualHost(
)
}
p.filterRequiredForListener[params.HttpListener] = struct{}{}
out.Cors = &envoy_config_route_v3.CorsPolicy{}
return p.translateCommonUserCorsConfig(params.Ctx, corsPlugin, out.GetCors())
corsPolicy := &envoy_config_cors_v3.CorsPolicy{}
if err := p.translateCommonUserCorsConfig(params.Ctx, corsPlugin, corsPolicy); err != nil {
return err
}

return pluginutils.SetVhostPerFilterConfig(out, wellknown.CORS, corsPolicy)
}

func (p *plugin) ProcessRoute(params plugins.RouteParams, in *v1.Route, out *envoy_config_route_v3.Route) error {
corsPlugin := in.GetOptions().GetCors()
if corsPlugin == nil {
return nil
}
// the cors plugin should only be used on routes that are of type envoyroute.Route_Route

// if the route has a direct response action, the cors filter will not apply headers to the response
// instead, configure ResponseHeadersToAdd on the direct response action
if _, ok := out.GetAction().(*envoy_config_route_v3.Route_DirectResponse); ok &&
!corsPlugin.GetDisableForRoute() {
out.ResponseHeadersToAdd = append(out.GetResponseHeadersToAdd(), getCorsResponseHeadersFromPolicy(corsPlugin)...)
return nil
}

// the cors filter can only be used on routes that are of type envoyroute.Route_Route
if out.GetAction() != nil && out.GetRoute() == nil {
return InvalidRouteActionError
}
Expand All @@ -96,18 +111,19 @@ func (p *plugin) ProcessRoute(params plugins.RouteParams, in *v1.Route, out *env
}

p.filterRequiredForListener[params.HttpListener] = struct{}{}
outRa.Cors = &envoy_config_route_v3.CorsPolicy{}
if err := p.translateCommonUserCorsConfig(params.Ctx, in.GetOptions().GetCors(), outRa.GetCors()); err != nil {
corsPolicy := &envoy_config_cors_v3.CorsPolicy{}
if err := p.translateCommonUserCorsConfig(params.Ctx, in.GetOptions().GetCors(), corsPolicy); err != nil {
return err
}
p.translateRouteSpecificCorsConfig(in.GetOptions().GetCors(), outRa.GetCors())
return nil
p.translateRouteSpecificCorsConfig(in.GetOptions().GetCors(), corsPolicy)

return pluginutils.SetRoutePerFilterConfig(out, wellknown.CORS, corsPolicy)
}

func (p *plugin) translateCommonUserCorsConfig(
ctx context.Context,
in *cors.CorsPolicy,
out *envoy_config_route_v3.CorsPolicy,
out *envoy_config_cors_v3.CorsPolicy,
) error {
if len(in.GetAllowOrigin()) == 0 && len(in.GetAllowOriginRegex()) == 0 {
return fmt.Errorf("must provide at least one of AllowOrigin or AllowOriginRegex")
Expand Down Expand Up @@ -135,16 +151,14 @@ func (p *plugin) translateCommonUserCorsConfig(
// not expecting this to be used
const runtimeKey = "gloo.routeplugin.cors"

func (p *plugin) translateRouteSpecificCorsConfig(in *cors.CorsPolicy, out *envoy_config_route_v3.CorsPolicy) {
func (p *plugin) translateRouteSpecificCorsConfig(in *cors.CorsPolicy, out *envoy_config_cors_v3.CorsPolicy) {
if in.GetDisableForRoute() {
out.EnabledSpecifier = &envoy_config_route_v3.CorsPolicy_FilterEnabled{
FilterEnabled: &envoy_config_core_v3.RuntimeFractionalPercent{
DefaultValue: &envoy_type_v3.FractionalPercent{
Numerator: 0,
Denominator: envoy_type_v3.FractionalPercent_HUNDRED,
},
RuntimeKey: runtimeKey,
out.FilterEnabled = &envoy_config_core_v3.RuntimeFractionalPercent{
DefaultValue: &envoy_type_v3.FractionalPercent{
Numerator: 0,
Denominator: envoy_type_v3.FractionalPercent_HUNDRED,
},
RuntimeKey: runtimeKey,
}
}
}
Expand All @@ -157,3 +171,75 @@ func (p *plugin) HttpFilters(params plugins.Params, listener *v1.HttpListener) (

return []plugins.StagedHttpFilter{plugins.MustNewStagedFilter(wellknown.CORS, &envoy_config_cors_v3.Cors{}, pluginStage)}, nil
}

// convert allowOrigin and allowOriginRegex options to a deduplicated slice of strings
func convertAllowOriginToSlice(corsPolicy *cors.CorsPolicy) []string {
exists := struct{}{}
allowOriginSet := make(map[string]struct{})
for _, origin := range corsPolicy.GetAllowOrigin() {
allowOriginSet[origin] = exists
}
for _, originRegex := range corsPolicy.GetAllowOriginRegex() {
allowOriginSet[originRegex] = exists
}

// concatenate the allow origin set into a string
allowedOrigins := []string{}
for origin := range allowOriginSet {
allowedOrigins = append(allowedOrigins, origin)
}

return allowedOrigins
}

// get response headers to add from cors policy
// this is only used when processing direct response actions, for which
// the cors filter is disabled
func getCorsResponseHeadersFromPolicy(corsPolicy *cors.CorsPolicy) []*envoy_config_core_v3.HeaderValueOption {
allowOriginString := strings.Join(convertAllowOriginToSlice(corsPolicy), ",")

return []*envoy_config_core_v3.HeaderValueOption{
{
Header: &envoy_config_core_v3.HeaderValue{
Key: "Access-Control-Allow-Origin",
Value: allowOriginString,
},
KeepEmptyValue: false,
},
{
Header: &envoy_config_core_v3.HeaderValue{
Key: "Access-Control-Allow-Methods",
Value: strings.Join(corsPolicy.GetAllowMethods(), ","),
},
KeepEmptyValue: false,
},
{
Header: &envoy_config_core_v3.HeaderValue{
Key: "Access-Control-Allow-Headers",
Value: strings.Join(corsPolicy.GetAllowHeaders(), ","),
},
KeepEmptyValue: false,
},
{
Header: &envoy_config_core_v3.HeaderValue{
Key: "Access-Control-Expose-Headers",
Value: strings.Join(corsPolicy.GetExposeHeaders(), ","),
},
KeepEmptyValue: false,
},
{
Header: &envoy_config_core_v3.HeaderValue{
Key: "Access-Control-Max-Age",
Value: corsPolicy.GetMaxAge(),
},
KeepEmptyValue: false,
},
{
Header: &envoy_config_core_v3.HeaderValue{
Key: "Access-Control-Allow-Credentials",
Value: strconv.FormatBool(corsPolicy.GetAllowCredentials()),
},
KeepEmptyValue: false,
},
}
}
54 changes: 31 additions & 23 deletions projects/gloo/pkg/plugins/cors/route_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (

envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
envoy_config_cors_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3"
envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/golang/protobuf/ptypes/any"
"github.com/golang/protobuf/ptypes/wrappers"

"github.com/solo-io/solo-kit/pkg/api/v1/resources/core"

"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/options/cors"
"github.com/solo-io/gloo/projects/gloo/pkg/utils"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -59,7 +62,7 @@ var _ = Describe("Route Plugin", func() {
Route: &envoy_config_route_v3.RouteAction{},
},
}
expected := &envoy_config_route_v3.CorsPolicy{
expected := &envoy_config_cors_v3.CorsPolicy{
AllowOriginStringMatch: []*envoy_type_matcher_v3.StringMatcher{
{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{Exact: allowOrigin1[0]},
Expand Down Expand Up @@ -89,20 +92,23 @@ var _ = Describe("Route Plugin", func() {
ExposeHeaders: strings.Join(exposeHeaders1, ","),
MaxAge: maxAge1,
AllowCredentials: &wrappers.BoolValue{Value: allowCredentials1},
EnabledSpecifier: &envoy_config_route_v3.CorsPolicy_FilterEnabled{
FilterEnabled: &envoy_config_core_v3.RuntimeFractionalPercent{
DefaultValue: &envoy_type_v3.FractionalPercent{
Numerator: 0,
Denominator: envoy_type_v3.FractionalPercent_HUNDRED,
},
RuntimeKey: runtimeKey,
FilterEnabled: &envoy_config_core_v3.RuntimeFractionalPercent{
DefaultValue: &envoy_type_v3.FractionalPercent{
Numerator: 0,
Denominator: envoy_type_v3.FractionalPercent_HUNDRED,
},
RuntimeKey: runtimeKey,
},
}
typedConfig, err := utils.MessageToAny(expected)
Expect(err).NotTo(HaveOccurred())

err := plugin.(plugins.RoutePlugin).ProcessRoute(params, inRoute, outRoute)
err = plugin.(plugins.RoutePlugin).ProcessRoute(params, inRoute, outRoute)
Expect(err).NotTo(HaveOccurred())
Expect(outRoute.Action.(*envoy_config_route_v3.Route_Route).Route.Cors).To(Equal(expected))

outCorsConfig := outRoute.TypedPerFilterConfig["envoy.filters.http.cors"]
Expect(outCorsConfig).NotTo(BeNil())
Expect(outCorsConfig).To(Equal(typedConfig))
})
It("should process minimal specification", func() {
inRoute := routeWithCors(&cors.CorsPolicy{
Expand All @@ -111,7 +117,7 @@ var _ = Describe("Route Plugin", func() {
outRoute := basicEnvoyRoute()
err := plugin.(plugins.RoutePlugin).ProcessRoute(params, inRoute, outRoute)
Expect(err).NotTo(HaveOccurred())
cSpec := &envoy_config_route_v3.CorsPolicy{
cSpec := &envoy_config_cors_v3.CorsPolicy{
AllowOriginStringMatch: []*envoy_type_matcher_v3.StringMatcher{
{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{Exact: allowOrigin1[0]},
Expand All @@ -122,19 +128,20 @@ var _ = Describe("Route Plugin", func() {
},
}
expected := basicEnvoyRouteWithCors(cSpec)
Expect(outRoute.Action.(*envoy_config_route_v3.Route_Route).Route.Cors).To(Equal(cSpec))
Expect(outRoute).To(Equal(expected))

Expect(outRoute.TypedPerFilterConfig).To(HaveKey("envoy.filters.http.cors"))
outCorsConfig := outRoute.TypedPerFilterConfig["envoy.filters.http.cors"]
Expect(outCorsConfig).NotTo(BeNil())
Expect(outRoute.TypedPerFilterConfig).To(Equal(expected.TypedPerFilterConfig))

})
It("should process empty specification", func() {
inRoute := routeWithCors(&cors.CorsPolicy{})
outRoute := basicEnvoyRoute()
err := plugin.(plugins.RoutePlugin).ProcessRoute(params, inRoute, outRoute)
Expect(err).To(HaveOccurred())
cSpec := &envoy_config_route_v3.CorsPolicy{}
expected := basicEnvoyRouteWithCors(cSpec)
Expect(outRoute.Action.(*envoy_config_route_v3.Route_Route).Route.Cors).To(Equal(cSpec))
Expect(outRoute.String()).To(Equal(expected.String()))
Expect(outRoute).To(Equal(expected))

Expect(outRoute.TypedPerFilterConfig).NotTo(HaveKey("envoy.filters.http.cors"))
})
It("should process null specification", func() {
inRoute := routeWithCors(nil)
Expand Down Expand Up @@ -183,12 +190,13 @@ func basicEnvoyRoute() *envoy_config_route_v3.Route {
}
}

func basicEnvoyRouteWithCors(cSpec *envoy_config_route_v3.CorsPolicy) *envoy_config_route_v3.Route {
func basicEnvoyRouteWithCors(cSpec *envoy_config_cors_v3.CorsPolicy) *envoy_config_route_v3.Route {
corsConfig, err := utils.MessageToAny(cSpec)
Expect(err).NotTo(HaveOccurred())

return &envoy_config_route_v3.Route{
Action: &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{
Cors: cSpec,
},
TypedPerFilterConfig: map[string]*any.Any{
"envoy.filters.http.cors": corsConfig,
},
}
}
43 changes: 26 additions & 17 deletions projects/gloo/pkg/plugins/cors/vhost_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"strings"

envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
envoy_config_cors_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3"
envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"github.com/golang/protobuf/ptypes/any"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/options/cors"
"github.com/solo-io/gloo/projects/gloo/pkg/utils"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -50,7 +53,7 @@ var _ = Describe("VirtualHost Plugin", func() {
},
}

out1 := &envoy_config_route_v3.CorsPolicy{
out1 := &envoy_config_cors_v3.CorsPolicy{

AllowOriginStringMatch: []*envoy_type_matcher_v3.StringMatcher{
{
Expand Down Expand Up @@ -82,8 +85,12 @@ var _ = Describe("VirtualHost Plugin", func() {
MaxAge: maxAge1,
AllowCredentials: &wrappers.BoolValue{Value: allowCredentials1},
}
typedConfig, err := utils.MessageToAny(out1)
Expect(err).NotTo(HaveOccurred())
envoy1 = &envoy_config_route_v3.VirtualHost{
Cors: out1,
TypedPerFilterConfig: map[string]*any.Any{
"envoy.filters.http.cors": typedConfig,
},
}

params = plugins.VirtualHostParams{}
Expand All @@ -108,18 +115,23 @@ var _ = Describe("VirtualHost Plugin", func() {
}
err := plugin.(plugins.VirtualHostPlugin).ProcessVirtualHost(params, inRoute, out)
Expect(err).NotTo(HaveOccurred())
envoy1min := &envoy_config_route_v3.VirtualHost{
Cors: &envoy_config_route_v3.CorsPolicy{
AllowOriginStringMatch: []*envoy_type_matcher_v3.StringMatcher{
{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{Exact: allowOrigin1[0]},
},
{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{Exact: allowOrigin1[1]},
},
out1min := &envoy_config_cors_v3.CorsPolicy{
AllowOriginStringMatch: []*envoy_type_matcher_v3.StringMatcher{
{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{Exact: allowOrigin1[0]},
},
{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{Exact: allowOrigin1[1]},
},
},
}
typedConfig, err := utils.MessageToAny(out1min)
Expect(err).NotTo(HaveOccurred())
envoy1min := &envoy_config_route_v3.VirtualHost{
TypedPerFilterConfig: map[string]*any.Any{
"envoy.filters.http.cors": typedConfig,
},
}
Expect(out).To(Equal(envoy1min))
})
It("should process virtual hosts - empty specification", func() {
Expand All @@ -131,9 +143,8 @@ var _ = Describe("VirtualHost Plugin", func() {
}
err := plugin.(plugins.VirtualHostPlugin).ProcessVirtualHost(params, inRoute, out)
Expect(err).To(HaveOccurred())
envoy1empty := &envoy_config_route_v3.VirtualHost{
Cors: &envoy_config_route_v3.CorsPolicy{},
}

envoy1empty := &envoy_config_route_v3.VirtualHost{}
Expect(out).To(Equal(envoy1empty))
})
It("should process virtual hosts - ignore route filter disabled spec", func() {
Expand All @@ -147,9 +158,7 @@ var _ = Describe("VirtualHost Plugin", func() {
}
err := plugin.(plugins.VirtualHostPlugin).ProcessVirtualHost(params, inRoute, out)
Expect(err).To(HaveOccurred())
envoy1empty := &envoy_config_route_v3.VirtualHost{
Cors: &envoy_config_route_v3.CorsPolicy{},
}
envoy1empty := &envoy_config_route_v3.VirtualHost{}
Expect(out).To(Equal(envoy1empty))
})
It("should process virtual hosts - null specification", func() {
Expand Down
Loading

0 comments on commit eadaa63

Please sign in to comment.