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 (
|
2021-03-06 00:14:45 +00:00
|
|
|
"bytes"
|
2021-01-22 18:00:27 +00:00
|
|
|
"context"
|
2021-03-10 00:58:44 +00:00
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
2021-03-03 20:53:23 +00:00
|
|
|
"encoding/base64"
|
2021-03-10 00:58:44 +00:00
|
|
|
"encoding/json"
|
2021-03-10 18:30:06 +00:00
|
|
|
"encoding/pem"
|
2021-01-22 18:00:27 +00:00
|
|
|
"fmt"
|
2021-03-06 00:14:45 +00:00
|
|
|
"io/ioutil"
|
2021-01-22 18:00:27 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2021-03-06 00:14:45 +00:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2021-01-22 18:00:27 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
2021-03-10 18:30:06 +00:00
|
|
|
"golang.org/x/net/websocket"
|
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-02-23 18:38:02 +00:00
|
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
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"
|
2021-02-23 18:38:02 +00:00
|
|
|
"k8s.io/apimachinery/pkg/types"
|
2021-03-10 00:58:44 +00:00
|
|
|
"k8s.io/apimachinery/pkg/watch"
|
2021-02-23 01:23:11 +00:00
|
|
|
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-03-03 20:53:23 +00:00
|
|
|
"go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
2021-03-10 18:30:06 +00:00
|
|
|
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
2021-03-03 20:53:23 +00:00
|
|
|
"go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
2021-02-12 01:22:47 +00:00
|
|
|
"go.pinniped.dev/internal/concierge/impersonator"
|
2021-03-06 00:14:45 +00:00
|
|
|
"go.pinniped.dev/internal/testutil"
|
2021-01-22 18:00:27 +00:00
|
|
|
"go.pinniped.dev/test/library"
|
|
|
|
)
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Note that this test supports being run on all of our integration test cluster types:
|
|
|
|
// - load balancers not supported, has squid proxy (e.g. kind)
|
|
|
|
// - load balancers supported, has squid proxy (e.g. EKS)
|
|
|
|
// - load balancers supported, no squid proxy (e.g. GKE)
|
2021-03-10 18:30:06 +00:00
|
|
|
func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's complex.
|
2021-01-22 18:00:27 +00:00
|
|
|
env := library.IntegrationEnv(t)
|
|
|
|
|
2021-03-03 20:53:23 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*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-03-03 20:53:23 +00:00
|
|
|
adminConciergeClient := library.NewConciergeClientset(t)
|
2021-01-22 18:00:27 +00:00
|
|
|
|
|
|
|
// Create a WebhookAuthenticator.
|
|
|
|
authenticator := library.CreateTestWebhookAuthenticator(ctx, t)
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// The address of the ClusterIP service that points at the impersonation proxy's port (used when there is no load balancer).
|
2021-02-25 18:27:19 +00:00
|
|
|
proxyServiceEndpoint := fmt.Sprintf("%s-proxy.%s.svc.cluster.local", env.ConciergeAppName, env.ConciergeNamespace)
|
2021-02-25 22:40:02 +00:00
|
|
|
// The error message that will be returned by squid when the impersonation proxy port inside the cluster is not listening.
|
|
|
|
serviceUnavailableViaSquidError := fmt.Sprintf(`Get "https://%s/api/v1/namespaces": Service Unavailable`, proxyServiceEndpoint)
|
2021-01-22 18:00:27 +00:00
|
|
|
|
2021-03-10 22:50:46 +00:00
|
|
|
credentialRequestSpecWithWorkingCredentials := loginv1alpha1.TokenCredentialRequestSpec{
|
|
|
|
Token: env.TestUser.Token,
|
|
|
|
Authenticator: authenticator,
|
|
|
|
}
|
|
|
|
|
2021-03-10 18:30:06 +00:00
|
|
|
credentialAlmostExpired := func(credential *loginv1alpha1.TokenCredentialRequest) bool {
|
|
|
|
pemBlock, _ := pem.Decode([]byte(credential.Status.Credential.ClientCertificateData))
|
|
|
|
parsedCredential, err := x509.ParseCertificate(pemBlock.Bytes)
|
|
|
|
require.NoError(t, err)
|
|
|
|
timeRemaining := time.Until(parsedCredential.NotAfter)
|
|
|
|
if timeRemaining < 2*time.Minute {
|
|
|
|
t.Logf("The TokenCredentialRequest cred is almost expired and needs to be refreshed. Expires in %s.", timeRemaining)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
t.Logf("The TokenCredentialRequest cred is good for some more time (%s) so using it.", timeRemaining)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
var tokenCredentialRequestResponse *loginv1alpha1.TokenCredentialRequest
|
|
|
|
refreshCredential := func() *loginv1alpha1.ClusterCredential {
|
|
|
|
if tokenCredentialRequestResponse == nil || credentialAlmostExpired(tokenCredentialRequestResponse) {
|
|
|
|
var err error
|
|
|
|
// Make a TokenCredentialRequest. This can either return a cert signed by the Kube API server's CA (e.g. on kind)
|
|
|
|
// or a cert signed by the impersonator's signing CA (e.g. on GKE). Either should be accepted by the impersonation
|
|
|
|
// proxy server as a valid authentication.
|
|
|
|
//
|
|
|
|
// However, we issue short-lived certs, so this cert will only be valid for a few minutes.
|
|
|
|
// Cache it until it is almost expired and then refresh it whenever it is close to expired.
|
2021-03-10 22:50:46 +00:00
|
|
|
tokenCredentialRequestResponse, err = library.CreateTokenCredentialRequest(ctx, t, credentialRequestSpecWithWorkingCredentials)
|
2021-03-10 18:30:06 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-03-10 21:08:15 +00:00
|
|
|
require.Nil(t, tokenCredentialRequestResponse.Status.Message,
|
|
|
|
"expected no error message but got: %s", library.Sdump(tokenCredentialRequestResponse.Status.Message))
|
2021-03-10 18:30:06 +00:00
|
|
|
require.NotEmpty(t, tokenCredentialRequestResponse.Status.Credential.ClientCertificateData)
|
|
|
|
require.NotEmpty(t, tokenCredentialRequestResponse.Status.Credential.ClientKeyData)
|
|
|
|
|
|
|
|
// At the moment the credential request should not have returned a token. In the future, if we make it return
|
|
|
|
// tokens, we should revisit this test's rest config below.
|
|
|
|
require.Empty(t, tokenCredentialRequestResponse.Status.Credential.Token)
|
|
|
|
}
|
|
|
|
return tokenCredentialRequestResponse.Status.Credential
|
|
|
|
}
|
|
|
|
|
|
|
|
impersonationProxyRestConfig := func(credential *loginv1alpha1.ClusterCredential, host string, caData []byte, doubleImpersonateUser string) *rest.Config {
|
2021-03-02 01:53:26 +00:00
|
|
|
config := rest.Config{
|
2021-03-10 18:30:06 +00:00
|
|
|
Host: host,
|
|
|
|
TLSClientConfig: rest.TLSClientConfig{
|
|
|
|
Insecure: caData == nil,
|
|
|
|
CAData: caData,
|
|
|
|
CertData: []byte(credential.ClientCertificateData),
|
|
|
|
KeyData: []byte(credential.ClientKeyData),
|
|
|
|
},
|
|
|
|
// kubectl would set both the client cert and the token, so we'll do that too.
|
|
|
|
// The Kube API server will ignore the token if the client cert successfully authenticates.
|
|
|
|
// Only if the client cert is not present or fails to authenticate will it use the token.
|
|
|
|
// Historically, it works that way because some web browsers will always send your
|
|
|
|
// corporate-assigned client cert even if it is not valid, and it doesn't want to treat
|
|
|
|
// that as a failure if you also sent a perfectly good token.
|
|
|
|
// We would like the impersonation proxy to imitate that behavior, so we test it here.
|
|
|
|
BearerToken: "this is not valid",
|
2021-03-02 01:53:26 +00:00
|
|
|
}
|
|
|
|
if doubleImpersonateUser != "" {
|
|
|
|
config.Impersonate = rest.ImpersonationConfig{UserName: doubleImpersonateUser}
|
|
|
|
}
|
|
|
|
return &config
|
|
|
|
}
|
|
|
|
|
2021-03-10 21:08:15 +00:00
|
|
|
kubeconfigProxyFunc := func() func(req *http.Request) (*url.URL, error) {
|
|
|
|
return func(req *http.Request) (*url.URL, error) {
|
2021-03-02 01:53:26 +00:00
|
|
|
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-25 18:27:19 +00:00
|
|
|
}
|
2021-03-10 21:08:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impersonationProxyViaSquidClient := func(proxyURL string, caData []byte, doubleImpersonateUser string) kubernetes.Interface {
|
|
|
|
t.Helper()
|
|
|
|
kubeconfig := impersonationProxyRestConfig(refreshCredential(), proxyURL, caData, doubleImpersonateUser)
|
|
|
|
kubeconfig.Proxy = kubeconfigProxyFunc()
|
|
|
|
return library.NewKubeclient(t, kubeconfig).Kubernetes
|
|
|
|
}
|
|
|
|
|
|
|
|
impersonationProxyViaSquidClientWithoutCredential := func() kubernetes.Interface {
|
|
|
|
t.Helper()
|
|
|
|
proxyURL := "https://" + proxyServiceEndpoint
|
|
|
|
kubeconfig := impersonationProxyRestConfig(&loginv1alpha1.ClusterCredential{}, proxyURL, nil, "")
|
|
|
|
kubeconfig.Proxy = kubeconfigProxyFunc()
|
2021-03-08 21:03:34 +00:00
|
|
|
return library.NewKubeclient(t, kubeconfig).Kubernetes
|
2021-02-25 22:40:02 +00:00
|
|
|
}
|
2021-01-22 18:00:27 +00:00
|
|
|
|
2021-03-08 21:03:34 +00:00
|
|
|
impersonationProxyViaLoadBalancerClient := func(proxyURL string, caData []byte, doubleImpersonateUser string) kubernetes.Interface {
|
2021-02-25 22:40:02 +00:00
|
|
|
t.Helper()
|
2021-03-10 18:30:06 +00:00
|
|
|
kubeconfig := impersonationProxyRestConfig(refreshCredential(), proxyURL, caData, doubleImpersonateUser)
|
2021-03-08 21:03:34 +00:00
|
|
|
return library.NewKubeclient(t, kubeconfig).Kubernetes
|
2021-02-25 18:27:19 +00:00
|
|
|
}
|
2021-01-22 18:00:27 +00:00
|
|
|
|
2021-03-10 18:30:06 +00:00
|
|
|
newImpersonationProxyClient := func(proxyURL string, impersonationProxyCACertPEM []byte, doubleImpersonateUser string) kubernetes.Interface {
|
|
|
|
if env.HasCapability(library.HasExternalLoadBalancerProvider) {
|
|
|
|
return impersonationProxyViaLoadBalancerClient(proxyURL, impersonationProxyCACertPEM, doubleImpersonateUser)
|
|
|
|
}
|
|
|
|
return impersonationProxyViaSquidClient(proxyURL, impersonationProxyCACertPEM, doubleImpersonateUser)
|
|
|
|
}
|
|
|
|
|
2021-03-02 17:31:24 +00:00
|
|
|
oldConfigMap, err := adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Get(ctx, impersonationProxyConfigMapName(env), metav1.GetOptions{})
|
2021-02-25 22:40:02 +00:00
|
|
|
if !k8serrors.IsNotFound(err) {
|
|
|
|
require.NoError(t, err) // other errors aside from NotFound are unexpected
|
2021-02-18 23:58:27 +00:00
|
|
|
t.Logf("stashing a pre-existing configmap %s", oldConfigMap.Name)
|
2021-03-02 17:31:24 +00:00
|
|
|
require.NoError(t, adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Delete(ctx, impersonationProxyConfigMapName(env), metav1.DeleteOptions{}))
|
2021-02-17 18:32:29 +00:00
|
|
|
}
|
2021-03-04 00:23:07 +00:00
|
|
|
// At the end of the test, clean up the ConfigMap.
|
|
|
|
t.Cleanup(func() {
|
2021-03-05 01:25:43 +00:00
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), time.Minute)
|
2021-03-04 00:23:07 +00:00
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// Delete any version that was created by this test.
|
|
|
|
t.Logf("cleaning up configmap at end of test %s", impersonationProxyConfigMapName(env))
|
|
|
|
err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Delete(ctx, impersonationProxyConfigMapName(env), metav1.DeleteOptions{})
|
|
|
|
if !k8serrors.IsNotFound(err) {
|
|
|
|
require.NoError(t, err) // only not found errors are acceptable
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only recreate it if it already existed at the start of this test.
|
|
|
|
if len(oldConfigMap.Data) != 0 {
|
|
|
|
t.Log(oldConfigMap)
|
|
|
|
oldConfigMap.UID = "" // cant have a UID yet
|
|
|
|
oldConfigMap.ResourceVersion = ""
|
|
|
|
t.Logf("restoring a pre-existing configmap %s", oldConfigMap.Name)
|
|
|
|
_, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, oldConfigMap, metav1.CreateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
})
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
if env.HasCapability(library.HasExternalLoadBalancerProvider) { //nolint:nestif // come on... it's just a test
|
2021-03-03 20:53:23 +00:00
|
|
|
// Check that load balancer has been automatically created by the impersonator's "auto" mode.
|
2021-02-25 22:40:02 +00:00
|
|
|
library.RequireEventuallyWithoutError(t, func() (bool, error) {
|
2021-03-02 17:31:24 +00:00
|
|
|
return hasImpersonationProxyLoadBalancerService(ctx, env, adminClient)
|
2021-03-02 23:49:01 +00:00
|
|
|
}, 30*time.Second, 500*time.Millisecond)
|
2021-02-12 01:22:47 +00:00
|
|
|
} else {
|
2021-02-25 22:40:02 +00:00
|
|
|
require.NotEmpty(t, env.Proxy,
|
|
|
|
"test cluster does not support load balancers but also doesn't have a squid proxy... "+
|
|
|
|
"this is not a supported configuration for test clusters")
|
|
|
|
|
2021-03-03 20:53:23 +00:00
|
|
|
// Check that no load balancer has been created by the impersonator's "auto" mode.
|
2021-02-25 22:40:02 +00:00
|
|
|
library.RequireNeverWithoutError(t, func() (bool, error) {
|
2021-03-02 17:31:24 +00:00
|
|
|
return hasImpersonationProxyLoadBalancerService(ctx, env, adminClient)
|
2021-02-12 01:22:47 +00:00
|
|
|
}, 10*time.Second, 500*time.Millisecond)
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Check that we can't use the impersonation proxy to execute kubectl commands yet.
|
2021-03-10 21:08:15 +00:00
|
|
|
_, err = impersonationProxyViaSquidClientWithoutCredential().CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
2021-02-25 22:40:02 +00:00
|
|
|
require.EqualError(t, err, serviceUnavailableViaSquidError)
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-03-03 20:53:23 +00:00
|
|
|
// Create configuration to make the impersonation proxy turn on with a hard coded endpoint (without a load balancer).
|
2021-03-02 17:31:24 +00:00
|
|
|
configMap := configMapForConfig(t, env, impersonator.Config{
|
2021-02-12 01:22:47 +00:00
|
|
|
Mode: impersonator.ModeEnabled,
|
2021-02-25 18:27:19 +00:00
|
|
|
Endpoint: proxyServiceEndpoint,
|
2021-02-12 01:22:47 +00:00
|
|
|
})
|
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)
|
|
|
|
}
|
|
|
|
|
2021-03-03 20:53:23 +00:00
|
|
|
// At this point the impersonator should be starting/running. When it is ready, the CredentialIssuer's
|
|
|
|
// strategies array should be updated to include a successful impersonation strategy which can be used
|
|
|
|
// to discover the impersonator's URL and CA certificate. Until it has finished starting, it may not be included
|
|
|
|
// in the strategies array or it may be included in an error state. It can be in an error state for
|
|
|
|
// awhile when it is waiting for the load balancer to be assigned an ip/hostname.
|
|
|
|
impersonationProxyURL, impersonationProxyCACertPEM := performImpersonatorDiscovery(ctx, t, env, adminConciergeClient)
|
2021-03-10 18:30:06 +00:00
|
|
|
if !env.HasCapability(library.HasExternalLoadBalancerProvider) {
|
2021-03-03 20:53:23 +00:00
|
|
|
// In this case, we specified the endpoint in the configmap, so check that it was reported correctly in the CredentialIssuer.
|
|
|
|
require.Equal(t, "https://"+proxyServiceEndpoint, impersonationProxyURL)
|
2021-03-10 18:30:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Because our credentials expire so quickly, we'll always use a new client, to give us a chance to refresh our
|
|
|
|
// credentials before they expire. Create a closure to capture the arguments to newImpersonationProxyClient
|
|
|
|
// so we don't have to keep repeating them.
|
|
|
|
// This client performs TLS checks, so it also provides test coverage that the impersonation proxy server is generating TLS certs correctly.
|
|
|
|
impersonationProxyClient := func() kubernetes.Interface {
|
|
|
|
return newImpersonationProxyClient(impersonationProxyURL, impersonationProxyCACertPEM, "")
|
2021-02-25 22:40:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Test that the user can perform basic actions through the client with their username and group membership
|
|
|
|
// influencing RBAC checks correctly.
|
2021-01-22 18:00:27 +00:00
|
|
|
t.Run(
|
|
|
|
"access as user",
|
2021-03-10 18:30:06 +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-03-10 18:30:06 +00:00
|
|
|
library.AccessAsGroupTest(ctx, group, impersonationProxyClient()),
|
2021-01-22 18:00:27 +00:00
|
|
|
)
|
|
|
|
}
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-03-10 00:58:44 +00:00
|
|
|
// Create a namespace, because it will be easier to exercise "deletecollection" if we have a namespace.
|
|
|
|
namespace, err := adminClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{
|
|
|
|
ObjectMeta: metav1.ObjectMeta{GenerateName: "impersonation-integration-test-"},
|
|
|
|
}, metav1.CreateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Schedule the namespace for cleanup.
|
|
|
|
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)
|
|
|
|
})
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Try more Kube API verbs through the impersonation proxy.
|
|
|
|
t.Run("watching all the basic verbs", func(t *testing.T) {
|
2021-02-23 01:23:11 +00:00
|
|
|
// Create an RBAC rule to allow this user to read/write everything.
|
2021-03-02 01:53:26 +00:00
|
|
|
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"},
|
2021-02-23 01:23:11 +00:00
|
|
|
)
|
2021-02-25 22:40:02 +00:00
|
|
|
// Wait for the above RBAC rule to take effect.
|
2021-02-23 01:23:11 +00:00
|
|
|
library.WaitForUserToHaveAccess(t, env.TestUser.ExpectedUsername, []string{}, &v1.ResourceAttributes{
|
2021-03-02 01:53:26 +00:00
|
|
|
Namespace: namespace.Name, Verb: "create", Group: "", Version: "v1", Resource: "configmaps",
|
2021-02-23 01:23:11 +00:00
|
|
|
})
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Create and start informer to exercise the "watch" verb for us.
|
2021-02-23 01:23:11 +00:00
|
|
|
informerFactory := k8sinformers.NewSharedInformerFactoryWithOptions(
|
2021-03-10 18:30:06 +00:00
|
|
|
impersonationProxyClient(),
|
2021-02-23 01:23:11 +00:00
|
|
|
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() {
|
2021-02-23 18:38:02 +00:00
|
|
|
// Shut down the informer.
|
|
|
|
close(stopChannel)
|
2021-02-23 01:23:11 +00:00
|
|
|
})
|
|
|
|
informerFactory.WaitForCacheSync(ctx.Done())
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Use labels on our created ConfigMaps to avoid accidentally listing other ConfigMaps that might
|
|
|
|
// exist in the namespace. In Kube 1.20+ there is a default ConfigMap in every namespace.
|
2021-02-24 18:08:41 +00:00
|
|
|
configMapLabels := labels.Set{
|
|
|
|
"pinniped.dev/testConfigMap": library.RandHex(t, 8),
|
|
|
|
}
|
2021-02-25 22:40:02 +00:00
|
|
|
|
|
|
|
// Test "create" verb through the impersonation proxy.
|
2021-03-10 18:30:06 +00:00
|
|
|
_, err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx,
|
2021-02-24 18:08:41 +00:00
|
|
|
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-1", Labels: configMapLabels}},
|
2021-02-23 01:23:11 +00:00
|
|
|
metav1.CreateOptions{},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
2021-03-10 18:30:06 +00:00
|
|
|
_, err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx,
|
2021-02-24 18:08:41 +00:00
|
|
|
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-2", Labels: configMapLabels}},
|
2021-02-23 01:23:11 +00:00
|
|
|
metav1.CreateOptions{},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
2021-03-10 18:30:06 +00:00
|
|
|
_, err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Create(ctx,
|
2021-02-24 18:08:41 +00:00
|
|
|
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-3", Labels: configMapLabels}},
|
2021-02-23 01:23:11 +00:00
|
|
|
metav1.CreateOptions{},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
2021-02-23 18:38:02 +00:00
|
|
|
|
|
|
|
// Make sure that all of the created ConfigMaps show up in the informer's cache to
|
|
|
|
// demonstrate that the informer's "watch" verb is working through the impersonation proxy.
|
2021-02-23 01:23:11 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-02-23 18:38:02 +00:00
|
|
|
_, err1 := informer.Lister().ConfigMaps(namespace.Name).Get("configmap-1")
|
|
|
|
_, err2 := informer.Lister().ConfigMaps(namespace.Name).Get("configmap-2")
|
|
|
|
_, err3 := informer.Lister().ConfigMaps(namespace.Name).Get("configmap-3")
|
|
|
|
return err1 == nil && err2 == nil && err3 == nil
|
|
|
|
}, 10*time.Second, 50*time.Millisecond)
|
2021-02-23 01:23:11 +00:00
|
|
|
|
2021-02-23 18:38:02 +00:00
|
|
|
// Test "get" verb through the impersonation proxy.
|
2021-03-10 18:30:06 +00:00
|
|
|
configMap3, err := impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Get(ctx, "configmap-3", metav1.GetOptions{})
|
2021-02-23 18:38:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Test "list" verb through the impersonation proxy.
|
2021-03-10 18:30:06 +00:00
|
|
|
listResult, err := impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).List(ctx, metav1.ListOptions{
|
2021-02-24 18:08:41 +00:00
|
|
|
LabelSelector: configMapLabels.String(),
|
|
|
|
})
|
2021-02-23 18:38:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, listResult.Items, 3)
|
|
|
|
|
|
|
|
// Test "update" verb through the impersonation proxy.
|
|
|
|
configMap3.Data = map[string]string{"foo": "bar"}
|
2021-03-10 18:30:06 +00:00
|
|
|
updateResult, err := impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Update(ctx, configMap3, metav1.UpdateOptions{})
|
2021-02-23 18:38:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "bar", updateResult.Data["foo"])
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Make sure that the updated ConfigMap shows up in the informer's cache.
|
2021-02-23 01:23:11 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-02-23 18:38:02 +00:00
|
|
|
configMap, err := informer.Lister().ConfigMaps(namespace.Name).Get("configmap-3")
|
|
|
|
return err == nil && configMap.Data["foo"] == "bar"
|
|
|
|
}, 10*time.Second, 50*time.Millisecond)
|
2021-02-23 01:23:11 +00:00
|
|
|
|
2021-02-23 18:38:02 +00:00
|
|
|
// Test "patch" verb through the impersonation proxy.
|
2021-03-10 18:30:06 +00:00
|
|
|
patchResult, err := impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Patch(ctx,
|
2021-02-23 18:38:02 +00:00
|
|
|
"configmap-3",
|
|
|
|
types.MergePatchType,
|
|
|
|
[]byte(`{"data":{"baz":"42"}}`),
|
|
|
|
metav1.PatchOptions{},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "bar", patchResult.Data["foo"])
|
|
|
|
require.Equal(t, "42", patchResult.Data["baz"])
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Make sure that the patched ConfigMap shows up in the informer's cache.
|
2021-02-23 18:38:02 +00:00
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
configMap, err := informer.Lister().ConfigMaps(namespace.Name).Get("configmap-3")
|
|
|
|
return err == nil && configMap.Data["foo"] == "bar" && configMap.Data["baz"] == "42"
|
|
|
|
}, 10*time.Second, 50*time.Millisecond)
|
|
|
|
|
|
|
|
// Test "delete" verb through the impersonation proxy.
|
2021-03-10 18:30:06 +00:00
|
|
|
err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).Delete(ctx, "configmap-3", metav1.DeleteOptions{})
|
2021-02-23 18:38:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Make sure that the deleted ConfigMap shows up in the informer's cache.
|
2021-02-23 18:38:02 +00:00
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
_, getErr := informer.Lister().ConfigMaps(namespace.Name).Get("configmap-3")
|
2021-02-24 18:08:41 +00:00
|
|
|
list, listErr := informer.Lister().ConfigMaps(namespace.Name).List(configMapLabels.AsSelector())
|
2021-02-23 18:38:02 +00:00
|
|
|
return k8serrors.IsNotFound(getErr) && listErr == nil && len(list) == 2
|
|
|
|
}, 10*time.Second, 50*time.Millisecond)
|
|
|
|
|
|
|
|
// Test "deletecollection" verb through the impersonation proxy.
|
2021-03-10 18:30:06 +00:00
|
|
|
err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{})
|
2021-02-23 18:38:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// Make sure that the deleted ConfigMaps shows up in the informer's cache.
|
2021-02-23 18:38:02 +00:00
|
|
|
require.Eventually(t, func() bool {
|
2021-02-24 18:08:41 +00:00
|
|
|
list, listErr := informer.Lister().ConfigMaps(namespace.Name).List(configMapLabels.AsSelector())
|
2021-02-23 18:38:02 +00:00
|
|
|
return listErr == nil && len(list) == 0
|
|
|
|
}, 10*time.Second, 50*time.Millisecond)
|
|
|
|
|
2021-02-25 22:40:02 +00:00
|
|
|
// There should be no ConfigMaps left.
|
2021-03-10 18:30:06 +00:00
|
|
|
listResult, err = impersonationProxyClient().CoreV1().ConfigMaps(namespace.Name).List(ctx, metav1.ListOptions{
|
2021-02-24 18:08:41 +00:00
|
|
|
LabelSelector: configMapLabels.String(),
|
|
|
|
})
|
2021-02-23 18:38:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, listResult.Items, 0)
|
2021-02-19 00:27:03 +00:00
|
|
|
})
|
|
|
|
|
2021-03-02 01:53:26 +00:00
|
|
|
t.Run("double impersonation is blocked", func(t *testing.T) {
|
|
|
|
// 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: "edit"},
|
|
|
|
)
|
|
|
|
// Wait for the above RBAC rule to take effect.
|
|
|
|
library.WaitForUserToHaveAccess(t, env.TestUser.ExpectedUsername, []string{}, &v1.ResourceAttributes{
|
|
|
|
Namespace: env.ConciergeNamespace, Verb: "get", Group: "", Version: "v1", Resource: "secrets",
|
|
|
|
})
|
|
|
|
|
|
|
|
// Make a client which will send requests through the impersonation proxy and will also add
|
|
|
|
// impersonate headers to the request.
|
2021-03-10 18:30:06 +00:00
|
|
|
doubleImpersonationClient := newImpersonationProxyClient(impersonationProxyURL, impersonationProxyCACertPEM, "other-user-to-impersonate")
|
2021-03-02 01:53:26 +00:00
|
|
|
|
2021-03-03 20:53:23 +00:00
|
|
|
// Check that we can get some resource through the impersonation proxy without any impersonation headers on the request.
|
|
|
|
// We could use any resource for this, but we happen to know that this one should exist.
|
2021-03-10 18:30:06 +00:00
|
|
|
_, err = impersonationProxyClient().CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{})
|
2021-03-02 01:53:26 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Now we'll see what happens when we add an impersonation header to the request. This should generate a
|
|
|
|
// request similar to the one above, except that it will have an impersonation header.
|
2021-03-02 17:31:24 +00:00
|
|
|
_, err = doubleImpersonationClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{})
|
2021-03-02 01:53:26 +00:00
|
|
|
// Double impersonation is not supported yet, so we should get an error.
|
2021-03-10 18:30:06 +00:00
|
|
|
require.EqualError(t, err, fmt.Sprintf(
|
|
|
|
`users "other-user-to-impersonate" is forbidden: `+
|
|
|
|
`User "%s" cannot impersonate resource "users" in API group "" at the cluster scope: `+
|
|
|
|
`impersonation is not allowed or invalid verb`,
|
|
|
|
env.TestUser.ExpectedUsername))
|
2021-03-02 01:53:26 +00:00
|
|
|
})
|
|
|
|
|
2021-03-06 00:14:45 +00:00
|
|
|
t.Run("kubectl as a client", func(t *testing.T) {
|
|
|
|
// 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: "edit"},
|
|
|
|
)
|
|
|
|
// Wait for the above RBAC rule to take effect.
|
|
|
|
library.WaitForUserToHaveAccess(t, env.TestUser.ExpectedUsername, []string{}, &v1.ResourceAttributes{
|
|
|
|
Verb: "get", Group: "", Version: "v1", Resource: "namespaces",
|
|
|
|
})
|
|
|
|
|
|
|
|
pinnipedExe := library.PinnipedCLIPath(t)
|
|
|
|
tempDir := testutil.TempDir(t)
|
|
|
|
|
|
|
|
var envVarsWithProxy []string
|
|
|
|
if !env.HasCapability(library.HasExternalLoadBalancerProvider) {
|
|
|
|
// Only if you don't have a load balancer, use the squid proxy when it's available.
|
|
|
|
envVarsWithProxy = append(os.Environ(), env.ProxyEnv()...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the kubeconfig.
|
|
|
|
getKubeConfigCmd := []string{"get", "kubeconfig",
|
|
|
|
"--concierge-api-group-suffix", env.APIGroupSuffix,
|
|
|
|
"--oidc-skip-browser",
|
|
|
|
"--static-token", env.TestUser.Token,
|
|
|
|
// Force the use of impersonation proxy strategy, but let it auto-discover the endpoint and CA.
|
|
|
|
"--concierge-mode", "ImpersonationProxy"}
|
|
|
|
t.Log("Running:", pinnipedExe, getKubeConfigCmd)
|
|
|
|
kubeconfigYAML, getKubeConfigStderr := runPinnipedCLI(t, envVarsWithProxy, pinnipedExe, getKubeConfigCmd...)
|
|
|
|
// "pinniped get kubectl" prints some status messages to stderr
|
|
|
|
t.Log(getKubeConfigStderr)
|
|
|
|
// Make sure that the "pinniped get kubeconfig" auto-discovered the impersonation proxy and we're going to
|
|
|
|
// make our kubectl requests through the impersonation proxy. Avoid using require.Contains because the error
|
|
|
|
// message would contain credentials.
|
|
|
|
require.True(t,
|
|
|
|
strings.Contains(kubeconfigYAML, "server: "+impersonationProxyURL+"\n"),
|
|
|
|
"the generated kubeconfig did not include the expected impersonation server address: %s",
|
|
|
|
impersonationProxyURL,
|
|
|
|
)
|
|
|
|
require.True(t,
|
|
|
|
strings.Contains(kubeconfigYAML, "- --concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(impersonationProxyCACertPEM)+"\n"),
|
|
|
|
"the generated kubeconfig did not include the base64 encoded version of this expected impersonation CA cert: %s",
|
|
|
|
impersonationProxyCACertPEM,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Write the kubeconfig to a temp file.
|
|
|
|
kubeconfigPath := filepath.Join(tempDir, "kubeconfig.yaml")
|
|
|
|
require.NoError(t, ioutil.WriteFile(kubeconfigPath, []byte(kubeconfigYAML), 0600))
|
|
|
|
|
2021-03-09 19:32:27 +00:00
|
|
|
// func to create kubectl commands with a kubeconfig
|
|
|
|
kubectlCommand := func(timeout context.Context, args ...string) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer) {
|
2021-03-06 00:14:45 +00:00
|
|
|
allArgs := append([]string{"--kubeconfig", kubeconfigPath}, args...)
|
|
|
|
//nolint:gosec // we are not performing malicious argument injection against ourselves
|
|
|
|
kubectlCmd := exec.CommandContext(timeout, "kubectl", allArgs...)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
kubectlCmd.Stdout = &stdout
|
|
|
|
kubectlCmd.Stderr = &stderr
|
|
|
|
kubectlCmd.Env = envVarsWithProxy
|
|
|
|
|
|
|
|
t.Log("starting kubectl subprocess: kubectl", strings.Join(allArgs, " "))
|
2021-03-09 19:32:27 +00:00
|
|
|
return kubectlCmd, &stdout, &stderr
|
|
|
|
}
|
|
|
|
// Func to run kubeconfig commands.
|
|
|
|
runKubectl := func(args ...string) (string, string, error) {
|
|
|
|
timeout, cancelFunc := context.WithTimeout(ctx, 2*time.Minute)
|
|
|
|
defer cancelFunc()
|
|
|
|
|
|
|
|
kubectlCmd, stdout, stderr := kubectlCommand(timeout, args...)
|
|
|
|
|
2021-03-06 00:14:45 +00:00
|
|
|
err := kubectlCmd.Run()
|
|
|
|
t.Logf("kubectl stdout output: %s", stdout.String())
|
|
|
|
t.Logf("kubectl stderr output: %s", stderr.String())
|
|
|
|
return stdout.String(), stderr.String(), err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get pods in concierge namespace and pick one.
|
2021-03-09 19:32:27 +00:00
|
|
|
// We want to make sure it's a concierge pod (not cert agent), because we need to be able to "exec echo" and port-forward a running port.
|
2021-03-06 00:14:45 +00:00
|
|
|
pods, err := adminClient.CoreV1().Pods(env.ConciergeNamespace).List(ctx, metav1.ListOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Greater(t, len(pods.Items), 0)
|
2021-03-09 19:32:27 +00:00
|
|
|
var podName string
|
|
|
|
for _, pod := range pods.Items {
|
|
|
|
if !strings.Contains(pod.Name, "kube-cert-agent") {
|
|
|
|
podName = pod.Name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if podName == "" {
|
|
|
|
t.Error("could not find a concierge pod")
|
|
|
|
}
|
2021-03-06 00:14:45 +00:00
|
|
|
|
|
|
|
// Try "kubectl exec" through the impersonation proxy.
|
|
|
|
echoString := "hello world"
|
2021-03-09 19:32:27 +00:00
|
|
|
stdout, _, err := runKubectl("exec", "--namespace", env.ConciergeNamespace, podName, "--", "echo", echoString)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, echoString+"\n", stdout)
|
|
|
|
|
|
|
|
// run the kubectl port-forward command
|
|
|
|
timeout, cancelFunc := context.WithTimeout(ctx, 2*time.Minute)
|
|
|
|
defer cancelFunc()
|
|
|
|
portForwardCmd, _, _ := kubectlCommand(timeout, "port-forward", "--namespace", env.ConciergeNamespace, podName, "443:8443")
|
|
|
|
portForwardCmd.Env = envVarsWithProxy
|
|
|
|
|
|
|
|
// start, but don't wait for the command to finish
|
|
|
|
err = portForwardCmd.Start()
|
2021-03-06 00:14:45 +00:00
|
|
|
require.NoError(t, err)
|
2021-03-09 19:32:27 +00:00
|
|
|
|
|
|
|
// then run curl something against it
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
timeout, cancelFunc = context.WithTimeout(ctx, 2*time.Minute)
|
|
|
|
defer cancelFunc()
|
|
|
|
curlCmd := exec.CommandContext(timeout, "curl", "-k", "https://127.0.0.1")
|
|
|
|
var curlStdOut, curlStdErr bytes.Buffer
|
|
|
|
curlCmd.Stdout = &curlStdOut
|
|
|
|
curlCmd.Stderr = &curlStdErr
|
|
|
|
err = curlCmd.Run()
|
|
|
|
if err != nil {
|
|
|
|
t.Log("curl error: " + err.Error())
|
|
|
|
t.Log("curlStdErr: " + curlStdErr.String())
|
|
|
|
t.Log("stdout: " + curlStdOut.String())
|
|
|
|
}
|
|
|
|
// we expect this to 403, but all we care is that it gets through
|
|
|
|
require.Contains(t, curlStdOut.String(), "\"forbidden: User \\\"system:anonymous\\\" cannot get path \\\"/\\\"\"")
|
2021-03-06 00:14:45 +00:00
|
|
|
})
|
|
|
|
|
2021-03-10 00:58:44 +00:00
|
|
|
t.Run("websocket client", func(t *testing.T) {
|
|
|
|
dest, _ := url.Parse(impersonationProxyURL)
|
|
|
|
dest.Scheme = "wss"
|
|
|
|
dest.Path = "/api/v1/namespaces/" + namespace.Name + "/configmaps"
|
|
|
|
dest.RawQuery = "watch=1&resourceVersion=0"
|
|
|
|
origin, _ := url.Parse("http://localhost")
|
|
|
|
|
|
|
|
rootCAs := x509.NewCertPool()
|
|
|
|
rootCAs.AppendCertsFromPEM(impersonationProxyCACertPEM)
|
|
|
|
tlsConfig := &tls.Config{
|
2021-03-10 18:30:06 +00:00
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
|
RootCAs: rootCAs,
|
2021-03-10 00:58:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
websocketConfig := websocket.Config{
|
|
|
|
Location: dest,
|
|
|
|
Origin: origin,
|
|
|
|
TlsConfig: tlsConfig,
|
|
|
|
Version: 13,
|
|
|
|
Header: http.Header(make(map[string][]string)),
|
|
|
|
}
|
|
|
|
ws, err := websocket.DialConfig(&websocketConfig)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("failed to dial websocket: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// perform a create through the admin client
|
|
|
|
_, err = adminClient.CoreV1().ConfigMaps(namespace.Name).Create(ctx,
|
|
|
|
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "configmap-1"}},
|
|
|
|
metav1.CreateOptions{},
|
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// see if the websocket client received an event for the create
|
|
|
|
var got watchJSON
|
|
|
|
err = websocket.JSON.Receive(ws, &got)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
|
|
}
|
|
|
|
if got.Type != watch.Added {
|
|
|
|
t.Errorf("Unexpected type: %v", got.Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
var createConfigMap corev1.ConfigMap
|
|
|
|
err = json.Unmarshal(got.Object, &createConfigMap)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "configmap-1", createConfigMap.Name)
|
|
|
|
})
|
|
|
|
|
2021-03-10 21:08:15 +00:00
|
|
|
t.Run("manually disabling the impersonation proxy feature", func(t *testing.T) {
|
|
|
|
// Update configuration to force the proxy to disabled mode
|
|
|
|
configMap := configMapForConfig(t, env, impersonator.Config{Mode: impersonator.ModeDisabled})
|
|
|
|
if env.HasCapability(library.HasExternalLoadBalancerProvider) {
|
|
|
|
t.Logf("creating configmap %s", configMap.Name)
|
|
|
|
_, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, &configMap, metav1.CreateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
} else {
|
|
|
|
t.Logf("updating configmap %s", configMap.Name)
|
|
|
|
_, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Update(ctx, &configMap, metav1.UpdateOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-03-10 21:08:15 +00:00
|
|
|
if env.HasCapability(library.HasExternalLoadBalancerProvider) {
|
|
|
|
// The load balancer should have been deleted when we disabled the impersonation proxy.
|
|
|
|
// Note that this can take kind of a long time on real cloud providers (e.g. ~22 seconds on EKS).
|
|
|
|
library.RequireEventuallyWithoutError(t, func() (bool, error) {
|
|
|
|
hasService, err := hasImpersonationProxyLoadBalancerService(ctx, env, adminClient)
|
|
|
|
return !hasService, err
|
|
|
|
}, 2*time.Minute, 500*time.Millisecond)
|
|
|
|
}
|
2021-02-25 18:27:19 +00:00
|
|
|
|
2021-03-10 21:08:15 +00:00
|
|
|
// Check that the impersonation proxy port has shut down.
|
|
|
|
// Ideally we could always check that the impersonation proxy's port has shut down, but on clusters where we
|
|
|
|
// do not run the squid proxy we have no easy way to see beyond the load balancer to see inside the cluster,
|
|
|
|
// so we'll skip this check on clusters which have load balancers but don't run the squid proxy.
|
|
|
|
// The other cluster types that do run the squid proxy will give us sufficient coverage here.
|
|
|
|
if env.Proxy != "" {
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
// 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.
|
|
|
|
_, err = impersonationProxyViaSquidClientWithoutCredential().CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
|
|
|
return err.Error() == serviceUnavailableViaSquidError
|
|
|
|
}, 20*time.Second, 500*time.Millisecond)
|
|
|
|
}
|
2021-02-25 22:40:02 +00:00
|
|
|
|
2021-03-10 21:08:15 +00:00
|
|
|
// Check that the generated TLS cert Secret was deleted by the controller because it's supposed to clean this up
|
|
|
|
// when we disable the impersonator.
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
_, err = adminClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyTLSSecretName(env), metav1.GetOptions{})
|
|
|
|
return k8serrors.IsNotFound(err)
|
|
|
|
}, 10*time.Second, 250*time.Millisecond)
|
|
|
|
|
|
|
|
// Check that the generated CA cert Secret was not deleted by the controller because it's supposed to keep this
|
|
|
|
// around in case we decide to later re-enable the impersonator. We want to avoid generating new CA certs when
|
|
|
|
// possible because they make their way into kubeconfigs on client machines.
|
|
|
|
_, err = adminClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, impersonationProxyCASecretName(env), metav1.GetOptions{})
|
|
|
|
require.NoError(t, err)
|
2021-03-03 20:53:23 +00:00
|
|
|
|
2021-03-10 21:08:15 +00:00
|
|
|
// At this point the impersonator should be stopped. The CredentialIssuer's strategies array should be updated to
|
|
|
|
// include an unsuccessful impersonation strategy saying that it was manually configured to be disabled.
|
|
|
|
requireDisabledByConfigurationStrategy(ctx, t, env, adminConciergeClient)
|
|
|
|
|
|
|
|
if !env.HasCapability(library.ClusterSigningKeyIsAvailable) {
|
|
|
|
// This cluster does not support the cluster signing key strategy, so now that we've manually disabled the
|
|
|
|
// impersonation strategy, we should be left with no working strategies.
|
|
|
|
// Given that there are no working strategies, a TokenCredentialRequest which would otherwise work should now
|
|
|
|
// fail, because there is no point handing out credentials that are not going to work for any strategy.
|
2021-03-10 22:50:46 +00:00
|
|
|
tokenCredentialRequestResponse, err = library.CreateTokenCredentialRequest(ctx, t, credentialRequestSpecWithWorkingCredentials)
|
2021-03-10 21:08:15 +00:00
|
|
|
require.NoError(t, err)
|
2021-03-10 22:50:46 +00:00
|
|
|
|
2021-03-10 21:08:15 +00:00
|
|
|
require.NotNil(t, tokenCredentialRequestResponse.Status.Message, "expected an error message but got nil")
|
|
|
|
require.Equal(t, "authentication failed", *tokenCredentialRequestResponse.Status.Message)
|
|
|
|
require.Nil(t, tokenCredentialRequestResponse.Status.Credential)
|
|
|
|
}
|
|
|
|
})
|
2021-03-03 20:53:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func performImpersonatorDiscovery(ctx context.Context, t *testing.T, env *library.TestEnv, adminConciergeClient versioned.Interface) (string, []byte) {
|
|
|
|
t.Helper()
|
|
|
|
var impersonationProxyURL string
|
|
|
|
var impersonationProxyCACertPEM []byte
|
|
|
|
|
|
|
|
t.Log("Waiting for CredentialIssuer strategy to be successful")
|
|
|
|
|
|
|
|
library.RequireEventuallyWithoutError(t, func() (bool, error) {
|
|
|
|
credentialIssuer, err := adminConciergeClient.ConfigV1alpha1().CredentialIssuers().Get(ctx, credentialIssuerName(env), metav1.GetOptions{})
|
|
|
|
if err != nil || credentialIssuer.Status.Strategies == nil {
|
|
|
|
t.Log("Did not find any CredentialIssuer with any strategies")
|
|
|
|
return false, nil // didn't find it, but keep trying
|
|
|
|
}
|
|
|
|
for _, strategy := range credentialIssuer.Status.Strategies {
|
|
|
|
// There will be other strategy types in the list, so ignore those.
|
|
|
|
if strategy.Type == v1alpha1.ImpersonationProxyStrategyType && strategy.Status == v1alpha1.SuccessStrategyStatus { //nolint:nestif
|
|
|
|
if strategy.Frontend == nil {
|
|
|
|
return false, fmt.Errorf("did not find a Frontend") // unexpected, fail the test
|
|
|
|
}
|
|
|
|
if strategy.Frontend.ImpersonationProxyInfo == nil {
|
|
|
|
return false, fmt.Errorf("did not find an ImpersonationProxyInfo") // unexpected, fail the test
|
|
|
|
}
|
|
|
|
impersonationProxyURL = strategy.Frontend.ImpersonationProxyInfo.Endpoint
|
|
|
|
impersonationProxyCACertPEM, err = base64.StdEncoding.DecodeString(strategy.Frontend.ImpersonationProxyInfo.CertificateAuthorityData)
|
|
|
|
if err != nil {
|
|
|
|
return false, err // unexpected, fail the test
|
|
|
|
}
|
|
|
|
return true, nil // found it, continue the test!
|
|
|
|
} else if strategy.Type == v1alpha1.ImpersonationProxyStrategyType {
|
|
|
|
t.Logf("Waiting for successful impersonation proxy strategy on %s: found status %s with reason %s and message: %s",
|
|
|
|
credentialIssuerName(env), strategy.Status, strategy.Reason, strategy.Message)
|
|
|
|
if strategy.Reason == v1alpha1.ErrorDuringSetupStrategyReason {
|
|
|
|
// The server encountered an unexpected error while starting the impersonator, so fail the test fast.
|
|
|
|
return false, fmt.Errorf("found impersonation strategy in %s state with message: %s", strategy.Reason, strategy.Message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
t.Log("Did not find any impersonation proxy strategy on CredentialIssuer")
|
|
|
|
return false, nil // didn't find it, but keep trying
|
|
|
|
}, 10*time.Minute, 10*time.Second)
|
|
|
|
|
|
|
|
t.Log("Found successful CredentialIssuer strategy")
|
|
|
|
return impersonationProxyURL, impersonationProxyCACertPEM
|
|
|
|
}
|
|
|
|
|
|
|
|
func requireDisabledByConfigurationStrategy(ctx context.Context, t *testing.T, env *library.TestEnv, adminConciergeClient versioned.Interface) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
library.RequireEventuallyWithoutError(t, func() (bool, error) {
|
|
|
|
credentialIssuer, err := adminConciergeClient.ConfigV1alpha1().CredentialIssuers().Get(ctx, credentialIssuerName(env), metav1.GetOptions{})
|
|
|
|
if err != nil || credentialIssuer.Status.Strategies == nil {
|
|
|
|
t.Log("Did not find any CredentialIssuer with any strategies")
|
|
|
|
return false, nil // didn't find it, but keep trying
|
|
|
|
}
|
|
|
|
for _, strategy := range credentialIssuer.Status.Strategies {
|
|
|
|
// There will be other strategy types in the list, so ignore those.
|
|
|
|
if strategy.Type == v1alpha1.ImpersonationProxyStrategyType &&
|
|
|
|
strategy.Status == v1alpha1.ErrorStrategyStatus &&
|
|
|
|
strategy.Reason == v1alpha1.DisabledStrategyReason { //nolint:nestif
|
|
|
|
return true, nil // found it, continue the test!
|
|
|
|
} else if strategy.Type == v1alpha1.ImpersonationProxyStrategyType {
|
|
|
|
t.Logf("Waiting for disabled impersonation proxy strategy on %s: found status %s with reason %s and message: %s",
|
|
|
|
credentialIssuerName(env), strategy.Status, strategy.Reason, strategy.Message)
|
|
|
|
if strategy.Reason == v1alpha1.ErrorDuringSetupStrategyReason {
|
|
|
|
// The server encountered an unexpected error while stopping the impersonator, so fail the test fast.
|
|
|
|
return false, fmt.Errorf("found impersonation strategy in %s state with message: %s", strategy.Reason, strategy.Message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
t.Log("Did not find any impersonation proxy strategy on CredentialIssuer")
|
|
|
|
return false, nil // didn't find it, but keep trying
|
|
|
|
}, 1*time.Minute, 500*time.Millisecond)
|
2021-02-12 01:22:47 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 17:31:24 +00:00
|
|
|
func configMapForConfig(t *testing.T, env *library.TestEnv, config impersonator.Config) corev1.ConfigMap {
|
2021-03-03 20:53:23 +00:00
|
|
|
t.Helper()
|
2021-02-12 01:22:47 +00:00
|
|
|
configString, err := yaml.Marshal(config)
|
|
|
|
require.NoError(t, err)
|
|
|
|
configMap := corev1.ConfigMap{
|
2021-02-18 23:58:27 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
2021-03-02 17:31:24 +00:00
|
|
|
Name: impersonationProxyConfigMapName(env),
|
2021-02-18 23:58:27 +00:00
|
|
|
},
|
2021-02-12 01:22:47 +00:00
|
|
|
Data: map[string]string{
|
|
|
|
"config.yaml": string(configString),
|
|
|
|
}}
|
|
|
|
return configMap
|
|
|
|
}
|
|
|
|
|
2021-03-02 17:31:24 +00:00
|
|
|
func hasImpersonationProxyLoadBalancerService(ctx context.Context, env *library.TestEnv, client kubernetes.Interface) (bool, error) {
|
|
|
|
service, err := client.CoreV1().Services(env.ConciergeNamespace).Get(ctx, impersonationProxyLoadBalancerName(env), metav1.GetOptions{})
|
2021-02-25 22:40:02 +00:00
|
|
|
if k8serrors.IsNotFound(err) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
return service.Spec.Type == corev1.ServiceTypeLoadBalancer, nil
|
|
|
|
}
|
2021-02-12 01:22:47 +00:00
|
|
|
|
2021-03-02 17:31:24 +00:00
|
|
|
func impersonationProxyConfigMapName(env *library.TestEnv) string {
|
|
|
|
return env.ConciergeAppName + "-impersonation-proxy-config"
|
|
|
|
}
|
|
|
|
|
|
|
|
func impersonationProxyTLSSecretName(env *library.TestEnv) string {
|
|
|
|
return env.ConciergeAppName + "-impersonation-proxy-tls-serving-certificate"
|
|
|
|
}
|
|
|
|
|
|
|
|
func impersonationProxyCASecretName(env *library.TestEnv) string {
|
|
|
|
return env.ConciergeAppName + "-impersonation-proxy-ca-certificate"
|
|
|
|
}
|
|
|
|
|
|
|
|
func impersonationProxyLoadBalancerName(env *library.TestEnv) string {
|
|
|
|
return env.ConciergeAppName + "-impersonation-proxy-load-balancer"
|
|
|
|
}
|
2021-03-03 20:53:23 +00:00
|
|
|
|
|
|
|
func credentialIssuerName(env *library.TestEnv) string {
|
|
|
|
return env.ConciergeAppName + "-config"
|
|
|
|
}
|
2021-03-10 00:58:44 +00:00
|
|
|
|
2021-03-10 18:30:06 +00:00
|
|
|
// watchJSON defines the expected JSON wire equivalent of watch.Event.
|
2021-03-10 00:58:44 +00:00
|
|
|
type watchJSON struct {
|
|
|
|
Type watch.EventType `json:"type,omitempty"`
|
|
|
|
Object json.RawMessage `json:"object,omitempty"`
|
|
|
|
}
|