ContainerImage.Pinniped/test/integration/concierge_credentialrequest...

185 lines
6.4 KiB
Go

// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"context"
"crypto/x509"
"encoding/pem"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/idp/v1alpha1"
loginv1alpha1 "go.pinniped.dev/generated/1.19/apis/login/v1alpha1"
"go.pinniped.dev/test/library"
)
func TestUnsuccessfulCredentialRequest(t *testing.T) {
library.SkipUnlessIntegration(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
response, err := makeRequest(ctx, t, validCredentialRequestSpecWithRealToken(t, corev1.TypedLocalObjectReference{
APIGroup: &idpv1alpha1.SchemeGroupVersion.Group,
Kind: "WebhookIdentityProvider",
Name: "some-webhook-that-does-not-exist",
}))
require.NoError(t, err)
require.Nil(t, response.Status.Credential)
require.NotNil(t, response.Status.Message)
require.Equal(t, "authentication failed", *response.Status.Message)
}
func TestSuccessfulCredentialRequest(t *testing.T) {
env := library.IntegrationEnv(t).WithCapability(library.ClusterSigningKeyIsAvailable)
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
defer cancel()
testWebhook := library.CreateTestWebhookIDP(ctx, t)
var response *loginv1alpha1.TokenCredentialRequest
successfulResponse := func() bool {
var err error
response, err = makeRequest(ctx, t, validCredentialRequestSpecWithRealToken(t, testWebhook))
require.NoError(t, err, "the request should never fail at the HTTP level")
return response.Status.Credential != nil
}
assert.Eventually(t, successfulResponse, 10*time.Second, 500*time.Millisecond)
require.NotNil(t, response.Status.Credential)
require.Empty(t, response.Status.Message)
require.Empty(t, response.Spec)
require.Empty(t, response.Status.Credential.Token)
require.NotEmpty(t, response.Status.Credential.ClientCertificateData)
require.Equal(t, env.TestUser.ExpectedUsername, getCommonName(t, response.Status.Credential.ClientCertificateData))
require.ElementsMatch(t, env.TestUser.ExpectedGroups, getOrganizations(t, response.Status.Credential.ClientCertificateData))
require.NotEmpty(t, response.Status.Credential.ClientKeyData)
require.NotNil(t, response.Status.Credential.ExpirationTimestamp)
require.InDelta(t, time.Until(response.Status.Credential.ExpirationTimestamp.Time), 1*time.Hour, float64(3*time.Minute))
// Create a client using the admin kubeconfig.
adminClient := library.NewClientset(t)
// Create a client using the certificate from the CredentialRequest.
clientWithCertFromCredentialRequest := library.NewClientsetWithCertAndKey(
t,
response.Status.Credential.ClientCertificateData,
response.Status.Credential.ClientKeyData,
)
t.Run(
"access as user",
library.AccessAsUserTest(ctx, adminClient, env.TestUser.ExpectedUsername, clientWithCertFromCredentialRequest),
)
for _, group := range env.TestUser.ExpectedGroups {
group := group
t.Run(
"access as group "+group,
library.AccessAsGroupTest(ctx, adminClient, group, clientWithCertFromCredentialRequest),
)
}
}
func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthenticateTheUser(t *testing.T) {
library.IntegrationEnv(t).WithCapability(library.ClusterSigningKeyIsAvailable)
response, err := makeRequest(context.Background(), t, loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token"})
require.NoError(t, err)
require.Empty(t, response.Spec)
require.Nil(t, response.Status.Credential)
require.Equal(t, stringPtr("authentication failed"), response.Status.Message)
}
func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken(t *testing.T) {
library.IntegrationEnv(t).WithCapability(library.ClusterSigningKeyIsAvailable)
response, err := makeRequest(context.Background(), t, loginv1alpha1.TokenCredentialRequestSpec{Token: ""})
require.Error(t, err)
statusError, isStatus := err.(*errors.StatusError)
require.True(t, isStatus)
require.Equal(t, 1, len(statusError.ErrStatus.Details.Causes))
cause := statusError.ErrStatus.Details.Causes[0]
require.Equal(t, metav1.CauseType("FieldValueRequired"), cause.Type)
require.Equal(t, "Required value: token must be supplied", cause.Message)
require.Equal(t, "spec.token.value", cause.Field)
require.Empty(t, response.Spec)
require.Nil(t, response.Status.Credential)
}
func TestCredentialRequest_OtherwiseValidRequestWithRealTokenShouldFailWhenTheClusterIsNotCapable(t *testing.T) {
library.IntegrationEnv(t).WithoutCapability(library.ClusterSigningKeyIsAvailable)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
testWebhook := library.CreateTestWebhookIDP(ctx, t)
response, err := makeRequest(ctx, t, validCredentialRequestSpecWithRealToken(t, testWebhook))
require.NoError(t, err)
require.Empty(t, response.Spec)
require.Nil(t, response.Status.Credential)
require.Equal(t, stringPtr("authentication failed"), response.Status.Message)
}
func makeRequest(ctx context.Context, t *testing.T, spec loginv1alpha1.TokenCredentialRequestSpec) (*loginv1alpha1.TokenCredentialRequest, error) {
t.Helper()
env := library.IntegrationEnv(t)
client := library.NewAnonymousPinnipedClientset(t)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
return client.LoginV1alpha1().TokenCredentialRequests(env.Namespace).Create(ctx, &loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{Namespace: env.Namespace},
Spec: spec,
}, metav1.CreateOptions{})
}
func validCredentialRequestSpecWithRealToken(t *testing.T, idp corev1.TypedLocalObjectReference) loginv1alpha1.TokenCredentialRequestSpec {
return loginv1alpha1.TokenCredentialRequestSpec{
Token: library.IntegrationEnv(t).TestUser.Token,
IdentityProvider: idp,
}
}
func stringPtr(s string) *string {
return &s
}
func getCommonName(t *testing.T, certPEM string) string {
t.Helper()
pemBlock, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(pemBlock.Bytes)
require.NoError(t, err)
return cert.Subject.CommonName
}
func getOrganizations(t *testing.T, certPEM string) []string {
t.Helper()
pemBlock, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(pemBlock.Bytes)
require.NoError(t, err)
return cert.Subject.Organization
}