Merge branch 'main' into initial_ldap

This commit is contained in:
Ryan Richard
2021-05-11 11:09:37 -07:00
37 changed files with 1949 additions and 231 deletions

View File

@@ -208,6 +208,8 @@ func TestCLILoginOIDC(t *testing.T) {
require.NoErrorf(t, json.Unmarshal(cmd2Output, &credOutput2),
"command returned something other than an ExecCredential:\n%s", string(cmd2Output))
require.Equal(t, credOutput, credOutput2)
// the logs contain only the ExecCredential. There are 2 elements because the last one is "".
require.Len(t, strings.Split(string(cmd2Output), "\n"), 2)
// Overwrite the cache entry to remove the access and ID tokens.
t.Logf("overwriting cache to remove valid ID token")
@@ -237,6 +239,26 @@ func TestCLILoginOIDC(t *testing.T) {
require.NoErrorf(t, json.Unmarshal(cmd3Output, &credOutput3),
"command returned something other than an ExecCredential:\n%s", string(cmd2Output))
require.NotEqual(t, credOutput2.Status.Token, credOutput3.Status.Token)
// the logs contain only the ExecCredential. There are 2 elements because the last one is "".
require.Len(t, strings.Split(string(cmd3Output), "\n"), 2)
t.Logf("starting fourth CLI subprocess to test debug logging")
err = os.Setenv("PINNIPED_DEBUG", "true")
require.NoError(t, err)
command := oidcLoginCommand(ctx, t, pinnipedExe, sessionCachePath)
cmd4CombinedOutput, err := command.CombinedOutput()
cmd4StringOutput := string(cmd4CombinedOutput)
require.NoError(t, err, cmd4StringOutput)
// the logs contain only the 4 debug lines plus the ExecCredential. There are 6 elements because the last one is "".
require.Len(t, strings.Split(cmd4StringOutput, "\n"), 6)
require.Contains(t, cmd4StringOutput, "Performing OIDC login")
require.Contains(t, cmd4StringOutput, "Found unexpired cached token")
require.Contains(t, cmd4StringOutput, "No concierge configured, skipping token credential exchange")
require.Contains(t, cmd4StringOutput, "caching cluster credential for future use.")
require.Contains(t, cmd4StringOutput, credOutput3.Status.Token)
err = os.Unsetenv("PINNIPED_DEBUG")
require.NoError(t, err)
}
func runPinnipedLoginOIDC(
@@ -271,6 +293,7 @@ func runPinnipedLoginOIDC(
// Start a background goroutine to read stderr from the CLI and parse out the login URL.
loginURLChan := make(chan string)
spawnTestGoroutine(t, func() (err error) {
t.Helper()
defer func() {
closeErr := stderr.Close()
if closeErr == nil || errors.Is(closeErr, os.ErrClosed) {
@@ -282,16 +305,18 @@ func runPinnipedLoginOIDC(
}()
reader := bufio.NewReader(library.NewLoggerReader(t, "stderr", stderr))
line, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("could not read login URL line from stderr: %w", err)
}
scanner := bufio.NewScanner(reader)
const prompt = "Please log in: "
if !strings.HasPrefix(line, prompt) {
return fmt.Errorf("expected %q to have prefix %q", line, prompt)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, prompt) {
loginURLChan <- strings.TrimPrefix(line, prompt)
return nil
}
}
loginURLChan <- strings.TrimPrefix(line, prompt)
return readAndExpectEmpty(reader)
return fmt.Errorf("expected stderr to contain %s", prompt)
})
// Start a background goroutine to read stdout from the CLI and parse out an ExecCredential.

View File

@@ -6,7 +6,11 @@ package integration
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
@@ -28,6 +32,8 @@ import (
"golang.org/x/net/http2"
authenticationv1 "k8s.io/api/authentication/v1"
authorizationv1 "k8s.io/api/authorization/v1"
certificatesv1 "k8s.io/api/certificates/v1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
@@ -42,6 +48,8 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
"k8s.io/client-go/util/cert"
"k8s.io/client-go/util/certificate/csr"
"k8s.io/client-go/util/keyutil"
"sigs.k8s.io/yaml"
@@ -607,16 +615,26 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
require.NoError(t, err)
expectedExtra := make(map[string]authenticationv1.ExtraValue, len(whoAmIAdmin.Status.KubernetesUserInfo.User.Extra))
for k, v := range whoAmIAdmin.Status.KubernetesUserInfo.User.Extra {
// The WhoAmI API is lossy:
// - It drops UID
// - It lowercases all extra keys
// the admin user on EKS has both a UID set and an extra key with uppercase characters
// Thus we fallback to the CSR API to grab the UID and Extra to handle this scenario
uid, extra := getUIDAndExtraViaCSR(ctx, t, whoAmIAdmin.Status.KubernetesUserInfo.User.UID,
newImpersonationProxyClientWithCredentials(t,
clusterAdminCredentials, impersonationProxyURL, impersonationProxyCACertPEM, nil).
Kubernetes,
)
expectedExtra := make(map[string]authenticationv1.ExtraValue, len(extra))
for k, v := range extra {
expectedExtra[k] = authenticationv1.ExtraValue(v)
}
expectedOriginalUserInfo := authenticationv1.UserInfo{
Username: whoAmIAdmin.Status.KubernetesUserInfo.User.Username,
// The WhoAmI API is lossy so this will fail when the admin user actually does have a UID
UID: whoAmIAdmin.Status.KubernetesUserInfo.User.UID,
Groups: whoAmIAdmin.Status.KubernetesUserInfo.User.Groups,
Extra: expectedExtra,
UID: uid,
Groups: whoAmIAdmin.Status.KubernetesUserInfo.User.Groups,
Extra: expectedExtra,
}
expectedOriginalUserInfoJSON, err := json.Marshal(expectedOriginalUserInfo)
require.NoError(t, err)
@@ -780,32 +798,148 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
whoAmI,
)
// Test using a service account token. Authenticating as Service Accounts through the impersonation
// proxy is not supported, so it should fail.
// Test using a service account token.
namespaceName := createTestNamespace(t, adminClient)
_, saToken, _ := createServiceAccountToken(ctx, t, adminClient, namespaceName)
saName, saToken, _ := createServiceAccountToken(ctx, t, adminClient, namespaceName)
impersonationProxyServiceAccountPinnipedConciergeClient := newImpersonationProxyClientWithCredentials(t,
&loginv1alpha1.ClusterCredential{Token: saToken},
impersonationProxyURL, impersonationProxyCACertPEM, nil).PinnipedConcierge
_, err = impersonationProxyServiceAccountPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
whoAmI, err = impersonationProxyServiceAccountPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
require.EqualError(t, err, "Internal error occurred: unimplemented functionality - unable to act as current user")
require.True(t, k8serrors.IsInternalError(err), err)
require.Equal(t, &k8serrors.StatusError{
ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusInternalServerError,
Reason: metav1.StatusReasonInternalError,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
{
Message: "unimplemented functionality - unable to act as current user",
},
require.NoError(t, err)
require.Equal(t,
expectedWhoAmIRequestResponse(
serviceaccount.MakeUsername(namespaceName, saName),
[]string{"system:serviceaccounts", "system:serviceaccounts:" + namespaceName, "system:authenticated"},
nil,
),
whoAmI,
)
})
t.Run("WhoAmIRequests and SA token request", func(t *testing.T) {
namespaceName := createTestNamespace(t, adminClient)
kubeClient := adminClient.CoreV1()
saName, _, saUID := createServiceAccountToken(ctx, t, adminClient, namespaceName)
_, tokenRequestProbeErr := kubeClient.ServiceAccounts(namespaceName).CreateToken(ctx, saName, &authenticationv1.TokenRequest{}, metav1.CreateOptions{})
if k8serrors.IsNotFound(tokenRequestProbeErr) && tokenRequestProbeErr.Error() == "the server could not find the requested resource" {
return // stop test early since the token request API is not enabled on this cluster - other errors are caught below
}
pod, err := kubeClient.Pods(namespaceName).Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-impersonation-proxy-",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "ignored-but-required",
Image: "does-not-matter",
},
},
Message: "Internal error occurred: unimplemented functionality - unable to act as current user",
ServiceAccountName: saName,
},
}, err)
}, metav1.CreateOptions{})
require.NoError(t, err)
tokenRequestBadAudience, err := kubeClient.ServiceAccounts(namespaceName).CreateToken(ctx, saName, &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
Audiences: []string{"should-fail-because-wrong-audience"}, // anything that is not an API server audience
BoundObjectRef: &authenticationv1.BoundObjectReference{
Kind: "Pod",
APIVersion: "",
Name: pod.Name,
UID: pod.UID,
},
},
}, metav1.CreateOptions{})
require.NoError(t, err)
impersonationProxySABadAudPinnipedConciergeClient := newImpersonationProxyClientWithCredentials(t,
&loginv1alpha1.ClusterCredential{Token: tokenRequestBadAudience.Status.Token},
impersonationProxyURL, impersonationProxyCACertPEM, nil).PinnipedConcierge
_, badAudErr := impersonationProxySABadAudPinnipedConciergeClient.IdentityV1alpha1().WhoAmIRequests().
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
require.True(t, k8serrors.IsUnauthorized(badAudErr), library.Sdump(badAudErr))
tokenRequest, err := kubeClient.ServiceAccounts(namespaceName).CreateToken(ctx, saName, &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
Audiences: []string{},
BoundObjectRef: &authenticationv1.BoundObjectReference{
Kind: "Pod",
APIVersion: "",
Name: pod.Name,
UID: pod.UID,
},
},
}, metav1.CreateOptions{})
require.NoError(t, err)
impersonationProxySAClient := newImpersonationProxyClientWithCredentials(t,
&loginv1alpha1.ClusterCredential{Token: tokenRequest.Status.Token},
impersonationProxyURL, impersonationProxyCACertPEM, nil)
whoAmITokenReq, err := impersonationProxySAClient.PinnipedConcierge.IdentityV1alpha1().WhoAmIRequests().
Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
require.NoError(t, err)
// new service account tokens include the pod info in the extra fields
require.Equal(t,
expectedWhoAmIRequestResponse(
serviceaccount.MakeUsername(namespaceName, saName),
[]string{"system:serviceaccounts", "system:serviceaccounts:" + namespaceName, "system:authenticated"},
map[string]identityv1alpha1.ExtraValue{
"authentication.kubernetes.io/pod-name": {pod.Name},
"authentication.kubernetes.io/pod-uid": {string(pod.UID)},
},
),
whoAmITokenReq,
)
// allow the test SA to create CSRs
library.CreateTestClusterRoleBinding(t,
rbacv1.Subject{Kind: rbacv1.ServiceAccountKind, Name: saName, Namespace: namespaceName},
rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "system:node-bootstrapper"},
)
library.WaitForUserToHaveAccess(t, serviceaccount.MakeUsername(namespaceName, saName), []string{}, &authorizationv1.ResourceAttributes{
Verb: "create", Group: certificatesv1.GroupName, Version: "*", Resource: "certificatesigningrequests",
})
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
csrPEM, err := cert.MakeCSR(privateKey, &pkix.Name{
CommonName: "panda-man",
Organization: []string{"living-the-dream", "need-more-sleep"},
}, nil, nil)
require.NoError(t, err)
csrName, _, err := csr.RequestCertificate(
impersonationProxySAClient.Kubernetes,
csrPEM,
"",
certificatesv1.KubeAPIServerClientSignerName,
[]certificatesv1.KeyUsage{certificatesv1.UsageClientAuth},
privateKey,
)
require.NoError(t, err)
saCSR, err := impersonationProxySAClient.Kubernetes.CertificatesV1beta1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{})
require.NoError(t, err)
err = adminClient.CertificatesV1beta1().CertificateSigningRequests().Delete(ctx, csrName, metav1.DeleteOptions{})
require.NoError(t, err)
// make sure the user info that the CSR captured matches the SA, including the UID
require.Equal(t, serviceaccount.MakeUsername(namespaceName, saName), saCSR.Spec.Username)
require.Equal(t, string(saUID), saCSR.Spec.UID)
require.Equal(t, []string{"system:serviceaccounts", "system:serviceaccounts:" + namespaceName, "system:authenticated"}, saCSR.Spec.Groups)
require.Equal(t, map[string]certificatesv1beta1.ExtraValue{
"authentication.kubernetes.io/pod-name": {pod.Name},
"authentication.kubernetes.io/pod-uid": {string(pod.UID)},
}, saCSR.Spec.Extra)
})
t.Run("kubectl as a client", func(t *testing.T) {
@@ -1581,17 +1715,18 @@ func getCredForConfig(t *testing.T, config *rest.Config) *loginv1alpha1.ClusterC
if tlsConfig != nil && tlsConfig.GetClientCertificate != nil {
cert, err := tlsConfig.GetClientCertificate(nil)
require.NoError(t, err)
require.Len(t, cert.Certificate, 1)
if len(cert.Certificate) > 0 {
require.Len(t, cert.Certificate, 1)
publicKey := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Certificate[0],
})
out.ClientCertificateData = string(publicKey)
publicKey := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Certificate[0],
})
out.ClientCertificateData = string(publicKey)
privateKey, err := keyutil.MarshalPrivateKeyToPEM(cert.PrivateKey)
require.NoError(t, err)
out.ClientKeyData = string(privateKey)
privateKey, err := keyutil.MarshalPrivateKeyToPEM(cert.PrivateKey)
require.NoError(t, err)
out.ClientKeyData = string(privateKey)
}
}
if *out == (loginv1alpha1.ClusterCredential{}) {
@@ -1600,3 +1735,39 @@ func getCredForConfig(t *testing.T, config *rest.Config) *loginv1alpha1.ClusterC
return out
}
func getUIDAndExtraViaCSR(ctx context.Context, t *testing.T, uid string, client kubernetes.Interface) (string, map[string]certificatesv1beta1.ExtraValue) {
t.Helper()
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
csrPEM, err := cert.MakeCSR(privateKey, &pkix.Name{
CommonName: "panda-man",
Organization: []string{"living-the-dream", "need-more-sleep"},
}, nil, nil)
require.NoError(t, err)
csrName, _, err := csr.RequestCertificate(
client,
csrPEM,
"",
certificatesv1.KubeAPIServerClientSignerName,
[]certificatesv1.KeyUsage{certificatesv1.UsageClientAuth},
privateKey,
)
require.NoError(t, err)
csReq, err := client.CertificatesV1beta1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{})
require.NoError(t, err)
err = client.CertificatesV1beta1().CertificateSigningRequests().Delete(ctx, csrName, metav1.DeleteOptions{})
require.NoError(t, err)
outUID := uid // in the future this may not be empty on some clusters
if len(outUID) == 0 {
outUID = csReq.Spec.UID
}
return outUID, csReq.Spec.Extra
}

View File

@@ -337,6 +337,6 @@ status:
pinnipedExe,
kubeconfigPath,
env.SupervisorUpstreamOIDC.Username,
expectedGroupsPlusUnauthenticated,
expectedGroupsPlusAuthenticated,
)
}

View File

@@ -48,10 +48,15 @@ func TestStorageGarbageCollection(t *testing.T) {
// in the same namespace just to get the controller to respond faster.
// This is just a performance optimization to make this test pass faster because otherwise
// this test has to wait ~3 minutes for the controller's next full-resync.
stopCh := make(chan bool, 1) // It is important that this channel be buffered.
go updateSecretEveryTwoSeconds(t, stopCh, secrets, secretNotYetExpired)
stopCh := make(chan struct{})
errCh := make(chan error)
go updateSecretEveryTwoSeconds(stopCh, errCh, secrets, secretNotYetExpired)
t.Cleanup(func() {
stopCh <- true
close(stopCh)
if updateErr := <-errCh; updateErr != nil {
panic(updateErr)
}
})
// Wait long enough for the next periodic sweep of the GC controller for the secrets to be deleted, which
@@ -69,10 +74,15 @@ func TestStorageGarbageCollection(t *testing.T) {
require.NoError(t, err)
}
func updateSecretEveryTwoSeconds(t *testing.T, stopCh chan bool, secrets corev1client.SecretInterface, secret *v1.Secret) {
func updateSecretEveryTwoSeconds(stopCh chan struct{}, errCh chan error, secrets corev1client.SecretInterface, secret *v1.Secret) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
var updateErr error
defer func() {
errCh <- updateErr
}()
i := 0
for {
select {
@@ -87,9 +97,25 @@ func updateSecretEveryTwoSeconds(t *testing.T, stopCh chan bool, secrets corev1c
i++
secret.Data["foo"] = []byte(fmt.Sprintf("bar-%d", i))
var updateErr error
secret, updateErr = secrets.Update(ctx, secret, metav1.UpdateOptions{})
require.NoError(t, updateErr)
switch {
case updateErr == nil:
// continue to next update
case k8serrors.IsConflict(updateErr), k8serrors.IsNotFound(updateErr):
select {
case _, ok := <-stopCh:
if !ok { // stopCh is closed meaning that test is already finished so these errors are expected
updateErr = nil
}
default:
}
return // even if the error is expected, we must stop
default:
return // unexpected error
}
}
}

View File

@@ -34,10 +34,43 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
Message: `secret "does-not-exist" not found`,
},
{
Type: "OIDCDiscoverySucceeded",
Status: v1alpha1.ConditionFalse,
Reason: "Unreachable",
Message: `failed to perform OIDC discovery against "https://127.0.0.1:444444/issuer"`,
Type: "OIDCDiscoverySucceeded",
Status: v1alpha1.ConditionFalse,
Reason: "Unreachable",
Message: `failed to perform OIDC discovery against "https://127.0.0.1:444444/issuer":
Get "https://127.0.0.1:444444/issuer/.well-known/openid-configuration": dial tcp: address 444444: in [truncated 10 chars]`,
},
})
})
t.Run("invalid issuer with trailing slash", func(t *testing.T) {
t.Parallel()
spec := v1alpha1.OIDCIdentityProviderSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer + "/",
TLS: &v1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: []string{"email", "profile"},
},
Client: v1alpha1.OIDCClient{
SecretName: library.CreateClientCredsSecret(t, "test-client-id", "test-client-secret").Name,
},
}
upstream := library.CreateTestOIDCIdentityProvider(t, spec, v1alpha1.PhaseError)
expectUpstreamConditions(t, upstream, []v1alpha1.Condition{
{
Type: "ClientCredentialsValid",
Status: v1alpha1.ConditionTrue,
Reason: "Success",
Message: "loaded client credentials",
},
{
Type: "OIDCDiscoverySucceeded",
Status: v1alpha1.ConditionFalse,
Reason: "Unreachable",
Message: `failed to perform OIDC discovery against "` + env.SupervisorUpstreamOIDC.Issuer + `/":
oidc: issuer did not match the issuer returned by provider, expected "` + env.SupervisorUpstreamOIDC.Issuer + `/" got "` + env.SupervisorUpstreamOIDC.Issuer + `"`,
},
})
})