From d53caf5deeb0fe099b26ff9ca44aac24a502fd4b Mon Sep 17 00:00:00 2001 From: huabing zhao Date: Tue, 14 May 2024 19:41:47 -0700 Subject: [PATCH] authorization impl Signed-off-by: huabing zhao --- api/v1alpha1/authorization_types.go | 42 +- api/v1alpha1/envoyproxy_types.go | 7 +- api/v1alpha1/zz_generated.deepcopy.go | 45 +- .../gateway.envoyproxy.io_envoyproxies.yaml | 6 + ...ateway.envoyproxy.io_securitypolicies.yaml | 11 +- internal/gatewayapi/backendtrafficpolicy.go | 15 +- internal/gatewayapi/helpers.go | 16 + internal/gatewayapi/securitypolicy.go | 90 +++- ...telimit-default-route-level-limit.out.yaml | 3 +- ...rafficpolicy-with-local-ratelimit.out.yaml | 3 +- ...ckendtrafficpolicy-with-ratelimit.out.yaml | 3 +- .../securitypolicy-with-authoriztion.in.yaml | 131 ++++++ .../securitypolicy-with-authoriztion.out.yaml | 399 ++++++++++++++++++ internal/ir/xds.go | 37 +- internal/ir/zz_generated.deepcopy.go | 73 ++++ internal/xds/translator/authorization.go | 266 ++++++++++++ internal/xds/translator/extauth.go | 1 - internal/xds/translator/httpfilters.go | 13 +- internal/xds/translator/httpfilters_test.go | 86 ++-- internal/xds/translator/local_ratelimit.go | 4 +- internal/xds/translator/ratelimit.go | 4 +- .../testdata/in/xds-ir/authorization.yaml | 109 +++++ .../out/xds-ir/authorization.clusters.yaml | 51 +++ .../out/xds-ir/authorization.endpoints.yaml | 36 ++ .../out/xds-ir/authorization.listeners.yaml | 37 ++ .../out/xds-ir/authorization.routes.yaml | 135 ++++++ site/content/en/latest/api/extension_types.md | 77 ++-- .../e2e/testdata/authorization-client-ip.yaml | 98 +++++ .../authorization-default-action.yaml | 58 +++ test/e2e/testdata/basic-auth.yaml | 2 - test/e2e/tests/authorization-client-ip.go | 174 ++++++++ .../e2e/tests/authorization-default-action.go | 92 ++++ 32 files changed, 1957 insertions(+), 167 deletions(-) create mode 100644 internal/gatewayapi/testdata/securitypolicy-with-authoriztion.in.yaml create mode 100755 internal/gatewayapi/testdata/securitypolicy-with-authoriztion.out.yaml create mode 100644 internal/xds/translator/authorization.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/authorization.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/authorization.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/authorization.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/authorization.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/authorization.routes.yaml create mode 100644 test/e2e/testdata/authorization-client-ip.yaml create mode 100644 test/e2e/testdata/authorization-default-action.yaml create mode 100644 test/e2e/tests/authorization-client-ip.go create mode 100644 test/e2e/tests/authorization-default-action.go diff --git a/api/v1alpha1/authorization_types.go b/api/v1alpha1/authorization_types.go index c52a2063b36..c2c6c78d99f 100644 --- a/api/v1alpha1/authorization_types.go +++ b/api/v1alpha1/authorization_types.go @@ -6,7 +6,8 @@ package v1alpha1 // Authorization defines the authorization configuration. -// +notImplementedHide +// +// Note: if neither `Rules` nor `DefaultAction` is specified, the default action is to deny all requests. type Authorization struct { // Rules defines a list of authorization rules. // These rules are evaluated in order, the first matching rule will be applied, @@ -16,50 +17,45 @@ type Authorization struct { // and the second rule denies it, when a request matches both rules, it will be allowed. // // +optional - Rules []Rule `json:"rules,omitempty"` + Rules []AuthorizationRule `json:"rules,omitempty"` // DefaultAction defines the default action to be taken if no rules match. // If not specified, the default action is Deny. // +optional - DefaultAction *RuleActionType `json:"defaultAction"` + DefaultAction *AuthorizationAction `json:"defaultAction"` } -// Rule defines the single authorization rule. -// +notImplementedHide -type Rule struct { +// AuthorizationRule defines the single authorization rule. +type AuthorizationRule struct { + // Name is a user-friendly name for the rule. It's just for display purposes. + // +optional + Name *string `json:"name"` + // Action defines the action to be taken if the rule matches. - Action RuleActionType `json:"action"` + Action AuthorizationAction `json:"action"` // Principal specifies the client identity of a request. Principal Principal `json:"principal"` - - // Permissions contains allowed HTTP methods. - // If empty, all methods are matching. - // - // +optional - // Permissions []string `json:"permissions,omitempty"` } // Principal specifies the client identity of a request. -// +notImplementedHide type Principal struct { - // ClientCIDR is the IP CIDR range of the client. + // ClientCIDRs are the IP CIDR ranges of the client. // Valid examples are "192.168.1.0/24" or "2001:db8::/64" // // By default, the client IP is inferred from the x-forwarder-for header and proxy protocol. // You can use the `EnableProxyProtocol` and `ClientIPDetection` options in // the `ClientTrafficPolicy` to configure how the client IP is detected. - ClientCIDR []string `json:"clientCIDR,omitempty"` + ClientCIDRs []string `json:"clientCIDRs,omitempty"` } -// RuleActionType specifies the types of authorization rule action. +// AuthorizationAction defines the action to be taken if a rule matches. // +kubebuilder:validation:Enum=Allow;Deny -// +notImplementedHide -type RuleActionType string +type AuthorizationAction string const ( - // Allow is the action to allow the request. - Allow RuleActionType = "Allow" - // Deny is the action to deny the request. - Deny RuleActionType = "Deny" + // AuthorizationActionAllow is the action to allow the request. + AuthorizationActionAllow AuthorizationAction = "Allow" + // AuthorizationActionDeny is the action to deny the request. + AuthorizationActionDeny AuthorizationAction = "Deny" ) diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index d8afe76ca78..9f463ab1af0 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -109,6 +109,8 @@ type EnvoyProxySpec struct { // // - envoy.filters.http.wasm // + // - envoy.filters.http.rbac + // // - envoy.filters.http.local_ratelimit // // - envoy.filters.http.ratelimit @@ -150,7 +152,7 @@ type FilterPosition struct { } // EnvoyFilter defines the type of Envoy HTTP filter. -// +kubebuilder:validation:Enum=envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.basic_authn;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.fault;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.wasm;envoy.filters.http.ext_proc +// +kubebuilder:validation:Enum=envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.basic_authn;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.fault;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.wasm;envoy.filters.http.ext_proc;envoy.filters.http.rbac type EnvoyFilter string const ( @@ -183,6 +185,9 @@ const ( // EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter. EnvoyFilterRateLimit EnvoyFilter = "envoy.filters.http.ratelimit" + // EnvoyFilterRBAC defines the Envoy RBAC filter. + EnvoyFilterRBAC EnvoyFilter = "envoy.filters.http.rbac" + // EnvoyFilterRouter defines the Envoy HTTP router filter. EnvoyFilterRouter EnvoyFilter = "envoy.filters.http.router" ) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index fd607c16af7..228e31987dd 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -156,14 +156,14 @@ func (in *Authorization) DeepCopyInto(out *Authorization) { *out = *in if in.Rules != nil { in, out := &in.Rules, &out.Rules - *out = make([]Rule, len(*in)) + *out = make([]AuthorizationRule, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.DefaultAction != nil { in, out := &in.DefaultAction, &out.DefaultAction - *out = new(RuleActionType) + *out = new(AuthorizationAction) **out = **in } } @@ -178,6 +178,27 @@ func (in *Authorization) DeepCopy() *Authorization { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationRule) DeepCopyInto(out *AuthorizationRule) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + in.Principal.DeepCopyInto(&out.Principal) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationRule. +func (in *AuthorizationRule) DeepCopy() *AuthorizationRule { + if in == nil { + return nil + } + out := new(AuthorizationRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackOffPolicy) DeepCopyInto(out *BackOffPolicy) { *out = *in @@ -3330,8 +3351,8 @@ func (in *PerRetryPolicy) DeepCopy() *PerRetryPolicy { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Principal) DeepCopyInto(out *Principal) { *out = *in - if in.ClientCIDR != nil { - in, out := &in.ClientCIDR, &out.ClientCIDR + if in.ClientCIDRs != nil { + in, out := &in.ClientCIDRs, &out.ClientCIDRs *out = make([]string, len(*in)) copy(*out, *in) } @@ -4056,22 +4077,6 @@ func (in *RetryOn) DeepCopy() *RetryOn { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Rule) DeepCopyInto(out *Rule) { - *out = *in - in.Principal.DeepCopyInto(&out.Principal) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rule. -func (in *Rule) DeepCopy() *Rule { - if in == nil { - return nil - } - out := new(Rule) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecurityPolicy) DeepCopyInto(out *SecurityPolicy) { *out = *in diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index 0282aed70d0..0fd7ab97314 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -253,6 +253,9 @@ spec: - envoy.filters.http.wasm + - envoy.filters.http.rbac + + - envoy.filters.http.local_ratelimit @@ -279,6 +282,7 @@ spec: - envoy.filters.http.ratelimit - envoy.filters.http.wasm - envoy.filters.http.ext_proc + - envoy.filters.http.rbac type: string before: description: |- @@ -295,6 +299,7 @@ spec: - envoy.filters.http.ratelimit - envoy.filters.http.wasm - envoy.filters.http.ext_proc + - envoy.filters.http.rbac type: string name: description: Name of the filter. @@ -309,6 +314,7 @@ spec: - envoy.filters.http.ratelimit - envoy.filters.http.wasm - envoy.filters.http.ext_proc + - envoy.filters.http.rbac type: string required: - name 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 919d272cf89..e19360829d2 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -70,7 +70,8 @@ spec: For example, if there are two rules: the first rule allows the request and the second rule denies it, when a request matches both rules, it will be allowed. items: - description: Rule defines the single authorization rule. + description: AuthorizationRule defines the single authorization + rule. properties: action: description: Action defines the action to be taken if the @@ -79,13 +80,17 @@ spec: - Allow - Deny type: string + name: + description: Name is a user-friendly name for the rule. + It's just for display purposes. + type: string principal: description: Principal specifies the client identity of a request. properties: - clientCIDR: + clientCIDRs: description: |- - ClientCIDR is the IP CIDR range of the client. + ClientCIDRs are the IP CIDR ranges of the client. Valid examples are "192.168.1.0/24" or "2001:db8::/64" diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index d29f7d10656..522693bbab4 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -8,7 +8,6 @@ package gatewayapi import ( "fmt" "math" - "net" "sort" "strings" "time" @@ -732,18 +731,12 @@ func buildRateLimitRule(rule egv1a1.RateLimitRule) (*ir.RateLimitRule, error) { distinct = true } - ip, ipn, err := net.ParseCIDR(sourceCIDR) + cidrMatch, err := parseCIDR(sourceCIDR) if err != nil { - return nil, fmt.Errorf("unable to translate rateLimit") - } - - mask, _ := ipn.Mask.Size() - irRule.CIDRMatch = &ir.CIDRMatch{ - CIDR: ipn.String(), - IPv6: ip.To4() == nil, - MaskLen: mask, - Distinct: distinct, + return nil, fmt.Errorf("unable to translate rateLimit: %w", err) } + cidrMatch.Distinct = distinct + irRule.CIDRMatch = cidrMatch } } return irRule, nil diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 2dfc04a1842..ff5f88c39f7 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -8,6 +8,7 @@ package gatewayapi import ( "errors" "fmt" + "net" "strings" v1 "k8s.io/api/core/v1" @@ -459,3 +460,18 @@ func listenersWithSameHTTPPort(xdsIR *ir.Xds, listener *ir.HTTPListener) []strin } return res } + +func parseCIDR(cidr string) (*ir.CIDRMatch, error) { + ip, ipn, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + + mask, _ := ipn.Mask.Size() + return &ir.CIDRMatch{ + CIDR: ipn.String(), + IP: ip.String(), + MaskLen: uint32(mask), + IsIPv6: ip.To4() == nil, + }, nil +} diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index e68cf3fabae..21da9836b9b 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -331,12 +331,13 @@ func (t *Translator) translateSecurityPolicyForRoute( ) error { // Build IR var ( - cors *ir.CORS - jwt *ir.JWT - oidc *ir.OIDC - basicAuth *ir.BasicAuth - extAuth *ir.ExtAuth - err, errs error + cors *ir.CORS + jwt *ir.JWT + oidc *ir.OIDC + basicAuth *ir.BasicAuth + extAuth *ir.ExtAuth + authorization *ir.Authorization + err, errs error ) if policy.Spec.CORS != nil { @@ -371,6 +372,12 @@ func (t *Translator) translateSecurityPolicyForRoute( } } + if policy.Spec.Authorization != nil { + if authorization, err = t.buildAuthorization(policy.Spec.Authorization); err != nil { + errs = errors.Join(errs, err) + } + } + // Apply IR to all relevant routes // Note: there are multiple features in a security policy, even if some of them // are invalid, we still want to apply the valid ones. @@ -384,11 +391,12 @@ func (t *Translator) translateSecurityPolicyForRoute( // This security policy matches the current route. // It should only be accepted if it doesn't match any other route r.Security = &ir.SecurityFeatures{ - CORS: cors, - JWT: jwt, - OIDC: oidc, - BasicAuth: basicAuth, - ExtAuth: extAuth, + CORS: cors, + JWT: jwt, + OIDC: oidc, + BasicAuth: basicAuth, + ExtAuth: extAuth, + Authorization: authorization, } } } @@ -403,12 +411,13 @@ func (t *Translator) translateSecurityPolicyForGateway( ) error { // Build IR var ( - cors *ir.CORS - jwt *ir.JWT - oidc *ir.OIDC - basicAuth *ir.BasicAuth - extAuth *ir.ExtAuth - err, errs error + cors *ir.CORS + jwt *ir.JWT + oidc *ir.OIDC + basicAuth *ir.BasicAuth + extAuth *ir.ExtAuth + authorization *ir.Authorization + err, errs error ) if policy.Spec.CORS != nil { @@ -443,6 +452,11 @@ func (t *Translator) translateSecurityPolicyForGateway( } } + if policy.Spec.Authorization != nil { + if authorization, err = t.buildAuthorization(policy.Spec.Authorization); err != nil { + errs = errors.Join(errs, err) + } + } // Apply IR to all the routes within the specific Gateway that originated // from the gateway to which this security policy was attached. // If the feature is already set, then skip it, since it must have be @@ -470,11 +484,12 @@ func (t *Translator) translateSecurityPolicyForGateway( } r.Security = &ir.SecurityFeatures{ - CORS: cors, - JWT: jwt, - OIDC: oidc, - BasicAuth: basicAuth, - ExtAuth: extAuth, + CORS: cors, + JWT: jwt, + OIDC: oidc, + BasicAuth: basicAuth, + ExtAuth: extAuth, + Authorization: authorization, } } } @@ -846,3 +861,34 @@ func irConfigName(policy *egv1a1.SecurityPolicy) string { strings.ToLower(KindSecurityPolicy), utils.NamespacedName(policy).String()) } + +func (t *Translator) buildAuthorization(authorization *egv1a1.Authorization) (*ir.Authorization, error) { + var ( + irAuth = &ir.Authorization{} + defaultAction = egv1a1.AuthorizationActionDeny + ) + + if authorization.DefaultAction != nil { + defaultAction = *authorization.DefaultAction + } + irAuth.DefaultAction = defaultAction + + for _, rule := range authorization.Rules { + principal := ir.Principal{} + + for _, cidr := range rule.Principal.ClientCIDRs { + cidrMatch, err := parseCIDR(cidr) + if err != nil { + return nil, fmt.Errorf("unable to translate authorization rule: %w", err) + } + + principal.ClientCIDRs = append(principal.ClientCIDRs, cidrMatch) + } + irAuth.Rules = append(irAuth.Rules, &ir.AuthorizationRule{ + Action: rule.Action, + Principal: principal, + }) + } + + return irAuth, nil +} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.out.yaml index 7e3d794b7c6..695ab6bd707 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit-default-route-level-limit.out.yaml @@ -196,7 +196,8 @@ xdsIR: - cidrMatch: cidr: 192.168.0.0/16 distinct: false - ipv6: false + ip: 192.168.0.0 + isIPv6: false maskLen: 16 headerMatches: - distinct: false diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.out.yaml index 9ebb100f64a..b7508bc79a7 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-local-ratelimit.out.yaml @@ -199,7 +199,8 @@ xdsIR: - cidrMatch: cidr: 192.168.0.0/16 distinct: false - ipv6: false + ip: 192.168.0.0 + isIPv6: false maskLen: 16 headerMatches: - distinct: false diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml index 3907527966b..bdb547ffea1 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-ratelimit.out.yaml @@ -338,7 +338,8 @@ xdsIR: - cidrMatch: cidr: 192.168.0.0/16 distinct: true - ipv6: false + ip: 192.168.0.0 + isIPv6: false maskLen: 16 headerMatches: [] limit: diff --git a/internal/gatewayapi/testdata/securitypolicy-with-authoriztion.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-authoriztion.in.yaml new file mode 100644 index 00000000000..57ed9a378b2 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-authoriztion.in.yaml @@ -0,0 +1,131 @@ +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 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-3 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/test" + backendRefs: + - name: service-1 + port: 8080 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway # This policy should attach httproute-2 + uid: b8284d0f-de82-4c65-b204-96a0d3f258a1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - 10.0.1.0/24 + - 10.0.2.0/24 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: policy-for-http-route-1 # This policy should attach httproute-1 + uid: 08335a80-83ba-4592-888f-6ac0bba44ce4 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + authorization: + defaultAction: Allow + rules: + - name: "deny-location-1" + action: Deny + principal: + clientCIDRs: + - 192.168.1.0/24 + - 192.168.2.0/24 + - name: "deny-location-2" + action: Deny + principal: + clientCIDRs: + - 10.75.1.0/24 + - 10.75.2.0/24 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: policy-for-http-route-3 # This policy should attach httproute-3 + uid: 08335a80-83ba-4592-888f-6ac0bba44ce4 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + authorization: {} diff --git a/internal/gatewayapi/testdata/securitypolicy-with-authoriztion.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-authoriztion.out.yaml new file mode 100755 index 00000000000..6e14ce3e56b --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-authoriztion.out.yaml @@ -0,0 +1,399 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 3 + 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 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + 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 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /bar + 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 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-3 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /test + 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 +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + 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 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route-1 + namespace: default + uid: 08335a80-83ba-4592-888f-6ac0bba44ce4 + spec: + authorization: + defaultAction: Allow + rules: + - action: Deny + name: deny-location-1 + principal: + clientCIDRs: + - 192.168.1.0/24 + - 192.168.2.0/24 + - action: Deny + name: deny-location-2 + principal: + clientCIDRs: + - 10.75.1.0/24 + - 10.75.2.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-http-route-3 + namespace: default + uid: 08335a80-83ba-4592-888f-6ac0bba44ce4 + spec: + authorization: + defaultAction: null + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + uid: b8284d0f-de82-4c65-b204-96a0d3f258a1 + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: null + principal: + clientCIDRs: + - 10.0.1.0/24 + - 10.0.2.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'This policy is being overridden by other securityPolicies for these + routes: [default/httproute-1 default/httproute-3]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-3/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-3/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /test + security: + authorization: + defaultAction: Deny + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + security: + authorization: + defaultAction: Allow + rules: + - action: Deny + principal: + clientCIDRs: + - cidr: 192.168.1.0/24 + distinct: false + ip: 192.168.1.0 + isIPv6: false + maskLen: 24 + - cidr: 192.168.2.0/24 + distinct: false + ip: 192.168.2.0 + isIPv6: false + maskLen: 24 + - action: Deny + principal: + clientCIDRs: + - cidr: 10.75.1.0/24 + distinct: false + ip: 10.75.1.0 + isIPv6: false + maskLen: 24 + - cidr: 10.75.2.0/24 + distinct: false + ip: 10.75.2.0 + isIPv6: false + maskLen: 24 + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar + security: + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - cidr: 10.0.1.0/24 + distinct: false + ip: 10.0.1.0 + isIPv6: false + maskLen: 24 + - cidr: 10.0.2.0/24 + distinct: false + ip: 10.0.2.0 + isIPv6: false + maskLen: 24 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index b77d26cf759..f1e1d293d7d 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -544,6 +544,8 @@ type SecurityFeatures struct { BasicAuth *BasicAuth `json:"basicAuth,omitempty" yaml:"basicAuth,omitempty"` // ExtAuth defines the schema for the external authorization. ExtAuth *ExtAuth `json:"extAuth,omitempty" yaml:"extAuth,omitempty"` + // Authorization defines the schema for the authorization. + Authorization *Authorization `json:"authorization,omitempty" yaml:"authorization,omitempty"` } func (s *SecurityFeatures) Printable() *SecurityFeatures { @@ -753,6 +755,36 @@ type GRPCExtAuthService struct { Authority string `json:"authority"` } +// Authorization defines the schema for the authorization. +// +// +k8s:deepcopy-gen=true +type Authorization struct { + // Rules defines the authorization rules. + Rules []*AuthorizationRule `json:"rules,omitempty"` + + // DefaultAction defines the default action to be taken if no rules match. + DefaultAction egv1a1.AuthorizationAction `json:"defaultAction"` +} + +// AuthorizationRule defines the schema for the authorization rule. +// +// +k8s:deepcopy-gen=true +type AuthorizationRule struct { + // Action defines the action to be taken if the rule matches. + Action egv1a1.AuthorizationAction `json:"action"` + + // Principal defines the principal to be matched. + Principal Principal `json:"principal"` +} + +// Principal defines the schema for the principal. +// +// +k8s:deepcopy-gen=true +type Principal struct { + // ClientCIDRs defines the client CIDRs to be matched. + ClientCIDRs []*CIDRMatch `json:"clientCIDRs,omitempty"` +} + // FaultInjection defines the schema for injecting faults into requests. // // +k8s:deepcopy-gen=true @@ -1430,8 +1462,9 @@ type RateLimitRule struct { type CIDRMatch struct { CIDR string `json:"cidr" yaml:"cidr"` - IPv6 bool `json:"ipv6" yaml:"ipv6"` - MaskLen int `json:"maskLen" yaml:"maskLen"` + IP string `json:"ip" yaml:"ip"` + MaskLen uint32 `json:"maskLen" yaml:"maskLen"` + IsIPv6 bool `json:"isIPv6" yaml:"isIPv6"` // Distinct means that each IP Address within the specified Source IP CIDR is treated as a distinct client selector // and uses a separate rate limit bucket/counter. Distinct bool `json:"distinct" yaml:"distinct"` diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 0024414525f..b72f7f06498 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -124,6 +124,48 @@ func (in *AddHeader) DeepCopy() *AddHeader { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authorization) DeepCopyInto(out *Authorization) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]*AuthorizationRule, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(AuthorizationRule) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authorization. +func (in *Authorization) DeepCopy() *Authorization { + if in == nil { + return nil + } + out := new(Authorization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationRule) DeepCopyInto(out *AuthorizationRule) { + *out = *in + in.Principal.DeepCopyInto(&out.Principal) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationRule. +func (in *AuthorizationRule) DeepCopy() *AuthorizationRule { + if in == nil { + return nil + } + out := new(AuthorizationRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackOffPolicy) DeepCopyInto(out *BackOffPolicy) { *out = *in @@ -1573,6 +1615,32 @@ func (in *PerRetryPolicy) DeepCopy() *PerRetryPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Principal) DeepCopyInto(out *Principal) { + *out = *in + if in.ClientCIDRs != nil { + in, out := &in.ClientCIDRs, &out.ClientCIDRs + *out = make([]*CIDRMatch, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(CIDRMatch) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Principal. +func (in *Principal) DeepCopy() *Principal { + if in == nil { + return nil + } + out := new(Principal) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProxyInfra) DeepCopyInto(out *ProxyInfra) { *out = *in @@ -1915,6 +1983,11 @@ func (in *SecurityFeatures) DeepCopyInto(out *SecurityFeatures) { *out = new(ExtAuth) (*in).DeepCopyInto(*out) } + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(Authorization) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityFeatures. diff --git a/internal/xds/translator/authorization.go b/internal/xds/translator/authorization.go new file mode 100644 index 00000000000..4a2629cf177 --- /dev/null +++ b/internal/xds/translator/authorization.go @@ -0,0 +1,266 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "errors" + "fmt" + + cncfv3 "github.com/cncf/xds/go/xds/core/v3" + matcherv3 "github.com/cncf/xds/go/xds/type/matcher/v3" + configv3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + rbacconfigv3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + rbacv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + networkinput "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/common_inputs/network/v3" + ipmatcherv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/matching/input_matchers/ip/v3" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/wrapperspb" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +func init() { + registerHTTPFilter(&rbac{}) +} + +type rbac struct{} + +var _ httpFilter = &rbac{} + +// patchHCM builds and appends the RBAC Filter to the HTTP Connection Manager if +// applicable. +func (*rbac) patchHCM( + mgr *hcmv3.HttpConnectionManager, + irListener *ir.HTTPListener, +) error { + if mgr == nil { + return errors.New("hcm is nil") + } + + if irListener == nil { + return errors.New("ir listener is nil") + } + + if !listenerContainsRBAC(irListener) { + return nil + } + + // Return early if filter already exists. + for _, f := range mgr.HttpFilters { + if f.Name == string(egv1a1.EnvoyFilterRBAC) { + return nil + } + } + + rbacFilter, err := buildHCMRBACFilter() + if err != nil { + return err + } + + // Ensure the RBAC filter is the first one in the filter chain. + mgr.HttpFilters = append([]*hcmv3.HttpFilter{rbacFilter}, mgr.HttpFilters...) + + return nil +} + +// buildHCMRBACFilter returns a RBAC filter from the provided IR listener. +func buildHCMRBACFilter() (*hcmv3.HttpFilter, error) { + rbacProto := &rbacv3.RBAC{} + rbacAny, err := anypb.New(rbacProto) + if err != nil { + return nil, err + } + + return &hcmv3.HttpFilter{ + Name: string(egv1a1.EnvoyFilterRBAC), + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: rbacAny, + }, + }, nil +} + +// listenerContainsRBAC returns true if the provided listener has RBAC +// policies attached to its routes. +func listenerContainsRBAC(irListener *ir.HTTPListener) bool { + if irListener == nil { + return false + } + + for _, route := range irListener.Routes { + if route.Security != nil && route.Security.Authorization != nil { + return true + } + } + + return false +} + +// patchRoute patches the provided route with the RBAC config if applicable. +func (*rbac) patchRoute(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.Security == nil || irRoute.Security.Authorization == nil { + return nil + } + + filterCfg := route.GetTypedPerFilterConfig() + if _, ok := filterCfg[string(egv1a1.EnvoyFilterRBAC)]; ok { + // This should not happen since this is the only place where the RBAC + // filter is added in a route. + return fmt.Errorf("route already contains rbac config: %+v", route) + } + + var ( + authorization = irRoute.Security.Authorization + allowAction *anypb.Any + denyAction *anypb.Any + sourceIPInput *anypb.Any + ipMatcher *anypb.Any + matcherList []*matcherv3.Matcher_MatcherList_FieldMatcher + err error + ) + + allow := &rbacconfigv3.Action{ + Name: "ALLOW", + Action: rbacconfigv3.RBAC_ALLOW, + } + if allowAction, err = anypb.New(allow); err != nil { + return err + } + + deny := &rbacconfigv3.Action{ + Name: "DENY", + Action: rbacconfigv3.RBAC_DENY, + } + if denyAction, err = anypb.New(deny); err != nil { + return err + } + + // Build a list of matchers based on the rules. + // The matchers will be evaluated in order, and the first one that matches + // will be used to determine the action, the rest of the matchers will be + // skipped. + // If no matcher matches, the default action will be used. + for _, rule := range authorization.Rules { + // Build the IPMatcher based on the client CIDRs. + ipRangeMatcher := &ipmatcherv3.Ip{ + StatPrefix: "source_ip", + } + + for _, cidr := range rule.Principal.ClientCIDRs { + ipRangeMatcher.CidrRanges = append(ipRangeMatcher.CidrRanges, &configv3.CidrRange{ + AddressPrefix: cidr.IP, + PrefixLen: &wrapperspb.UInt32Value{ + Value: cidr.MaskLen, + }, + }) + } + + if ipMatcher, err = anypb.New(ipRangeMatcher); err != nil { + return err + } + + if sourceIPInput, err = anypb.New(&networkinput.SourceIPInput{}); err != nil { + return err + } + + // Determine the action for the current rule. + ruleAction := allowAction + if rule.Action == egv1a1.AuthorizationActionDeny { + ruleAction = denyAction + } + + // Add the matcher generated with the current rule to the matcher list. + matcherList = append(matcherList, &matcherv3.Matcher_MatcherList_FieldMatcher{ + Predicate: &matcherv3.Matcher_MatcherList_Predicate{ + MatchType: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_{ + SinglePredicate: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate{ + Input: &cncfv3.TypedExtensionConfig{ + Name: "source_ip", + TypedConfig: sourceIPInput, + }, + Matcher: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_CustomMatch{ + CustomMatch: &cncfv3.TypedExtensionConfig{ + Name: "ip_matcher", + TypedConfig: ipMatcher, + }, + }, + }, + }, + }, + OnMatch: &matcherv3.Matcher_OnMatch{ + OnMatch: &matcherv3.Matcher_OnMatch_Action{ + Action: &cncfv3.TypedExtensionConfig{ + Name: "action", + TypedConfig: ruleAction, + }, + }, + }, + }) + } + + // Set the default action. + defaultAction := denyAction + if authorization.DefaultAction == egv1a1.AuthorizationActionAllow { + defaultAction = allowAction + } + + routeCfgProto := &rbacv3.RBACPerRoute{ + Rbac: &rbacv3.RBAC{ + Matcher: &matcherv3.Matcher{ + MatcherType: &matcherv3.Matcher_MatcherList_{ + MatcherList: &matcherv3.Matcher_MatcherList{ + Matchers: matcherList, + }, + }, + OnNoMatch: &matcherv3.Matcher_OnMatch{ + OnMatch: &matcherv3.Matcher_OnMatch_Action{ + Action: &cncfv3.TypedExtensionConfig{ + Name: "default", + TypedConfig: defaultAction, + }, + }, + }, + }, + }, + } + + // If there are no matchers, the default action will be used for all requests. + // Setting the matcher type to nil since Proto validation will fail if the list + // is empty. + if len(matcherList) == 0 { + routeCfgProto.Rbac.Matcher.MatcherType = nil + } + + if err = routeCfgProto.ValidateAll(); err != nil { + return err + } + + routeCfgAny, err := anypb.New(routeCfgProto) + if err != nil { + return err + } + + if filterCfg == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + + route.TypedPerFilterConfig[string(egv1a1.EnvoyFilterRBAC)] = routeCfgAny + + return nil +} + +func (c *rbac) patchResources(*types.ResourceVersionTable, []*ir.HTTPRoute) error { + return nil +} diff --git a/internal/xds/translator/extauth.go b/internal/xds/translator/extauth.go index 0d8edfce242..3961a0b13b0 100644 --- a/internal/xds/translator/extauth.go +++ b/internal/xds/translator/extauth.go @@ -37,7 +37,6 @@ var _ httpFilter = &extAuth{} // if applicable, and it does not already exist. // Note: this method creates an ext_authz filter for each route that contains an ExtAuthz config. // The filter is disabled by default. It is enabled on the route level. -// TODO: zhaohuabing avoid duplicated HTTP filters func (*extAuth) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { var errs error diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index 5738d621eb8..1c50b5b52a2 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -17,7 +17,7 @@ import ( "github.com/envoyproxy/go-control-plane/pkg/wellknown" "k8s.io/utils/ptr" - "github.com/envoyproxy/gateway/api/v1alpha1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/xds/filters" "github.com/envoyproxy/gateway/internal/xds/types" @@ -94,6 +94,7 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { // Set a rational order for all the filters. // When the fault filter is configured to be at the first, the computation of // the remaining filters is skipped when rejected early + // TODO (zhaohuabing): remove duplicate filter type constants and replace them with the type constants in the api package switch { case isFilterType(filter, wellknown.Fault): order = 1 @@ -111,12 +112,14 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { order = 7 + mustGetFilterIndex(filter.Name) case isFilterType(filter, wasmFilter): order = 100 + mustGetFilterIndex(filter.Name) - case isFilterType(filter, localRateLimitFilter): + case isFilterType(filter, string(egv1a1.EnvoyFilterRBAC)): order = 201 - case isFilterType(filter, wellknown.HTTPRateLimit): + case isFilterType(filter, localRateLimitFilter): order = 202 - case isFilterType(filter, wellknown.Router): + case isFilterType(filter, wellknown.HTTPRateLimit): order = 203 + case isFilterType(filter, wellknown.Router): + order = 204 } return &OrderedHTTPFilter{ @@ -144,7 +147,7 @@ func (o OrderedHTTPFilters) Swap(i, j int) { // For example, the cors filter should be put at the first to avoid unnecessary // processing of other filters for unauthorized cross-region access. // The router filter must be the last one since it's a terminal filter. -func sortHTTPFilters(filters []*hcmv3.HttpFilter, filterOrder []v1alpha1.FilterPosition) []*hcmv3.HttpFilter { +func sortHTTPFilters(filters []*hcmv3.HttpFilter, filterOrder []egv1a1.FilterPosition) []*hcmv3.HttpFilter { // Sort the filters in the default order. orderedFilters := make(OrderedHTTPFilters, len(filters)) for i := 0; i < len(filters); i++ { diff --git a/internal/xds/translator/httpfilters_test.go b/internal/xds/translator/httpfilters_test.go index 2f277aa969a..5516dc0d95b 100644 --- a/internal/xds/translator/httpfilters_test.go +++ b/internal/xds/translator/httpfilters_test.go @@ -13,14 +13,14 @@ import ( "github.com/stretchr/testify/assert" "k8s.io/utils/ptr" - "github.com/envoyproxy/gateway/api/v1alpha1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) func Test_sortHTTPFilters(t *testing.T) { tests := []struct { name string filters []*hcmv3.HttpFilter - filterOrder []v1alpha1.FilterPosition + filterOrder []egv1a1.FilterPosition want []*hcmv3.HttpFilter }{ { @@ -40,6 +40,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, want: []*hcmv3.HttpFilter{ httpFilterForTest(wellknown.Fault), @@ -53,6 +54,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/2"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wellknown.HTTPRateLimit), httpFilterForTest(wellknown.Router), @@ -75,15 +77,16 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, - filterOrder: []v1alpha1.FilterPosition{ + filterOrder: []egv1a1.FilterPosition{ { - Name: v1alpha1.EnvoyFilterFault, - After: ptr.To(v1alpha1.EnvoyFilterCORS), + Name: egv1a1.EnvoyFilterFault, + After: ptr.To(egv1a1.EnvoyFilterCORS), }, { - Name: v1alpha1.EnvoyFilterRateLimit, - Before: ptr.To(v1alpha1.EnvoyFilterJWTAuthn), + Name: egv1a1.EnvoyFilterRateLimit, + Before: ptr.To(egv1a1.EnvoyFilterJWTAuthn), }, }, want: []*hcmv3.HttpFilter{ @@ -99,6 +102,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/2"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wellknown.Router), }, @@ -120,11 +124,12 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, - filterOrder: []v1alpha1.FilterPosition{ + filterOrder: []egv1a1.FilterPosition{ { - Name: v1alpha1.EnvoyFilterRateLimit, - Before: ptr.To(v1alpha1.EnvoyFilterWasm), + Name: egv1a1.EnvoyFilterRateLimit, + Before: ptr.To(egv1a1.EnvoyFilterWasm), }, }, want: []*hcmv3.HttpFilter{ @@ -140,6 +145,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/2"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wellknown.Router), }, @@ -161,11 +167,12 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, - filterOrder: []v1alpha1.FilterPosition{ + filterOrder: []egv1a1.FilterPosition{ { - Name: v1alpha1.EnvoyFilterJWTAuthn, - After: ptr.To(v1alpha1.EnvoyFilterWasm), + Name: egv1a1.EnvoyFilterJWTAuthn, + After: ptr.To(egv1a1.EnvoyFilterWasm), }, }, want: []*hcmv3.HttpFilter{ @@ -180,6 +187,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/2"), httpFilterForTest(jwtAuthn), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wellknown.HTTPRateLimit), httpFilterForTest(wellknown.Router), @@ -202,11 +210,12 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, - filterOrder: []v1alpha1.FilterPosition{ + filterOrder: []egv1a1.FilterPosition{ { - Name: v1alpha1.EnvoyFilterWasm, - Before: ptr.To(v1alpha1.EnvoyFilterJWTAuthn), + Name: egv1a1.EnvoyFilterWasm, + Before: ptr.To(egv1a1.EnvoyFilterJWTAuthn), }, }, want: []*hcmv3.HttpFilter{ @@ -221,6 +230,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(jwtAuthn), httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wellknown.HTTPRateLimit), httpFilterForTest(wellknown.Router), @@ -243,11 +253,12 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, - filterOrder: []v1alpha1.FilterPosition{ + filterOrder: []egv1a1.FilterPosition{ { - Name: v1alpha1.EnvoyFilterWasm, - After: ptr.To(v1alpha1.EnvoyFilterRateLimit), + Name: egv1a1.EnvoyFilterWasm, + After: ptr.To(egv1a1.EnvoyFilterRateLimit), }, }, want: []*hcmv3.HttpFilter{ @@ -259,6 +270,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(jwtAuthn), httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wellknown.HTTPRateLimit), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), @@ -284,11 +296,12 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, - filterOrder: []v1alpha1.FilterPosition{ + filterOrder: []egv1a1.FilterPosition{ { - Name: v1alpha1.EnvoyFilterWasm, - Before: ptr.To(v1alpha1.EnvoyFilterExtProc), + Name: egv1a1.EnvoyFilterWasm, + Before: ptr.To(egv1a1.EnvoyFilterExtProc), }, }, want: []*hcmv3.HttpFilter{ @@ -303,6 +316,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/2"), httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wellknown.HTTPRateLimit), httpFilterForTest(wellknown.Router), @@ -325,11 +339,12 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, - filterOrder: []v1alpha1.FilterPosition{ + filterOrder: []egv1a1.FilterPosition{ { - Name: v1alpha1.EnvoyFilterExtProc, - After: ptr.To(v1alpha1.EnvoyFilterWasm), + Name: egv1a1.EnvoyFilterExtProc, + After: ptr.To(egv1a1.EnvoyFilterWasm), }, }, want: []*hcmv3.HttpFilter{ @@ -344,6 +359,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/2"), httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wellknown.HTTPRateLimit), httpFilterForTest(wellknown.Router), @@ -366,23 +382,24 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(extProcFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/0"), httpFilterForTest(localRateLimitFilter), httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/1"), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), }, - filterOrder: []v1alpha1.FilterPosition{ + filterOrder: []egv1a1.FilterPosition{ { - Name: v1alpha1.EnvoyFilterLocalRateLimit, - Before: ptr.To(v1alpha1.EnvoyFilterJWTAuthn), + Name: egv1a1.EnvoyFilterLocalRateLimit, + Before: ptr.To(egv1a1.EnvoyFilterJWTAuthn), }, { - Name: v1alpha1.EnvoyFilterLocalRateLimit, - After: ptr.To(v1alpha1.EnvoyFilterCORS), + Name: egv1a1.EnvoyFilterLocalRateLimit, + After: ptr.To(egv1a1.EnvoyFilterCORS), }, { - Name: v1alpha1.EnvoyFilterWasm, - Before: ptr.To(v1alpha1.EnvoyFilterOAuth2), + Name: egv1a1.EnvoyFilterWasm, + Before: ptr.To(egv1a1.EnvoyFilterOAuth2), }, { - Name: v1alpha1.EnvoyFilterExtProc, - Before: ptr.To(v1alpha1.EnvoyFilterWasm), + Name: egv1a1.EnvoyFilterExtProc, + Before: ptr.To(egv1a1.EnvoyFilterWasm), }, }, want: []*hcmv3.HttpFilter{ @@ -398,6 +415,7 @@ func Test_sortHTTPFilters(t *testing.T) { httpFilterForTest(wasmFilter + "/envoyextensionpolicy/default/policy-for-http-route-1/2"), httpFilterForTest(oauth2Filter + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(jwtAuthn), + httpFilterForTest(string(egv1a1.EnvoyFilterRBAC) + "/securitypolicy/default/policy-for-http-route-1"), httpFilterForTest(wellknown.HTTPRateLimit), httpFilterForTest(wellknown.Router), }, diff --git a/internal/xds/translator/local_ratelimit.go b/internal/xds/translator/local_ratelimit.go index 1a0d88d5da0..231bda44f60 100644 --- a/internal/xds/translator/local_ratelimit.go +++ b/internal/xds/translator/local_ratelimit.go @@ -249,8 +249,8 @@ func buildRouteLocalRateLimits(local *ir.LocalRateLimit) ( // Setup MaskedRemoteAddress action mra := &routev3.RateLimit_Action_MaskedRemoteAddress{} - maskLen := &wrapperspb.UInt32Value{Value: uint32(rule.CIDRMatch.MaskLen)} - if rule.CIDRMatch.IPv6 { + maskLen := &wrapperspb.UInt32Value{Value: rule.CIDRMatch.MaskLen} + if rule.CIDRMatch.IsIPv6 { mra.V6PrefixMaskLen = maskLen } else { mra.V4PrefixMaskLen = maskLen diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index 4534f303fe2..1167b6e0c71 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -225,8 +225,8 @@ func buildRouteRateLimits(descriptorPrefix string, global *ir.GlobalRateLimit) [ if rule.CIDRMatch != nil { // Setup MaskedRemoteAddress action mra := &routev3.RateLimit_Action_MaskedRemoteAddress{} - maskLen := &wrapperspb.UInt32Value{Value: uint32(rule.CIDRMatch.MaskLen)} - if rule.CIDRMatch.IPv6 { + maskLen := &wrapperspb.UInt32Value{Value: rule.CIDRMatch.MaskLen} + if rule.CIDRMatch.IsIPv6 { mra.V6PrefixMaskLen = maskLen } else { mra.V4PrefixMaskLen = maskLen diff --git a/internal/xds/translator/testdata/in/xds-ir/authorization.yaml b/internal/xds/translator/testdata/in/xds-ir/authorization.yaml new file mode 100644 index 00000000000..738b757d112 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/authorization.yaml @@ -0,0 +1,109 @@ +http: +- address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-3/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-3/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /test + security: + authorization: + defaultAction: Deny + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + security: + authorization: + defaultAction: Allow + rules: + - action: Deny + principal: + clientCIDRs: + - cidr: 192.168.1.0/24 + distinct: false + ip: 192.168.1.0 + isIPv6: false + maskLen: 24 + - cidr: 192.168.2.0/24 + distinct: false + ip: 192.168.2.0 + isIPv6: false + maskLen: 24 + - action: Deny + principal: + clientCIDRs: + - cidr: 10.75.1.0/24 + distinct: false + ip: 10.75.1.0 + isIPv6: false + maskLen: 24 + - cidr: 10.75.2.0/24 + distinct: false + ip: 10.75.2.0 + isIPv6: false + maskLen: 24 + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar + security: + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - cidr: 10.0.1.0/24 + distinct: false + ip: 10.0.1.0 + isIPv6: false + maskLen: 24 + - cidr: 10.0.2.0/24 + distinct: false + ip: 10.0.2.0 + isIPv6: false + maskLen: 24 diff --git a/internal/xds/translator/testdata/out/xds-ir/authorization.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/authorization.clusters.yaml new file mode 100644 index 00000000000..b3f75f0e04e --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/authorization.clusters.yaml @@ -0,0 +1,51 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-3/rule/0 + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-3/rule/0 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-1/rule/0 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-2/rule/0 + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-2/rule/0 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/authorization.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/authorization.endpoints.yaml new file mode 100644 index 00000000000..24596d841a3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/authorization.endpoints.yaml @@ -0,0 +1,36 @@ +- clusterName: httproute/default/httproute-3/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-3/rule/0/backend/0 +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 +- clusterName: httproute/default/httproute-2/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-2/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/authorization.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/authorization.listeners.yaml new file mode 100644 index 00000000000..7469999607f --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/authorization.listeners.yaml @@ -0,0 +1,37 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.rbac + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: envoy-gateway/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http + useRemoteAddress: true + drainType: MODIFY_ONLY + name: envoy-gateway/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/authorization.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/authorization.routes.yaml new file mode 100644 index 00000000000..9d0cc127036 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/authorization.routes.yaml @@ -0,0 +1,135 @@ +- ignorePortInHostMatching: true + name: envoy-gateway/gateway-1/http + virtualHosts: + - domains: + - www.example.com + name: envoy-gateway/gateway-1/http/www_example_com + routes: + - match: + pathSeparatedPrefix: /test + name: httproute/default/httproute-3/rule/0/match/0/www_example_com + route: + cluster: httproute/default/httproute-3/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + matcher: + onNoMatch: + action: + name: default + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + action: DENY + name: DENY + - match: + pathSeparatedPrefix: /foo + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + route: + cluster: httproute/default/httproute-1/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + matcher: + matcherList: + matchers: + - onMatch: + action: + name: action + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + action: DENY + name: DENY + predicate: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 192.168.1.0 + prefixLen: 24 + - addressPrefix: 192.168.2.0 + prefixLen: 24 + statPrefix: source_ip + input: + name: source_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + - onMatch: + action: + name: action + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + action: DENY + name: DENY + predicate: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 10.75.1.0 + prefixLen: 24 + - addressPrefix: 10.75.2.0 + prefixLen: 24 + statPrefix: source_ip + input: + name: source_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + onNoMatch: + action: + name: default + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + name: ALLOW + - match: + pathSeparatedPrefix: /bar + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + route: + cluster: httproute/default/httproute-2/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + matcher: + matcherList: + matchers: + - onMatch: + action: + name: action + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + name: ALLOW + predicate: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 10.0.1.0 + prefixLen: 24 + - addressPrefix: 10.0.2.0 + prefixLen: 24 + statPrefix: source_ip + input: + name: source_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + onNoMatch: + action: + name: default + typedConfig: + '@type': type.googleapis.com/envoy.config.rbac.v3.Action + action: DENY + name: DENY diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 6ac4fe6b123..618b78267ad 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -195,13 +195,48 @@ _Appears in:_ Authorization defines the authorization configuration. + +Note: if neither `Rules` nor `DefaultAction` is specified, the default action is to deny all requests. + _Appears in:_ - [SecurityPolicySpec](#securitypolicyspec) | Field | Type | Required | Description | | --- | --- | --- | --- | -| `rules` | _[Rule](#rule) array_ | false | Rules defines a list of authorization rules.
These rules are evaluated in order, the first matching rule will be applied,
and the rest will be skipped.

For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed. | -| `defaultAction` | _[RuleActionType](#ruleactiontype)_ | false | DefaultAction defines the default action to be taken if no rules match.
If not specified, the default action is Deny. | +| `rules` | _[AuthorizationRule](#authorizationrule) array_ | false | Rules defines a list of authorization rules.
These rules are evaluated in order, the first matching rule will be applied,
and the rest will be skipped.

For example, if there are two rules: the first rule allows the request
and the second rule denies it, when a request matches both rules, it will be allowed. | +| `defaultAction` | _[AuthorizationAction](#authorizationaction)_ | false | DefaultAction defines the default action to be taken if no rules match.
If not specified, the default action is Deny. | + + +#### AuthorizationAction + +_Underlying type:_ _string_ + +AuthorizationAction defines the action to be taken if a rule matches. + +_Appears in:_ +- [Authorization](#authorization) +- [AuthorizationRule](#authorizationrule) + +| Value | Description | +| ----- | ----------- | +| `Allow` | AuthorizationActionAllow is the action to allow the request.
| +| `Deny` | AuthorizationActionDeny is the action to deny the request.
| + + +#### AuthorizationRule + + + +AuthorizationRule defines the single authorization rule. + +_Appears in:_ +- [Authorization](#authorization) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `name` | _string_ | false | Name is a user-friendly name for the rule. It's just for display purposes. | +| `action` | _[AuthorizationAction](#authorizationaction)_ | true | Action defines the action to be taken if the rule matches. | +| `principal` | _[Principal](#principal)_ | true | Principal specifies the client identity of a request. | #### BackOffPolicy @@ -823,6 +858,7 @@ _Appears in:_ | `envoy.filters.http.wasm` | EnvoyFilterWasm defines the Envoy HTTP WebAssembly filter.
| | `envoy.filters.http.local_ratelimit` | EnvoyFilterLocalRateLimit defines the Envoy HTTP local rate limit filter.
| | `envoy.filters.http.ratelimit` | EnvoyFilterRateLimit defines the Envoy HTTP rate limit filter.
| +| `envoy.filters.http.rbac` | EnvoyFilterRBAC defines the Envoy RBAC filter.
| | `envoy.filters.http.router` | EnvoyFilterRouter defines the Envoy HTTP router filter.
| @@ -1269,7 +1305,7 @@ _Appears in:_ | `extraArgs` | _string array_ | false | ExtraArgs defines additional command line options that are provided to Envoy.
More info: https://www.envoyproxy.io/docs/envoy/latest/operations/cli#command-line-options
Note: some command line options are used internally(e.g. --log-level) so they cannot be provided here. | | `mergeGateways` | _boolean_ | false | MergeGateways defines if Gateway resources should be merged onto the same Envoy Proxy Infrastructure.
Setting this field to true would merge all Gateway Listeners under the parent Gateway Class.
This means that the port, protocol and hostname tuple must be unique for every listener.
If a duplicate listener is detected, the newer listener (based on timestamp) will be rejected and its status will be updated with a "Accepted=False" condition. | | `shutdown` | _[ShutdownConfig](#shutdownconfig)_ | false | Shutdown defines configuration for graceful envoy shutdown process. | -| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_authn

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.router | +| `filterOrder` | _[FilterPosition](#filterposition) array_ | false | FilterOrder defines the order of filters in the Envoy proxy's HTTP filter chain.
The FilterPosition in the list will be applied in the order they are defined.
If unspecified, the default filter order is applied.
Default filter order is:

- envoy.filters.http.fault

- envoy.filters.http.cors

- envoy.filters.http.ext_authz

- envoy.filters.http.basic_authn

- envoy.filters.http.oauth2

- envoy.filters.http.jwt_authn

- envoy.filters.http.ext_proc

- envoy.filters.http.wasm

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit

- envoy.filters.http.ratelimit

- envoy.filters.http.router | | `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ | false | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.
These settings are applied on backends for which TLS policies are specified. | @@ -2399,11 +2435,11 @@ _Appears in:_ Principal specifies the client identity of a request. _Appears in:_ -- [Rule](#rule) +- [AuthorizationRule](#authorizationrule) | Field | Type | Required | Description | | --- | --- | --- | --- | -| `clientCIDR` | _string array_ | true | ClientCIDR is the IP CIDR range of the client.
Valid examples are "192.168.1.0/24" or "2001:db8::/64"

By default, the client IP is inferred from the x-forwarder-for header and proxy protocol.
You can use the `EnableProxyProtocol` and `ClientIPDetection` options in
the `ClientTrafficPolicy` to configure how the client IP is detected. | +| `clientCIDRs` | _string array_ | true | ClientCIDRs are the IP CIDR ranges of the client.
Valid examples are "192.168.1.0/24" or "2001:db8::/64"

By default, the client IP is inferred from the x-forwarder-for header and proxy protocol.
You can use the `EnableProxyProtocol` and `ClientIPDetection` options in
the `ClientTrafficPolicy` to configure how the client IP is detected. | #### ProcessingModeOptions @@ -3037,37 +3073,6 @@ _Appears in:_ | `httpStatusCodes` | _[HTTPStatus](#httpstatus) array_ | false | HttpStatusCodes specifies the http status codes to be retried.
The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. | -#### Rule - - - -Rule defines the single authorization rule. - -_Appears in:_ -- [Authorization](#authorization) - -| Field | Type | Required | Description | -| --- | --- | --- | --- | -| `action` | _[RuleActionType](#ruleactiontype)_ | true | Action defines the action to be taken if the rule matches. | -| `principal` | _[Principal](#principal)_ | true | Principal specifies the client identity of a request. | - - -#### RuleActionType - -_Underlying type:_ _string_ - -RuleActionType specifies the types of authorization rule action. - -_Appears in:_ -- [Authorization](#authorization) -- [Rule](#rule) - -| Value | Description | -| ----- | ----------- | -| `Allow` | Allow is the action to allow the request.
| -| `Deny` | Deny is the action to deny the request.
| - - #### SecurityPolicy diff --git a/test/e2e/testdata/authorization-client-ip.yaml b/test/e2e/testdata/authorization-client-ip.yaml new file mode 100644 index 00000000000..dde54a832cd --- /dev/null +++ b/test/e2e/testdata/authorization-client-ip.yaml @@ -0,0 +1,98 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-authorization-client-ip-1 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: Exact + value: /protected1 + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-authorization-client-ip-2 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: Exact + value: /protected2 + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: authorization-client-ip-1 + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-authorization-client-ip-1 + authorization: + defaultAction: Allow + rules: + - name: "deny-location-1" # First matching rule is applied, so 192.168.1.0/24 will be denied + action: Deny + principal: + clientCIDRs: + - 192.168.1.0/24 + - name: "allow-location-1" + action: Allow + principal: + clientCIDRs: + - 192.168.1.0/24 + - 192.168.2.0/24 # First matching rule is applied, so 12.168.2.0/24 will be allowed + - name: "deny-location-2" + action: Allow + principal: + clientCIDRs: + - 192.168.2.0/24 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: authorization-client-ip-2 + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-authorization-client-ip-2 + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - 10.0.1.0/24 + - 10.0.2.0/24 +--- +# This is a client traffic policy that enables client IP detection using a custom header. +# So, the client IP can be detected from the custom header `client-ip` and used for authorization. +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: ClientTrafficPolicy +metadata: + name: enable-client-ip-detection + namespace: gateway-conformance-infra +spec: + clientIPDetection: + customHeader: + name: client-ip + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace diff --git a/test/e2e/testdata/authorization-default-action.yaml b/test/e2e/testdata/authorization-default-action.yaml new file mode 100644 index 00000000000..bc35b2ba80b --- /dev/null +++ b/test/e2e/testdata/authorization-default-action.yaml @@ -0,0 +1,58 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-authorization-empty + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: Exact + value: /empty-authorization + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-authorization-allow-all + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: Exact + value: /allow-all + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: authorization-empty + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-authorization-empty + authorization: {} # An empty authorization policy means deny all since default action is deny +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: authorization-allow-all + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-authorization-allow-all + authorization: # Allow all since default action is allow and no rules are defined + defaultAction: Allow diff --git a/test/e2e/testdata/basic-auth.yaml b/test/e2e/testdata/basic-auth.yaml index 036bb788eaa..7bf3e0f6716 100644 --- a/test/e2e/testdata/basic-auth.yaml +++ b/test/e2e/testdata/basic-auth.yaml @@ -59,7 +59,6 @@ spec: group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-basic-auth-1 - namespace: gateway-conformance-infra basicAuth: users: name: "basic-auth-users-secret-1" @@ -74,7 +73,6 @@ spec: group: gateway.networking.k8s.io kind: HTTPRoute name: http-with-basic-auth-2 - namespace: gateway-conformance-infra basicAuth: users: name: "basic-auth-users-secret-2" diff --git a/test/e2e/tests/authorization-client-ip.go b/test/e2e/tests/authorization-client-ip.go new file mode 100644 index 00000000000..1adc1aa25ed --- /dev/null +++ b/test/e2e/tests/authorization-client-ip.go @@ -0,0 +1,174 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e +// +build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" +) + +func init() { + ConformanceTests = append(ConformanceTests, AuthorizationClientIPTest) +} + +var AuthorizationClientIPTest = suite.ConformanceTest{ + ShortName: "Authorization with client IP", + Description: "Authorization with client IP Allow/Deny list", + Manifests: []string{"testdata/authorization-client-ip.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + route1NN := types.NamespacedName{Name: "http-with-authorization-client-ip-1", Namespace: ns} + route2NN := types.NamespacedName{Name: "http-with-authorization-client-ip-2", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), route1NN, route2NN) + + ancestorRef := gwv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwv1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwv1.ObjectName(gwNN.Name), + } + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "authorization-client-ip-1", Namespace: ns}, suite.ControllerName, ancestorRef) + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "authorization-client-ip-2", Namespace: ns}, suite.ControllerName, ancestorRef) + + t.Run("first route-denied IP", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected1", + Headers: map[string]string{ + "client-ip": "192.168.1.1", // in the denied list + }, + }, + Response: http.Response{ + StatusCode: 403, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + t.Run("first route-allowed IP", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected1", + Headers: map[string]string{ + "client-ip": "192.168.2.1", // in the allowed list + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + t.Run("first route-default action: allow", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected1", + Headers: map[string]string{ + "client-ip": "192.168.3.1", // not in the denied list + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + // Test the second route + t.Run("second route-allowed IP", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected2", + Headers: map[string]string{ + "client-ip": "10.0.1.1", // in the allowed list + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + t.Run("second route-default action: deny", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected2", + Headers: map[string]string{ + "client-ip": "192.168.3.1", // not in the allowed list + }, + }, + Response: http.Response{ + StatusCode: 403, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + }, +} diff --git a/test/e2e/tests/authorization-default-action.go b/test/e2e/tests/authorization-default-action.go new file mode 100644 index 00000000000..1a58c32e381 --- /dev/null +++ b/test/e2e/tests/authorization-default-action.go @@ -0,0 +1,92 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e +// +build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" +) + +func init() { + ConformanceTests = append(ConformanceTests, AuthorizationDefaultActionTest) +} + +var AuthorizationDefaultActionTest = suite.ConformanceTest{ + ShortName: "Authorization with default actions", + Description: "Authorization with default actions", + Manifests: []string{"testdata/authorization-default-action.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + route1NN := types.NamespacedName{Name: "http-with-authorization-empty", Namespace: ns} + route2NN := types.NamespacedName{Name: "http-with-authorization-allow-all", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), route1NN, route2NN) + + ancestorRef := gwv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwv1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwv1.ObjectName(gwNN.Name), + } + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "authorization-empty", Namespace: ns}, suite.ControllerName, ancestorRef) + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "authorization-allow-all", Namespace: ns}, suite.ControllerName, ancestorRef) + + t.Run("Empty Authorization should deny all traffic", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/empty-authorization", + }, + Response: http.Response{ + StatusCode: 403, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + t.Run("Authorization with empty rules and Allow default action should allow all traffic", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/allow-all", + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + }, +}