blob: 341057099601ac3ddc54b04c881c84d796f5cb17 [file] [log] [blame]
// Copyright Istio Authors
//
// 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 builder
import (
"fmt"
"net/url"
"sort"
"strconv"
"strings"
)
import (
"github.com/davecgh/go-spew/spew"
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
extauthzhttp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
extauthztcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/ext_authz/v3"
envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
envoytypev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/hashicorp/go-multierror"
"google.golang.org/protobuf/types/known/durationpb"
meshconfig "istio.io/api/mesh/v1alpha1"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/extensionproviders"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
authzmodel "github.com/apache/dubbo-go-pixiu/pilot/pkg/security/authz/model"
"github.com/apache/dubbo-go-pixiu/pkg/config/validation"
)
const (
extAuthzMatchPrefix = "istio-ext-authz"
)
var (
rbacPolicyMatchAll = &rbacpb.Policy{
Permissions: []*rbacpb.Permission{{Rule: &rbacpb.Permission_Any{Any: true}}},
Principals: []*rbacpb.Principal{{Identifier: &rbacpb.Principal_Any{Any: true}}},
}
rbacDefaultDenyAll = &rbacpb.RBAC{
Action: rbacpb.RBAC_DENY,
Policies: map[string]*rbacpb.Policy{
"default-deny-all-due-to-bad-CUSTOM-action": rbacPolicyMatchAll,
},
}
supportedStatus = func() []int {
var supported []int
for code := range envoytypev3.StatusCode_name {
supported = append(supported, int(code))
}
sort.Ints(supported)
return supported
}()
)
type builtExtAuthz struct {
http *extauthzhttp.ExtAuthz
tcp *extauthztcp.ExtAuthz
err error
}
func processExtensionProvider(push *model.PushContext) map[string]*builtExtAuthz {
resolved := map[string]*builtExtAuthz{}
for i, config := range push.Mesh.ExtensionProviders {
var errs error
if config.Name == "" {
errs = multierror.Append(errs, fmt.Errorf("extension provider name must not be empty, found empty at index: %d", i))
} else if _, found := resolved[config.Name]; found {
errs = multierror.Append(errs, fmt.Errorf("extension provider name must be unique, found duplicate: %s", config.Name))
}
var parsed *builtExtAuthz
var err error
// TODO(yangminzhu): Refactor and cache the ext_authz config.
switch p := config.Provider.(type) {
case *meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp:
if err = validation.ValidateExtensionProviderEnvoyExtAuthzHTTP(p.EnvoyExtAuthzHttp); err == nil {
parsed, err = buildExtAuthzHTTP(push, p.EnvoyExtAuthzHttp)
}
case *meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc:
if err = validation.ValidateExtensionProviderEnvoyExtAuthzGRPC(p.EnvoyExtAuthzGrpc); err == nil {
parsed, err = buildExtAuthzGRPC(push, p.EnvoyExtAuthzGrpc)
}
default:
continue
}
if err != nil {
errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("failed to parse extension provider %q:", config.Name)))
}
if parsed == nil {
parsed = &builtExtAuthz{}
}
parsed.err = errs
resolved[config.Name] = parsed
}
if authzLog.DebugEnabled() {
authzLog.Debugf("Resolved extension providers: %v", spew.Sdump(resolved))
}
return resolved
}
func notAllTheSame(names []string) bool {
for i := 1; i < len(names); i++ {
if names[i-1] != names[i] {
return true
}
}
return false
}
func getExtAuthz(resolved map[string]*builtExtAuthz, providers []string) (*builtExtAuthz, error) {
if resolved == nil {
return nil, fmt.Errorf("extension provider is either invalid or undefined")
}
if len(providers) < 1 {
return nil, fmt.Errorf("no provider specified in authorization policy")
}
if notAllTheSame(providers) {
return nil, fmt.Errorf("only 1 provider can be used per workload, found multiple providers: %v", providers)
}
provider := providers[0]
ret, found := resolved[provider]
if !found {
var li []string
for p := range resolved {
li = append(li, p)
}
return nil, fmt.Errorf("available providers are %v but found %q", li, provider)
} else if ret.err != nil {
return nil, fmt.Errorf("found errors in provider %s: %v", provider, ret.err)
}
return ret, nil
}
func buildExtAuthzHTTP(push *model.PushContext,
config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider) (*builtExtAuthz, error) {
var errs error
port, err := parsePort(config.Port)
if err != nil {
errs = multierror.Append(errs, err)
}
hostname, cluster, err := extensionproviders.LookupCluster(push, config.Service, port)
if err != nil {
errs = multierror.Append(errs, err)
}
status, err := parseStatusOnError(config.StatusOnError)
if err != nil {
errs = multierror.Append(errs, err)
}
if config.PathPrefix != "" {
if _, err := url.Parse(config.PathPrefix); err != nil {
errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("invalid pathPrefix %q:", config.PathPrefix)))
}
if !strings.HasPrefix(config.PathPrefix, "/") {
errs = multierror.Append(errs, fmt.Errorf("pathPrefix must begin with `/`, found: %q", config.PathPrefix))
}
}
checkWildcard := func(field string, values []string) {
for _, val := range values {
if val == "*" {
errs = multierror.Append(errs, fmt.Errorf("a single wildcard (\"*\") is not supported, change it to either prefix or suffix match: %s", field))
}
}
}
checkWildcard("IncludeRequestHeadersInCheck", config.IncludeRequestHeadersInCheck)
checkWildcard("IncludeHeadersInCheck", config.IncludeHeadersInCheck)
checkWildcard("HeadersToDownstreamOnDeny", config.HeadersToDownstreamOnDeny)
checkWildcard("HeadersToDownstreamOnAllow", config.HeadersToDownstreamOnAllow)
checkWildcard("HeadersToUpstreamOnAllow", config.HeadersToUpstreamOnAllow)
if errs != nil {
return nil, errs
}
return generateHTTPConfig(hostname, cluster, status, config), nil
}
func buildExtAuthzGRPC(push *model.PushContext,
config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider) (*builtExtAuthz, error) {
var errs error
port, err := parsePort(config.Port)
if err != nil {
errs = multierror.Append(errs, err)
}
_, cluster, err := extensionproviders.LookupCluster(push, config.Service, port)
if err != nil {
errs = multierror.Append(errs, err)
}
status, err := parseStatusOnError(config.StatusOnError)
if err != nil {
errs = multierror.Append(errs, err)
}
if errs != nil {
return nil, errs
}
return generateGRPCConfig(cluster, config, status), nil
}
func parsePort(port uint32) (int, error) {
if 1 <= port && port <= 65535 {
return int(port), nil
}
return 0, fmt.Errorf("port must be in the range [1, 65535], found: %d", port)
}
func parseStatusOnError(status string) (*envoytypev3.HttpStatus, error) {
if status == "" {
return nil, nil
}
code, err := strconv.ParseInt(status, 10, 32)
if err != nil {
return nil, multierror.Prefix(err, fmt.Sprintf("invalid statusOnError %q:", status))
}
if _, found := envoytypev3.StatusCode_name[int32(code)]; !found {
return nil, fmt.Errorf("unsupported statusOnError %s, supported values: %v", status, supportedStatus)
}
return &envoytypev3.HttpStatus{Code: envoytypev3.StatusCode(code)}, nil
}
func generateHTTPConfig(hostname, cluster string, status *envoytypev3.HttpStatus,
config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider) *builtExtAuthz {
service := &extauthzhttp.HttpService{
PathPrefix: config.PathPrefix,
ServerUri: &envoy_config_core_v3.HttpUri{
// Timeout is required.
Timeout: timeoutOrDefault(config.Timeout),
// Uri is required but actually not used in the ext_authz filter.
Uri: fmt.Sprintf("http://%s", hostname),
HttpUpstreamType: &envoy_config_core_v3.HttpUri_Cluster{
Cluster: cluster,
},
},
}
allowedHeaders := generateHeaders(config.IncludeRequestHeadersInCheck)
if allowedHeaders == nil {
// IncludeHeadersInCheck is deprecated, only use it if IncludeRequestHeadersInCheck is not set.
// TODO: Remove the IncludeHeadersInCheck field before promoting to beta.
allowedHeaders = generateHeaders(config.IncludeHeadersInCheck)
}
var headersToAdd []*envoy_config_core_v3.HeaderValue
var additionalHeaders []string
for k := range config.IncludeAdditionalHeadersInCheck {
additionalHeaders = append(additionalHeaders, k)
}
sort.Strings(additionalHeaders)
for _, k := range additionalHeaders {
headersToAdd = append(headersToAdd, &envoy_config_core_v3.HeaderValue{
Key: k,
Value: config.IncludeAdditionalHeadersInCheck[k],
})
}
if allowedHeaders != nil || len(headersToAdd) != 0 {
service.AuthorizationRequest = &extauthzhttp.AuthorizationRequest{
AllowedHeaders: allowedHeaders,
HeadersToAdd: headersToAdd,
}
}
if len(config.HeadersToUpstreamOnAllow) > 0 || len(config.HeadersToDownstreamOnDeny) > 0 ||
len(config.HeadersToDownstreamOnAllow) > 0 {
service.AuthorizationResponse = &extauthzhttp.AuthorizationResponse{
AllowedUpstreamHeaders: generateHeaders(config.HeadersToUpstreamOnAllow),
AllowedClientHeaders: generateHeaders(config.HeadersToDownstreamOnDeny),
AllowedClientHeadersOnSuccess: generateHeaders(config.HeadersToDownstreamOnAllow),
}
}
http := &extauthzhttp.ExtAuthz{
StatusOnError: status,
FailureModeAllow: config.FailOpen,
TransportApiVersion: envoy_config_core_v3.ApiVersion_V3,
Services: &extauthzhttp.ExtAuthz_HttpService{
HttpService: service,
},
FilterEnabledMetadata: generateFilterMatcher(wellknown.HTTPRoleBasedAccessControl),
WithRequestBody: withBodyRequest(config.IncludeRequestBodyInCheck),
}
return &builtExtAuthz{http: http}
}
func generateGRPCConfig(cluster string, config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider,
status *envoytypev3.HttpStatus) *builtExtAuthz {
// The cluster includes the character `|` that is invalid in gRPC authority header and will cause the connection
// rejected in the server side, replace it with a valid character and set in authority otherwise ext_authz will
// use the cluster name as default authority.
authority := strings.ReplaceAll(cluster, "|", "_.")
grpc := &envoy_config_core_v3.GrpcService{
TargetSpecifier: &envoy_config_core_v3.GrpcService_EnvoyGrpc_{
EnvoyGrpc: &envoy_config_core_v3.GrpcService_EnvoyGrpc{
ClusterName: cluster,
Authority: authority,
},
},
Timeout: timeoutOrDefault(config.Timeout),
}
http := &extauthzhttp.ExtAuthz{
StatusOnError: status,
FailureModeAllow: config.FailOpen,
Services: &extauthzhttp.ExtAuthz_GrpcService{
GrpcService: grpc,
},
FilterEnabledMetadata: generateFilterMatcher(wellknown.HTTPRoleBasedAccessControl),
TransportApiVersion: envoy_config_core_v3.ApiVersion_V3,
WithRequestBody: withBodyRequest(config.IncludeRequestBodyInCheck),
}
tcp := &extauthztcp.ExtAuthz{
StatPrefix: "tcp.",
FailureModeAllow: config.FailOpen,
TransportApiVersion: envoy_config_core_v3.ApiVersion_V3,
GrpcService: grpc,
FilterEnabledMetadata: generateFilterMatcher(wellknown.RoleBasedAccessControl),
}
return &builtExtAuthz{http: http, tcp: tcp}
}
func generateHeaders(headers []string) *envoy_type_matcher_v3.ListStringMatcher {
if len(headers) == 0 {
return nil
}
var patterns []*envoy_type_matcher_v3.StringMatcher
for _, header := range headers {
pattern := &envoy_type_matcher_v3.StringMatcher{
IgnoreCase: true,
}
if strings.HasPrefix(header, "*") {
pattern.MatchPattern = &envoy_type_matcher_v3.StringMatcher_Suffix{
Suffix: strings.TrimPrefix(header, "*"),
}
} else if strings.HasSuffix(header, "*") {
pattern.MatchPattern = &envoy_type_matcher_v3.StringMatcher_Prefix{
Prefix: strings.TrimSuffix(header, "*"),
}
} else {
pattern.MatchPattern = &envoy_type_matcher_v3.StringMatcher_Exact{
Exact: header,
}
}
patterns = append(patterns, pattern)
}
return &envoy_type_matcher_v3.ListStringMatcher{Patterns: patterns}
}
func generateFilterMatcher(name string) *envoy_type_matcher_v3.MetadataMatcher {
return &envoy_type_matcher_v3.MetadataMatcher{
Filter: name,
Path: []*envoy_type_matcher_v3.MetadataMatcher_PathSegment{
{
Segment: &envoy_type_matcher_v3.MetadataMatcher_PathSegment_Key{
Key: authzmodel.RBACExtAuthzShadowRulesStatPrefix + authzmodel.RBACShadowEffectivePolicyID,
},
},
},
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_Prefix{
Prefix: extAuthzMatchPrefix,
},
},
},
},
}
}
func timeoutOrDefault(t *durationpb.Duration) *durationpb.Duration {
if t == nil {
// Default timeout is 600s.
return &durationpb.Duration{Seconds: 600}
}
return t
}
func withBodyRequest(config *meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationRequestBody) *extauthzhttp.BufferSettings {
if config == nil {
return nil
}
return &extauthzhttp.BufferSettings{
MaxRequestBytes: config.MaxRequestBytes,
AllowPartialMessage: config.AllowPartialMessage,
PackAsBytes: config.PackAsBytes,
}
}