istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/security/authz/model/generator_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  	"testing"
    19  
    20  	rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
    21  	"github.com/google/go-cmp/cmp"
    22  	"google.golang.org/protobuf/proto"
    23  	"google.golang.org/protobuf/testing/protocmp"
    24  
    25  	"istio.io/istio/pkg/util/protomarshal"
    26  )
    27  
    28  func TestRequestPrincipal(t *testing.T) {
    29  	cases := []struct {
    30  		in   string
    31  		want string
    32  	}{
    33  		{
    34  			in: "*",
    35  			want: `
    36          and_ids:
    37            ids:
    38            - metadata:
    39                filter: envoy.filters.http.jwt_authn
    40                path:
    41                - key: payload
    42                - key: iss
    43                value:
    44                  string_match:
    45                    safe_regex: {regex: .+}
    46            - metadata:
    47                filter: envoy.filters.http.jwt_authn
    48                path:
    49                - key: payload
    50                - key: sub
    51                value:
    52                  string_match:
    53                    safe_regex: {regex: .+}
    54  `,
    55  		},
    56  		{
    57  			in: "foo*",
    58  			want: `
    59          and_ids:
    60            ids:
    61            - metadata:
    62                filter: envoy.filters.http.jwt_authn
    63                path:
    64                - key: payload
    65                - key: iss
    66                value:
    67                  string_match:
    68                    prefix: foo
    69            - metadata:
    70                filter: envoy.filters.http.jwt_authn
    71                path:
    72                - key: payload
    73                - key: sub
    74                value:
    75                  string_match:
    76                    safe_regex: {regex: .+}
    77  `,
    78  		},
    79  		{
    80  			in: "foo/*",
    81  			want: `
    82          and_ids:
    83            ids:
    84            - metadata:
    85                filter: envoy.filters.http.jwt_authn
    86                path:
    87                - key: payload
    88                - key: iss
    89                value:
    90                  string_match:
    91                    exact: foo
    92            - metadata:
    93                filter: envoy.filters.http.jwt_authn
    94                path:
    95                - key: payload
    96                - key: sub
    97                value:
    98                  string_match:
    99                    safe_regex: {regex: .+}
   100  `,
   101  		},
   102  		{
   103  			in: "foo/bar*",
   104  			want: `
   105          and_ids:
   106            ids:
   107            - metadata:
   108                filter: envoy.filters.http.jwt_authn
   109                path:
   110                - key: payload
   111                - key: iss
   112                value:
   113                  string_match:
   114                    exact: foo
   115            - metadata:
   116                filter: envoy.filters.http.jwt_authn
   117                path:
   118                - key: payload
   119                - key: sub
   120                value:
   121                  string_match:
   122                    prefix: bar
   123  `,
   124  		},
   125  		{
   126  			in: "*foo",
   127  			want: `
   128          and_ids:
   129            ids:
   130            - metadata:
   131                filter: envoy.filters.http.jwt_authn
   132                path:
   133                - key: payload
   134                - key: iss
   135                value:
   136                  string_match:
   137                    safe_regex: {regex: .+}
   138            - metadata:
   139                filter: envoy.filters.http.jwt_authn
   140                path:
   141                - key: payload
   142                - key: sub
   143                value:
   144                  string_match:
   145                    suffix: foo
   146  `,
   147  		},
   148  		{
   149  			in: "*/foo",
   150  			want: `
   151          and_ids:
   152            ids:
   153            - metadata:
   154                filter: envoy.filters.http.jwt_authn
   155                path:
   156                - key: payload
   157                - key: iss
   158                value:
   159                  string_match:
   160                    safe_regex: {regex: .+}
   161            - metadata:
   162                filter: envoy.filters.http.jwt_authn
   163                path:
   164                - key: payload
   165                - key: sub
   166                value:
   167                  string_match:
   168                    exact: foo
   169  `,
   170  		},
   171  		{
   172  			in: "*bar/foo",
   173  			want: `
   174          and_ids:
   175            ids:
   176            - metadata:
   177                filter: envoy.filters.http.jwt_authn
   178                path:
   179                - key: payload
   180                - key: iss
   181                value:
   182                  string_match:
   183                    suffix: bar
   184            - metadata:
   185                filter: envoy.filters.http.jwt_authn
   186                path:
   187                - key: payload
   188                - key: sub
   189                value:
   190                  string_match:
   191                    exact: foo
   192  `,
   193  		},
   194  		{
   195  			in: "foo/bar",
   196  			want: `
   197          and_ids:
   198            ids:
   199            - metadata:
   200                filter: envoy.filters.http.jwt_authn
   201                path:
   202                - key: payload
   203                - key: iss
   204                value:
   205                  string_match:
   206                    exact: foo
   207            - metadata:
   208                filter: envoy.filters.http.jwt_authn
   209                path:
   210                - key: payload
   211                - key: sub
   212                value:
   213                  string_match:
   214                    exact: bar
   215  `,
   216  		},
   217  	}
   218  	rpg := requestPrincipalGenerator{}
   219  	for _, tc := range cases {
   220  		t.Run(tc.in, func(t *testing.T) {
   221  			got, err := rpg.extendedPrincipal("", []string{tc.in}, false)
   222  			if err != nil {
   223  				t.Fatal(err)
   224  			}
   225  			principal := yamlPrincipal(t, tc.want)
   226  			if diff := cmp.Diff(got, principal, protocmp.Transform()); diff != "" {
   227  				t.Errorf("diff detected: %v", diff)
   228  			}
   229  		})
   230  	}
   231  }
   232  
   233  func TestGenerator(t *testing.T) {
   234  	cases := []struct {
   235  		name   string
   236  		g      generator
   237  		key    string
   238  		value  string
   239  		forTCP bool
   240  		want   any
   241  	}{
   242  		{
   243  			name:  "destIPGenerator",
   244  			g:     destIPGenerator{},
   245  			value: "1.2.3.4",
   246  			want: yamlPermission(t, `
   247           destinationIp:
   248            addressPrefix: 1.2.3.4
   249            prefixLen: 32`),
   250  		},
   251  		{
   252  			name:  "destPortGenerator",
   253  			g:     destPortGenerator{},
   254  			value: "80",
   255  			want: yamlPermission(t, `
   256           destinationPort: 80`),
   257  		},
   258  		{
   259  			name:  "connSNIGenerator",
   260  			g:     connSNIGenerator{},
   261  			value: "exact.com",
   262  			want: yamlPermission(t, `
   263           requestedServerName:
   264            exact: exact.com`),
   265  		},
   266  		{
   267  			name:  "envoyFilterGenerator-string",
   268  			g:     envoyFilterGenerator{},
   269  			key:   "experimental.a.b.c[d]",
   270  			value: "val",
   271  			want: yamlPermission(t, `
   272           metadata:
   273            filter: a.b.c
   274            path:
   275            - key: d
   276            value:
   277              stringMatch:
   278                exact: val`),
   279  		},
   280  		{
   281  			name:  "envoyFilterGenerator-invalid",
   282  			g:     envoyFilterGenerator{},
   283  			key:   "experimental.a.b.c]",
   284  			value: "val",
   285  		},
   286  		{
   287  			name:  "envoyFilterGenerator-list",
   288  			g:     envoyFilterGenerator{},
   289  			key:   "experimental.a.b.c[d]",
   290  			value: "[v1, v2]",
   291  			want: yamlPermission(t, `
   292           metadata:
   293            filter: a.b.c
   294            path:
   295            - key: d
   296            value:
   297              listMatch:
   298                oneOf:
   299                  stringMatch:
   300                    exact: v1, v2`),
   301  		},
   302  		{
   303  			name:  "srcIPGenerator",
   304  			g:     srcIPGenerator{},
   305  			value: "1.2.3.4",
   306  			want: yamlPrincipal(t, `
   307           directRemoteIp:
   308            addressPrefix: 1.2.3.4
   309            prefixLen: 32`),
   310  		},
   311  		{
   312  			name:  "remoteIPGenerator",
   313  			g:     remoteIPGenerator{},
   314  			value: "1.2.3.4",
   315  			want: yamlPrincipal(t, `
   316           remoteIp:
   317            addressPrefix: 1.2.3.4
   318            prefixLen: 32`),
   319  		},
   320  		{
   321  			name:  "srcNamespaceGenerator-http",
   322  			g:     srcNamespaceGenerator{},
   323  			value: "foo",
   324  			want: yamlPrincipal(t, `
   325           filter_state:
   326             key: io.istio.peer_principal
   327             string_match:
   328              safeRegex:
   329                regex: .*/ns/foo/.*`),
   330  		},
   331  		{
   332  			name:   "srcNamespaceGenerator-tcp",
   333  			g:      srcNamespaceGenerator{},
   334  			value:  "foo",
   335  			forTCP: true,
   336  			want: yamlPrincipal(t, `
   337           filter_state:
   338             key: io.istio.peer_principal
   339             string_match:
   340              safeRegex:
   341                regex: .*/ns/foo/.*`),
   342  		},
   343  		{
   344  			name:  "srcPrincipalGenerator-http",
   345  			g:     srcPrincipalGenerator{},
   346  			key:   "source.principal",
   347  			value: "foo",
   348  			want: yamlPrincipal(t, `
   349           filter_state:
   350             key: io.istio.peer_principal
   351             string_match:
   352              exact: spiffe://foo`),
   353  		},
   354  		{
   355  			name:   "srcPrincipalGenerator-tcp",
   356  			g:      srcPrincipalGenerator{},
   357  			key:    "source.principal",
   358  			value:  "foo",
   359  			forTCP: true,
   360  			want: yamlPrincipal(t, `
   361           filter_state:
   362             key: io.istio.peer_principal
   363             string_match:
   364              exact: spiffe://foo`),
   365  		},
   366  		{
   367  			name:  "requestPrincipalGenerator",
   368  			g:     requestPrincipalGenerator{},
   369  			key:   "request.auth.principal",
   370  			value: "foo",
   371  			want: yamlPrincipal(t, `
   372           metadata:
   373            filter: istio_authn
   374            path:
   375            - key: request.auth.principal
   376            value:
   377              stringMatch:
   378                exact: foo`),
   379  		},
   380  		{
   381  			name:  "requestAudiencesGenerator",
   382  			g:     requestAudiencesGenerator{},
   383  			key:   "request.auth.audiences",
   384  			value: "foo",
   385  			want: yamlPrincipal(t, `
   386           metadata:
   387            filter: istio_authn
   388            path:
   389            - key: request.auth.audiences
   390            value:
   391              stringMatch:
   392                exact: foo`),
   393  		},
   394  		{
   395  			name:  "requestPresenterGenerator",
   396  			g:     requestPresenterGenerator{},
   397  			key:   "request.auth.presenter",
   398  			value: "foo",
   399  			want: yamlPrincipal(t, `
   400           metadata:
   401            filter: istio_authn
   402            path:
   403            - key: request.auth.presenter
   404            value:
   405              stringMatch:
   406                exact: foo`),
   407  		},
   408  		{
   409  			name:  "requestHeaderGenerator",
   410  			g:     requestHeaderGenerator{},
   411  			key:   "request.headers[x-foo]",
   412  			value: "foo",
   413  			want: yamlPrincipal(t, `
   414          header:
   415            name: x-foo
   416            stringMatch:
   417              exact: foo`),
   418  		},
   419  		{
   420  			name:  "requestClaimGenerator",
   421  			g:     requestClaimGenerator{},
   422  			key:   "request.auth.claims[bar]",
   423  			value: "foo",
   424  			want: yamlPrincipal(t, `
   425           metadata:
   426            filter: istio_authn
   427            path:
   428            - key: request.auth.claims
   429            - key: bar
   430            value:
   431              listMatch:
   432                oneOf:
   433                  stringMatch:
   434                    exact: foo`),
   435  		},
   436  		{
   437  			name:  "requestNestedClaimsGenerator",
   438  			g:     requestClaimGenerator{},
   439  			key:   "request.auth.claims[bar][baz]",
   440  			value: "foo",
   441  			want: yamlPrincipal(t, `
   442           metadata:
   443            filter: istio_authn
   444            path:
   445            - key: request.auth.claims
   446            - key: bar
   447            - key: baz
   448            value:
   449              listMatch:
   450                oneOf:
   451                  stringMatch:
   452                    exact: foo`),
   453  		},
   454  		{
   455  			name:  "hostGenerator",
   456  			g:     hostGenerator{},
   457  			value: "foo",
   458  			want: yamlPermission(t, `
   459           header:
   460            stringMatch:
   461              exact: foo
   462              ignoreCase: true
   463            name: :authority`),
   464  		},
   465  		{
   466  			name:  "pathGenerator",
   467  			g:     pathGenerator{},
   468  			value: "/abc",
   469  			want: yamlPermission(t, `
   470           urlPath:
   471            path:
   472              exact: /abc`),
   473  		},
   474  		{
   475  			name:  "pathGenerator-template",
   476  			g:     pathGenerator{},
   477  			value: "/abc/{*}",
   478  			want: yamlPermission(t, `
   479           uriTemplate:
   480             name: uri-template
   481             typedConfig:
   482              '@type': type.googleapis.com/envoy.extensions.path.match.uri_template.v3.UriTemplateMatchConfig
   483              pathTemplate: /abc/*`),
   484  		},
   485  		{
   486  			name:  "methodGenerator",
   487  			g:     methodGenerator{},
   488  			value: "GET",
   489  			want: yamlPermission(t, `
   490           header:
   491            name: :method
   492            stringMatch:
   493              exact: GET`),
   494  		},
   495  	}
   496  
   497  	for _, tc := range cases {
   498  		t.Run(tc.name, func(t *testing.T) {
   499  			var got any
   500  			var err error
   501  			// nolint: gocritic
   502  			if _, ok := tc.want.(*rbacpb.Permission); ok {
   503  				got, err = tc.g.permission(tc.key, tc.value, tc.forTCP)
   504  				if err != nil {
   505  					t.Errorf("both permission and principal returned error")
   506  				}
   507  			} else if _, ok := tc.want.(*rbacpb.Principal); ok {
   508  				got, err = tc.g.principal(tc.key, tc.value, tc.forTCP, false)
   509  				if err != nil {
   510  					t.Errorf("both permission and principal returned error")
   511  				}
   512  			} else {
   513  				_, err1 := tc.g.principal(tc.key, tc.value, tc.forTCP, false)
   514  				_, err2 := tc.g.permission(tc.key, tc.value, tc.forTCP)
   515  				if err1 == nil || err2 == nil {
   516  					t.Fatalf("wanted error")
   517  				}
   518  				return
   519  			}
   520  			if diff := cmp.Diff(got, tc.want, protocmp.Transform()); diff != "" {
   521  				var gotYaml string
   522  				gotProto, ok := got.(proto.Message)
   523  				if !ok {
   524  					t.Fatal("failed to extract proto")
   525  				}
   526  				if gotYaml, err = protomarshal.ToYAML(gotProto); err != nil {
   527  					t.Fatalf("%s: failed to parse yaml: %s", tc.name, err)
   528  				}
   529  				t.Errorf("got:\n %v\n but want:\n %v", gotYaml, tc.want)
   530  			}
   531  		})
   532  	}
   533  }
   534  
   535  func yamlPermission(t *testing.T, yaml string) *rbacpb.Permission {
   536  	t.Helper()
   537  	p := &rbacpb.Permission{}
   538  	if err := protomarshal.ApplyYAML(yaml, p); err != nil {
   539  		t.Fatalf("failed to parse yaml: %s", err)
   540  	}
   541  	return p
   542  }
   543  
   544  func yamlPrincipal(t *testing.T, yaml string) *rbacpb.Principal {
   545  	t.Helper()
   546  	p := &rbacpb.Principal{}
   547  	if err := protomarshal.ApplyYAML(yaml, p); err != nil {
   548  		t.Fatalf("failed to parse yaml: %s", err)
   549  	}
   550  	return p
   551  }