ContainerImage.Pinniped/test/integration/concierge_credentialrequest_test.go
Monis Khan cd686ffdf3
Force the use of secure TLS config
This change updates the TLS config used by all pinniped components.
There are no configuration knobs associated with this change.  Thus
this change tightens our static defaults.

There are four TLS config levels:

1. Secure (TLS 1.3 only)
2. Default (TLS 1.2+ best ciphers that are well supported)
3. Default LDAP (TLS 1.2+ with less good ciphers)
4. Legacy (currently unused, TLS 1.2+ with all non-broken ciphers)

Highlights per component:

1. pinniped CLI
   - uses "secure" config against KAS
   - uses "default" for all other connections
2. concierge
   - uses "secure" config as an aggregated API server
   - uses "default" config as a impersonation proxy API server
   - uses "secure" config against KAS
   - uses "default" config for JWT authenticater (mostly, see code)
   - no changes to webhook authenticater (see code)
3. supervisor
   - uses "default" config as a server
   - uses "secure" config against KAS
   - uses "default" config against OIDC IDPs
   - uses "default LDAP" config against LDAP IDPs

Signed-off-by: Monis Khan <mok@vmware.com>
2021-11-17 16:55:35 -05:00

227 lines
8.3 KiB
Go

// Copyright 2020-2021 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/require"
jwtpkg "gopkg.in/square/go-jose.v2/jwt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
"go.pinniped.dev/test/testlib"
)
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
func TestUnsuccessfulCredentialRequest_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
response, err := testlib.CreateTokenCredentialRequest(ctx, t,
loginv1alpha1.TokenCredentialRequestSpec{
Token: env.TestUser.Token,
Authenticator: corev1.TypedLocalObjectReference{
APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
Kind: "WebhookAuthenticator",
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)
}
// TestSuccessfulCredentialRequest cannot run in parallel because runPinnipedLoginOIDC uses a fixed port
// for its localhost listener via --listen-port=env.CLIUpstreamOIDC.CallbackURL.Port() per oidcLoginCommand.
// Since ports are global to the process, tests using oidcLoginCommand must be run serially.
func TestSuccessfulCredentialRequest(t *testing.T) {
env := testlib.IntegrationEnv(t).WithCapability(testlib.ClusterSigningKeyIsAvailable)
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
defer cancel()
tests := []struct {
name string
authenticator func(context.Context, *testing.T) corev1.TypedLocalObjectReference
token func(t *testing.T) (token string, username string, groups []string)
}{
{
name: "webhook",
authenticator: testlib.CreateTestWebhookAuthenticator,
token: func(t *testing.T) (string, string, []string) {
return testlib.IntegrationEnv(t).TestUser.Token, env.TestUser.ExpectedUsername, env.TestUser.ExpectedGroups
},
},
{
name: "jwt authenticator",
authenticator: testlib.CreateTestJWTAuthenticatorForCLIUpstream,
token: func(t *testing.T) (string, string, []string) {
pinnipedExe := testlib.PinnipedCLIPath(t)
credOutput, _ := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
token := credOutput.Status.Token
// By default, the JWTAuthenticator expects the username to be in the "username" claim and the
// groups to be in the "groups" claim.
// However, we are configuring Pinniped in the `CreateTestJWTAuthenticatorForCLIUpstream` method above
// to read the username from the "sub" claim of the token instead.
username, groups := getJWTSubAndGroupsClaims(t, token)
return token, username, groups
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
authenticator := test.authenticator(ctx, t)
token, username, groups := test.token(t)
var response *loginv1alpha1.TokenCredentialRequest
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
var err error
response, err = testlib.CreateTokenCredentialRequest(ctx, t,
loginv1alpha1.TokenCredentialRequestSpec{Token: token, Authenticator: authenticator},
)
requireEventually.NoError(err, "the request should never fail at the HTTP level")
requireEventually.NotNil(response)
requireEventually.NotNil(response.Status.Credential, "the response should contain a credential")
requireEventually.Emptyf(response.Status.Message, "value is: %q", safeDerefStringPtr(response.Status.Message))
requireEventually.NotNil(response.Status.Credential)
requireEventually.Empty(response.Spec)
requireEventually.Empty(response.Status.Credential.Token)
requireEventually.NotEmpty(response.Status.Credential.ClientCertificateData)
requireEventually.Equal(username, getCommonName(t, response.Status.Credential.ClientCertificateData))
requireEventually.ElementsMatch(groups, getOrganizations(t, response.Status.Credential.ClientCertificateData))
requireEventually.NotEmpty(response.Status.Credential.ClientKeyData)
requireEventually.NotNil(response.Status.Credential.ExpirationTimestamp)
requireEventually.InDelta(5*time.Minute, time.Until(response.Status.Credential.ExpirationTimestamp.Time), float64(time.Minute))
}, 10*time.Second, 500*time.Millisecond)
// Create a client using the certificate from the CredentialRequest.
clientWithCertFromCredentialRequest := testlib.NewClientsetWithCertAndKey(
t,
response.Status.Credential.ClientCertificateData,
response.Status.Credential.ClientKeyData,
)
t.Run(
"access as user",
testlib.AccessAsUserTest(ctx, username, clientWithCertFromCredentialRequest),
)
for _, group := range groups {
group := group
t.Run(
"access as group "+group,
testlib.AccessAsGroupTest(ctx, group, clientWithCertFromCredentialRequest),
)
}
})
}
}
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthenticateTheUser_Parallel(t *testing.T) {
_ = testlib.IntegrationEnv(t).WithCapability(testlib.ClusterSigningKeyIsAvailable)
// Create a testWebhook so we have a legitimate authenticator to pass to the
// TokenCredentialRequest API.
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t)
response, err := testlib.CreateTokenCredentialRequest(context.Background(), t,
loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token", Authenticator: testWebhook},
)
require.NoError(t, err)
require.Empty(t, response.Spec)
require.Nil(t, response.Status.Credential)
require.Equal(t, pointer.StringPtr("authentication failed"), response.Status.Message)
}
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken_Parallel(t *testing.T) {
_ = testlib.IntegrationEnv(t).WithCapability(testlib.ClusterSigningKeyIsAvailable)
// Create a testWebhook so we have a legitimate authenticator to pass to the
// TokenCredentialRequest API.
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t)
response, err := testlib.CreateTokenCredentialRequest(context.Background(), t,
loginv1alpha1.TokenCredentialRequestSpec{Token: "", Authenticator: testWebhook},
)
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 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
}
func safeDerefStringPtr(s *string) string {
if s == nil {
return "<nil>"
}
return *s
}
func getJWTSubAndGroupsClaims(t *testing.T, jwt string) (string, []string) {
t.Helper()
token, err := jwtpkg.ParseSigned(jwt)
require.NoError(t, err)
var claims struct {
Sub string `json:"sub"`
Groups []string `json:"groups"`
}
err = token.UnsafeClaimsWithoutVerification(&claims)
require.NoError(t, err)
return claims.Sub, claims.Groups
}