blob: 3a26ffa57009bc5ae4d963e88ece1af6710c44e4 [file] [log] [blame]
//go:build integ
// +build integ
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"fmt"
"net/http"
"strings"
"testing"
"time"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/config/protocol"
"github.com/apache/dubbo-go-pixiu/pkg/http/headers"
echoClient "github.com/apache/dubbo-go-pixiu/pkg/test/echo"
"github.com/apache/dubbo-go-pixiu/pkg/test/echo/common/scheme"
"github.com/apache/dubbo-go-pixiu/pkg/test/env"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/check"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/deployment"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/match"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/istio"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/namespace"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/label"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/resource"
"github.com/apache/dubbo-go-pixiu/pkg/test/kube"
"github.com/apache/dubbo-go-pixiu/tests/common/jwt"
"github.com/apache/dubbo-go-pixiu/tests/integration/security/util"
"github.com/apache/dubbo-go-pixiu/tests/integration/security/util/scheck"
)
func newRootNS(ctx framework.TestContext) namespace.Instance {
return istio.ClaimSystemNamespaceOrFail(ctx, ctx)
}
// TestAuthorization_mTLS tests v1beta1 authorization with mTLS.
func TestAuthorization_mTLS(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.mtls-local").
Run(func(t framework.TestContext) {
b := match.Namespace(apps.Namespace1).GetMatches(apps.B)
vm := match.Namespace(apps.Namespace1).GetMatches(apps.VM)
for _, to := range []echo.Instances{b, vm} {
to := to
t.ConfigIstio().EvalFile(apps.Namespace1.Name(), map[string]string{
"Namespace": apps.Namespace1.Name(),
"Namespace2": apps.Namespace2.Name(),
"dst": to.Config().Service,
}, "testdata/authz/v1beta1-mtls.yaml.tmpl").ApplyOrFail(t, resource.Wait)
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
for _, cluster := range t.Clusters() {
a := match.And(match.Cluster(cluster), match.Namespace(apps.Namespace1)).GetMatches(apps.A)
c := match.And(match.Cluster(cluster), match.Namespace(apps.Namespace2)).GetMatches(apps.C)
if len(a) == 0 || len(c) == 0 {
continue
}
t.NewSubTestf("From %s", cluster.StableName()).Run(func(t framework.TestContext) {
newTestCase := func(from echo.Instance, path string, expectAllowed bool) func(t framework.TestContext) {
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Path: path,
},
Count: callCount,
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
// a and c send requests to dst
cases := []func(testContext framework.TestContext){
newTestCase(a[0], "/principal-a", true),
newTestCase(a[0], "/namespace-2", false),
newTestCase(c[0], "/principal-a", false),
newTestCase(c[0], "/namespace-2", true),
}
for _, c := range cases {
c(t)
}
})
}
}
})
}
// TestAuthorization_JWT tests v1beta1 authorization with JWT token claims.
func TestAuthorization_JWT(t *testing.T) {
framework.NewTest(t).
Label(label.IPv4). // https://github.com/istio/istio/issues/35835
Features("security.authorization.jwt-token").
Run(func(t framework.TestContext) {
ns := apps.Namespace1
b := match.Namespace(ns).GetMatches(apps.B)
c := match.Namespace(ns).GetMatches(apps.C)
vm := match.Namespace(ns).GetMatches(apps.VM)
for _, dst := range []echo.Instances{b, vm} {
args := map[string]string{
"Namespace": apps.Namespace1.Name(),
"Namespace2": apps.Namespace2.Name(),
"dst": dst[0].Config().Service,
}
t.ConfigIstio().EvalFile(ns.Name(), args, "testdata/authz/v1beta1-jwt.yaml.tmpl").ApplyOrFail(t, resource.Wait)
for _, srcCluster := range t.Clusters() {
a := match.And(match.Cluster(srcCluster), match.Namespace(ns)).GetMatches(apps.A)
if len(a) == 0 {
continue
}
t.NewSubTestf("From %s", srcCluster.StableName()).Run(func(t framework.TestContext) {
newTestCase := func(from echo.Instance, to echo.Target, namePrefix, jwt, path string, expectAllowed bool) func(t framework.TestContext) {
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Path: path,
Headers: headers.New().WithAuthz(jwt).Build(),
},
Count: callCount,
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
name := newRbacTestName(namePrefix, expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
cases := []func(testContext framework.TestContext){
newTestCase(a[0], dst, "[NoJWT]", "", "/token1", false),
newTestCase(a[0], dst, "[NoJWT]", "", "/token2", false),
newTestCase(a[0], dst, "[Token3]", jwt.TokenIssuer1, "/token3", false),
newTestCase(a[0], dst, "[Token3]", jwt.TokenIssuer2, "/token3", true),
newTestCase(a[0], dst, "[Token1]", jwt.TokenIssuer1, "/token1", true),
newTestCase(a[0], dst, "[Token1]", jwt.TokenIssuer1, "/token2", false),
newTestCase(a[0], dst, "[Token2]", jwt.TokenIssuer2, "/token1", false),
newTestCase(a[0], dst, "[Token2]", jwt.TokenIssuer2, "/token2", true),
newTestCase(a[0], dst, "[Token1]", jwt.TokenIssuer1, "/tokenAny", true),
newTestCase(a[0], dst, "[Token2]", jwt.TokenIssuer2, "/tokenAny", true),
newTestCase(a[0], dst, "[PermissionToken1]", jwt.TokenIssuer1, "/permission", false),
newTestCase(a[0], dst, "[PermissionToken2]", jwt.TokenIssuer2, "/permission", false),
newTestCase(a[0], dst, "[PermissionTokenWithSpaceDelimitedScope]", jwt.TokenIssuer2WithSpaceDelimitedScope, "/permission", true),
newTestCase(a[0], dst, "[NestedToken1]", jwt.TokenIssuer1WithNestedClaims1, "/nested-key1", true),
newTestCase(a[0], dst, "[NestedToken2]", jwt.TokenIssuer1WithNestedClaims2, "/nested-key1", false),
newTestCase(a[0], dst, "[NestedToken1]", jwt.TokenIssuer1WithNestedClaims1, "/nested-key2", false),
newTestCase(a[0], dst, "[NestedToken2]", jwt.TokenIssuer1WithNestedClaims2, "/nested-key2", true),
newTestCase(a[0], dst, "[NestedToken1]", jwt.TokenIssuer1WithNestedClaims1, "/nested-2-key1", true),
newTestCase(a[0], dst, "[NestedToken2]", jwt.TokenIssuer1WithNestedClaims2, "/nested-2-key1", false),
newTestCase(a[0], dst, "[NestedToken1]", jwt.TokenIssuer1WithNestedClaims1, "/nested-non-exist", false),
newTestCase(a[0], dst, "[NestedToken2]", jwt.TokenIssuer1WithNestedClaims2, "/nested-non-exist", false),
newTestCase(a[0], dst, "[NoJWT]", "", "/tokenAny", false),
newTestCase(a[0], c, "[NoJWT]", "", "/somePath", true),
// Test condition "request.auth.principal" on path "/valid-jwt".
newTestCase(a[0], dst, "[NoJWT]", "", "/valid-jwt", false),
newTestCase(a[0], dst, "[Token1]", jwt.TokenIssuer1, "/valid-jwt", true),
newTestCase(a[0], dst, "[Token1WithAzp]", jwt.TokenIssuer1WithAzp, "/valid-jwt", true),
newTestCase(a[0], dst, "[Token1WithAud]", jwt.TokenIssuer1WithAud, "/valid-jwt", true),
// Test condition "request.auth.presenter" on suffix "/presenter".
newTestCase(a[0], dst, "[Token1]", jwt.TokenIssuer1, "/request/presenter", false),
newTestCase(a[0], dst, "[Token1WithAud]", jwt.TokenIssuer1, "/request/presenter", false),
newTestCase(a[0], dst, "[Token1WithAzp]", jwt.TokenIssuer1WithAzp, "/request/presenter-x", false),
newTestCase(a[0], dst, "[Token1WithAzp]", jwt.TokenIssuer1WithAzp, "/request/presenter", true),
// Test condition "request.auth.audiences" on suffix "/audiences".
newTestCase(a[0], dst, "[Token1]", jwt.TokenIssuer1, "/request/audiences", false),
newTestCase(a[0], dst, "[Token1WithAzp]", jwt.TokenIssuer1WithAzp, "/request/audiences", false),
newTestCase(a[0], dst, "[Token1WithAud]", jwt.TokenIssuer1WithAud, "/request/audiences-x", false),
newTestCase(a[0], dst, "[Token1WithAud]", jwt.TokenIssuer1WithAud, "/request/audiences", true),
}
for _, c := range cases {
c(t)
}
})
}
}
})
}
// TestAuthorization_WorkloadSelector tests the workload selector for the v1beta1 policy in two namespaces.
func TestAuthorization_WorkloadSelector(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.workload-selector").
Run(func(t framework.TestContext) {
bInNS1 := match.Namespace(apps.Namespace1).GetMatches(apps.B)
vmInNS1 := match.Namespace(apps.Namespace1).GetMatches(apps.VM)
cInNS1 := match.Namespace(apps.Namespace1).GetMatches(apps.C)
cInNS2 := match.Namespace(apps.Namespace2).GetMatches(apps.C)
ns1 := apps.Namespace1
ns2 := apps.Namespace2
rootns := newRootNS(t)
newTestCase := func(from echo.Instance, to echo.Target, namePrefix, path string,
expectAllowed bool) func(t framework.TestContext) {
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Path: path,
},
Count: callCount,
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
name := newRbacTestName(namePrefix, expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
for _, srcCluster := range t.Clusters() {
a := match.And(match.Cluster(srcCluster), match.Namespace(apps.Namespace1)).GetMatches(apps.A)
if len(a) == 0 {
continue
}
t.NewSubTestf("From %s", srcCluster.StableName()).Run(func(t framework.TestContext) {
applyPolicy := func(filename string, ns namespace.Instance) {
t.ConfigIstio().EvalFile(ns.Name(), map[string]string{
"Namespace1": ns1.Name(),
"Namespace2": ns2.Name(),
"RootNamespace": rootns.Name(),
"b": util.BSvc,
"c": util.CSvc,
}, filename).ApplyOrFail(t, resource.Wait)
}
applyPolicy("testdata/authz/v1beta1-workload-ns1.yaml.tmpl", ns1)
applyPolicy("testdata/authz/v1beta1-workload-ns2.yaml.tmpl", ns2)
applyPolicy("testdata/authz/v1beta1-workload-ns-root.yaml.tmpl", rootns)
cases := []func(test framework.TestContext){
newTestCase(a[0], bInNS1, "[bInNS1]", "/policy-ns1-b", true),
newTestCase(a[0], bInNS1, "[bInNS1]", "/policy-ns1-vm", false),
newTestCase(a[0], bInNS1, "[bInNS1]", "/policy-ns1-c", false),
newTestCase(a[0], bInNS1, "[bInNS1]", "/policy-ns1-x", false),
newTestCase(a[0], bInNS1, "[bInNS1]", "/policy-ns1-all", true),
newTestCase(a[0], bInNS1, "[bInNS1]", "/policy-ns2-c", false),
newTestCase(a[0], bInNS1, "[bInNS1]", "/policy-ns2-all", false),
newTestCase(a[0], bInNS1, "[bInNS1]", "/policy-ns-root-c", false),
newTestCase(a[0], cInNS1, "[cInNS1]", "/policy-ns1-b", false),
newTestCase(a[0], cInNS1, "[cInNS1]", "/policy-ns1-vm", false),
newTestCase(a[0], cInNS1, "[cInNS1]", "/policy-ns1-c", true),
newTestCase(a[0], cInNS1, "[cInNS1]", "/policy-ns1-x", false),
newTestCase(a[0], cInNS1, "[cInNS1]", "/policy-ns1-all", true),
newTestCase(a[0], cInNS1, "[cInNS1]", "/policy-ns2-c", false),
newTestCase(a[0], cInNS1, "[cInNS1]", "/policy-ns2-all", false),
newTestCase(a[0], cInNS1, "[cInNS1]", "/policy-ns-root-c", true),
newTestCase(a[0], cInNS2, "[cInNS2]", "/policy-ns1-b", false),
newTestCase(a[0], cInNS2, "[cInNS2]", "/policy-ns1-vm", false),
newTestCase(a[0], cInNS2, "[cInNS2]", "/policy-ns1-c", false),
newTestCase(a[0], cInNS2, "[cInNS2]", "/policy-ns1-x", false),
newTestCase(a[0], cInNS2, "[cInNS2]", "/policy-ns1-all", false),
newTestCase(a[0], cInNS2, "[cInNS2]", "/policy-ns2-c", true),
newTestCase(a[0], cInNS2, "[cInNS2]", "/policy-ns2-all", true),
newTestCase(a[0], cInNS2, "[cInNS2]", "/policy-ns-root-c", true),
}
for _, c := range cases {
c(t)
}
})
// TODO(JimmyCYJ): Support multiple VMs in different namespaces for workload selector test and set c to service on VM.
t.NewSubTestf("VM From %s", srcCluster.StableName()).Run(func(t framework.TestContext) {
applyPolicy := func(filename string, ns namespace.Instance) {
t.ConfigIstio().EvalFile(ns.Name(), map[string]string{
"Namespace1": ns1.Name(),
"Namespace2": ns2.Name(),
"RootNamespace": rootns.Name(),
"b": util.VMSvc, // This is the only difference from standard args.
"c": util.CSvc,
}, filename).ApplyOrFail(t, resource.Wait)
}
applyPolicy("testdata/authz/v1beta1-workload-ns1.yaml.tmpl", ns1)
applyPolicy("testdata/authz/v1beta1-workload-ns2.yaml.tmpl", ns2)
applyPolicy("testdata/authz/v1beta1-workload-ns-root.yaml.tmpl", rootns)
cases := []func(test framework.TestContext){
newTestCase(a[0], vmInNS1, "[vmInNS1]", "/policy-ns1-b", false),
newTestCase(a[0], vmInNS1, "[vmInNS1]", "/policy-ns1-vm", true),
newTestCase(a[0], vmInNS1, "[vmInNS1]", "/policy-ns1-c", false),
newTestCase(a[0], vmInNS1, "[vmInNS1]", "/policy-ns1-x", false),
newTestCase(a[0], vmInNS1, "[vmInNS1]", "/policy-ns1-all", true),
newTestCase(a[0], vmInNS1, "[vmInNS1]", "/policy-ns2-b", false),
newTestCase(a[0], vmInNS1, "[vmInNS1]", "/policy-ns2-all", false),
newTestCase(a[0], vmInNS1, "[vmInNS1]", "/policy-ns-root-c", false),
}
for _, c := range cases {
c(t)
}
})
}
})
}
// TestAuthorization_Deny tests the authorization policy with action "DENY".
func TestAuthorization_Deny(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.deny-action").
Run(func(t framework.TestContext) {
if t.Clusters().IsMulticluster() {
t.Skip("https://github.com/istio/istio/issues/37307")
}
ns := apps.Namespace1
rootns := newRootNS(t)
b := match.Namespace(apps.Namespace1).GetMatches(apps.B)
c := match.Namespace(apps.Namespace1).GetMatches(apps.C)
vm := match.Namespace(apps.Namespace1).GetMatches(apps.VM)
applyPolicy := func(filename string, ns namespace.Instance) {
t.ConfigIstio().EvalFile(ns.Name(), map[string]string{
"Namespace": ns.Name(),
"RootNamespace": rootns.Name(),
"b": b[0].Config().Service,
"c": c[0].Config().Service,
"vm": vm[0].Config().Service,
}, filename).ApplyOrFail(t, resource.Wait)
}
applyPolicy("testdata/authz/v1beta1-deny.yaml.tmpl", ns)
applyPolicy("testdata/authz/v1beta1-deny-ns-root.yaml.tmpl", rootns)
for _, srcCluster := range t.Clusters() {
a := match.And(match.Cluster(srcCluster), match.Namespace(apps.Namespace1)).GetMatches(apps.A)
if len(a) == 0 {
continue
}
t.NewSubTestf("From %s", srcCluster.StableName()).Run(func(t framework.TestContext) {
newTestCase := func(from echo.Instance, to echo.Target, path string, expectAllowed bool) func(t framework.TestContext) {
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Path: path,
},
Count: callCount,
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
cases := []func(t framework.TestContext){
newTestCase(a[0], b, "/deny", false),
newTestCase(a[0], b, "/deny?param=value", false),
newTestCase(a[0], b, "/global-deny", false),
newTestCase(a[0], b, "/global-deny?param=value", false),
newTestCase(a[0], b, "/other", true),
newTestCase(a[0], b, "/other?param=value", true),
newTestCase(a[0], b, "/allow", true),
newTestCase(a[0], b, "/allow?param=value", true),
newTestCase(a[0], c, "/allow/admin", false),
newTestCase(a[0], c, "/allow/admin?param=value", false),
newTestCase(a[0], c, "/global-deny", false),
newTestCase(a[0], c, "/global-deny?param=value", false),
newTestCase(a[0], c, "/other", false),
newTestCase(a[0], c, "/other?param=value", false),
newTestCase(a[0], c, "/allow", true),
newTestCase(a[0], c, "/allow?param=value", true),
// TODO(JimmyCYJ): support multiple VMs and test deny policies on multiple VMs.
newTestCase(a[0], vm, "/allow/admin", false),
newTestCase(a[0], vm, "/allow/admin?param=value", false),
newTestCase(a[0], vm, "/global-deny", false),
newTestCase(a[0], vm, "/global-deny?param=value", false),
newTestCase(a[0], vm, "/other", false),
newTestCase(a[0], vm, "/other?param=value", false),
newTestCase(a[0], vm, "/allow", true),
newTestCase(a[0], vm, "/allow?param=value", true),
}
for _, c := range cases {
c(t)
}
})
}
})
}
// TestAuthorization_NegativeMatch tests the authorization policy with negative match.
func TestAuthorization_NegativeMatch(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.negative-match").
Run(func(t framework.TestContext) {
ns := apps.Namespace1
ns2 := apps.Namespace2
b := match.Namespace(apps.Namespace1).GetMatches(apps.B)
c := match.Namespace(apps.Namespace1).GetMatches(apps.C)
d := match.Namespace(apps.Namespace1).GetMatches(apps.D)
vm := match.Namespace(apps.Namespace1).GetMatches(apps.VM)
t.ConfigIstio().EvalFile("", map[string]string{
"Namespace": ns.Name(),
"Namespace2": ns2.Name(),
"b": b[0].Config().Service,
"c": c[0].Config().Service,
"d": d[0].Config().Service,
"vm": vm[0].Config().Service,
}, "testdata/authz/v1beta1-negative-match.yaml.tmpl").ApplyOrFail(t)
type request struct {
method string // default value is GET if not specified
host string // only set if not empty
port string // default value is http if not specified
}
reqParam := request{method: "GET", port: "http"}
reqNotMethodParam := request{method: "PUT", port: "http"}
reqNotPortParam := request{method: "GET", port: "http-8091"}
reqNotHostParam := request{method: "GET", port: "http", host: "deny.com"}
for _, srcCluster := range t.Clusters() {
a := match.And(match.Cluster(srcCluster), match.Namespace(apps.Namespace1)).GetMatches(apps.A)
bInNS2 := match.And(match.Cluster(srcCluster), match.Namespace(apps.Namespace2)).GetMatches(apps.B)
if len(a) == 0 || len(bInNS2) == 0 {
continue
}
t.NewSubTestf("From %s", srcCluster.StableName()).Run(func(t framework.TestContext) {
newTestCaseWithRequest := func(from echo.Instance, to echo.Target, path string, expectAllowed bool, request request) func(t framework.TestContext) {
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: request.port,
},
HTTP: echo.HTTP{
Path: path,
Method: request.method,
Headers: headers.New().WithHost(request.host).Build(),
},
Count: callCount,
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
newTestCase := func(from echo.Instance, to echo.Target, path string, expectAllowed bool) func(t framework.TestContext) {
return newTestCaseWithRequest(from, to, path, expectAllowed, reqParam)
}
// a, b, c and d are in the same namespace and another b(bInNs2) is in a different namespace.
// a connects to b, c and d in ns1 with mTLS.
// bInNs2 connects to b and c with mTLS, to d with plain-text.
cases := []func(testContext framework.TestContext){
// Test the policy with overlapped `paths`, `not_paths` and `not_methods` on b.
// a and bInNs2 should have the same results:
// - path with prefix `/prefix` should be denied explicitly.
// - path `/prefix/allowlist` should be excluded from the deny.
// - path `/allow` should be allowed implicitly.
newTestCaseWithRequest(a[0], b, "/", false, reqNotMethodParam),
newTestCase(a[0], b, "/prefix", false),
newTestCase(a[0], b, "/prefix/other", false),
newTestCase(a[0], b, "/prefix/allowlist", true),
newTestCase(a[0], b, "/allow", true),
newTestCase(bInNS2[0], b, "/prefix", false),
newTestCase(bInNS2[0], b, "/prefix/other", false),
newTestCase(bInNS2[0], b, "/prefix/allowlist", true),
newTestCase(bInNS2[0], b, "/allow", true),
// Test the policy that denies other namespace on c.
// a should be allowed because it's from the same namespace.
// any request to path deny.com should be denied
// bInNs2 should be denied because it's from a different namespace.
newTestCase(a[0], c, "/", true),
newTestCaseWithRequest(a[0], c, "/", false, reqNotHostParam),
newTestCase(bInNS2[0], c, "/", false),
// Test the policy that denies plain-text traffic on d.
// a should be allowed because it's using mTLS.
// any request to port 8091 should be denied
// bInNs2 should be denied because it's using plain-text.
newTestCase(a[0], d, "/", true),
newTestCaseWithRequest(a[0], d, "/", false, reqNotPortParam),
newTestCase(bInNS2[0], d, "/", false),
// Test the policy with overlapped `paths` and `not_paths` on vm.
// a and bInNs2 should have the same results:
// - path with prefix `/prefix` should be denied explicitly.
// - path `/prefix/allowlist` should be excluded from the deny.
// - path `/allow` should be allowed implicitly.
// TODO(JimmyCYJ): support multiple VMs and test negative match on multiple VMs.
newTestCase(a[0], vm, "/prefix", false),
newTestCase(a[0], vm, "/prefix/other", false),
newTestCase(a[0], vm, "/prefix/allowlist", true),
newTestCase(a[0], vm, "/allow", true),
newTestCase(bInNS2[0], vm, "/prefix", false),
newTestCase(bInNS2[0], vm, "/prefix/other", false),
newTestCase(bInNS2[0], vm, "/prefix/allowlist", true),
newTestCase(bInNS2[0], vm, "/allow", true),
}
for _, c := range cases {
c(t)
}
})
}
})
}
// TestAuthorization_IngressGateway tests the authorization policy on ingress gateway.
func TestAuthorization_IngressGateway(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.ingress-gateway").
Run(func(t framework.TestContext) {
ns := apps.Namespace1
rootns := newRootNS(t)
b := match.Namespace(apps.Namespace1).GetMatches(apps.B)
// Gateways on VMs are not supported yet. This test verifies that security
// policies at gateways are useful for managing accessibility to services
// running on a VM.
vm := match.Namespace(apps.Namespace1).GetMatches(apps.VM)
for _, dst := range []echo.Instances{b, vm} {
t.NewSubTestf("to %s/", dst[0].Config().Service).Run(func(t framework.TestContext) {
t.ConfigIstio().EvalFile("", map[string]string{
"Namespace": ns.Name(),
"RootNamespace": rootns.Name(),
"dst": dst[0].Config().Service,
}, "testdata/authz/v1beta1-ingress-gateway.yaml.tmpl").ApplyOrFail(t)
ingr := ist.IngressFor(t.Clusters().Default())
cases := []struct {
Name string
Host string
Path string
IP string
WantCode int
}{
{
Name: "case-insensitive-deny deny.company.com",
Host: "deny.company.com",
WantCode: http.StatusForbidden,
},
{
Name: "case-insensitive-deny DENY.COMPANY.COM",
Host: "DENY.COMPANY.COM",
WantCode: http.StatusForbidden,
},
{
Name: "case-insensitive-deny Deny.Company.Com",
Host: "Deny.Company.Com",
WantCode: http.StatusForbidden,
},
{
Name: "case-insensitive-deny deny.suffix.company.com",
Host: "deny.suffix.company.com",
WantCode: http.StatusForbidden,
},
{
Name: "case-insensitive-deny DENY.SUFFIX.COMPANY.COM",
Host: "DENY.SUFFIX.COMPANY.COM",
WantCode: http.StatusForbidden,
},
{
Name: "case-insensitive-deny Deny.Suffix.Company.Com",
Host: "Deny.Suffix.Company.Com",
WantCode: http.StatusForbidden,
},
{
Name: "case-insensitive-deny prefix.company.com",
Host: "prefix.company.com",
WantCode: http.StatusForbidden,
},
{
Name: "case-insensitive-deny PREFIX.COMPANY.COM",
Host: "PREFIX.COMPANY.COM",
WantCode: http.StatusForbidden,
},
{
Name: "case-insensitive-deny Prefix.Company.Com",
Host: "Prefix.Company.Com",
WantCode: http.StatusForbidden,
},
{
Name: "allow www.company.com",
Host: "www.company.com",
Path: "/",
IP: "172.16.0.1",
WantCode: http.StatusOK,
},
{
Name: "deny www.company.com/private",
Host: "www.company.com",
Path: "/private",
IP: "172.16.0.1",
WantCode: http.StatusForbidden,
},
{
Name: "allow www.company.com/public",
Host: "www.company.com",
Path: "/public",
IP: "172.16.0.1",
WantCode: http.StatusOK,
},
{
Name: "deny internal.company.com",
Host: "internal.company.com",
Path: "/",
IP: "172.16.0.1",
WantCode: http.StatusForbidden,
},
{
Name: "deny internal.company.com/private",
Host: "internal.company.com",
Path: "/private",
IP: "172.16.0.1",
WantCode: http.StatusForbidden,
},
{
Name: "deny 172.17.72.46",
Host: "remoteipblocks.company.com",
Path: "/",
IP: "172.17.72.46",
WantCode: http.StatusForbidden,
},
{
Name: "deny 192.168.5.233",
Host: "remoteipblocks.company.com",
Path: "/",
IP: "192.168.5.233",
WantCode: http.StatusForbidden,
},
{
Name: "allow 10.4.5.6",
Host: "remoteipblocks.company.com",
Path: "/",
IP: "10.4.5.6",
WantCode: http.StatusOK,
},
{
Name: "deny 10.2.3.4",
Host: "notremoteipblocks.company.com",
Path: "/",
IP: "10.2.3.4",
WantCode: http.StatusForbidden,
},
{
Name: "allow 172.23.242.188",
Host: "notremoteipblocks.company.com",
Path: "/",
IP: "172.23.242.188",
WantCode: http.StatusOK,
},
{
Name: "deny 10.242.5.7",
Host: "remoteipattr.company.com",
Path: "/",
IP: "10.242.5.7",
WantCode: http.StatusForbidden,
},
{
Name: "deny 10.124.99.10",
Host: "remoteipattr.company.com",
Path: "/",
IP: "10.124.99.10",
WantCode: http.StatusForbidden,
},
{
Name: "allow 10.4.5.6",
Host: "remoteipattr.company.com",
Path: "/",
IP: "10.4.5.6",
WantCode: http.StatusOK,
},
{
Name: "allow 172.19.19.19",
Host: "ipblocks.company.com",
Path: "/",
IP: "172.19.19.19",
WantCode: http.StatusOK,
},
{
Name: "deny 172.19.19.20",
Host: "notipblocks.company.com",
Path: "/",
IP: "172.19.19.20",
WantCode: http.StatusForbidden,
},
}
for _, tc := range cases {
t.NewSubTest(tc.Name).Run(func(t framework.TestContext) {
opts := echo.CallOptions{
Port: echo.Port{
Protocol: protocol.HTTP,
},
HTTP: echo.HTTP{
Path: tc.Path,
Headers: headers.New().WithHost(tc.Host).WithXForwardedFor(tc.IP).Build(),
},
Check: check.Status(tc.WantCode),
}
ingr.CallOrFail(t, opts)
})
}
})
}
})
}
// TestAuthorization_EgressGateway tests v1beta1 authorization on egress gateway.
func TestAuthorization_EgressGateway(t *testing.T) {
framework.NewTest(t).
Label(label.IPv4). // https://github.com/istio/istio/issues/35835
Features("security.authorization.egress-gateway").
Run(func(t framework.TestContext) {
ns := apps.Namespace1
rootns := newRootNS(t)
a := match.Namespace(apps.Namespace1).GetMatches(apps.A)
vm := match.Namespace(apps.Namespace1).GetMatches(apps.VM)
c := match.Namespace(apps.Namespace1).GetMatches(apps.C)
// Gateways on VMs are not supported yet. This test verifies that security
// policies at gateways are useful for managing accessibility to external
// services running on a VM.
for _, a := range []echo.Instances{a, vm} {
t.NewSubTestf("to %s", a[0].Config().Service).Run(func(t framework.TestContext) {
t.ConfigIstio().EvalFile("", map[string]string{
"Namespace": ns.Name(),
"RootNamespace": rootns.Name(),
"a": a[0].Config().Service,
}, "testdata/authz/v1beta1-egress-gateway.yaml.tmpl").ApplyOrFail(t)
cases := []struct {
name string
path string
code int
body string
host string
from echo.Instances
token string
}{
{
name: "allow path to company.com",
path: "/allow",
code: http.StatusOK,
body: "handled-by-egress-gateway",
host: "www.company.com",
from: a,
},
{
name: "deny path to company.com",
path: "/deny",
code: http.StatusForbidden,
body: "RBAC: access denied",
host: "www.company.com",
from: a,
},
{
name: "allow service account a to a-only.com over mTLS",
path: "/",
code: http.StatusOK,
body: "handled-by-egress-gateway",
host: fmt.Sprintf("%s-only.com", a[0].Config().Service),
from: a,
},
{
name: "deny service account b to a-only.com over mTLS",
path: "/",
code: http.StatusForbidden,
body: "RBAC: access denied",
host: fmt.Sprintf("%s-only.com", a[0].Config().Service),
from: c,
},
{
name: "allow a with JWT to jwt-only.com over mTLS",
path: "/",
code: http.StatusOK,
body: "handled-by-egress-gateway",
host: "jwt-only.com",
from: a,
token: jwt.TokenIssuer1,
},
{
name: "allow b with JWT to jwt-only.com over mTLS",
path: "/",
code: http.StatusOK,
body: "handled-by-egress-gateway",
host: "jwt-only.com",
from: c,
token: jwt.TokenIssuer1,
},
{
name: "deny b with wrong JWT to jwt-only.com over mTLS",
path: "/",
code: http.StatusForbidden,
body: "RBAC: access denied",
host: "jwt-only.com",
from: c,
token: jwt.TokenIssuer2,
},
{
name: "allow service account a with JWT to jwt-and-a-only.com over mTLS",
path: "/",
code: http.StatusOK,
body: "handled-by-egress-gateway",
host: fmt.Sprintf("jwt-and-%s-only.com", a[0].Config().Service),
from: a,
token: jwt.TokenIssuer1,
},
{
name: "deny service account c with JWT to jwt-and-a-only.com over mTLS",
path: "/",
code: http.StatusForbidden,
body: "RBAC: access denied",
host: fmt.Sprintf("jwt-and-%s-only.com", a[0].Config().Service),
from: c,
token: jwt.TokenIssuer1,
},
{
name: "deny service account a with wrong JWT to jwt-and-a-only.com over mTLS",
path: "/",
code: http.StatusForbidden,
body: "RBAC: access denied",
host: fmt.Sprintf("jwt-and-%s-only.com", a[0].Config().Service),
from: a,
token: jwt.TokenIssuer2,
},
}
for _, tc := range cases {
t.NewSubTest(tc.name).Run(func(t framework.TestContext) {
tc.from[0].CallOrFail(t, echo.CallOptions{
Port: echo.Port{
Protocol: protocol.HTTP,
ServicePort: 80,
},
Timeout: time.Second,
// Use a fake IP to make sure the request is handled by our test.
Address: "10.4.4.4",
HTTP: echo.HTTP{
Headers: headers.New().WithHost(tc.host).WithAuthz(tc.token).Build(),
Path: tc.path,
},
Check: check.And(
check.NoError(),
check.Status(tc.code),
check.BodyContains(tc.body)),
})
})
}
})
}
})
}
// TestAuthorization_TCP tests the authorization policy on workloads using the raw TCP protocol.
func TestAuthorization_TCP(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.tcp").
Run(func(t framework.TestContext) {
newTestCase := func(from echo.Instance, to echo.Target, s scheme.Instance, portName string, expectAllowed bool) func(t framework.TestContext) {
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: portName,
},
Scheme: s,
HTTP: echo.HTTP{
Path: "/data",
},
}
opts.FillDefaultsOrFail(t)
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(opts.Port.Protocol)
}
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
ns := apps.Namespace1
ns2 := apps.Namespace2
a := match.Namespace(ns).GetMatches(apps.A)
b := match.Namespace(ns).GetMatches(apps.B)
c := match.Namespace(ns).GetMatches(apps.C)
eInNS2 := match.Namespace(ns2).GetMatches(apps.E)
d := match.Namespace(ns).GetMatches(apps.D)
e := match.Namespace(ns).GetMatches(apps.E)
t.NewSubTest("non-vms").
Run(func(t framework.TestContext) {
t.ConfigIstio().EvalFile("", map[string]string{
"Namespace": ns.Name(),
"Namespace2": ns2.Name(),
"b": b[0].Config().Service,
"c": c[0].Config().Service,
"d": d[0].Config().Service,
"e": e[0].Config().Service,
"a": a[0].Config().Service,
}, "testdata/authz/v1beta1-tcp.yaml.tmpl").ApplyOrFail(t)
cases := []func(testContext framework.TestContext){
// The policy on workload b denies request with path "/data" to port 8091:
// - request to port http-8091 should be denied because both path and port are matched.
// - request to port http-8092 should be allowed because the port is not matched.
// - request to port tcp-8093 should be allowed because the port is not matched.
newTestCase(a[0], b, scheme.HTTP, "http-8091", false),
newTestCase(a[0], b, scheme.HTTP, "http-8092", true),
newTestCase(a[0], b, scheme.TCP, "tcp-8093", true),
// The policy on workload c denies request to port 8091:
// - request to port http-8091 should be denied because the port is matched.
// - request to http port 8092 should be allowed because the port is not matched.
// - request to tcp port 8093 should be allowed because the port is not matched.
// - request from b to tcp port 8093 should be allowed by default.
// - request from b to tcp port 8094 should be denied because the principal is matched.
// - request from eInNS2 to tcp port 8093 should be denied because the namespace is matched.
// - request from eInNS2 to tcp port 8094 should be allowed by default.
newTestCase(a[0], c, scheme.HTTP, "http-8091", false),
newTestCase(a[0], c, scheme.HTTP, "http-8092", true),
newTestCase(a[0], c, scheme.TCP, "tcp-8093", true),
newTestCase(b[0], c, scheme.TCP, "tcp-8093", true),
newTestCase(b[0], c, scheme.TCP, "tcp-8094", false),
newTestCase(eInNS2[0], c, scheme.TCP, "tcp-8093", false),
newTestCase(eInNS2[0], c, scheme.TCP, "tcp-8094", true),
// The policy on workload d denies request from service account a and workloads in namespace 2:
// - request from a to d should be denied because it has service account a.
// - request from b to d should be allowed.
// - request from c to d should be allowed.
// - request from eInNS2 to a should be allowed because there is no policy on a.
// - request from eInNS2 to d should be denied because it's in namespace 2.
newTestCase(a[0], d, scheme.TCP, "tcp-8093", false),
newTestCase(b[0], d, scheme.TCP, "tcp-8093", true),
newTestCase(c[0], d, scheme.TCP, "tcp-8093", true),
newTestCase(eInNS2[0], a, scheme.TCP, "tcp-8093", true),
newTestCase(eInNS2[0], d, scheme.TCP, "tcp-8093", false),
// The policy on workload e denies request with path "/other":
// - request to port http-8091 should be allowed because the path is not matched.
// - request to port http-8092 should be allowed because the path is not matched.
// - request to port tcp-8093 should be denied because policy uses HTTP fields.
newTestCase(a[0], e, scheme.HTTP, "http-8091", true),
newTestCase(a[0], e, scheme.HTTP, "http-8092", true),
newTestCase(a[0], e, scheme.TCP, "tcp-8093", false),
}
for _, c := range cases {
c(t)
}
})
// TODO(JimmyCYJ): support multiple VMs and apply different security policies to each VM.
vm := match.Namespace(ns).GetMatches(apps.VM)
t.NewSubTest("vms").
Run(func(t framework.TestContext) {
t.ConfigIstio().EvalFile("", map[string]string{
"Namespace": ns.Name(),
"Namespace2": ns2.Name(),
"b": b[0].Config().Service,
"c": vm[0].Config().Service,
"d": d[0].Config().Service,
"e": e[0].Config().Service,
"a": a[0].Config().Service,
}, "testdata/authz/v1beta1-tcp.yaml.tmpl").ApplyOrFail(t)
cases := []func(testContext framework.TestContext){
// The policy on workload vm denies request to port 8091:
// - request to port http-8091 should be denied because the port is matched.
// - request to http port 8092 should be allowed because the port is not matched.
// - request to tcp port 8093 should be allowed because the port is not matched.
// - request from b to tcp port 8093 should be allowed by default.
// - request from b to tcp port 8094 should be denied because the principal is matched.
// - request from eInNS2 to tcp port 8093 should be denied because the namespace is matched.
// - request from eInNS2 to tcp port 8094 should be allowed by default.
newTestCase(a[0], vm, scheme.HTTP, "http-8091", false),
newTestCase(a[0], vm, scheme.HTTP, "http-8092", true),
newTestCase(a[0], vm, scheme.TCP, "tcp-8093", true),
newTestCase(b[0], vm, scheme.TCP, "tcp-8093", true),
newTestCase(b[0], vm, scheme.TCP, "tcp-8094", false),
newTestCase(eInNS2[0], vm, scheme.TCP, "tcp-8093", false),
newTestCase(eInNS2[0], vm, scheme.TCP, "tcp-8094", true),
}
for _, c := range cases {
c(t)
}
})
})
}
// TestAuthorization_Conditions tests v1beta1 authorization with conditions.
func TestAuthorization_Conditions(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.conditions").
Run(func(t framework.TestContext) {
nsA := apps.Namespace1
nsB := apps.Namespace2
nsC := apps.Namespace3
c := match.Namespace(nsC).GetMatches(apps.C)
vm := match.Namespace(nsA).GetMatches(apps.VM)
for _, to := range []echo.Instances{c, vm} {
to := to
for _, a := range match.Namespace(nsA).GetMatches(apps.A) {
a := a
bs := match.And(match.Cluster(a.Config().Cluster), match.Namespace(nsB)).GetMatches(apps.B)
if len(bs) < 1 {
t.Skip()
}
b := bs[0]
t.NewSubTestf("from %s to %s in %s",
a.Config().Cluster.StableName(), to.Config().Service, to.Config().Cluster.StableName()).
Run(func(t framework.TestContext) {
args := map[string]interface{}{
"NamespaceA": nsA.Name(),
"NamespaceB": nsB.Name(),
"NamespaceC": to.Config().Namespace.Name(),
"cSet": to.Config().Service,
"ipA": a.WorkloadsOrFail(t).Addresses(),
"ipB": b.WorkloadsOrFail(t).Addresses(),
"ipC": to.WorkloadsOrFail(t).Addresses(),
"portC": "8090",
"a": util.ASvc,
"b": util.BSvc,
}
t.ConfigIstio().EvalFile("", args, "testdata/authz/v1beta1-conditions.yaml.tmpl").ApplyOrFail(t)
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
newTestCase := func(from echo.Instance, path string, headers http.Header, expectAllowed bool) func(t framework.TestContext) {
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Path: path,
Headers: headers,
},
Count: callCount,
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
cases := []func(framework.TestContext){
newTestCase(a, "/request-headers", headers.New().With("x-foo", "foo").Build(), true),
newTestCase(b, "/request-headers", headers.New().With("x-foo", "foo").Build(), true),
newTestCase(a, "/request-headers", headers.New().With("x-foo", "bar").Build(), false),
newTestCase(b, "/request-headers", headers.New().With("x-foo", "bar").Build(), false),
newTestCase(a, "/request-headers", nil, false),
newTestCase(b, "/request-headers", nil, false),
newTestCase(a, "/request-headers-notValues-bar", headers.New().With("x-foo", "foo").Build(), true),
newTestCase(a, "/request-headers-notValues-bar", headers.New().With("x-foo", "bar").Build(), false),
newTestCase(a, fmt.Sprintf("/source-ip-%s", args["a"]), nil, true),
newTestCase(b, fmt.Sprintf("/source-ip-%s", args["a"]), nil, false),
newTestCase(a, fmt.Sprintf("/source-ip-%s", args["b"]), nil, false),
newTestCase(b, fmt.Sprintf("/source-ip-%s", args["b"]), nil, true),
newTestCase(a, fmt.Sprintf("/source-ip-notValues-%s", args["b"]), nil, true),
newTestCase(b, fmt.Sprintf("/source-ip-notValues-%s", args["b"]), nil, false),
newTestCase(a, fmt.Sprintf("/source-namespace-%s", args["a"]), nil, true),
newTestCase(b, fmt.Sprintf("/source-namespace-%s", args["a"]), nil, false),
newTestCase(a, fmt.Sprintf("/source-namespace-%s", args["b"]), nil, false),
newTestCase(b, fmt.Sprintf("/source-namespace-%s", args["b"]), nil, true),
newTestCase(a, fmt.Sprintf("/source-namespace-notValues-%s", args["b"]), nil, true),
newTestCase(b, fmt.Sprintf("/source-namespace-notValues-%s", args["b"]), nil, false),
newTestCase(a, fmt.Sprintf("/source-principal-%s", args["a"]), nil, true),
newTestCase(b, fmt.Sprintf("/source-principal-%s", args["a"]), nil, false),
newTestCase(a, fmt.Sprintf("/source-principal-%s", args["b"]), nil, false),
newTestCase(b, fmt.Sprintf("/source-principal-%s", args["b"]), nil, true),
newTestCase(a, fmt.Sprintf("/source-principal-notValues-%s", args["b"]), nil, true),
newTestCase(b, fmt.Sprintf("/source-principal-notValues-%s", args["b"]), nil, false),
newTestCase(a, "/destination-ip-good", nil, true),
newTestCase(b, "/destination-ip-good", nil, true),
newTestCase(a, "/destination-ip-bad", nil, false),
newTestCase(b, "/destination-ip-bad", nil, false),
newTestCase(a, fmt.Sprintf("/destination-ip-notValues-%s-or-%s", args["a"], args["b"]), nil, true),
newTestCase(a, fmt.Sprintf("/destination-ip-notValues-%s-or-%s-or-%s", args["a"], args["b"], args["cSet"]), nil, false),
newTestCase(a, "/destination-port-good", nil, true),
newTestCase(b, "/destination-port-good", nil, true),
newTestCase(a, "/destination-port-bad", nil, false),
newTestCase(b, "/destination-port-bad", nil, false),
newTestCase(a, fmt.Sprintf("/destination-port-notValues-%s", args["cSet"]), nil, false),
newTestCase(b, fmt.Sprintf("/destination-port-notValues-%s", args["cSet"]), nil, false),
newTestCase(a, "/connection-sni-good", nil, true),
newTestCase(b, "/connection-sni-good", nil, true),
newTestCase(a, "/connection-sni-bad", nil, false),
newTestCase(b, "/connection-sni-bad", nil, false),
newTestCase(a, fmt.Sprintf("/connection-sni-notValues-%s-or-%s", args["a"], args["b"]), nil, true),
newTestCase(a, fmt.Sprintf("/connection-sni-notValues-%s-or-%s-or-%s", args["a"], args["b"], args["cSet"]), nil, false),
newTestCase(a, "/other", nil, false),
newTestCase(b, "/other", nil, false),
}
for _, c := range cases {
c(t)
}
})
}
}
})
}
// TestAuthorization_GRPC tests v1beta1 authorization with gRPC protocol.
func TestAuthorization_GRPC(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.grpc-protocol").
Run(func(t framework.TestContext) {
ns := apps.Namespace1
a := match.Namespace(apps.Namespace1).GetMatches(apps.A)
b := match.Namespace(apps.Namespace1).GetMatches(apps.B)
c := match.Namespace(apps.Namespace1).GetMatches(apps.C)
d := match.Namespace(apps.Namespace1).GetMatches(apps.D)
vm := match.Namespace(apps.Namespace1).GetMatches(apps.VM)
for _, a := range []echo.Instances{a, vm} {
for _, b := range []echo.Instances{b, vm} {
if a[0].Config().Service == b[0].Config().Service {
t.Skip()
}
t.NewSubTestf("to %s in %s", a[0].Config().Service, a[0].Config().Cluster.StableName()).
Run(func(t framework.TestContext) {
args := map[string]string{
"Namespace": ns.Name(),
"a": a[0].Config().Service,
"b": b[0].Config().Service,
"c": c[0].Config().Service,
"d": d[0].Config().Service,
}
t.ConfigIstio().EvalFile(ns.Name(), args, "testdata/authz/v1beta1-grpc.yaml.tmpl").ApplyOrFail(t, resource.Wait)
newTestCase := func(from echo.Instance, to echo.Target, expectAllowed bool) func(t framework.TestContext) {
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "grpc",
},
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.GRPC)
}
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
cases := []func(testContext framework.TestContext){
newTestCase(b[0], a, true),
newTestCase(c[0], a, false),
newTestCase(d[0], a, true),
}
for _, c := range cases {
c(t)
}
})
}
}
})
}
// TestAuthorization_Path tests the path is normalized before using in authorization. For example, a request
// with path "/a/../b" should be normalized to "/b" before using in authorization.
func TestAuthorization_Path(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.path-normalization").
Run(func(t framework.TestContext) {
ns := apps.Namespace1
a := match.Namespace(ns).GetMatches(apps.A)
vm := match.Namespace(ns).GetMatches(apps.VM)
for _, a := range []echo.Instances{a, vm} {
for _, srcCluster := range t.Clusters() {
b := match.And(match.Cluster(srcCluster), match.Namespace(ns)).GetMatches(apps.B)
if len(b) == 0 {
continue
}
t.NewSubTestf("In %s", srcCluster.StableName()).Run(func(t framework.TestContext) {
args := map[string]string{
"Namespace": ns.Name(),
"a": a[0].Config().Service,
}
t.ConfigIstio().EvalFile(ns.Name(), args, "testdata/authz/v1beta1-path.yaml.tmpl").ApplyOrFail(t, resource.Wait)
newTestCase := func(from echo.Instance, to echo.Target, path string, expectAllowed bool) func(t framework.TestContext) {
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Path: path,
},
Count: callCount,
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
cases := []func(test framework.TestContext){
newTestCase(b[0], a, "/public", true),
newTestCase(b[0], a, "/public/../public", true),
newTestCase(b[0], a, "/private", false),
newTestCase(b[0], a, "/public/../private", false),
newTestCase(b[0], a, "/public/./../private", false),
newTestCase(b[0], a, "/public/.././private", false),
newTestCase(b[0], a, "/public/%2E%2E/private", false),
newTestCase(b[0], a, "/public/%2e%2e/private", false),
newTestCase(b[0], a, "/public/%2E/%2E%2E/private", false),
newTestCase(b[0], a, "/public/%2e/%2e%2e/private", false),
newTestCase(b[0], a, "/public/%2E%2E/%2E/private", false),
newTestCase(b[0], a, "/public/%2e%2e/%2e/private", false),
}
for _, c := range cases {
c(t)
}
})
}
}
})
}
// TestAuthorization_Audit tests that the AUDIT action does not impact allowing or denying a request
func TestAuthorization_Audit(t *testing.T) {
framework.NewTest(t).
Run(func(t framework.TestContext) {
ns := apps.Namespace1
a := match.Namespace(ns).GetMatches(apps.A)
b := match.Namespace(ns).GetMatches(apps.B)
c := match.Namespace(ns).GetMatches(apps.C)
d := match.Namespace(ns).GetMatches(apps.D)
vm := match.Namespace(ns).GetMatches(apps.VM)
policy := func(filename string) func(t framework.TestContext) {
return func(t framework.TestContext) {
t.ConfigIstio().EvalFile(ns.Name(), map[string]string{
"b": b[0].Config().Service,
"c": c[0].Config().Service,
"d": d[0].Config().Service,
"Namespace": ns.Name(),
"RootNamespace": istio.GetOrFail(t, t).Settings().SystemNamespace,
}, filename).ApplyOrFail(t, resource.Wait)
}
}
vmPolicy := func(filename string) func(t framework.TestContext) {
return func(t framework.TestContext) {
t.ConfigIstio().EvalFile(ns.Name(), map[string]string{
"Namespace": ns.Name(),
"dst": vm[0].Config().Service,
}, filename).ApplyOrFail(t, resource.Wait)
}
}
newTestCase := func(applyPolicy func(t framework.TestContext), from echo.Instance, to echo.Target,
path string, expectAllowed bool) func(t framework.TestContext) {
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
HTTP: echo.HTTP{
Path: path,
},
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
applyPolicy(t)
from.CallOrFail(t, opts)
})
}
}
cases := []func(t framework.TestContext){
newTestCase(policy("testdata/authz/v1beta1-audit.yaml.tmpl"), a[0], b, "/allow", true),
newTestCase(policy("testdata/authz/v1beta1-audit.yaml.tmpl"), a[0], b, "/audit", false),
newTestCase(policy("testdata/authz/v1beta1-audit.yaml.tmpl"), a[0], c, "/audit", true),
newTestCase(policy("testdata/authz/v1beta1-audit.yaml.tmpl"), a[0], c, "/deny", false),
newTestCase(policy("testdata/authz/v1beta1-audit.yaml.tmpl"), a[0], d, "/audit", true),
newTestCase(policy("testdata/authz/v1beta1-audit.yaml.tmpl"), a[0], d, "/other", true),
// (TODO)JimmyCYJ: Support multiple VMs and apply audit policies to multiple VMs for testing.
// The tests below are duplicated from above for VM workloads. With support for multiple VMs,
// These tests will be merged to the tests above.
newTestCase(vmPolicy("testdata/authz/v1beta1-audit-allow.yaml.tmpl"), b[0], vm, "/allow", true),
newTestCase(vmPolicy("testdata/authz/v1beta1-audit-allow.yaml.tmpl"), b[0], vm, "/audit", false),
newTestCase(vmPolicy("testdata/authz/v1beta1-audit-deny.yaml.tmpl"), b[0], vm, "/audit", true),
newTestCase(vmPolicy("testdata/authz/v1beta1-audit-deny.yaml.tmpl"), b[0], vm, "/deny", false),
newTestCase(vmPolicy("testdata/authz/v1beta1-audit-default.yaml.tmpl"), b[0], vm, "/audit", true),
newTestCase(vmPolicy("testdata/authz/v1beta1-audit-default.yaml.tmpl"), b[0], vm, "/other", true),
}
for _, c := range cases {
c(t)
}
})
}
// TestAuthorization_Custom tests that the CUSTOM action with the sample ext_authz server.
func TestAuthorization_Custom(t *testing.T) {
framework.NewTest(t).
Features("security.authorization.custom").
Run(func(t framework.TestContext) {
ns := namespace.NewOrFail(t, t, namespace.Config{
Prefix: "v1beta1-custom",
Inject: true,
})
customAuthzYAML, err := readCustomAuthzYAML(t)
if err != nil {
t.Fatal(err)
}
// Deploy and wait for the ext-authz server to be ready.
t.ConfigIstio().YAML(ns.Name(), customAuthzYAML).ApplyOrFail(t)
if _, _, err := kube.WaitUntilServiceEndpointsAreReady(t.Clusters().Default(), ns.Name(), "ext-authz"); err != nil {
t.Fatalf("Wait for ext-authz server failed: %v", err)
}
// Update the mesh config extension provider for the ext-authz service.
extService := fmt.Sprintf("ext-authz.%s.svc.cluster.local", ns.Name())
extServiceWithNs := fmt.Sprintf("%s/%s", ns.Name(), extService)
istio.PatchMeshConfigOrFail(t, ist.Settings().SystemNamespace, t.Clusters(), fmt.Sprintf(`
extensionProviders:
- name: "ext-authz-http"
envoyExtAuthzHttp:
service: %q
port: 8000
pathPrefix: "/check"
headersToUpstreamOnAllow: ["x-ext-authz-*"]
headersToDownstreamOnDeny: ["x-ext-authz-*"]
includeRequestHeadersInCheck: ["x-ext-authz"]
includeAdditionalHeadersInCheck:
x-ext-authz-additional-header-new: additional-header-new-value
x-ext-authz-additional-header-override: additional-header-override-value
- name: "ext-authz-grpc"
envoyExtAuthzGrpc:
service: %q
port: 9000
- name: "ext-authz-http-local"
envoyExtAuthzHttp:
service: ext-authz-http.local
port: 8000
pathPrefix: "/check"
headersToUpstreamOnAllow: ["x-ext-authz-*"]
headersToDownstreamOnDeny: ["x-ext-authz-*"]
includeRequestHeadersInCheck: ["x-ext-authz"]
includeAdditionalHeadersInCheck:
x-ext-authz-additional-header-new: additional-header-new-value
x-ext-authz-additional-header-override: additional-header-override-value
- name: "ext-authz-grpc-local"
envoyExtAuthzGrpc:
service: ext-authz-grpc.local
port: 9000`, extService, extServiceWithNs))
t.ConfigIstio().EvalFile("", map[string]string{
"Namespace": ns.Name(),
"RootNamespace": istio.GetOrFail(t, t).Settings().SystemNamespace,
}, "testdata/authz/v1beta1-custom.yaml.tmpl").ApplyOrFail(t)
ports := []echo.Port{
{
Name: "tcp-8092",
Protocol: protocol.TCP,
WorkloadPort: 8092,
},
{
Name: "tcp-8093",
Protocol: protocol.TCP,
WorkloadPort: 8093,
},
{
Name: "http",
Protocol: protocol.HTTP,
WorkloadPort: 8090,
},
}
var a, b, c, d, e, f, g, x echo.Instance
echoConfig := func(name string, includeExtAuthz bool) echo.Config {
cfg := util.EchoConfig(name, ns, false, nil)
cfg.IncludeExtAuthz = includeExtAuthz
cfg.Ports = ports
return cfg
}
deployment.New(t).
With(&a, echoConfig("a", false)).
With(&b, echoConfig("b", false)).
With(&c, echoConfig("c", false)).
With(&d, echoConfig("d", true)).
With(&e, echoConfig("e", true)).
With(&f, echoConfig("f", false)).
With(&g, echoConfig("g", false)).
With(&x, echoConfig("x", false)).
BuildOrFail(t)
newTestCase := func(from echo.Instance, to echo.Target, s scheme.Instance, port, path string, headers http.Header,
checker echo.Checker, expectAllowed bool) func(t framework.TestContext) {
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: port,
},
Scheme: s,
HTTP: echo.HTTP{
Path: path,
Headers: headers,
},
}
opts.FillDefaultsOrFail(t)
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(opts.Port.Protocol)
}
opts.Check = check.And(opts.Check, checker)
name := newRbacTestName("", expectAllowed, from, &opts)
t.NewSubTest(name.String()).Run(func(t framework.TestContext) {
name.SkipIfNecessary(t)
from.CallOrFail(t, opts)
})
}
}
checkHTTPHeaders := func(hType echoClient.HeaderType) echo.Checker {
return check.And(
scheck.HeaderContains(hType, map[string][]string{
"X-Ext-Authz-Check-Received": {"additional-header-new-value", "additional-header-override-value"},
"X-Ext-Authz-Additional-Header-Override": {"additional-header-override-value"},
}),
scheck.HeaderNotContains(hType, map[string][]string{
"X-Ext-Authz-Check-Received": {"should-be-override"},
"X-Ext-Authz-Additional-Header-Override": {"should-be-override"},
}))
}
checkGRPCHeaders := func(hType echoClient.HeaderType) echo.Checker {
return check.And(
scheck.HeaderContains(hType, map[string][]string{
"X-Ext-Authz-Check-Received": {"should-be-override"},
"X-Ext-Authz-Additional-Header-Override": {"grpc-additional-header-override-value"},
}),
scheck.HeaderNotContains(hType, map[string][]string{
"X-Ext-Authz-Additional-Header-Override": {"should-be-override"},
}))
}
authzHeaders := func(h string) http.Header {
return headers.New().With("x-ext-authz", h).With("x-ext-authz-additional-header-override", "should-be-override").Build()
}
// Path "/custom" is protected by ext-authz service and is accessible with the header `x-ext-authz: allow`.
// Path "/health" is not protected and is accessible to public.
cases := []func(test framework.TestContext){
// workload b is using an ext-authz service in its own pod of HTTP API.
newTestCase(x, b, scheme.HTTP, "http", "/custom", authzHeaders("allow"), checkHTTPHeaders(echoClient.RequestHeader), true),
newTestCase(x, b, scheme.HTTP, "http", "/custom", authzHeaders("deny"), checkHTTPHeaders(echoClient.ResponseHeader), false),
newTestCase(x, b, scheme.HTTP, "http", "/health", authzHeaders("allow"), nil, true),
newTestCase(x, b, scheme.HTTP, "http", "/health", authzHeaders("deny"), nil, true),
// workload c is using an ext-authz service in its own pod of gRPC API.
newTestCase(x, c, scheme.HTTP, "http", "/custom", authzHeaders("allow"), checkGRPCHeaders(echoClient.RequestHeader), true),
newTestCase(x, c, scheme.HTTP, "http", "/custom", authzHeaders("deny"), checkGRPCHeaders(echoClient.ResponseHeader), false),
newTestCase(x, c, scheme.HTTP, "http", "/health", authzHeaders("allow"), nil, true),
newTestCase(x, c, scheme.HTTP, "http", "/health", authzHeaders("deny"), nil, true),
// workload d is using an local ext-authz service in the same pod as the application of HTTP API.
newTestCase(x, d, scheme.HTTP, "http", "/custom", authzHeaders("allow"), checkHTTPHeaders(echoClient.RequestHeader), true),
newTestCase(x, d, scheme.HTTP, "http", "/custom", authzHeaders("deny"), checkHTTPHeaders(echoClient.ResponseHeader), false),
newTestCase(x, d, scheme.HTTP, "http", "/health", authzHeaders("allow"), nil, true),
newTestCase(x, d, scheme.HTTP, "http", "/health", authzHeaders("deny"), nil, true),
// workload e is using an local ext-authz service in the same pod as the application of gRPC API.
newTestCase(x, e, scheme.HTTP, "http", "/custom", authzHeaders("allow"), checkGRPCHeaders(echoClient.RequestHeader), true),
newTestCase(x, e, scheme.HTTP, "http", "/custom", authzHeaders("deny"), checkGRPCHeaders(echoClient.ResponseHeader), false),
newTestCase(x, e, scheme.HTTP, "http", "/health", authzHeaders("allow"), nil, true),
newTestCase(x, e, scheme.HTTP, "http", "/health", authzHeaders("deny"), nil, true),
// workload f is using an ext-authz service in its own pod of TCP API.
newTestCase(a, f, scheme.TCP, "tcp-8092", "", authzHeaders(""), nil, true),
newTestCase(x, f, scheme.TCP, "tcp-8092", "", authzHeaders(""), nil, false),
newTestCase(a, f, scheme.TCP, "tcp-8093", "", authzHeaders(""), nil, true),
newTestCase(x, f, scheme.TCP, "tcp-8093", "", authzHeaders(""), nil, true),
}
for _, c := range cases {
c(t)
}
t.NewSubTest("ingress").Run(func(t framework.TestContext) {
ingr := ist.IngressFor(t.Clusters().Default())
newIngressTestCase := func(from, to echo.Instance, path string, h http.Header,
checker echo.Checker, expectAllowed bool) func(t framework.TestContext) {
return func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Protocol: protocol.HTTP,
},
Scheme: scheme.HTTP,
HTTP: echo.HTTP{
Path: path,
Headers: headers.New().
WithHost("www.company.com").
With("X-Ext-Authz", h.Get("x-ext-authz")).
Build(),
},
}
if expectAllowed {
opts.Check = check.And(check.OK(), scheck.ReachedClusters(t.AllClusters(), &opts))
} else {
opts.Check = check.Forbidden(protocol.HTTP)
}
opts.Check = check.And(opts.Check, checker)
name := fmt.Sprintf("%s->%s%s[%t]",
from.Config().Service,
to.Config().Service,
path,
expectAllowed)
t.NewSubTest(name).Run(func(t framework.TestContext) {
ingr.CallOrFail(t, opts)
})
}
}
ingressCases := []func(framework.TestContext){
// workload g is using an ext-authz service in its own pod of HTTP API.
newIngressTestCase(x, g, "/custom", authzHeaders("allow"), checkHTTPHeaders(echoClient.RequestHeader), true),
newIngressTestCase(x, g, "/custom", authzHeaders("deny"), checkHTTPHeaders(echoClient.ResponseHeader), false),
newIngressTestCase(x, g, "/health", authzHeaders("allow"), nil, true),
newIngressTestCase(x, g, "/health", authzHeaders("deny"), nil, true),
}
for _, c := range ingressCases {
c(t)
}
})
})
}
func readCustomAuthzYAML(ctx resource.Context) (string, error) {
// Read the samples file.
filePath := fmt.Sprintf("%s/samples/extauthz/ext-authz.yaml", env.IstioSrc)
data, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
yamlText := string(data)
// Replace the image.
s := ctx.Settings().Image
oldImage := "gcr.io/istio-testing/ext-authz:latest"
newImage := fmt.Sprintf("%s/ext-authz:%s", s.Hub, strings.TrimSuffix(s.Tag, "-distroless"))
yamlText = strings.ReplaceAll(yamlText, oldImage, newImage)
// Replace the image pull policy
oldPolicy := "IfNotPresent"
newPolicy := s.PullPolicy
yamlText = strings.ReplaceAll(yamlText, oldPolicy, newPolicy)
return yamlText, nil
}
type rbacTestName string
func (n rbacTestName) String() string {
return string(n)
}
func (n rbacTestName) SkipIfNecessary(t framework.TestContext) {
t.Helper()
if strings.Contains(n.String(), "source-ip") && t.Clusters().IsMulticluster() {
t.Skip("https://github.com/istio/istio/issues/37307: " +
"Current source ip based authz test cases are not required in multicluster setup because " +
"cross-network traffic will lose the origin source ip info")
}
}
func newRbacTestName(prefix string, expectAllowed bool, from echo.Instance, opts *echo.CallOptions) rbacTestName {
want := "deny"
if expectAllowed {
want = "allow"
}
return rbacTestName(fmt.Sprintf("%s%s->%s:%s%s[%s]",
prefix,
from.Config().Service,
opts.To.Config().Service,
opts.Port.Name,
opts.HTTP.Path,
want))
}