ContainerImage.Pinniped/internal/controller/supervisorconfig/jwks_observer_test.go

303 lines
11 KiB
Go
Raw Normal View History

// 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
}
func (f *fakeIssuerToJWKSMapSetter) SetIssuerToJWKSMap(issuerToJWKSMap map[string]*jose.JSONWebKeySet) {
f.setIssuerToJWKSMapWasCalled = true
f.issuerToJWKSMapReceived = issuerToJWKSMap
}
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)
})
})
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-jwks-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 + `]}`),
},
}
badJWKSSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-jwks-secret-name",
Namespace: installedInNamespace,
},
Data: map[string][]byte{"junk": nil},
}
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithoutSecret1))
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithoutSecret2))
r.NoError(pinnipedInformerClient.Tracker().Add(oidcProviderWithBadSecret))
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(badJWKSSecret))
})
requireJWKJSON := 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))
}
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)
// the actual JWK should match the one from the test fixture that was put into the secret
requireJWKJSON(expectedJWK1, issuerToJWKSSetter.issuerToJWKSMapReceived["https://issuer-with-good-secret1.com"])
requireJWKJSON(expectedJWK2, issuerToJWKSSetter.issuerToJWKSMapReceived["https://issuer-with-good-secret2.com"])
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}