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

Add CORS policy to config-contour #1069

Merged
merged 13 commits into from
Apr 10, 2024
16 changes: 16 additions & 0 deletions config/config-contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ data:
ClusterLocal:
class: contour-internal
service: contour-internal/envoy
# cors-policy contains the configuration to set CORS policy for HTTPProxies.
cors-policy: |
allowCredentials: true
allowOrigin:
- example.com
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
43 changes: 42 additions & 1 deletion pkg/reconciler/contour/config/contour.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package config

import (
"fmt"
"regexp"
"time"

v1 "github.com/projectcontour/contour/apis/projectcontour/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
Expand All @@ -39,6 +41,7 @@ const (
defaultTLSSecretConfigKey = "default-tls-secret"
timeoutPolicyIdleKey = "timeout-policy-idle"
timeoutPolicyResponseKey = "timeout-policy-response"
corsPolicy = "cors-policy"
)

// Contour contains contour related configuration defined in the
Expand All @@ -49,18 +52,20 @@ type Contour struct {
DefaultTLSSecret *types.NamespacedName
TimeoutPolicyResponse string
TimeoutPolicyIdle string
CORSPolicy *v1.CORSPolicy
}

type visibilityValue struct {
Class string `json:"class"`
Service string `json:"service"`
}

// NewContourFromConfigMap creates an Contour config from the supplied ConfigMap
// NewContourFromConfigMap creates a Contour config from the supplied ConfigMap
func NewContourFromConfigMap(configMap *corev1.ConfigMap) (*Contour, error) {
var tlsSecret *types.NamespacedName
var timeoutPolicyResponse = "infinity"
var timeoutPolicyIdle = "infinity"
var contourCORSPolicy *v1.CORSPolicy

if err := configmap.Parse(configMap.Data,
configmap.AsOptionalNamespacedName(defaultTLSSecretConfigKey, &tlsSecret),
Expand All @@ -70,6 +75,40 @@ func NewContourFromConfigMap(configMap *corev1.ConfigMap) (*Contour, error) {
return nil, err
}

cors, ok := configMap.Data[corsPolicy]
if ok {
if err := yaml.Unmarshal([]byte(cors), &contourCORSPolicy); err != nil {
return nil, err
dprotaso marked this conversation as resolved.
Show resolved Hide resolved
}

if len(contourCORSPolicy.AllowOrigin) == 0 || len(contourCORSPolicy.AllowMethods) == 0 {
return nil, fmt.Errorf("the following fields are required but are missing or empty: %s.allowOrigin and %s.allowMethods", corsPolicy, corsPolicy)
}

fields := [][]v1.CORSHeaderValue{
contourCORSPolicy.AllowMethods,
contourCORSPolicy.AllowHeaders,
contourCORSPolicy.ExposeHeaders,
}
for _, field := range fields {
if len(field) > 0 {
var validOption = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+.^_`|~-]+$")
for _, option := range field {
if !validOption.MatchString(string(option)) {
return nil, fmt.Errorf("%s.allowMethods, %s.allowHeaders, and %s.exposeHeaders have to be validly formatted. Option %s is invalid", corsPolicy, corsPolicy, corsPolicy, option)
krsna-m marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

if len(contourCORSPolicy.MaxAge) > 0 {
_, err := time.ParseDuration(contourCORSPolicy.MaxAge)
if err != nil {
return nil, fmt.Errorf("failed to parse %s.maxAge: %w", corsPolicy, err)
}
krsna-m marked this conversation as resolved.
Show resolved Hide resolved
}
}

v, ok := configMap.Data[visibilityConfigKey]
if !ok {
// These are the defaults.
Expand All @@ -85,6 +124,7 @@ func NewContourFromConfigMap(configMap *corev1.ConfigMap) (*Contour, error) {
},
TimeoutPolicyResponse: timeoutPolicyResponse,
TimeoutPolicyIdle: timeoutPolicyIdle,
CORSPolicy: contourCORSPolicy,
}, nil
}
entry := make(map[v1alpha1.IngressVisibility]visibilityValue)
Expand All @@ -107,6 +147,7 @@ func NewContourFromConfigMap(configMap *corev1.ConfigMap) (*Contour, error) {
VisibilityClasses: make(map[v1alpha1.IngressVisibility]string, 2),
TimeoutPolicyResponse: timeoutPolicyResponse,
TimeoutPolicyIdle: timeoutPolicyIdle,
CORSPolicy: contourCORSPolicy,
}
for key, value := range entry {
// Check that the visibility makes sense.
Expand Down
201 changes: 201 additions & 0 deletions pkg/reconciler/contour/config/contour_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package config
import (
"testing"

"github.com/google/go-cmp/cmp"

v1 "github.com/projectcontour/contour/apis/projectcontour/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -40,6 +43,204 @@ func TestContour(t *testing.T) {
}
}

func TestCORSPolicy(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: true
allowOrigin:
- "*"
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
}

cfg, err := NewContourFromConfigMap(cm)
if err != nil {
t.Error("NewContourFromConfigMap(corsPolicy) =", err)
t.FailNow()
}

want := &v1.CORSPolicy{
AllowCredentials: true,
AllowOrigin: []string{"*"},
AllowMethods: []v1.CORSHeaderValue{"GET", "POST", "OPTIONS"},
AllowHeaders: []v1.CORSHeaderValue{"authorization", "cache-control"},
ExposeHeaders: []v1.CORSHeaderValue{"Content-Length", "Content-Range"},
MaxAge: "10m",
}
got := cfg.CORSPolicy
if !cmp.Equal(got, want) {
t.Errorf("Got = %v, want: %v, diff:\n%s", got, want, cmp.Diff(got, want))
}
}
func TestCORSPolicyConfigurationErrors(t *testing.T) {
tests := []struct {
name string
wantErr bool
config *corev1.ConfigMap
}{{
name: "failure parsing yaml",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: "moo",
},
},
}, {
name: "wrong type",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: 3
allowOrigin: true
allowMethods: "moo"
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
},
}, {
name: "incomplete configuration",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
},
}, {
name: "empty value",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: false
allowOrigin: []
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
},
}, {
name: "wrong option",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: true
allowOrigin:
- "*"
allowMethods:
- ((GET))
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10m"
`,
},
},
}, {
name: "invalid duration",
wantErr: true,
config: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: system.Namespace(),
Name: ContourConfigName,
},
Data: map[string]string{
corsPolicy: `
allowCredentials: true
allowOrigin:
- "*"
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders:
- authorization
- cache-control
exposeHeaders:
- Content-Length
- Content-Range
maxAge: "10"
`,
},
},
}}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewContourFromConfigMap(tt.config)
t.Log(err)
if (err != nil) != tt.wantErr {
t.Fatalf("Test: %q; NewContourFromConfigMap() error = %v, WantErr %v", tt.name, err, tt.wantErr)
}
})
}
}

func TestDefaultTLSSecret(t *testing.T) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Expand Down
6 changes: 6 additions & 0 deletions pkg/reconciler/contour/config/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions pkg/reconciler/contour/resources/httpproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,16 @@ func MakeHTTPProxies(ctx context.Context, ing *v1alpha1.Ingress, serviceToProtoc
}

hostProxy.Name = kmeta.ChildName(ing.Name+"-"+class+"-", host)
hostProxy.Spec.VirtualHost = &v1.VirtualHost{
Fqdn: host,

if cfg.Contour.CORSPolicy != nil {
hostProxy.Spec.VirtualHost = &v1.VirtualHost{
Fqdn: host,
CORSPolicy: cfg.Contour.CORSPolicy,
}
} else {
hostProxy.Spec.VirtualHost = &v1.VirtualHost{
Fqdn: host,
}
}
izabelacg marked this conversation as resolved.
Show resolved Hide resolved

// Set ExtensionService if annotation is present
Expand Down
Loading
Loading