blob: 8ccd459494057105d5ecb3b3995fc2744f565c95 [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 (
"os"
"testing"
)
import (
tcppb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
meshconfig "istio.io/api/mesh/v1alpha1"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/config/kube/crd"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/config/memory"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/plugin"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/security/trustdomain"
"github.com/apache/dubbo-go-pixiu/pilot/test/util"
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/host"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/collections"
"github.com/apache/dubbo-go-pixiu/pkg/util/protomarshal"
)
const (
basePath = "testdata/"
)
var (
httpbin = map[string]string{
"app": "httpbin",
"version": "v1",
}
meshConfigGRPCNoNamespace = &meshconfig.MeshConfig{
ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
{
Name: "default",
Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc{
EnvoyExtAuthzGrpc: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider{
Service: "my-custom-ext-authz.foo.svc.cluster.local",
Port: 9000,
FailOpen: true,
StatusOnError: "403",
},
},
},
},
}
meshConfigGRPC = &meshconfig.MeshConfig{
ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
{
Name: "default",
Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc{
EnvoyExtAuthzGrpc: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider{
Service: "foo/my-custom-ext-authz.foo.svc.cluster.local",
Port: 9000,
Timeout: &durationpb.Duration{Nanos: 2000 * 1000},
FailOpen: true,
StatusOnError: "403",
IncludeRequestBodyInCheck: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationRequestBody{
MaxRequestBytes: 4096,
AllowPartialMessage: true,
},
},
},
},
},
}
meshConfigHTTP = &meshconfig.MeshConfig{
ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
{
Name: "default",
Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp{
EnvoyExtAuthzHttp: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider{
Service: "foo/my-custom-ext-authz.foo.svc.cluster.local",
Port: 9000,
Timeout: &durationpb.Duration{Seconds: 10},
FailOpen: true,
StatusOnError: "403",
PathPrefix: "/check",
IncludeRequestHeadersInCheck: []string{"x-custom-id", "x-prefix-*", "*-suffix"},
IncludeHeadersInCheck: []string{"should-not-include-when-IncludeRequestHeadersInCheck-is-set"},
IncludeAdditionalHeadersInCheck: map[string]string{"x-header-1": "value-1", "x-header-2": "value-2"},
IncludeRequestBodyInCheck: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationRequestBody{
MaxRequestBytes: 2048,
AllowPartialMessage: true,
PackAsBytes: true,
},
HeadersToUpstreamOnAllow: []string{"Authorization", "x-prefix-*", "*-suffix"},
HeadersToDownstreamOnDeny: []string{"Set-cookie", "x-prefix-*", "*-suffix"},
HeadersToDownstreamOnAllow: []string{"Set-cookie", "x-prefix-*", "*-suffix"},
},
},
},
},
}
meshConfigInvalid = &meshconfig.MeshConfig{
ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
{
Name: "default",
Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp{
EnvoyExtAuthzHttp: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider{
Service: "foo/my-custom-ext-authz",
Port: 999999,
PathPrefix: "check",
StatusOnError: "999",
},
},
},
},
}
)
func TestGenerator_GenerateHTTP(t *testing.T) {
testCases := []struct {
name string
tdBundle trustdomain.Bundle
meshConfig *meshconfig.MeshConfig
version *model.IstioVersion
input string
want []string
}{
{
name: "allow-empty-rule",
input: "allow-empty-rule-in.yaml",
want: []string{"allow-empty-rule-out.yaml"},
},
{
name: "allow-full-rule",
input: "allow-full-rule-in.yaml",
want: []string{"allow-full-rule-out.yaml"},
},
{
name: "allow-nil-rule",
input: "allow-nil-rule-in.yaml",
want: []string{"allow-nil-rule-out.yaml"},
},
{
name: "allow-path",
input: "allow-path-in.yaml",
want: []string{"allow-path-out.yaml"},
},
{
name: "audit-full-rule",
input: "audit-full-rule-in.yaml",
want: []string{"audit-full-rule-out.yaml"},
},
{
name: "custom-grpc-provider-no-namespace",
meshConfig: meshConfigGRPCNoNamespace,
input: "custom-simple-http-in.yaml",
want: []string{"custom-grpc-provider-no-namespace-out1.yaml", "custom-grpc-provider-no-namespace-out2.yaml"},
},
{
name: "custom-grpc-provider",
meshConfig: meshConfigGRPC,
input: "custom-simple-http-in.yaml",
want: []string{"custom-grpc-provider-out1.yaml", "custom-grpc-provider-out2.yaml"},
},
{
name: "custom-http-provider",
meshConfig: meshConfigHTTP,
input: "custom-simple-http-in.yaml",
want: []string{"custom-http-provider-out1.yaml", "custom-http-provider-out2.yaml"},
},
{
name: "custom-bad-multiple-providers",
meshConfig: meshConfigHTTP,
input: "custom-bad-multiple-providers-in.yaml",
want: []string{"custom-bad-out.yaml"},
},
{
name: "custom-bad-invalid-config",
meshConfig: meshConfigInvalid,
input: "custom-simple-http-in.yaml",
want: []string{"custom-bad-out.yaml"},
},
{
name: "deny-and-allow",
input: "deny-and-allow-in.yaml",
want: []string{"deny-and-allow-out1.yaml", "deny-and-allow-out2.yaml"},
},
{
name: "deny-empty-rule",
input: "deny-empty-rule-in.yaml",
want: []string{"deny-empty-rule-out.yaml"},
},
{
name: "dry-run-allow-and-deny",
input: "dry-run-allow-and-deny-in.yaml",
want: []string{"dry-run-allow-and-deny-out1.yaml", "dry-run-allow-and-deny-out2.yaml"},
},
{
name: "dry-run-allow",
input: "dry-run-allow-in.yaml",
want: []string{"dry-run-allow-out.yaml"},
},
{
name: "dry-run-mix",
input: "dry-run-mix-in.yaml",
want: []string{"dry-run-mix-out.yaml"},
},
{
name: "multiple-policies",
input: "multiple-policies-in.yaml",
want: []string{"multiple-policies-out.yaml"},
},
{
name: "single-policy",
input: "single-policy-in.yaml",
want: []string{"single-policy-out.yaml"},
},
{
name: "trust-domain-one-alias",
tdBundle: trustdomain.NewBundle("td1", []string{"cluster.local"}),
input: "simple-policy-td-aliases-in.yaml",
want: []string{"simple-policy-td-aliases-out.yaml"},
},
{
name: "trust-domain-multiple-aliases",
tdBundle: trustdomain.NewBundle("td1", []string{"cluster.local", "some-td"}),
input: "simple-policy-multiple-td-aliases-in.yaml",
want: []string{"simple-policy-multiple-td-aliases-out.yaml"},
},
{
name: "trust-domain-wildcard-in-principal",
tdBundle: trustdomain.NewBundle("td1", []string{"foobar"}),
input: "simple-policy-principal-with-wildcard-in.yaml",
want: []string{"simple-policy-principal-with-wildcard-out.yaml"},
},
{
name: "trust-domain-aliases-in-source-principal",
tdBundle: trustdomain.NewBundle("new-td", []string{"old-td", "some-trustdomain"}),
input: "td-aliases-source-principal-in.yaml",
want: []string{"td-aliases-source-principal-out.yaml"},
},
}
baseDir := "http/"
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
option := Option{
IsCustomBuilder: tc.meshConfig != nil,
Logger: &AuthzLogger{},
}
in := inputParams(t, baseDir+tc.input, tc.meshConfig, tc.version)
defer option.Logger.Report(in)
policies := in.Push.AuthzPolicies.ListAuthorizationPolicies(in.Node.ConfigNamespace, in.Node.Metadata.Labels)
g := New(tc.tdBundle, in.Push, policies, option)
if g == nil {
t.Fatalf("failed to create generator")
}
got := g.BuildHTTP()
verify(t, convertHTTP(got), baseDir, tc.want, false /* forTCP */)
})
}
}
func TestGenerator_GenerateTCP(t *testing.T) {
testCases := []struct {
name string
tdBundle trustdomain.Bundle
meshConfig *meshconfig.MeshConfig
input string
want []string
}{
{
name: "allow-both-http-tcp",
input: "allow-both-http-tcp-in.yaml",
want: []string{"allow-both-http-tcp-out.yaml"},
},
{
name: "allow-only-http",
input: "allow-only-http-in.yaml",
want: []string{"allow-only-http-out.yaml"},
},
{
name: "audit-both-http-tcp",
input: "audit-both-http-tcp-in.yaml",
want: []string{"audit-both-http-tcp-out.yaml"},
},
{
name: "custom-both-http-tcp",
meshConfig: meshConfigGRPC,
input: "custom-both-http-tcp-in.yaml",
want: []string{"custom-both-http-tcp-out1.yaml", "custom-both-http-tcp-out2.yaml"},
},
{
name: "custom-only-http",
meshConfig: meshConfigHTTP,
input: "custom-only-http-in.yaml",
want: []string{},
},
{
name: "deny-both-http-tcp",
input: "deny-both-http-tcp-in.yaml",
want: []string{"deny-both-http-tcp-out.yaml"},
},
{
name: "dry-run-mix",
input: "dry-run-mix-in.yaml",
want: []string{"dry-run-mix-out.yaml"},
},
}
baseDir := "tcp/"
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
option := Option{
IsCustomBuilder: tc.meshConfig != nil,
Logger: &AuthzLogger{},
}
in := inputParams(t, baseDir+tc.input, tc.meshConfig, nil)
defer option.Logger.Report(in)
policies := in.Push.AuthzPolicies.ListAuthorizationPolicies(in.Node.ConfigNamespace, in.Node.Metadata.Labels)
g := New(tc.tdBundle, in.Push, policies, option)
if g == nil {
t.Fatalf("failed to create generator")
}
got := g.BuildTCP()
verify(t, convertTCP(got), baseDir, tc.want, true /* forTCP */)
})
}
}
func verify(t *testing.T, gots []proto.Message, baseDir string, wants []string, forTCP bool) {
t.Helper()
if len(gots) != len(wants) {
t.Fatalf("got %d configs but want %d", len(gots), len(wants))
}
for i, got := range gots {
gotYaml, err := protomarshal.ToYAML(got)
if err != nil {
t.Fatalf("failed to convert to YAML: %v", err)
}
wantFile := basePath + baseDir + wants[i]
want := yamlConfig(t, wantFile, forTCP)
wantYaml, err := protomarshal.ToYAML(want)
if err != nil {
t.Fatalf("failed to convert to YAML: %v", err)
}
util.RefreshGoldenFile(t, []byte(gotYaml), wantFile)
if err := util.Compare([]byte(gotYaml), []byte(wantYaml)); err != nil {
t.Error(err)
}
}
}
func yamlPolicy(t *testing.T, filename string) *model.AuthorizationPolicies {
t.Helper()
data, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("failed to read input yaml file: %v", err)
}
c, _, err := crd.ParseInputs(string(data))
if err != nil {
t.Fatalf("failde to parse CRD: %v", err)
}
var configs []*config.Config
for i := range c {
configs = append(configs, &c[i])
}
return newAuthzPolicies(t, configs)
}
func yamlConfig(t *testing.T, filename string, forTCP bool) proto.Message {
t.Helper()
data, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
if forTCP {
out := &tcppb.Filter{}
if err := protomarshal.ApplyYAML(string(data), out); err != nil {
t.Fatalf("failed to parse YAML: %v", err)
}
return out
}
out := &httppb.HttpFilter{}
if err := protomarshal.ApplyYAML(string(data), out); err != nil {
t.Fatalf("failed to parse YAML: %v", err)
}
return out
}
func convertHTTP(in []*httppb.HttpFilter) []proto.Message {
ret := make([]proto.Message, len(in))
for i := range in {
ret[i] = in[i]
}
return ret
}
func convertTCP(in []*tcppb.Filter) []proto.Message {
ret := make([]proto.Message, len(in))
for i := range in {
ret[i] = in[i]
}
return ret
}
func newAuthzPolicies(t *testing.T, policies []*config.Config) *model.AuthorizationPolicies {
store := model.MakeIstioStore(memory.Make(collections.Pilot))
for _, p := range policies {
if _, err := store.Create(*p); err != nil {
t.Fatalf("newAuthzPolicies: %v", err)
}
}
authzPolicies, err := model.GetAuthorizationPolicies(&model.Environment{
ConfigStore: store,
})
if err != nil {
t.Fatalf("newAuthzPolicies: %v", err)
}
return authzPolicies
}
func inputParams(t *testing.T, input string, mc *meshconfig.MeshConfig, version *model.IstioVersion) *plugin.InputParams {
t.Helper()
ret := &plugin.InputParams{
Node: &model.Proxy{
ID: "test-node",
ConfigNamespace: "foo",
Metadata: &model.NodeMetadata{
Labels: httpbin,
},
IstioVersion: version,
},
Push: &model.PushContext{
AuthzPolicies: yamlPolicy(t, basePath+input),
Mesh: mc,
},
}
ret.Push.ServiceIndex.HostnameAndNamespace = map[host.Name]map[string]*model.Service{
"my-custom-ext-authz.foo.svc.cluster.local": {
"foo": &model.Service{
Hostname: "my-custom-ext-authz.foo.svc.cluster.local",
},
},
}
return ret
}