istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/authorization_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  	"fmt"
    19  	"reflect"
    20  	"testing"
    21  
    22  	"google.golang.org/protobuf/proto"
    23  
    24  	meshconfig "istio.io/api/mesh/v1alpha1"
    25  	authpb "istio.io/api/security/v1beta1"
    26  	selectorpb "istio.io/api/type/v1beta1"
    27  	"istio.io/istio/pkg/config"
    28  	"istio.io/istio/pkg/config/constants"
    29  	"istio.io/istio/pkg/config/labels"
    30  	"istio.io/istio/pkg/config/mesh"
    31  	"istio.io/istio/pkg/config/schema/collection"
    32  	"istio.io/istio/pkg/config/schema/gvk"
    33  )
    34  
    35  func TestAuthorizationPolicies_ListAuthorizationPolicies(t *testing.T) {
    36  	policy := &authpb.AuthorizationPolicy{
    37  		Rules: []*authpb.Rule{
    38  			{
    39  				From: []*authpb.Rule_From{
    40  					{
    41  						Source: &authpb.Source{
    42  							Principals: []string{"sleep"},
    43  						},
    44  					},
    45  				},
    46  				To: []*authpb.Rule_To{
    47  					{
    48  						Operation: &authpb.Operation{
    49  							Methods: []string{"GET"},
    50  						},
    51  					},
    52  				},
    53  			},
    54  		},
    55  	}
    56  	policyWithSelector := proto.Clone(policy).(*authpb.AuthorizationPolicy)
    57  	policyWithSelector.Selector = &selectorpb.WorkloadSelector{
    58  		MatchLabels: labels.Instance{
    59  			"app":     "httpbin",
    60  			"version": "v1",
    61  		},
    62  	}
    63  	policyWithTargetRef := proto.Clone(policy).(*authpb.AuthorizationPolicy)
    64  	policyWithTargetRef.TargetRef = &selectorpb.PolicyTargetReference{
    65  		Group:     gvk.KubernetesGateway.Group,
    66  		Kind:      gvk.KubernetesGateway.Kind,
    67  		Name:      "my-gateway",
    68  		Namespace: "bar",
    69  	}
    70  
    71  	policyWithServiceRef := proto.Clone(policy).(*authpb.AuthorizationPolicy)
    72  	policyWithServiceRef.TargetRef = &selectorpb.PolicyTargetReference{
    73  		Group:     gvk.Service.Group,
    74  		Kind:      gvk.Service.Kind,
    75  		Name:      "foo-svc",
    76  		Namespace: "foo",
    77  	}
    78  
    79  	denyPolicy := proto.Clone(policy).(*authpb.AuthorizationPolicy)
    80  	denyPolicy.Action = authpb.AuthorizationPolicy_DENY
    81  
    82  	auditPolicy := proto.Clone(policy).(*authpb.AuthorizationPolicy)
    83  	auditPolicy.Action = authpb.AuthorizationPolicy_AUDIT
    84  
    85  	customPolicy := proto.Clone(policy).(*authpb.AuthorizationPolicy)
    86  	customPolicy.Action = authpb.AuthorizationPolicy_CUSTOM
    87  
    88  	cases := []struct {
    89  		name          string
    90  		selectionOpts WorkloadPolicyMatcher
    91  		configs       []config.Config
    92  		wantDeny      []AuthorizationPolicy
    93  		wantAllow     []AuthorizationPolicy
    94  		wantAudit     []AuthorizationPolicy
    95  		wantCustom    []AuthorizationPolicy
    96  	}{
    97  		{
    98  			name: "no policies",
    99  			selectionOpts: WorkloadPolicyMatcher{
   100  				Namespace: "foo",
   101  			},
   102  			wantAllow: nil,
   103  		},
   104  		{
   105  			name: "no policies in namespace foo",
   106  			selectionOpts: WorkloadPolicyMatcher{
   107  				Namespace: "foo",
   108  			},
   109  			configs: []config.Config{
   110  				newConfig("authz-1", "bar", policy),
   111  				newConfig("authz-2", "bar", policy),
   112  			},
   113  			wantAllow: nil,
   114  		},
   115  		{
   116  			name: "no policies with a targetRef in namespace foo",
   117  			selectionOpts: WorkloadPolicyMatcher{
   118  				Namespace: "foo",
   119  				WorkloadLabels: labels.Instance{
   120  					constants.GatewayNameLabel: "my-gateway",
   121  				},
   122  			},
   123  			configs: []config.Config{
   124  				newConfig("authz-1", "bar", policyWithTargetRef),
   125  			},
   126  			wantAllow: nil,
   127  		},
   128  		{
   129  			name: "one allow policy",
   130  			selectionOpts: WorkloadPolicyMatcher{
   131  				Namespace: "bar",
   132  			},
   133  			configs: []config.Config{
   134  				newConfig("authz-1", "bar", policy),
   135  			},
   136  			wantAllow: []AuthorizationPolicy{
   137  				{
   138  					Name:      "authz-1",
   139  					Namespace: "bar",
   140  					Spec:      policy,
   141  				},
   142  			},
   143  		},
   144  		{
   145  			name: "one deny policy",
   146  			selectionOpts: WorkloadPolicyMatcher{
   147  				Namespace: "bar",
   148  			},
   149  			configs: []config.Config{
   150  				newConfig("authz-1", "bar", denyPolicy),
   151  			},
   152  			wantDeny: []AuthorizationPolicy{
   153  				{
   154  					Name:      "authz-1",
   155  					Namespace: "bar",
   156  					Spec:      denyPolicy,
   157  				},
   158  			},
   159  		},
   160  		{
   161  			name: "one audit policy",
   162  			selectionOpts: WorkloadPolicyMatcher{
   163  				Namespace: "bar",
   164  			},
   165  			configs: []config.Config{
   166  				newConfig("authz-1", "bar", auditPolicy),
   167  			},
   168  			wantAudit: []AuthorizationPolicy{
   169  				{
   170  					Name:      "authz-1",
   171  					Namespace: "bar",
   172  					Spec:      auditPolicy,
   173  				},
   174  			},
   175  		},
   176  		{
   177  			name: "one custom policy",
   178  			selectionOpts: WorkloadPolicyMatcher{
   179  				Namespace: "bar",
   180  			},
   181  			configs: []config.Config{
   182  				newConfig("authz-1", "bar", customPolicy),
   183  			},
   184  			wantCustom: []AuthorizationPolicy{
   185  				{
   186  					Name:      "authz-1",
   187  					Namespace: "bar",
   188  					Spec:      customPolicy,
   189  				},
   190  			},
   191  		},
   192  		{
   193  			name: "two policies",
   194  			selectionOpts: WorkloadPolicyMatcher{
   195  				Namespace: "bar",
   196  			},
   197  			configs: []config.Config{
   198  				newConfig("authz-1", "foo", policy),
   199  				newConfig("authz-1", "bar", policy),
   200  				newConfig("authz-2", "bar", policy),
   201  			},
   202  			wantAllow: []AuthorizationPolicy{
   203  				{
   204  					Name:      "authz-1",
   205  					Namespace: "bar",
   206  					Spec:      policy,
   207  				},
   208  				{
   209  					Name:      "authz-2",
   210  					Namespace: "bar",
   211  					Spec:      policy,
   212  				},
   213  			},
   214  		},
   215  		{
   216  			name: "mixing allow, deny, and audit policies",
   217  			selectionOpts: WorkloadPolicyMatcher{
   218  				Namespace: "bar",
   219  			},
   220  			configs: []config.Config{
   221  				newConfig("authz-1", "bar", policy),
   222  				newConfig("authz-2", "bar", denyPolicy),
   223  				newConfig("authz-3", "bar", auditPolicy),
   224  				newConfig("authz-4", "bar", auditPolicy),
   225  			},
   226  			wantDeny: []AuthorizationPolicy{
   227  				{
   228  					Name:      "authz-2",
   229  					Namespace: "bar",
   230  					Spec:      denyPolicy,
   231  				},
   232  			},
   233  			wantAllow: []AuthorizationPolicy{
   234  				{
   235  					Name:      "authz-1",
   236  					Namespace: "bar",
   237  					Spec:      policy,
   238  				},
   239  			},
   240  			wantAudit: []AuthorizationPolicy{
   241  				{
   242  					Name:      "authz-3",
   243  					Namespace: "bar",
   244  					Spec:      auditPolicy,
   245  				},
   246  				{
   247  					Name:      "authz-4",
   248  					Namespace: "bar",
   249  					Spec:      auditPolicy,
   250  				},
   251  			},
   252  		},
   253  		{
   254  			name: "targetRef is an exact match",
   255  			selectionOpts: WorkloadPolicyMatcher{
   256  				Namespace: "bar",
   257  				WorkloadLabels: labels.Instance{
   258  					constants.GatewayNameLabel: "my-gateway",
   259  				},
   260  			},
   261  			configs: []config.Config{
   262  				newConfig("authz-1", "bar", policyWithTargetRef),
   263  			},
   264  			wantAllow: []AuthorizationPolicy{
   265  				{
   266  					Name:      "authz-1",
   267  					Namespace: "bar",
   268  					Spec:      policyWithTargetRef,
   269  				},
   270  			},
   271  		},
   272  		{
   273  			name: "selector exact match",
   274  			selectionOpts: WorkloadPolicyMatcher{
   275  				Namespace: "bar",
   276  				WorkloadLabels: labels.Instance{
   277  					"app":     "httpbin",
   278  					"version": "v1",
   279  				},
   280  			},
   281  			configs: []config.Config{
   282  				newConfig("authz-1", "bar", policyWithSelector),
   283  			},
   284  			wantAllow: []AuthorizationPolicy{
   285  				{
   286  					Name:      "authz-1",
   287  					Namespace: "bar",
   288  					Spec:      policyWithSelector,
   289  				},
   290  			},
   291  		},
   292  		{
   293  			name: "selector subset match",
   294  			selectionOpts: WorkloadPolicyMatcher{
   295  				Namespace: "bar",
   296  				WorkloadLabels: labels.Instance{
   297  					"app":     "httpbin",
   298  					"version": "v1",
   299  					"env":     "dev",
   300  				},
   301  			},
   302  			configs: []config.Config{
   303  				newConfig("authz-1", "bar", policyWithSelector),
   304  			},
   305  			wantAllow: []AuthorizationPolicy{
   306  				{
   307  					Name:      "authz-1",
   308  					Namespace: "bar",
   309  					Spec:      policyWithSelector,
   310  				},
   311  			},
   312  		},
   313  		{
   314  			name: "targetRef is not a match",
   315  			selectionOpts: WorkloadPolicyMatcher{
   316  				Namespace: "bar",
   317  				WorkloadLabels: labels.Instance{
   318  					constants.GatewayNameLabel: "my-gateway2",
   319  				},
   320  			},
   321  			configs: []config.Config{
   322  				newConfig("authz-1", "bar", policyWithTargetRef),
   323  			},
   324  			wantAllow: nil,
   325  		},
   326  		{
   327  			name: "selector not match",
   328  			selectionOpts: WorkloadPolicyMatcher{
   329  				Namespace: "bar",
   330  				WorkloadLabels: labels.Instance{
   331  					"app":     "httpbin",
   332  					"version": "v2",
   333  				},
   334  			},
   335  			configs: []config.Config{
   336  				newConfig("authz-1", "bar", policyWithSelector),
   337  			},
   338  			wantAllow: nil,
   339  		},
   340  		{
   341  			name: "namespace not match",
   342  			selectionOpts: WorkloadPolicyMatcher{
   343  				Namespace: "foo",
   344  				WorkloadLabels: labels.Instance{
   345  					"app":     "httpbin",
   346  					"version": "v1",
   347  				},
   348  			},
   349  			configs: []config.Config{
   350  				newConfig("authz-1", "bar", policyWithSelector),
   351  			},
   352  			wantAllow: nil,
   353  		},
   354  		{
   355  			name: "root namespace",
   356  			selectionOpts: WorkloadPolicyMatcher{
   357  				Namespace: "bar",
   358  			},
   359  			configs: []config.Config{
   360  				newConfig("authz-1", "istio-config", policy),
   361  			},
   362  			wantAllow: []AuthorizationPolicy{
   363  				{
   364  					Name:      "authz-1",
   365  					Namespace: "istio-config",
   366  					Spec:      policy,
   367  				},
   368  			},
   369  		},
   370  		{
   371  			name: "root namespace equals config namespace",
   372  			selectionOpts: WorkloadPolicyMatcher{
   373  				Namespace: "istio-config",
   374  			},
   375  			configs: []config.Config{
   376  				newConfig("authz-1", "istio-config", policy),
   377  			},
   378  			wantAllow: []AuthorizationPolicy{
   379  				{
   380  					Name:      "authz-1",
   381  					Namespace: "istio-config",
   382  					Spec:      policy,
   383  				},
   384  			},
   385  		},
   386  		{
   387  			name: "root namespace and config namespace",
   388  			selectionOpts: WorkloadPolicyMatcher{
   389  				Namespace: "bar",
   390  			},
   391  			configs: []config.Config{
   392  				newConfig("authz-1", "istio-config", policy),
   393  				newConfig("authz-2", "bar", policy),
   394  			},
   395  			wantAllow: []AuthorizationPolicy{
   396  				{
   397  					Name:      "authz-1",
   398  					Namespace: "istio-config",
   399  					Spec:      policy,
   400  				},
   401  				{
   402  					Name:      "authz-2",
   403  					Namespace: "bar",
   404  					Spec:      policy,
   405  				},
   406  			},
   407  		},
   408  		{
   409  			name: "waypoint service attached",
   410  			selectionOpts: WorkloadPolicyMatcher{
   411  				IsWaypoint: true,
   412  				Service:    "foo-svc",
   413  				Namespace:  "foo",
   414  				WorkloadLabels: labels.Instance{
   415  					constants.GatewayNameLabel: "foo-waypoint",
   416  					// labels match in selector policy but ignore them for waypoint
   417  					"app":     "httpbin",
   418  					"version": "v1",
   419  				},
   420  			},
   421  			configs: []config.Config{
   422  				newConfig("authz-1", "foo", policyWithServiceRef),
   423  				newConfig("authz-2", "foo", policyWithSelector),
   424  			},
   425  			wantAllow: []AuthorizationPolicy{
   426  				{Name: "authz-1", Namespace: "foo", Spec: policyWithServiceRef},
   427  			},
   428  		},
   429  	}
   430  
   431  	for _, tc := range cases {
   432  		t.Run(tc.name, func(t *testing.T) {
   433  			authzPolicies := createFakeAuthorizationPolicies(tc.configs)
   434  
   435  			result := authzPolicies.ListAuthorizationPolicies(tc.selectionOpts)
   436  			if !reflect.DeepEqual(tc.wantAllow, result.Allow) {
   437  				t.Errorf("wantAllow:%v\n but got: %v\n", tc.wantAllow, result.Allow)
   438  			}
   439  			if !reflect.DeepEqual(tc.wantDeny, result.Deny) {
   440  				t.Errorf("wantDeny:%v\n but got: %v\n", tc.wantDeny, result.Deny)
   441  			}
   442  			if !reflect.DeepEqual(tc.wantAudit, result.Audit) {
   443  				t.Errorf("wantAudit:%v\n but got: %v\n", tc.wantAudit, result.Audit)
   444  			}
   445  			if !reflect.DeepEqual(tc.wantCustom, result.Custom) {
   446  				t.Errorf("wantCustom:%v\n but got: %v\n", tc.wantCustom, result.Custom)
   447  			}
   448  		})
   449  	}
   450  }
   451  
   452  func createFakeAuthorizationPolicies(configs []config.Config) *AuthorizationPolicies {
   453  	store := &authzFakeStore{}
   454  	for _, cfg := range configs {
   455  		store.add(cfg)
   456  	}
   457  	environment := &Environment{
   458  		ConfigStore: store,
   459  		Watcher:     mesh.NewFixedWatcher(&meshconfig.MeshConfig{RootNamespace: "istio-config"}),
   460  	}
   461  	authzPolicies := GetAuthorizationPolicies(environment)
   462  	return authzPolicies
   463  }
   464  
   465  func newConfig(name, ns string, spec config.Spec) config.Config {
   466  	return config.Config{
   467  		Meta: config.Meta{
   468  			GroupVersionKind: gvk.AuthorizationPolicy,
   469  			Name:             name,
   470  			Namespace:        ns,
   471  		},
   472  		Spec: spec,
   473  	}
   474  }
   475  
   476  type authzFakeStore struct {
   477  	data []struct {
   478  		typ config.GroupVersionKind
   479  		ns  string
   480  		cfg config.Config
   481  	}
   482  }
   483  
   484  func (fs *authzFakeStore) add(cfg config.Config) {
   485  	fs.data = append(fs.data, struct {
   486  		typ config.GroupVersionKind
   487  		ns  string
   488  		cfg config.Config
   489  	}{
   490  		typ: cfg.GroupVersionKind,
   491  		ns:  cfg.Namespace,
   492  		cfg: cfg,
   493  	})
   494  }
   495  
   496  func (fs *authzFakeStore) Schemas() collection.Schemas {
   497  	return collection.SchemasFor()
   498  }
   499  
   500  func (fs *authzFakeStore) Get(_ config.GroupVersionKind, _, _ string) *config.Config {
   501  	return nil
   502  }
   503  
   504  func (fs *authzFakeStore) List(typ config.GroupVersionKind, namespace string) []config.Config {
   505  	var configs []config.Config
   506  	for _, data := range fs.data {
   507  		if data.typ == typ {
   508  			if namespace != "" && data.ns == namespace {
   509  				continue
   510  			}
   511  			configs = append(configs, data.cfg)
   512  		}
   513  	}
   514  	return configs
   515  }
   516  
   517  func (fs *authzFakeStore) Delete(_ config.GroupVersionKind, _, _ string, _ *string) error {
   518  	return fmt.Errorf("not implemented")
   519  }
   520  
   521  func (fs *authzFakeStore) Create(config.Config) (string, error) {
   522  	return "not implemented", nil
   523  }
   524  
   525  func (fs *authzFakeStore) Update(config.Config) (string, error) {
   526  	return "not implemented", nil
   527  }
   528  
   529  func (fs *authzFakeStore) UpdateStatus(config.Config) (string, error) {
   530  	return "not implemented", nil
   531  }
   532  
   533  func (fs *authzFakeStore) Patch(orig config.Config, patchFn config.PatchFunc) (string, error) {
   534  	return "not implemented", nil
   535  }