ContainerImage.Pinniped/internal/controller/supervisorconfig/jwks_observer_test.go
Andrew Keesler 58237d0e7d
WIP: start to wire signing key into token handler
This commit includes a failing test (amongst other compiler failures) for the
dynamic signing key fetcher that we will inject into fosite. We are checking it
in so that we can pass the WIP off.

Signed-off-by: Margo Crawford <margaretc@vmware.com>
2020-12-03 15:37:25 -05:00

363 lines
14 KiB
Go

// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package supervisorconfig
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"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"
kubeinformers "k8s.io/client-go/informers"
kubernetesfake "k8s.io/client-go/kubernetes/fake"
"go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1"
pinnipedfake "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned/fake"
pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/testutil"
)
func TestJWKSObserverControllerInformerFilters(t *testing.T) {
spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) {
var (
r *require.Assertions
observableWithInformerOption *testutil.ObservableWithInformerOption
secretsInformerFilter controllerlib.Filter
oidcProviderInformerFilter controllerlib.Filter
)
it.Before(func() {
r = require.New(t)
observableWithInformerOption = testutil.NewObservableWithInformerOption()
secretsInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().Secrets()
oidcProviderInformer := pinnipedinformers.NewSharedInformerFactory(nil, 0).Config().V1alpha1().OIDCProviders()
_ = NewJWKSObserverController(
nil,
secretsInformer,
oidcProviderInformer,
observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters
)
secretsInformerFilter = observableWithInformerOption.GetFilterForInformer(secretsInformer)
oidcProviderInformerFilter = observableWithInformerOption.GetFilterForInformer(oidcProviderInformer)
})
when("watching Secret objects", func() {
var (
subject controllerlib.Filter
secret, otherSecret *corev1.Secret
)
it.Before(func() {
subject = secretsInformerFilter
secret = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "any-name", Namespace: "any-namespace"}}
otherSecret = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "any-other-name", Namespace: "any-other-namespace"}}
})
when("any Secret changes", func() {
it("returns true to trigger the sync method", func() {
r.True(subject.Add(secret))
r.True(subject.Update(secret, otherSecret))
r.True(subject.Update(otherSecret, secret))
r.True(subject.Delete(secret))
})
})
})
when("watching OIDCProvider objects", func() {
var (
subject controllerlib.Filter
provider, otherProvider *v1alpha1.OIDCProvider
)
it.Before(func() {
subject = oidcProviderInformerFilter
provider = &v1alpha1.OIDCProvider{ObjectMeta: metav1.ObjectMeta{Name: "any-name", Namespace: "any-namespace"}}
otherProvider = &v1alpha1.OIDCProvider{ObjectMeta: metav1.ObjectMeta{Name: "any-other-name", Namespace: "any-other-namespace"}}
})
when("any OIDCProvider changes", func() {
it("returns true to trigger the sync method", func() {
r.True(subject.Add(provider))
r.True(subject.Update(provider, otherProvider))
r.True(subject.Update(otherProvider, provider))
r.True(subject.Delete(provider))
})
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}
type fakeIssuerToJWKSMapSetter struct {
setIssuerToJWKSMapWasCalled bool
issuerToJWKSMapReceived map[string]*jose.JSONWebKeySet
issuerToActiveJWKMapReceived map[string]*jose.JSONWebKey
}
func (f *fakeIssuerToJWKSMapSetter) SetIssuerToJWKSMap(
issuerToJWKSMap map[string]*jose.JSONWebKeySet,
issuerToActiveJWKMap map[string]*jose.JSONWebKey,
) {
f.setIssuerToJWKSMapWasCalled = true
f.issuerToJWKSMapReceived = issuerToJWKSMap
f.issuerToActiveJWKMapReceived = issuerToActiveJWKMap
}
func TestJWKSObserverControllerSync(t *testing.T) {
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
const installedInNamespace = "some-namespace"
var (
r *require.Assertions
subject controllerlib.Controller
pinnipedInformerClient *pinnipedfake.Clientset
kubeInformerClient *kubernetesfake.Clientset
pinnipedInformers pinnipedinformers.SharedInformerFactory
kubeInformers kubeinformers.SharedInformerFactory
timeoutContext context.Context
timeoutContextCancel context.CancelFunc
syncContext *controllerlib.Context
issuerToJWKSSetter *fakeIssuerToJWKSMapSetter
)
// Defer starting the informers until the last possible moment so that the
// nested Before's can keep adding things to the informer caches.
var startInformersAndController = func() {
// Set this at the last second to allow for injection of server override.
subject = NewJWKSObserverController(
issuerToJWKSSetter,
kubeInformers.Core().V1().Secrets(),
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
controllerlib.WithInformer,
)
// Set this at the last second to support calling subject.Name().
syncContext = &controllerlib.Context{
Context: timeoutContext,
Name: subject.Name(),
Key: controllerlib.Key{
Namespace: installedInNamespace,
Name: "any-name",
},
}
// Must start informers before calling TestRunSynchronously()
kubeInformers.Start(timeoutContext.Done())
pinnipedInformers.Start(timeoutContext.Done())
controllerlib.TestRunSynchronously(t, subject)
}
it.Before(func() {
r = require.New(t)
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
kubeInformerClient = kubernetesfake.NewSimpleClientset()
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
pinnipedInformerClient = pinnipedfake.NewSimpleClientset()
pinnipedInformers = pinnipedinformers.NewSharedInformerFactory(pinnipedInformerClient, 0)
issuerToJWKSSetter = &fakeIssuerToJWKSMapSetter{}
unrelatedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some other unrelated secret",
Namespace: installedInNamespace,
},
}
r.NoError(kubeInformerClient.Tracker().Add(unrelatedSecret))
})
it.After(func() {
timeoutContextCancel()
})
when("there are no OIDCProviders and no JWKS Secrets yet", func() {
it("sets the issuerToJWKSSetter's map to be empty", func() {
startInformersAndController()
err := controllerlib.TestSync(t, subject, *syncContext)
r.NoError(err)
r.True(issuerToJWKSSetter.setIssuerToJWKSMapWasCalled)
r.Empty(issuerToJWKSSetter.issuerToJWKSMapReceived)
r.Empty(issuerToJWKSSetter.issuerToActiveJWKMapReceived)
})
})
when("there are OIDCProviders where some have corresponding JWKS Secrets and some don't", func() {
var (
expectedJWK1, expectedJWK2 string
)
it.Before(func() {
oidcProviderWithoutSecret1 := &v1alpha1.OIDCProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "no-secret-oidcprovider1",
Namespace: installedInNamespace,
},
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://no-secret-issuer1.com"},
Status: v1alpha1.OIDCProviderStatus{}, // no JWKSSecret field
}
oidcProviderWithoutSecret2 := &v1alpha1.OIDCProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "no-secret-oidcprovider2",
Namespace: installedInNamespace,
},
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://no-secret-issuer2.com"},
// no Status field
}
oidcProviderWithBadSecret := &v1alpha1.OIDCProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-secret-oidcprovider",
Namespace: installedInNamespace,
},
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-secret-issuer.com"},
Status: v1alpha1.OIDCProviderStatus{
JWKSSecret: corev1.LocalObjectReference{Name: "bad-secret-name"},
},
}
oidcProviderWithBadJWKSSecret := &v1alpha1.OIDCProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-jwks-secret-oidcprovider",
Namespace: installedInNamespace,
},
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-jwks-secret-issuer.com"},
Status: v1alpha1.OIDCProviderStatus{
JWKSSecret: corev1.LocalObjectReference{Name: "bad-jwks-secret-name"},
},
}
oidcProviderWithBadActiveJWKSecret := &v1alpha1.OIDCProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-active-jwk-secret-oidcprovider",
Namespace: installedInNamespace,
},
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://bad-active-jwk-secret-issuer.com"},
Status: v1alpha1.OIDCProviderStatus{
JWKSSecret: corev1.LocalObjectReference{Name: "bad-active-jwk-secret-name"},
},
}
oidcProviderWithGoodSecret1 := &v1alpha1.OIDCProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "good-secret-oidcprovider1",
Namespace: installedInNamespace,
},
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://issuer-with-good-secret1.com"},
Status: v1alpha1.OIDCProviderStatus{
JWKSSecret: corev1.LocalObjectReference{Name: "good-jwks-secret-name1"},
},
}
oidcProviderWithGoodSecret2 := &v1alpha1.OIDCProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "good-secret-oidcprovider2",
Namespace: installedInNamespace,
},
Spec: v1alpha1.OIDCProviderSpec{Issuer: "https://issuer-with-good-secret2.com"},
Status: v1alpha1.OIDCProviderStatus{
JWKSSecret: corev1.LocalObjectReference{Name: "good-jwks-secret-name2"},
},
}
expectedJWK1 = string(readJWKJSON(t, "testdata/public-jwk.json"))
r.NotEmpty(expectedJWK1)
expectedJWK2 = string(readJWKJSON(t, "testdata/public-jwk2.json"))
r.NotEmpty(expectedJWK2)
goodJWKSSecret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "good-jwks-secret-name1",
Namespace: installedInNamespace,
},
Data: map[string][]byte{
"activeJWK": []byte(expectedJWK1),
"jwks": []byte(`{"keys": [` + expectedJWK1 + `]}`),
},
}
goodJWKSSecret2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "good-jwks-secret-name2",
Namespace: installedInNamespace,
},
Data: map[string][]byte{
"activeJWK": []byte(expectedJWK2),
"jwks": []byte(`{"keys": [` + expectedJWK2 + `]}`),
},
}
badSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-secret-name",
Namespace: installedInNamespace,
},
Data: map[string][]byte{"junk": nil},
}
badJWKSSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-jwks-secret-name",
Namespace: installedInNamespace,
},
Data: map[string][]byte{
"activeJWK": []byte(expectedJWK2),
"jwks": []byte("bad"),
},
}
badActiveJWKSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-active-jwk-secret-name",
Namespace: installedInNamespace,
},
Data: map[string][]byte{
"activeJWK": []byte("bad"),
"jwks": []byte(`{"keys": [` + expectedJWK2 + `]}`),
},
}
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithoutSecret1))
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithoutSecret2))
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithBadSecret))
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithBadJWKSSecret))
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithBadActiveJWKSecret))
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithGoodSecret1))
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithGoodSecret2))
r.NoError(kubeInformerClient.Tracker().Add(goodJWKSSecret1))
r.NoError(kubeInformerClient.Tracker().Add(goodJWKSSecret2))
r.NoError(kubeInformerClient.Tracker().Add(badSecret))
r.NoError(kubeInformerClient.Tracker().Add(badJWKSSecret))
r.NoError(kubeInformerClient.Tracker().Add(badActiveJWKSecret))
})
requireJWKSJSON := func(expectedJWKJSON string, actualJWKS *jose.JSONWebKeySet) {
r.NotNil(actualJWKS)
r.Len(actualJWKS.Keys, 1)
actualJWK := actualJWKS.Keys[0]
actualJWKJSON, err := json.Marshal(actualJWK)
r.NoError(err)
r.JSONEq(expectedJWKJSON, string(actualJWKJSON))
}
requireJWKJSON := func(expectedJWKJSON string, actualJWK *jose.JSONWebKey) {
r.NotNil(actualJWK)
actualJWKJSON, err := json.Marshal(actualJWK)
r.NoError(err)
r.JSONEq(expectedJWKJSON, string(actualJWKJSON))
}
it("updates the issuerToJWKSSetter's map to include only the issuers that had valid JWKS", func() {
startInformersAndController()
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
r.True(issuerToJWKSSetter.setIssuerToJWKSMapWasCalled)
r.Len(issuerToJWKSSetter.issuerToJWKSMapReceived, 2)
r.Len(issuerToJWKSSetter.issuerToActiveJWKMapReceived, 2)
// the actual JWK should match the one from the test fixture that was put into the secret
requireJWKSJSON(expectedJWK1, issuerToJWKSSetter.issuerToJWKSMapReceived["https://issuer-with-good-secret1.com"])
requireJWKJSON(expectedJWK1, issuerToJWKSSetter.issuerToActiveJWKMapReceived["https://issuer-with-good-secret1.com"])
requireJWKSJSON(expectedJWK2, issuerToJWKSSetter.issuerToJWKSMapReceived["https://issuer-with-good-secret2.com"])
requireJWKJSON(expectedJWK2, issuerToJWKSSetter.issuerToActiveJWKMapReceived["https://issuer-with-good-secret2.com"])
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}