blob: bd03e52db9861cef88f9f6e8e8450542aa3d1b28 [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 (
"net/http"
"strings"
"testing"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/config/protocol"
"github.com/apache/dubbo-go-pixiu/pkg/http/headers"
"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/echotest"
"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/label"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/resource"
"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"
)
// TestRequestAuthentication tests beta authn policy for jwt.
func TestRequestAuthentication(t *testing.T) {
payload1 := strings.Split(jwt.TokenIssuer1, ".")[1]
payload2 := strings.Split(jwt.TokenIssuer2, ".")[1]
framework.NewTest(t).
Label(label.IPv4). // https://github.com/istio/istio/issues/35835
Features("security.authentication.jwt").
Run(func(t framework.TestContext) {
ns := apps.Namespace1
t.ConfigKube().EvalFile(ns.Name(), map[string]string{
"Namespace": ns.Name(),
}, "../../../samples/jwt-server/jwt-server.yaml").ApplyOrFail(t)
type testCase struct {
name string
customizeCall func(t resource.Context, from echo.Instance, opts *echo.CallOptions)
}
newTest := func(policy string, cases []testCase) func(framework.TestContext) {
return func(t framework.TestContext) {
echotest.New(t, apps.All).
SetupForDestination(func(t framework.TestContext, to echo.Target) error {
if policy != "" {
args := map[string]string{
"Namespace": ns.Name(),
"dst": to.Config().Service,
}
return t.ConfigIstio().EvalFile(ns.Name(), args, policy).Apply(resource.Wait)
}
return nil
}).
FromMatch(
// TODO(JimmyCYJ): enable VM for all test cases.
util.SourceMatcher(ns, true)).
ConditionallyTo(echotest.ReachableDestinations).
ToMatch(util.DestMatcher(ns, true)).
Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
for _, c := range cases {
t.NewSubTest(c.name).Run(func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
Count: callCount,
}
// Apply any custom options for the test.
c.customizeCall(t, from, &opts)
from.CallOrFail(t, opts)
})
}
})
}
}
t.NewSubTest("authn-only").Run(newTest("testdata/requestauthn/authn-only.yaml.tmpl", []testCase{
{
name: "valid-token-noauthz",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token-noauthz"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts),
check.RequestHeaders(map[string]string{
headers.Authorization: "",
"X-Test-Payload": payload1,
}))
},
},
{
name: "valid-token-2-noauthz",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token-2-noauthz"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer2).Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts),
check.RequestHeaders(map[string]string{
headers.Authorization: "",
"X-Test-Payload": payload2,
}))
},
},
{
name: "expired-token-noauthz",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/expired-token-noauthz"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenExpired).Build()
opts.Check = check.Status(http.StatusUnauthorized)
},
},
{
name: "expired-token-cors-preflight-request-allowed",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/expired-token-cors-preflight-request-allowed"
opts.HTTP.Method = "OPTIONS"
opts.HTTP.Headers = headers.New().
WithAuthz(jwt.TokenExpired).
With(headers.AccessControlRequestMethod, "POST").
With(headers.Origin, "https://istio.io").
Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
{
name: "expired-token-bad-cors-preflight-request-rejected",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/expired-token-cors-preflight-request-allowed"
opts.HTTP.Method = "OPTIONS"
opts.HTTP.Headers = headers.New().
WithAuthz(jwt.TokenExpired).
With(headers.AccessControlRequestMethod, "POST").
// the required Origin header is missing.
Build()
opts.Check = check.Status(http.StatusUnauthorized)
},
},
{
name: "no-token-noauthz",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/no-token-noauthz"
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
}))
t.NewSubTest("authn-authz").Run(newTest("testdata/requestauthn/authn-authz.yaml.tmpl", []testCase{
{
name: "valid-token",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts),
check.RequestHeader(headers.Authorization, ""))
},
},
{
name: "expired-token",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/expired-token"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenExpired).Build()
opts.Check = check.Status(http.StatusUnauthorized)
},
},
{
name: "no-token",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/no-token"
opts.Check = check.Status(http.StatusForbidden)
},
},
}))
t.NewSubTest("no-authn-authz").Run(newTest("", []testCase{
{
name: "no-authn-authz",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/no-authn-authz"
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
}))
t.NewSubTest("forward").Run(newTest("testdata/requestauthn/forward.yaml.tmpl", []testCase{
{
name: "valid-token-forward",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token-forward"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts),
check.RequestHeaders(map[string]string{
headers.Authorization: "Bearer " + jwt.TokenIssuer1,
"X-Test-Payload": payload1,
}))
},
},
}))
t.NewSubTest("remote").Run(newTest("testdata/requestauthn/remote.yaml.tmpl", []testCase{
{
name: "valid-token-forward-remote-jwks",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token-forward-remote-jwks"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts),
check.RequestHeaders(map[string]string{
headers.Authorization: "Bearer " + jwt.TokenIssuer1,
"X-Test-Payload": payload1,
}))
},
},
}))
t.NewSubTest("aud").Run(newTest("testdata/requestauthn/aud.yaml.tmpl", []testCase{
{
name: "invalid-aud",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-aud"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
opts.Check = check.Status(http.StatusForbidden)
},
},
{
name: "valid-aud",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-aud"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1WithAud).Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
{
name: "verify-policies-are-combined",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/verify-policies-are-combined"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer2).Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
}))
t.NewSubTest("invalid-jwks").Run(newTest("testdata/requestauthn/invalid-jwks.yaml.tmpl", []testCase{
{
name: "invalid-jwks-valid-token-noauthz",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = ""
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
opts.Check = check.Status(http.StatusUnauthorized)
},
},
{
name: "invalid-jwks-expired-token-noauthz",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/invalid-jwks-valid-token-noauthz"
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenExpired).Build()
opts.Check = check.Status(http.StatusUnauthorized)
},
},
{
name: "invalid-jwks-no-token-noauthz",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/invalid-jwks-no-token-noauthz"
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
}))
t.NewSubTest("headers-params").Run(newTest("testdata/requestauthn/headers-params.yaml.tmpl", []testCase{
{
name: "valid-params",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token?token=" + jwt.TokenIssuer1
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
{
name: "valid-params-secondary",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token?secondary_token=" + jwt.TokenIssuer1
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
{
name: "invalid-params",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token?token_value=" + jwt.TokenIssuer1
opts.Check = check.Status(http.StatusForbidden)
},
},
{
name: "valid-token-set",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token?token=" + jwt.TokenIssuer1 + "&secondary_token=" + jwt.TokenIssuer1
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
{
name: "invalid-token-set",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = "/valid-token?token=" + jwt.TokenIssuer1 + "&secondary_token=" + jwt.TokenExpired
opts.Check = check.Status(http.StatusUnauthorized)
},
},
{
name: "valid-header",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = ""
opts.HTTP.Headers = headers.New().
With("X-Jwt-Token", "Value "+jwt.TokenIssuer1).
Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
{
name: "valid-header-secondary",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = ""
opts.HTTP.Headers = headers.New().
With("Auth-Token", "Token "+jwt.TokenIssuer1).
Build()
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
{
name: "invalid-header",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Path = ""
opts.HTTP.Headers = headers.New().
With("Auth-Header-Param", "Bearer "+jwt.TokenIssuer1).
Build()
opts.Check = check.Status(http.StatusForbidden)
},
},
}))
})
}
// TestIngressRequestAuthentication tests beta authn policy for jwt on ingress.
// The policy is also set at global namespace, with authorization on ingressgateway.
func TestIngressRequestAuthentication(t *testing.T) {
framework.NewTest(t).
Label(label.IPv4). // https://github.com/istio/istio/issues/35835
Features("security.authentication.ingressjwt").
Run(func(t framework.TestContext) {
ns := apps.Namespace1
// Apply the policy.
t.ConfigIstio().EvalFile(newRootNS(t).Name(), map[string]string{
"Namespace": ns.Name(),
"RootNamespace": istio.GetOrFail(t, t).Settings().SystemNamespace,
}, "testdata/requestauthn/global-jwt.yaml.tmpl").ApplyOrFail(t, resource.Wait)
type testCase struct {
name string
customizeCall func(resource.Context, echo.Instance, *echo.CallOptions)
}
newTest := func(policy string, cases []testCase) func(framework.TestContext) {
return func(t framework.TestContext) {
echotest.New(t, apps.All).
SetupForDestination(func(t framework.TestContext, to echo.Target) error {
if policy != "" {
args := map[string]string{
"Namespace": ns.Name(),
"dst": to.Config().Service,
}
return t.ConfigIstio().EvalFile(ns.Name(), args, policy).Apply(resource.Wait)
}
return nil
}).
FromMatch(util.SourceMatcher(ns, false)).
ConditionallyTo(echotest.ReachableDestinations).
ConditionallyTo(func(from echo.Instance, to echo.Instances) echo.Instances {
return match.Cluster(from.Config().Cluster).GetMatches(to)
}).
ToMatch(util.DestMatcher(ns, false)).
Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
callCount := util.CallsPerCluster * to.WorkloadsOrFail(t).Len()
for _, c := range cases {
t.NewSubTest(c.name).Run(func(t framework.TestContext) {
opts := echo.CallOptions{
To: to,
Port: echo.Port{
Name: "http",
},
Count: callCount,
}
// Apply any custom options for the test.
c.customizeCall(t, from, &opts)
from.CallOrFail(t, opts)
})
}
})
}
}
t.NewSubTest("in-mesh-authn").Run(newTest("testdata/requestauthn/ingress.yaml.tmpl", []testCase{
{
name: "in-mesh-with-expired-token",
customizeCall: func(_ resource.Context, _ echo.Instance, opts *echo.CallOptions) {
opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenExpired).Build()
opts.Check = check.Status(http.StatusUnauthorized)
},
},
{
name: "in-mesh-without-token",
customizeCall: func(t resource.Context, from echo.Instance, opts *echo.CallOptions) {
opts.Check = check.And(
check.OK(),
scheck.ReachedClusters(t.AllClusters(), opts))
},
},
}))
t.NewSubTest("ingress-authn").Run(func(t framework.TestContext) {
// TODO(JimmyCYJ): add workload-agnostic test pattern to support ingress gateway tests.
t.ConfigIstio().EvalFile(ns.Name(), map[string]string{
"Namespace": ns.Name(),
"dst": util.BSvc,
}, "testdata/requestauthn/ingress.yaml.tmpl").ApplyOrFail(t)
for _, cluster := range t.Clusters() {
ingr := ist.IngressFor(cluster)
// These test cases verify requests go through ingress will be checked for validate token.
ingTestCases := []struct {
name string
customizeCall func(opts *echo.CallOptions)
}{
{
name: "deny without token",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/"
opts.HTTP.Headers = headers.New().WithHost("example.com").Build()
opts.Check = check.Status(http.StatusForbidden)
},
},
{
name: "allow with sub-1 token",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/"
opts.HTTP.Headers = headers.New().
WithHost("example.com").
WithAuthz(jwt.TokenIssuer1).
Build()
opts.Check = check.OK()
},
},
{
name: "deny with sub-2 token",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/"
opts.HTTP.Headers = headers.New().
WithHost("example.com").
WithAuthz(jwt.TokenIssuer2).
Build()
opts.Check = check.Status(http.StatusForbidden)
},
},
{
name: "deny with expired token",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/"
opts.HTTP.Headers = headers.New().
WithHost("example.com").
WithAuthz(jwt.TokenExpired).
Build()
opts.Check = check.Status(http.StatusUnauthorized)
},
},
{
name: "allow with sub-1 token on any.com",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/"
opts.HTTP.Headers = headers.New().
WithHost("any-request-principlal-ok.com").
WithAuthz(jwt.TokenIssuer1).
Build()
opts.Check = check.OK()
},
},
{
name: "allow with sub-2 token on any.com",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/"
opts.HTTP.Headers = headers.New().
WithHost("any-request-principlal-ok.com").
WithAuthz(jwt.TokenIssuer2).
Build()
opts.Check = check.OK()
},
},
{
name: "deny without token on any.com",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/"
opts.HTTP.Headers = headers.New().
WithHost("any-request-principlal-ok.com").
Build()
opts.Check = check.Status(http.StatusForbidden)
},
},
{
name: "deny with token on other host",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/"
opts.HTTP.Headers = headers.New().
WithHost("other-host.com").
WithAuthz(jwt.TokenIssuer1).
Build()
opts.Check = check.Status(http.StatusForbidden)
},
},
{
name: "allow healthz",
customizeCall: func(opts *echo.CallOptions) {
opts.HTTP.Path = "/healthz"
opts.HTTP.Headers = headers.New().
WithHost("example.com").
Build()
opts.Check = check.OK()
},
},
}
for _, c := range ingTestCases {
t.NewSubTest(c.name).Run(func(t framework.TestContext) {
opts := echo.CallOptions{
Port: echo.Port{
Protocol: protocol.HTTP,
},
}
c.customizeCall(&opts)
ingr.CallOrFail(t, opts)
})
}
}
})
})
}