2021-02-03 19:32:29 +00:00
|
|
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
2021-01-22 18:00:27 +00:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package integration
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
2021-02-23 01:23:11 +00:00
|
|
|
v1 "k8s.io/api/authorization/v1"
|
2021-01-22 18:00:27 +00:00
|
|
|
corev1 "k8s.io/api/core/v1"
|
2021-02-23 01:23:11 +00:00
|
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
2021-01-22 18:00:27 +00:00
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2021-02-23 01:23:11 +00:00
|
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
|
|
k8sinformers "k8s.io/client-go/informers"
|
2021-01-22 18:00:27 +00:00
|
|
|
"k8s.io/client-go/kubernetes"
|
|
|
|
"k8s.io/client-go/rest"
|
2021-02-12 01:22:47 +00:00
|
|
|
"sigs.k8s.io/yaml"
|
2021-01-22 18:00:27 +00:00
|
|
|
|
2021-02-12 01:22:47 +00:00
|
|
|
"go.pinniped.dev/internal/concierge/impersonator"
|
2021-02-15 23:00:10 +00:00
|
|
|
"go.pinniped.dev/internal/testutil/impersonationtoken"
|
2021-01-22 18:00:27 +00:00
|
|
|
"go.pinniped.dev/test/library"
|
|
|
|
)
|
|
|
|
|
2021-02-18 23:58:27 +00:00
|
|
|
// TODO don't hard code "pinniped-concierge-" in this string. It should be constructed from the env app name.
|
|
|
|
const impersonationProxyConfigMapName = "pinniped-concierge-impersonation-proxy-config"
|
|
|
|
|
2021-01-22 18:00:27 +00:00
|
|
|
func TestImpersonationProxy(t *testing.T) {
|
|
|
|
env := library.IntegrationEnv(t)
|
|
|
|
if env.Proxy == "" {
|
|
|
|
t.Skip("this test can only run in environments with the in-cluster proxy right now")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-12 01:22:47 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
2021-01-22 18:00:27 +00:00
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// Create a client using the admin kubeconfig.
|
2021-02-12 01:22:47 +00:00
|
|
|
adminClient := library.NewKubernetesClientset(t)
|
2021-01-22 18:00:27 +00:00
|
|
|
|
|
|
|
// Create a WebhookAuthenticator.
|
|
|
|
authenticator := library.CreateTestWebhookAuthenticator(ctx, t)
|
|
|
|
|
2021-02-12 01:22:47 +00:00
|
|
|
// The address of the ClusterIP service that points at the impersonation proxy's port
|
2021-01-22 18:00:27 +00:00
|
|
|
proxyServiceURL := fmt.Sprintf("https://%s-proxy.%s.svc.cluster.local", env.ConciergeAppName, env.ConciergeNamespace)
|
|
|
|
t.Logf("making kubeconfig that points to %q", proxyServiceURL)
|
|
|
|
|
|
|
|
kubeconfig := &rest.Config{
|
|
|
|
Host: proxyServiceURL,
|
|
|
|
TLSClientConfig: rest.TLSClientConfig{Insecure: true},
|
2021-02-15 23:00:10 +00:00
|
|
|
BearerToken: impersonationtoken.Make(t, env.TestUser.Token, &authenticator, env.APIGroupSuffix),
|
2021-01-22 18:00:27 +00:00
|
|
|
Proxy: func(req *http.Request) (*url.URL, error) {
|
|
|
|
proxyURL, err := url.Parse(env.Proxy)
|
|
|
|
require.NoError(t, err)
|
|
|
|
t.Logf("passing request for %s through proxy %s", req.URL, proxyURL.String())
|
|
|
|
return proxyURL, nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-02-12 01:22:47 +00:00
|
|
|
impersonationProxyClient, err := kubernetes.NewForConfig(kubeconfig)
|
2021-01-22 18:00:27 +00:00
|
|
|
require.NoError(t, err, "unexpected failure from kubernetes.NewForConfig()")
|
|
|
|
|
2021-02-18 23:58:27 +00:00
|
|
|
oldConfigMap, err := adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Get(ctx, impersonationProxyConfigMapName, metav1.GetOptions{})
|
2021-02-17 18:32:29 +00:00
|
|
|
if oldConfigMap.Data != nil {
|
2021-02-18 23:58:27 +00:00
|
|
|
t.Logf("stashing a pre-existing configmap %s", oldConfigMap.Name)
|
|
|
|
require.NoError(t, adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Delete(ctx, impersonationProxyConfigMapName, metav1.DeleteOptions{}))
|
2021-02-17 18:32:29 +00:00
|
|
|
}
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-02-15 23:04:12 +00:00
|
|
|
serviceUnavailableError := fmt.Sprintf(`Get "%s/api/v1/namespaces": Service Unavailable`, proxyServiceURL)
|
2021-02-12 01:22:47 +00:00
|
|
|
if env.HasCapability(library.HasExternalLoadBalancerProvider) {
|
|
|
|
// Check that load balancer has been created
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
return hasLoadBalancerService(ctx, t, adminClient, env.ConciergeNamespace)
|
|
|
|
}, 10*time.Second, 500*time.Millisecond)
|
|
|
|
} else {
|
|
|
|
// Check that no load balancer has been created
|
|
|
|
require.Never(t, func() bool {
|
|
|
|
return hasLoadBalancerService(ctx, t, adminClient, env.ConciergeNamespace)
|
|
|
|
}, 10*time.Second, 500*time.Millisecond)
|
|
|
|
|
|
|
|
// Check that we can't use the impersonation proxy to execute kubectl commands yet
|
|
|
|
_, err = impersonationProxyClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
2021-02-15 23:04:12 +00:00
|
|
|
require.EqualError(t, err, serviceUnavailableError)
|
2021-02-12 01:22:47 +00:00
|
|
|
|
|
|
|
// Create configuration to make the impersonation proxy turn on with a hard coded endpoint (without a LoadBalancer)
|
|
|
|
configMap := configMapForConfig(t, impersonator.Config{
|
|
|
|
Mode: impersonator.ModeEnabled,
|
|
|
|
Endpoint: proxyServiceURL,
|
|
|
|
TLS: nil,
|
|
|
|
})
|
2021-02-18 23:58:27 +00:00
|
|
|
t.Logf("creating configmap %s", configMap.Name)
|
2021-02-12 01:22:47 +00:00
|
|
|
_, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, &configMap, metav1.CreateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
t.Cleanup(func() {
|
2021-02-17 18:32:29 +00:00
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
|
|
|
defer cancel()
|
2021-02-18 23:58:27 +00:00
|
|
|
t.Logf("cleaning up configmap at end of test %s", impersonationProxyConfigMapName)
|
|
|
|
err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Delete(ctx, impersonationProxyConfigMapName, metav1.DeleteOptions{})
|
2021-02-17 18:32:29 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
if len(oldConfigMap.Data) != 0 {
|
|
|
|
t.Log(oldConfigMap)
|
|
|
|
oldConfigMap.UID = "" // cant have a UID yet
|
|
|
|
oldConfigMap.ResourceVersion = ""
|
2021-02-18 23:58:27 +00:00
|
|
|
t.Logf("restoring a pre-existing configmap %s", oldConfigMap.Name)
|
2021-02-17 18:32:29 +00:00
|
|
|
_, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, oldConfigMap, metav1.CreateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
2021-02-12 01:22:47 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-01-22 18:00:27 +00:00
|
|
|
t.Run(
|
|
|
|
"access as user",
|
2021-02-12 01:22:47 +00:00
|
|
|
library.AccessAsUserTest(ctx, env.TestUser.ExpectedUsername, impersonationProxyClient),
|
2021-01-22 18:00:27 +00:00
|
|
|
)
|
|
|
|
for _, group := range env.TestUser.ExpectedGroups {
|
|
|
|
group := group
|
|
|
|
t.Run(
|
|
|
|
"access as group "+group,
|
2021-02-12 01:22:47 +00:00
|
|
|
library.AccessAsGroupTest(ctx, group, impersonationProxyClient),
|
2021-01-22 18:00:27 +00:00
|
|
|
)
|
|
|
|
}
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-02-19 00:27:03 +00:00
|
|
|
t.Run("watching all the verbs", func(t *testing.T) {
|
|
|
|
// Create a namespace, because it will be easier to deletecollection if we have a namespace.
|
|
|
|
// t.Cleanup Delete the namespace.
|
2021-02-23 01:23:11 +00:00
|
|
|
namespace, err := adminClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{
|
|
|
|
ObjectMeta: metav1.ObjectMeta{GenerateName: "impersonation-integration-test-"},
|
|
|
|
}, metav1.CreateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
|
|
t.Logf("cleaning up test namespace %s", namespace.Name)
|
|
|
|
err = adminClient.CoreV1().Namespaces().Delete(context.Background(), namespace.Name, metav1.DeleteOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
})
|
|
|
|
|
|
|
|
// Create an RBAC rule to allow this user to read/write everything.
|
|
|
|
library.CreateTestClusterRoleBinding(
|
|
|
|
t,
|
|
|
|
rbacv1.Subject{
|
|
|
|
Kind: rbacv1.UserKind,
|
|
|
|
APIGroup: rbacv1.GroupName,
|
|
|
|
Name: env.TestUser.ExpectedUsername,
|
|
|
|
},
|
|
|
|
rbacv1.RoleRef{
|
|
|
|
Kind: "ClusterRole",
|
|
|
|
APIGroup: rbacv1.GroupName,
|
|
|
|
Name: "cluster-admin",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
library.WaitForUserToHaveAccess(t, env.TestUser.ExpectedUsername, []string{}, &v1.ResourceAttributes{
|
|
|
|
Namespace: namespace.Name,
|
|
|
|
Verb: "create",
|
|
|
|
Group: "",
|
|
|
|
Version: "v1",
|
|
|
|
Resource: "configmaps",
|
|
|
|
})
|
|
|
|
|
|
|
|
// Create and start informer.
|
|
|
|
informerFactory := k8sinformers.NewSharedInformerFactoryWithOptions(
|
|
|
|
impersonationProxyClient,
|
|
|
|
0,
|
|
|
|
k8sinformers.WithNamespace(namespace.Name))
|
|
|
|
informer := informerFactory.Core().V1().ConfigMaps()
|
|
|
|
informer.Informer() // makes sure that the informer will cache
|
|
|
|
stopChannel := make(chan struct{})
|
|
|
|
informerFactory.Start(stopChannel)
|
|
|
|
t.Cleanup(func() {
|
|
|
|
stopChannel <- struct{}{}
|
|
|
|
})
|
|
|
|
informerFactory.WaitForCacheSync(ctx.Done())
|
|
|
|
|
|
|
|
// Test "create" verb.
|
|
|
|
_, err = impersonationProxyClient.CoreV1().ConfigMaps(namespace.Name).Create(
|
|
|
|
ctx,
|
|
|
|
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-1"}},
|
|
|
|
metav1.CreateOptions{},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
_, err = informer.Lister().ConfigMaps(namespace.Name).Get("configmap-1")
|
|
|
|
return err == nil
|
|
|
|
}, 10*time.Second, 500*time.Millisecond)
|
|
|
|
|
|
|
|
_, err = impersonationProxyClient.CoreV1().ConfigMaps(namespace.Name).Create(
|
|
|
|
ctx,
|
|
|
|
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-2"}},
|
|
|
|
metav1.CreateOptions{},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
_, err = informer.Lister().ConfigMaps(namespace.Name).Get("configmap-2")
|
|
|
|
return err == nil
|
|
|
|
}, 10*time.Second, 500*time.Millisecond)
|
|
|
|
|
|
|
|
_, err = impersonationProxyClient.CoreV1().ConfigMaps(namespace.Name).Create(
|
|
|
|
ctx,
|
|
|
|
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-3"}},
|
|
|
|
metav1.CreateOptions{},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
_, err = informer.Lister().ConfigMaps(namespace.Name).Get("configmap-3")
|
|
|
|
return err == nil
|
|
|
|
}, 10*time.Second, 500*time.Millisecond)
|
|
|
|
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
configmaps, err := informer.Lister().ConfigMaps(namespace.Name).List(labels.Everything())
|
|
|
|
return err == nil && len(configmaps) == 3
|
|
|
|
}, 10*time.Second, 500*time.Millisecond)
|
|
|
|
|
|
|
|
// TODO, test more verbs
|
2021-02-19 00:27:03 +00:00
|
|
|
// "get" one them.
|
|
|
|
// "list" them all.
|
|
|
|
// "update" one of them.
|
|
|
|
// "patch" one of them.
|
|
|
|
// "delete" one of them.
|
|
|
|
// "deletecollection" all of them.
|
|
|
|
// Make sure the watch sees all of those actions.
|
|
|
|
})
|
|
|
|
|
2021-02-12 01:22:47 +00:00
|
|
|
// Update configuration to force the proxy to disabled mode
|
|
|
|
configMap := configMapForConfig(t, impersonator.Config{Mode: impersonator.ModeDisabled})
|
2021-02-17 18:32:29 +00:00
|
|
|
if env.HasCapability(library.HasExternalLoadBalancerProvider) {
|
2021-02-18 23:58:27 +00:00
|
|
|
t.Logf("creating configmap %s", configMap.Name)
|
2021-02-17 18:32:29 +00:00
|
|
|
_, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, &configMap, metav1.CreateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
} else {
|
2021-02-18 23:58:27 +00:00
|
|
|
t.Logf("updating configmap %s", configMap.Name)
|
2021-02-17 18:32:29 +00:00
|
|
|
_, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Update(ctx, &configMap, metav1.UpdateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-02-19 00:27:03 +00:00
|
|
|
// Check that the impersonation proxy has shut down
|
2021-02-18 19:08:13 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-02-19 00:27:03 +00:00
|
|
|
// It's okay if this returns RBAC errors because this user has no role bindings.
|
|
|
|
// What we want to see is that the proxy eventually shuts down entirely.
|
2021-02-18 19:08:13 +00:00
|
|
|
_, err = impersonationProxyClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
|
|
|
return err.Error() == serviceUnavailableError
|
|
|
|
}, 10*time.Second, 500*time.Millisecond)
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-02-17 18:32:29 +00:00
|
|
|
if env.HasCapability(library.HasExternalLoadBalancerProvider) {
|
2021-02-18 23:58:27 +00:00
|
|
|
// The load balancer should not exist after we disable the impersonation proxy.
|
|
|
|
// Note that this can take kind of a long time on real cloud providers (e.g. ~22 seconds on EKS).
|
2021-02-17 18:32:29 +00:00
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
return !hasLoadBalancerService(ctx, t, adminClient, env.ConciergeNamespace)
|
2021-02-18 23:58:27 +00:00
|
|
|
}, time.Minute, 500*time.Millisecond)
|
2021-02-17 18:32:29 +00:00
|
|
|
}
|
2021-02-12 01:22:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func configMapForConfig(t *testing.T, config impersonator.Config) corev1.ConfigMap {
|
|
|
|
configString, err := yaml.Marshal(config)
|
|
|
|
require.NoError(t, err)
|
|
|
|
configMap := corev1.ConfigMap{
|
2021-02-18 23:58:27 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Name: impersonationProxyConfigMapName,
|
|
|
|
},
|
2021-02-12 01:22:47 +00:00
|
|
|
Data: map[string]string{
|
|
|
|
"config.yaml": string(configString),
|
|
|
|
}}
|
|
|
|
return configMap
|
|
|
|
}
|
|
|
|
|
|
|
|
func hasLoadBalancerService(ctx context.Context, t *testing.T, client kubernetes.Interface, namespace string) bool {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
services, err := client.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
for _, service := range services.Items {
|
|
|
|
if service.Spec.Type == corev1.ServiceTypeLoadBalancer {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
2021-01-22 18:00:27 +00:00
|
|
|
}
|