From 00d5270e5a315bc304c840ab0a790b238737da30 Mon Sep 17 00:00:00 2001 From: huabing zhao Date: Wed, 25 Oct 2023 18:31:04 +0800 Subject: [PATCH] Add CORS to SecurityPolicy Signed-off-by: huabing zhao --- api/v1alpha1/securitypolicy_types.go | 66 ++++ api/v1alpha1/zz_generated.deepcopy.go | 67 ++++ ...ateway.envoyproxy.io_securitypolicies.yaml | 60 +++ internal/gatewayapi/securitypolicy.go | 129 +++++-- .../testdata/securitypolicy-with-cors.in.yaml | 119 ++++++ .../securitypolicy-with-cors.out.yaml | 350 ++++++++++++++++++ internal/gatewayapi/translator.go | 2 +- internal/ir/xds.go | 8 + internal/ir/zz_generated.deepcopy.go | 5 + internal/status/securitypolicy.go | 12 + .../translator/testdata/in/xds-ir/cors.yaml | 1 - site/content/en/latest/api/extension_types.md | 46 +++ 12 files changed, 842 insertions(+), 23 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/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go index f53d237323a..2ec4e969d2a 100644 --- a/api/v1alpha1/securitypolicy_types.go +++ b/api/v1alpha1/securitypolicy_types.go @@ -41,8 +41,74 @@ 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. + AllowOrigins []StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins,omitempty"` + // AllowMethods defines the methods that are allowed to make requests. + 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. +// TODO: zhaohuabing make this a shared type for all APIs +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"` + + // IgnoreCase specifies whether the match should be case insensitive. + // This has no effect for the safe_regex match. + // Defaults to false. + // +optional + // +kubebuilder:default=False + IgnoreCase bool `json:"caseSensitive,omitempty"` +} + +// MatchType specifies the semantics of how a string value should be compared. +// Valid MatchType values are "Exact", "Prefix", "Suffix", "Contains", "RegularExpression". +// +// +kubebuilder:validation:Enum=Exact;Prefix;Suffix;Contains;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" + + // MatchContains :the input string must contain the match value. + MatchContains MatchType = "Contains" + + // 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..b031093b7bb 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,66 @@ 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 + type: array + allowOrigins: + description: AllowOrigins defines the origins that are allowed + to make requests. + items: + description: 'StringMatch defines how to match any strings. + TODO: zhaohuabing make this a shared type for all APIs' + properties: + caseSensitive: + default: "False" + description: IgnoreCase specifies whether the match should + be case insensitive. This has no effect for the safe_regex + match. Defaults to false. + type: boolean + type: + default: Exact + description: Type specifies how to match against a string. + enum: + - Exact + - Prefix + - Suffix + - Contains + - 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 + 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..adaff622789 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,100 @@ 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, + IgnoreCase: origin.IgnoreCase, + }) + case egv1a1.MatchPrefix: + allowOrigins = append(allowOrigins, &ir.StringMatch{ + Prefix: &origin.Value, + IgnoreCase: origin.IgnoreCase, + }) + case egv1a1.MatchSuffix: + allowOrigins = append(allowOrigins, &ir.StringMatch{ + Suffix: &origin.Value, + IgnoreCase: origin.IgnoreCase, + }) + case egv1a1.MatchContains: + allowOrigins = append(allowOrigins, &ir.StringMatch{ + Contains: &origin.Value, + IgnoreCase: origin.IgnoreCase, + }) + 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..c9bdc0b331d --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml @@ -0,0 +1,350 @@ +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 + ignoreCase: false + name: "" + safeRegex: '*.example.com' + - distinct: false + exact: foo.bar.com + ignoreCase: false + 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 + ignoreCase: false + name: "" + prefix: example + - distinct: false + ignoreCase: 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 + ignoreCase: 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..a343b173f4a 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -675,11 +675,16 @@ type StringMatch struct { Prefix *string `json:"prefix,omitempty" yaml:"prefix,omitempty"` // Suffix match condition. Suffix *string `json:"suffix,omitempty" yaml:"suffix,omitempty"` + // Contains match condition. + Contains *string `json:"contains,omitempty" yaml:"contains,omitempty"` // SafeRegex match condition. SafeRegex *string `json:"safeRegex,omitempty" yaml:"safeRegex,omitempty"` // Distinct match condition. // Used to match any and all possible unique values encountered within the Name field. Distinct bool `json:"distinct" yaml:"distinct"` + // IgnoreCase specifies whether the match should be case insensitive. + // This has no effect for the safe_regex match. + IgnoreCase bool `json:"ignoreCase" yaml:"ignoreCase"` } // Validate the fields within the StringMatch structure @@ -695,6 +700,9 @@ func (s StringMatch) Validate() error { if s.Suffix != nil { matchCount++ } + if s.Contains != nil { + matchCount++ + } if s.SafeRegex != nil { matchCount++ } diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index ef56202cafc..d1bbb050455 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -978,6 +978,11 @@ func (in *StringMatch) DeepCopyInto(out *StringMatch) { *out = new(string) **out = **in } + if in.Contains != nil { + in, out := &in.Contains, &out.Contains + *out = new(string) + **out = **in + } if in.SafeRegex != nil { in, out := &in.SafeRegex, &out.SafeRegex *out = new(string) 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/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..28069b5670c 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", "Contains", "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,22 @@ _Appears in:_ +#### StringMatch + + + +StringMatch defines how to match any strings. TODO: zhaohuabing make this a shared type for all APIs + +_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. | +| `caseSensitive` _boolean_ | IgnoreCase specifies whether the match should be case insensitive. This has no effect for the safe_regex match. Defaults to false. | + + #### TCPKeepalive