From a9f552c93b8f29bf3b0b338bd7c3c2e40b00b942 Mon Sep 17 00:00:00 2001 From: zc <2510165121@qq.com> Date: Sun, 26 Mar 2023 23:10:18 +0800 Subject: [PATCH 1/3] feature: support external authorization by annotation (#207) --- pkg/ingress/config/ingress_config.go | 344 +++++++++++++++++- pkg/ingress/config/ingress_config_test.go | 53 +++ pkg/ingress/kube/annotations/annotations.go | 3 + pkg/ingress/kube/annotations/authz.go | 205 +++++++++++ pkg/ingress/kube/annotations/authz_test.go | 107 ++++++ .../conformance/tests/httproute-ext-authz.go | 107 ++++++ .../tests/httproute-ext-authz.yaml | 79 ++++ test/ingress/e2e_test.go | 1 + 8 files changed, 891 insertions(+), 8 deletions(-) create mode 100644 pkg/ingress/kube/annotations/authz.go create mode 100644 pkg/ingress/kube/annotations/authz_test.go create mode 100644 test/ingress/conformance/tests/httproute-ext-authz.go create mode 100644 test/ingress/conformance/tests/httproute-ext-authz.yaml diff --git a/pkg/ingress/config/ingress_config.go b/pkg/ingress/config/ingress_config.go index e78cf1fe7c..e41e110937 100644 --- a/pkg/ingress/config/ingress_config.go +++ b/pkg/ingress/config/ingress_config.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "k8s.io/api/networking/v1beta1" "strings" "sync" @@ -444,13 +445,14 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) [] } // We generate some specific envoy filter here to avoid duplicated computation. - m.convertEnvoyFilter(&convertOptions) + m.convertEnvoyFilter(&convertOptions, configs) return out } -func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions) { +func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions, configs []common.WrapperConfig) { + var envoyFilters []config.Config - mappings := map[string]*common.Rule{} + basicAuthMappings := map[string]*common.Rule{} for _, routes := range convertOptions.HTTPRoutes { for _, route := range routes { @@ -464,8 +466,8 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions } key := auth.AuthSecret.String() + "/" + auth.AuthRealm - if rule, exist := mappings[key]; !exist { - mappings[key] = &common.Rule{ + if rule, exist := basicAuthMappings[key]; !exist { + basicAuthMappings[key] = &common.Rule{ Realm: auth.AuthRealm, MatchRoute: []string{route.HTTPRoute.Name}, Credentials: auth.Credentials, @@ -477,10 +479,31 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions } } - IngressLog.Infof("Found %d number of basic auth", len(mappings)) - if len(mappings) > 0 { + for _, cfg := range configs { + ingressV1, ok := cfg.Config.Spec.(v1beta1.IngressSpec) + if !ok { + continue + } + authz := cfg.AnnotationsConfig.Authz + ignoreCaseConfig := cfg.AnnotationsConfig.IgnoreCase + ignoreCase := false + if ignoreCaseConfig != nil && ignoreCaseConfig.IgnoreUriCase { + ignoreCase = true + } + if authz != nil { + extAuthz, err := constructExtAuthzEnvoyFilter(authz, ingressV1.Rules, m.namespace, ignoreCase) + if err != nil { + IngressLog.Errorf("Construct external authz filter error %v", err) + } else { + envoyFilters = append(envoyFilters, *extAuthz) + } + } + } + + IngressLog.Infof("Found %d number of basic auth", len(basicAuthMappings)) + if len(basicAuthMappings) > 0 { rules := &common.BasicAuthRules{} - for _, rule := range mappings { + for _, rule := range basicAuthMappings { rules.Rules = append(rules.Rules, rule) } @@ -967,6 +990,311 @@ func (m *IngressConfig) applyCanaryIngresses(convertOptions *common.ConvertOptio } } +func constructExtAuthzEnvoyFilter(authz *annotations.AuthzConfig, rules []v1beta1.IngressRule, namespace string, ignoreCase bool) (*config.Config, error) { + extAuthz := authz.ExtAuthz + extAuthzFilterConfig := map[string]*types.Value{ + "@type": buildGogoTypedStringValue("type.googleapis.com/envoy.config.filter.network.ext_authz.v2.ExtAuthz"), + "filter_enabled_metadata": { + Kind: &types.Value_StructValue{StructValue: &types.Struct{ + Fields: map[string]*types.Value{ + "filter": buildGogoTypedStringValue("envoy.filters.http.rbac"), + "path": buildGogoTypedListValue( + []*types.Value{ + buildGogoTypedSingleStringPairMap("key", authz.ExtAuthz.RbacPolicyId), + }), + "value": buildGogoTypedSinglePairMap("string_match", + buildGogoTypedSingleStringPairMap("exact", extAuthz.RbacPolicyId)), + }, + }}, + }, + } + authzService := extAuthz.AuthzService + cluster := fmt.Sprintf( + "outbound|%d||%s.%s.svc.cluster.local", + authzService.ServicePort, + authzService.ServiceName, namespace) + uri := fmt.Sprintf("%s.%s.svc.cluster.local", authzService.ServiceName, namespace) + if extAuthz.AuthzProto == annotations.HTTP { + httpServiceConfig := map[string]*types.Value{ + "serverUri": buildGogoTypedStructValue( + map[string]*types.Value{ + "uri": buildGogoTypedStringValue(uri), + "cluster": buildGogoTypedStringValue(cluster), + "timeout": buildGogoTypedStringValue(authzService.Timeout), + }), + } + if authzService.ServicePathPrefix != "" { + httpServiceConfig["pathPrefix"] = buildGogoTypedStringValue(authzService.ServicePathPrefix) + } + authorizationRequestConfig := map[string]*types.Value{} + var reqHeadersAllowed []*types.Value + reqHeadersAllowed = appendListStringAsSinglePairMapToList("prefix", authzService.ReqAllowedHeadersContains, reqHeadersAllowed) + reqHeadersAllowed = appendListStringAsSinglePairMapToList("exact", authzService.ReqAllowedHeadersExact, reqHeadersAllowed) + reqHeadersAllowed = appendListStringAsSinglePairMapToList("suffix", authzService.ReqAllowedHeadersSuffix, reqHeadersAllowed) + reqHeadersAllowed = appendListStringAsSinglePairMapToList("contains", authzService.ReqAllowedHeadersContains, reqHeadersAllowed) + + if len(reqHeadersAllowed) > 0 { + authorizationRequestConfig["allowedHeaders"] = buildGogoTypedListValue(reqHeadersAllowed) + authorizationRequest := buildGogoTypedStructValue(authorizationRequestConfig) + httpServiceConfig["authorizationRequest"] = authorizationRequest + } + + authorizationResponseConfig := map[string]*types.Value{} + var respUpstreamHeadersAllowed []*types.Value + respUpstreamHeadersAllowed = appendListStringAsSinglePairMapToList("prefix", authzService.RespAllowedUpstreamHeadersPrefix, respUpstreamHeadersAllowed) + respUpstreamHeadersAllowed = appendListStringAsSinglePairMapToList("exact", authzService.RespAllowedUpstreamHeadersContains, respUpstreamHeadersAllowed) + respUpstreamHeadersAllowed = appendListStringAsSinglePairMapToList("suffix", authzService.RespAllowedUpstreamHeadersSuffix, respUpstreamHeadersAllowed) + respUpstreamHeadersAllowed = appendListStringAsSinglePairMapToList("contains", authzService.RespAllowedUpstreamHeadersContains, respUpstreamHeadersAllowed) + if len(respUpstreamHeadersAllowed) > 0 { + authorizationResponseConfig["allowedUpstreamHeaders"] = buildGogoTypedListValue(respUpstreamHeadersAllowed) + } + + var respClientHeadersAllowed []*types.Value + respClientHeadersAllowed = appendListStringAsSinglePairMapToList("prefix", authzService.RespAllowedClientHeadersPrefix, respClientHeadersAllowed) + respClientHeadersAllowed = appendListStringAsSinglePairMapToList("exact", authzService.RespAllowedClientHeadersExact, respClientHeadersAllowed) + respClientHeadersAllowed = appendListStringAsSinglePairMapToList("suffix", authzService.RespAllowedClientHeadersSuffix, respClientHeadersAllowed) + respClientHeadersAllowed = appendListStringAsSinglePairMapToList("prefix", authzService.RespAllowedClientHeadersContains, respClientHeadersAllowed) + + if len(respClientHeadersAllowed) > 0 { + authorizationResponseConfig["allowed_client_headers"] = buildGogoTypedListValue(respClientHeadersAllowed) + } + + if len(authorizationResponseConfig) > 0 { + authorizationResponse := buildGogoTypedStructValue(authorizationResponseConfig) + httpServiceConfig["authorizationResponse"] = authorizationResponse + } + + httpService := &types.Value{ + Kind: &types.Value_StructValue{StructValue: &types.Struct{ + Fields: httpServiceConfig, + }}, + } + extAuthzFilterConfig["httpService"] = httpService + } + + if extAuthz.AuthzProto == annotations.GRPC { + grpcService := &types.Value{ + Kind: &types.Value_StructValue{StructValue: &types.Struct{ + Fields: map[string]*types.Value{ + "envoyGrpc": buildGogoTypedSingleStringPairMap("clusterName", cluster), + "timeout": buildGogoTypedStringValue(authzService.Timeout), + }, + }}, + } + extAuthzFilterConfig["grpcService"] = grpcService + } + + withRequestBodyConfig := map[string]*types.Value{ + "packAsBytes": { + Kind: &types.Value_BoolValue{ + BoolValue: extAuthz.PackAsBytes, + }, + }, + "allowPartialMessage": { + Kind: &types.Value_BoolValue{ + BoolValue: extAuthz.ReqAllowPartial, + }, + }, + } + if extAuthz.ReqMaxBytes > 0 { + withRequestBodyConfig["maxRequestBytes"] = &types.Value{ + Kind: &types.Value_NumberValue{ + NumberValue: float64(extAuthz.ReqMaxBytes), + }, + } + } + extAuthzFilterConfig["withRequestBody"] = buildGogoTypedStructValue(withRequestBodyConfig) + var permissions *types.Value + var routePathList []*types.Value + permissionsStructed := false + for _, rule := range rules { + host := rule.Host + if host == "*" { + permissions = buildGogoTypedListValue([]*types.Value{ + buildGogoTypedSingleStringPairMap("any", "true"), + }) + permissionsStructed = true + break + } + hostType := "exact" + if strings.HasPrefix(host, "*") { + host = host[1:] + hostType = "suffix" + } + httpPaths := rule.HTTP.Paths + var andRules []*types.Value + for _, httpPath := range httpPaths { + + var pathType string + switch *httpPath.PathType { + case v1beta1.PathTypeExact: + pathType = "exact" + case v1beta1.PathTypePrefix: + pathType = "prefix" + } + path := map[string]*types.Value{ + "ignore_case": &types.Value{ + Kind: &types.Value_BoolValue{ + BoolValue: ignoreCase, + }, + }, + pathType: buildGogoTypedStringValue(httpPath.Path), + } + pathRule := buildGogoTypedSinglePairMap("url_path", buildGogoTypedStructValue(path)) + hostRule := buildGogoTypedSinglePairMap( + "header", + buildGogoTypedStructValue(map[string]*types.Value{ + "name": buildGogoTypedStringValue("Host"), + "string_match": buildGogoTypedSinglePairMap(hostType, buildGogoTypedStringValue(host)), + })) + andRules = append(andRules, pathRule) + andRules = append(andRules, hostRule) + } + orRule := &types.Value{} + routePathList = append(routePathList, orRule) + } + if !permissionsStructed { + permissions = buildGogoTypedListValue([]*types.Value{ + buildGogoTypedSinglePairMap("or_rules", + buildGogoTypedSinglePairMap("rules", buildGogoTypedListValue(routePathList))), + }) + } + policy := &types.Value{ + Kind: &types.Value_StructValue{StructValue: &types.Struct{ + Fields: map[string]*types.Value{ + "permissions": permissions, + "principals": buildGogoTypedListValue([]*types.Value{ + buildGogoTypedSingleStringPairMap("any", "true"), + }), + }, + }}, + } + rbacFilterConfig := map[string]*types.Value{ + "@type": buildGogoTypedStringValue("type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC"), + "shadow_rules": { + Kind: &types.Value_StructValue{StructValue: &types.Struct{ + Fields: map[string]*types.Value{ + "action": buildGogoTypedStringValue("ALLOW"), + "policies": buildGogoTypedSinglePairMap(authz.ExtAuthz.RbacPolicyId, policy), + }, + }}, + }, + } + return &config.Config{ + Meta: config.Meta{ + GroupVersionKind: gvk.EnvoyFilter, + Name: common.CreateConvertedName(constants.IstioIngressGatewayName, "extAuthz"), + Namespace: namespace, + }, + Spec: &networking.EnvoyFilter{ + ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{ + { + ApplyTo: networking.EnvoyFilter_HTTP_FILTER, + Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{ + Context: networking.EnvoyFilter_GATEWAY, + ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &networking.EnvoyFilter_ListenerMatch{ + FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{ + Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{ + Name: "envoy.http_connection_manager", + SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{ + Name: "envoy.filters.http.cors", + }, + }, + }, + }, + }, + }, + Patch: &networking.EnvoyFilter_Patch{ + Operation: networking.EnvoyFilter_Patch_INSERT_AFTER, + Value: &types.Struct{ + Fields: map[string]*types.Value{ + "typed_config": buildGogoTypedStructValue(extAuthzFilterConfig), + }, + }, + }, + }, + { + ApplyTo: networking.EnvoyFilter_HTTP_FILTER, + Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{ + Context: networking.EnvoyFilter_GATEWAY, + ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &networking.EnvoyFilter_ListenerMatch{ + FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{ + Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{ + Name: "envoy.http_connection_manager", + SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{ + Name: "envoy.filters.http.ext_authz", + }, + }, + }, + }, + }, + }, + Patch: &networking.EnvoyFilter_Patch{ + Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE, + Value: &types.Struct{ + Fields: map[string]*types.Value{ + "typed_config": buildGogoTypedStructValue(rbacFilterConfig), + }, + }, + }, + }, + }, + }, + }, nil +} + +func appendListStringAsSinglePairMapToList(key string, values []string, gogoList []*types.Value) []*types.Value { + if len(values) > 0 { + for _, value := range values { + gogoList = append(gogoList, buildGogoTypedSingleStringPairMap(key, value)) + } + } + return gogoList +} + +func buildGogoTypedSingleStringPairMap(key string, value string) *types.Value { + gogo := buildGogoTypedSinglePairMap(key, buildGogoTypedStringValue(value)) + return gogo +} + +func buildGogoTypedSinglePairMap(key string, value *types.Value) *types.Value { + gogo := &types.Value{ + Kind: &types.Value_StructValue{StructValue: &types.Struct{ + Fields: map[string]*types.Value{ + key: value, + }, + }}, + } + return gogo +} + +func buildGogoTypedStringValue(value string) *types.Value { + gogo := &types.Value{ + Kind: &types.Value_StringValue{StringValue: value}, + } + return gogo +} + +func buildGogoTypedStructValue(mapValue map[string]*types.Value) *types.Value { + gogo := &types.Value{ + Kind: &types.Value_StructValue{StructValue: &types.Struct{ + Fields: mapValue, + }}, + } + return gogo +} + +func buildGogoTypedListValue(listValue []*types.Value) *types.Value { + gogo := &types.Value{ + Kind: &types.Value_ListValue{ListValue: &types.ListValue{ + Values: listValue, + }}, + } + return gogo +} + func constructBasicAuthEnvoyFilter(rules *common.BasicAuthRules, namespace string) (*config.Config, error) { rulesStr, err := json.Marshal(rules) if err != nil { diff --git a/pkg/ingress/config/ingress_config_test.go b/pkg/ingress/config/ingress_config_test.go index 54e0df0d43..c070572ffe 100644 --- a/pkg/ingress/config/ingress_config_test.go +++ b/pkg/ingress/config/ingress_config_test.go @@ -15,6 +15,8 @@ package config import ( + networkingv1beta1 "k8s.io/api/networking/v1beta1" + "k8s.io/apimachinery/pkg/util/intstr" "testing" httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" @@ -604,3 +606,54 @@ func TestConstructBasicAuthEnvoyFilter(t *testing.T) { target := proto.Clone(pb).(*httppb.HttpFilter) t.Log(target) } + +func TestConstructExtAuthzEnvoyFilter(t *testing.T) { + authz := &annotations.AuthzConfig{ + AuthzType: "ext-authz", + ExtAuthz: &annotations.ExtAuthzConfig{ + AuthzProto: annotations.HTTP, + AuthzService: &annotations.ServiceConfig{ + Timeout: "10s", + ServiceName: "ext-authz", + ServicePort: 8000, + }, + RbacPolicyId: "ext-authz-policy", + }, + } + pathType := networkingv1beta1.PathTypeExact + rules := []networkingv1beta1.IngressRule{ + { + Host: "*", + IngressRuleValue: networkingv1beta1.IngressRuleValue{ + HTTP: &networkingv1beta1.HTTPIngressRuleValue{ + Paths: []networkingv1beta1.HTTPIngressPath{ + { + Path: "/foo", + PathType: &pathType, + Backend: networkingv1beta1.IngressBackend{ + ServiceName: "foo-service", + ServicePort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: 5678, + }, + }, + }, + }, + }, + }, + }, + } + + config, err := constructExtAuthzEnvoyFilter(authz, rules, "test", true) + if err != nil { + t.Fatalf("construct error %v", err) + } + envoyFilter := config.Spec.(*networking.EnvoyFilter) + pb, err := xds.BuildXDSObjectFromStruct(networking.EnvoyFilter_HTTP_FILTER, envoyFilter.ConfigPatches[0].Patch.Value, false) + //pb, err := xds.BuildXDSObjectFromStruct(networking.EnvoyFilter_HTTP_FILTER, envoyFilter.ConfigPatches[1].Patch.Value, false) + if err != nil { + t.Fatalf("build object error %v", err) + } + target := proto.Clone(pb).(*httppb.HttpFilter) + t.Log(target) +} diff --git a/pkg/ingress/kube/annotations/annotations.go b/pkg/ingress/kube/annotations/annotations.go index cd326783ff..91a586699a 100644 --- a/pkg/ingress/kube/annotations/annotations.go +++ b/pkg/ingress/kube/annotations/annotations.go @@ -62,6 +62,8 @@ type Ingress struct { Auth *AuthConfig + Authz *AuthzConfig + Destination *DestinationConfig IgnoreCase *IgnoreCaseConfig @@ -138,6 +140,7 @@ func NewAnnotationHandlerManager() AnnotationHandler { destination{}, ignoreCaseMatching{}, match{}, + authz{}, }, gatewayHandlers: []GatewayHandler{ downstreamTLS{}, diff --git a/pkg/ingress/kube/annotations/authz.go b/pkg/ingress/kube/annotations/authz.go new file mode 100644 index 0000000000..f407d03f89 --- /dev/null +++ b/pkg/ingress/kube/annotations/authz.go @@ -0,0 +1,205 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package annotations + +import ( + . "github.com/alibaba/higress/pkg/ingress/log" +) + +const ( + authzTypeAnn = "authz-type" + protoAnn = "ext-authz-proto" + serviceAnn = "ext-authz-service" + servicePortAnn = "ext-authz-service-port" + servicePathPrefixAnn = "ext-authz-http-service-path-prefix" + reqAllowedHeadersExactAnn = "ext-authz-req-allowed-headers-exact" + reqAllowedHeadersPrefixAnn = "ext-authz-req-allowed-headers-prefix" + reqAllowedHeadersSuffixAnn = "ext-authz-req-allowed-headers-suffix" + reqAllowedHeadersContainsAnn = "ext-authz-req-allowed-headers-contains" + respAllowedUpstreamHeadersExactAnn = "ext-authz-req-allowed-upstream-headers-exact" + respAllowedUpstreamHeadersPrefixAnn = "ext-authz-req-allowed-upstream-headers-prefix" + respAllowedUpstreamHeadersSuffixAnn = "ext-authz-req-allowed-upstream-headers-suffix" + respAllowedUpstreamHeadersContainsAnn = "ext-authz-req-allowed-upstream-headers-contains" + respAllowedClientHeadersExactAnn = "ext-authz-req-allowed-client-headers-exact" + respAllowedClientHeadersPrefixAnn = "ext-authz-req-allowed-client-headers-prefix" + respAllowedClientHeadersSuffixAnn = "ext-authz-req-allowed-client-headers-suffix" + respAllowedClientHeadersContainsAnn = "ext-authz-req-allowed-client-headers-contains" + rbacPolicyIdAnn = "ext-authz-rbac-policy-id" + reqMaxBytesAnn = "ext-authz-req-max-bytes" + reqAllowPartialAnn = "ext-authz-req-allow-partial" + packAsBytesAnn = "ext-authz-pack-as-bytes" + serviceTimeOutAnn = "ext-authz-timeout" + + defaultAuthzType = "ext-authz" +) + +type extAuthzProto string + +const ( + GRPC extAuthzProto = "grpc" + HTTP extAuthzProto = "http" +) + +var _ Parser = authz{} + +type AuthzConfig struct { + AuthzType string + ExtAuthz *ExtAuthzConfig +} + +type ExtAuthzConfig struct { + AuthzProto extAuthzProto + AuthzService *ServiceConfig + RbacPolicyId string + ReqMaxBytes uint32 + ReqAllowPartial bool + PackAsBytes bool +} + +type ServiceConfig struct { + Timeout string + ServiceName string + ServicePort int + ServicePathPrefix string + ReqAllowedHeadersExact []string + ReqAllowedHeadersPrefix []string + ReqAllowedHeadersSuffix []string + ReqAllowedHeadersContains []string + RespAllowedUpstreamHeadersExact []string + RespAllowedUpstreamHeadersPrefix []string + RespAllowedUpstreamHeadersSuffix []string + RespAllowedUpstreamHeadersContains []string + RespAllowedClientHeadersExact []string + RespAllowedClientHeadersPrefix []string + RespAllowedClientHeadersSuffix []string + RespAllowedClientHeadersContains []string +} + +type authz struct{} + +func (a authz) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error { + IngressLog.Infof("Parse authz annotations") + if !needAuthzConfig(annotations) { + return nil + } + + authzConfig := &AuthzConfig{ + AuthzType: defaultAuthzType, + } + + authzType, err := annotations.ParseStringForHigress(authzTypeAnn) + if err != nil { + IngressLog.Errorf("Parse authz type error %v within ingress %/%s", err, config.Namespace, config.Name) + return nil + } + if authzType != defaultAuthzType { + IngressLog.Errorf("Auth type %s within ingress %/%s is not supported yet.", authzType, config.Namespace, config.Name) + return nil + } + proto := GRPC + if rawProto, err := annotations.ParseStringForHigress(protoAnn); err == nil { + resultProto := extAuthzProto(rawProto) + if resultProto == GRPC || resultProto == HTTP { + proto = resultProto + } + } + extAuthzConfig := &ExtAuthzConfig{ + AuthzProto: proto, + } + + serviceConfig := &ServiceConfig{} + if timeout, err := annotations.ParseStringForHigress(serviceTimeOutAnn); err == nil { + serviceConfig.Timeout = timeout + } + if service, err := annotations.ParseStringForHigress(serviceAnn); err == nil && service != "" { + serviceConfig.ServiceName = service + } else { + IngressLog.Errorf("Authz service name within ingress %s/%s is not configure", config.Namespace, config.Name) + return nil + } + if servicePort, err := annotations.ParseIntForHigress(servicePortAnn); err == nil { + if servicePort <= 0 || servicePort > 65535 { + IngressLog.Errorf("Authz service port within ingress %s/%s is invalid", config.Namespace, config.Name) + return nil + } + serviceConfig.ServicePort = servicePort + } else { + serviceConfig.ServicePort = 80 + } + if servicePathPrefix, err := annotations.ParseStringForHigress(servicePathPrefixAnn); err == nil { + serviceConfig.ServicePathPrefix = servicePathPrefix + } + if reqAllowedHeadersExact, err := annotations.ParseStringForHigress(reqAllowedHeadersExactAnn); err == nil { + serviceConfig.ReqAllowedHeadersExact = splitStringWithSpaceTrim(reqAllowedHeadersExact) + } + if reqAllowedHeadersPrefix, err := annotations.ParseStringForHigress(reqAllowedHeadersPrefixAnn); err == nil { + serviceConfig.ReqAllowedHeadersPrefix = splitStringWithSpaceTrim(reqAllowedHeadersPrefix) + } + if reqAllowedHeadersSuffix, err := annotations.ParseStringForHigress(reqAllowedHeadersSuffixAnn); err == nil { + serviceConfig.ReqAllowedHeadersSuffix = splitStringWithSpaceTrim(reqAllowedHeadersSuffix) + } + if reqAllowedHeadersContains, err := annotations.ParseStringForHigress(reqAllowedHeadersContainsAnn); err == nil { + serviceConfig.ReqAllowedHeadersContains = splitStringWithSpaceTrim(reqAllowedHeadersContains) + } + if respAllowedUpstreamHeadersExact, err := annotations.ParseStringForHigress(respAllowedUpstreamHeadersExactAnn); err == nil { + serviceConfig.RespAllowedUpstreamHeadersExact = splitStringWithSpaceTrim(respAllowedUpstreamHeadersExact) + } + if respAllowedUpstreamHeadersPrefix, err := annotations.ParseStringForHigress(respAllowedUpstreamHeadersPrefixAnn); err == nil { + serviceConfig.RespAllowedUpstreamHeadersPrefix = splitStringWithSpaceTrim(respAllowedUpstreamHeadersPrefix) + } + if respAllowedUpstreamHeadersSuffix, err := annotations.ParseStringForHigress(respAllowedUpstreamHeadersSuffixAnn); err == nil { + serviceConfig.RespAllowedUpstreamHeadersSuffix = splitStringWithSpaceTrim(respAllowedUpstreamHeadersSuffix) + } + if respAllowedUpstreamHeadersContains, err := annotations.ParseStringForHigress(respAllowedUpstreamHeadersContainsAnn); err == nil { + serviceConfig.RespAllowedUpstreamHeadersContains = splitStringWithSpaceTrim(respAllowedUpstreamHeadersContains) + } + if respAllowedClientHeadersExact, err := annotations.ParseStringForHigress(respAllowedClientHeadersExactAnn); err == nil { + serviceConfig.RespAllowedClientHeadersExact = splitStringWithSpaceTrim(respAllowedClientHeadersExact) + } + if respAllowedClientHeadersPrefix, err := annotations.ParseStringForHigress(respAllowedClientHeadersPrefixAnn); err == nil { + serviceConfig.RespAllowedClientHeadersPrefix = splitStringWithSpaceTrim(respAllowedClientHeadersPrefix) + } + if respAllowedClientHeadersSuffix, err := annotations.ParseStringForHigress(respAllowedClientHeadersSuffixAnn); err == nil { + serviceConfig.RespAllowedClientHeadersSuffix = splitStringWithSpaceTrim(respAllowedClientHeadersSuffix) + } + if respAllowedClientHeadersContains, err := annotations.ParseStringForHigress(respAllowedClientHeadersContainsAnn); err == nil { + serviceConfig.RespAllowedClientHeadersContains = splitStringWithSpaceTrim(respAllowedClientHeadersContains) + } + if rbacPolicyId, err := annotations.ParseStringForHigress(rbacPolicyIdAnn); err == nil { + extAuthzConfig.RbacPolicyId = rbacPolicyId + } else { + rbacPolicyId = config.Namespace + "-" + config.Name + "-ext-authz-policy" + extAuthzConfig.RbacPolicyId = rbacPolicyId + } + if reqMaxBytes, err := annotations.ParseUint32ForHigress(reqMaxBytesAnn); err == nil { + + extAuthzConfig.ReqMaxBytes = reqMaxBytes + } + if reqAllowPartial, err := annotations.ParseBoolForHigress(reqAllowPartialAnn); err == nil { + extAuthzConfig.ReqAllowPartial = reqAllowPartial + } + if packAsBytes, err := annotations.ParseBoolForHigress(packAsBytesAnn); err == nil { + extAuthzConfig.PackAsBytes = packAsBytes + } + extAuthzConfig.AuthzService = serviceConfig + authzConfig.ExtAuthz = extAuthzConfig + config.Authz = authzConfig + return nil +} + +func needAuthzConfig(annotations Annotations) bool { + return annotations.HasASAP(authzTypeAnn) && + annotations.HasASAP(serviceAnn) +} diff --git a/pkg/ingress/kube/annotations/authz_test.go b/pkg/ingress/kube/annotations/authz_test.go new file mode 100644 index 0000000000..b0fb692efc --- /dev/null +++ b/pkg/ingress/kube/annotations/authz_test.go @@ -0,0 +1,107 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package annotations + +import ( + "reflect" + "testing" +) + +func TestAuthzParse(t *testing.T) { + authz := authz{} + inputCases := []struct { + input map[string]string + expect *AuthzConfig + }{ + { + input: map[string]string{ + buildHigressAnnotationKey(authzTypeAnn): defaultAuthzType, + }, + expect: nil, + }, + { + input: map[string]string{ + buildHigressAnnotationKey(authzTypeAnn): defaultAuthzType, + buildHigressAnnotationKey(serviceAnn): "ext-authz-service", + }, + expect: &AuthzConfig{ + AuthzType: "ext-authz", + ExtAuthz: &ExtAuthzConfig{ + AuthzProto: GRPC, + AuthzService: &ServiceConfig{ + ServiceName: "ext-authz-service", + ServicePort: 80, + }, + RbacPolicyId: "default-ingress-test-ext-authz-policy", + }, + }, + }, + { + input: map[string]string{ + buildHigressAnnotationKey(authzTypeAnn): defaultAuthzType, + buildHigressAnnotationKey(serviceAnn): "ext-authz-service", + buildHigressAnnotationKey(protoAnn): "grpc", + buildHigressAnnotationKey(servicePortAnn): "9000", + buildHigressAnnotationKey(rbacPolicyIdAnn): "test-policy", + }, + expect: &AuthzConfig{ + AuthzType: "ext-authz", + ExtAuthz: &ExtAuthzConfig{ + AuthzProto: GRPC, + AuthzService: &ServiceConfig{ + ServiceName: "ext-authz-service", + ServicePort: 9000, + }, + RbacPolicyId: "test-policy", + }, + }, + }, + { + input: map[string]string{ + buildHigressAnnotationKey(authzTypeAnn): defaultAuthzType, + buildHigressAnnotationKey(serviceAnn): "ext-authz-service", + buildHigressAnnotationKey(protoAnn): "http", + }, + expect: &AuthzConfig{ + AuthzType: "ext-authz", + ExtAuthz: &ExtAuthzConfig{ + AuthzProto: HTTP, + AuthzService: &ServiceConfig{ + ServiceName: "ext-authz-service", + ServicePort: 80, + }, + RbacPolicyId: "default-ingress-test-ext-authz-policy", + }, + }, + }, + } + + for _, inputCase := range inputCases { + t.Run("", func(t *testing.T) { + config := &Ingress{ + Meta: Meta{ + Namespace: "default", + Name: "ingress-test", + ClusterId: "cluster", + }, + } + + _ = authz.Parse(inputCase.input, config, nil) + if !reflect.DeepEqual(inputCase.expect, config.Authz) { + t.Fatal("Should be equal") + } + }) + } +} diff --git a/test/ingress/conformance/tests/httproute-ext-authz.go b/test/ingress/conformance/tests/httproute-ext-authz.go new file mode 100644 index 0000000000..d48330f728 --- /dev/null +++ b/test/ingress/conformance/tests/httproute-ext-authz.go @@ -0,0 +1,107 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/alibaba/higress/test/ingress/conformance/utils/http" + "github.com/alibaba/higress/test/ingress/conformance/utils/suite" +) + +func init() { + HigressConformanceTests = append(HigressConformanceTests, HTTPRouteExtAuthz) +} + +var HTTPRouteExtAuthz = suite.ConformanceTest{ + ShortName: "HTTPRouteExtAuthz", + Description: "A single Ingress in the higress-conformance-infra namespace uses the external authorization service.", + Manifests: []string{"tests/httproute-ext-authz.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Method: "GET", + Host: "foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Method: "GET", + Host: "foo.com", + Headers: map[string]string{ + "x-ext-authz": "allow", + }, + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Method: "GET", + Host: "foo.com", + Headers: map[string]string{ + "x-ext-authz": "blabla", + }, + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, + } + + t.Run("check HTTP Authorization by ext-authz", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/ingress/conformance/tests/httproute-ext-authz.yaml b/test/ingress/conformance/tests/httproute-ext-authz.yaml new file mode 100644 index 0000000000..3329c18891 --- /dev/null +++ b/test/ingress/conformance/tests/httproute-ext-authz.yaml @@ -0,0 +1,79 @@ +# Copyright (c) 2022 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: Service +metadata: + name: ext-authz + namespace: higress-conformance-infra + labels: + app: ext-authz +spec: + ports: + - name: http + port: 8000 + targetPort: 8000 + - name: grpc + port: 9000 + targetPort: 9000 + selector: + app: ext-authz +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ext-authz + namespace: higress-conformance-infra +spec: + replicas: 1 + selector: + matchLabels: + app: ext-authz + template: + metadata: + labels: + app: ext-authz + spec: + containers: + - image: gcr.io/istio-testing/ext-authz:latest + imagePullPolicy: IfNotPresent + name: ext-authz + ports: + - containerPort: 8000 + - containerPort: 9000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + higress.io/authz-type: "ext-authz" + higress.io/ext-authz-proto: "grpc" + higress.io/ext-authz-http-service: "ext-authz" + higress.io/ext-authz-http-service-port: "9000" + higress.io/ext-authz-rbac-policy-id: "ext-authz-test" + name: httproute-ext-authz + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 \ No newline at end of file diff --git a/test/ingress/e2e_test.go b/test/ingress/e2e_test.go index 219e8f7543..a655a9d561 100644 --- a/test/ingress/e2e_test.go +++ b/test/ingress/e2e_test.go @@ -68,6 +68,7 @@ func TestHigressConformanceTests(t *testing.T) { tests.HTTPRouteSameHostAndPath, tests.HTTPRouteCanaryHeaderWithCustomizedHeader, tests.HTTPRouteWhitelistSourceRange, + tests.HTTPRouteExtAuthz, } cSuite.Run(t, higressTests) From 96979b309cb8ddfa9898bb2650e2959dd8521574 Mon Sep 17 00:00:00 2001 From: zc <2510165121@qq.com> Date: Fri, 31 Mar 2023 03:13:48 +0800 Subject: [PATCH 2/3] refactor: change the way to build envoyfilter with using httppb rather than full use gogotype (#207) --- pkg/ingress/config/ingress_config.go | 382 ++++++++++------------ pkg/ingress/config/ingress_config_test.go | 12 +- pkg/ingress/kube/annotations/authz.go | 2 + 3 files changed, 190 insertions(+), 206 deletions(-) diff --git a/pkg/ingress/config/ingress_config.go b/pkg/ingress/config/ingress_config.go index e41e110937..2d1e51ed6c 100644 --- a/pkg/ingress/config/ingress_config.go +++ b/pkg/ingress/config/ingress_config.go @@ -18,14 +18,21 @@ import ( "encoding/json" "errors" "fmt" + rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" + envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + v32 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + "github.com/golang/protobuf/ptypes/duration" "k8s.io/api/networking/v1beta1" "strings" "sync" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + ext_authz "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3" + rbac "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" wasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3" httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3" + "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" "github.com/gogo/protobuf/types" "github.com/golang/protobuf/ptypes/wrappers" "google.golang.org/protobuf/types/known/anypb" @@ -992,20 +999,29 @@ func (m *IngressConfig) applyCanaryIngresses(convertOptions *common.ConvertOptio func constructExtAuthzEnvoyFilter(authz *annotations.AuthzConfig, rules []v1beta1.IngressRule, namespace string, ignoreCase bool) (*config.Config, error) { extAuthz := authz.ExtAuthz - extAuthzFilterConfig := map[string]*types.Value{ - "@type": buildGogoTypedStringValue("type.googleapis.com/envoy.config.filter.network.ext_authz.v2.ExtAuthz"), - "filter_enabled_metadata": { - Kind: &types.Value_StructValue{StructValue: &types.Struct{ - Fields: map[string]*types.Value{ - "filter": buildGogoTypedStringValue("envoy.filters.http.rbac"), - "path": buildGogoTypedListValue( - []*types.Value{ - buildGogoTypedSingleStringPairMap("key", authz.ExtAuthz.RbacPolicyId), - }), - "value": buildGogoTypedSinglePairMap("string_match", - buildGogoTypedSingleStringPairMap("exact", extAuthz.RbacPolicyId)), + e := &ext_authz.ExtAuthz{ + FilterEnabledMetadata: &v32.MetadataMatcher{ + Filter: "envoy.filters.http.rbac", + Path: []*envoy_type_matcher_v3.MetadataMatcher_PathSegment{ + { + Segment: &envoy_type_matcher_v3.MetadataMatcher_PathSegment_Key{ + Key: "shadow_effective_policy_id", + }, + }, + }, + Value: &envoy_type_matcher_v3.ValueMatcher{ + MatchPattern: &envoy_type_matcher_v3.ValueMatcher_StringMatch{ + StringMatch: &envoy_type_matcher_v3.StringMatcher{ + MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{ + Exact: extAuthz.RbacPolicyId, + }, + }, }, - }}, + }, + }, + WithRequestBody: &ext_authz.BufferSettings{ + AllowPartialMessage: extAuthz.ReqAllowPartial, + PackAsBytes: extAuthz.PackAsBytes, }, } authzService := extAuthz.AuthzService @@ -1015,171 +1031,189 @@ func constructExtAuthzEnvoyFilter(authz *annotations.AuthzConfig, rules []v1beta authzService.ServiceName, namespace) uri := fmt.Sprintf("%s.%s.svc.cluster.local", authzService.ServiceName, namespace) if extAuthz.AuthzProto == annotations.HTTP { - httpServiceConfig := map[string]*types.Value{ - "serverUri": buildGogoTypedStructValue( - map[string]*types.Value{ - "uri": buildGogoTypedStringValue(uri), - "cluster": buildGogoTypedStringValue(cluster), - "timeout": buildGogoTypedStringValue(authzService.Timeout), - }), + httpService := &ext_authz.ExtAuthz_HttpService{ + HttpService: &ext_authz.HttpService{ + ServerUri: &corev3.HttpUri{ + HttpUpstreamType: &corev3.HttpUri_Cluster{ + Cluster: cluster, + }, + Timeout: &duration.Duration{ + Seconds: 10, + }, + Uri: uri, + }, + PathPrefix: authzService.ServicePathPrefix, + }, } if authzService.ServicePathPrefix != "" { - httpServiceConfig["pathPrefix"] = buildGogoTypedStringValue(authzService.ServicePathPrefix) + httpService.HttpService.PathPrefix = authzService.ServicePathPrefix } - authorizationRequestConfig := map[string]*types.Value{} - var reqHeadersAllowed []*types.Value - reqHeadersAllowed = appendListStringAsSinglePairMapToList("prefix", authzService.ReqAllowedHeadersContains, reqHeadersAllowed) - reqHeadersAllowed = appendListStringAsSinglePairMapToList("exact", authzService.ReqAllowedHeadersExact, reqHeadersAllowed) - reqHeadersAllowed = appendListStringAsSinglePairMapToList("suffix", authzService.ReqAllowedHeadersSuffix, reqHeadersAllowed) - reqHeadersAllowed = appendListStringAsSinglePairMapToList("contains", authzService.ReqAllowedHeadersContains, reqHeadersAllowed) - - if len(reqHeadersAllowed) > 0 { - authorizationRequestConfig["allowedHeaders"] = buildGogoTypedListValue(reqHeadersAllowed) - authorizationRequest := buildGogoTypedStructValue(authorizationRequestConfig) - httpServiceConfig["authorizationRequest"] = authorizationRequest + if authzService.AuthorizationRequest != "" { + AuthorizationRequest := &ext_authz.AuthorizationRequest{} + json.Unmarshal([]byte(authzService.AuthorizationRequest), AuthorizationRequest) + httpService.HttpService.AuthorizationRequest = AuthorizationRequest } - - authorizationResponseConfig := map[string]*types.Value{} - var respUpstreamHeadersAllowed []*types.Value - respUpstreamHeadersAllowed = appendListStringAsSinglePairMapToList("prefix", authzService.RespAllowedUpstreamHeadersPrefix, respUpstreamHeadersAllowed) - respUpstreamHeadersAllowed = appendListStringAsSinglePairMapToList("exact", authzService.RespAllowedUpstreamHeadersContains, respUpstreamHeadersAllowed) - respUpstreamHeadersAllowed = appendListStringAsSinglePairMapToList("suffix", authzService.RespAllowedUpstreamHeadersSuffix, respUpstreamHeadersAllowed) - respUpstreamHeadersAllowed = appendListStringAsSinglePairMapToList("contains", authzService.RespAllowedUpstreamHeadersContains, respUpstreamHeadersAllowed) - if len(respUpstreamHeadersAllowed) > 0 { - authorizationResponseConfig["allowedUpstreamHeaders"] = buildGogoTypedListValue(respUpstreamHeadersAllowed) + if authzService.AuthorizationResponse != "" { + AuthorizationResponse := &ext_authz.AuthorizationResponse{} + json.Unmarshal([]byte(authzService.AuthorizationRequest), AuthorizationResponse) + httpService.HttpService.AuthorizationResponse = AuthorizationResponse } - - var respClientHeadersAllowed []*types.Value - respClientHeadersAllowed = appendListStringAsSinglePairMapToList("prefix", authzService.RespAllowedClientHeadersPrefix, respClientHeadersAllowed) - respClientHeadersAllowed = appendListStringAsSinglePairMapToList("exact", authzService.RespAllowedClientHeadersExact, respClientHeadersAllowed) - respClientHeadersAllowed = appendListStringAsSinglePairMapToList("suffix", authzService.RespAllowedClientHeadersSuffix, respClientHeadersAllowed) - respClientHeadersAllowed = appendListStringAsSinglePairMapToList("prefix", authzService.RespAllowedClientHeadersContains, respClientHeadersAllowed) - - if len(respClientHeadersAllowed) > 0 { - authorizationResponseConfig["allowed_client_headers"] = buildGogoTypedListValue(respClientHeadersAllowed) - } - - if len(authorizationResponseConfig) > 0 { - authorizationResponse := buildGogoTypedStructValue(authorizationResponseConfig) - httpServiceConfig["authorizationResponse"] = authorizationResponse - } - - httpService := &types.Value{ - Kind: &types.Value_StructValue{StructValue: &types.Struct{ - Fields: httpServiceConfig, - }}, - } - extAuthzFilterConfig["httpService"] = httpService + e.Services = httpService } if extAuthz.AuthzProto == annotations.GRPC { - grpcService := &types.Value{ - Kind: &types.Value_StructValue{StructValue: &types.Struct{ - Fields: map[string]*types.Value{ - "envoyGrpc": buildGogoTypedSingleStringPairMap("clusterName", cluster), - "timeout": buildGogoTypedStringValue(authzService.Timeout), + grpcService := &ext_authz.ExtAuthz_GrpcService{ + GrpcService: &corev3.GrpcService{ + TargetSpecifier: &corev3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &corev3.GrpcService_EnvoyGrpc{ + ClusterName: cluster, + }, + }, + Timeout: &duration.Duration{ + Seconds: 10, }, - }}, + }, } - extAuthzFilterConfig["grpcService"] = grpcService + e.Services = grpcService } - withRequestBodyConfig := map[string]*types.Value{ - "packAsBytes": { - Kind: &types.Value_BoolValue{ - BoolValue: extAuthz.PackAsBytes, - }, - }, - "allowPartialMessage": { - Kind: &types.Value_BoolValue{ - BoolValue: extAuthz.ReqAllowPartial, - }, + if extAuthz.ReqMaxBytes > 0 { + e.WithRequestBody.MaxRequestBytes = extAuthz.ReqMaxBytes + } + + extAuthzAny, err := anypb.New(e) + if err != nil { + return nil, err + } + + typedConfig := &httppb.HttpFilter{ + Name: "envoy.filters.http.ext_authz", + ConfigType: &httppb.HttpFilter_TypedConfig{ + TypedConfig: extAuthzAny, }, } - if extAuthz.ReqMaxBytes > 0 { - withRequestBodyConfig["maxRequestBytes"] = &types.Value{ - Kind: &types.Value_NumberValue{ - NumberValue: float64(extAuthz.ReqMaxBytes), - }, - } + + gogoTypedConfig, err := util.MessageToGoGoStruct(typedConfig) + if err != nil { + return nil, err } - extAuthzFilterConfig["withRequestBody"] = buildGogoTypedStructValue(withRequestBodyConfig) - var permissions *types.Value - var routePathList []*types.Value - permissionsStructed := false + var permissions []*rbacpb.Permission for _, rule := range rules { host := rule.Host if host == "*" { - permissions = buildGogoTypedListValue([]*types.Value{ - buildGogoTypedSingleStringPairMap("any", "true"), - }) - permissionsStructed = true + permissions = []*rbacpb.Permission{ + { + Rule: &rbacpb.Permission_Any{ + Any: true, + }, + }, + } break } - hostType := "exact" + hostPermission := &rbacpb.Permission{} if strings.HasPrefix(host, "*") { host = host[1:] - hostType = "suffix" + hostMatcher := &envoy_type_matcher_v3.StringMatcher_Suffix{ + Suffix: host, + } + hostPermission.Rule = &rbacpb.Permission_UrlPath{ + UrlPath: &envoy_type_matcher_v3.PathMatcher{ + Rule: &envoy_type_matcher_v3.PathMatcher_Path{ + Path: &envoy_type_matcher_v3.StringMatcher{ + MatchPattern: hostMatcher, + IgnoreCase: ignoreCase, + }, + }, + }, + } + } else { + hostMatcher := &envoy_type_matcher_v3.StringMatcher_Exact{ + Exact: host, + } + hostPermission.Rule = &rbacpb.Permission_UrlPath{ + UrlPath: &envoy_type_matcher_v3.PathMatcher{ + Rule: &envoy_type_matcher_v3.PathMatcher_Path{ + Path: &envoy_type_matcher_v3.StringMatcher{ + MatchPattern: hostMatcher, + IgnoreCase: ignoreCase, + }, + }, + }, + } } httpPaths := rule.HTTP.Paths - var andRules []*types.Value + var andRules []*rbacpb.Permission for _, httpPath := range httpPaths { - - var pathType string + pathPermission := &rbacpb.Permission{} switch *httpPath.PathType { case v1beta1.PathTypeExact: - pathType = "exact" + pathMatchPattern := &envoy_type_matcher_v3.StringMatcher_Exact{ + Exact: httpPath.Path, + } + pathPermission.Rule = &rbacpb.Permission_Header{ + Header: &envoy_config_route_v3.HeaderMatcher{ + HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ + StringMatch: &envoy_type_matcher_v3.StringMatcher{ + MatchPattern: pathMatchPattern, + IgnoreCase: ignoreCase, + }, + }, + }, + } case v1beta1.PathTypePrefix: - pathType = "prefix" - } - path := map[string]*types.Value{ - "ignore_case": &types.Value{ - Kind: &types.Value_BoolValue{ - BoolValue: ignoreCase, + pathMatchPattern := &envoy_type_matcher_v3.StringMatcher_Prefix{ + Prefix: httpPath.Path, + } + pathPermission.Rule = &rbacpb.Permission_Header{ + Header: &envoy_config_route_v3.HeaderMatcher{ + HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ + StringMatch: &envoy_type_matcher_v3.StringMatcher{ + MatchPattern: pathMatchPattern, + IgnoreCase: ignoreCase, + }, + }, }, - }, - pathType: buildGogoTypedStringValue(httpPath.Path), + } } - pathRule := buildGogoTypedSinglePairMap("url_path", buildGogoTypedStructValue(path)) - hostRule := buildGogoTypedSinglePairMap( - "header", - buildGogoTypedStructValue(map[string]*types.Value{ - "name": buildGogoTypedStringValue("Host"), - "string_match": buildGogoTypedSinglePairMap(hostType, buildGogoTypedStringValue(host)), - })) - andRules = append(andRules, pathRule) - andRules = append(andRules, hostRule) + andRules = append(andRules, hostPermission) + andRules = append(andRules, pathPermission) } - orRule := &types.Value{} - routePathList = append(routePathList, orRule) - } - if !permissionsStructed { - permissions = buildGogoTypedListValue([]*types.Value{ - buildGogoTypedSinglePairMap("or_rules", - buildGogoTypedSinglePairMap("rules", buildGogoTypedListValue(routePathList))), - }) - } - policy := &types.Value{ - Kind: &types.Value_StructValue{StructValue: &types.Struct{ - Fields: map[string]*types.Value{ - "permissions": permissions, - "principals": buildGogoTypedListValue([]*types.Value{ - buildGogoTypedSingleStringPairMap("any", "true"), - }), + orRule := &rbacpb.Permission{ + Rule: &rbacpb.Permission_OrRules{ + OrRules: &rbacpb.Permission_Set{ + Rules: andRules, + }, }, - }}, - } - rbacFilterConfig := map[string]*types.Value{ - "@type": buildGogoTypedStringValue("type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC"), - "shadow_rules": { - Kind: &types.Value_StructValue{StructValue: &types.Struct{ - Fields: map[string]*types.Value{ - "action": buildGogoTypedStringValue("ALLOW"), - "policies": buildGogoTypedSinglePairMap(authz.ExtAuthz.RbacPolicyId, policy), + } + permissions = append(permissions, orRule) + } + + Rbac := &rbac.RBAC{ + ShadowRules: &rbacpb.RBAC{ + Policies: map[string]*rbacpb.Policy{ + authz.ExtAuthz.RbacPolicyId: { + Permissions: permissions, }, - }}, + }, + Action: rbacpb.RBAC_ALLOW, + }, + } + rbacAny, err := anypb.New(Rbac) + if err != nil { + return nil, err + } + + rbacTypedConfig := &httppb.HttpFilter{ + Name: "envoy.filters.http.rbac", + ConfigType: &httppb.HttpFilter_TypedConfig{ + TypedConfig: rbacAny, }, } + + rbacGogoTypedConfig, err := util.MessageToGoGoStruct(rbacTypedConfig) + if err != nil { + return nil, err + } return &config.Config{ Meta: config.Meta{ GroupVersionKind: gvk.EnvoyFilter, @@ -1207,11 +1241,7 @@ func constructExtAuthzEnvoyFilter(authz *annotations.AuthzConfig, rules []v1beta }, Patch: &networking.EnvoyFilter_Patch{ Operation: networking.EnvoyFilter_Patch_INSERT_AFTER, - Value: &types.Struct{ - Fields: map[string]*types.Value{ - "typed_config": buildGogoTypedStructValue(extAuthzFilterConfig), - }, - }, + Value: gogoTypedConfig, }, }, { @@ -1233,11 +1263,7 @@ func constructExtAuthzEnvoyFilter(authz *annotations.AuthzConfig, rules []v1beta }, Patch: &networking.EnvoyFilter_Patch{ Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE, - Value: &types.Struct{ - Fields: map[string]*types.Value{ - "typed_config": buildGogoTypedStructValue(rbacFilterConfig), - }, - }, + Value: rbacGogoTypedConfig, }, }, }, @@ -1245,56 +1271,6 @@ func constructExtAuthzEnvoyFilter(authz *annotations.AuthzConfig, rules []v1beta }, nil } -func appendListStringAsSinglePairMapToList(key string, values []string, gogoList []*types.Value) []*types.Value { - if len(values) > 0 { - for _, value := range values { - gogoList = append(gogoList, buildGogoTypedSingleStringPairMap(key, value)) - } - } - return gogoList -} - -func buildGogoTypedSingleStringPairMap(key string, value string) *types.Value { - gogo := buildGogoTypedSinglePairMap(key, buildGogoTypedStringValue(value)) - return gogo -} - -func buildGogoTypedSinglePairMap(key string, value *types.Value) *types.Value { - gogo := &types.Value{ - Kind: &types.Value_StructValue{StructValue: &types.Struct{ - Fields: map[string]*types.Value{ - key: value, - }, - }}, - } - return gogo -} - -func buildGogoTypedStringValue(value string) *types.Value { - gogo := &types.Value{ - Kind: &types.Value_StringValue{StringValue: value}, - } - return gogo -} - -func buildGogoTypedStructValue(mapValue map[string]*types.Value) *types.Value { - gogo := &types.Value{ - Kind: &types.Value_StructValue{StructValue: &types.Struct{ - Fields: mapValue, - }}, - } - return gogo -} - -func buildGogoTypedListValue(listValue []*types.Value) *types.Value { - gogo := &types.Value{ - Kind: &types.Value_ListValue{ListValue: &types.ListValue{ - Values: listValue, - }}, - } - return gogo -} - func constructBasicAuthEnvoyFilter(rules *common.BasicAuthRules, namespace string) (*config.Config, error) { rulesStr, err := json.Marshal(rules) if err != nil { diff --git a/pkg/ingress/config/ingress_config_test.go b/pkg/ingress/config/ingress_config_test.go index c070572ffe..c49d55fc9a 100644 --- a/pkg/ingress/config/ingress_config_test.go +++ b/pkg/ingress/config/ingress_config_test.go @@ -649,11 +649,17 @@ func TestConstructExtAuthzEnvoyFilter(t *testing.T) { t.Fatalf("construct error %v", err) } envoyFilter := config.Spec.(*networking.EnvoyFilter) - pb, err := xds.BuildXDSObjectFromStruct(networking.EnvoyFilter_HTTP_FILTER, envoyFilter.ConfigPatches[0].Patch.Value, false) - //pb, err := xds.BuildXDSObjectFromStruct(networking.EnvoyFilter_HTTP_FILTER, envoyFilter.ConfigPatches[1].Patch.Value, false) + extAuthzPb, err := xds.BuildXDSObjectFromStruct(networking.EnvoyFilter_HTTP_FILTER, envoyFilter.ConfigPatches[0].Patch.Value, false) if err != nil { t.Fatalf("build object error %v", err) } - target := proto.Clone(pb).(*httppb.HttpFilter) + target := proto.Clone(extAuthzPb).(*httppb.HttpFilter) + t.Log(target) + + rbacPb, err := xds.BuildXDSObjectFromStruct(networking.EnvoyFilter_HTTP_FILTER, envoyFilter.ConfigPatches[1].Patch.Value, false) + if err != nil { + t.Fatalf("build object error %v", err) + } + target = proto.Clone(rbacPb).(*httppb.HttpFilter) t.Log(target) } diff --git a/pkg/ingress/kube/annotations/authz.go b/pkg/ingress/kube/annotations/authz.go index f407d03f89..09f84bf353 100644 --- a/pkg/ingress/kube/annotations/authz.go +++ b/pkg/ingress/kube/annotations/authz.go @@ -73,6 +73,8 @@ type ServiceConfig struct { ServiceName string ServicePort int ServicePathPrefix string + AuthorizationRequest string + AuthorizationResponse string ReqAllowedHeadersExact []string ReqAllowedHeadersPrefix []string ReqAllowedHeadersSuffix []string From 924637051021ec5760f7bd1bddb6ddcc91988083 Mon Sep 17 00:00:00 2001 From: zc <2510165121@qq.com> Date: Sat, 1 Apr 2023 00:40:42 +0800 Subject: [PATCH 3/3] refactor: authorizationReq and authorizationResp config use json string instead of singal field (#207) --- pkg/ingress/kube/annotations/authz.go | 97 ++++++--------------------- 1 file changed, 22 insertions(+), 75 deletions(-) diff --git a/pkg/ingress/kube/annotations/authz.go b/pkg/ingress/kube/annotations/authz.go index 09f84bf353..8906edec44 100644 --- a/pkg/ingress/kube/annotations/authz.go +++ b/pkg/ingress/kube/annotations/authz.go @@ -19,28 +19,18 @@ import ( ) const ( - authzTypeAnn = "authz-type" - protoAnn = "ext-authz-proto" - serviceAnn = "ext-authz-service" - servicePortAnn = "ext-authz-service-port" - servicePathPrefixAnn = "ext-authz-http-service-path-prefix" - reqAllowedHeadersExactAnn = "ext-authz-req-allowed-headers-exact" - reqAllowedHeadersPrefixAnn = "ext-authz-req-allowed-headers-prefix" - reqAllowedHeadersSuffixAnn = "ext-authz-req-allowed-headers-suffix" - reqAllowedHeadersContainsAnn = "ext-authz-req-allowed-headers-contains" - respAllowedUpstreamHeadersExactAnn = "ext-authz-req-allowed-upstream-headers-exact" - respAllowedUpstreamHeadersPrefixAnn = "ext-authz-req-allowed-upstream-headers-prefix" - respAllowedUpstreamHeadersSuffixAnn = "ext-authz-req-allowed-upstream-headers-suffix" - respAllowedUpstreamHeadersContainsAnn = "ext-authz-req-allowed-upstream-headers-contains" - respAllowedClientHeadersExactAnn = "ext-authz-req-allowed-client-headers-exact" - respAllowedClientHeadersPrefixAnn = "ext-authz-req-allowed-client-headers-prefix" - respAllowedClientHeadersSuffixAnn = "ext-authz-req-allowed-client-headers-suffix" - respAllowedClientHeadersContainsAnn = "ext-authz-req-allowed-client-headers-contains" - rbacPolicyIdAnn = "ext-authz-rbac-policy-id" - reqMaxBytesAnn = "ext-authz-req-max-bytes" - reqAllowPartialAnn = "ext-authz-req-allow-partial" - packAsBytesAnn = "ext-authz-pack-as-bytes" - serviceTimeOutAnn = "ext-authz-timeout" + authzTypeAnn = "authz-type" + protoAnn = "ext-authz-proto" + serviceAnn = "ext-authz-service" + servicePortAnn = "ext-authz-service-port" + servicePathPrefixAnn = "ext-authz-http-service-path-prefix" + rbacPolicyIdAnn = "ext-authz-rbac-policy-id" + reqMaxBytesAnn = "ext-authz-req-max-bytes" + reqAllowPartialAnn = "ext-authz-req-allow-partial" + packAsBytesAnn = "ext-authz-pack-as-bytes" + serviceTimeOutAnn = "ext-authz-timeout" + authorizationRequestAnn = "ext-authz-request" + authorizationResponseAnn = "ext-authz-response" defaultAuthzType = "ext-authz" ) @@ -69,24 +59,12 @@ type ExtAuthzConfig struct { } type ServiceConfig struct { - Timeout string - ServiceName string - ServicePort int - ServicePathPrefix string - AuthorizationRequest string - AuthorizationResponse string - ReqAllowedHeadersExact []string - ReqAllowedHeadersPrefix []string - ReqAllowedHeadersSuffix []string - ReqAllowedHeadersContains []string - RespAllowedUpstreamHeadersExact []string - RespAllowedUpstreamHeadersPrefix []string - RespAllowedUpstreamHeadersSuffix []string - RespAllowedUpstreamHeadersContains []string - RespAllowedClientHeadersExact []string - RespAllowedClientHeadersPrefix []string - RespAllowedClientHeadersSuffix []string - RespAllowedClientHeadersContains []string + Timeout string + ServiceName string + ServicePort int + ServicePathPrefix string + AuthorizationRequest string + AuthorizationResponse string } type authz struct{} @@ -143,41 +121,11 @@ func (a authz) Parse(annotations Annotations, config *Ingress, globalContext *Gl if servicePathPrefix, err := annotations.ParseStringForHigress(servicePathPrefixAnn); err == nil { serviceConfig.ServicePathPrefix = servicePathPrefix } - if reqAllowedHeadersExact, err := annotations.ParseStringForHigress(reqAllowedHeadersExactAnn); err == nil { - serviceConfig.ReqAllowedHeadersExact = splitStringWithSpaceTrim(reqAllowedHeadersExact) + if authorizationRequest, err := annotations.ParseStringForHigress(authorizationRequestAnn); err == nil { + serviceConfig.AuthorizationRequest = authorizationRequest } - if reqAllowedHeadersPrefix, err := annotations.ParseStringForHigress(reqAllowedHeadersPrefixAnn); err == nil { - serviceConfig.ReqAllowedHeadersPrefix = splitStringWithSpaceTrim(reqAllowedHeadersPrefix) - } - if reqAllowedHeadersSuffix, err := annotations.ParseStringForHigress(reqAllowedHeadersSuffixAnn); err == nil { - serviceConfig.ReqAllowedHeadersSuffix = splitStringWithSpaceTrim(reqAllowedHeadersSuffix) - } - if reqAllowedHeadersContains, err := annotations.ParseStringForHigress(reqAllowedHeadersContainsAnn); err == nil { - serviceConfig.ReqAllowedHeadersContains = splitStringWithSpaceTrim(reqAllowedHeadersContains) - } - if respAllowedUpstreamHeadersExact, err := annotations.ParseStringForHigress(respAllowedUpstreamHeadersExactAnn); err == nil { - serviceConfig.RespAllowedUpstreamHeadersExact = splitStringWithSpaceTrim(respAllowedUpstreamHeadersExact) - } - if respAllowedUpstreamHeadersPrefix, err := annotations.ParseStringForHigress(respAllowedUpstreamHeadersPrefixAnn); err == nil { - serviceConfig.RespAllowedUpstreamHeadersPrefix = splitStringWithSpaceTrim(respAllowedUpstreamHeadersPrefix) - } - if respAllowedUpstreamHeadersSuffix, err := annotations.ParseStringForHigress(respAllowedUpstreamHeadersSuffixAnn); err == nil { - serviceConfig.RespAllowedUpstreamHeadersSuffix = splitStringWithSpaceTrim(respAllowedUpstreamHeadersSuffix) - } - if respAllowedUpstreamHeadersContains, err := annotations.ParseStringForHigress(respAllowedUpstreamHeadersContainsAnn); err == nil { - serviceConfig.RespAllowedUpstreamHeadersContains = splitStringWithSpaceTrim(respAllowedUpstreamHeadersContains) - } - if respAllowedClientHeadersExact, err := annotations.ParseStringForHigress(respAllowedClientHeadersExactAnn); err == nil { - serviceConfig.RespAllowedClientHeadersExact = splitStringWithSpaceTrim(respAllowedClientHeadersExact) - } - if respAllowedClientHeadersPrefix, err := annotations.ParseStringForHigress(respAllowedClientHeadersPrefixAnn); err == nil { - serviceConfig.RespAllowedClientHeadersPrefix = splitStringWithSpaceTrim(respAllowedClientHeadersPrefix) - } - if respAllowedClientHeadersSuffix, err := annotations.ParseStringForHigress(respAllowedClientHeadersSuffixAnn); err == nil { - serviceConfig.RespAllowedClientHeadersSuffix = splitStringWithSpaceTrim(respAllowedClientHeadersSuffix) - } - if respAllowedClientHeadersContains, err := annotations.ParseStringForHigress(respAllowedClientHeadersContainsAnn); err == nil { - serviceConfig.RespAllowedClientHeadersContains = splitStringWithSpaceTrim(respAllowedClientHeadersContains) + if authorizationResponse, err := annotations.ParseStringForHigress(authorizationResponseAnn); err == nil { + serviceConfig.AuthorizationRequest = authorizationResponse } if rbacPolicyId, err := annotations.ParseStringForHigress(rbacPolicyIdAnn); err == nil { extAuthzConfig.RbacPolicyId = rbacPolicyId @@ -186,7 +134,6 @@ func (a authz) Parse(annotations Annotations, config *Ingress, globalContext *Gl extAuthzConfig.RbacPolicyId = rbacPolicyId } if reqMaxBytes, err := annotations.ParseUint32ForHigress(reqMaxBytesAnn); err == nil { - extAuthzConfig.ReqMaxBytes = reqMaxBytes } if reqAllowPartial, err := annotations.ParseBoolForHigress(reqAllowPartialAnn); err == nil {