3efa7bdcc2
This fixes some rare test flakes caused by a data race inherent in the way we use `assert.Eventually()` with extra variables for followup assertions. This function is tricky to use correctly because it runs the passed function in a separate goroutine, and you have no guarantee that any shared variables are in a coherent state when the `assert.Eventually()` call returns. Even if you add manual mutexes, it's tricky to get the semantics right. This has been a recurring pain point and the cause of several test flakes. This change introduces a new `library.RequireEventually()` that works by internally constructing a per-loop `*require.Assertions` and running everything on a single goroutine (using `wait.PollImmediate()`). This makes it very easy to write eventual assertions. Signed-off-by: Matt Moyer <moyerm@vmware.com>
170 lines
5.9 KiB
Go
170 lines
5.9 KiB
Go
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/square/go-jose.v2"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
|
"go.pinniped.dev/test/library"
|
|
)
|
|
|
|
func TestSupervisorSecrets(t *testing.T) {
|
|
env := library.IntegrationEnv(t)
|
|
kubeClient := library.NewKubernetesClientset(t)
|
|
supervisorClient := library.NewSupervisorClientset(t)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
|
|
// Create our FederationDomain under test.
|
|
federationDomain := library.CreateTestFederationDomain(ctx, t, "", "", "")
|
|
|
|
tests := []struct {
|
|
name string
|
|
secretName func(federationDomain *configv1alpha1.FederationDomain) string
|
|
ensureValid func(t *testing.T, secret *corev1.Secret)
|
|
}{
|
|
{
|
|
name: "csrf cookie signing key",
|
|
secretName: func(federationDomain *configv1alpha1.FederationDomain) string {
|
|
return env.SupervisorAppName + "-key"
|
|
},
|
|
ensureValid: ensureValidSymmetricSecretOfTypeFunc("secrets.pinniped.dev/supervisor-csrf-signing-key"),
|
|
},
|
|
{
|
|
name: "jwks",
|
|
secretName: func(federationDomain *configv1alpha1.FederationDomain) string {
|
|
return federationDomain.Status.Secrets.JWKS.Name
|
|
},
|
|
ensureValid: ensureValidJWKS,
|
|
},
|
|
{
|
|
name: "hmac signing secret",
|
|
secretName: func(federationDomain *configv1alpha1.FederationDomain) string {
|
|
return federationDomain.Status.Secrets.TokenSigningKey.Name
|
|
},
|
|
ensureValid: ensureValidSymmetricSecretOfTypeFunc("secrets.pinniped.dev/federation-domain-token-signing-key"),
|
|
},
|
|
{
|
|
name: "state signature secret",
|
|
secretName: func(federationDomain *configv1alpha1.FederationDomain) string {
|
|
return federationDomain.Status.Secrets.StateSigningKey.Name
|
|
},
|
|
ensureValid: ensureValidSymmetricSecretOfTypeFunc("secrets.pinniped.dev/federation-domain-state-signing-key"),
|
|
},
|
|
{
|
|
name: "state encryption secret",
|
|
secretName: func(federationDomain *configv1alpha1.FederationDomain) string {
|
|
return federationDomain.Status.Secrets.StateEncryptionKey.Name
|
|
},
|
|
ensureValid: ensureValidSymmetricSecretOfTypeFunc("secrets.pinniped.dev/federation-domain-state-encryption-key"),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
// Ensure a secret is created with the FederationDomain's JWKS.
|
|
var updatedFederationDomain *configv1alpha1.FederationDomain
|
|
library.RequireEventually(t, func(requireEventually *require.Assertions) {
|
|
resp, err := supervisorClient.
|
|
ConfigV1alpha1().
|
|
FederationDomains(env.SupervisorNamespace).
|
|
Get(ctx, federationDomain.Name, metav1.GetOptions{})
|
|
requireEventually.NoError(err)
|
|
requireEventually.NotEmpty(test.secretName(resp))
|
|
updatedFederationDomain = resp
|
|
}, time.Second*10, time.Millisecond*500)
|
|
|
|
// Ensure the secret actually exists.
|
|
secret, err := kubeClient.
|
|
CoreV1().
|
|
Secrets(env.SupervisorNamespace).
|
|
Get(ctx, test.secretName(updatedFederationDomain), metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
// Ensure that the secret was labelled.
|
|
for k, v := range env.SupervisorCustomLabels {
|
|
require.Equalf(t, v, secret.Labels[k], "expected secret to have label `%s: %s`", k, v)
|
|
}
|
|
require.Equal(t, env.SupervisorAppName, secret.Labels["app"])
|
|
|
|
// Ensure that the secret is valid.
|
|
test.ensureValid(t, secret)
|
|
|
|
// Ensure upon deleting the secret, it is eventually brought back.
|
|
err = kubeClient.
|
|
CoreV1().
|
|
Secrets(env.SupervisorNamespace).
|
|
Delete(ctx, test.secretName(updatedFederationDomain), metav1.DeleteOptions{})
|
|
require.NoError(t, err)
|
|
library.RequireEventually(t, func(requireEventually *require.Assertions) {
|
|
var err error
|
|
secret, err = kubeClient.
|
|
CoreV1().
|
|
Secrets(env.SupervisorNamespace).
|
|
Get(ctx, test.secretName(updatedFederationDomain), metav1.GetOptions{})
|
|
requireEventually.NoError(err)
|
|
}, time.Second*10, time.Millisecond*500)
|
|
|
|
// Ensure that the new secret is valid.
|
|
test.ensureValid(t, secret)
|
|
})
|
|
}
|
|
|
|
// Upon deleting the FederationDomain, the secret is deleted (we test this behavior in our uninstall tests).
|
|
}
|
|
|
|
func ensureValidJWKS(t *testing.T, secret *corev1.Secret) {
|
|
t.Helper()
|
|
|
|
// Ensure the secret has the right type.
|
|
require.Equal(t, corev1.SecretType("secrets.pinniped.dev/federation-domain-jwks"), secret.Type)
|
|
|
|
// Ensure the secret has an active key.
|
|
jwkData, ok := secret.Data["activeJWK"]
|
|
require.True(t, ok, "secret is missing active jwk")
|
|
|
|
// Ensure the secret's active key is valid.
|
|
var activeJWK jose.JSONWebKey
|
|
require.NoError(t, json.Unmarshal(jwkData, &activeJWK))
|
|
require.True(t, activeJWK.Valid(), "active jwk is invalid")
|
|
require.False(t, activeJWK.IsPublic(), "active jwk is public")
|
|
|
|
// Ensure the secret has a JWKS.
|
|
jwksData, ok := secret.Data["jwks"]
|
|
require.True(t, ok, "secret is missing jwks")
|
|
|
|
// Ensure the secret's JWKS is valid, public, and contains the singing key.
|
|
var jwks jose.JSONWebKeySet
|
|
require.NoError(t, json.Unmarshal(jwksData, &jwks))
|
|
foundActiveJWK := false
|
|
for _, jwk := range jwks.Keys {
|
|
require.Truef(t, jwk.Valid(), "jwk %s is invalid", jwk.KeyID)
|
|
require.Truef(t, jwk.IsPublic(), "jwk %s is not public", jwk.KeyID)
|
|
if jwk.KeyID == activeJWK.KeyID {
|
|
foundActiveJWK = true
|
|
}
|
|
}
|
|
require.True(t, foundActiveJWK, "could not find active JWK in JWKS: %s", jwks)
|
|
}
|
|
|
|
func ensureValidSymmetricSecretOfTypeFunc(secretTypeValue string) func(*testing.T, *corev1.Secret) {
|
|
return func(t *testing.T, secret *corev1.Secret) {
|
|
t.Helper()
|
|
require.Equal(t, corev1.SecretType(secretTypeValue), secret.Type)
|
|
key, ok := secret.Data["key"]
|
|
require.Truef(t, ok, "secret data does not contain 'key': %s", secret.Data)
|
|
require.Equal(t, 32, len(key))
|
|
}
|
|
}
|