istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/security/authz/model/model_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 model
    16  
    17  import (
    18  	"reflect"
    19  	"strings"
    20  	"testing"
    21  
    22  	"github.com/davecgh/go-spew/spew"
    23  	rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
    24  	matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
    25  
    26  	authzpb "istio.io/api/security/v1beta1"
    27  	"istio.io/istio/pilot/pkg/security/trustdomain"
    28  	"istio.io/istio/pkg/util/protomarshal"
    29  )
    30  
    31  func TestModel_MigrateTrustDomain(t *testing.T) {
    32  	cases := []struct {
    33  		name     string
    34  		tdBundle trustdomain.Bundle
    35  		rule     *authzpb.Rule
    36  		want     []string
    37  		notWant  []string
    38  	}{
    39  		{
    40  			name:     "no-source-principal",
    41  			tdBundle: trustdomain.NewBundle("td-1", []string{"td-2"}),
    42  			rule: yamlRule(t, `
    43  from:
    44  - source:
    45      requestPrincipals: ["td-1/ns/foo/sa/sleep"]
    46  `),
    47  			want: []string{
    48  				"td-1/ns/foo/sa/sleep",
    49  			},
    50  			notWant: []string{
    51  				"td-2/ns/foo/sa/sleep",
    52  			},
    53  		},
    54  		{
    55  			name:     "source-principal-field",
    56  			tdBundle: trustdomain.NewBundle("td-1", []string{"td-2"}),
    57  			rule: yamlRule(t, `
    58  from:
    59  - source:
    60      principals: ["td-1/ns/foo/sa/sleep"]
    61  `),
    62  			want: []string{
    63  				"td-1/ns/foo/sa/sleep",
    64  				"td-2/ns/foo/sa/sleep",
    65  			},
    66  		},
    67  		{
    68  			name:     "source-principal-attribute",
    69  			tdBundle: trustdomain.NewBundle("td-1", []string{"td-2"}),
    70  			rule: yamlRule(t, `
    71  when:
    72  - key: source.principal
    73    values: ["td-1/ns/foo/sa/sleep"]
    74  `),
    75  			want: []string{
    76  				"td-1/ns/foo/sa/sleep",
    77  				"td-2/ns/foo/sa/sleep",
    78  			},
    79  		},
    80  	}
    81  
    82  	for _, tc := range cases {
    83  		t.Run(tc.name, func(t *testing.T) {
    84  			got, err := New(tc.rule, false)
    85  			if err != nil {
    86  				t.Fatal(err)
    87  			}
    88  			got.MigrateTrustDomain(tc.tdBundle)
    89  			gotStr := spew.Sdump(got)
    90  			for _, want := range tc.want {
    91  				if !strings.Contains(gotStr, want) {
    92  					t.Errorf("got %s but not found %s", gotStr, want)
    93  				}
    94  			}
    95  			for _, notWant := range tc.notWant {
    96  				if strings.Contains(gotStr, notWant) {
    97  					t.Errorf("got %s but not want %s", gotStr, notWant)
    98  				}
    99  			}
   100  		})
   101  	}
   102  }
   103  
   104  func TestModel_Generate(t *testing.T) {
   105  	rule := yamlRule(t, `
   106  from:
   107  - source:
   108      requestPrincipals: ["td-1/ns/foo/sa/sleep-1"]
   109      notRequestPrincipals: ["td-1/ns/foo/sa/sleep-2"]
   110  - source:
   111      requestPrincipals: ["td-1/ns/foo/sa/sleep-3"]
   112      notRequestPrincipals: ["td-1/ns/foo/sa/sleep-4"]
   113  to:
   114  - operation:
   115      ports: ["8001"]
   116      notPorts: ["8002"]
   117  - operation:
   118      ports: ["8003"]
   119      notPorts: ["8004"]
   120  when:
   121  - key: "source.principal"
   122    values: ["td-1/ns/foo/sa/httpbin-1"]
   123    notValues: ["td-1/ns/foo/sa/httpbin-2"]
   124  - key: "destination.ip"
   125    values: ["10.0.0.1"]
   126    notValues: ["10.0.0.2"]
   127  `)
   128  
   129  	cases := []struct {
   130  		name    string
   131  		forTCP  bool
   132  		action  rbacpb.RBAC_Action
   133  		rule    *authzpb.Rule
   134  		want    []string
   135  		notWant []string
   136  	}{
   137  		{
   138  			name:   "allow-http",
   139  			action: rbacpb.RBAC_ALLOW,
   140  			rule:   rule,
   141  			want: []string{
   142  				"td-1/ns/foo/sa/sleep-1",
   143  				"td-1/ns/foo/sa/sleep-2",
   144  				"td-1/ns/foo/sa/sleep-3",
   145  				"td-1/ns/foo/sa/sleep-4",
   146  				"td-1/ns/foo/sa/httpbin-1",
   147  				"td-1/ns/foo/sa/httpbin-2",
   148  				"8001",
   149  				"8002",
   150  				"8003",
   151  				"8004",
   152  				"10.0.0.1",
   153  				"10.0.0.2",
   154  			},
   155  		},
   156  		{
   157  			name:   "allow-tcp",
   158  			action: rbacpb.RBAC_ALLOW,
   159  			forTCP: true,
   160  			rule:   rule,
   161  			notWant: []string{
   162  				"td-1/ns/foo/sa/sleep-1",
   163  				"td-1/ns/foo/sa/sleep-2",
   164  				"td-1/ns/foo/sa/sleep-3",
   165  				"td-1/ns/foo/sa/sleep-4",
   166  				"td-1/ns/foo/sa/httpbin-1",
   167  				"td-1/ns/foo/sa/httpbin-2",
   168  				"8001",
   169  				"8002",
   170  				"8003",
   171  				"8004",
   172  				"10.0.0.1",
   173  				"10.0.0.2",
   174  			},
   175  		},
   176  		{
   177  			name:   "deny-http",
   178  			action: rbacpb.RBAC_DENY,
   179  			rule:   rule,
   180  			want: []string{
   181  				"td-1/ns/foo/sa/sleep-1",
   182  				"td-1/ns/foo/sa/sleep-2",
   183  				"td-1/ns/foo/sa/sleep-3",
   184  				"td-1/ns/foo/sa/sleep-4",
   185  				"td-1/ns/foo/sa/httpbin-1",
   186  				"td-1/ns/foo/sa/httpbin-2",
   187  				"8001",
   188  				"8002",
   189  				"8003",
   190  				"8004",
   191  				"10.0.0.1",
   192  				"10.0.0.2",
   193  			},
   194  		},
   195  		{
   196  			name:   "deny-tcp",
   197  			action: rbacpb.RBAC_DENY,
   198  			forTCP: true,
   199  			rule:   rule,
   200  			want: []string{
   201  				"8001",
   202  				"8002",
   203  				"8003",
   204  				"8004",
   205  				"10.0.0.1",
   206  				"10.0.0.2",
   207  				"td-1/ns/foo/sa/httpbin-1",
   208  				"td-1/ns/foo/sa/httpbin-2",
   209  			},
   210  			notWant: []string{
   211  				"td-1/ns/foo/sa/sleep-1",
   212  				"td-1/ns/foo/sa/sleep-2",
   213  				"td-1/ns/foo/sa/sleep-3",
   214  				"td-1/ns/foo/sa/sleep-4",
   215  			},
   216  		},
   217  		{
   218  			name:   "audit-http",
   219  			action: rbacpb.RBAC_LOG,
   220  			rule:   rule,
   221  			want: []string{
   222  				"td-1/ns/foo/sa/sleep-1",
   223  				"td-1/ns/foo/sa/sleep-2",
   224  				"td-1/ns/foo/sa/sleep-3",
   225  				"td-1/ns/foo/sa/sleep-4",
   226  				"td-1/ns/foo/sa/httpbin-1",
   227  				"td-1/ns/foo/sa/httpbin-2",
   228  				"8001",
   229  				"8002",
   230  				"8003",
   231  				"8004",
   232  				"10.0.0.1",
   233  				"10.0.0.2",
   234  			},
   235  		},
   236  		{
   237  			name:   "audit-tcp",
   238  			action: rbacpb.RBAC_LOG,
   239  			forTCP: true,
   240  			rule:   rule,
   241  			want: []string{
   242  				"8001",
   243  				"8002",
   244  				"8003",
   245  				"8004",
   246  				"10.0.0.1",
   247  				"10.0.0.2",
   248  				"td-1/ns/foo/sa/httpbin-1",
   249  				"td-1/ns/foo/sa/httpbin-2",
   250  			},
   251  			notWant: []string{
   252  				"td-1/ns/foo/sa/sleep-1",
   253  				"td-1/ns/foo/sa/sleep-2",
   254  				"td-1/ns/foo/sa/sleep-3",
   255  				"td-1/ns/foo/sa/sleep-4",
   256  			},
   257  		},
   258  	}
   259  
   260  	for _, tc := range cases {
   261  		t.Run(tc.name, func(t *testing.T) {
   262  			m, err := New(tc.rule, false)
   263  			if err != nil {
   264  				t.Fatal(err)
   265  			}
   266  			p, _ := m.Generate(tc.forTCP, false, tc.action)
   267  			var gotYaml string
   268  			if p != nil {
   269  				if gotYaml, err = protomarshal.ToYAML(p); err != nil {
   270  					t.Fatalf("%s: failed to parse yaml: %s", tc.name, err)
   271  				}
   272  			}
   273  
   274  			for _, want := range tc.want {
   275  				if !strings.Contains(gotYaml, want) {
   276  					t.Errorf("got:\n%s but not found %s", gotYaml, want)
   277  				}
   278  			}
   279  			for _, notWant := range tc.notWant {
   280  				if strings.Contains(gotYaml, notWant) {
   281  					t.Errorf("got:\n%s but not want %s", gotYaml, notWant)
   282  				}
   283  			}
   284  		})
   285  	}
   286  }
   287  
   288  func TestRule_Principal(t *testing.T) {
   289  	tests := []struct {
   290  		name             string
   291  		r                rule
   292  		forTCP           bool
   293  		useAuthenticated bool
   294  		action           rbacpb.RBAC_Action
   295  		want             []*rbacpb.Principal
   296  	}{
   297  		{
   298  			name: "useAuthenticated:true",
   299  			r: rule{
   300  				key:       "foo",
   301  				values:    []string{"value"},
   302  				notValues: []string{"notValue"},
   303  				g:         srcPrincipalGenerator{},
   304  			},
   305  			forTCP:           true,
   306  			useAuthenticated: true,
   307  			action:           rbacpb.RBAC_ALLOW,
   308  			want: []*rbacpb.Principal{
   309  				{
   310  					Identifier: &rbacpb.Principal_OrIds{
   311  						OrIds: &rbacpb.Principal_Set{
   312  							Ids: []*rbacpb.Principal{
   313  								{
   314  									Identifier: &rbacpb.Principal_Authenticated_{
   315  										Authenticated: &rbacpb.Principal_Authenticated{
   316  											PrincipalName: &matcherv3.StringMatcher{
   317  												MatchPattern: &matcherv3.StringMatcher_Exact{
   318  													Exact: "spiffe://value",
   319  												},
   320  											},
   321  										},
   322  									},
   323  								},
   324  							},
   325  						},
   326  					},
   327  				},
   328  				{
   329  					Identifier: &rbacpb.Principal_NotId{
   330  						NotId: &rbacpb.Principal{
   331  							Identifier: &rbacpb.Principal_OrIds{
   332  								OrIds: &rbacpb.Principal_Set{
   333  									Ids: []*rbacpb.Principal{
   334  										{
   335  											Identifier: &rbacpb.Principal_Authenticated_{
   336  												Authenticated: &rbacpb.Principal_Authenticated{
   337  													PrincipalName: &matcherv3.StringMatcher{
   338  														MatchPattern: &matcherv3.StringMatcher_Exact{
   339  															Exact: "spiffe://notValue",
   340  														},
   341  													},
   342  												},
   343  											},
   344  										},
   345  									},
   346  								},
   347  							},
   348  						},
   349  					},
   350  				},
   351  			},
   352  		},
   353  		{
   354  			name: "useAuthenticated:false",
   355  			r: rule{
   356  				key:       "foo",
   357  				values:    []string{"value"},
   358  				notValues: []string{"notValue"},
   359  				g:         srcNamespaceGenerator{},
   360  			},
   361  			forTCP:           false,
   362  			useAuthenticated: false,
   363  			action:           rbacpb.RBAC_ALLOW,
   364  			want: []*rbacpb.Principal{
   365  				{
   366  					Identifier: &rbacpb.Principal_OrIds{
   367  						OrIds: &rbacpb.Principal_Set{
   368  							Ids: []*rbacpb.Principal{
   369  								{
   370  									Identifier: &rbacpb.Principal_FilterState{
   371  										FilterState: &matcherv3.FilterStateMatcher{
   372  											Key: "io.istio.peer_principal",
   373  											Matcher: &matcherv3.FilterStateMatcher_StringMatch{
   374  												StringMatch: &matcherv3.StringMatcher{
   375  													MatchPattern: &matcherv3.StringMatcher_SafeRegex{
   376  														SafeRegex: &matcherv3.RegexMatcher{
   377  															Regex: ".*/ns/value/.*",
   378  														},
   379  													},
   380  												},
   381  											},
   382  										},
   383  									},
   384  								},
   385  							},
   386  						},
   387  					},
   388  				},
   389  				{
   390  					Identifier: &rbacpb.Principal_NotId{
   391  						NotId: &rbacpb.Principal{
   392  							Identifier: &rbacpb.Principal_OrIds{
   393  								OrIds: &rbacpb.Principal_Set{
   394  									Ids: []*rbacpb.Principal{
   395  										{
   396  											Identifier: &rbacpb.Principal_FilterState{
   397  												FilterState: &matcherv3.FilterStateMatcher{
   398  													Key: "io.istio.peer_principal",
   399  													Matcher: &matcherv3.FilterStateMatcher_StringMatch{
   400  														StringMatch: &matcherv3.StringMatcher{
   401  															MatchPattern: &matcherv3.StringMatcher_SafeRegex{
   402  																SafeRegex: &matcherv3.RegexMatcher{
   403  																	Regex: ".*/ns/notValue/.*",
   404  																},
   405  															},
   406  														},
   407  													},
   408  												},
   409  											},
   410  										},
   411  									},
   412  								},
   413  							},
   414  						},
   415  					},
   416  				},
   417  			},
   418  		},
   419  	}
   420  	for _, tt := range tests {
   421  		t.Run(tt.name, func(t *testing.T) {
   422  			got, _ := tt.r.principal(tt.forTCP, tt.useAuthenticated, tt.action)
   423  			if !reflect.DeepEqual(got, tt.want) {
   424  				t.Errorf("rule.principal got %v, want %v", got, tt.want)
   425  			}
   426  		})
   427  	}
   428  }
   429  
   430  func yamlRule(t *testing.T, yaml string) *authzpb.Rule {
   431  	t.Helper()
   432  	p := &authzpb.Rule{}
   433  	if err := protomarshal.ApplyYAML(yaml, p); err != nil {
   434  		t.Fatalf("failed to parse yaml: %s", err)
   435  	}
   436  	return p
   437  }