diff --git a/test/integration/cli_test.go b/test/integration/cli_test.go new file mode 100644 index 00000000..968af4e9 --- /dev/null +++ b/test/integration/cli_test.go @@ -0,0 +1,91 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ +package integration + +import ( + "context" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/suzerain-io/pinniped/test/library" +) + +func TestCLI(t *testing.T) { + library.SkipUnlessIntegration(t) + library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable) + token := library.GetEnv(t, "PINNIPED_TEST_USER_TOKEN") + namespaceName := library.GetEnv(t, "PINNIPED_NAMESPACE") + testUsername := library.GetEnv(t, "PINNIPED_TEST_USER_USERNAME") + expectedTestUserGroups := strings.Split( + strings.ReplaceAll(library.GetEnv(t, "PINNIPED_TEST_USER_GROUPS"), " ", ""), ",", + ) + + // Build pinniped CLI. + pinnipedExe, cleanupFunc := buildPinnipedCLI(t) + defer cleanupFunc() + + // Run pinniped CLI to get kubeconfig. + kubeConfig := runPinnipedCLI(t, pinnipedExe, token, namespaceName) + + // Create Kubernetes client with kubeconfig from pinniped CLI. + kubeClient := library.NewClientsetForKubeConfig(t, kubeConfig) + + // Validate that we can auth to the API via our user. + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*3) + defer cancelFunc() + + adminClient := library.NewClientset(t) + + t.Run("access as user", accessAsUserTest(ctx, adminClient, testUsername, kubeClient)) + for _, group := range expectedTestUserGroups { + group := group + t.Run( + "access as group "+group, + accessAsGroupTest(ctx, adminClient, group, kubeClient), + ) + } +} + +func buildPinnipedCLI(t *testing.T) (string, func()) { + t.Helper() + + pinnipedExeDir, err := ioutil.TempDir("", "pinniped-cli-test-*") + require.NoError(t, err) + + pinnipedExe := filepath.Join(pinnipedExeDir, "pinniped") + output, err := exec.Command( + "go", + "build", + "-o", + pinnipedExe, + "github.com/suzerain-io/pinniped/cmd/pinniped", + ).CombinedOutput() + require.NoError(t, err, string(output)) + + return pinnipedExe, func() { + require.NoError(t, os.RemoveAll(pinnipedExeDir)) + } +} + +func runPinnipedCLI(t *testing.T, pinnipedExe, token, namespaceName string) string { + t.Helper() + + output, err := exec.Command( + pinnipedExe, + "get-kubeconfig", + "--token", token, + "--pinniped-namespace", namespaceName, + ).CombinedOutput() + require.NoError(t, err, string(output)) + + return string(output) +} diff --git a/test/integration/common_test.go b/test/integration/common_test.go new file mode 100644 index 00000000..abd35975 --- /dev/null +++ b/test/integration/common_test.go @@ -0,0 +1,102 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ +package integration + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// accessAsUserTest runs a generic test in which a clientUnderTest operating with username +// testUsername tries to auth to the kube API (i.e., list namespaces). +// +// Use this function if you want to simply validate that a user can auth to the kube API after +// performing a Pinniped credential exchange. +func accessAsUserTest( + ctx context.Context, + adminClient kubernetes.Interface, + testUsername string, + clientUnderTest kubernetes.Interface, +) func(t *testing.T) { + return func(t *testing.T) { + addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "integration-test-user-readonly-role-binding", + }, + Subjects: []rbacv1.Subject{{ + Kind: rbacv1.UserKind, + APIGroup: rbacv1.GroupName, + Name: testUsername, + }}, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + APIGroup: rbacv1.GroupName, + Name: "view", + }, + }) + + // Use the client which is authenticated as the test user to list namespaces + var listNamespaceResponse *v1.NamespaceList + var err error + var canListNamespaces = func() bool { + listNamespaceResponse, err = clientUnderTest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + return err == nil + } + assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond) + require.NoError(t, err) // prints out the error and stops the test in case of failure + require.NotEmpty(t, listNamespaceResponse.Items) + } +} + +// accessAsGroupTest runs a generic test in which a clientUnderTest with membership in group +// testGroup tries to auth to the kube API (i.e., list namespaces). +// +// Use this function if you want to simply validate that a user can auth to the kube API (via +// a group membership) after performing a Pinniped credential exchange. +func accessAsGroupTest( + ctx context.Context, + adminClient kubernetes.Interface, + testGroup string, + clientUnderTest kubernetes.Interface, +) func(t *testing.T) { + return func(t *testing.T) { + addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "integration-test-group-readonly-role-binding", + }, + Subjects: []rbacv1.Subject{{ + Kind: rbacv1.GroupKind, + APIGroup: rbacv1.GroupName, + Name: testGroup, + }}, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + APIGroup: rbacv1.GroupName, + Name: "view", + }, + }) + + // Use the client which is authenticated as the test user to list namespaces + var listNamespaceResponse *v1.NamespaceList + var err error + var canListNamespaces = func() bool { + listNamespaceResponse, err = clientUnderTest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + return err == nil + } + assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond) + require.NoError(t, err) // prints out the error and stops the test in case of failure + require.NotEmpty(t, listNamespaceResponse.Items) + } +} diff --git a/test/integration/credentialrequest_test.go b/test/integration/credentialrequest_test.go index 28557858..ba8bd773 100644 --- a/test/integration/credentialrequest_test.go +++ b/test/integration/credentialrequest_test.go @@ -14,9 +14,7 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -61,65 +59,16 @@ func TestSuccessfulCredentialRequest(t *testing.T) { response.Status.Credential.ClientKeyData, ) - t.Run("access as user", func(t *testing.T) { - addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: "integration-test-user-readonly-role-binding", - }, - Subjects: []rbacv1.Subject{{ - Kind: rbacv1.UserKind, - APIGroup: rbacv1.GroupName, - Name: testUsername, - }}, - RoleRef: rbacv1.RoleRef{ - Kind: "ClusterRole", - APIGroup: rbacv1.GroupName, - Name: "view", - }, - }) - - // Use the client which is authenticated as the test user to list namespaces - var listNamespaceResponse *v1.NamespaceList - var canListNamespaces = func() bool { - listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) - return err == nil - } - assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond) - require.NoError(t, err) // prints out the error and stops the test in case of failure - require.NotEmpty(t, listNamespaceResponse.Items) - }) - + t.Run( + "access as user", + accessAsUserTest(ctx, adminClient, testUsername, clientWithCertFromCredentialRequest), + ) for _, group := range expectedTestUserGroups { group := group - t.Run("access as group "+group, func(t *testing.T) { - addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: "integration-test-group-readonly-role-binding", - }, - Subjects: []rbacv1.Subject{{ - Kind: rbacv1.GroupKind, - APIGroup: rbacv1.GroupName, - Name: group, - }}, - RoleRef: rbacv1.RoleRef{ - Kind: "ClusterRole", - APIGroup: rbacv1.GroupName, - Name: "view", - }, - }) - - // Use the client which is authenticated as the test user to list namespaces - var listNamespaceResponse *v1.NamespaceList - var canListNamespaces = func() bool { - listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) - return err == nil - } - assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond) - require.NoError(t, err) // prints out the error and stops the test in case of failure - require.NotEmpty(t, listNamespaceResponse.Items) - }) + t.Run( + "access as group "+group, + accessAsGroupTest(ctx, adminClient, group, clientWithCertFromCredentialRequest), + ) } } diff --git a/test/library/client.go b/test/library/client.go index a7e895db..1eb13328 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -36,6 +36,22 @@ func NewClientset(t *testing.T) kubernetes.Interface { return newClientsetWithConfig(t, NewClientConfig(t)) } +func NewClientsetForKubeConfig(t *testing.T, kubeConfig string) kubernetes.Interface { + t.Helper() + + kubeConfigFile, err := ioutil.TempFile("", "pinniped-cli-test-*") + require.NoError(t, err) + defer os.Remove(kubeConfigFile.Name()) + + _, err = kubeConfigFile.Write([]byte(kubeConfig)) + require.NoError(t, err) + + restConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigFile.Name()) + require.NoError(t, err) + + return newClientsetWithConfig(t, restConfig) +} + func NewClientsetWithCertAndKey(t *testing.T, clientCertificateData, clientKeyData string) kubernetes.Interface { t.Helper()