From 4fa3d77108a33ee779553bf6c692978b6e2b6fe8 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Thu, 26 Oct 2023 07:41:10 +0800 Subject: [PATCH] feat: add CORS to SecurityPolicy (#2065) * Add CORS to SecurityPolicy Signed-off-by: huabing zhao * follow golang name convention: change Cors to CORS Signed-off-by: huabing zhao * add TODO to refactor string match types Signed-off-by: huabing zhao * fix test Signed-off-by: huabing zhao * fix test Signed-off-by: huabing zhao * address comment Signed-off-by: huabing zhao * address comment Signed-off-by: huabing zhao * fix test Signed-off-by: huabing zhao * set min length for AllowOrigins and AllowMethods Signed-off-by: huabing zhao * fix test Signed-off-by: huabing zhao * fix generate Signed-off-by: huabing zhao --------- Signed-off-by: huabing zhao --- api/v1alpha1/envoyproxy_metric_types.go | 4 +- api/v1alpha1/ratelimitfilter_types.go | 2 +- api/v1alpha1/securitypolicy_types.go | 59 +++ api/v1alpha1/zz_generated.deepcopy.go | 67 ++++ ...ateway.envoyproxy.io_securitypolicies.yaml | 56 +++ internal/gatewayapi/securitypolicy.go | 121 ++++-- .../testdata/securitypolicy-with-cors.in.yaml | 119 ++++++ .../securitypolicy-with-cors.out.yaml | 345 ++++++++++++++++++ internal/gatewayapi/translator.go | 2 +- internal/ir/xds.go | 8 +- internal/ir/zz_generated.deepcopy.go | 54 +-- internal/status/securitypolicy.go | 12 + internal/xds/translator/cors.go | 34 +- internal/xds/translator/httpfilters.go | 4 +- .../translator/testdata/in/xds-ir/cors.yaml | 1 - site/content/en/latest/api/extension_types.md | 45 +++ 16 files changed, 857 insertions(+), 76 deletions(-) create mode 100644 internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml create mode 100755 internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml diff --git a/api/v1alpha1/envoyproxy_metric_types.go b/api/v1alpha1/envoyproxy_metric_types.go index 170a1686b00..6cf486ac4b6 100644 --- a/api/v1alpha1/envoyproxy_metric_types.go +++ b/api/v1alpha1/envoyproxy_metric_types.go @@ -60,7 +60,7 @@ type ProxyPrometheusProvider struct { } // Match defines the stats match configuration. -type Match struct { +type Match struct { // TODO: zhaohuabing this type should be renamed to StatsMatch // MatcherType defines the stats matcher type // // +kubebuilder:validation:Enum=RegularExpression;Prefix;Suffix @@ -70,7 +70,7 @@ type Match struct { type MatcherType string -const ( +const ( // TODO: zhaohuabing the const types should be prefixed with StatsMatch Prefix MatcherType = "Prefix" RegularExpression MatcherType = "RegularExpression" Suffix MatcherType = "Suffix" diff --git a/api/v1alpha1/ratelimitfilter_types.go b/api/v1alpha1/ratelimitfilter_types.go index 44a24ef935e..5ee825d3b3c 100644 --- a/api/v1alpha1/ratelimitfilter_types.go +++ b/api/v1alpha1/ratelimitfilter_types.go @@ -130,7 +130,7 @@ type SourceMatch struct { } // HeaderMatch defines the match attributes within the HTTP Headers of the request. -type HeaderMatch struct { +type HeaderMatch struct { // TODO: zhaohuabing this type could be replaced with a general purpose StringMatch type. // Type specifies how to match against the value of the header. // // +optional diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go index f53d237323a..05117a77278 100644 --- a/api/v1alpha1/securitypolicy_types.go +++ b/api/v1alpha1/securitypolicy_types.go @@ -41,8 +41,67 @@ type SecurityPolicySpec struct { // for this Policy to have effect and be applied to the Gateway. // TargetRef TargetRef gwapiv1a2.PolicyTargetReferenceWithSectionName `json:"targetRef"` + + // CORS defines the configuration for Cross-Origin Resource Sharing (CORS). + CORS *CORS `json:"cors,omitempty"` +} + +// CORS defines the configuration for Cross-Origin Resource Sharing (CORS). +type CORS struct { + // AllowOrigins defines the origins that are allowed to make requests. + // +kubebuilder:validation:MinItems=1 + AllowOrigins []StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins,omitempty"` + // AllowMethods defines the methods that are allowed to make requests. + // +kubebuilder:validation:MinItems=1 + AllowMethods []string `json:"allowMethods,omitempty" yaml:"allowMethods,omitempty"` + // AllowHeaders defines the headers that are allowed to be sent with requests. + AllowHeaders []string `json:"allowHeaders,omitempty" yaml:"allowHeaders,omitempty"` + // ExposeHeaders defines the headers that can be exposed in the responses. + ExposeHeaders []string `json:"exposeHeaders,omitempty" yaml:"exposeHeaders,omitempty"` + // MaxAge defines how long the results of a preflight request can be cached. + MaxAge *metav1.Duration `json:"maxAge,omitempty" yaml:"maxAge,omitempty"` +} + +// StringMatch defines how to match any strings. +// This is a general purpose match condition that can be used by other EG APIs +// that need to match against a string. +type StringMatch struct { + // Type specifies how to match against a string. + // + // +optional + // +kubebuilder:default=Exact + Type *MatchType `json:"type,omitempty"` + + // Value specifies the string value that the match must have. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=1024 + Value string `json:"value"` } +// MatchType specifies the semantics of how a string value should be compared. +// Valid MatchType values are "Exact", "Prefix", "Suffix", "RegularExpression". +// +// +kubebuilder:validation:Enum=Exact;Prefix;Suffix;RegularExpression +type MatchType string + +const ( + // MatchExact :the input string must match exactly the match value. + MatchExact MatchType = "Exact" + + // MatchPrefix :the input string must start with the match value. + MatchPrefix MatchType = "Prefix" + + // MatchSuffix :the input string must end with the match value. + MatchSuffix MatchType = "Suffix" + + // MatchRegularExpression :The input string must match the regular expression + // specified in the match value. + // The regex string must adhere to the syntax documented in + // https://github.com/google/re2/wiki/Syntax. + MatchRegularExpression MatchType = "RegularExpression" +) + // SecurityPolicyStatus defines the state of SecurityPolicy type SecurityPolicyStatus struct { // Conditions describe the current conditions of the SecurityPolicy. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 78a560cbfcb..3fb30ec6006 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -199,6 +199,48 @@ func (in *BackendTrafficPolicyStatus) DeepCopy() *BackendTrafficPolicyStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CORS) DeepCopyInto(out *CORS) { + *out = *in + if in.AllowOrigins != nil { + in, out := &in.AllowOrigins, &out.AllowOrigins + *out = make([]StringMatch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AllowMethods != nil { + in, out := &in.AllowMethods, &out.AllowMethods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowHeaders != nil { + in, out := &in.AllowHeaders, &out.AllowHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExposeHeaders != nil { + in, out := &in.ExposeHeaders, &out.ExposeHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.MaxAge != nil { + in, out := &in.MaxAge, &out.MaxAge + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CORS. +func (in *CORS) DeepCopy() *CORS { + if in == nil { + return nil + } + out := new(CORS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClaimToHeader) DeepCopyInto(out *ClaimToHeader) { *out = *in @@ -2136,6 +2178,11 @@ func (in *SecurityPolicyList) DeepCopyObject() runtime.Object { func (in *SecurityPolicySpec) DeepCopyInto(out *SecurityPolicySpec) { *out = *in in.TargetRef.DeepCopyInto(&out.TargetRef) + if in.CORS != nil { + in, out := &in.CORS, &out.CORS + *out = new(CORS) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicySpec. @@ -2190,6 +2237,26 @@ func (in *SourceMatch) DeepCopy() *SourceMatch { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StringMatch) DeepCopyInto(out *StringMatch) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(MatchType) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StringMatch. +func (in *StringMatch) DeepCopy() *StringMatch { + if in == nil { + return nil + } + out := new(StringMatch) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TCPKeepalive) DeepCopyInto(out *TCPKeepalive) { *out = *in diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index 5e1418bf4ae..e3bf0d73926 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -42,6 +42,62 @@ spec: spec: description: Spec defines the desired state of SecurityPolicy. properties: + cors: + description: CORS defines the configuration for Cross-Origin Resource + Sharing (CORS). + properties: + allowHeaders: + description: AllowHeaders defines the headers that are allowed + to be sent with requests. + items: + type: string + type: array + allowMethods: + description: AllowMethods defines the methods that are allowed + to make requests. + items: + type: string + minItems: 1 + type: array + allowOrigins: + description: AllowOrigins defines the origins that are allowed + to make requests. + items: + description: StringMatch defines how to match any strings. This + is a general purpose match condition that can be used by other + EG APIs that need to match against a string. + properties: + type: + default: Exact + description: Type specifies how to match against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string value that the match + must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - value + type: object + minItems: 1 + type: array + exposeHeaders: + description: ExposeHeaders defines the headers that can be exposed + in the responses. + items: + type: string + type: array + maxAge: + description: MaxAge defines how long the results of a preflight + request can be cached. + type: string + type: object targetRef: description: TargetRef is the name of the Gateway resource this policy is being attached to. This Policy and the TargetRef MUST be in the diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 24ae703e6b3..57df03abee4 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -8,6 +8,7 @@ package gatewayapi import ( "fmt" "sort" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -15,11 +16,12 @@ import ( gwv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" "github.com/envoyproxy/gateway/internal/status" "github.com/envoyproxy/gateway/internal/utils/ptr" ) -func ProcessSecurityPolicies(securityPolicies []*egv1a1.SecurityPolicy, +func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.SecurityPolicy, gateways []*GatewayContext, routes []RouteContext, xdsIR XdsIRMap) []*egv1a1.SecurityPolicy { @@ -66,39 +68,29 @@ func ProcessSecurityPolicies(securityPolicies []*egv1a1.SecurityPolicy, continue } - translateSecurityPolicy(policy, xdsIR) + t.translateSecurityPolicyForRoute(policy, route, xdsIR) - // Set Accepted=True - status.SetSecurityPolicyCondition(policy, - gwv1a2.PolicyConditionAccepted, - metav1.ConditionTrue, - gwv1a2.PolicyReasonAccepted, - "SecurityPolicy has been accepted.", - ) + message := "SecurityPolicy has been accepted." + status.SetSecurityPolicyAcceptedIfUnset(&policy.Status, message) } } - // Process the policies targeting Gateways with a section name + // Process the policies targeting Gateways for _, policy := range securityPolicies { if policy.Spec.TargetRef.Kind == KindGateway { policy := policy.DeepCopy() res = append(res, policy) // Negative statuses have already been assigned so its safe to skip - gatewayKey := resolveSecurityPolicyGatewayTargetRef(policy, gatewayMap) - if gatewayKey == nil { + gateway := resolveSecurityPolicyGatewayTargetRef(policy, gatewayMap) + if gateway == nil { continue } - translateSecurityPolicy(policy, xdsIR) + t.translateSecurityPolicyForGateway(policy, gateway, xdsIR) - // Set Accepted=True - status.SetSecurityPolicyCondition(policy, - gwv1a2.PolicyConditionAccepted, - metav1.ConditionTrue, - gwv1a2.PolicyReasonAccepted, - "SecurityPolicy has been accepted.", - ) + message := "SecurityPolicy has been accepted." + status.SetSecurityPolicyAcceptedIfUnset(&policy.Status, message) } } @@ -227,5 +219,92 @@ func resolveSecurityPolicyRouteTargetRef(policy *egv1a1.SecurityPolicy, routes m return route.RouteContext } -func translateSecurityPolicy(policy *egv1a1.SecurityPolicy, xdsIR XdsIRMap) { + +func (t *Translator) translateSecurityPolicyForRoute(policy *egv1a1.SecurityPolicy, route RouteContext, xdsIR XdsIRMap) { + // Build IR + var cors *ir.CORS + if policy.Spec.CORS != nil { + cors = t.buildCORS(policy) + } + + // Apply IR to all relevant routes + prefix := irRoutePrefix(route) + for _, ir := range xdsIR { + for _, http := range ir.HTTP { + for _, r := range http.Routes { + // Apply if there is a match + if strings.HasPrefix(r.Name, prefix) { + r.CORS = cors + } + } + } + + } +} + +func (t *Translator) translateSecurityPolicyForGateway(policy *egv1a1.SecurityPolicy, gateway *GatewayContext, xdsIR XdsIRMap) { + // Build IR + var cors *ir.CORS + if policy.Spec.CORS != nil { + cors = t.buildCORS(policy) + } + + // Apply IR to all the routes within the specific Gateway + // If the feature is already set, then skip it, since it must be have + // set by a policy attaching to the route + irKey := t.getIRKey(gateway.Gateway) + // Should exist since we've validated this + ir := xdsIR[irKey] + + for _, http := range ir.HTTP { + for _, r := range http.Routes { + // Apply if not already set + if r.CORS == nil { + r.CORS = cors + } + } + } + +} + +func (t *Translator) buildCORS(policy *egv1a1.SecurityPolicy) *ir.CORS { + var allowOrigins []*ir.StringMatch + + for _, origin := range policy.Spec.CORS.AllowOrigins { + origin := origin.DeepCopy() + + // matchType default to exact + matchType := egv1a1.MatchExact + if origin.Type != nil { + matchType = *origin.Type + } + + // TODO zhaohuabing: extract a utils function to build StringMatch + switch matchType { + case egv1a1.MatchExact: + allowOrigins = append(allowOrigins, &ir.StringMatch{ + Exact: &origin.Value, + }) + case egv1a1.MatchPrefix: + allowOrigins = append(allowOrigins, &ir.StringMatch{ + Prefix: &origin.Value, + }) + case egv1a1.MatchSuffix: + allowOrigins = append(allowOrigins, &ir.StringMatch{ + Suffix: &origin.Value, + }) + case egv1a1.MatchRegularExpression: + allowOrigins = append(allowOrigins, &ir.StringMatch{ + SafeRegex: &origin.Value, + }) + } + } + + return &ir.CORS{ + AllowOrigins: allowOrigins, + AllowMethods: policy.Spec.CORS.AllowMethods, + AllowHeaders: policy.Spec.CORS.AllowHeaders, + ExposeHeaders: policy.Spec.CORS.ExposeHeaders, + MaxAge: policy.Spec.CORS.MaxAge, + } } diff --git a/internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml new file mode 100644 index 00000000000..fd6c99debd1 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml @@ -0,0 +1,119 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + namespace: default + name: grpcroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + cors: + allowOrigins: + - type: RegularExpression + value: "*.example.com" + - type: Exact + value: foo.bar.com + allowMethods: + - GET + - POST + allowHeaders: + - "x-header-1" + - "x-header-2" + exposeHeaders: + - "x-header-3" + - "x-header-4" + maxAge: 1000s +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: default + cors: + allowOrigins: + - type: Prefix + value: example + - type: Suffix + value: bar.org + allowMethods: + - GET + - POST + allowHeaders: + - "x-header-5" + - "x-header-6" + exposeHeaders: + - "x-header-7" + - "x-header-8" + maxAge: 2000s diff --git a/internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml new file mode 100755 index 00000000000..89485439e63 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml @@ -0,0 +1,345 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + creationTimestamp: null + name: grpcroute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-2 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 + envoy-gateway/gateway-2: + proxy: + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-2 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-route + namespace: default + spec: + cors: + allowHeaders: + - x-header-5 + - x-header-6 + allowMethods: + - GET + - POST + allowOrigins: + - type: Prefix + value: example + - type: Suffix + value: bar.org + exposeHeaders: + - x-header-7 + - x-header-8 + maxAge: 33m20s + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: default + status: + conditions: + - lastTransitionTime: null + message: SecurityPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + cors: + allowHeaders: + - x-header-1 + - x-header-2 + allowMethods: + - GET + - POST + allowOrigins: + - type: RegularExpression + value: '*.example.com' + - type: Exact + value: foo.bar.com + exposeHeaders: + - x-header-3 + - x-header-4 + maxAge: 16m40s + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: SecurityPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + cors: + allowHeaders: + - x-header-1 + - x-header-2 + allowMethods: + - GET + - POST + allowOrigins: + - distinct: false + name: "" + safeRegex: '*.example.com' + - distinct: false + exact: foo.bar.com + name: "" + exposeHeaders: + - x-header-3 + - x-header-4 + maxAge: 16m40s + destination: + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - endpoints: + - host: 7.7.7.7 + port: 8080 + weight: 1 + hostname: '*' + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* + envoy-gateway/gateway-2: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-2/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + cors: + allowHeaders: + - x-header-5 + - x-header-6 + allowMethods: + - GET + - POST + allowOrigins: + - distinct: false + name: "" + prefix: example + - distinct: false + name: "" + suffix: bar.org + exposeHeaders: + - x-header-7 + - x-header-8 + maxAge: 33m20s + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - endpoints: + - host: 7.7.7.7 + port: 8080 + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index 04c4c1c93ce..f5d6b54bc22 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -188,7 +188,7 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult { backendTrafficPolicies := t.ProcessBackendTrafficPolicies( resources.BackendTrafficPolicies, gateways, routes, xdsIR) // Process SecurityPolicies - securityPolicies := ProcessSecurityPolicies( + securityPolicies := t.ProcessSecurityPolicies( resources.SecurityPolicies, gateways, routes, xdsIR) // Sort xdsIR based on the Gateway API spec diff --git a/internal/ir/xds.go b/internal/ir/xds.go index c4e6fe32d0b..e7bf0415621 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -279,8 +279,8 @@ type HTTPRoute struct { Timeout *metav1.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` // load balancer policy to use when routing to the backend endpoints. LoadBalancer *LoadBalancer `json:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"` - // Cors policy for the route. - Cors *Cors `json:"cors,omitempty" yaml:"cors,omitempty"` + // CORS policy for the route. + CORS *CORS `json:"cors,omitempty" yaml:"cors,omitempty"` // ExtensionRefs holds unstructured resources that were introduced by an extension and used on the HTTPRoute as extensionRef filters ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"` } @@ -314,10 +314,10 @@ type JwtRequestAuthentication struct { Providers []egv1a1.JwtAuthenticationFilterProvider `json:"providers,omitempty" yaml:"providers,omitempty"` } -// Cors holds the Cross-Origin Resource Sharing (CORS) policy for the route. +// CORS holds the Cross-Origin Resource Sharing (CORS) policy for the route. // // +k8s:deepcopy-gen=true -type Cors struct { +type CORS struct { // AllowOrigins defines the origins that are allowed to make requests. AllowOrigins []*StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins,omitempty"` // AllowMethods defines the methods that are allowed to make requests. diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index ef56202cafc..02173a1ae7f 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -78,27 +78,7 @@ func (in *AddHeader) DeepCopy() *AddHeader { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConsistentHash) DeepCopyInto(out *ConsistentHash) { - *out = *in - if in.SourceIP != nil { - in, out := &in.SourceIP, &out.SourceIP - *out = new(bool) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsistentHash. -func (in *ConsistentHash) DeepCopy() *ConsistentHash { - if in == nil { - return nil - } - out := new(ConsistentHash) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Cors) DeepCopyInto(out *Cors) { +func (in *CORS) DeepCopyInto(out *CORS) { *out = *in if in.AllowOrigins != nil { in, out := &in.AllowOrigins, &out.AllowOrigins @@ -133,12 +113,32 @@ func (in *Cors) DeepCopyInto(out *Cors) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cors. -func (in *Cors) DeepCopy() *Cors { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CORS. +func (in *CORS) DeepCopy() *CORS { if in == nil { return nil } - out := new(Cors) + out := new(CORS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConsistentHash) DeepCopyInto(out *ConsistentHash) { + *out = *in + if in.SourceIP != nil { + in, out := &in.SourceIP, &out.SourceIP + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsistentHash. +func (in *ConsistentHash) DeepCopy() *ConsistentHash { + if in == nil { + return nil + } + out := new(ConsistentHash) in.DeepCopyInto(out) return out } @@ -456,9 +456,9 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { *out = new(LoadBalancer) (*in).DeepCopyInto(*out) } - if in.Cors != nil { - in, out := &in.Cors, &out.Cors - *out = new(Cors) + if in.CORS != nil { + in, out := &in.CORS, &out.CORS + *out = new(CORS) (*in).DeepCopyInto(*out) } if in.ExtensionRefs != nil { diff --git a/internal/status/securitypolicy.go b/internal/status/securitypolicy.go index ee3cfebe53f..c538a972308 100644 --- a/internal/status/securitypolicy.go +++ b/internal/status/securitypolicy.go @@ -18,3 +18,15 @@ func SetSecurityPolicyCondition(c *egv1a1.SecurityPolicy, conditionType gwv1a2.P cond := newCondition(string(conditionType), status, string(reason), message, time.Now(), c.Generation) c.Status.Conditions = MergeConditions(c.Status.Conditions, cond) } + +func SetSecurityPolicyAcceptedIfUnset(s *egv1a1.SecurityPolicyStatus, message string) { + // Return early if Accepted condition is already set + for _, c := range s.Conditions { + if c.Type == string(gwv1a2.PolicyConditionAccepted) { + return + } + } + + cond := newCondition(string(gwv1a2.PolicyConditionAccepted), metav1.ConditionTrue, string(gwv1a2.PolicyReasonAccepted), message, time.Now(), 0) + s.Conditions = MergeConditions(s.Conditions, cond) +} diff --git a/internal/xds/translator/cors.go b/internal/xds/translator/cors.go index 775eeeb9ed5..b070b7fb54a 100644 --- a/internal/xds/translator/cors.go +++ b/internal/xds/translator/cors.go @@ -22,9 +22,9 @@ import ( "github.com/envoyproxy/gateway/internal/ir" ) -// patchHCMWithCorsFilter builds and appends the Cors Filter to the HTTP +// patchHCMWithCORSFilter builds and appends the CORS Filter to the HTTP // Connection Manager if applicable. -func patchHCMWithCorsFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { +func patchHCMWithCORSFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { if mgr == nil { return errors.New("hcm is nil") } @@ -33,7 +33,7 @@ func patchHCMWithCorsFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTT return errors.New("ir listener is nil") } - if !listenerContainsCors(irListener) { + if !listenerContainsCORS(irListener) { return nil } @@ -44,7 +44,7 @@ func patchHCMWithCorsFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTT } } - corsFilter, err := buildHCMCorsFilter() + corsFilter, err := buildHCMCORSFilter() if err != nil { return err } @@ -55,8 +55,8 @@ func patchHCMWithCorsFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTT return nil } -// buildHCMCorsFilter returns a Cors filter from the provided IR listener. -func buildHCMCorsFilter() (*hcmv3.HttpFilter, error) { +// buildHCMCORSFilter returns a CORS filter from the provided IR listener. +func buildHCMCORSFilter() (*hcmv3.HttpFilter, error) { corsProto := &corsv3.Cors{} corsAny, err := anypb.New(corsProto) @@ -72,15 +72,15 @@ func buildHCMCorsFilter() (*hcmv3.HttpFilter, error) { }, nil } -// listenerContainsCors returns true if the provided listener has Cors +// listenerContainsCORS returns true if the provided listener has CORS // policies attached to its routes. -func listenerContainsCors(irListener *ir.HTTPListener) bool { +func listenerContainsCORS(irListener *ir.HTTPListener) bool { if irListener == nil { return false } for _, route := range irListener.Routes { - if route.Cors != nil { + if route.CORS != nil { return true } } @@ -88,16 +88,16 @@ func listenerContainsCors(irListener *ir.HTTPListener) bool { return false } -// patchRouteWithCorsConfig patches the provided route with the Cors config if +// patchRouteWithCORSConfig patches the provided route with the CORS config if // applicable. -func patchRouteWithCorsConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error { +func patchRouteWithCORSConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error { if route == nil { return errors.New("xds route is nil") } if irRoute == nil { return errors.New("ir route is nil") } - if irRoute.Cors == nil { + if irRoute.CORS == nil { return nil } @@ -119,14 +119,14 @@ func patchRouteWithCorsConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error //nolint:gocritic - for _, origin := range irRoute.Cors.AllowOrigins { + for _, origin := range irRoute.CORS.AllowOrigins { allowOrigins = append(allowOrigins, buildXdsStringMatcher(origin)) } - allowMethods = strings.Join(irRoute.Cors.AllowMethods, ", ") - allowHeaders = strings.Join(irRoute.Cors.AllowHeaders, ", ") - exposeHeaders = strings.Join(irRoute.Cors.ExposeHeaders, ", ") - maxAge = strconv.Itoa(int(irRoute.Cors.MaxAge.Seconds())) + allowMethods = strings.Join(irRoute.CORS.AllowMethods, ", ") + allowHeaders = strings.Join(irRoute.CORS.AllowHeaders, ", ") + exposeHeaders = strings.Join(irRoute.CORS.ExposeHeaders, ", ") + maxAge = strconv.Itoa(int(irRoute.CORS.MaxAge.Seconds())) routeCfgProto := &corsv3.CorsPolicy{ AllowOriginStringMatch: allowOrigins, diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index da5c5aca045..cef974cec33 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -106,7 +106,7 @@ func (t *Translator) patchHCMWithFilters( } // Add the cors filter, if needed - if err := patchHCMWithCorsFilter(mgr, irListener); err != nil { + if err := patchHCMWithCORSFilter(mgr, irListener); err != nil { return err } @@ -135,7 +135,7 @@ func patchRouteWithFilters( } // Add the cors per route config to the route, if needed. - if err := patchRouteWithCorsConfig(route, irRoute); err != nil { + if err := patchRouteWithCORSConfig(route, irRoute); err != nil { return err } return nil diff --git a/internal/xds/translator/testdata/in/xds-ir/cors.yaml b/internal/xds/translator/testdata/in/xds-ir/cors.yaml index d7dc9f71f7a..6887b75f9c3 100644 --- a/internal/xds/translator/testdata/in/xds-ir/cors.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/cors.yaml @@ -33,4 +33,3 @@ http: - "x-header-3" - "x-header-4" maxAge: 1000s - allowPrivateNetworkAccess: false diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 3516609bf33..ccc579e9da5 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -132,6 +132,24 @@ _Appears in:_ +#### CORS + + + +CORS defines the configuration for Cross-Origin Resource Sharing (CORS). + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Description | +| --- | --- | +| `allowOrigins` _[StringMatch](#stringmatch) array_ | AllowOrigins defines the origins that are allowed to make requests. | +| `allowMethods` _string array_ | AllowMethods defines the methods that are allowed to make requests. | +| `allowHeaders` _string array_ | AllowHeaders defines the headers that are allowed to be sent with requests. | +| `exposeHeaders` _string array_ | ExposeHeaders defines the headers that can be exposed in the responses. | +| `maxAge` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#duration-v1-meta)_ | MaxAge defines how long the results of a preflight request can be cached. | + + #### ClaimToHeader @@ -1033,6 +1051,17 @@ _Appears in:_ | `value` _string_ | | +#### MatchType + +_Underlying type:_ `string` + +MatchType specifies the semantics of how a string value should be compared. Valid MatchType values are "Exact", "Prefix", "Suffix", "RegularExpression". + +_Appears in:_ +- [StringMatch](#stringmatch) + + + #### MatcherType _Underlying type:_ `string` @@ -1557,6 +1586,7 @@ _Appears in:_ | Field | Description | | --- | --- | | `targetRef` _[PolicyTargetReferenceWithSectionName](#policytargetreferencewithsectionname)_ | TargetRef is the name of the Gateway resource this policy is being attached to. This Policy and the TargetRef MUST be in the same namespace for this Policy to have effect and be applied to the Gateway. TargetRef | +| `cors` _[CORS](#cors)_ | CORS defines the configuration for Cross-Origin Resource Sharing (CORS). | @@ -1583,6 +1613,21 @@ _Appears in:_ +#### StringMatch + + + +StringMatch defines how to match any strings. This is a general purpose match condition that can be used by other EG APIs that need to match against a string. + +_Appears in:_ +- [CORS](#cors) + +| Field | Description | +| --- | --- | +| `type` _[MatchType](#matchtype)_ | Type specifies how to match against a string. | +| `value` _string_ | Value specifies the string value that the match must have. | + + #### TCPKeepalive