Skip to content

Commit

Permalink
Add integration tests for RBAC v2 (#13353)
Browse files Browse the repository at this point in the history
* Implement RBAC v2 intergration test

* Add Galley to app for security tests
  • Loading branch information
pitlv2109 authored and istio-testing committed Apr 16, 2019
1 parent 9767831 commit 770ddce
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 6 deletions.
4 changes: 4 additions & 0 deletions pkg/test/framework/components/apps/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const (
AppProtocolHTTP = "http"
// AppProtocolGRPC calls the app with GRPC
AppProtocolGRPC = "grpc"
// AppProtocolHTTP calls the app with TCP
AppProtocolTCP = "tcp"
// AppProtocolWebSocket calls the app with WebSocket
AppProtocolWebSocket = "ws"
)
Expand Down Expand Up @@ -106,6 +108,8 @@ type AppCallOptions struct {

// UseShortHostname indicates whether shortened hostnames should be used. This may be ignored by the environment.
UseShortHostname bool

Path string
}

// AppEndpoint represents a single endpoint in a DeployedApp.
Expand Down
3 changes: 2 additions & 1 deletion pkg/test/framework/components/apps/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ func (e *endpoint) makeURL(opts AppCallOptions) *url.URL {
switch protocol {
case AppProtocolHTTP:
case AppProtocolGRPC:
case AppProtocolTCP:
case AppProtocolWebSocket:
default:
protocol = string(AppProtocolHTTP)
Expand All @@ -480,6 +481,7 @@ func (e *endpoint) makeURL(opts AppCallOptions) *url.URL {
return &url.URL{
Scheme: protocol,
Host: net.JoinHostPort(host, strconv.Itoa(e.networkEndpoint.ServicePort.Port)),
Path: opts.Path,
}
}

Expand Down Expand Up @@ -703,7 +705,6 @@ type deploymentFactory struct {
}

func (d *deploymentFactory) newDeployment(e *kube.Environment, namespace namespace.Instance) (*deployment.Instance, error) {

s, err := deployment2.SettingsFromCommandLine()
if err != nil {
return nil, err
Expand Down
2 changes: 2 additions & 0 deletions pkg/test/framework/components/apps/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ func (e *nativeEndpoint) makeURL(opts AppCallOptions) *url.URL {
switch protocol {
case AppProtocolHTTP:
case AppProtocolGRPC:
case AppProtocolTCP:
case AppProtocolWebSocket:
default:
protocol = string(AppProtocolHTTP)
Expand All @@ -458,5 +459,6 @@ func (e *nativeEndpoint) makeURL(opts AppCallOptions) *url.URL {
return &url.URL{
Scheme: protocol,
Host: net.JoinHostPort(localhost, strconv.Itoa(e.port.ProxyPort)),
Path: opts.Path,
}
}
3 changes: 2 additions & 1 deletion pkg/test/util/connection/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Connection struct {
To apps.KubeApp
Protocol apps.AppProtocol
Port int
Path string
ExpectedSuccess bool
}

Expand All @@ -35,7 +36,7 @@ func CheckConnection(t *testing.T, conn Connection) error {
return fmt.Errorf("cannot get upstream endpoint for connection test %v", conn)
}

results, err := conn.From.Call(ep, apps.AppCallOptions{Protocol: conn.Protocol})
results, err := conn.From.Call(ep, apps.AppCallOptions{Protocol: conn.Protocol, Path: conn.Path})
if conn.ExpectedSuccess {
if err != nil || len(results) == 0 || results[0].Code != "200" {
// Addition log for debugging purpose.
Expand Down
18 changes: 17 additions & 1 deletion pkg/test/util/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,16 @@ func (p TestPolicy) TearDown() {
}
}

// ApplyPolicyFile applies a policy file from testdata directory of the test.
func ApplyPolicyFile(t *testing.T, env *kube.Environment, namespace string, fileName string) *TestPolicy {
joinedPath := path.Join(testDataDir, fileName)
return ApplyPolicyAnyFilePath(t, env, namespace, joinedPath)
}

// ApplyPolicyAnyFilePath runs `kubectl -f <namespace> <fileName>` where fileName is the real/explicit path of the file.
func ApplyPolicyAnyFilePath(t *testing.T, env *kube.Environment, namespace string, fileName string) *TestPolicy {
scopes.CI.Infof("Applying policy file %v", fileName)
if err := env.Apply(namespace, path.Join(testDataDir, fileName)); err != nil {
if err := env.Apply(namespace, fileName); err != nil {
t.Fatalf("Cannot apply %q to namespace %q: %v", fileName, namespace, err)
return nil
}
Expand All @@ -56,3 +63,12 @@ func ApplyPolicyFile(t *testing.T, env *kube.Environment, namespace string, file
fileName: fileName,
}
}

// ApplyPolicyFiles applies multiple policy files to the same namespace
func ApplyPolicyFiles(t *testing.T, env *kube.Environment, namespace string, fileNames []string) []*TestPolicy {
var testPolicies []*TestPolicy
for _, fileName := range fileNames {
testPolicies = append(testPolicies, ApplyPolicyAnyFilePath(t, env, namespace, fileName))
}
return testPolicies
}
147 changes: 147 additions & 0 deletions tests/integration/security/rbac/rbac_v2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2019 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 rbac

import (
"testing"
"time"

"istio.io/istio/pkg/test/framework/components/galley"
"istio.io/istio/pkg/test/framework/components/pilot"

"istio.io/istio/pkg/test/framework"
"istio.io/istio/pkg/test/framework/components/apps"
"istio.io/istio/pkg/test/framework/components/environment"
"istio.io/istio/pkg/test/framework/components/environment/kube"
"istio.io/istio/pkg/test/framework/components/istio"
"istio.io/istio/pkg/test/util/connection"
"istio.io/istio/pkg/test/util/policy"
"istio.io/istio/pkg/test/util/retry"
"istio.io/istio/tests/util"
)

const (
rbacClusterConfigTmpl = "testdata/istio-clusterrbacconfig.yaml.tmpl"
rbacV2RulesTmpl = "testdata/istio-rbac-v2-rules.yaml.tmpl"
)

var (
inst istio.Instance
successIfAuthEnabled bool
)

func TestMain(m *testing.M) {
framework.Main("rbac_v2_test", m, istio.SetupOnKube(&inst, setupConfig))
}

func setupConfig(cfg *istio.Config) {
if cfg == nil {
return
}
//cfg.Values["global.mtls.enabled"] = "true"
if cfg.IsMtlsEnabled() {
successIfAuthEnabled = true
} else {
successIfAuthEnabled = false
}
cfg.Values["sidecarInjectorWebhook.rewriteAppHTTPProbe"] = "true"
}

func TestRBACV2(t *testing.T) {
ctx := framework.NewContext(t)
defer ctx.Done(t)
ctx.RequireOrSkip(t, environment.Kube)

env := ctx.Environment().(*kube.Environment)
g := galley.NewOrFail(t, ctx, galley.Config{})
p := pilot.NewOrFail(t, ctx, pilot.Config{
Galley: g,
})
appInst := apps.NewOrFail(t, ctx, apps.Config{Pilot: p, Galley: g})

appA, _ := appInst.GetAppOrFail("a", t).(apps.KubeApp)
appB, _ := appInst.GetAppOrFail("b", t).(apps.KubeApp)
appC, _ := appInst.GetAppOrFail("c", t).(apps.KubeApp)
appD, _ := appInst.GetAppOrFail("d", t).(apps.KubeApp)

connections := []connection.Connection{
// Port 80 is where HTTP is served, 90 is where TCP is served.
// In RBAC, ExpectedSuccess = true means that the client receives a valid, non-RBAC denied response from the server.
{To: appA, From: appB, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/xyz", ExpectedSuccess: false},
{To: appA, From: appB, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},
{To: appA, From: appC, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/", ExpectedSuccess: false},
{To: appA, From: appC, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},
{To: appA, From: appD, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/", ExpectedSuccess: false},
{To: appA, From: appD, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},

{To: appB, From: appA, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/xyz", ExpectedSuccess: successIfAuthEnabled},
{To: appB, From: appA, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/secret", ExpectedSuccess: false},
{To: appB, From: appA, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: successIfAuthEnabled},
{To: appB, From: appC, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/", ExpectedSuccess: successIfAuthEnabled},
{To: appB, From: appC, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: successIfAuthEnabled},
{To: appB, From: appD, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/", ExpectedSuccess: successIfAuthEnabled},
{To: appB, From: appD, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: successIfAuthEnabled},

{To: appC, From: appA, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/", ExpectedSuccess: false},
{To: appC, From: appA, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/secrets/admin", ExpectedSuccess: false},
{To: appC, From: appA, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},
{To: appC, From: appB, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/", ExpectedSuccess: false},
{To: appC, From: appB, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/credentials/admin", ExpectedSuccess: false},
{To: appC, From: appB, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},
{To: appC, From: appD, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/", ExpectedSuccess: successIfAuthEnabled},
{To: appC, From: appD, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/any_path/admin", ExpectedSuccess: false},
{To: appC, From: appD, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},

{To: appD, From: appA, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/xyz", ExpectedSuccess: true},
{To: appD, From: appA, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},
{To: appD, From: appB, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/", ExpectedSuccess: true},
{To: appD, From: appB, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},
{To: appD, From: appC, Port: 80, Protocol: apps.AppProtocolHTTP, Path: "/any_path", ExpectedSuccess: true},
{To: appD, From: appC, Port: 90, Protocol: apps.AppProtocolTCP, ExpectedSuccess: false},
}

testDir := ctx.WorkDir()
testNameSpace := appInst.Namespace().Name()
rbacYamlFiles := getRbacYamlFiles(t, testDir, testNameSpace)

policy.ApplyPolicyFiles(t, env, testNameSpace, rbacYamlFiles)

// Sleep 5 seconds for the policy to take effect.
// TODO(pitlv2109: Check to make sure policies have been created instead.
time.Sleep(5 * time.Second)
for _, conn := range connections {
retry.UntilSuccessOrFail(t, func() error {
return connection.CheckConnection(t, conn)
}, retry.Delay(time.Second), retry.Timeout(10*time.Second))
}
}

// getRbacYamlFiles fills the template RBAC policy files with the given namespace, writes the files to outDir
// and return the list of file paths.
func getRbacYamlFiles(t *testing.T, outDir, namespace string) []string {
var rbacYamlFiles []string
namespaceParams := map[string]string{
"Namespace": namespace,
}
rbacTmplFiles := []string{rbacClusterConfigTmpl, rbacV2RulesTmpl}
for _, rbacTmplFile := range rbacTmplFiles {
yamlFile, err := util.CreateAndFill(outDir, rbacTmplFile, namespaceParams)
if err != nil {
t.Fatalf("Cannot create and fill %v", rbacTmplFile)
}
rbacYamlFiles = append(rbacYamlFiles, yamlFile)
}
return rbacYamlFiles
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# istio-rbac-enable.yaml to enable istio RBAC
apiVersion: "rbac.istio.io/v1alpha1"
kind: ClusterRbacConfig
metadata:
name: default
spec:
mode: 'ON_WITH_INCLUSION'
inclusion:
namespaces: ["{{ .Namespace }}"]
---
129 changes: 129 additions & 0 deletions tests/integration/security/rbac/testdata/istio-rbac-v2-rules.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# istio-rbac-v2-rules.yaml to enforce access control for both http and tcp services using Istio RBAC v2 rules.

# For service a, allow no one to access it.
# This will also result a default tcp rule for service a that denies all access.
# This actually means nobody could access a.
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
name: access-a-http
spec:
rules:
- methods: ["GET"]
constraints:
- key: "destination.labels[app]"
values: ["a"]
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: AuthorizationPolicy
metadata:
name: authz-policy-access-a-http
spec:
allow:
- subjects:
- not_names: ["allUsers"]
roleRef:
kind: ServiceRole
name: "access-a-http"
---

# For service b, only allow authenticated user to access it with GET at any paths except /secret*
# or only access it at tcp port 90.
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
name: access-b-http-tcp
spec:
rules:
- methods: ["GET"]
not_paths: ["/secret*"]
- constraints:
- key: "destination.port"
values: ["90"]
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: AuthorizationPolicy
metadata:
name: authz-policy-access-b-http-tcp
spec:
workload_selector:
labels:
app: b
allow:
- subjects:
- names: ["allAuthenticatedUsers"]
roleRef:
kind: ServiceRole
name: "access-b-http-tcp"
---

# For service c:
# * Allow GET requests for any path, except paths ending with /admin for service account d.
# * Deny all requests for service account b.
# * Deny anyone else by default.
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
name: deny-get-at-admin-prefix-path-rules
spec:
rules:
- methods: ["GET"]
not_paths: ["*/admin"]
paths: ["*"]
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
name: deny-all-access-rules
spec:
rules:
- not_methods: ["*"]
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: AuthorizationPolicy
metadata:
name: authz-policy-c
spec:
workload_selector:
labels:
app: c
allow:
- subjects:
- names: ["cluster.local/ns/{{ .Namespace }}/sa/d"]
roleRef:
kind: ServiceRole
name: "deny-get-at-admin-prefix-path-rules"
- subjects:
- names: ["cluster.local/ns/{{ .Namespace }}/sa/b"]
roleRef:
kind: ServiceRole
name: "deny-all-access-rules"
---

# For service d:
# * anyone can access it via HTTP requests
# * no one can access TCP at port 90.
apiVersion: "rbac.istio.io/v1alpha1"
kind: ServiceRole
metadata:
name: access-d-http-tcp
spec:
rules:
- methods: ["*"]
- not_ports: [90]
---
apiVersion: "rbac.istio.io/v1alpha1"
kind: AuthorizationPolicy
metadata:
name: authz-policy-access-d-http-tcp
spec:
workload_selector:
labels:
app: d
allow:
- subjects:
- names: ["allUsers"]
roleRef:
kind: ServiceRole
name: "access-d-http-tcp"
---
Loading

0 comments on commit 770ddce

Please sign in to comment.