istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/security/authz/builder/builder_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package builder
    16  
    17  import (
    18  	"os"
    19  	"testing"
    20  
    21  	listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    22  	hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/types/known/durationpb"
    25  
    26  	meshconfig "istio.io/api/mesh/v1alpha1"
    27  	"istio.io/istio/pilot/pkg/config/kube/crd"
    28  	"istio.io/istio/pilot/pkg/config/memory"
    29  	"istio.io/istio/pilot/pkg/model"
    30  	"istio.io/istio/pilot/pkg/security/trustdomain"
    31  	"istio.io/istio/pilot/test/util"
    32  	"istio.io/istio/pkg/config"
    33  	"istio.io/istio/pkg/config/host"
    34  	"istio.io/istio/pkg/config/schema/collections"
    35  	"istio.io/istio/pkg/util/protomarshal"
    36  )
    37  
    38  const (
    39  	basePath = "testdata/"
    40  )
    41  
    42  var (
    43  	httpbin = map[string]string{
    44  		"app":     "httpbin",
    45  		"version": "v1",
    46  	}
    47  	meshConfigGRPCNoNamespace = &meshconfig.MeshConfig{
    48  		ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
    49  			{
    50  				Name: "default",
    51  				Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc{
    52  					EnvoyExtAuthzGrpc: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider{
    53  						Service:       "my-custom-ext-authz.foo.svc.cluster.local",
    54  						Port:          9000,
    55  						FailOpen:      true,
    56  						StatusOnError: "403",
    57  					},
    58  				},
    59  			},
    60  		},
    61  	}
    62  	meshConfigGRPC = &meshconfig.MeshConfig{
    63  		ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
    64  			{
    65  				Name: "default",
    66  				Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc{
    67  					EnvoyExtAuthzGrpc: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider{
    68  						Service:       "foo/my-custom-ext-authz.foo.svc.cluster.local",
    69  						Port:          9000,
    70  						Timeout:       &durationpb.Duration{Nanos: 2000 * 1000},
    71  						FailOpen:      true,
    72  						StatusOnError: "403",
    73  						IncludeRequestBodyInCheck: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationRequestBody{
    74  							MaxRequestBytes:     4096,
    75  							AllowPartialMessage: true,
    76  						},
    77  					},
    78  				},
    79  			},
    80  		},
    81  	}
    82  	meshConfigHTTP = &meshconfig.MeshConfig{
    83  		ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
    84  			{
    85  				Name: "default",
    86  				Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp{
    87  					EnvoyExtAuthzHttp: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider{
    88  						Service:                      "foo/my-custom-ext-authz.foo.svc.cluster.local",
    89  						Port:                         9000,
    90  						Timeout:                      &durationpb.Duration{Seconds: 10},
    91  						FailOpen:                     true,
    92  						StatusOnError:                "403",
    93  						PathPrefix:                   "/check",
    94  						IncludeRequestHeadersInCheck: []string{"x-custom-id", "x-prefix-*", "*-suffix"},
    95  						//nolint: staticcheck
    96  						IncludeHeadersInCheck:           []string{"should-not-include-when-IncludeRequestHeadersInCheck-is-set"},
    97  						IncludeAdditionalHeadersInCheck: map[string]string{"x-header-1": "value-1", "x-header-2": "value-2"},
    98  						IncludeRequestBodyInCheck: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationRequestBody{
    99  							MaxRequestBytes:     2048,
   100  							AllowPartialMessage: true,
   101  							PackAsBytes:         true,
   102  						},
   103  						HeadersToUpstreamOnAllow:   []string{"Authorization", "x-prefix-*", "*-suffix"},
   104  						HeadersToDownstreamOnDeny:  []string{"Set-cookie", "x-prefix-*", "*-suffix"},
   105  						HeadersToDownstreamOnAllow: []string{"Set-cookie", "x-prefix-*", "*-suffix"},
   106  					},
   107  				},
   108  			},
   109  		},
   110  	}
   111  	meshConfigInvalid = &meshconfig.MeshConfig{
   112  		ExtensionProviders: []*meshconfig.MeshConfig_ExtensionProvider{
   113  			{
   114  				Name: "default",
   115  				Provider: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp{
   116  					EnvoyExtAuthzHttp: &meshconfig.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationHttpProvider{
   117  						Service:       "foo/my-custom-ext-authz",
   118  						Port:          999999,
   119  						PathPrefix:    "check",
   120  						StatusOnError: "999",
   121  					},
   122  				},
   123  			},
   124  		},
   125  	}
   126  )
   127  
   128  func TestGenerator_GenerateHTTP(t *testing.T) {
   129  	testCases := []struct {
   130  		name       string
   131  		tdBundle   trustdomain.Bundle
   132  		meshConfig *meshconfig.MeshConfig
   133  		version    *model.IstioVersion
   134  		input      string
   135  		want       []string
   136  	}{
   137  		{
   138  			name:  "allow-empty-rule",
   139  			input: "allow-empty-rule-in.yaml",
   140  			want:  []string{"allow-empty-rule-out.yaml"},
   141  		},
   142  		{
   143  			name:  "allow-full-rule",
   144  			input: "allow-full-rule-in.yaml",
   145  			want:  []string{"allow-full-rule-out.yaml"},
   146  		},
   147  		{
   148  			name:  "allow-nil-rule",
   149  			input: "allow-nil-rule-in.yaml",
   150  			want:  []string{"allow-nil-rule-out.yaml"},
   151  		},
   152  		{
   153  			name:  "allow-path",
   154  			input: "allow-path-in.yaml",
   155  			want:  []string{"allow-path-out.yaml"},
   156  		},
   157  		{
   158  			name:  "audit-full-rule",
   159  			input: "audit-full-rule-in.yaml",
   160  			want:  []string{"audit-full-rule-out.yaml"},
   161  		},
   162  		{
   163  			name:       "custom-grpc-provider-no-namespace",
   164  			meshConfig: meshConfigGRPCNoNamespace,
   165  			input:      "custom-simple-http-in.yaml",
   166  			want:       []string{"custom-grpc-provider-no-namespace-out1.yaml", "custom-grpc-provider-no-namespace-out2.yaml"},
   167  		},
   168  		{
   169  			name:       "custom-grpc-provider",
   170  			meshConfig: meshConfigGRPC,
   171  			input:      "custom-simple-http-in.yaml",
   172  			want:       []string{"custom-grpc-provider-out1.yaml", "custom-grpc-provider-out2.yaml"},
   173  		},
   174  		{
   175  			name:       "custom-http-provider",
   176  			meshConfig: meshConfigHTTP,
   177  			input:      "custom-simple-http-in.yaml",
   178  			want:       []string{"custom-http-provider-out1.yaml", "custom-http-provider-out2.yaml"},
   179  		},
   180  		{
   181  			name:       "custom-bad-multiple-providers",
   182  			meshConfig: meshConfigHTTP,
   183  			input:      "custom-bad-multiple-providers-in.yaml",
   184  			want:       []string{"custom-bad-out.yaml"},
   185  		},
   186  		{
   187  			name:       "custom-bad-invalid-config",
   188  			meshConfig: meshConfigInvalid,
   189  			input:      "custom-simple-http-in.yaml",
   190  			want:       []string{"custom-bad-out.yaml"},
   191  		},
   192  		{
   193  			name:  "deny-and-allow",
   194  			input: "deny-and-allow-in.yaml",
   195  			want:  []string{"deny-and-allow-out1.yaml", "deny-and-allow-out2.yaml"},
   196  		},
   197  		{
   198  			name:  "deny-empty-rule",
   199  			input: "deny-empty-rule-in.yaml",
   200  			want:  []string{"deny-empty-rule-out.yaml"},
   201  		},
   202  		{
   203  			name:  "dry-run-allow-and-deny",
   204  			input: "dry-run-allow-and-deny-in.yaml",
   205  			want:  []string{"dry-run-allow-and-deny-out1.yaml", "dry-run-allow-and-deny-out2.yaml"},
   206  		},
   207  		{
   208  			name:  "dry-run-allow",
   209  			input: "dry-run-allow-in.yaml",
   210  			want:  []string{"dry-run-allow-out.yaml"},
   211  		},
   212  		{
   213  			name:  "dry-run-mix",
   214  			input: "dry-run-mix-in.yaml",
   215  			want:  []string{"dry-run-mix-out.yaml"},
   216  		},
   217  		{
   218  			name:  "multiple-policies",
   219  			input: "multiple-policies-in.yaml",
   220  			want:  []string{"multiple-policies-out.yaml"},
   221  		},
   222  		{
   223  			name:  "single-policy",
   224  			input: "single-policy-in.yaml",
   225  			want:  []string{"single-policy-out.yaml"},
   226  		},
   227  		{
   228  			name:     "trust-domain-one-alias",
   229  			tdBundle: trustdomain.NewBundle("td1", []string{"cluster.local"}),
   230  			input:    "simple-policy-td-aliases-in.yaml",
   231  			want:     []string{"simple-policy-td-aliases-out.yaml"},
   232  		},
   233  		{
   234  			name:     "trust-domain-multiple-aliases",
   235  			tdBundle: trustdomain.NewBundle("td1", []string{"cluster.local", "some-td"}),
   236  			input:    "simple-policy-multiple-td-aliases-in.yaml",
   237  			want:     []string{"simple-policy-multiple-td-aliases-out.yaml"},
   238  		},
   239  		{
   240  			name:     "trust-domain-wildcard-in-principal",
   241  			tdBundle: trustdomain.NewBundle("td1", []string{"foobar"}),
   242  			input:    "simple-policy-principal-with-wildcard-in.yaml",
   243  			want:     []string{"simple-policy-principal-with-wildcard-out.yaml"},
   244  		},
   245  		{
   246  			name:     "trust-domain-aliases-in-source-principal",
   247  			tdBundle: trustdomain.NewBundle("new-td", []string{"old-td", "some-trustdomain"}),
   248  			input:    "td-aliases-source-principal-in.yaml",
   249  			want:     []string{"td-aliases-source-principal-out.yaml"},
   250  		},
   251  	}
   252  
   253  	baseDir := "http/"
   254  	for _, extended := range []bool{false, true} {
   255  		for _, tc := range testCases {
   256  			t.Run(tc.name, func(t *testing.T) {
   257  				option := Option{
   258  					IsCustomBuilder: tc.meshConfig != nil,
   259  					UseExtendedJwt:  extended,
   260  				}
   261  				push := push(t, baseDir+tc.input, tc.meshConfig)
   262  				proxy := node(tc.version)
   263  				selectionOpts := model.PolicyMatcherForProxy(proxy)
   264  				policies := push.AuthzPolicies.ListAuthorizationPolicies(selectionOpts)
   265  				g := New(tc.tdBundle, push, policies, option)
   266  				if g == nil {
   267  					t.Fatalf("failed to create generator")
   268  				}
   269  				got := g.BuildHTTP()
   270  				wants := tc.want
   271  				if extended {
   272  					for i := range wants {
   273  						wants[i] = "extended-" + wants[i]
   274  					}
   275  				}
   276  				verify(t, convertHTTP(got), baseDir, tc.want, false /* forTCP */)
   277  			})
   278  		}
   279  	}
   280  }
   281  
   282  func TestGenerator_GenerateTCP(t *testing.T) {
   283  	testCases := []struct {
   284  		name       string
   285  		tdBundle   trustdomain.Bundle
   286  		meshConfig *meshconfig.MeshConfig
   287  		input      string
   288  		want       []string
   289  	}{
   290  		{
   291  			name:  "allow-both-http-tcp",
   292  			input: "allow-both-http-tcp-in.yaml",
   293  			want:  []string{"allow-both-http-tcp-out.yaml"},
   294  		},
   295  		{
   296  			name:  "allow-only-http",
   297  			input: "allow-only-http-in.yaml",
   298  			want:  []string{"allow-only-http-out.yaml"},
   299  		},
   300  		{
   301  			name:  "audit-both-http-tcp",
   302  			input: "audit-both-http-tcp-in.yaml",
   303  			want:  []string{"audit-both-http-tcp-out.yaml"},
   304  		},
   305  		{
   306  			name:       "custom-both-http-tcp",
   307  			meshConfig: meshConfigGRPC,
   308  			input:      "custom-both-http-tcp-in.yaml",
   309  			want:       []string{"custom-both-http-tcp-out1.yaml", "custom-both-http-tcp-out2.yaml"},
   310  		},
   311  		{
   312  			name:       "custom-only-http",
   313  			meshConfig: meshConfigHTTP,
   314  			input:      "custom-only-http-in.yaml",
   315  			want:       []string{},
   316  		},
   317  		{
   318  			name:  "deny-both-http-tcp",
   319  			input: "deny-both-http-tcp-in.yaml",
   320  			want:  []string{"deny-both-http-tcp-out.yaml"},
   321  		},
   322  		{
   323  			name:  "dry-run-mix",
   324  			input: "dry-run-mix-in.yaml",
   325  			want:  []string{"dry-run-mix-out.yaml"},
   326  		},
   327  	}
   328  
   329  	baseDir := "tcp/"
   330  	for _, tc := range testCases {
   331  		t.Run(tc.name, func(t *testing.T) {
   332  			option := Option{
   333  				IsCustomBuilder: tc.meshConfig != nil,
   334  			}
   335  			push := push(t, baseDir+tc.input, tc.meshConfig)
   336  			proxy := node(nil)
   337  			selectionOpts := model.PolicyMatcherForProxy(proxy)
   338  			policies := push.AuthzPolicies.ListAuthorizationPolicies(selectionOpts)
   339  			g := New(tc.tdBundle, push, policies, option)
   340  			if g == nil {
   341  				t.Fatalf("failed to create generator")
   342  			}
   343  			got := g.BuildTCP()
   344  			verify(t, convertTCP(got), baseDir, tc.want, true /* forTCP */)
   345  		})
   346  	}
   347  }
   348  
   349  func verify(t *testing.T, gots []proto.Message, baseDir string, wants []string, forTCP bool) {
   350  	t.Helper()
   351  
   352  	if len(gots) != len(wants) {
   353  		t.Fatalf("got %d configs but want %d", len(gots), len(wants))
   354  	}
   355  	for i, got := range gots {
   356  		gotYaml, err := protomarshal.ToYAML(got)
   357  		if err != nil {
   358  			t.Fatalf("failed to convert to YAML: %v", err)
   359  		}
   360  
   361  		wantFile := basePath + baseDir + wants[i]
   362  		util.RefreshGoldenFile(t, []byte(gotYaml), wantFile)
   363  		want := yamlConfig(t, wantFile, forTCP)
   364  		wantYaml, err := protomarshal.ToYAML(want)
   365  		if err != nil {
   366  			t.Fatalf("failed to convert to YAML: %v", err)
   367  		}
   368  
   369  		if err := util.Compare([]byte(gotYaml), []byte(wantYaml)); err != nil {
   370  			t.Error(err)
   371  		}
   372  	}
   373  }
   374  
   375  func yamlPolicy(t *testing.T, filename string) *model.AuthorizationPolicies {
   376  	t.Helper()
   377  	data, err := os.ReadFile(filename)
   378  	if err != nil {
   379  		t.Fatalf("failed to read input yaml file: %v", err)
   380  	}
   381  	c, _, err := crd.ParseInputs(string(data))
   382  	if err != nil {
   383  		t.Fatalf("failde to parse CRD: %v", err)
   384  	}
   385  	var configs []*config.Config
   386  	for i := range c {
   387  		configs = append(configs, &c[i])
   388  	}
   389  
   390  	return newAuthzPolicies(t, configs)
   391  }
   392  
   393  func yamlConfig(t *testing.T, filename string, forTCP bool) proto.Message {
   394  	t.Helper()
   395  	data, err := os.ReadFile(filename)
   396  	if err != nil {
   397  		t.Fatalf("failed to read file: %v", err)
   398  	}
   399  	if forTCP {
   400  		out := &listener.Filter{}
   401  		if err := protomarshal.ApplyYAML(string(data), out); err != nil {
   402  			t.Fatalf("failed to parse YAML: %v", err)
   403  		}
   404  		return out
   405  	}
   406  	out := &hcm.HttpFilter{}
   407  	if err := protomarshal.ApplyYAML(string(data), out); err != nil {
   408  		t.Fatalf("failed to parse YAML: %v", err)
   409  	}
   410  	return out
   411  }
   412  
   413  func convertHTTP(in []*hcm.HttpFilter) []proto.Message {
   414  	ret := make([]proto.Message, len(in))
   415  	for i := range in {
   416  		ret[i] = in[i]
   417  	}
   418  	return ret
   419  }
   420  
   421  func convertTCP(in []*listener.Filter) []proto.Message {
   422  	ret := make([]proto.Message, len(in))
   423  	for i := range in {
   424  		ret[i] = in[i]
   425  	}
   426  	return ret
   427  }
   428  
   429  func newAuthzPolicies(t *testing.T, policies []*config.Config) *model.AuthorizationPolicies {
   430  	store := memory.Make(collections.Pilot)
   431  	for _, p := range policies {
   432  		if _, err := store.Create(*p); err != nil {
   433  			t.Fatalf("newAuthzPolicies: %v", err)
   434  		}
   435  	}
   436  
   437  	authzPolicies := model.GetAuthorizationPolicies(&model.Environment{
   438  		ConfigStore: store,
   439  	})
   440  	return authzPolicies
   441  }
   442  
   443  func push(t *testing.T, input string, mc *meshconfig.MeshConfig) *model.PushContext {
   444  	t.Helper()
   445  	p := &model.PushContext{
   446  		AuthzPolicies: yamlPolicy(t, basePath+input),
   447  		Mesh:          mc,
   448  	}
   449  	p.ServiceIndex.HostnameAndNamespace = map[host.Name]map[string]*model.Service{
   450  		"my-custom-ext-authz.foo.svc.cluster.local": {
   451  			"foo": &model.Service{
   452  				Hostname: "my-custom-ext-authz.foo.svc.cluster.local",
   453  			},
   454  		},
   455  	}
   456  	return p
   457  }
   458  
   459  func node(version *model.IstioVersion) *model.Proxy {
   460  	return &model.Proxy{
   461  		ID:              "test-node",
   462  		ConfigNamespace: "foo",
   463  		Labels:          httpbin,
   464  		Metadata: &model.NodeMetadata{
   465  			Labels:    httpbin,
   466  			Namespace: "foo",
   467  		},
   468  		IstioVersion: version,
   469  	}
   470  }