58237d0e7d
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>
363 lines
14 KiB
Go
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{}))
|
|
}
|