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 }